42 Commits

Author SHA1 Message Date
Ranieri 75d729ce40 Feature/iop emulator (#244)
* refactor: from guest  threads to EE scheduler

* feat: bad wip mpeg fix for code veronica

* feat: cheap copy from host
feat: small perf o vsync tick

* feat: added EE clock Hz
fix: fix MPEG out of sync with new EE refactor

* fix: fix lotr tests

* fix: fix cri dtx loading
fix: fix wrong mmi instruction translation
fix: fix thread info params
feat: added EE  timers decoder and consumer
feat: split SFI and IOP memory to prevent collision and overrides

* feat: revert wrong changes

* refactor: change GS architecture

* feat: IOP emulator
refactor: codegen to catch callbacks on mips code
feat: added a lot of entries or IOP emulator

* feat: analyzer resolve the complete constant-producing sequence with five-instruction backward scan stopped at LUI and therefore

* feat: remove recompiled version of GetRomName
refactor: split IOP emulator logic
feat: added more HLE IOP modules
feat: added ps2_path

* eat: enhance ELF parser with improved callable entry detection and control flow analysis

* feat: update memory hint handling and enhance entry point discovery logic

* feat: add SET_GPR_ZE32 macro for zero-extending loads with unsigned semantics

* refactor: Refactor PS2 IOP Host Adapter and Memory Management
feat: Added PS2Vfs for virtual file system operations, including file opening, reading, writing, and path resolution.
feat: Improve VIF1 data processing to handle GIF image packets more efficiently.

* feat: added a lot of tests

* fix: fix texture caching
feat: wip multi version on dbcman

* feat: remove LLE IOPs
2026-09-19 21:31:44 -03:00
Sinan 14b1e5cb39 Start the main thread with COP0 Status.IE set (#214)
Guest code reads COP0 Status to decide whether interrupts are enabled,
and two different bits are involved:

  IE  (bit 0)   the architectural MIPS interrupt enable, set once by the
                kernel during boot and normally left set.
  EIE (bit 16)  the EE-specific enable that `ei` and `di` toggle.

We never execute the boot ROM, so nothing was setting either one, and
R5900Context started with Status at zero.

That is not cosmetic. libkernel's StartThread opens with
`mfc0 Status; xori 1; andi 1` and bails out with -1 when IE is clear --
its "you must call iStartThread from an interrupt handler" guard. With
Status at zero that guard fired every time, so every StartThread failed.
Dragon Quest VIII hits this during boot: it creates its CD streaming
thread, gets -1, prints "Can't start thread for streaming." and then
deadlocks with every thread blocked and none runnable. Nothing in the
runtime logs anything, because from its point of view the guest simply
asked a question and got an answer.

EIE matters for the matching reason: DIntr reports whether it was set so
the caller knows whether to pair it with an EIntr. Starting at zero makes
DIntr always answer "already disabled", so the re-enable never happens.

Two changes, both needed:

- R5900Context's constructor now sets Status to EIE | IE rather than 0.
  BEV is deliberately left clear -- that selects the boot exception
  vectors, which is the pre-handoff state, not this one.

- PS2Runtime's constructor no longer memsets m_cpuContext. R5900Context
  already zeroes itself before applying its reset values, so the memset
  only threw those values away. Threads created later were unaffected
  because EeScheduler::startThread assigns `R5900Context{}`, which is
  why this presented as "the main thread cannot start threads" rather
  than something more obviously global.

(cherry picked from commit 2ac2ce6320)
2026-08-18 19:50:13 -03:00
Sinan d9ea4fb63d Let a thread resume at the instruction after a syscall (#210)
A syscall can hand control back to the scheduler before the instruction
after it runs. SetSyscall lets the guest install its own handler for a
syscall number; dispatchSyscallOverride then suspends the calling
thread and queues that handler as a GuestInvocation. When the
invocation finishes, EeScheduler resumes the parent thread at the
address the generated code stored just before calling handleSyscall --
the instruction right after the syscall.

The analyzer never marked that address as an entry point. It queues
resume entries for JAL and JALR only, so no generated function could be
re-entered there, EeScheduler's hasFunction() check failed, and the
thread was made dormant instead of resumed. The thread simply stops;
because the scheduler then drains normally and run() returns, it looks
like a clean shutdown rather than a fault, which makes it awkward to
recognise.

This is reachable during early boot on a real title. Dragon Quest VIII
hits it in crt0: the Metrowerks startup code installs a handler for
syscall 0x83 and immediately issues it, and execution ends there,
roughly ten functions into the binary.

Note the offset is +4, not the +8 used for JAL and JALR -- syscall has
no delay slot.

(cherry picked from commit becb2be5bd)
2026-08-18 19:49:38 -03:00
Ranieri d74a3ce139 Feature/gs refactor (#204)
* refactor: from guest  threads to EE scheduler

* feat: bad wip mpeg fix for code veronica

* feat: cheap copy from host
feat: small perf o vsync tick

* feat: added EE clock Hz
fix: fix MPEG out of sync with new EE refactor

* fix: fix lotr tests

* fix: fix cri dtx loading
fix: fix wrong mmi instruction translation
fix: fix thread info params
feat: added EE  timers decoder and consumer
feat: split SFI and IOP memory to prevent collision and overrides

* feat: revert wrong changes

* refactor: change GS architecture
2026-08-13 22:49:04 -03:00
Ranieri 8d7e8a5a46 Feature/ee timers and fixes (#203)
* refactor: from guest  threads to EE scheduler

* feat: bad wip mpeg fix for code veronica

* feat: cheap copy from host
feat: small perf o vsync tick

* feat: added EE clock Hz
fix: fix MPEG out of sync with new EE refactor

* fix: fix lotr tests

* fix: fix cri dtx loading
fix: fix wrong mmi instruction translation
fix: fix thread info params
feat: added EE  timers decoder and consumer
feat: split SFI and IOP memory to prevent collision and overrides

* feat: revert wrong changes
2026-08-12 12:11:06 -03:00
Ranieri f4309cd18c refactor: from guest threads to EE scheduler (#184)
* refactor: from guest  threads to EE scheduler

* feat: bad wip mpeg fix for code veronica

* feat: cheap copy from host
feat: small perf o vsync tick

* feat: added EE clock Hz
fix: fix MPEG out of sync with new EE refactor

* fix: fix lotr tests
2026-08-11 11:35:50 -03:00
Ranieri f49ca4edbc refactor: refactor VU1 (#191)
* feat: implement fix and changes based on dark cloud report
fix: fix GS AFAIL for RGB/alpha/Z, ZMSK
fix: fix VU1 flags mask and pipeline
fix: small VU1 cache fix
feat: __ct__, __sinit_ are not sillent stubs anymore

* feat: fix song JP pulling

* feat: sound update for lotR

* feat: prevent guest execution to be very slow

* fix: small gs size bug

* feat: refactor VU
fix: fix cliping and other issues on gs
fix: fix wrong vu0 register on recompiler

* fix fix ACC scheduler stall
feat: remove unused test
fix: .fix overflow e underflow on FMAC

* feat: small setting  for windows test
2026-08-05 14:50:24 -03:00
Shane Michael Mathews (Personal Account) 61300792a0 fix(recomp): include <cstdint> before elfio in elf_parser.h (#199)
ELFIO, pinned at Release_3.12 via FetchContent, uses uint16_t, uint32_t
and uint64_t in elf_types.hpp without including <cstdint> itself. Newer
libstdc++ releases trimmed the transitive includes that used to supply
those typedefs, so building elf_parser.cpp fails:

  elf_types.hpp:30:20: error: 'uint16_t' does not name a type
     30 | using Elf_Half   = uint16_t;

Include <cstdint> ahead of elfio.hpp so the typedefs are visible when
that header is processed. Header-only change; no behaviour is affected.
2026-08-01 12:48:48 -03:00
gustavoauneth d87bff4256 fix(recomp): incorrect opcode mapping for MMI2 in PMADDH, PHMADH, PMSUBH and PHMSBH instructions (#194) 2026-08-01 00:58:03 -03:00
Shane Michael Mathews (Personal Account) 6970f3a2c4 feat(hle): implement sceVu0 macro-mode matrix/vector math library (#183)
Fill the remaining TODO_NAMED bodies in Kernel/Stubs/VU.cpp so the
out-of-line libvu0 macro-mode routines write their documented results to
the caller's output operand instead of leaving guest memory untouched (the
TODO_NAMED stub throws, or returns -1 once its per-name warning budget is
spent, and never writes the destination).

Implements the matrix/geometry/clip/lighting entry points:
CameraMatrix, InversMatrix, MulMatrix, TransMatrix, RotMatrix,
NormalLightMatrix, LightColorMatrix, DropShadowMatrix, ViewScreenMatrix,
RotTransPers/RotTransPersN, ClipScreen/ClipScreen3/ClipAll, MulVector,
ScaleVectorXYZ, DivVector/DivVectorXYZ, InterVector/InterVectorXYZ,
ClampVector, ecossin. Pure input->output arithmetic over guest memory
through the existing getMemPtr/getConstMemPtr and getRegU32/ctx->f[]
accessors already used by the implemented sceVu0 functions in the same
file. No new runtime state, no signature changes, no VU.h changes.

The clip and view/shadow routines provide the functional contract callers
rely on (nonzero => offscreen; projection by formula shape) rather than a
bit-exact reproduction of the COP2 sticky clip-flag register.

Adds ps2xTest/src/ps2_vu_tests.cpp covering each routine against its
documented formula.
2026-07-28 15:21:01 -03:00
Ranieri f3687c5ae6 Feature/random fixes platform support (#179)
* feat: implement memory card IOP

* feat: IOP trace

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

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

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

* feat: remove idea from track
2026-07-22 02:37:53 -03:00
Shane Michael Mathews (Personal Account) 1176609890 test(sema): make contended semaphore poll/signal test deterministic (#176)
The "Semaphore poll/signal remains stable under host-thread contention"
test created the semaphore full (init == max == 1), so SignalSema could
only succeed after PollSema had already freed a slot. With no start
barrier, under host-thread contention the signaler thread could run all
64 of its iterations before the poller's first timeslice, making every
SignalSema legitimately return KE_SEMA_OVF and leaving signalOkCount at
0 — a false failure of "contended SignalSema should observe successful
releases". An earlier investigation of the unfixed test observed this
twice in 24 contended (8-way-parallel) full-suite runs and never in 20
serial runs; CI has independently hit the same assertion on the
unrelated draft PR #174, confirming the trigger is host scheduling, not
the code under review.

Seed the semaphore with headroom (init=1, max=2) so the first PollSema
and the first SignalSema each succeed regardless of scheduling order,
and add a start barrier so both workers start together, maximizing the
opportunity to interleave. Widen the final-count range check to the new
max. The semaphore implementation is unchanged; both threads still
contend concurrently on the same per-semaphore mutex.
2026-07-21 11:09:38 -03:00
Ranieri ee149581aa Refactor IOP system (#170)
* feat: implement memory card IOP

* feat: IOP trace

* feat: move IOP logic to ps2xIOP
refactor: small refactor on audio api on runtime
2026-07-15 01:44:06 -03:00
TH3BACKLOG 905b4edfa8 fix(recomp): advance ctx->pc on fallthrough functions with no terminating branch (#168)
* fix(recomp): advance ctx->pc on fallthrough functions with no terminating branch

FunctionEmitter::emit only ever advances ctx->pc via the per-instruction
`ctx->pc = 0x<addr>u;` assignment (overwritten by the next instruction in
the same function) or via handleBranchDelaySlots when the last instruction
is a branch/jump. A function whose last instruction is neither (e.g. a
lone padduw/NOP-style instruction with no terminator) leaves ctx->pc
pointing at its own last instruction forever after returning, since
nothing ever advances it to the next function.

dispatchLoop then reads ctx->pc, looks up the same function, and calls it
again -- forever. No exception, no crash, just an infinite loop that
silently never makes forward progress.

Reproduced on SDBZ's SLUS_214.42 ELF entry point: 0x100008 is emitted as a
standalone 1-instruction function (padduw $at, $zero, $zero) with no
branch, causing dispatchLoop to spin on pc=0x100008 indefinitely.

Fix: track whether the last processed instruction had a delay slot (i.e.
was a branch/jump); if the function ends without one, emit an
unconditional ctx->pc = function.end before closing the function so
dispatchLoop resumes at the next function instead of spinning.

* review: trim overly verbose comment per ran-j feedback
2026-07-13 09:50:36 -03:00
Shane Michael Mathews (Personal Account) 81f2a7f5e1 fix(runtime): stop double-counting nloop in sceGifPkRefLoadImage A+D header GIFtag (#149)
sceGifPkRefLoadImage seeded its A+D header GIFtag with nloop=4 via
makeGiftagAplusD(4u), then closePacketGifTag computed the appended
register-qword count (4) from the write-cursor delta and ADDED it to the
seed, finalizing nloop=8 for a tag with only 4 A+D register qwords
(BITBLTBUF/TRXPOS/TRXREG/TRXDIR). A GIF parser consuming that tag eats the
following IMAGE GIFtag and its first payload qwords as bogus A+D data,
starving the CLUT/texture upload that the setup packet precedes (e.g. DQ8
font-glyph CLUTs staying all-zero).

Seed an open A+D tag (nloop=0, nreg=1, EOP clear) instead and let
closePacketGifTag add the true count, finalizing 0x1000000000000004 -- the
value the regression test pins. The seed is written via a named
makeGiftagAplusDOpen(nloop) helper in Support.h (mirrors makeGiftagAplusD but
leaves EOP/bit 15 clear for a chained tag whose nloop is finalized at close),
rather than a bare (1ULL << 60) literal, to keep the file's
GIFtag-construction convention consistent. makeGiftagAplusD(0u) can't be used
because it always sets EOP.

Adds a byte-level regression test that drives sceGifPkRefLoadImage (setup
only, qwcRemaining=0) against a scratch packet-builder state and asserts the
finalized header tag lo/hi, the four A+D register descriptors (0x50..0x53),
and their four register payloads (BITBLTBUF dbp/dbw/psm, TRXPOS, TRXREG
width/height, TRXDIR) -- pinning the full setup packet.

Full build clean; ps2x_tests 297/297.
2026-07-10 14:20:59 -03:00
Shane Michael Mathews (Personal Account) c4355c8cda Pads open in DIGITAL mode, not analog (spec 06) (#153)
Real PS2 pads power up in DIGITAL mode (CURID 4, mode byte 0x41) and only
switch to analog (CURID 7, 0x73) when the game calls scePadSetMainMode. The
three open-time sites in Pad.cpp incorrectly set analogMode=true, so
scePadInfoMode(PAD_MODECURID) reported analog from frame 0. Games that gate
logic on the analog transition (e.g. DQ8 fn_165b20) saw an impossible state.

- PadPortState default, initializePadPortLocked, scePadPortOpen: analogMode=false
- scePadSetMainMode remains the only path into analog mode (unchanged)
- Updated the existing pad-info test's at-open CURID expectation (7 -> 4)
- Added a regression test: open reports digital (CURID 4, 0x41), then
  scePadSetMainMode(mode=1) switches to analog (CURID 7, 0x73)
2026-07-10 14:10:23 -03:00
Shane Michael Mathews (Personal Account) d50ed4e300 test(gs): H-format shared-plane texture regression coverage (T8H/T4HL/T4HH) (#155)
Add byte-level regression tests for the GS "high" indexed texture formats,
which store their index in the alpha byte of a shared PSMCT32 word -- T4HL in
bits 24-27, T4HH in bits 28-31, T8H in the full byte 24-31. The format-aware
VRAM path (the #140 refactor) already implements these at main; these tests
pin the plane-separation and over-transfer behavior so it can't silently
regress.

- Plane separation: uploading distinct T4HL then T4HH patterns to the same dbp
  leaves both planes intact (RMW into the shared CT32 word), cross-checked
  against GSMem::ReadP4HL/ReadP4HH.
- T8H coverage: uploads the full byte into bits 24-31 via a real BITBLTBUF+GIF
  IMAGE transfer (one byte per texel through WriteP8H) and round-trips it via
  GSMem::ReadP8H. T8H takes the plain byte-store branch of
  PixelStorageTraits::Write and its own upload loop, distinct from the
  T4HL/T4HH nibble RMW path, so it needs independent coverage.
- Clobber interaction: a T4HL nibble upload over a prior T8H byte overwrites
  bits 24-27 with the nibble and preserves bits 28-31 (the T8H byte's high
  nibble) -- the hardware-modeled RMW outcome.
- Sampled channels: each plane resolves through its own 16-entry CT32 CLUT to
  the expected RGBA, with distractor CLUT entries so a cross-plane nibble read
  produces a non-matching color.
- Over-transfer hard-stop: an ~8x oversized T4HL IMAGE payload writes only
  rrw*rrh texels and deactivates the transfer (trxdir=3), and a subsequent
  transfer to a different dbp is byte-correct.

Test-only; no product code change. ps2x_tests: 299/299.
2026-07-10 12:06:20 -03:00
Ranieri cf4a90c4ab feat: default capture history gs to false (#167)
fix: remove wrong logic on frameupload
fix: fix option to enable and disable logs
2026-07-08 22:09:23 -03:00
Ranieri ecc86f4b5d Feature vu1 cache (#158)
* feat: explode vu1 in files
feat: added way  more tests for vu1

* feat: added VU1 cache
2026-07-07 21:48:26 -03:00
Ranieri 52edf07657 Feature/agressive recompiler (#146)
* feat: added guestBranchKind enum to categorize branch types
feat: added missingFunctionPolicy enum to define behaviors for missing function scenarios
refactor: added handle guest branches and report missing functions
feat lookupFunction to utilize new dispatch logic and improve error handling for unregistered functions

* fix: fix test conflict

* feat: added debug sound driver logs

* feat: emmiter for return

* feat: added recompiler reporter
feat: added strict diagnostics flag for heavy debug calls

* feat: staticc table insted of hashmap for runtime

* feat: back file to ignore

* feat: explode code across helpers and classes

* feat: update codegen test
feat: better guest nop check

* feat: fix link problem on linux

* feat: fix Segmentation fault

* feat: added recompile replace for DMA and MMIO
feat: added a clean memory helpers
feat: use memory helpers across the project
feat: fix ucrt on msvc

* feat: undo messup merge
2026-07-07 10:14:25 -03:00
Shane Michael Mathews (Personal Account) 61621b8313 fix(runtime): make GS CSR atomic to fix vsync-worker/guest data race (#145)
updateGsCsrFieldForVSync runs on the detached vsync worker and updated the
FIELD bit with a non-atomic read-modify-write of GSRegisters::csr while
guest threads concurrently read/write the same word (MMIO read32/read64 and
the W1C handling in write32/write64) and the GIF path sets SIGNAL/FINISH.
Even though the writers touch disjoint bits, a whole-word RMW loses the
other side's update: a clobbered SIGNAL/FINISH set hangs a game
synchronizing on GS completion, a clobbered W1C clear re-asserts a handled
interrupt, and a clobbered FIELD toggle stalls interlace field polling.
ThreadSanitizer flags the race on main (updateGsCsrFieldForVSync vs
PS2Memory::write64 and GS::writeRegister).

Make the field std::atomic<uint64_t> and perform every update as a single
atomic RMW:

- vsync FIELD toggle -> fetch_or / fetch_and
- MMIO W1C writes -> compare_exchange loop in shared helpers (a
  load-then-store pair would still race); a 32-bit store to the CSR's upper
  dword previously bypassed the W1C special case entirely and went through
  the plain merge branch - both halves now share the same atomic helper
  with unchanged guest-visible semantics
- GIF SIGNAL/FINISH -> fetch_or
- reads -> load()

std::atomic<uint64_t> is lock-free on all supported targets (static_assert
added), the struct's size/alignment asserts are unchanged, GSRegisters is
never copied by value, and default (seq_cst) ordering is used throughout -
these operations are rare (vblank ticks, GIF signals, CSR MMIO), so
reviewability wins over micro-optimization.

New regression test: two racer threads each own one status bit (SIGNAL /
FINISH) and loop 80k GIF-set + W1C-clear cycles verifying their own bit
after each half-op while the vsync worker toggles FIELD. Fails 20/20 runs
against the previous code, passes 50/50 with the fix, ~350ms runtime, no
sanitizer needed.
2026-07-06 12:35:44 -03:00
Ranieri 5196a6672a Refactor runtime for move speed and better code style (#140)
* feat: added guestBranchKind enum to categorize branch types
feat: added missingFunctionPolicy enum to define behaviors for missing function scenarios
refactor: added handle guest branches and report missing functions
feat lookupFunction to utilize new dispatch logic and improve error handling for unregistered functions

* fix: fix test conflict

* feat: added debug sound driver logs

* feat: emmiter for return

* feat: added recompiler reporter
feat: added strict diagnostics flag for heavy debug calls

* feat: staticc table insted of hashmap for runtime

* feat: back file to ignore

* feat: explode code across helpers and classes

* feat: update codegen test
feat: better guest nop check

* feat: fix link problem on linux

* feat: fix Segmentation fault
2026-07-04 00:43:22 -03:00
Jeffrey Shulkin 3a2e0d69fd feat: add SSE2 support and fix SSE4.1 paths for PADDSW and PSUBSW instructions (#141)
* feat: add sses2 support for PADDSW and PSUBSW instructions

* Fixed SSE4.1 PADDSW/PSUBSW implementations
2026-07-04 00:02:00 -03:00
Jeffrey Shulkin db3d44fbde Added explicit FPU ACC register (#143) 2026-07-03 10:03:59 -03:00
Ranieri 8c8a97af65 Feature/mpeg decoder (#120)
* feat: added ffmepg as dependency

* feat: wip decoder video

* feat: some perf and cleanup

* feat:  added generic MPEG stream notification

* feat: CMakeLists.txt in ps2xStudio to configure SDL2 build options for static linking.
fix: fix ffmpeg setup for linux
fix: now MPEG decoder now identify that movie has ended and can play again anytime
feat: better audio stub to not block games

* feat: fix expansion test

* feat: foo

* a

* feat: finally added a helper to to prevent thread starvation

* feat: added basic  vu0 code execution

* feat: added yield Guest Execution After Wake to prevent deadlock

* feat: added  options  on  cmake for logs
feat: better input for keyboard pad

* feat: small corrections like top and itop vu branches etc

* feat: changes

* feat: working feature

* feat: fatal frame iop

* feat: test fix
feat: z buffer fix

* fix: gix GsPutIMR IMR

* feat: added rl imgui

* feat: added helper to get snapshot

* feat: added debug panel consuming snapshots

* feat: added pad snapshot
feat: added RCP debug events

* feat: final cleanup from old code

* feat: added EE timer counter
feat: applyed sound driver for Lotr
feat: better check for sound driver compat layout
feat: enquee and cosumed DMa cause
feat: added Pad execCMd
feat: update GS vsync signal flag
feat: custom IOPs for LotR

* feat: small cleanups

* fix: fix wrong import
2026-06-26 21:04:39 -03:00
tealalchemist a111059a70 [runtime] fixup rasterizer (#139)
- missing RGBA5551 to RGBA8888 conversion in lookupClut
- round depth up for triangles (Code Veronica text bug)
2026-06-24 14:07:47 -03:00
tealalchemist 9bf9c08d9d [runtime] various fixup (#138) 2026-06-22 11:16:54 -03:00
Shane Michael Mathews (Personal Account) a8828f5c77 fix: semaphore syscalls return sid on success instead of KE_OK (#136)
* fix: semaphore syscalls return sid on success instead of KE_OK

The PS2 EE BIOS returns the semaphore ID on success for all semaphore
syscalls, not zero. PollSema, WaitSema, SignalSema, and DeleteSema were
all returning KE_OK (0) on success, which breaks games that compare the
return value against the semaphore ID.

DQ8's init code polls a mutex semaphore at 0x12A670 and checks the result
against the semaphore ID using bne. With KE_OK returned, the comparison
always fails and the game spins forever at PC 0x164978 and never starts.
Other games that follow the same EE BIOS convention would hit the same
problem. CreateSema already returned the ID correctly; this brings the
other four operations in line with the same convention.

The fix is five changes in Sync.cpp: the four return sites are updated to
return sid, and the count-decrement guard in WaitSema is changed from
ret == 0 to ret >= 0. The guard change is necessary because ret is now
seeded to sid (a positive value) on the success path, and the old equality
check would have silently stopped decrementing the semaphore count. Error
paths (KE_WAIT_DELETE, KE_RELEASE_WAIT) are negative and still correctly
bypass the decrement.

Tests are updated to expect sid instead of KE_OK on success paths, and new
test cases cover the blocked-wait-then-signal path (the actual DQ8 scenario),
force-release via ReleaseWaitThread, and the count-decrement guard directly.

* fix: update stale KE_OK assertions in expansion and SIF RPC tests

Two test files were not updated alongside the semaphore return-value change.
PollSema and SignalSema now return sid on success; update the four assertions
that expected KE_OK on those success paths.

* fix: tighten WaitSema count-decrement guard to ret == sid

ret is seeded to sid on success and only ever overwritten with negative
error codes, so ret == sid precisely expresses "this wait acquired this
semaphore" — more explicit than the looser ret >= 0.

Suggested by ran-j in PR review.
2026-06-21 00:07:58 -03:00
tealalchemist 91592e3faa GS rasterizer rework (#132)
* [runtime] Memory swizzling rework

Reimplementation of memory swizzling supporting all valid GS texture
storage types.

An additional optimization was done to improve the speed of lookups.
Since page access is linear, we can precompute the non-linear bit
(blocks and columns) for an entire page for each format. This reduces
the runtime calculation to just calculating the correct page in 2d
memory space and then performing a lookup in the table to get the
location in memory inside a page. Since all page accesses are the same,
this works across all pages. Speed improvements should be compounded by
the number of accesses but there might be some reductions as a result of
actually implementing formats that were not implemented before.

I took care to do this with simple math. GS swizzling is complicated and
optimizing it for debug builds is outside the scope of my current work.

This is just the standalone code. It will be worked into the current
rasterizer in a follow up commit.

* [runtime] Reimplement GS host -> local transfers

Reimplement the host -> local transfers using new swizzling code

* [runtime] Reimplement sampling with new swizzling

Reimplements the rasterizer sampling code to use the new swizzling code

* [runtime] Replace fb writes with new swizzling

Replace the framebuffer write code with the new swizzling code

* [runtime] Reduce vram read/write to func lookup

Reduces the vram read write switches to a function lookup

* [tests] fix GS tests not handling depth test

* [runtime] z interpolation and write support

Add support for interpolating z from primitives and writing to the z
buffer after z testing and masking

* [runtime] local to local transfer rework

* [runtime] host readout uses new sizzling code

plus cleanup some dead code

* [runtime] rework local->host transfer

update to the new swizzling code

* [runtime] update clear function with new swizzling
2026-06-21 00:07:26 -03:00
Ranieri 790aaf4cda feat: invert codegen hight to low convertion (#131)
* feat: invert codegen hight to low convertion
feat: added copy and GetEntryAddress
feat: handle truncated DMAC

* feat: always use address on analyzer now

* feat: correct pick syscalls ID

* feat: added deci2Call

* feat: added wip dbcmain IOP

* feat: added InitTLB
feat: added err logs on thread for debug sus crash

* fix: fix SetupHeap for strange cases

* feat: fix incorrect SetupHeap test(it use a wrong idea on how heap allocate memory)

* feat: added memalign and memalign_r
feat: added GetOsdConfigParam2 and  SetOsdConfigParam2 but idk if was a good idea

* feat: added more memory stuff

* feat: back to library functions
2026-06-13 16:39:39 -03:00
Ranieri 7562ec14c9 better analyzer and integrating sce-symbol-scanner (#130)
* feat: modularize elf analyzer
feat: added experimental sce symbol scanner
feat: change analyzer order
feat: small optimizations on analyzer

* feat: remove example_config.toml because its causing confusion on some people

* feat: embed sce symbol but leave optional import path
feat: killed skip function on analyzer but leave it so you can skip manual if you want

* feat: pin elfio tag

* feat: manually create string view with size

* feat: update ghidra script
2026-06-06 23:37:12 -03:00
Sinan ed8b3ebee1 Reduce recompiler output memory usage, and added multi-threading to recomp process (#128)
* feat(recomp): Reduce recompiler output memory usage

Stream output generation, add low-memory config controls, and avoid
pathological indirect-jump switch expansion in generated C++.

Low-memory mode now avoids retaining per-instruction disassembly strings
while still emitting asm comments during output generation. Output workers
are bounded/configurable, combined output is streamed, and decoded buffers
are released after generation.

Also document the new output memory settings.

* fix(recomp): added tests for unregistered JR/JALR, updated fallback logic to cover JR/JALR, moved Rabbitizer formatting into R5900Decoder
2026-06-06 00:15:14 -03:00
Ranieri 93e221feaa Feature/runtime ecosystem refactor (#107)
* feat: remove memory and pad from stub section

* feat: add support for resume entry targets in CodeGenerator (this allow jumps in address outside function)
feat: refactor entry point discovery one more try to reduce big generated file

* feat: optmizations for release build

* feat: remove unused  file

* feat: added some test cases for code gen

* feat: added log macro and remove win specific code

* feat: refactor runtime folder structure
feat: added reset sound driver RPC state and compatibility layout
feat: rename and added new test
feat: update RPC calls to use defined constants
feat: added more PSMC(16, 32)
feat: change cd read to try find the asset ignoring case sensitive
fix: fix some render problems
feat: add logs on pad
feat: added more RPC handles

* feat: added game override for code veronica

* feat: apply vita patch

* feat: flags to disable build

* feat: fix merges
feat: break a lot of tests

* feat: better throw error on empty cd path
feat: remove recompiler unusde function
feat: apply missing patch

* feat: gamedp is now part of lib
feat: missing file

* feat: small cleanup

* feat: missing vita changes

* feat: fix more merge

* feat: fix tests

* feat: last missing feature

* feat: added missing import

* feat: rename test local functions

* feat: init syscall on ps2 list

* feat: added DMA helpers

* feat: faster builds
feat: more implement for darkcloud

* feat: back missing file

* feat: added missing includes

* feat: remove test

* feat: missing include

* feat: read register funtion

* feat: build  fix

* feat: force  exit on detach thread
2026-04-04 23:09:56 -03:00
roby65 553a9027d8 sceGsSetDefDBuff and sceGsSwapDBuff implementation based on the DC version (#106) 2026-03-24 21:34:17 -03:00
Ranieri cad1ca0bb5 Feature/added execution guess gs psmt (#104)
* fix: CRITICAL fix on code gen on generating BEQ translation, I added a small yeld because goto could spin forever and monopolize guest execution

* feat: add scratchpad alias base and improve scratchpad address handling

* feat: added debug logging on GifArbiter for submit and drain operations

* feat: add interrupt and thread management syscall implementations
fix: change some IDs calls to match ps2sdk

* feat: added vif1 logs

* feat: added logs on gs gpu
feat: added performLocalToLocalTransfer to GS emulation path for TRXDIR = 2. (emulates the PS2 GS “copy this rectangle from one place in VRAM to another”)

* feat: added PSMT8 and refactor PSMT4

* feat: some identation on vu1
feat: added some logs on vu1

* feat: added GuestExecutionScope to temporarily stop owning guest execution, then restore it exactly as it was.
feat: added vsync wizardry
feat: added some regression test

* fix: fix gs logger

* feat: remove extra logs I think they will help no one
feat: move join all threads to prevent the app to get stuck on close, but now it random crash on closing
feat: one more small test on psmt4 to try fix ghosting on re code veronica

* feat: added a small case for exporter from ghidra for metal slug 3

* feat: added ugly code to pass on test
2026-03-18 19:14:40 -03:00
Ranieri 7ca6a866c9 Feature/ghidra export and syscall fixes (#100)
* docs: deprecate the local analyzer workflow in favor of Ghidra
feat: improve the Ghidra exporter for stripped games and internal entry points
fix: correct FindAddress behavior in the runtime
fix: emit missing delay-slot code in recompiler  edge cases
feat: add SetSyscall support from @Whoneon
feat: add dispatchSyscallOverride support from @Whoneon
fix: fix  Unmatched '{' due to missing newlines from issue #96
feat: delete python ghidra script I never updated it anyway
feat: added a lot more of regression test
feat: added a lot of logs to help debug on runtime
fix: fix wrong syscall ID on runtime
2026-03-09 00:41:51 -03:00
Antonio Guastella 9e4fd794c1 kernel: implement syscall 0x83 FindAddress + unit tests (#93) 2026-03-06 12:55:34 -03:00
Antonio Guastella 493522a769 recompiler: filter auto-generated map symbols to reduce oversized outputs (#95) 2026-03-05 11:14:41 -03:00
Antonio Guastella da10073cb9 ghidra: normalize Windows executable path in exported TOML input (#92) 2026-03-03 16:19:31 -03:00
Antonio Guastella ccd17e5244 Issue 13: Minimal libpad (SIO2/PAD) stubs, input mapping and tests (#69)
* pad: implement basic input mapping and stubs

Add a minimal libpad implementation backed by raylib input
Provide override hooks for deterministic tests
Add pad input tests and link runtime into test target

* FIX: moved pad override test hooks to ps2_call_list.h

* pad: move test hooks to X macro list + removing __cplusplus ifdef

* removed one last ifdef remaining

* refresh PR merge status

---------

Co-authored-by: Ranieri <ran-junior@hotmail.com>
2026-03-03 16:02:04 -03:00
Vova Vovchok 765cbcca0f Integrate ps2xStudio and improve elf_analyzer callback detection (#83)
* Integrate ps2xStudio and improve elf_analyzer callback detection

* Update CMakeLists.txt for ImGui libraries

* CMakeLists Fixes Again

Updated dependencies and improved comments for clarity.

* Again CMakeLists. Now it definitely works

* again CMakeLists

* Update CMakeLists.txt ps2xStudio & ps2xTest

* Update CMakeLists.txt

* Update CMakeLists.txt

* Update StudioState.hpp

* Update CMakeLists.txt
2026-03-01 16:41:33 -03:00
roby65 b3be2650b0 Fixed recompiler error when the cpp filename is too long (#88)
* Fix error when filename is too long

* Fix typo

* Added tests
2026-03-01 16:39:42 -03:00
318 changed files with 80698 additions and 23890 deletions
+1 -1
View File
@@ -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
View File
@@ -17,4 +17,9 @@ ps2xRuntime/include/ps2_recompiled_functions.h
ps2xRuntime/include/ps2_recompiled_stubs.h
ps2xRuntime/src/runner/ps2_recompiled_functions.cpp
ps2xRuntime/src/runner/register_functions.cpp
ps2xRuntime/output
ps2xRuntime/output
ps2xRuntime/src/runner
android/app/.cxx/
android/local.properties
android/.gradle/
.idea/
+61 -11
View File
@@ -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()
+27
View File
@@ -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": []
}
]
}
+31 -38
View File
@@ -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
+36
View File
@@ -0,0 +1,36 @@
# Android runner
## Requirements
- Android Studio (recommended) or a local Gradle 8.7+ / JDK 17 install
- Android SDK 34 + NDK (installed automatically by Android Studio on first sync)
- CMake 3.22.1 from the SDK (Android Studio installs it on demand)
## Building
Option A — Android Studio: open the `android/` folder and run the `app` configuration.
Option B — command line (no wrapper is committed; generate it once):
```sh
cd android
gradle wrapper --gradle-version 8.9
./gradlew assembleRelease
```
APK output: `android/app/build/outputs/apk/release/app-release.apk`.
## Running a game
Since there is no argv on Android, the guest ELF path comes from
`PS2X_DEFAULT_BOOT_ELF`, set via the `ps2xBootElf` Gradle property
(`android/gradle.properties`, or `-Pps2xBootElf=...` on the command line).
```sh
adb install app/build/outputs/apk/release/app-release.apk
adb shell mkdir -p /storage/emulated/0/Android/data/com.ps2x.runner/files
adb push "path/to/game/." /storage/emulated/0/Android/data/com.ps2x.runner/files/
```
Logs: raylib output goes to logcat (`adb logcat -s raylib`); runtime `std::cout`/`cerr`
output is not redirected to logcat yet.
+59
View File
@@ -0,0 +1,59 @@
plugins {
id 'com.android.application'
}
// Override with: gradlew assembleRelease -Pps2xBootElf=/absolute/path/on/device.elf
def ps2xBootElf = project.findProperty('ps2xBootElf') ?: '/storage/emulated/0/Android/data/com.ps2x.runner/files/game.elf'
android {
namespace 'com.ps2x.runner'
compileSdk 34
ndkVersion '28.2.13676358'
defaultConfig {
applicationId 'com.ps2x.runner'
minSdk 28
targetSdk 34
versionCode 1
versionName '0.1.0'
externalNativeBuild {
cmake {
arguments '-DPS2X_BUILD_RECOMP=OFF',
'-DPS2X_BUILD_ANALYZER=OFF',
'-DPS2X_BUILD_TEST=OFF',
'-DPS2X_BUILD_STUDIO=OFF',
'-DPS2X_ENABLE_SCCACHE=OFF',
'-DPS2X_RUNNER_UNITY_BUILD_BATCH_SIZE=32',
'-DANDROID_CPP_FEATURES=rtti exceptions',
"-DPS2X_DEFAULT_BOOT_ELF=${ps2xBootElf}"
targets 'ps2EntryRunner'
}
}
ndk {
abiFilters 'arm64-v8a', 'x86_64'
}
}
externalNativeBuild {
cmake {
path '../../CMakeLists.txt'
version '3.22.1'
}
}
buildTypes {
debug {
externalNativeBuild {
cmake {
arguments '-DCMAKE_BUILD_TYPE=RelWithDebInfo'
}
}
}
release {
minifyEnabled false
signingConfig signingConfigs.debug
}
}
}
+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="PS2 Recomp"
android:hasCode="false"
android:isGame="true">
<activity
android:name="android.app.NativeActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboard|keyboardHidden|navigation"
android:screenOrientation="landscape"
android:exported="true">
<!-- Runner shared library built by ps2xRuntime/CMakeLists.txt -->
<meta-data
android:name="android.app.lib_name"
android:value="ps2EntryRunner" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
+3
View File
@@ -0,0 +1,3 @@
plugins {
id 'com.android.application' version '8.6.1' apply false
}
+4
View File
@@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx4g
android.useAndroidX=true
# ps2xBootElf=/storage/emulated/0/Android/data/com.ps2x.runner/files/SLUS_201.84
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+17
View File
@@ -0,0 +1,17 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = 'PS2Recomp'
include ':app'
+24
View File
@@ -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
View File
@@ -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> &sections);
static std::vector<JumpTable> detectJumpTables(
const std::vector<Instruction> &instructions,
const std::vector<Section> &sections,
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
+45 -16
View File
@@ -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> &sections);
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> &sections, 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> &sections) 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
+446
View File
@@ -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> &sections)
{
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 &section : 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> &sections,
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 = [&sections](uint32_t addr) -> bool
{
if (addr == 0)
{
return false;
}
if (sections.empty())
{
return true;
}
for (const auto &section : 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;
}
}
+17 -2
View File
@@ -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;
}
}
}
+83
View File
@@ -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);
}
}
File diff suppressed because it is too large Load Diff
+218
View File
@@ -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);
}
}
+793
View File
@@ -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> &sections) const
{
std::unordered_map<uint32_t, std::map<std::string, Candidate>> candidatesByAddress;
if (!m_root)
{
return {};
}
for (const Section &section : 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 &section, 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 &section, 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> &sections) const
{
return m_impl->scan(sections);
}
const std::string &SceSymbolScanner::lastError() const
{
return m_impl->lastError();
}
}
+237
View File
@@ -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;
}
}
+75
View File
@@ -0,0 +1,75 @@
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)
endif()
install(TARGETS ps2_iop
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
install(DIRECTORY include/
DESTINATION include
)
+57
View File
@@ -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.
+135
View File
@@ -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;
};
}
+50
View File
@@ -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;
};
}
+155
View File
@@ -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;
};
}
+35
View File
@@ -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);
}
+497
View File
@@ -0,0 +1,497 @@
#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)
{
const uint32_t pc = cpu.pc;
const uint32_t instruction = m_memory.read32(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;
}
}
+42
View File
@@ -0,0 +1,42 @@
#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);
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;
};
}
+744
View File
@@ -0,0 +1,744 @@
#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;
}
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));
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.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;
thread.state = IopThreadState::Ready;
setV0(0);
return true;
}
case 8: // ExitThread
case 9: // ExitDeleteThread
if (m_currentThread != nullptr)
{
m_currentThread->state = 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;
}
it->second.state = 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);
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)
{
it->second.state = 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)
it->second.state = 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;
}
it->second.state = 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)
it->second.state = 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)
{
m_currentThread->state = 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;
best->state = 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;
thread.state = 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)
{
thread.state = 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)
{
m_currentThread->state = 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;
m_currentThread->state = IopThreadState::Sleep;
cpu.yielded = true;
}
void IopKernel::delayCurrentUntil(uint64_t wakeCycle, IopCpuState &cpu)
{
if (m_currentThread == nullptr)
return;
m_currentThread->wakeCycle = wakeCycle;
m_currentThread->state = IopThreadState::Delay;
cpu.yielded = true;
}
IopThread *IopKernel::beginNextReady(uint64_t currentCycle)
{
for (auto &[id, thread] : m_threads)
{
if (thread.state == IopThreadState::Delay && thread.wakeCycle <= currentCycle)
thread.state = IopThreadState::Ready;
}
IopThread *next = nullptr;
for (auto &[id, thread] : m_threads)
{
if (thread.state != IopThreadState::Ready)
continue;
if (next == nullptr || thread.priority < next->priority ||
(thread.priority == next->priority && thread.id < next->id))
next = &thread;
}
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
{
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.cpu.pc == returnSentinel || thread.cpu.stopped)
thread.state = IopThreadState::Dormant;
else if (thread.state == IopThreadState::Running)
thread.state = IopThreadState::Ready;
m_currentThread = nullptr;
cleanupDeadThreads();
}
void IopKernel::cleanupDeadThreads()
{
for (auto thread = m_threads.begin(); thread != m_threads.end();)
{
if (thread->second.state != IopThreadState::Dead)
{
++thread;
continue;
}
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)
thread.state = IopThreadState::Dead;
}
}
}
+103
View File
@@ -0,0 +1,103 @@
#pragma once
#include "iop_cpu.h"
#include <cstddef>
#include <cstdint>
#include <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);
IopMemory &m_memory;
std::map<int, IopThread> m_threads;
std::map<int, Semaphore> m_semaphores;
std::map<int, EventFlag> m_eventFlags;
uint32_t m_nextThreadId = 1;
uint32_t m_nextSemaphoreId = 1;
uint32_t m_nextEventFlagId = 1;
IopThread *m_currentThread = nullptr;
};
}
+371
View File
@@ -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;
}
}
+91
View File
@@ -0,0 +1,91 @@
#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]] 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;
};
}
+751
View File
@@ -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();
}
}
+43
View File
@@ -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,195 @@
#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
{
if (m_memory.read32(pc) != 0x03E00008u)
return std::nullopt;
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,50 @@
#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]] 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:
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;
}
}
}
+32
View File
@@ -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;
}
}
}
+26
View File
@@ -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;
}
}
}
+26
View File
@@ -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;
}
}
}
+20
View File
@@ -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;
};
}
+830
View File
@@ -0,0 +1,830 @@
#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();
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;
}
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;
if (const auto import = imports.decode(cpu.pc))
{
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);
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 && !pendingDmaInterrupts.empty())
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 || pendingDmaInterrupts.empty())
return;
servicingDmaInterrupts = true;
std::vector<int> completed;
for (auto it = pendingDmaInterrupts.begin(); it != pendingDmaInterrupts.end();)
{
if (it->second > totalCycles)
{
++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);
for (const auto &[irq, completionCycle] : pendingDmaInterrupts)
nextWake = std::min(nextWake, completionCycle);
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;
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());
}
}
+49
View File
@@ -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;
};
}
+15
View File
@@ -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> &sections,
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(&section, 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(&section, 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 &section : 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 &section : 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);
};
}
+349
View File
@@ -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;
}
}
+78
View File
@@ -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;
};
}
+180
View File
@@ -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);
}
}
+49
View File
@@ -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;
};
}
+48
View File
@@ -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>>;
}
+277
View File
@@ -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;
}
}
+10
View File
@@ -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);
}
+138
View File
@@ -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);
}
}
+69
View File
@@ -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);
}
}
+404
View File
@@ -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, &parameter, 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, &parameter, 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 &parameter)
{
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 &parameter)
{
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);
}
}
+123
View File
@@ -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));
}
}
+22
View File
@@ -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);
}
}
+237
View File
@@ -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;
}
}
+315
View File
@@ -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
+338
View File
@@ -0,0 +1,338 @@
#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 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())
return 1;
std::cout << "ps2xIOP import tests passed\n";
return 0;
}
+170
View File
@@ -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);
}
+9 -9
View File
@@ -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()
-59
View File
@@ -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
+18 -5
View File
@@ -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;
};
}
@@ -0,0 +1,43 @@
#ifndef PS2RECOMP_CONTROL_FLOW_ANALYZER_H
#define PS2RECOMP_CONTROL_FLOW_ANALYZER_H
#include <cstdint>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace ps2recomp
{
struct Function;
struct Instruction;
struct Section;
class RecompilerReporter;
struct ControlFlowAnalysisResult
{
std::unordered_set<uint32_t> entryPoints;
std::unordered_set<uint32_t> externalEntryPoints;
std::unordered_set<uint32_t> resumeEntryPoints;
std::unordered_set<uint32_t> indirectFallbackEntryPoints;
std::unordered_map<uint32_t, std::vector<uint32_t>> jumpTableTargets;
};
class ControlFlowAnalyzer
{
public:
ControlFlowAnalyzer(const std::vector<Section> &sections,
const std::unordered_map<uint32_t, std::vector<uint32_t>> &configuredJumpTableTargetsByAddress,
RecompilerReporter *reporter);
ControlFlowAnalysisResult analyze(const Function &function,
const std::vector<Instruction> &instructions,
const std::vector<Function> *allFunctions = nullptr) const;
private:
const std::vector<Section> &m_sections;
const std::unordered_map<uint32_t, std::vector<uint32_t>> &m_configJumpTableTargetsByAddress;
RecompilerReporter *m_reporter = nullptr;
};
}
#endif // PS2RECOMP_CONTROL_FLOW_ANALYZER_H

Some files were not shown because too many files have changed in this diff Show More