Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
c235b8a
Bind sceCdLayerSearchFile to the host CD file search
AlenDavid Oct 18, 2026
89bda68
Load sceSifLoadModuleBuffer images from IOP RAM before EE RAM
AlenDavid Oct 18, 2026
c511e48
Add sceSifSearchModuleByAddress backed by the IOP module table
AlenDavid Oct 18, 2026
1b87317
Share one first-fit IOP arena between modules and heap allocations
AlenDavid Oct 18, 2026
be7d152
Format vfprintf into guest newlib string FILEs and read va_list as do…
AlenDavid Oct 18, 2026
cde9283
Return the previous priority from ChangeThreadPriority
AlenDavid Oct 29, 2026
d660bd7
Keep async callback stacks below a top-of-RAM main thread stack
AlenDavid Oct 29, 2026
e405abd
Warn when an EE thread exits by reaching pc=0
AlenDavid Oct 29, 2026
c9fc41a
Read sceCdReadIOPm sectors into IOP RAM
AlenDavid Oct 29, 2026
41ded1f
Implement sceDmaRecvN as a normal-mode receive
AlenDavid Oct 29, 2026
133c51e
Synthesize wrappers for configured stubs the function scan missed
AlenDavid Oct 29, 2026
3e9bf09
Treat DMAC addresses with bit 31 set as scratchpad offsets
AlenDavid Oct 29, 2026
e077bbb
Read PMFHL/PMTHL words from the matching HI/LO pipeline registers
AlenDavid Oct 30, 2026
3f369e4
Place the second sceGsSetDefDBuffDc frame buffer before the Z buffer
AlenDavid Oct 30, 2026
6726540
Honor libgraph image packet layout and transfer packing in sceGsExecL…
AlenDavid Oct 30, 2026
f84cc3f
Emulate the IPU for intra MPEG decoding and colour conversion
AlenDavid Oct 30, 2026
0982658
Serve libpad2 sockets and libvib actuator calls from host input
AlenDavid Oct 30, 2026
ee2ad90
Test the full 64-bit register in sign-comparing branches
AlenDavid Oct 31, 2026
55b73e5
Return the thread id from SuspendThread and ResumeThread
AlenDavid Oct 31, 2026
5b34b0d
Send the swap clear only when the dbuff GIF tag includes it
AlenDavid Oct 31, 2026
29880dd
Emulate SIO2 and an 8 MB memory card image on the IOP
AlenDavid Oct 31, 2026
c609e55
Emulate IPU VDEC/BDEC and pace toIPU, scratchpad chain and stall DMA …
AlenDavid Oct 31, 2026
818ab03
Make generated-function call tracing opt-in via PS2X_TRACE_CALLS
AlenDavid Oct 31, 2026
0140d1b
Cache decoded IOP import stubs instead of rescanning for the table on…
AlenDavid Oct 31, 2026
52149f5
Trim per-instruction and per-slice IOP interpreter overhead
AlenDavid Oct 31, 2026
9358125
Run the IOP in 2048-cycle batches from guest checkpoints
AlenDavid Oct 31, 2026
49514cb
Speed up IPU CSC input, macroblock output, sparse IDCT rows and toIPU…
AlenDavid Oct 31, 2026
7bd7746
Hoist GS pixel, sprite texel and image-upload position work out of pe…
AlenDavid Oct 31, 2026
c47e88b
Leave the DMA tag QWC to Terminate in sceGifPk HLE stubs so Sofdec st…
AlenDavid Oct 31, 2026
194d407
Carry CT24/CT16/CT32 pixels that straddle host-to-local image chunks
AlenDavid Oct 31, 2026
c6753c8
Test CT24 macroblock strip uploads through a GIF REF chain
AlenDavid Oct 31, 2026
b963656
Complete SPU2 AutoDMA transfers at the 48 kHz playback rate instead o…
AlenDavid Oct 31, 2026
4cd7282
Keep an IOP thread running across emulator slice ends until it blocks…
AlenDavid Oct 31, 2026
25f16ad
Find a jump table's bound check beyond 30 instructions while the inde…
AlenDavid Oct 31, 2026
d5162ec
Read the SQRT.S operand from ft as the R5900 encodes it
AlenDavid Oct 31, 2026
3629b9d
Let IOP time pass while an RPC handler waits on an event flag, semaph…
AlenDavid Oct 31, 2026
6a4f364
Emulate SPU2 voices, sound RAM, DMA and AutoDMA on the IOP and play t…
AlenDavid Oct 31, 2026
40a709d
Reach the memory card over SIO2: dmacman DMA, a DualShock 2 on port 1…
AlenDavid Oct 31, 2026
727eef4
Rasterize batched GS primitives in row bands on worker threads, with …
AlenDavid Oct 31, 2026
43188f3
Stop SPU2 AutoDMA when its channel's STR or the core's ADMAS bit is c…
AlenDavid Oct 31, 2026
f0b37a5
Draw GS primitives that sample the top byte of their own RGB24/Z24 ta…
AlenDavid Oct 31, 2026
1952136
Inline GS pixel writes, resolve texel fetchers per primitive and addr…
AlenDavid Oct 31, 2026
145e05b
Run the GS CPU backend on its own thread so the EE keeps running whil…
AlenDavid Oct 31, 2026
b019f7f
Let presentation show what the GS thread has drawn instead of waiting…
AlenDavid Oct 31, 2026
a51042a
Add PS2X_ENABLE_LTO to build Release targets without interprocedural …
AlenDavid Oct 31, 2026
5fdfdf0
Run IOP threads that outrank an RPC server's thread before executing …
AlenDavid Oct 31, 2026
9b9bc1b
Hold an EE RPC while an IOP thread stopped inside CpuSuspendIntr stil…
AlenDavid Oct 31, 2026
1477c73
Batch GS primitives that sample their own RGB24 target into one paral…
AlenDavid Oct 31, 2026
d282cd7
Answer IOP sceCdReadClock so memory card saves get real timestamps
AlenDavid Oct 31, 2026
e5f35ca
Fetch the four texels of a bilinear GS sample with one page lookup an…
AlenDavid Oct 31, 2026
cd663fe
Present the frame the GS thread captured at the latest vblank instead…
AlenDavid Nov 1, 2026
01c07f7
Rasterize with all cores but two by default since 3D scenes are GS fi…
AlenDavid Nov 1, 2026
b7a994d
Log guest vblanks and host frames per second when PS2X_PERF_LOG is set
AlenDavid Nov 10, 2026
b0ba2a8
Keep switch jump table labels inside their function instead of treati…
AlenDavid Nov 10, 2026
df1e783
Re-anchor a vblank that fires more than a period late so a slow stret…
AlenDavid Nov 10, 2026
dd5ff81
Match libvu0 operand order in sceVu0MulMatrix and use xyz only in Nor…
AlenDavid Nov 10, 2026
8b4e181
Count GS primitives, batches, flush reasons, pixels and GS thread tim…
AlenDavid Nov 10, 2026
ca39f66
Split GS batches into 8-row bands taken on demand and reuse band text…
AlenDavid Nov 10, 2026
b4e4230
Rasterize batched textured triangles in row chunks bounded by their e…
AlenDavid Nov 10, 2026
e694d31
Test and mark queued GS page spans as word masks instead of bit by bit
AlenDavid Nov 10, 2026
352f3fe
Skip queued primitives outside a band before their per-band setup
AlenDavid Nov 10, 2026
9bb909c
Record the repeated reads of a filtered texel quad in constant time
AlenDavid Nov 10, 2026
d4f9935
Draw small self-sampling GS batches as one band so palette loads from…
AlenDavid Nov 11, 2026
1368cf0
Let the GS thread queue the next batch while the raster workers draw …
AlenDavid Nov 11, 2026
303117a
Clear GS framebuffers on the raster threads and hand out the heaviest…
AlenDavid Nov 11, 2026
6149ea7
Implement sceGsSetDefClear so offscreen targets get cleared before ga…
AlenDavid Nov 11, 2026
fdc9a75
Write batched GS spans with colour and depth formats fixed at compile…
AlenDavid Nov 11, 2026
a60ad57
Inline the tracked-batch undo log append
AlenDavid Nov 11, 2026
99bdd58
Blend batched GS spans four pixels at a time with NEON
AlenDavid Nov 11, 2026
2278c45
Sample batched textured sprites a row chunk at a time and write them …
AlenDavid Nov 11, 2026
ec32f4f
Decode the delay slot of a branch in a function's last word even when…
AlenDavid Nov 11, 2026
e22a617
Pass argc/argv to IRX entries, implement look_ctype_table and expand …
AlenDavid Nov 11, 2026
46d4967
sceGszbufaddr: read psm/w/h from a0-a2 and count one buffer only in i…
AlenDavid Nov 11, 2026
0a8b9b6
sceVu0ScaleVector: take the scale from f12 only so a 0.0f scale zeroe…
AlenDavid Nov 11, 2026
1a20e00
sceVu0NormalLightMatrix: normalize light xyz only and keep w out of t…
AlenDavid Nov 11, 2026
9d6ab15
sceVu0RotTransPers(N): mode 0 converts all lanes with FTOI4, nonzero …
AlenDavid Nov 11, 2026
ef27296
sceVu0ClipScreen/ClipScreen3/ClipAll: return libvu0's sticky sign/zer…
AlenDavid Nov 11, 2026
f3ce70b
libgraph stubs: read arguments 5-7 from t0-t2 only instead of falling…
AlenDavid Nov 11, 2026
d65f213
sceGsSetDefDrawEnv2: truncate FRAME_2 FBW to w >> 6 like libgraph
AlenDavid Nov 11, 2026
988ff60
sceGsSetDefDBuffDc: assign frame pages, dithering and clear packets t…
AlenDavid Nov 11, 2026
3cb1d13
sceGsSetDefDispEnv/SetDefDBuffDc: write PMODE, SMODE2, DISPLAY and BG…
AlenDavid Nov 11, 2026
787d4a7
sceGsResetGraph: call SetGsCrt and honour mode 5 instead of sending d…
AlenDavid Nov 11, 2026
bb89569
sceDmaSend/SendN/RecvN: keep the channel's CHCR bits instead of forci…
AlenDavid Nov 11, 2026
5325cbd
rand/srand: implement newlib's 31-bit LCG instead of the host's 15-bi…
AlenDavid Nov 11, 2026
216de10
sceGsPutDrawEnv: send the NLOOP A+D pairs behind the GIF tag instead …
AlenDavid Nov 11, 2026
2561fa9
Present the whole frame buffer in interlaced field mode instead of li…
AlenDavid Nov 11, 2026
813c01e
Discover handler-table functions laid out at a fixed stride in data
AlenDavid Nov 11, 2026
cf78823
Present the vblank capture whenever one exists for the requested tick
AlenDavid Nov 11, 2026
1a50c19
Count black frames and audio ring underruns and drops in the perf log
AlenDavid Nov 11, 2026
fb50e22
Register block starts after unconditional transfers as resume entries…
AlenDavid Nov 11, 2026
d4b4b05
Resample host audio toward a 100 ms queue instead of skipping or star…
AlenDavid Nov 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions ps2xIOP/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@ add_library(ps2_iop STATIC
src/emulator/core/iop_cpu.cpp
src/emulator/core/iop_kernel.cpp
src/emulator/core/iop_memory.cpp
src/emulator/core/iop_sio2.cpp
src/emulator/core/iop_spu2.cpp
src/emulator/services/iop_module_loader.cpp
src/emulator/services/iop_rpc.cpp
src/emulator/imports/iop_cdvd.cpp
src/emulator/imports/iop_dmacman.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_format.cpp
src/emulator/imports/iop_sysclib.cpp
src/emulator/imports/iop_sysmem.cpp
src/emulator/imports/iop_timrman.cpp
Expand Down Expand Up @@ -62,6 +66,11 @@ if(PS2X_IOP_BUILD_TESTS)
target_include_directories(ps2_iop_import_version_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
add_test(NAME ps2_iop_import_version_tests COMMAND ps2_iop_import_version_tests)

add_executable(ps2_iop_spu2_tests tests/iop_spu2_tests.cpp)
target_link_libraries(ps2_iop_spu2_tests PRIVATE ps2_iop)
target_include_directories(ps2_iop_spu2_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
add_test(NAME ps2_iop_spu2_tests COMMAND ps2_iop_spu2_tests)

endif()

install(TARGETS ps2_iop
Expand Down
7 changes: 7 additions & 0 deletions ps2xIOP/include/ps2x/iop/iop_host.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ namespace ps2x::iop

virtual void audioCommand(uint32_t sid, uint32_t function, GuestBuffer send, GuestBuffer receive) = 0;

// Mixed SPU2 output: interleaved stereo 16-bit frames at 48 kHz, produced in emulated time.
virtual void audioOutput(const int16_t *samples, size_t frames)
{
(void)samples;
(void)frames;
}

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;
Expand Down
2 changes: 2 additions & 0 deletions ps2xIOP/include/ps2x/iop/iop_subsystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ namespace ps2x::iop
[[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);
// Returns the id of the physical IRX whose image contains the IOP address, or -1.
[[nodiscard]] int32_t findModuleByAddress(uint32_t iopAddress) const noexcept;
void runEeCycles(uint64_t eeCycles) noexcept;

[[nodiscard]] RpcAbi selectRpcAbi(const RpcAbiRequest &request) const;
Expand Down
10 changes: 8 additions & 2 deletions ps2xIOP/src/emulator/core/iop_cpu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ namespace ps2x::iop::detail
{
scheduleLoad(reg, value, scheduledLoad, scheduledReg, scheduledValue);
};
// LWL/LWR merge into a value still in the load delay slot, so an lwl/lwr pair
// assembles one word even though the register has not been written yet.
auto unalignedLoadBase = [&](uint32_t reg)
{
return cpu.pendingLoad && cpu.pendingLoadReg == reg ? cpu.pendingLoadValue : cpu.gpr[reg];
};
auto overflowAdd = [&](int32_t lhs, int32_t rhs, uint32_t reg)
{
const int64_t result = static_cast<int64_t>(lhs) + rhs;
Expand Down Expand Up @@ -356,7 +362,7 @@ namespace ps2x::iop::detail
{
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];
const uint32_t old = unalignedLoadBase(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]));
Expand All @@ -377,7 +383,7 @@ namespace ps2x::iop::detail
{
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];
const uint32_t old = unalignedLoadBase(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]));
Expand Down
67 changes: 60 additions & 7 deletions ps2xIOP/src/emulator/core/iop_kernel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ namespace ps2x::iop::detail
m_nextSemaphoreId = 1;
m_nextEventFlagId = 1;
m_currentThread = nullptr;
m_maybeDeadThreads = false;
}

bool IopKernel::dispatchThreadImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle)
Expand Down Expand Up @@ -107,6 +108,7 @@ namespace ps2x::iop::detail
if (m_currentThread != nullptr)
{
m_currentThread->state = ordinal == 9 ? IopThreadState::Dead : IopThreadState::Dormant;
m_maybeDeadThreads = m_maybeDeadThreads || ordinal == 9;
cpu.stopped = true;
cpu.yielded = true;
}
Expand Down Expand Up @@ -471,6 +473,38 @@ namespace ps2x::iop::detail
}
}

