PlayStation 2 SIF: EE-to-IOP DMA, RPC Packets, and Cache Ownership
Follow PS2 SIF commands and RPC data across the Emotion Engine and IOP, including DMA completion, cache writeback, server dispatch, and reply lifetime.
The PlayStation 2’s Subsystem Interface (SIF) is the communication path between the Emotion Engine (EE) and the I/O Processor (IOP). It moves command packets and associated data using DMA; higher-level services such as remote procedure call (RPC) build request, dispatch, and reply semantics on top of that transport. An EE call into an IOP library is not a normal function call across a shared stack. It is a cross-processor transaction with buffers, cache state, command identity, completion events, and potentially a sleeping client thread.
The open-source PS2SDK makes these layers visible in its SIF command and RPC implementations. PCSX2’s SIF source models the EE-side transfer state and serializes it in save states. The SDK is strong evidence for the software ABI and sequencing expected by homebrew applications, while an emulator implementation is corroborating evidence for hardware state transitions; neither should be described as a measurement from a physical console.
Transport and service are different layers
At the transport layer, an EE or IOP producer prepares a command packet and asks the SIF DMA interface to move it to the other processor. A command can also associate a larger data transfer with the packet. The command packet identifies what the receiver should do; the attached data is a separate source/destination range. This split means command arrival and payload visibility should be tested separately.
The PS2SDK’s sceSifSendCmd path prepares a DMA transfer for the packet and, when a nonzero extra-data length is specified, another transfer for the associated buffer. It writes back the EE data cache for the packet and conditionally for attached data according to the mode flags. A memory copy performed on the EE does not by itself guarantee that the IOP sees the latest bytes if the source remains dirty in an EE cache line.
At the RPC layer, the packet identifies an RPC endpoint and function number, includes send/receive lengths and buffers, and ties the request to a client context. The IOP registers command handlers, finds the target server by ID, places work on a server data queue, and wakes the server thread. The server executes the function on the IOP, produces a response, and sends an end/reply indication with the return data. The client can wait synchronously or request a nonblocking completion path.
Do not conflate three distinct milestones: the EE DMA engine accepted a transfer; the IOP received and dispatched a command; and the RPC function finished and delivered its result. A transfer ID can report DMA progress, while a semaphore or callback represents a later protocol completion. Treating the first one as “the RPC returned” can let the EE consume a buffer before the IOP has produced it.
Direction, address spaces, and ownership
The EE and IOP are separate processors with separate memory spaces and software state. A pointer meaningful to one processor cannot automatically be dereferenced as a pointer in the other’s address space. SIF DMA describes a source and destination for the transfer; the software interface must select memory visible to the appropriate endpoint and obey the device’s addressing rules. Keep a trace of both endpoint addresses and label which processor owns each buffer.
The PS2SDK headers name the command and status paths in main-processor/sub-processor terms and expose separate DMA operations. Their API names should be interpreted with the EE and IOP role made explicit in the test harness. Avoid copying an address from an EE debugger window into an IOP pointer field without checking whether it is an EE-side DMA source, an IOP-side destination, or a shared protocol pointer that the receiver resolves independently.
Cache ownership is part of the transaction contract. Before transmitting an EE buffer, the SDK may write back dirty cache lines so the transfer engine reads current data. For a receive buffer, software needs the appropriate cache handling before the CPU consumes DMA-written bytes. The exact cache operation depends on transfer direction, API mode, and buffer aliasing. A generic “flush everything after every call” can hide a wrong ownership model while imposing unnecessary cost; tests should assert the specific buffer range and operation.
Buffers also have a lifetime. The EE must not reuse or free a command packet while the SIF command handler may still reference it. An asynchronous RPC’s send data must remain valid until the transfer has consumed it; a receive buffer must remain available through reply DMA completion. Synchronous mode hides some of that lifetime by waiting, but a callback or nonblocking mode makes it the application’s responsibility.
Packet lifecycle and client completion
The PS2SDK allocates fixed-size command and RPC packets from tables, tracks allocation flags, and returns an allocation or send error when a slot or transport operation is unavailable. Its RPC call path writes request fields, optionally performs cache writeback, submits the command, and either waits on a semaphore or returns to the caller for later completion. On the IOP side, command dispatch queues the RPC server record and wakes a server thread if the queue is idle.
The response path is another SIF operation. The server runs the selected function, sets up a reply/end packet, and transfers response bytes or a compact completion packet. The client-side handler updates bind state or calls the end function, signals the semaphore if one exists, and releases the allocated packet. This is why a correct RPC model needs request IDs and packet ownership through the full round trip; it is not sufficient to dispatch a function immediately when the first EE packet arrives.
The SDK also has bind and command initialization phases. RPC initialization installs handlers for end, bind, call, and additional-data commands and coordinates initialization state across the processors. If the IOP reboots, prior service bindings and command handlers can become invalid. sceSifInitRpc checks for an IOP reboot and reinitializes the command path. An emulator should preserve or reset that state in response to the modeled IOP reset instead of retaining stale bound server pointers.
DMA completion versus RPC completion
A low-level SIF DMA request can be polled or waited on through a transfer ID. That completion says the data movement ended; the remote service may still need to parse the packet, execute code, and submit a reply. Conversely, a protocol end callback is meaningful only after the response packet and any response data are visible. Use separate event names and timestamps for DMA begin, DMA finish, command interrupt, server dequeue, function start/finish, reply submission, reply DMA completion, and client wake.
This separation helps diagnose a set of common failures. If the remote sees an old payload, investigate cache writeback or address mapping. If it sees a new packet with a stale RPC ID, inspect packet reuse. If the server never wakes, inspect command registration, server binding, and IOP queue state. If the function finishes but the caller blocks forever, inspect end-packet delivery, semaphore lifetime, and initialization. A single “SIF stalled” message loses all of these distinctions.
A transaction trace for emulator tests
This Python structure records the processor ownership and milestones for one RPC. It is a validation schema, not a hardware packet definition.
from dataclasses import dataclass
@dataclass(frozen=True)
class SifEvent:
cycle: int
actor: str
phase: str
request_id: int
address: int | None = None
size: int = 0
def validate_rpc_trace(events):
previous_cycle = -1
allowed_actors = {"EE", "IOP", "SIF"}
for event in events:
if event.actor not in allowed_actors:
raise ValueError("invalid SIF participant")
if event.cycle < previous_cycle or event.size < 0:
raise ValueError("invalid transfer ordering or size")
if event.request_id < 0:
raise ValueError("request id must be nonnegative")
previous_cycle = event.cycle
trace = [
SifEvent(100, "EE", "rpc-submit", 7, 0x00100000, 32),
SifEvent(104, "SIF", "dma-start", 7, 0x00100000, 32),
SifEvent(120, "IOP", "rpc-dispatch", 7),
SifEvent(160, "EE", "rpc-complete", 7, 0x00110000, 16),
]
validate_rpc_trace(trace)
A real trace should include cache operation ranges, command ID, server ID, RPC function number, transfer IDs, endpoint address spaces, packet-pool slot, queue state, and completion mechanism. Then a test can assert that the EE does not release or reuse a buffer until the corresponding transfer or RPC lifecycle event has completed.
Verification matrix
Test one-way command packets first, then a command with attached data, then a request/reply RPC. Exercise zero-length send and receive buffers, the largest supported packet layout, allocation exhaustion, invalid server binding, and send failure. Verify the expected error path frees the packet slot and does not leave an orphaned semaphore or queue node.
Run synchronous and nonblocking RPC modes. In synchronous mode, prove the client blocks until the reply handler signals completion. In nonblocking mode, confirm the callback runs once, only after the return data is visible, and that the packet can then be safely recycled. Delay the IOP server thread and test multiple queued requests to validate ordering and wakeup behavior.
Poison EE source and receive buffers before a transfer. Modify source data immediately before the SDK cache operation, then verify the IOP receives the fresh bytes. Have the IOP write a known response and verify the EE does not read stale cache contents. Repeat with aligned and intentionally misaligned test buffers only where the documented SDK/API permits; report alignment restrictions instead of hiding them with host allocations.
Reboot the IOP between bind and call, and during a queued request. Confirm the EE detects the new processor state, reinitializes SIF command and RPC handlers, and does not dispatch through a stale server pointer. Save states at the moment of DMA, after command delivery, with an RPC queued, inside the server call, and while the reply is in flight. A restore should reproduce DMA progress, interrupt/queue state, packet ownership, and client completion exactly.
Acceptance criteria
A reliable SIF model separates command transport, associated DMA payloads, IOP server dispatch, RPC execution, reply transfer, and EE client completion. It includes processor-specific address visibility and cache ownership, plus pending packet, DMA, callback/semaphore, and queue state. Its trace can say whether a fault occurred before remote delivery, during IOP execution, or in the response path. That is the standard needed to preserve games and system services that use the IOP as an active subsystem rather than a transparent peripheral.
Related:
- PlayStation 2 VIF and GIF: DMA Paths, Vector Unpacking, and Graphics Handoffs
- PlayStation 2 IPU: Bitstream FIFO, Macroblock Decode, and DMA
Sources: