This document is written for a PS2 developer reviewing the MX4SIO work in SMS (Simple Media System) for a bounty. It explains what the bug actually was, why the fix is the correct one (not a workaround), and how to reproduce the build. Every claim below is grounded in the shipped source; file:line references are given so the reviewer can check each one against the tree.
MX4SIO is a microSD-over-SIO2 adapter that the modern PS2SDK exposes as a mass:
BDM block device. SMS could mount an MX4SIO card and list directories, but
playback hard-stalled the moment a large file read was issued. The root cause
is in the mx4sio_bd IOP driver: its read path uses unbounded busy-waits and a
no-timeout DMA-completion wait, and it holds the SIO2 bus across a long in-ISR
re-arm chain. Under the heavily-loaded SMS IOP, a single large multi-block read
stalls; the thousands of tiny reads that directory listing and format-probing
perform always survived because their chains are only 1–2 sectors.
The fix has two halves, both shipped:
- Driver hardening (
patches/mx4sio_bd-sms.patch, applied to ps2sdk'siop/sio/mx4sio_bd, rebuilt into the embeddedirx/mx4sio_bd.irx): every SIO2 busy-wait is bounded, the no-timeout DMA wait is replaced by a bounded poll-based watchdog, and multi-block reads are chopped into small batches (SPISD_MAX_BATCH= 8 sectors / 4 KiB) with correct sector advance on a partial read. - EE-side access pattern (
src/SMS_FileContext.c):mass:reads are issued to the device inMX4SIO_RD_CHUNK-sized (4 KiB) pieces — one safe driver batch per read, with the iomanX/SIF round-trip between reads giving the SD card its inter-command gap. This is the same shape as the small-read pattern that was always reliable.
Verified on real hardware: a 640×480 XviD/MP3 AVI and an MP3 both play smoothly off an MX4SIO microSD; file-switch re-launch, settings persistence, and a clean first boot all work.
Built on the project's pinned CI toolchain ps2dev/ps2dev:v1.0 against Krah's
BDM base (commit f716b3a); the patched mx4sio_bd.irx is rebuilt with the
current (mipsel-none-elf) IOP toolchain and embedded via bin2c.
SMS embeds its IOP modules and loads them at boot (SMS_IOPReset /
SMS_IOPInit in src/SMS_IOP.c). For the MX4SIO/BDM build the load order is:
iomanx → filexio (+ fileXioInit) → bdm → bdmfs_fatfs
→ [sio2man] → mcman → mcserv → padman
→ usbd → usbmass_bd → mx4sio_bd
(src/SMS_IOP.c:248-260 for the core stack; usbd/usbmass_bd in
SMS_IOPStartUSB, mx4sio_bd in SMS_IOPStartMX4SIO at :386-474.)
In this stack:
mx4sio_bd.irxis the block device (microSD over the SIO2 controller bus);bdm.irxis the Block Device Manager,bdmfs_fatfs.irxthe FAT/exFAT layer;- the card surfaces as
massN:— the same abstraction OPL / NHDDL / wLaunchELF use.
Because MX4SIO shares the SIO2 bus with the controller (padman) and memory card
(mcman), SMS swaps the legacy rom0:SIO2MAN for the PS2SDK sio2man so the
driver can hook it; that swap is what forces the libpad/libmc moves discussed in
§7. The card is detected by checkConnectedMassDev() (src/SMS_IOP.c:363-376),
which fioDopens each massN: unit.
Why this only broke in SMS. The same driver works in OPL/NHDDL/wLaunchELF. Those are file managers: they read in small pieces and the IOP is otherwise idle. SMS keeps the IOP continuously busy during reads (SPU/audio, GUI, pad polling, mcman/padman all resident) and a media player streams large buffers. The driver defects below only surface under exactly those conditions, which is why prior attempts got the card mounting but stalled on playback.
The card mounts and directories list, but opening a media file stalls. The decisive evidence is the small-read-vs-large-read split:
- Directory listings and format detection (a few sectors at a time) — always worked.
- A large single multi-block read (a streaming media buffer) — stalled.
That split points squarely at the driver's multi-block read path, not at the EE file API or the filesystem layer.
(a) Unbounded busy-waits, including in the ISR. The original driver waited
for each SIO2 transfer to finish by spinning on a status bit with no bound —
including inside the DMA-completion interrupt handler mx_sio2_dma_isr_rx, where
interrupts are masked:
while ((inl_sio2_stat6c_get() & (1 << 12)) == 0)
;If a transfer stalls (bus contention while the host polls the pad/MC), that spin
never returns; in the ISR it strands the whole IOP, and the EE then hangs waiting
on it. The patch replaces every such spin with the bounded mx_sio2_xfer_done()
helper (patches/mx4sio_bd-sms.patch, mx4sio.c hunks): a real transfer
completes in microseconds, far under the cap, so the bound only ever fires on a
genuine stall — turning a permanent freeze into a recoverable, signalled error.
(b) No-timeout DMA-completion wait over a long in-ISR re-arm chain. A read is
interrupt-driven: the SIO2 DMA ISR re-arms the next sector from interrupt
context, and the worker thread blocked on WaitEventFlag with no timeout. A
single large transfer (hundreds of sectors) is therefore one long,
uninterruptible in-ISR re-arm chain; under load a single late or dropped SD data
token strands the worker on that wait forever. Small reads survived because their
chain is only 1–2 sectors. See the in-code rationale at
refs/ps2sdk/iop/sio/mx4sio_bd/src/spi_sdcard_driver.c:611-639 (the
SPISD_MAX_BATCH block) and :432-442 (the watchdog block).
These two stack: even with the busy-waits bounded, a single large read holds the SIO2 lock across many internal batches with stale FIFO state and still stalls. Both have to be addressed.
A reviewer's first question is "is this a real fix or a band-aid?" The honest answer: both halves are real fixes, and the EE-side chunking is the correct access pattern for this device, not an avoidance hack.
Applied to ps2sdk's iop/sio/mx4sio_bd; the result is the embedded
irx/mx4sio_bd.irx. Three changes:
- Bound every SIO2 "transfer complete" busy-wait with
mx_sio2_xfer_done()(mx4sio.c), including the in-ISR ones. On timeout the RX ISR setscmd.abort = CMD_ABORT_NO_READ_TOKENand signals the event flag so the read aborts cleanly instead of hanging the IOP. - Replace the no-timeout completion wait with a bounded watchdog.
spisd_read_multi_donow pollsPollEventFlagin a loop (READ_WATCHDOG_POLLS×READ_WATCHDOG_POLL_US≈ 300 ms cap) and clears stale signal bits before each transfer; a missing DMA interrupt becomes a recoverableNO_READ_TOKENabort routed through the existing card-reset/retry path (spi_sdcard_driver.c:444-516). AddsI_PollEventFlagtoimports.lst. - Batch multi-block reads.
spisd_readsplits a request intoSPISD_MAX_BATCH-sector batches, keeping each in-ISR re-arm chain short and self-terminating, and — fixing a latent bug — advances the sector number (not just the buffer pointer) on a partial read (spi_sdcard_driver.c:641-714, theb_sector = b_sector + resultsline).
SPISD_MAX_BATCH is 8 sectors (4 KiB) in the shipped driver
(spi_sdcard_driver.c:630-639). 16 KiB/32 KiB batches still stall under SMS
load (a late token strands the longer chain); 8 sectors matches the size of the
directory/4 KiB reads that were always reliable.
mass: files are opened through fileXio (the native BDM/IOMANX API), tracked
by a per-file m_fXio flag (SMS_FileContext.c:1219-1226, set in
STIO_InitFileContext at :1618-1620). Every read to a mass: fd is then
issued to the driver in MX4SIO_RD_CHUNK = 4 KiB pieces:
STIO_Fillchunk loop:SMS_FileContext.c:1338-1352.STIO_Stream(the streaming path) chunk loop:SMS_FileContext.c:1452-1485.- the chunk constant + rationale:
SMS_FileContext.c:14-23.
This is the right pattern for the device, for two reasons:
- One read = one safe block-transfer the driver reliably services (4 KiB = one 8-sector batch). There is no oversized single transfer to stall on.
- The iomanX/SIF processing between reads gives the SD card its natural inter-command gap — exactly the rhythm of the thousands-of-small-reads pattern that was always smooth. The chunk loop reproduces that rhythm on purpose rather than fighting it.
4 KiB is the empirically-confirmed reliable ceiling on hardware; 8/16/32 KiB single reads stall.
These were tried and rejected — included to answer "did you try X?":
- fileXio instead of fio. Switching the EE API alone did not fix it; the
stall lives below the EE file API (in bdmfs/fileXio's large-transfer path and
the driver), so the choice of
fiovsfileXiodoes not by itself decide it. (The shipped code does usefileXioformass:— but paired with chunking, as the combination is what works.) - Shrinking the driver batch + per-batch SIO2 (re)locking. Reducing batch size and taking the lock per batch did not make a single large EE read succeed on its own; the large transfer still stalled.
- Larger EE chunk sizes (8/16/32 KiB). All stalled on hardware. 4 KiB is the ceiling.
Each is small and independently justified.
- fioSync guard on
mass:stream reset (SMS_FileContext.c:1431-1436). The legacySTIO_StreamanBlocks == 0reset callsfioSync(FIO_WAIT,…)+fioSetBlockMode(FIO_WAIT).mass:reads are synchronousfileXiocalls with no asyncfioop pending, and that legacy sync could stall after heavy use, surfacing as a "Loading indices" hang on file-switch re-launch. The reset now skips the fio sync/blockmode form_fXiofds. - Settings load/save coherence (
SMS_Config.c). Save wrote viafioto themc0:path, but load read via libmc (MC_GetDir/MC_OpenS) — incoherent on the modern iomanX + mcman stack, so settings appeared to save but never loaded.SMS_LoadConfignow reads viafiofrom the samemc0:path it was written to (SMS_Config.c:291-313), so settings persist. - libmc card-detection retry (
_mc_get_info,SMS_Config.c:188-200). The sio2man swap moved the memory card onto libmc, whosemcGetInforeturns a transient "card changed" status on the first query after init. A single query intermittently looked like "no card" and skipped save/load; the code now retriesMC_GetInfo/MC_Syncuntil the status stabilises (used by bothSMS_LoadConfigandSMS_SaveConfig). - Clean-boot default (
SMS_Config.c:211). With no saved settings,m_NetworkFlags = 0, so no device auto-starts at boot (no more forced HDD). The user enables HDD/USB/MX4SIO/network from the menu, persisted once saved (auto-start gates atSMS_IOP.c:602-607).
MX4SIO is a first-class device in the UI, distinct from USB:
- Enable / autostart entry. The device menu carries an "Autostart MX4SIO"
toggle (
SMS_GUIMenuSMS.c:194-208, handler_automx4sio_handlerat:1150-1156, flagSMS_DF_AUTO_MX4SIO). The BDM device menu is sized to 8 items vs 7 in the non-BDM build (:526-530). - "Start MX4SIO support" now entry. Appended dynamically when MX4SIO is not
yet running (
SMS_GUIMenuSMS.c:574-581), wired to_startmx4sio_handler→_start_device(SMS_IOPStartMX4SIO)(:1200-1206,:1168-1186). - Distinct device icon. MX4SIO units are told apart from USB by a
before/after unit-mask delta:
SMS_IOPStartMX4SIOrecords whichmassN:units existed before loadingmx4sio_bd, then setsg_Mx4sioMaskonly for units that appear after (SMS_IOP.c:442-474). In the device-bar renderer, amass:unit whose bit is ing_Mx4sioMaskis remapped from device-id 0 (USB) to device-id 7 (SMS_GUIDevMenu.c:250-255), which draws the CDDA glyph rather than the USB icon (icon tables_pBrowserDevIcons[7] = s_IconCDDA,SMS_GUIcons.c:1348-1356). Unmount handling treats id-7 as amass:device too (SMS_GUIDevMenu.c:302-308).
Cosmetic re-theme, kept cheap:
- Jellyfish desktop background. A JPEG (
images/jellyfish.jpg, embedded viabin2casjellyfish_jpg, Makefile rule:70-72and object at:40) is decoded once through SMS's existing JPEG decoder (SMS_JPEGInit/SMS_JPEGLoad) into a cached PSMCT32 texture (_DecodeJellyfish,SMS_GUIDesktop.c:225-278) and then blitted full-screen exactly like a skin image (_DrawJellyfish,:323-333). The procedural gradient is kept as a fallback behind the opaque image. - Regenerated credits in
src/About_Data.s(linked viaAbout_Data.o, Makefile:26).
SMB is outside the MX4SIO bounty scope, but it was modernised along the way:
- SMS's SMB was migrated from the legacy custom SMB1 client (
iop/SMSSMB/,SMSSMB.IRX) to the modern ps2sdksmbmanmodule — the same battle-tested SMB1 client OPL uses. It is embedded (smbman_irxin the Makefile) and loaded insrc/SMS_IOP.c(SifExecDecompModuleBuffer(&smbman_irx, …)); it presents an iomanXsmbdevice reached viafileXioDevctl. The legacyiop/SMSSMB/tree is now dead code (not built, not loaded), so the oldSMB_Readread-loop defect noted in earlier drafts is moot. - SMB is now fully GUI-configurable — server, share, user, password and IP are
entered on-screen with an on-screen keyboard (
src/SMS_GUISMBrowser.c), with no pre-authored config file. - The remaining real-world limitation is server-side:
smbmanspeaks SMB1, which modern Windows/Samba disable by default. True SMB2/3 would be a separate client effort, well beyond a "MX4SIO support" bounty.
The embedded IRX are committed in irx/, so the EE app builds without step 1.
# 1) (optional) rebuild the patched MX4SIO driver IRX from a ps2sdk checkout:
cd <ps2sdk>
patch -p1 < <repo>/patches/mx4sio_bd-sms.patch
cd iop/sio/mx4sio_bd && PS2SDKSRC=<ps2sdk> make # -> mx4sio_bd.irx
# copy the result into this repo's irx/mx4sio_bd.irx
# 2) build the EE app on the project's pinned toolchain:
docker run --rm -v "$PWD":/src -w /src ps2dev/ps2dev:v1.0 \
sh -c 'apk add --no-cache build-base && make all' # -> bin/SMS.elfmake embeds the BDM IRX set (iomanx, filexio, bdm, bdmfs_fatfs,
sio2man, mcman, mcserv, padman, usbd, usbmass_bd, mx4sio_bd) and the
jellyfish JPEG via bin2c, and links -lmc -lpadx -lfileXio (Makefile
:42-50). BDM defaults on (BDM ?= 1).
On real PS2 hardware (slim, with HDD + network adapter present):
- AVI playback — 640×480 XviD video + MP3 audio off an MX4SIO microSD (FAT32): smooth.
- MP3 playback — smooth.
- File-switch re-launch — selecting another file after one finishes no longer hangs at "Loading indices" (the §6 fioSync guard).
- Settings persistence — save then reboot reloads the saved config (the §6 load/save coherence + libmc retry fixes).
- Clean first boot — with no saved settings, no device is force-started; the
card is detected and selectable as a
mass:device with its own icon, and directory browsing works.
Diagnosis used temporary on-screen instrumentation (no serial/IOP-TTY on the target); that instrumentation is removed — the shipped code contains only the fixes above.
- Throughput headroom. 4 KiB per read is the confirmed-safe ceiling and is
smooth for the tested media. Raising
SPISD_MAX_BATCH/MX4SIO_RD_CHUNKtogether, once a wider range of cards is confirmed stable, would increase throughput; both are single-point constants by design. - Card breadth. Tested on the author's cards; the bounded watchdog makes a misbehaving card fail soft (logged abort + retry) rather than freeze, but a broad card-compatibility sweep is future work.
- SMB remains the legacy SMB1 stack (§9); modernizing to SMB2 is separate work.
- Eugene Plotnikov — original SMS.
- Krah (KrahJolito) — MX4SIO/BDM integration foundation (BDM module loading, libpad/libmc switch, device-menu wiring).
- El_isra, Ripto (submitter), Berion — additional work and testing.
- This branch — the driver hardening (busy-wait bounds, completion watchdog, read
batching) and the correct EE
mass:access pattern that make playback work, the settings/clean-boot fixes, the MX4SIO GUI integration and icon, the theme, and this document.