bool IopKernel::eventFlagReady(int id, uint32_t bits, uint32_t mode) const
{
const auto event = m_eventFlags.find(id);
return event == m_eventFlags.end() || bits == 0u || eventSatisfied(event->second, bits, mode);
}

bool IopKernel::hasReadyAtOrAbove(int priority) const noexcept
{
for (const auto &[id, thread] : m_threads)
{
if (thread.state == IopThreadState::Ready && static_cast<int>(thread.priority) <= priority)
return true;
}
return false;
}

bool IopKernel::hasCriticalSectionInProgress() const noexcept
{
for (const auto &[id, thread] : m_threads)
{
if (thread.state == IopThreadState::Ready && thread.sliceEndedWithInterruptsOff)
return true;
}
return false;
}

bool IopKernel::semaphoreReady(int id) const
{
const auto semaphore = m_semaphores.find(id);
return semaphore == m_semaphores.end() || semaphore->second.current > 0;
}

bool IopKernel::eventSatisfied(const EventFlag &event, uint32_t bits, uint32_t mode)
{
if (bits == 0u)
Expand Down Expand Up @@ -671,19 +705,25 @@ namespace ps2x::iop::detail

IopThread *IopKernel::beginNextReady(uint64_t currentCycle)
{
IopThread *next = nullptr;
bool resumeCriticalSection = false;
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)
if (thread.state != IopThreadState::Ready || resumeCriticalSection)
continue;
if (thread.sliceEndedWithInterruptsOff)
{
next = &thread;
resumeCriticalSection = true;
continue;
}
// Among equal priorities the interrupted thread goes first; one that merely became ready
// (e.g. its DelayThread expired) must not cut into it.
if (next == nullptr || thread.priority < next->priority ||
(thread.priority == next->priority && thread.id < next->id))
(thread.priority == next->priority &&
(thread.sliceEnded != next->sliceEnded ? thread.sliceEnded : thread.id < next->id)))
next = &thread;
}
if (next == nullptr)
Expand All @@ -709,16 +749,26 @@ namespace ps2x::iop::detail

void IopKernel::endTimeslice(IopThread &thread, uint32_t returnSentinel)
{
thread.sliceEnded = false;
thread.sliceEndedWithInterruptsOff = false;
if (thread.cpu.pc == returnSentinel || thread.cpu.stopped)
thread.state = IopThreadState::Dormant;
else if (thread.state == IopThreadState::Running)
{
thread.state = IopThreadState::Ready;
// A yield (e.g. RotateThreadReadyQueue) gives way; running out of emulator budget does not.
thread.sliceEnded = !thread.cpu.yielded;
thread.sliceEndedWithInterruptsOff = thread.sliceEnded && m_memory.interruptControl() == 0u;
}
m_currentThread = nullptr;
cleanupDeadThreads();
}

void IopKernel::cleanupDeadThreads()
{
if (!m_maybeDeadThreads)
return;
m_maybeDeadThreads = false;
for (auto thread = m_threads.begin(); thread != m_threads.end();)
{
if (thread->second.state != IopThreadState::Dead)
Expand All @@ -738,7 +788,10 @@ namespace ps2x::iop::detail
{
const uint32_t pc = IopMemory::physicalAddress(thread.cpu.pc);
if (pc >= base && pc < base + size)
{
thread.state = IopThreadState::Dead;
m_maybeDeadThreads = true;
}
}
}
}
22 changes: 22 additions & 0 deletions ps2xIOP/src/emulator/core/iop_kernel.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ namespace ps2x::iop::detail
uint32_t waitMode = 0;
uint32_t waitResultAddress = 0;
int wakeupCount = 0;
// The emulator ended this thread's slice although the thread could still run. The IOP kernel does
// not time-slice: such a thread keeps the CPU until it blocks or a higher-priority thread becomes
// ready, and with interrupts suspended (CpuSuspendIntr) not even that can preempt it.
bool sliceEnded = false;
bool sliceEndedWithInterruptsOff = false;
};

