STATE — status, invariants & known issues
← POPSLoader docs · view on GitHub ↗
Last updated: 2026-07-27 (experimental channel EXP73 — see Experimental channel state (EXP32–EXP73) below, which supersedes the pre-EXP device-layer / cover-art / defaults bullets in the body). Released line: 1.1.0 (2026-07-21); rolling is plain v1.1.1-dev; the experimental channel is v1.1.1-dev-EXP73. EXP73 has also been merged to dev, so rolling and experimental now carry identical code (only the stamp and the translator hand-off files differ). Body below (2026-07-13, 1.0.1-era) reflects the R3Z3N settings-review round 2 — START settings menu, region-native confirm swap, keyboard rework, accordion animation, profiles system removed — on top of the accordion/i18n round 1 and the 2026-06 work. Tip moves per push; see git log.
STATE
This is the canonical status doc for POPSLoader. Current runtime state, behavioral invariants, preservation contracts, known issues, and hardware-verification status all live here; the other docs (README, AGENTS, CONTRIBUTING, ROADMAP, ROLLING_NOTES) point here instead of duplicating, so this is the one place to keep current. QA_REGRESSION_MATRIX.md is the detailed run ledger.
Project Identity
POPSLoader is a PS2 launcher for POPStarter built on Enceladus runtime pieces, with behavior primarily orchestrated by embedded Lua modules (system.lua, ui.lua, images.lua). The Lua is bin2c'd into the EE ELF at build time — so a runtime Lua error (nil global, type error, load-order error) is invisible to luac -p and to CI, and only surfaces on real PS2 / PCSX2. (pops_profiles.lua was removed 2026-07-13 with the profile-preset system.)
Experimental channel state (EXP32–EXP73) — current goal state
This section reflects the experimental channel (
v1.1.1-dev-EXP73; public is 1.1.0, rolling is plainv1.1.1-dev). Where it conflicts with older bullets further down — the "2026-07-20 storage wave" device-layer entry, the pre-EXP cover-art/defaults descriptions — this section wins; those predate the EXP32 device-layer rebuild and are retained only as history.
- Device layer rebuilt (EXP32) to match the working reference launchers (OPL/RiptOPL, wLaunchELF-R3Z, NHDDL). Storage drivers load lazily when a device is engaged (no all-at-boot probing; cwd/
-page=decides what inits) — one exception since EXP61: theata_bdbring-up worker is kicked unconditionally at boot frommain.cpp(see the EXP61 bullet below). The EXP31 freesio2 / freepad SIO2 stack is resident. MMCE and MX4SIO coexist with NO gate — the oldSystem.getSio2OwnerSIO2-owner guard, the "device conflict / restart" dialog, the MX4SIO crash-marker files, and the per-device 6/4-try retry ladders were all deleted (official OPL runs both drivers resident and works; maintainer decision). A legacymasscwd handed over by an older launcher is resolved to its real device by devctl driver-name (sdc/mx4⇒ MX4SIO). APA (PFS) and exFAT-internal coexist — no gate between them. - Internal exFAT warm-up (EXP32/33, superseded by EXP61): brought up on a boot-time EE worker (
System.initATAAsync). As of EXP61 the kick is unconditional and comes frommain.cpp(KickAtaAsyncBoot(), right afterEnsureUsbMass()) — no longer gated on the Internal-HDD setting, no longer kicked bydo_boot_init.EnsureAtaBdmis serialized by a binary semaphore (created before the worker spawns) plus a Lua cascade bound inLoadHDDModules(waits screen-alive forinitATAStatus() ~= 1, then reports not-ready instead of falling through — EXP61), so the boot warm-up and the APA page can't double-loadata_bdand re-reset the live ATA bus (EXP33 — the CosmicScale APA-Jail 42% class). INVARIANT preserved: never load BDM-atad synchronously at page time; BDM drivers stay a single matched vintage. - EXP61 — the ata_bd load window moved back to boot, asynchronously. Root cause of the EXP55–60 wedge (sAGA's SCPH-30004, internal exFAT, stuck at
exFAT 1c: status 1, 10.0s [iop128k=ok], drive never ready across re-entries): the byte-identicalata_bd.irx(same md5 in both builds) succeeds loaded at boot into a lean IOP (EXP22 and the 2026-07-18 rolling release — both HW-confirmed on his console) and wedges loaded mid-session onto a full IOP (freesio2/freepad, ds34, audsrv, mcman, USB stack). The worker stayed1forever becauseSifExecModuleBuffernever returned; the page's 10s poll gave up but nothing ever completed the load. EXP61 kicks the SAME async worker from C at the exact boot point rr0718 loaded synchronously — the leanest IOP this build ever has — so the EXP24 verdict holds (no 5–6s serial boot cost; the probe runs behind the remaining boot work). Driveless consoles are unaffected (ata_bdself-exits; the resident dev9/bdm/bdmfs footprint is the one rr0718 shipped unconditionally). MX4SIO/MMCE paths untouched. Also in EXP61:LoadHDDModulesno longer falls through intoHDD.Initialize()while the worker is still running (that was a full-UI-freeze via theEnsureAtaBdmsema — it now reports "The internal drive is still starting" and returns). Hardware verification PENDING (sAGA). If it still wedges, the next lever is a bounded join under the splash, not a mid-session load. - EXP62 — join the boot kick, bounded: the EXP61 black-screen fix. EXP61 moved the ata_bd load back to boot but fired the worker and raced ahead into the main thread's ds34usb/ds34bt/audsrv
SifExecModuleBuffercalls -- two EE threads inside one SIF RPC client that is not thread-safe. On sAGA's SCPH-30004 that wedged the boot itself (HDD spins up = worker loading dev9; flash = initGraphics; steady black = a corrupted SIF call never returns). EXP62 adds a worker-done semaphore andWaitAtaAsyncBootBounded(8000)inmain.cppright afterKickAtaAsyncBoot(), before the ds34 loads: serial, like the rr0718-proven window, but capped -- a wedged load now costs the exFAT page's "still starting" report (the EXP61 not-ready path) instead of the console.KickAtaAsyncBootdrains any stale done-token before a (re)spawn; the Lua binding and page-entry behavior are unchanged. Trade-off accepted: drive-present boots pay up to ~2s of serial wait (what rr0718 always cost); driveless boots pay only the worker's two 1s settle sleeps. Hardware verification PENDING (sAGA). - EXP63 — verify drive readiness, SMS-proven window (sAGA's 15s-boot clue). sAGA noticed the working rr0718 build boots ~15s; SMS's
SMS_IOPStartATAdoes the same on purpose (~10s of post-load delay before scanning).EnsureAtaBdmno longer trustsata_bd's return: after the load it polls the BDM mass slots for theataunit via the same GET_DRIVERNAME ioctl the classifier uses (exact"ata"match), 500ms steps, ~10s bound, early-exit on first sight. A no-show is now a REAL failure:g_ata_bd_loadedis cleared so the page-entry retry re-runs the full bring-up -- a self-exited ata_bd re-probes the now-spun-up drive on a fresh instance, and a still-resident copy fails the reload without re-running_start(no double bus reset in practice). Boot join cap raised 8s -> 20s to contain the window. Driveless-fat consoles pay the full ~13s once per boot (bounded, then the page reports not-ready); driveless slims still skip fast atEnsureDev9. Hardware verification PENDING (sAGA). - EXP64 — the spin-up wait moves under the splash (maintainer feedback: EXP63 boot too long). The worker is split in two: the MODULE chain (dev9/bdm/bdmfs/ata_bd) still joins serially at boot (SifExecModuleBuffer cannot race the ds34/audsrv loads -- the EXP61 class), ~3-5s; the EXP63 verification is pure fileXio polling (no module loads) and now runs CONCURRENTLY with the splash/intro. New
g_ata_async_modules_semasignals module-phase completion;WaitAtaAsyncBootBounded(8000)joins only that phase. Lua-facing states are unchanged (1 running / 2 ok / 3 fail), so the page-entry cascade, the MX4SIO bound and the "still starting" report all behave as before. Expected UX: instant intro on every rig; a slow drive shows "still starting" if the user opens the page inside the first ~10s, then just works on re-entry. Hardware verification PENDING (sAGA). - EXP65 — the unified theory, applied: NOTHING storage-related runs concurrently with anything else (SMS-proven). EXP64's boot-time verification raced the boot's module loads and Lua traffic (maintainer: MC boot, black screen, MX4SIO light stuck ON) -- the same SIF race class as EXP61, one level down. Combined with SMS's
SMS_IOPStartATA(serial, main-thread, ATA+MX4SIO+MMCE coexisting) the pattern is now treated as the cause of the WHOLE saga: the driver was never the problem; concurrent SIF use was. The boot worker is MODULES-ONLY (bounded join, EXP62/64 shape); the exFAT page NEVER spawns the worker -- it loads modules serially on the main thread via the newSystem.initATAModules(STILL_STARTING -> not-ready, retry on re-entry), and drive readiness comes from the sweep itself under a raised ata retry budget (10 passes, ~10s, "retrying" reports narrate the spin-up). Boot does ZERO storage probing (the MX4SIO light stays off).g_ata_bd_loaded= "module resident" again; verification clears it on no-show so retries re-probe. Harness fixtures updated to the new contract (39/39). Hardware verification PENDING (maintainer + sAGA). - EXP66 — the async worker is DELETED; boot does zero ATA work (maintainer: EXP65 still black-booted). The EXP65 8s module join released the main thread into the still-loading worker on rigs where ata_bd's
_startis slow (spinning-up drive) -- the EXP61 race with a shorter window. With three async variants dead the same way, the experiment is over: SMS's shape is the answer.main.cppno longer kicks or joins anything ATA; the worker thread, its semas,KickAtaAsyncBoot,WaitAtaAsyncBootBounded,initATAAsync, andinitATAStatusare all removed. The exFAT page does the whole bring-up in the foreground on the main thread:System.initATAModules(serial, load-once), then the sweep under the 10-pass retry budget narrates the spin-up, and an exhausted sweep calls the newSystem.clearATAso a self-exited ata_bd gets a fresh probe on the next entry.EnsureAtaBdm(sync, modules+verify) remains for the APA/PFS path. Boot cost is zero on every rig. Harness 39/39. Hardware verification PENDING (maintainer + sAGA). - EXP67 — two EXP66 regressions fixed (maintainer HW). (1) APA/PFS HDD not detected: EXP63's
VerifyAtaDriveReadyhad leaked into the SYNCEnsureAtaBdm(the APA path), where it demands a bdmfs-visibleataunit that an APA/PFS-only drive never publishes -- every APA bring-up failed. The sync path is modules-only again (pre-EXP63 semantics); readiness stays an exFAT-sweep concern only. (2) Dual-USB listed one drive:BuildUsbIdentityDeferredearly-returned on the first non-empty pass, so a second stick mounting a beat later never made the list. It now keeps probing while the root set is growing and settles when a pass adds nothing (single-drive users pay <=1 extra second). Harness 39/39. Hardware verification PENDING (maintainer). - EXP68 — the load window IS the variable; the chain loads at boot again (sAGA's EXP67 photos). EXP67's serial main-thread page load HUNG at "1a: loading the driver" (iop128k=ok) on his 4TB rig -- so the mid-session wedge is not a race at all. Final form of the evidence: same ata_bd bytes COMPLETE at boot (rr0718, 15s, HW-proven) and never return mid-session, on any thread arrangement. The worker + bounded join is restored (modules only, 20s cap, before the ds34 loads -- the EXP62 shape that booted the maintainer's rig at ~5s), keeping EXP67's page narration/sweep, clearATA retry, APA fix and dual-USB fix. Per-module BootStamps (dev9/bdm/bdmfs/ata_bd) land on the Credits ledger so any future hang names its module from one photo.
initATAStatusis back for the mx4sio/APA cascade bounds; the page's STILL_STARTING guard returns with it. Harness 39/39. Hardware verification PENDING (sAGA) -- this is the rr0718 configuration with instrumentation. - EXP69 — the winners' recipe: dev9 early, ata_bd lazy (maintainer rejected EXP68's extended boot). EXP68's bounded boot-chain join "worked" but was crude, and the launchers that read sAGA's drive don't do it: SMS (
SMS_IOPStartATA), OPL/RiptOPL (dev9 in the boot module set, atad/ata_bd later), R3Z and NHDDL all keep dev9 + the FS layers early (fast, no probing) and load ata_bd -- the only drive-touching piece -- lazily. Reframed by that, the EXP67 "exFAT 1a" hang reads as dev9 loading mid-session into a live USB block stack (SPEED-chip conflict class), not ata_bd at all. EXP69 loadsEnsureDev9()at boot beforeEnsureUsbMass()(~100ms, stamped), deletes the boot-chain kick/join, and leaves ata_bd to the page's serialinitATAModules(resident parts no-op) + narrated sweep + clearATA retry -- the exact SMS arrangement, with EXP67's instant boot kept.EnsureDev9is non-static now (main.cpp calls it). Harness 39/39. Hardware verification PENDING (sAGA). - EXP70 — Adaptive (auto) BDMA always stages USBEXFAT for USB (maintainer directive 2026-07-25). The exFAT BDMA pair reads FAT32 too, so one variant plays every USB stick and nobody has to guess the filesystem.
ResolveAdaptiveBdmaTarget("USB")now returns USBEXFAT unconditionally -- the saved-mode gate is gone; FAT32/no-BDMA (POPStarter's built-in USB stack) is a MANUAL-only choice (BDMA Mode = FAT32 with Adaptive OFF). Harness T5/T10 updated to the new contract (39/39). RiptOPL already behaves this way at launch (bdmsupport.c's device-type switch always equips usbexfat for BDM_TYPE_USB under auto-apply). - EXP71 — cover art reads EXACTLY one path per device (maintainer directive). Mass devices:
<device>:/ART/<gamefilename>_COV.png. APA/PFS only:hdd0:__common/POPS/ART/<gamefilename>_COV.png. The disc-marker-stripped family name is dropped from both branches: tried first, it could shadow exact per-disc art (and every extra candidate is a full dir-chain walk on a big ART folder). Partition-installed APA entries still name art after the partition minus its 3-char prefix (POPStarter's own convention). Harness 39/39. - EXP72 — game details via a worker-side .txt channel. DID NOT WORK; superseded by EXP73. Kept here because it shipped to
experimentalanddevand testers ran it. The intent was right (read the sidecar on the cover worker's thread so details show with or without a cover, preserving T35's zero-render-IO invariant) but the channel was never wired:CoverCache:BeginLoad— the path every first-time cover load takes — never calledGraphics.coverLoadTextPath, and the cover-less text-only job never setself.pending, soCoverCache:Pump(which early-returns on a nil pending) never collected it. It also regressed games that HAVE cover art, because the same commit deleted the EXP54 read that had been serving them. 39/39 stayed green throughout: T35 only asserts zero blocking render-thread reads, which code that reads nothing satisfies trivially. - EXP73 — game details actually reach the screen, and the i18n reachability sweep. Four fixes to the details channel:
BeginLoadnow always sets the companion text path (clearing it with""when the game has none, so a leftover path cannot ride the next job and show the wrong game's blurb), the text-only job setsself.pendingso it is polled, the cover-miss branch collects the sidecar, and the already-cached-cover path issues its own job instead of returning early.lua_coverloadtextpathnow refuses whileCOVER_LOAD_BUSY— it had beenfree()ing the exact pointer the worker was blocked on insidefopen. The retry path drains an abandonedDONEresult rather than being refused by it forever. Harness T40 asserts the outcome nothing had asserted before (thatlast_descis actually populated: with a cover, without one on both first visit and revisit, and behind a cached cover) and was verified to FAIL on the pre-fix tree before being accepted.
i18n: toasts assembled by concatenation beforeUI.Notif_queue.addwere defeating its add-timePLDR.Land discarding translations that already shipped in all six languages (Failed to load HDDamong them); swept.LAUNCH FAILEDis wrapped, and its screen's hardcoded "Press X/O" now uses the ROM-aware glyph helpers. NewPLDR.LFmttranslates a format template then fills it underpcall, so a translator dropping a%sdegrades to English instead of raising inside the toast reporting the original fault (T41).No games found on hdd0:was re-keyed in all six languages — EXP33 had swapped its static second line for a runtime diagnostic and orphaned the translation.src/luagraphics.cppwas restored to a text file; EXP72 had written three raw NUL bytes into it (a NUL escape was intended), so git treated it as binary and the C half of that commit produced no reviewable diff.
Translator hand-off: both.tsvfiles repaired (two records the exporter had written with a real newline instead of the two characters, which split each across three physical lines; plus rows for strings deleted back in EXP32). oldman63's PR #559 translations are injected intoPLDR.I18N.HU.docs_verifygainedcheck_translation_handoffto fail on both defect shapes. Remember the two gates: a string is translatable only if the draw site callsPLDR.LAND the key exists in the table — the.tsvis a hand-off document, not a build input. - Cover art: ONE fixed folder per device, OPL
<name>_COV.pngonly, exact game filename only. See the EXP71 bullet above; the EXP34 user-selectable folder was removed in EXP35 and the EXP34 folder listing was removed in EXP46-EXP58. - New factory defaults (EXP34; all user-changeable, existing saved settings preserved):
HDD_FS= BOTH (was PFS);ART_LOCATION=art(device-rootART/, OPL layout; waspops_art);HIDDEN_DEVICES=ILINK(the i.Link page ships Hidden); SMB/network = PS2 IP192.168.1.10, gateway/DNS192.168.1.1, server192.168.1.100, sharegames, userguest, port1111(DHCP stays ON). Reset Defaults targets these (Reset == factory-fresh). - Mass-slot identity comes from the SLOT (EXP41). EXP36 resolved each enumerated BDM device to a slot with
mass<parId>:/, on the premise that a block device carries its own mass unit inparId. That premise is false and it was the "MX4SIO page lists the ATA drive's games" bug. In ps2sdk every whole-disk driver hardcodesparId = 0x00(ps2atad.c:361,usbmass_bd/scsi.c:336,IEEE1394_bd/scsi.c:354, mx4siospi_sdcard_driver.c:56); the partition drivers set the MBR partition-TYPE byte (part_driver_mbr.c:126) or0(part_driver_gpt.c:146). So ATA and MX4SIO both resolved tomass:/and were recorded into BOTH buckets;mass:/is slot 0, i.e. whatever connected first. Deterministic, which is why re-scanning never helped. On MBR it instead mapped a FAT32 partition to a nonexistentmass12:/and the device vanished. There is no correct BDM-device→massN mapping (bdm_get_bdexposes BDM's own mount order, unrelated to bdmfs_fatfs's volume index), so the slot is asked directly viaGET_DRIVERNAME. EXP36's anti-stall goal is preserved by thedoesFolderExist()gate, which skips absent/unmounted slots before any ioctl.BuildMassRootIdentityalso returnsidentity.drivers(raw driver name per mounted root) andidentity.bdm_devicesfor diagnosis. Guarded by harness T29, which previously asserted the EXP36 behaviour and passed only because its fixture inventedparId = 1for MX4SIO — a value the real driver never emits. - Cover art is a persisted setting (EXP42).
COVER_ART(Settings → Game List → Cover art, default ON) replaces the session-only Square toggle, which reset every boot and was unadvertised after the footer legend was trimmed. Square is now unbound on the game lists. Applied live throughUI.SetCoverPreview, which carries the old handler's exact side effects (re-arm the deferred load on, drop the decoded selection off). The "No cover. Looked for:<path>" caption is REMOVED along with itslast_cover_probestate (maintainer: "totally useless"; it predates EXP35 hard-locking the cover location). Guarded by harness T32, including the absent-key case — an older sidecar with noCOVER_ART=line must default ON. - THE COVER PATH: settled (EXP46-EXP58). Read this before touching cover art. Four fixes failed because all four attacked probe COUNT, which was never the problem. EXP34/EXP44/EXP46 each tried "list the ART folder once";
System.listDirectoryon the cover folder is now implicated THREE times and must NOT come back (it builds a Lua sub-table per entry, andiomanX_dreadreturns ONE dirent per RPC, so listing a large folder is thousands of blocking round trips). EXP37'sdir_presentgate only fired when the folder was ABSENT, which is never the interesting case. The actual cause was doing the probe on the RENDER thread: a FatFs miss walks the directory to its end with no negative cache, so a large shared-with-OPLART/costs ~0.3-0.6s per newly-selected title. OPL probes MORE than we do with the identical primitive (oneopen(), no readdir, no folder check) and never stutters, purely because its probes run on a worker while the render thread draws a placeholder. - Covers load on a RESIDENT worker (EXP58). Woken by a semaphore, created lazily on first use, never at boot, never exits. EXP49-57 created a thread PER REQUEST whose failure mode was a permanently jammed loader (sAGA's stuck "Loading ART..."). The details
.txtwas the LAST blocking read on the render thread and now rides a successfully-loaded cover (EXP54). INVARIANT: selecting a game performs ZERO blocking file reads -- pinned by harness T30 + T35. External launches DRAIN the worker first (EXP58, T39): an in-flightfopenacross the launch'sSifIopResetis the self-inflicted "polluted parent" statesrc/main.cpp:607guards at boot, and DKWDRV is the sensitive consumer. - Devices resolve by TYPED SDK NAME, not a mass slot (EXP55).
fs_driver_resolve_volume'smassbranch returns the requested unit VERBATIM with no mounted-device check, over indices handed out first-free across ALL device types -- raw connection order. That is why the MX4SIO page could list the ATA drive. The typed branch matches the MOUNTED bd'spath, somx4sio0:can only ever be an MX4SIO volume.BuildMassRootIdentityprobes<typed><unit>:/for units 0-3 first and SKIPS the legacy walk on a hit; the walk survives only as a fallback (a typed device does not exist until the first device of its type connects). Units are a per-path ORDINAL recomputed per call -- never persist one. Pinned by T36; hardware-confirmed, each page lists its own device. - Cover art layout is FIXED, one location, no fallback (EXP56).
device:/POPS/<game>.VCD->device:/ART/<game>_COV.png; APA/PFS is the sole exception athdd0:__common/POPS/ART/<game>_COV.png. The device prefix is matched with%w+:/-- the old%a+%d*:could not express a device whose NAME contains a digit, somx4sio0:ALONE fell through todevice:/POPS/ART/and MX4SIO covers were unfindable. Pinned by T37 across every device name. - Hidden state must not leak (EXP57).
CleanupGameListclearedPLDR.GAMESbut leftPLDR.HIDDEN, and every fresh scan calls through it -- so hidden marks survived across scans AND devices and were written into whichever.gamecachesaved next (sAGA found anHline for a game with no.hidesidecar). Pinned by T38. HIDE_TEXTdefaults ON (EXP56, graphics team) as a proper default-ON boolean: the parse site applies the sidecar value only when the key is PRESENT, so existing installs keep their choice. Same idiom asBOOT_SOUND/COVER_ART.- Internal exFAT (sAGA's 4TB drive): STILL FAILING; hardware confirmation of the current arrangement PENDING. EXP61 through EXP68 tried async, bounded-join and boot-chain variants and all were abandoned. EXP69 is the shipped shape:
EnsureDev9()early at boot frommain.cpp,ata_bdleft to the exFAT page's serialSystem.initATAModulesplus the narrated sweep andSystem.clearATAretry. Read the EXP69 bullet above, not EXP61. It stops in the driver bring-up, NOT the scan. Step 1 reports 1a (pre-load +iop128kheadroom), 1b (initATAAsyncreturn), 1c (live status + elapsed, 1 Hz), 1d (final code + elapsed). The EXP57/EXP59 photos (1c: status 1at the 10s give-up,iop128k=ok) plus the byte-identical driver pinned it: the load window, not the driver, not IOP RAM — mid-session full-IOP load wedges, boot-time lean-IOP load works (EXP22 + rr-0718, both his console). DEAD THEORIES, do not re-test:** EXP40's phantom-slave disable did not help his drive; the 4TB-capacity idea is refuted (our vendoredps2atadhandles capacity identically to reference ps2sdk -- bothu32 total_sectors, both clamp >32-bit to0xffffffff); probe-count reduction is refuted for the cover path; IOP-memory exhaustion is refuted (iop128k=okon the freeze line). - PROCESS NOTE, earned the hard way (2026-07-23). Every fix that worked came from a measurement or from diffing against something that works (BETA-12, OPL, reference ps2sdk). Every failure came from reasoning about a plausible mechanism: five wrong cover fixes and one bricked boot (EXP51 -- the harness is a LUA gate and cannot execute EE C, so it passed on a build that would not boot). Two of the sharpest finds came from a tester reading his own
.gamecache. Instrument first; guess last. - The exFAT freeze channel is back (EXP43). EXP11 split the silent "Locating exFAT HDD POPS folder... 42%" step into numbered sub-steps so a frozen screen names the stuck call; the EXP32 device-layer rebuild dropped it, and its absence is why EXP38, EXP39 and EXP40 each spent a hardware round and produced no evidence.
BuildMassRootIdentity/BuildBoundedIdentityDeferred/GetATAMassRootNow/InitATAPopsRootnow take an optionalreport(msg)and paint before each call that can block: step 1 (starting the drive), step 2.N (checkingmassN:, thedopen), step 3.N (identifyingmassN:, the devctl), step 4 (reading the device list). A hung IOP call never returns to repaint, so whatever is on screen IS the diagnosis, from one photo. The reporter is optional at every level, so non-UI callers are unaffected. Guarded by harness T33, verified to FAIL against the pre-EXP43 tree. - APA scans self-diagnose (EXP33/34):
PLDR.HDD.SCAN_DIAGcounts partitions/files/VCDs and why a counted VCD yields zero games (hidden via Global Hide vs multi-disc-collapsed); the "no games" toast prints it (e.g.1 part, 5 files, 1 VCD (1 hidden — Global Hide is on)), so an accidentally-hidden game reads as hidden, not broken. - i.Link (
ILINK) remains intentionally unimplemented and is now hidden by default — show it under Settings → Device List to see the "not implemented" notice. - Regression net:
tools/host_harness.pyruns the realsystem.lua/ui.luaunder Lua mocks — 53/53 (device-classification, coexistence, cascade-bound, APA-diagnostics, hidden-count and config-defaults tests, plus T29 slot-identity-not-parId, T32 COVER_ART persistence, T33 the exFAT freeze channel, from EXP73 T40 game details actually reach the screen and T41PLDR.LFmtsurvives a malformed translation, T42 L3 hides a game while Hidden games = Hidden, T43IPCONFIG.DATis never deleted/synthesised and nothing is defaulted, T44 the POPSTARTER.DATare read as well as written, T45SMBCONFIG.DATround-trips so hand-added credentials and blank guest lines survive a write, T46 the SMB page reports a missing memory card and offers to install a missing pack, T47 a launch is blocked (naming the empty fields) when POPSTARTER lacks required values, T48 Adaptive BDMA defaults ON with-bdma=<mode>pinning the variant, and T49-page/-moderouting driven for real in a second Lua runtime (the layer had ZERO coverage and had regressed three times unnoticed), T50 an ATA bring-up failure reports its status instead of guessing "reformat the drive", and T51 ONE BDM device-kind-label resolver with ata probe-only at boot; T34 was removed with the EXP44 revert). T40 and T42-T53 were each verified to FAIL on the pre-fix tree before being accepted (T49 against a deliberately reintroduced 2026-06-09 defect) — a test that cannot fail is not a regression net, which is how EXP72 shipped broken at a green 39/39.
Repo-Verified Runtime State
Boot and runtime
- Every
.irxin the launcher's own directory is loaded at boot (LoadIrxFromDir(APP_DIR_LOCAL),system.lua), in directory order, with anIRX/subfolder as the fallback when the app dir has none. No allow-list, no logging of what was picked up. This is deliberate (drop a driver beside the ELF to load it) and it is also a footgun: on 2026-07-28 a strayilink_bd_assault.irxleft in the maintainer's Downloads folder from June was auto-loaded, initialised the i.Link controller and polled a FireWire status register PCSX2 only stubs, hanging the emulator indefinitely (31,742 reads over 77s) while real hardware was unaffected. It was alphabetically first, so it hung before the other strays even loaded. When an emulator hangs but hardware is fine, check the boot folder for loose.irxfiles before suspecting a code change. Documented for users in README > Loading extra IRX drivers. - Boot/runtime uses embedded Lua scripts (
etc/boot.lua→system.lua→ui.lua). bin/POPSLDR/IMG/default.pngis an optional legacy cover override (MakefileOPTIONAL_EMBEDDED_RSC, gated on$(wildcard …/default.png)): when absent from the checkout it is simply not embedded, and there is no fallback —IMG_FALLBACKSis empty (images.lua). The game-list cover box uses the now-required embeddedcover_default.png(+cover_missing.pngoverlay), notdefault.png, for its no-cover / preview-off states.MISSING.pnghas been removed (no longer embedded, registered, or referenced anywhere — see Cover art)._ps2sdk_memory_init()insrc/main.cppperforms an IOP reset beforemain()runs (RESET_IOP=1Makefile default); it also runsSifExitRpc(), freshSifInitRpc(0), andfileXioExit()first to detach any inherited RPC client from a parent that leftfileXioloaded (e.g. wLaunchELF). Architecture/revert history:docs/archive/LAUNCH_HYGIENE.md.Timer.getTime()returns MICROSECONDS on PS2, not milliseconds. It is the rawclock() - tickdelta (src/luatimer.cpplua_time);CLOCKS_PER_SECis1e6on the EE toolchain. The canonical Enceladus idiom for any per-frame rate-limit (nav auto-repeat, description scroll) is therefore frame-counting (one counter increment per vblank-paced frame), not reading the wall clock.os.clock()(stock Lua, returns seconds) is the only pre-converted Lua time source and is currently unused. With the µs-as-ms sweep complete (9c3f64f+a8e61f3), the remaining time-based UI sites are unit-correct or frame-paced: PathEditor key-flash + caret blink and the launch-watchdog label divide to real ms (Timer.getTime()/1000), the scene/boot fades and carousel slide advance a fixed step per vblank-paced frame, and the deadMIN_ACTION_MSaction debounce was removed (rising-edgepressed = GPAD & ~OLDPADis the real gate). Only cosmetic/inert sites (busy-overlay throttle, saving marquee, disabled debug log) intentionally remain on the raw clock. See Known Open Work #4.
Settings (single-device parity)
- Settings persist at
PLDR.SETTINGS_PATH, resolved at load time byLoadSettingsNonFatal: the per-device sidecarAPP_DIR_LOCAL/.pldrs(the directory POPSLOADER.ELF lives in) is preferred for every device. - HDD installs now save settings ON THE HDD boot partition itself.
PLDR.HDD.EnsureBootPartitionWritabletakes over the launcher's boot pfs mount — explicitly unmounts it, then remounts the same partition read-write at the same pfs slot ("own your mount", the OPL pattern) — so the.pldrssidecar is written on-HDD. There is nomc0:fallback for an HDD-cwd install. Single-device parity with USB / MX4SIO / MMCE. - This supersedes the old PR #466 design ("HDD saves to
mc0:/POPSTARTER/.pldrsbecause the bundledps2hdd-osd.irxcan't reliably write PFS"). That premise no longer holds. - STATUS: implemented, boots on PCSX2; provato confirmed the HDD is RW-writable on real hardware; the full settings flow is still validating on hardware (not yet broadly hardware-confirmed).
mc0:/POPSTARTER/.pldrsremains only as a legacy fallback when no sidecar can be computed.- Settings edits are staged and committed on save/leave. Inside Settings, START opens the Save Changes / Reset Defaults / Discard & Exit modal menu (they are no longer inline rows — R3Z3N review round 2). Circle with unsaved edits opens that SAME menu (round 3: "the back button prompt is different than the start prompt ... they should be the same"); the old X-Save/O-Cancel/Triangle-Don't-Save prompt is gone. Circle with nothing staged leaves immediately. The one deliberate difference is the save target: Circle exits to the scene it came from and tolerates a failed save (the user was already leaving), START stays put on failure so drafts survive a retry.
- Credits live in Settings → About → Credits (round 3, "put credits in settings imo"); the Triangle→Credits binding and its footer entry are gone from every scene. Footers are now the primary action plus the way out only: main menu = Select/Settings/Exit, game list = Launch/Back, Settings = Select/Menu/Back. The dropped bindings still work where they did (Square toggles cover art, START opens Settings) — only the legend clutter went. Select no longer toggles Hide-Text on the Settings page (it still does on the device list and game lists); use Display → Hide UI Text there.
- Persisted settings — 24 keys (
EncodeSettings,system.lua):POPSTARTER_PATH,BDMA,BDMA_ADAPTIVE,DKWDRV_PATH,STRICT_HDD_PREEXEC_GATE,VIDEO_STANDARD,HIDE_TEXT,KEYBOARD_LAYOUT,LANGUAGE,BOOT_PAGE,MULTIDISC_COLLAPSE,GLOBAL_HIDE,POPSTARTER_MC_FOLDER,HIDDEN_DEVICES,SHOW_DETAILS,DETAILS_ALIGN,ART_LOCATION,HDD_FS,COVER_ART,GAMELIST_CACHE,BOOT_SOUND,OVERSCAN,SMB_MODULES— then the SMB connection block (SMB_*) is appended after these bySmbAppendLines.PROFILE=andPOPSTARTER_MODE=are no longer written (2026-07-13, profiles dropped):POPSTARTER_PATH=""= Automatic. On load, a legacy file'sPROFILE=Npreset pick (N>=2) is migrated intoPOPSTARTER_PATHas that preset's absolute path (the Automatic ladder doesn't probe every old preset location — adversarial-review finding);PROFILE=1(the relative default) lands on Automatic. The first save persists the migrated value and sheds the legacy keys.
Per-game hide layer
- A
<name>.hidesidecar next to the game's.VCDmarks it hidden (read for free during the scan; tracked inPLDR.HIDDEN). L3 toggles hide/show on the selected game. To unhide, set Settings → Game List → Hidden games to Visible (manage), which shows hidden games dimmed so L3 can toggle them back (Hidden filters them out). R3 is a transient, session-only reveal (712fc90): on a device game list it temporarily overridesGLOBAL_HIDE(the saved value is cached inPLDR._GLOBAL_HIDE_SAVED) and rebuilds the list in place (reusing the R1 refresh path) — revealing hidden games dimmed (then L3 unhides) or re-hiding them. R3 never persists; the persisted setting belongs to the Settings page, and leaving the page (or entering Settings, which queues a rebuild for the return) restores it. - In-app hide writes work on every device page — USB / MX4SIO / MMCE / Memory Card and the internal HDD. On HDD the
.hideis written via the RW mount take-over (EnsureBootPartitionWritable); the "add the.hidefile from a PC" message is now only a write-failure fallback, not the primary path. - STATUS: implemented, boots on PCSX2, validating on hardware.
BDMA mode / POPSTARTER memory-card folder
- BDMA mode (mass-storage backend) keys:
FAT32/USBEXFAT/MX4SIO/MMCE/ATA(internal-HDD exFAT, aliasHDDEXFAT). The installed mode is recorded in a marker file in the POPSTARTER pack folder:bdma_mode.txt(plain-text mode key). Renamed 2026-06-17 from.pldr_bdma_modefor a clearer, shared name (mSAS reads the same file); the legacy.pldr_bdma_mode/.pldr_bdmanames are still read for back-compat, and the current name is always written. - POPSTARTER Memory Card Folder toggle (Settings → Memory Card): disabling it deletes
mc:/POPSTARTER(destructive-action confirm). BDMA ⟺ folder interlock: the folder cannot be disabled while BDMA mode is on, and BDMA cannot be enabled while the folder is off. (With Adaptive BDMA the interlock extends: the folder cannot be disabled while Adaptive is on, and Adaptive cannot be turned on while the folder is off.) - Adaptive BDMA (Settings → Storage, under BDMA Mode;
PLDR.BDMA_ADAPTIVE, persistedBDMA_ADAPTIVE=, default Off; issue #509): stages the BDMA variant for the launched game's device at launch time (PLDR.MaybeApplyAdaptiveBdmainRunPOPStarterGame, so-page=/-game=auto-launches are covered): exFAT-HDD page →ATA(scene-checked; that page masquerades as USB), MX4SIO →MX4SIO, MMCE / MMCE-translated SMB →MMCE; HDD (PFS), net-SMB and DKWDRV are left untouched. Under Adaptive, the USB page always stagesUSBEXFAT(EXP70, maintainer directive 2026-07-25): the exFAT pair reads FAT32 too, so one variant serves every stick and the old saved-mode gate is gone (ResolveAdaptiveBdmaTarget,system.lua). FAT32 / no-BDMA (POPStarter's built-in stack) is a MANUAL-only choice: BDMA ModeFAT32with Adaptive off. Equipped check before any write (PLDR.IsBdmaModeEquipped): already-correct card = zero MC writes. Staging runs after the cheap launch validations (a blocked launch costs no card writes); a staging failure cancels the launch with a warn toast (the card may hold wrong/half-written modules, and post-exec nothing can render a notice — cancelling returns to the menu where it shows). While Adaptive is on, the boot-time marker reconcile no longer overrides the savedBDMA=preference (bdma_mode.txtthen tracks the last launch, not the chosen mode; format unchanged — mSAS reads it); turning Adaptive off restages the chosen mode in the same save (equipped-gated, zero-write when the card already matches) so the marker re-converges to the preference instead of silently adopting the last launch. STATUS: implemented 2026-07-09 + review fixes, not hardware-tested (needs an MMCE+USB rig — the reporter's setup).
Video standard
- Video Standard: Auto (default — matches the console region) / NTSC / PAL. On PAL the UI now renders natively at 640×512 so it fills the screen (no letterbox); NTSC is 640×448. The display-change confirm prompt auto-reverts if not confirmed (like OPL); hold START during boot to skip past a bad video mode; the boot screen is centered. STATUS: PAL hardware validation pending.
Input / navigation (UI.Pad.Listen in ui.lua)
- Nav auto-repeat is frame-counted, not wall-clock. In
resolve_nav:nav_fps = (UI.SCR.Y >= 512) and 50 or 60;NAV_DELAY_FRAMES = ceil(nav_fps*0.6)(~0.6 s);NAV_RATE_FRAMES = ceil(nav_fps*0.2)(~5/s). A per-directionUI.Pad.NavHoldFramescounter increments once per frame; a press fires immediately, held UP/DOWN repeat after the delay then at the rate, LEFT/RIGHT are edge-only. This replaced an earlier wall-clock scheme that readTimer.getTime()as ms (µs in reality) and flew (one click ≈ 5 lines). CONFIRMED on hardware (oldman63). - Analog-stick → d-pad fold is gated on real analog mode + per-axis hysteresis. The left stick is folded into the d-pad direction bits (
UI.Pad.StickV/StickHlatches) only whenPads.getMode()reportsPAD_ANALOGorPAD_DUALSHOCK; the latch asserts at|v| > 64, releases at< 40, so a deadzone-parked stick can't dither. When not analog this frame the latches are cleared. The new C bindingPads.getMode()(src/luacontrols.cpplua_getmode) returns the live negotiated mode viapadInfoMode(port, 0, PAD_MODECURID, 0); the pre-existingPads.getType()(lua_gettype,PAD_MODETABLE) reads a capability-table entry and is not usable for this gate. Mirrors OPLsrc/pad.c. WHY: an ungated fold injected a phantom −127 on a digital pad and broke up/down nav. CONFIRMED on hardware (oldman63). - Description right-stick scroll is frame-counted (
UI.GameList.DescScrollFrames): one step everyceil(_secs * fps)frames, fixed at the Fast pace (_secs0.15 s, ~7 lines/sec). The Fast/Medium/Slow speed setting was removed (provato: Fast is best); the frame-counting fix it shipped with stands. - Region-native confirm button (2026-07-13, R3Z3N round 2): at boot
system.luareadsrom0:ROMVER; byte 5 =='J'(Japanese ROM) setsPLDR.CONFIRM_CIRCLE, and CIRCLE becomes confirm / CROSS cancel. Single flip points: theUI.Pad.Listenmap (every scene consumes abstractCONFIRM/BACKevents) plus theUI.Confirm*/Back*glyph helpers — the footer legend (semanticconfirm/backorder tokens, resolved inResolveLegend), the modalX:/O:hints (UI.Modal.ButtonHint), the composed blocking-prompt hints (UI.PadHintPair, word-level i18n keys) and the path-editor discard warning all follow. Probe failure keeps the Western cross-confirm default. Footer legends draw the confirm action FAR LEFT on every page, back/exit at the right edge. STATUS: implemented, not hardware-tested (needs a Japanese-ROM console for the swap + any Western console for no-change). - Settings page, R3Z3N round 2 (2026-07-13): Save/Reset/Discard live behind START (modal list menu; the inline action rows are gone); section headers render in their own warm-gold color and read-only info rows render shades darker than selectable rows; accordion sections open with a ~6-frame top-down reveal (frame-counted); "Hide UI Text" values read On/Off. On-screen keyboard: number row first, opens UPPERCASE with the cursor on the first letter row, the in-keyboard layout strip is removed (Settings → Startup → Keyboard Layout is the one chooser), the Case/Symbols label names the state R2 switches to, and a held CONFIRM keeps the key visually pressed for the whole hold. STATUS: implemented, not hardware-tested.
Boot sound
- Boot sound On/Off setting (default On;
PLDR.BOOT_SOUND,system.lua) gates the splash chime (embed:boot.adp, played inui.lua'sTryBootSound). Plumbed through the full settings chain incl.CommitSettingsChanges(next_boot_sound→next_state.boot_sound) and persisted as theBOOT_SOUND=key. CONFIRMED saving + surviving reboot on hardware (oldman63).
Overscan (CRT inset)
- OPL-style render-coordinate inset:
Screen.setOverscan(permille)/getOverscan()(src/luaScreen.cpp) drive a C-core scale-toward-center transform (OVX()/OVY(),set_overscan/get_overscaninsrc/graphics.cpp) applied at the singlegsKit_prim_*draw chokepoint; the math is identical to OPLrmSetOverscanand is the identity at permille 0, so it is inert by default. Exposed as the Overscan (CRT inset) live adjuster in Settings (±5 step, live preview, discard restores), persisted asOVERSCAN=. STATUS: not yet CRT/HW-eyeballed.
Boot-context resolution
- Single canonical resolver
ResolveBootContext()insystem.luacombines the C-side argv[0] classification hint (main.cpp detectBootDeviceHintFromArgv0(), viaSystem.getBootDeviceHint()), Lua-side prefix matching (mass/mmce/mx4sio/pfs/hdd/smb/host/usb/ata/apa), and the mx4siomass:fix (classify_mass_bootvia BDM driver lookup +.boot_mx4sio/.boot_usbmarkers).DetectBootDevice(),PLDR.GetBootContext(),PLDR.GetBootKind(),ComputeSettingsSidecarPathall read this one resolver.
Launch arguments (NHDDL-style)
parseLaunchArgs()inmain.cpprecognizes-page=*,-mode=*(NHDDL alias),-game=*,-debug; exposed viaSystem.getLaunchArgs()and normalized intoPLDR.LAUNCH_ARGS = {page, page_raw, game, debug}.-page=(and-mode=alias) drives carousel auto-nav viaNormalizeLaunchPage:ata/ata0/ataN→ the exFAT page (opt 3,GBDMHDD);hdd/hdd0/apa/apa0/anypfs→ the PFS page (opt 4);mmce→1,mx4sio→2,usb→5; barebdmais a no-op page value.-game=triggersPLDR.AutoLaunchFromLaunchArgs()(requires-page=; HDD game formatPARTITION|relpath, USB/MX4SIO/MMCE formatFILE.VCD; falls through to the menu with an error toast on failure);-debugqueues a boot-context toast (PLDR.SurfaceLaunchArgsDebug()).
Backend init / runtime
- Startup backend auto-init uses the boot path and the configured executable paths (POPSTARTER/DKWDRV). HDD startup targets run
PLDR.LoadHDDModules(). - USB vs MX4SIO classification is by mount-driver identity:
mass:/boots stay USB-only unless explicit MX4SIO evidence (mx4sio:/prefix,sdc/mx4ioctl, or.boot_mx4sio).mx4sio_bd.irxloads only on that evidence;usbmass_bd.irxalways loads first (mx4sio depends on it). Runtime device access is not gated by the old device-lock system (thecanEnterDevice/setDeviceLocksubsystem was removed).
Launch paths (current routing)
- HDD POPSTARTER on HDD partition (D-10):
LoadELFFromFileExecPS2RebootIOPWithPartition→ExecuteHddBackedViaEmbeddedLoader→ child loaderis_hdd_partition_contextbranch (fileXioUmount + SifExitRpc/Cmd + ExecPS2, no IOP reset). Byte-identical to the 2026-05-22 B2 hardware-passing fix at commit4ae6679. - Non-HDD POPSTARTER + HDD game (D-15): same route with the boot partition's PFS slot preserved via keep_mask.
- DKWDRV from MC: reboot variant direct path, IOP reset + reload
SIO2MAN/MCMAN/MCSERV+ ExecPS2 with synthesized argv0. - DKWDRV from HDD custom path (FIXED, PRs #486/#487): partition-aware path + live pfs-slot scan.
- BOOT.ELF from USB-booted POPSLoader (V2 route at
d23520a): non-reboot variant → BOOT.ELF special-case →ExecuteViaEmbeddedLoadernon-HDD branch (no IOP reset). - BOOT.ELF from HDD-booted POPSLoader (U-10, FIXED): launches with
reboot_iop=0via PR #479. POPSTARTER.ELFresolved per device at launch (PLDR.ResolveLaunchPopstarterPath,system.lua). REMOVABLE devices (USB / internal-exFAT-ATA / MX4SIO / MMCE), in order: 1. an explicit user-configured absolute custom path (the "POPSTARTER Path" setting) when it resolves; 2. the game's own<device>:/POPS/POPSTARTER.ELFwhen it exists; 3.POPSTARTER.ELFin the launcher's own folder (cwd); 4. themc0:→mc1:/POPSTARTER fallback net. The device and cwd steps are existence-gated, so a device with noPOPSTARTER.ELFsimply falls through — this lets a per-device build be used without forcing it (e.g. a USB-delay POPSTARTER dropped in the USB drive'sPOPS/folder serves USB games, a faster build elsewhere). INTERNAL-PFS HDD (APA), in order: 1. custom; 2.hdd0:__common/POPS/POPSTARTER.ELF; 3. cwd / boot-sidecar; 4. mc net — the__commonstep resolves through the partition machinery that preserves the D-10/D-15 partition-context + embedded-loader path. The order landed pre-1.0.0 (28e40bb→9f2477c→26bb06c). 2026-07-13 (R3Z3N round 2): the 16-entry Profile preset system was REMOVED —pops_profiles.luadeleted;PLDR.POPSTARTER_PATHis the single value,""= Automatic (steps 2-4 / the APA ladder; shown as "Automatic" in Settings), a set path = step-1 custom with silent fallback. A legacy config'sPROFILE=Npick is migrated into the path at load (see Settings above). The resolver ladder itself is UNCHANGED (the removal is UI/config-layer only), and the only user-facing warning remains "noPOPSTARTER.ELFfound anywhere" at launch. Not yet hardware-tested. 2026-07-16: the USB page could never actually reach step 2 — theLaunchSelectedGamepreflight inui.luaresolved withPLDR.GAMEPATH, which is always""on the USB page (its entries are encoded"<root>POPS/|name.vcd"and carry their own device root for multi-drive lists), so the device rung never built a candidate and a drive-resident-only POPSTARTER hard-blocked the launch with "No POPSTARTER.ELF found" (sAGA/oldman63) — even though the actual launch callRunPOPStarterGame(root, rel)right below resolves with the entry root and would have worked. Fixed by resolving the preflight with the game entry's own device root (non-GHDD scenes; bare-name pages MMCE/MX4SIO/ATA/SMB are bit-identical, GHDD is scene-guarded). Born broken (the feature commitee4cba0readGAMEPATHfrom day one; the USB page pre-dates it withGAMEPATH=""), so this is a day-one gap, not a regression. Bug-class to preserve: a UI preflight that re-implements a resolver must be fed the SAME root as the launch call it guards.
Main menu feature status
- Partition-installed POPS games (HDDOSD / PSBBN style; 2026-07-09): the HDD (PFS) page now also lists games installed as one APA/PFS partition per game —
PP.<name>(visible) /__.<name>(hidden) with the image atpfs:/IMAGE0.VCD— discovered via the newHDD.ListPartitions()APA enumerator (src/luaHDD.cpp, the OPL/wLEfileXioDread("hdd0:")technique) + a mount-probe forIMAGE0.VCD(name alone is a false-positive trap: HDDOSD apps ship asPP.*too). Shown as the partition name minus its 3-char prefix; art/details resolve as__common/POPS/ART/<that name>; launch argv0 =PP.<partition>.ELF(the label used literally — case-sensitive). Entries reuse the standard"PART|rel"encoding so caching / L3 hide (IMAGE0.hideon the game's partition) / R3 reveal flow through unchanged. STATUS: implemented, not hardware-tested (argv0 contract vs the r13 beta + the wiki-documented old-launch-type__commonasset bug are the open HW questions). - Implemented:
MMCE,MX4SIO,HDD (PFS),USB,Disc (DKWDRV),HDD (exFAT)(BDMA ModeATA; scans/launches as amass:device viaata_bd.irx+ R3Z3N's ATA BDM Assault launch drivers — implementeddf2eb9d, CI/Rolling green, validating on hardware), andSMB (v1)(network game browsing: sceneGSMBNET, main-menu OPT==7; settings + an "SMB modules" install toggle + lazy connect/browse/launch via OPL's netman recipe; argv0 selectorsmb:/POPS/SB.<name>.ELF; disconnect-on-leave; blank-shareGETSHARELISTpicker; in-gameIPCONFIG.DAT/SMBCONFIG.DATbackfilled on save — implemented this session, commitsee4d454/121823d/0cf7f81/43033dc/68f9ed5/154c872/5d0e302/f5ac26c/1169dbc, CI/Rolling green, browse + launch HARDWARE-CONFIRMED — the maintainer has been launching SMB games successfully for some time on a static IP configuration, which also confirms the argv0 selector and the connect handshake. The DHCP path was broken from the start and is not yet confirmed: POPStarter has no DHCP of its own, andSyncSmbDatdeleted theIPCONFIG.DATit needs whenever IP assignment was DHCP — the shipped default — so the default config browsed fine on the loader's own lease and then died at the handoff (issue #560; fixedbb62f2be, which writes the leased address instead). The blank-ShareGETSHARELISTpicker also remains unconfirmed. IP addressing only — NetBIOS deferred). Not implemented:ILINK. - Carousel device visibility (Settings → Device List; the section was renamed from "Carousel Devices" 2026-07-15 per R3Z3N, "carousel is actually device list"): a Shown/Hidden toggle per carousel device lets the user hide entries they don't use (e.g. the not-implemented stubs). Persisted as
HIDDEN_DEVICES(CSV of stable device keys; all shown by default; a guard keeps ≥1 visible). The carousel nav/render skip hidden entries with no gaps;UI.MainMenu.OPTstays the real opts index so the launch dispatch is unchanged (with nothing hidden, behavior is identical). STATUS: implemented, validating on PCSX2/hardware.
Exit handoff
- Exit modal exposes OSDSYS, Cancel, BOOT.ELF. BOOT.ELF lookup order:
mc0:/BOOT/BOOT.ELF,mc1:/BOOT/BOOT.ELF.
Cover art (game-list preview box, ui.lua)
- A live cover is a
<name>_COV.pngsidecar (OPL naming, EXP34); it draws into its ownCOVER_Winset.BuildCoverCandidates(ui.lua) builds the candidate list. On removable devices the lookup path is FIXED at<device>:/ART/<game>_COV.png(OPL'smass:/ARTlayout). The Cover/details folder row was REMOVED in EXP35 andART_LOCATIONis INERT: it is still parsed and written so older sidecars load, but nothing reads it for the lookup. EXP34: exactly ONE folder is read — the always-on beside-the-.vcdfallback was removed, and the OPL<name>_COV.pngname is the only accepted cover (the legacy<name>.pngis gone). On HDD/PFS the path is fixed:hdd0:__common/POPS/ART/<game>_COV.png(mounted from the__commonpartition). Within the folder only the exact<gamefilename>_COV.pngis tried (EXP71): the disc-marker-stripped family name was dropped from both branches because it could shadow exact per-disc art, so multi-disc games need art per disc. The matching<name>.txtdetails sidecar rides the same folder (the_COVsuffix is stripped before the.png→.txtswap). The EXP34 folder listing (CoverCache.dir_listing,System.listDirectory) was REMOVED and must NOT come back (EXP46-EXP58):iomanX_dreadreturns one dirent per RPC, so listing a largeART/folder is thousands of blocking round trips. The real fix was moving the probe off the render thread onto the resident cover worker (EXP58); a miss is one boundedfopenthere. EXP42: the "No cover. Looked for:<path>" caption and itslast_cover_probestate are REMOVED — a missing cover simply draws the placeholder. Whether covers draw at all is the persistedCOVER_ARTsetting (Settings → Game List → Cover art, default ON), which replaced the Square toggle. - Placeholder is a two-asset layer, both embedded:
bin/POPSLDR/IMG/cover_default.png(base) +cover_missing.png(overlay). Cover preview disabled (Settings → Game List → Cover art = Off) →cover_default.pngonly (the old "Cover disabled" text label is gone). Preview enabled but the game has no cover →cover_default.pngwithcover_missing.pngdrawn on top. The default cover, the missing overlay, andframe.pngall share the frame's aspect-corrected, right-anchored rect so they register with the jewel-case window on both NTSC and PAL (HW eyeball still pending). MISSING.pngis removed entirely (ELF 1802052 → 1739428, ~−62 KB): dropped from the Makefile (BIN2Srule +EMBEDDED_RSC),src/embed_assets.cpp(extern + both bare-name andPOPSLDR/IMG/-prefixedASSET_ENTRYs + thedefault.png→MISSING fallback), andimages.lua(registration + theIMG_FALLBACKSdefault). It was also the (never-hit, nil-safe)ResolveIconicon fallback, dropped too. Any reference toMISSING.pngas the cover placeholder is stale.
Embedded-asset mechanism
- Adding or removing an embedded asset is 3 explicit coordinated places (it is NOT auto-glob): (a) Makefile — a
BIN2Srule plus the.oinEMBEDDED_RSC(orOPTIONAL_EMBEDDED_RSCfor an optional one likedefault.png); (b)src/embed_assets.cpp— anexternplus anASSET_ENTRYin both lookup tables (the bare-name table and thePOPSLDR/IMG/-prefixed table); (c)bin/POPSLDR/images.luaIMG_REGISTRATIONS(looked up by the bare filename).cover_default.png/cover_missing.pngwere added this way;MISSING.pngwas removed from all three.
CI / release
- Release packaging policy is
PS1_POPSLOADER/*+POPS/PATCH_5.BINwith strict manifest validation; build is gated on embedded build-identity markers (Exec path:,PrepareForColdExternalELFLaunch,BOOT.ELF launch failed) inbin/enceladus.elf; embedded-loader blob staleness check; CI image pinned tops2dev/ps2dev:v2.0.0. - The embedded-Lua syntax gate is now LIVE (
luac5.4 -ponbin/POPSLDR/*.lua+etc/boot.lua). It used to silently skip because the ps2dev image shipped noluac; the workflows nowapk add lua5.4and hard-fail on a syntax error. It catches SYNTAX only — runtime nil-global / type / load-order errors stay invisible to CI (thed4b04beboot brick was exactly such a case). rolling-release.ymlpublishes both the barePOPSLOADER.ELFand the zip from one build to the floatingrolling-releaseGitHub Release on push todevonly. PR events still build and run the gates, but the publish step is gated ongithub.event_name == 'push'(PR #511, 2026-07-16), so a PR no longer republishes the rolling channel.POPSTARTER.ELFships in both zips (the redistributable POPStarter launcher; the POPS engine binaries are NOT redistributable).rolling-release.ymlputsPOPSTARTER.ELFat the zip root (next toPOPSLOADER.ELF) and inPOPS/; the formalcompilation.ymlinstall zip ships it inPS1_POPSLOADER/(next toPOPSLOADER.ELF) andPOPS/.POPS/PATCH_5.BINand aPOPSTARTER/SMB pack folder ship at the rolling-zip root.
Behavioral Invariants (must preserve)
- Cover art filenames MUST keep OPL's
_COVsuffix (<gamefilename>_COV.png). This is a CROSS-COMPATIBILITY contract, not an internal preference: users share oneART/folder between OPL and POPSLoader, and accepting a bare<name>.pngas a fallback would break that. Do NOT re-propose widening it -- it was raised and rejected on this ground 2026-07-28. The asymmetry with the details sidecar (<name>.txt, no suffix) is a known usability trap that costs real tester time; the fix for that is better failure feedback, never a second cover candidate.
(absorbed from the former TRUTHSHEET.md — invariants that changes must preserve unless an explicit migration is planned)
- Boot/runtime Lua is embedded-only (
src/luaplayer.cpp,etc/boot.lua,Makefile): the embedded searcher is installed, filesystem Lua loaders are disabled, required Lua blobs are embedded. - Settings persistence is transactional and per-device — including HDD. Edits stage in drafts;
CommitSettingsChangesruns on confirm/leave.PLDR.SETTINGS_PATHresolves to the per-deviceAPP_DIR_LOCAL/.pldrssidecar; HDD installs persist on the HDD boot partition via theEnsureBootPartitionWritableRW take-over (nomc0:fallback). (Supersedes the old HDD-to-MC exception.) - USB vs MX4SIO identity comes from the ioctl driver name;
mx4sio_bdloads conditionally. Maintainer rule: if a mass device's ioctl/devctl is anything other thansdc/mx4it is USB;sdc/mx4means MX4SIO.usbmass_bdalways loads beforemx4sio_bd. Pure USB boots never loadmx4sio_bd. - Startup backend auto-init is path-driven — boot source plus the configured POPSTARTER/DKWDRV paths drive which backends init before the first page visit.
- Runtime device selection is not hard-locked — the old runtime device-lock subsystem (
canEnterDevice/setDeviceLock) was removed (commits a3e04b8, cef61af); any device page can be entered at runtime. - Probe/retry loops are bounded — finite attempt counts and fixed phases (no frame stalls/hangs).
- Launch failure feedback must be explicit — missing POPStarter/DKWDRV paths and launch-return failures produce user-visible notifications/screens.
- Release package manifest is strict — CI enforces the exact ZIP set and rejects legacy
POPS/*.tm2entries. - BDMA ⟺ POPSTARTER-MC-folder interlock — BDMA can't be enabled while the POPSTARTER MC folder is off; the folder can't be disabled while BDMA is on.
- HDD
.hideis in-app on every device — the<name>.hideper-game marker is written/removed in-app via the L3 toggle on all device pages including HDD via the RW mount take-over. - Per-frame UI timing is frame-counted, not wall-clock —
Timer.getTime()is microseconds on PS2, so nav auto-repeat and description scroll count frames (the canonical Enceladus idiom), not the clock. New time-based UI rate-limits must frame-count (or useos.clock()seconds), never treatgetTime()as ms. - The analog-stick → d-pad fold must stay gated on
Pads.getMode()being analog/DualShock — an ungated fold injects a phantom −127 on a digital pad and breaks up/down nav.Pads.getMode()(PAD_MODECURID, live mode) is the correct source;Pads.getType()(PAD_MODETABLE) is not. - Embedded assets are wired in 3 explicit coordinated places — Makefile (
BIN2S+EMBEDDED_RSC),src/embed_assets.cpp(extern +ASSET_ENTRYin both lookup tables),bin/POPSLDR/images.lua(IMG_REGISTRATIONS, bare-filename key). Adding/removing an asset that touches fewer than all three is a build or runtime break.
Intentionally not implemented (must keep reporting that status until feature work lands): ILINK. (HDD (exFAT) is now implemented via BDMA Mode ATA — df2eb9d, CI/Rolling green, validating on hardware. SMB (v1) network game browsing is implemented — settings/modules/lazy-connect/browse/launch via OPL's netman recipe, commits ee4d454/121823d/0cf7f81/43033dc/68f9ed5/154c872/5d0e302/f5ac26c/1169dbc, CI/Rolling green, browse + launch hardware-confirmed on a static IP config; the DHCP path is fixed but unconfirmed — see the device-support list above.)
Preservation Contracts (hardware-load-bearing — do NOT regress)
See docs/PRESERVATION_CONTRACTS.md for the detailed code-level contract specs — exact path:line citations, what-breaks-it for each, and how to retest on hardware.
- D-10 HDD POPSTARTER + HDD game — B2 fix 4ae6679 (PFS unmount before ExecPS2).
- D-14 HDD POPSTARTER + non-HDD game — same partition-aware route.
- D-15 non-HDD POPSTARTER + HDD game — keep-mask preserves the boot partition's PFS slot.
- DKWDRV from MC — reboot variant direct path with argv0 synthesis.
- BOOT.ELF from USB-booted POPSLoader (L-07) — V2 route at d23520a.
- EnsureBootPartitionWritable (boot pfs-slot unmount→remount-RW take-over) — now load-bearing for HDD settings save and HDD in-app .hide; any launch-path / mount change must not break it.
Reported Hardware Status
| Case | Last result | Date | Notes |
|---|---|---|---|
| D-10 HDD POPSTARTER + HDD game | PASS (contract) | 2026-05-22, reconfirmed 2026-05-28 (Nuno) | B2 fix 4ae6679. Must be preserved. |
| D-14 HDD POPSTARTER + non-HDD game | PASS (contract) | 2026-05-22 | Same route as D-10. |
| D-15 non-HDD POPSTARTER + HDD game | PASS (contract) | 2026-05-22 | Keep-mask. |
| DKWDRV from MC | PASS (contract) | 2026-05-25, reconfirmed 2026-05-28 (Nuno) | Reboot variant + argv0 synthesis. |
| DKWDRV from HDD custom path | PASS (resolved) | 2026-06-04/06-06 (Nuno) | PRs #486/#487. Was known-broken through BETA-10-5. |
| BOOT.ELF from USB-booted POPSLoader (L-07) | PASS | 2026-05-28 (Nuno) | V2 route d23520a. |
| BOOT.ELF from HDD-launched POPSLoader (U-10) | PASS (resolved) | 2026-05-31 (Nuno) | PR #479 (reboot_iop=0). |
| HOSDmenu → POPSLoader (Class A start) | PASS (resolved) | maintainer 2026-06-15 | Mechanism not pinned; reverify if it regresses. |
| wLaunchELF → POPSLoader (Class A start, some builds) | PASS (resolved) | maintainer 2026-06-15 | PR #458 Layer A + remaining builds confirmed. |
| PSBBN / Browser / HOSDMenu / OSDMenu → POPSLoader | PASS (contract) | CosmicScale 2026-05-25 + Nuno 2026-05-28 | |
| Settings save on USB / MC-installed POPSLoader | PASS | 2026-05-27 (Nuno) | Per-device APP_DIR/.pldrs. |
| HDD is RW-writable on real hardware | PASS | provato 2026-06 | Confirmed the boot-partition RW take-over works; full HDD settings/.hide flow still validating. |
HDD-resident settings save + in-app .hide |
Implemented / boots on PCSX2 | 2026-06-17 | Validating on hardware. Not yet broadly hardware-confirmed. |
| PAL native 640×512 full-screen render | Implemented / boots on PCSX2 | 2026-06-17 | PAL hardware validation pending. |
| U-06 PAL/NTSC asset proportions | Targets the new PAL-512 render | — | Verify the full-screen fill + auto-revert confirm on PAL hardware. |
| D-12 startup backend auto-init | PASS | 2026-03-28 | |
| D-16 first-entry USB backend discovery | PASS | after 2026-03-27 | |
| Up/down + analog-stick nav (frame-counted repeat; analog fold gated) | PASS | 2026-06-20 (oldman63) | Lands on individual items; continuous scroll fine. |
| Boot sound On/Off save | PASS | 2026-06-20 (oldman63) | Saves and survives reboot. |
| Overall latest rolling | PASS ("everything working fantastically") | 2026-06-21 (Nuno6573) | General confirmation, not item-by-item. |
| Overscan (CRT inset) | Implemented / boots on PCSX2 | 2026-06-20 | Not yet CRT/HW-eyeballed. |
Cover-art layering (cover_default + cover_missing overlay) |
Implemented / boots on PCSX2 | 2026-06-20 | Eyeball that both register inside the jewel-case frame on NTSC + PAL. |
| Cover art lookup on hardware (superseded contract) | PASS, needs a re-run | 2026-06-30 (FifthFox) | Covers showed on hardware, but this ran under the selectable Cover/details folder setting, removed in EXP35. The "No cover. Looked for:" caption was removed in EXP42 and EXP71 fixed the lookup at one exact path per device, so the current contract has no hardware pass. The earlier "POPS/ART fails" report was tester name/placement (user error), matching the SDK finding (open==fopen, nested reads work; wf w0ucpisvc). The 6641860 pre-probe drop was inert. |
| HDD scan steered off the boot pfs slot (Proposal A) | Implemented / boots on PCSX2 | 2026-06-20 | b159a43. Wants a deliberate HW test that game partitions still mount/list off the boot slot. |
Known Issues (canonical — the single list; README / AGENTS / ROLLING_NOTES point here)
Open (awaiting the hardware retest):
- ~~"No USB backend detected" (issue #508)~~ — RESOLVED on hardware (sAGA, EXP23-29 era, 2026-07-20): fixed by the R3Z BDM driver-blob adoption (c1debd1, EXP7) + the boot-time usbmass load; USB confirmed working through the whole 2026-07-20 storage wave. -Os was exonerated (the bug outlived its revert). NOTE the same c1debd1 half-swap is what broke MX4SIO for five days — see the matched-driver-set invariant below.
- SMB connect failure — FIXED and HARDWARE-CONFIRMED (maintainer report 2026-07-06; fixed 2026-07-07; confirmed working 2026-07-28) — EnsureNet initialized the EE-side lwip stack while smbman runs on the IOP-side stack, so IP config/DHCP never reached the stack doing the networking and connect failed regardless of settings. Fixed with OPL's ps2ip_init() RPC bind (d2496ec), plus phased connect progress, honest step-naming errors (incl. "Server refused SMBv1"), field trim/validation, the OSK symbol shift, and the SMB-modules launch gate (a546d45). Connect → browse → launch is now hardware-confirmed — the maintainer has been launching SMB games successfully for some time, on a static IP configuration. That static-only shape is exactly what hid the DHCP defect for so long (see the device-support list): POPStarter has no DHCP of its own, and SyncSmbDat deleted the IPCONFIG.DAT carrying the address whenever IP assignment was DHCP — the shipped default — so every DHCP user browsed fine and then died at the handoff (#560, elvengf; fixed bb62f2be, guarded by harness T43). Still unconfirmed: the DHCP path, disconnect-on-leave (D-07C), the clean-failure path (D-07D), and the blank-Share GETSHARELIST picker. The 2026-07-07 audit — 62 verified findings — lives in docs/REPO_AUDIT_2026-07-07.md; 61 were actioned across 6 revertable commits (d2496ec 4665b19 a546d45 8809925 7ec708a + the docs wave; checkpoint checkpoint/pre-audit-fixes). Note: the SMB Port default 1111 is deliberate (maintainer's PS2-Servers custom-port ecosystem; DECISIONS.md has the rationale).
- "Failed to load HDD" from a non-HDD boot (config-specific; Nuno 2026-06-14) — when POPSLoader is launched from a non-HDD device (USB / MC) via a launcher, a specific configuration faults while building the HDD game list (most setups list the HDD fine). POPSLoader itself starts normally. Workaround: boot POPSLoader from the HDD, or open the HDD page a few seconds after the menu. Instrument + isolate; do not assert a cause from source — bare-reset hardware disproved the #490 theory. (Distinct from the fixed second-boot cache crash below.) Instrumented 2026-07-09: the raw partition-mount rc now reaches the page (the "No '__.POPS' partitions" toast carries last mount rc: N), a dir-read fault on a mounted partition toasts instead of silently showing "No games found", and a bad first HDD status no longer latches dead — it is re-probed on each page visit (cheap devctl, no IRX reload). The next report from the affected rig should say where it fails.
- MMCE not detected on first switch straight from MX4SIO (retry succeeds) (maintainer HW report 2026-07-23, experimental-claudeless) — jumping directly from the MX4SIO page to the MMCE page can fail to detect the MMCE device on the first attempt; retrying detects it. All devices otherwise functional. Parked as a future QoL refinement, not a blocker — unproven from source. NOTE: the System.getSio2Owner exclusion guard and the restart dialog were deleted in EXP32, before this report, so the suspect is a settle/probe-timing gap in the freesio2 coexistence path, not in a guard handoff.
- ~~Cover art: some PNGs silently don't load~~ — FIXED in code, needs the hardware retest (maintainer HW report 2026-07-23; fixed same day on experimental-claudeless, diffed against official OPL's textures.c): four gaps vs OPL, all closed. (1) 8-bit grayscale / grayscale+alpha PNGs hit the decoder's reject branch — added png_set_gray_to_rgb() (the same fix OPL ported from wOPL for its issue #225). (2) The RGB branch copied with a 3-byte stride while png_set_filler pads RGB rows to 4 B/px — plain-RGB covers rendered color-shifted; stride fixed to 4 (matches OPL's texReadPixels24Row). (3) ANY failed load was memoized "absent" for the whole session — now only genuine file-absence is memoized (CoverCache:MemoizeMiss, ui.lua); decode/transient failures retry on next visit, and the worker-busy give-up no longer memoizes (OPL's ERR_BAD_FILE-vs-decode-error model). (4) No dimension cap — a >~590px CT32 cover could freeze the UI in gsKit's VRAM eviction loop; now OPL's rule (≤1024px per side AND ≤720·512·4 bytes decoded). Separately, per the 2026-07-23 isolation report: (5) the EXP34/35 hard-lock to <device>:/ART/<name>_COV.png was reported as a lookup-contract loss vs older builds, and on 2026-07-23 the legacy families were restored additively. That restoration was REVERTED by EXP71 (maintainer directive): cover art reads EXACTLY one path per device again, <device>:/ART/<gamefilename>_COV.png (APA/PFS: hdd0:__common/POPS/ART/<gamefilename>_COV.png), with no disc-marker-stripped family name. Test: grayscale + plain-RGB + previously-vanishing covers should all appear, and nothing should freeze on large art. Art named plain Game.png, or living in POPS/ART/, will NOT be found, and multi-disc games need art per disc: that is current intended behaviour, not a regression. JPEG covers remain unsupported by design (dropped 2026-07-15; OPL parity would need a JPEG decoder restore — declined for now). Files that are JPEGs renamed .png should be converted to real PNG.
- ~~DKWDRV exit back to the Memory Card "hangs on the pic"~~ — CLOSED as not-ours (maintainer call 2026-07-23): the hang on exit is DKWDRV-side (their app), not a POPSLoader defect. Removed from the open list; TESTING.md checklist references should follow suit.
Hardware-CONFIRMED 2026-07-28 (sAGA, rolling RR74 / EXP74, internal exFAT ata0:):
- Game details (<gamefilename>.txt) DISPLAY. The EXP73 rebuild of the details channel is confirmed working on real hardware. (EXP72's version was broken and also regressed covered games; see the EXP72/EXP73 bullets above.) His layout: VCDs in ata0:/POPS/, sidecars in ata0:/ART/.
- Hiding / unhiding games works with Hidden games = Hidden. The EXP74 L3 fix is confirmed on hardware. This was his 2026-07-27 report: with the filter on, L3 did nothing at all.
- OPEN, same session: cover images do NOT display on ata0: while the details .txt in the SAME ART/ folder does. The two paths differ only in the filename rule -- details are <devroot>ART/<gamefilename>.txt, covers are <devroot>ART/<gamefilename>_COV.png (EXP71, exactly one candidate). Since his .txt resolves, the folder and devroot logic are proven correct on that device, which points at the filename (_COV suffix) or at PNG decode rather than at path resolution. Note a found-but-undecodable PNG and an absent one are indistinguishable to the UI -- both surface as a nil texture -- and the caption that would have told a tester which was removed in EXP42.
In testing on hardware (implemented + boots on PCSX2; not yet broadly hardware-confirmed — these are what the current rolling build asks testers to verify):
- EXP73 game details (<gamefilename>.txt) — the headline test for this build. Turn Settings → Game details ON (it defaults Off) and leave cover preview on, then check three cases: a game with cover art and a .txt (this is the one EXP72 regressed), a game with only a .txt (oldman63's original report, needs checking on the first visit and on a revisit), and a game with neither. Then scroll a mixed folder quickly and confirm no game ever shows another game's text. Repository-verified and harness-gated (T40), zero hardware runs.
- Launch, on every reachable device, after browsing a list. EXP73 changed the resident cover worker, which is drained on the launch path (the EXP58 Quiesce contract). MX4SIO and USB matter most — they are the devices with large ART/ folders.
- Settings save on an HDD-loaded rig. The read-only failure toast and both __.POPS write-test toasts were rewritten as formattable keys; the write test fires on every save with the HDD loaded.
- Hungarian rendering. On the HDD page, the "no games found" and "list refreshed" toasts should now appear in Hungarian — they had translations in all six languages that concatenation was discarding before they reached the lookup. oldman63's PR #559 strings (Verzió, Kész, SIKERTELEN INDÍTÁS and the rest) are injected and should render.
- The 2026-07-20 storage wave (graduated from EXP24-29; maintainer HW-confirmed the core flows same-day, broader coverage wanted): (a) one matched-vintage BDM driver set — bdm/bdmfs_fatfs/usbmass_bd/mx4sio_bd all resolve from the pinned SDK via vpath, R3Z's probe-fixed ata_bd.irx embedded (INVARIANT: BDM drivers are a matched set, one vintage — never re-pin one alone; the c1debd1 half-swap broke MX4SIO for five days and got misattributed to EXP22); (b) internal exFAT HDD loads on exFAT-page entry. The EXP32 System.initATAAsync EE worker is GONE: EXP66 deleted the binding and EXP69 settled on System.initATAModules, a serial main-thread load on page entry, with EnsureDev9() moved to boot in main.cpp, so the old "never load BDM-atad synchronously at page time" invariant no longer holds. The 4TB GPT read+launch pass was on the 2026-07-20 arrangement; the current EXP69 shape is hardware-unverified (sAGA's 4TB rig still fails); (c) MMCE↔MX4SIO coexistence. The EXP28/EXP29 System.getSio2Owner guard and its restart-to-switch dialog were DELETED in EXP32: both drivers now run resident on freesio2 as OPL does, with no gate (the C binding survives with no Lua caller) — the two drivers both raw-own SIO2 port 3, coexistence hangs scans at 48%; (d) Adaptive-BDMA launch staging = two direct embed pastes (self-healing marker-last); (e) live cover art fitted inside frame.png's alpha-measured case window (was overflowing 5-21px). Remaining: an hdd0:/APA boot smoke test (D-10 contract), PAL eyeballs on the cover fit, sAGA's full pass.
- HDD in-app .hide (L3 toggle; unhide via Settings → Game List → Hidden games).
- R3 reveal/hide on a device game list — transient session-only reveal (712fc90; does NOT persist) that overrides GLOBAL_HIDE and rebuilds the list in place (reuses the R1 refresh path).
- HDD-resident settings save (boot-partition RW take-over; provato confirmed the HDD is RW-writable).
- PAL native 640×512 full-screen render + auto-revert display-change confirm.
- POPSTARTER Memory Card Folder toggle + the BDMA interlock.
- Overscan (CRT inset) — eyeball the inset on a real CRT.
- Cover-art layering (cover_default + cover_missing overlay) — eyeball that both register inside the jewel-case frame on NTSC + PAL.
- HDD scan steered off the boot pfs slot (Proposal A, b159a43) — deliberate HW test that game partitions still mount/list off the boot slot.
- Adaptive BDMA (2026-07-09, issue #509) — turn it on, then launch an MMCE game and a USB game back-to-back without touching Settings; both booting IS the pass signal (no on-screen notice exists on success — the launch execs POPStarter before anything could render; mc:/POPSTARTER/bdma_mode.txt naming the launched device's variant is the optional post-hoc check). A staging failure cancels the launch with a visible warn toast.
- Partition-installed POPS games (2026-07-09) — a PP.<name> / __.<name> partition holding IMAGE0.VCD should appear on the HDD (PFS) page under its partition-derived name and launch (this exercises the PP.<name>.ELF argv0 contract against the shipped POPStarter r13 beta — unproven).
- Boot chime re-encode (2026-07-09) — the chime is now 22.05 kHz (half size); it should sound identical on a real console (pitch/speed are header-driven; PCSX2 is not proof for SPU2).
- HDD status re-probe + mount-rc breadcrumbs (2026-07-09) — on the affected non-HDD-boot rig, the HDD page should now report last mount rc: N / a dir-read toast / recover once the drive settles, instead of a bare empty list.
Recently resolved:
- Both "hangs at 42%" (MX4SIO page + internal exFAT page, on rolling AND experimental) — two distinct root causes, both fixed in the 2026-07-20 storage wave (above): the c1debd1 half-swapped driver vintage (MX4SIO's device registration never completed against the R3Z-era bdm core) and the synchronous page-time ata_bd load (inline _start detection wedging the IOP module loader, which then also hanged the NEXT page's driver load — one wedge, two pages). The wedge cascade class is structurally closed: the exFAT load is async with a screen-alive poll, and other pages decline rather than queue behind an in-flight load. HW-confirmed same-day: exFAT 4TB scan+launch, MX4SIO scan+launch.
- MMCE game scan hang at 48% after an MX4SIO visit (and vice versa) — mmceman and mx4sio_bd both raw-drive SIO2 port 3 with no mutual arbitration (mx4sio spins with no timeout holding the shared SIO2 semaphore); every reference launcher treats them as exclusive. Fixed 2026-07-20 with the session exclusion guard + the restart-to-switch dialog (HW-confirmed).
- USB page "No POPSTARTER.ELF found" with the launcher present on the drive (sAGA/oldman63, 2026-07-16) — the launch preflight resolved POPSTARTER with PLDR.GAMEPATH (always empty on the USB page) instead of the game entry's own device root, so <drive>:/POPS/POPSTARTER.ELF was never checked and drive-resident-only setups were blocked before the (correct) launch call ever ran. Fixed in ui.lua LaunchSelectedGame; the preflight now resolves with the same root the launch uses. Day-one gap (not a regression); masked for anyone with a cwd/mc copy. Pending the sAGA/oldman63 hardware retest.
- Nav auto-repeat flew / all desc-scroll speeds felt the same — Timer.getTime() is µs not ms, so wall-clock gates were sub-frame; nav auto-repeat and description scroll are now frame-counted (description scroll is fixed at the Fast pace; the Fast/Medium/Slow setting was later removed). Up/down + analog-stick nav and boot-sound save are HW-confirmed (oldman63, 2026-06-20).
- Phantom analog input broke up/down nav — the analog-stick → d-pad fold is now gated on Pads.getMode() being analog/DualShock with per-axis hysteresis. HW-confirmed (oldman63).
- HDD settings save failed after a game scan ("...may be read-only") — a game scan borrowed the boot pfs slot and a never-cleared RW flag stranded the save path; fixed 8d1e67a (liveness-validate the boot RW mount via doesFolderExist on the save path) and b159a43 (Proposal A: steer the scan to non-boot slots). The latter still wants a deliberate HW test.
- MISSING.png cover placeholder replaced by the cover_default + cover_missing layer; MISSING.png removed (~−62 KB ELF).
- Codex BETA-13 audit — 6 findings, all verified real and fixed (ec81de3): PromoteTmpToDest now requires its backup before truncating dest; BMP pixel-size/stride validation; PNG dimension cap; stale mc0: probe cleanup; two System.writeFile full-byte-count checks; R3 no success-toast on a failed save. Report: docs/AUDIT_CODEX_2026-06-20.md.
- "Failed to load HDD" on the second boot (cache/loadfile crash) — fixed; the HDD list loads every boot, and a real error string now surfaces if it ever fails.
- Load-order boot brick — PLDR.HDD methods were defined before PLDR.HDD existed, which made the recent HDD-feature rolling builds un-bootable; fixed d4b04be (2026-06-17). (Invisible to luac -p/CI; only fatal at runtime.)
- U-10 BOOT.ELF-from-HDD-boot — PR #479. DKWDRV from a custom HDD path — PRs #486/#487. Class-A HOSDmenu / some-wLE start failures — maintainer-confirmed 2026-06-15. MX4SIO-rooted settings save — PR #477.
Investigation artifacts archived under docs/archive/: U10_INVESTIGATION.md, LAUNCH_HYGIENE.md, HDD_POPSTARTER_HANDOFF.md.
Known Open Work
- Settings UI redesign (Berion mockup) — gated on the outstanding hardware verification (D-10/D-14/U-10 plus the new HDD/PAL features) settling, and on the mockup PNGs landing.
- "Failed to load HDD" from a non-HDD / via-launcher boot — the remaining open launch-adjacent issue. Instrument + isolate.
- Layer C lazy IRX loading — CLOSED. The safe win shipped:
mmcemandeferral (PR #471 — eager only on MMCE boots, lazy elsewhere +System.reinitPad). The furtherds34bt/usbddeferrals are DECLINED (2026-06-22):ds34usbandds34btboth hard-importusbd, and the only defer trigger (boot-device hint) classifies the boot medium, not pad transport — deferring would strand USB/BT controller input on every non-USB boot, unrecoverable without a reboot, for a likely-small boot-time gain. Keep them eager; do not re-propose. - µs-as-ms timer sweep — DONE (
9c3f64f+a8e61f3).Timer.getTime()is microseconds (source-verified). Fixed: PathEditor key-flash + caret blink, launch-watchdog label; removed the dead action debounce (edge-triggering is the real gate); frame-paced the scene-fade / boot-fade / carousel preserving their current feel. Only cosmetic/inert sites (busy-overlay throttle, saving marquee, disabled debug log) intentionally remain. ILINK— intentionally unimplemented.HDD (exFAT)is now implemented (BDMA ModeATA,df2eb9d); validating on hardware.SMB (v1)network game browsing is implemented (commitsee4d454/121823d/0cf7f81/43033dc/68f9ed5/154c872/5d0e302/f5ac26c/1169dbc, CI/Rolling green); browse + launch hardware-confirmed on a static IP config, DHCP path fixed inbb62f2bebut unconfirmed.
(The old "ps2hdd-osd.irx → ps2hdd.irx driver swap probe" item is removed: HDD read-write was achieved instead via the EnsureBootPartitionWritable boot-partition remount take-over, and provato confirmed the HDD is RW-writable on hardware — the IRX swap is no longer the gating path.)
Verification Status
- 1.1.0 is the public release (2026-07-21; previously 1.0.1, 1.0.0 2026-07-10, BETA-12 2026-06-18, BETA-11 2026-06-15). The line was cut from
BETA-13-PLAY, which was renamed todevafter the 1.0.0 cut and remains the rolling-publish source. The branch tip moves per push (seegit log) — code/build statements above are repository-verified around the current tip. - The 2026-06 HDD/PAL/BDMA features are repository-verified and boot on PCSX2; provato confirmed the HDD is RW-writable on real hardware; the full feature flows are still validating on hardware and are not broadly hardware-confirmed.
- Hardware behavior is
Unknown (verify on hardware)unless explicitly recorded above (or inQA_REGRESSION_MATRIX.md) with a date. - See
QA_REGRESSION_MATRIX.mdfor the full experiment chronology andDECISIONS.mdfor the decision log.