class IopKernel
Expand All @@ -67,6 +72,21 @@ namespace ps2x::iop::detail
void terminateThreadsInRange(uint32_t base, uint32_t size);

[[nodiscard]] size_t threadCount() const noexcept { return m_threads.size(); }
[[nodiscard]] bool inThread() const noexcept { return m_currentThread != nullptr; }
// Priority of the thread now running, or -1 outside any thread.
[[nodiscard]] int currentPriority() const noexcept
{
return m_currentThread != nullptr ? static_cast<int>(m_currentThread->priority) : -1;
}
// Whether a thread that would run before one of `priority` is ready (lower value = more urgent;
// without time-slicing an equal-priority thread that already runs keeps the CPU).
[[nodiscard]] bool hasReadyAtOrAbove(int priority) const noexcept;
// Whether the emulator stopped a thread mid-run while it held interrupts off (CpuSuspendIntr).
[[nodiscard]] bool hasCriticalSectionInProgress() const noexcept;
// Whether a WaitEventFlag / WaitSema issued now could return without blocking (unknown IDs count as
// ready so the regular import reports the error).
[[nodiscard]] bool eventFlagReady(int id, uint32_t bits, uint32_t mode) const;
[[nodiscard]] bool semaphoreReady(int id) const;

private:
struct Semaphore
Expand All @@ -93,6 +113,8 @@ namespace ps2x::iop::detail

IopMemory &m_memory;
std::map<int, IopThread> m_threads;
// Set whenever a thread may have become Dead; endTimeslice runs every slice and must not walk all threads.
bool m_maybeDeadThreads = false;
std::map<int, Semaphore> m_semaphores;
std::map<int, EventFlag> m_eventFlags;
uint32_t m_nextThreadId = 1;
Expand Down
Loading