Skip to content

Latest commit

 

History

History
531 lines (505 loc) · 35.2 KB

File metadata and controls

531 lines (505 loc) · 35.2 KB

Last updated: 2026-07-27 (Release 1.1.0; rolling v1.1.1-dev / experimental v1.1.1-dev-EXP73; HEAD = dev; BETA-12-PLAY is ARCHIVAL/frozen). For current Settings behavior, Known Issues, Preservation Contracts, Behavioral Invariants, and Hardware Status, see STATE.md (canonical). Where dated historical sections below differ from STATE.md, STATE.md wins.

COMPONENTS

Purpose

Current technical map of POPSLoader source files, their responsibility, and key entry points. POPSLoader is a PS1-game launcher built on the Enceladus runtime: EE C/C++, an embedded Lua application (bin/POPSLDR/*.lua compiled into the EE ELF via bin2c), embedded IOP IRX modules, and a BRAM child ELF-loader. Every technical claim below cites path:line against this worktree.

Scope note: this file documents what is actually present and wired in the dev rolling state (the active branch; BETA-12-PLAY is frozen). Where a component is on disk but unused (dead or dormant), that is called out explicitly rather than omitted. Several files an external audit flagged for removal — the 3D render pipeline (commit a56441c), md5, and the orphaned SMB / strUtils source (commit f83dbbb) — have since been removed from the tree and are documented as removed below. (The old orphan src/luaSMB.cpp is gone; SMB (v1) is now a real, wired feature — a C binding inside the System.* surface plus the GSMBNET scene — documented below. Implemented this cycle, CI+Rolling green, validating on hardware.)

1. EE bootstrap and runtime (src/)

src/main.cpp — EE entry, pre-main IOP hygiene, IRX bring-up

  • _ps2sdk_memory_init() (main.cpp:619) runs BEFORE main() inside newlib's memory-init hook. Gated on -DRESET_IOP (set in Makefile:34, applied Makefile:59-61). It performs SifExitRpc -> SifInitRpc(0) -> fileXioExit -> while(!SifIopReset) -> while(!SifIopSync) -> SifInitRpc(0) (main.cpp:658-663) to recover from "polluted parent" launchers (wLaunchELF off non-HDD devices) whose live fileXio modules would otherwise hang a plain SifIopReset (ps2sdk #425). Anyone reading main() top-down will miss this reset.
  • detectBootDeviceHintFromArgv0() (main.cpp:134) derives an advisory pre-Lua boot-device hint from argv[0]. HDD variants hdd/pfs/ata/apa all map to "HDD" (main.cpp:155-160); both mass and usb map to "USB" (main.cpp:139-144) — the MX4SIO-vs-USB disambiguation happens later in Lua.
  • parseLaunchArgs() (main.cpp:198) parses NHDDL-style args: -page=/-mode= (-mode is a pure alias, both write launch_arg_page, main.cpp:220-226), -game=, -debug.
  • main() (main.cpp:439) parses launch args, installs SBV patches, then loads the embedded IRX stack via SifExecModuleBuffer (LOAD_IRX/LoadIrxChecked wrappers, main.cpp:388-410). Boot IRX order is fixed and partly conditional: iomanX (main.cpp:470) -> fileXio + fileXioInit (main.cpp:475-477, gated on iomanX) -> sio2man (main.cpp:488) -> mmceman ONLY if hint == MMCE (main.cpp:504-538; LoadIrxChecked call at main.cpp:507) -> mcman/mcserv (main.cpp:545-546) -> initMC (main.cpp:547) -> padman (main.cpp:548) -> libsd (main.cpp:550) -> usbd (main.cpp:554) -> ds34usb/ds34bt (main.cpp:558-559) -> audsrv (main.cpp:563). Device-specific stacks (BDM/usbmass/mx4sio/cdfs/HDD) are NOT loaded here; they are lazy-loaded on demand from luasystem.cpp / luaHDD.cpp.
  • After IRX bring-up main() sets boot path/app dir, inits gsKit + pad, then enters a loop running embedded boot.lua via runScript.

src/luaplayer.cpp — Lua VM lifecycle and embedded asset wiring

  • g_embedded_lua_assets (luaplayer.cpp) is the embedded script table: boot.lua, system.lua, ui.lua, images.lua (pops_profiles.lua was removed 2026-07-13 with the profile-preset system).
  • runScript() (luaplayer.cpp:254) creates the Lua state, installs the embedded searcher (InstallEmbeddedLuaSearcher, luaplayer.cpp:272), disables on-disk script loaders (DisableLuaFilesystemScriptLoaders, luaplayer.cpp:273 — nils dofile/loadfile, clears package.path/cpath), registers all Lua module bindings (luaplayer.cpp:282-288), then loads the requested embedded script buffer. A missing embedded asset is a hard FATAL, not a disk fallback (luaplayer.cpp:294-302).
  • Registered binding modules (luaplayer.cpp:282-288): luaGraphics_init, luaControls_init, luaScreen_init, luaTimer_init, luaSystem_init, luaSound_init, luaHDD_init. NOTE: there is no separate luaSMB_init — the SMB (v1) network client lives inside the System.* surface (initSMB/connectSMB/disconnectSMB, registered by luaSystem_init; the Lua-side PLDR.InitSMBPopsRoot wraps them), not a standalone module. The old orphan src/luaSMB.cpp (commit f83dbbb) is unrelated and removed (see Orphaned/dead-on-disk).

src/luasystem.cpp — the largest binding surface (System.*)

  • Lazy IRX loaders (Layer C): EnsureBDM/EnsureBDMFatFs/EnsureUsbMass chain (luasystem.cpp:80-120), EnsureMmceman (luasystem.cpp:141).
  • Mass-backend classification: FetchBdmList/ClassifyMassBackend (luasystem.cpp:184-217), driver-name lookup GetMassMountDriverNameBySlot (luasystem.cpp:312).
  • MX4SIO init enforces USB mass first: lua_mx4sio_init (luasystem.cpp:1298) calls EnsureUsbMass() (luasystem.cpp:1316) before loading mx4sio_bd.irx (luasystem.cpp:1318).
  • External-ELF launch bindings: lua_loadELF (luasystem.cpp:974), lua_loadELFWithPartition (luasystem.cpp:1020, requires reboot_iop != 0 and an hdd?:PART: partition context, luasystem.cpp:1033-1037), lua_loadELFRebootIOP (luasystem.cpp:1068). Keep-PFS mask binding lua_set_exec_keep_pfs_mask (luasystem.cpp:945).
  • Launch-arg binding lua_getLaunchArgs (luasystem.cpp:1211) and boot-hint binding lua_getBootDeviceHint (luasystem.cpp:1227).
  • SMB (v1) network client (Path B = OPL's netman recipe; implemented this cycle, CI+Rolling green, validating on hardware). Lazy net stack EnsureNet (luasystem.cpp:1412): brings up dev9 once via the shared EnsureDev9/ g_dev9_loaded guard, then loads netman + smap + ps2ips + smbman and ps2ip and calls NetManInit — NEVER at boot, only on a menu/settings action. Bindings registered in the System.* table (luasystem.cpp:1776-1779): initSMB (lua_smb_init), connectSMB (lua_smb_connect, luasystem.cpp:1578), disconnectSMB (lua_smb_disconnect, luasystem.cpp:1724 — CLOSESHARE+LOGOFF, also torn down on a failed connect so no half-open session lingers). Connect drives the ps2smb.h devctls (LOGON/ECHO/OPENSHARE) on smb0:. A blank Share field triggers SMB_DEVCTL_GETSHARELIST (luasystem.cpp:1686-1701) to enumerate the server's shares for the in-UI picker. EE links -lnetman -lps2ip -lps2ips. NetBIOS is NOT supported (deferred: nbns.irx is OPL-custom, not stock ps2sdk — address type must be IP).
  • lua_rename (luasystem.cpp:753) is a non-atomic copy+delete, but the safe-promote fix has landed in this worktree: it calls the shared copy_file_contents (luasystem.cpp:714) and only remove()s the source if the copy returned 0 (luasystem.cpp:760-762). copy_file_contents returns -1 on an open/short-write/mid-stream-read error, so a failed copy no longer deletes the source. (There is no longer a separate lua_movefile; only lua_rename is registered, luasystem.cpp:1330. copyFile is historical — the comment at luasystem.cpp:713 notes the shared helper was "historically used by copyFile".) Not on the launch path.

src/luaHDD.cpp — HDD (dev9) IRX stack and PFS mounting

  • Load_HDD_IRX (luaHDD.cpp:120, exposed as HDD.Initialize at luaHDD.cpp:173) calls EnsureAtaBdm() (dev9 -> bdm -> bdmfs_fatfs -> ata_bd, i.e. ps2atad built with ATA_ENABLE_BDM=1 so one atad instance serves APA/PFS and exFAT), then ps2hdd-osd -> ps2fs, with HDD args -o 4 -n 20 and PFS args -m 4 -o 10 -n 40. Plain ps2atad is no longer loaded. There ARE settles now: sleep(1) before the ata_bd load and sleep(1) after it (luasystem.cpp EnsureAtaBdmModulesInner), matching NHDDL/wLaunchELF ordering.
  • GetHDDStatus via HDIOC_STATUS (luaHDD.cpp:93-96); on-demand mount MountPart/mnt (luaHDD.cpp:21-92) producing pfs%d:/ mount points. HDD.MountPartition returns (ok, rc) — the second value is the raw fileXioMount rc (2026-07-09 diagnostics; mnt no longer collapses failures to a constant -4).
  • HDD.ListPartitions (2026-07-09) — read-only APA-table enumerator: fileXioDopen("hdd0:") + fileXioDread, keeping main-partition records with the PFS format magic (the OPL / wLaunchELF technique; constants mirror ps2sdk libhdd.h). Names are snapshotted into a static buffer and the dir fd closed BEFORE any Lua-heap call so an allocation longjmp can't leak a ps2hdd slot. Returns the name array, or nil, rc before ps2hdd loads. Consumed by PLDR.HDD.DiscoverPartitionGames (partition-installed PP./__. games).

src/embed_assets.cpp — runtime name -> embedded blob resolver

  • embedded_get() (embed_assets.cpp:195) normalizes paths (strips embed:/, embed_assets.cpp:205; ./, embed_assets.cpp:209; leading /, embed_assets.cpp:212) and resolves against the static g_embedded_assets table (embed_assets.cpp:93-181) via embedded_find (embed_assets.cpp:184). There is NO icon/MISSING.png fallback — a missing key returns 0 and the Lua caller decides. Each asset appears TWICE in the table: under its bare name and under a POPSLDR/IMG/-prefixed key (mirror tables at embed_assets.cpp:94-138 and 141-181). default.png is an OPTIONAL legacy cover override, declared and added only inside #ifdef HAVE_ASSET_DEFAULT_PNG (extern embed_assets.cpp:56-59; entries embed_assets.cpp:117 & 164); the cover box does NOT depend on it. The new cover_default.png / cover_missing.png placeholders are MANDATORY embeds (extern embed_assets.cpp:60-63; entries embed_assets.cpp:119-120 & 166-167).

Other EE runtime files

  • src/graphics.cpp / src/luagraphics.cpp — gsKit 2D drawing + Graphics.* Lua bindings (luaGraphics_init, luagraphics.cpp). Image load/draw, the Graphics.loadImageEmbedded path used by images.lua. Also hosts the overscan (CRT inset) transform (graphics.cpp:1128-1166): g_overscan permille (graphics.cpp:1135), set_overscan/get_overscan (graphics.cpp:1155, 1163; clamped 0..200), recompute_overscan (graphics.cpp:1140), and the OVX()/OVY() inline scalers (graphics.cpp:1165-1166) that every gsKit draw site routes through. The math is OPL rmSetOverscan exactly (margin = W*permille/2000 per edge, scale = 1 - permille/1000); at permille 0 the transform is the IDENTITY so the default render is unchanged, and recompute_overscan is re-run on a screen-dim change (graphics.cpp:1339).
  • src/fntsys.cpp / src/atlas.cpp — TrueType font system and glyph atlas.
  • src/luaScreen.cpp (luaScreen_init) — Screen.* flip/clear bindings, plus the overscan Lua surface Screen.setOverscan(permille) / Screen.getOverscan() (lua_set_overscan luaScreen.cpp:64 / lua_get_overscan luaScreen.cpp:71; registered luaScreen.cpp:162-163) wrapping graphics.cpp's set_overscan/ get_overscan.
  • src/luacontrols.cpp (luaControls_init) — pad input, registered under the global Pads (NOT Controls; Pads_functions luacontrols.cpp:273, lua_setglobal "Pads" luacontrols.cpp:289). Pads.getMode() (lua_getmode luacontrols.cpp:70; registered luacontrols.cpp:278) returns the LIVE negotiated controller mode via padInfoMode(port,0,PAD_MODECURID,0) (luacontrols.cpp:78) — high nibble 0x5 analog / 0x7 DualShock / 0x4 digital / 0 no-data. The pre-existing Pads.getType() (lua_gettype luacontrols.cpp:9; registered luacontrols.cpp:277) reads PAD_MODETABLE (a capability-table entry, luacontrols.cpp:17) and is NOT the live mode — ui.lua's stick-fold gate must use getMode. getLeftStick/getRightStick (lua_getleft luacontrols.cpp:82 / lua_getright luacontrols.cpp:119) are hardened: zero-init + neutral (0,0) default, and they only report a non-neutral axis when a pad read actually returned data, so an unread/failed frame can't inject a phantom -127. The PAD_ANALOG/PAD_DUALSHOCK globals the gate compares against are exported here (luacontrols.cpp:343, 346).
  • src/pad.cpp — low-level pad init/read.
  • src/sound.cpp / src/luasound.cpp (luaSound_init) — audsrv-backed audio.
  • src/luatimer.cpp (luaTimer_init) — Timer.* bindings.
  • src/system.cpp — small EE system helpers.

3D rendering pipeline (removed)

The legacy 3D render pipeline (src/render.cpp, src/calc_3d.cpp, src/gsKit3d_sup.cpp, src/luaRender.cpp and the Render/Lights/Camera Lua bindings, plus the -lmath3d link) was dead at the application level and has been removed from the tree (commit a56441c). These files are no longer on disk, no longer in the Makefile object lists, and luaRender_init is no longer called from luaplayer.cpp.

Orphaned / dead-on-disk

  • src/luaSMB.cpp (the old orphan SMB network-share logon helpers) was orphan source — never in the Makefile object lists and never initialized — and was DELETED in commit f83dbbb (2026-06-13). It no longer exists in the tree. This dead file is NOT the current SMB feature: SMB (v1) was re-implemented this cycle as a live C binding inside the System.* surface (luasystem.cpp, EnsureNet/lua_smb_connect) plus Lua wiring (GSMBNET scene, OPT7) — see the luasystem.cpp SMB notes above and Feature Surface below.

2. Embedded Lua application (bin/POPSLDR/)

All bin/POPSLDR/*.lua are bin2c'd into the EE ELF at build time; the on-card copies are not read at runtime. Editing them requires a rebuild.

bin/POPSLDR/system.lua — controller, device/launch engine, settings

  • Owns device resolution, settings persistence, game-list building, the HDD cache, and the launch engine. requires ui/images.
  • Boot-device classification: ResolveBootContext (system.lua:1849) / DetectBootDevice (system.lua:1983), prefix rules under ResolveBootContext. mass:/ is disambiguated via the BDM driver name (classify_mass_boot, system.lua:1866; sdc/mx4 => MX4SIO).
  • Launch-arg ingest: NormalizeLaunchPage (system.lua; ata*->EXFAT, hdd*/apa*/pfs*->HDD, bare bdma->no-op page value), PLDR.LAUNCH_ARGS, carousel page auto-nav page_to_opt (MMCE=1/MX4SIO=2/EXFAT=3/ATA=3/HDD=4/USB=5/SMB=7).
  • Settings: EncodeSettings (24 keys + appended SMB block: POPSTARTER_PATH ("" = Automatic; the legacy PROFILE/POPSTARTER_MODE keys are no longer written, and on load a legacy PROFILE=N pick migrates into POPSTARTER_PATH — profiles dropped 2026-07-13), 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 — the SMB connection block is appended after these by SmbAppendLines), LoadSettingsNonFatal (system.lua:3650, normalizes CRLF before parsing — a Notepad-edited sidecar used to silently revert most settings), SaveSettingsAtomic (system.lua:3598, retries the MC fallback once when a non-MC sidecar write fails) -> WriteAtomic (system.lua:2793), CommitSettingsChanges (transactional, system.lua:3912). Per-device sidecar .pldrs at APP_DIR for every device; HDD installs now persist on the HDD boot partition via the PLDR.HDD.EnsureBootPartitionWritable RW mount take-over (system.lua:2159) — no mc0: carve-out. mc0:/POPSTARTER/.pldrs remains only a legacy fallback. See STATE.md > Settings (single-device parity).
  • Game-list builders: GetPS1GameLists (system.lua:4529, MMCE/MX4SIO, bare basenames), BuildMassGameListByType (system.lua:4593, USB, root|name), HDD.BuildGameList (system.lua:4720, partition|relpath). HDD cache (CreateCache system.lua:4880 / ReadCache system.lua:4927) is gated on the PLDR.GAMELIST_CACHE setting (opt-in, default OFF; default set at system.lua:3672; runtime gate checks live in the cache save/load helpers). USECACHE (system.lua:2048) is a dead legacy flag. The same GAMELIST_CACHE gate covers the USB/MMCE/MX4SIO list cache (SaveGameListCache system.lua:4816; there is no separate ReadGameListCache function — the HDD reader is PLDR.HDD.ReadCache system.lua:4927).
  • Launch engine: LaunchEngine (system.lua:5436), RunPOPStarterGame (system.lua:5653), BuildPopstarterLaunchCommand (system.lua:5631, sets per-device reboot_iop). HDD pre-exec gate ValidateHddPopstarterExecGate (system.lua:1655). Keep-PFS-mask prep PrepareForExternalELFLaunch (local def system.lua:1120, PLDR wrapper system.lua:1793).
  • POPSTARTER.ELF resolution PLDR.ResolveLaunchPopstarterPath (system.lua) is PER DEVICE, existence-gated at each step so a device with no copy falls through. REMOVABLE (USB / exFAT-ATA / MX4SIO / MMCE), in order:
    1. the explicit user-configured ABSOLUTE path (the "POPSTARTER Path" setting) when it resolves; 2. the game's own <device>:/POPS/POPSTARTER.ELF when it exists (lets a per-device build — e.g. a USB-delay POPSTARTER dropped in the USB drive's POPS folder — be used WITHOUT forcing it); 3. POPSTARTER.ELF in the launcher's own folder (cwd, ResolveCwdSidecarPopstarter); 4. the mc0:/mc1:/POPSTARTER fallback net. INTERNAL-PFS HDD (APA): 1. custom; 2. hdd0:__common/POPS/POPSTARTER.ELF (resolved through the partition machinery that preserves the D-10/D-15 partition-context
    • embedded-loader handoff); 3. cwd / boot-sidecar; 4. mc net. The 16-preset profile system was removed 2026-07-13 (R3Z3N review): PLDR.POPSTARTER_PATH is the single value, "" = Automatic (the ladder), a set path = step-1 custom with silent fallback; the ladder itself is unchanged. Validating on hardware.
  • Startup ordering at module end: LoadSettingsNonFatal -> AutoInitStartupBackends (system.lua:3920) -> SurfaceLaunchArgsDebug -> AutoLaunchFromLaunchArgs (system.lua:6139), then the single render loop (system.lua:6364-6376; dispatch body 6365-6375 per-scene Play() + UI.flip()).

bin/POPSLDR/ui.lua — the entire UI table, no main loop

  • Defines one UI table literal (ui.lua:446) and return UI (ui.lua:4991). Contains all scenes, the scene/transition state machine, notification queue, busy overlay, cover-art cache, path-editor keyboard, modals, and input layer. It has NO main loop — the loop lives at the bottom of system.lua.
  • Scenes enum UI.SCENES (ui.lua:448-458): GUSBFAT=1, GSMB=3 (the MMCE list page — despite the name, NOT the network-SMB page), GMX4SIO=4, GHDD=5 (GAPAHDD aliases 5), GBDMHDD=6, GSMBNET=7 (the live SMB (v1) network page), MMAIN=8, MPROFILE=9, CREDITS=10. SMB (v1) now has its OWN dedicated scene (GSMBNET=7); it does NOT reuse GSMB=3.
  • Main menu carousel UI.MainMenu (table ui.lua:3976, opts ui.lua:3978; the CONFIRM/Play dispatch is the MainMenu Play handler at ui.lua:3997, OPT switch ~4227-4404). Game list UI.GameList (table ui.lua:2324), launch trigger LaunchSelectedGame (ui.lua:2750). Settings page UI.ProfileQuery (table ui.lua:2976, Play ui.lua:2979). DKWDRV/BOOT.ELF/exit handoffs OpenDKWDRV (ui.lua:1371), LaunchBootElf (ui.lua:1592), ConfirmExit.
  • Input layer UI.Pad.Listen (ui.lua:4440) — folds the LEFT ANALOG STICK into the d-pad bits and resolves nav. Both timing concerns here are now FRAME-COUNTED, not wall-clock (Timer.getTime() reads microseconds on PS2, so the old _ms gates fired every frame):
    • Nav auto-repeat resolve_nav (ui.lua:4583): nav_fps = 50 when SCR.Y>=512 else 60 (ui.lua:4580); NAV_DELAY_FRAMES = ceil(nav_fps*0.6), NAV_RATE_FRAMES = ceil(nav_fps*0.2) (ui.lua:4581-4582); a per-direction UI.Pad.NavHoldFrames counter (ui.lua:4430) ticks once per frame. Press fires immediately; only UP/DOWN repeat (~0.6 s delay then ~0.2 s, ~5/s) — LEFT/RIGHT stay edge-only (ui.lua:4607-4610).
    • Stick→d-pad fold (ui.lua:4469-4511) is GATED on Pads.getMode() reporting PAD_ANALOG/PAD_DUALSHOCK (ui.lua:4470-4475) plus a per-axis hysteresis latch (StickV/StickH, assert |v|>64, release <40; ui.lua:4479-4503). On a digital/non-analog pad the fold is skipped and any latch is dropped (ui.lua:4506-4511), so stale analog bytes can't inject a phantom PAD_UP/LEFT.
  • Cover-art preview box (game-list render, ui.lua:2440-2591) layers two placeholder assets and NO longer draws a "Cover disabled" text label: no live cover → IMG.cover_default (ui.lua:2582-2583); preview ENABLED but the game has no cover → cover_default with IMG.cover_missing overlaid (ui.lua:2585-2586); a LIVE cover uses its own COVER_W inset (ui.lua:2573-2576). The default, the missing overlay, and IMG.frame all share the frame's aspect-corrected, right-anchored rect (frame_x/draw_y/frame_w/frame_h, ui.lua:2572-2590) so they register with the jewel-case window on both NTSC and PAL.
  • Right-stick description scroll (ui.lua:2669-2700) is FRAME-COUNTED too via UI.GameList.DescScrollFrames (ui.lua:2334): step every ceil(_secs*fps) frames (ui.lua:2684-2687), fixed at the Fast pace (_secs = 0.15, ~7 lines/sec; the Fast/Medium/Slow "Description scroll speed" setting was removed).
  • Cover-art LRU CoverCache (ui.lua:305-374), candidate builder BuildCoverCandidates (ui.lua:190). On removable devices the cover is ONE path with an EXACT filename (EXP71): BuildCoverCandidates returns a single candidate <device-root>/ART/<gamefilename>_COV.png. The Cover/details folder setting was removed in EXP35 (PLDR.ART_LOCATION still parses but is INERT), there is no POPS/ART mode, no beside-the-.vcd fallback, and no disc-marker-stripped family name — so multi-disc games need art per disc. HDD/PFS is fixed to hdd0:__common/POPS/ART/<gamefilename>_COV.png via PLDR.ResolveHddPartitionReadablePath. The .txt details sidecar does NOT ride the cover candidates: removable devices compute <device-root>/ART/<gamefilename>.txt (no _COV) and hand it to the resident cover worker (Graphics.coverLoadTextPath), and HDD resolves it separately. GetOrLoad loads via Graphics.loadImage/fopen directly with NO doesFileExist/open() pre-probe -- a redundant-syscall cleanup, since ps2sdk routes open and fopen through the same libcglue _open (nested reads work; OPL reads mass:/ART/ the same way). A missing POPS/ART cover is a filename/location issue, and the list view's "No cover. Looked for: " caption and its last_cover_probe state were REMOVED (EXP42) -- a missing cover just draws the placeholder. Cover drawing is gated by the persisted COVER_ART setting (UI.CoverPreviewEnabled, applied via UI.SetCoverPreview); the Square toggle is gone.
  • WRITE-GUARD GOTCHA: __newindex metatables on UI.MainMenu and UI (ui.lua:4957 & 4979) silently DROP writes to UI.MainMenu.OPT (unless Carousel.allowOptWrite, ui.lua:4960) and UI.CURSCENE (unless Transition.allowSceneWrite, ui.lua:4981). Build-info display reads BUILD_INFO.txt (LoadBuildInfo, ui.lua:4687).

bin/POPSLDR/images.lua — embedded UI glyph/chrome atlas

  • IMG_REGISTRATIONS (images.lua:11-37): 25 {key, filename} pairs (device icons, backgrounds, splash layers, button/d-pad glyphs, frame, the optional legacy default, and the cover placeholders cover_default + cover_missing; the old missing key is GONE). IMG_SOURCES maps each key to its bare filename (images.lua:39-44); the cover_default/cover_missing covers are consumed by the ui.lua preview box via IMG.cover_default/IMG.cover_missing. Lazy IMG metatable (__index, images.lua:51) fetches each PNG by that filename through System.getEmbeddedAsset -> Graphics.loadImageEmbedded (images.lua:56-59) and caches it. IMG_FALLBACKS (images.lua:46) is now an EMPTY table — the old default -> missing fallback was removed with MISSING.png, so an unresolved key just returns nil (marked in IMG_FAILED). This is UI chrome, NOT per-game box art (game covers are UI.CoverCache in ui.lua).

3. Boot script (etc/)

  • etc/boot.lua (HDD-boot branch etc/boot.lua:37) mounts the HDD boot partition to pfs1: (warning "NEVER USE IT FOR ANYTHING ELSE", boot.lua:48), normalizes cwd to pfs1: (boot.lua:63-64), loads fonts, then require("system") (boot.lua:181). System.sleep(2) (boot.lua:47) is SECONDS, not ms (the binding calls C sleep, luasystem.cpp:774-780), so it is a full 2-second HDD settle. CI requires this file end with a 0x0A newline.
  • etc/update_lua_globals.sh — dev helper for syncing Lua globals.

4. External ELF-handoff layer (src/elf_loader/)

  • src/elf_loader/src/elf.c — the EE-side parent loader. Central reboot/HDD routing fork LoadELFFromFileExecPS2RebootIOPWithPartition (elf.c:618): HDD partition AND filename both HDD-backed -> ExecuteHddBackedViaEmbeddedLoader (elf.c:648); resolved-path/partition HDD-backed -> same (elf.c:656); else direct SifLoadElf (elf.c:661) -> unmount-pfs (keep-mask) -> SifIopReset -> reload rom0:SIO2MAN/MCMAN/MCSERV -> ExecPS2. BOOT.ELF and DKWDRV-HDD special-cases in LoadELFFromFileWithPartition (elf.c:481, BOOT.ELF mc-case elf.c:499, DKWDRV elf.c:505/519). ExecuteViaEmbeddedLoader (elf.c:397) writes BRAM metadata (magic POPL, addr 0x00083C00; defines elf.c:159-160) and ExecPS2s the child.
  • src/elf_loader/src/loader/src/loader.c — the BRAM child loader. main() (loader.c:280) reads metadata at 0x00083C00 (loader.c:144-145) and branches three ways before ExecPS2: filexio-direct (loader.c:373-379); the HDD-partition-context branch for D-10 (loader.c:381-403 — unmounts the pfs prefix, then SifExitRpc + SifExitCmd at loader.c:396-397); and the generic/empty-context branch for BOOT.ELF/DKWDRV (loader.c:404-427 — SifExitRpc only at loader.c:404, INTENTIONALLY omits SifExitCmd, comment loader.c:405; the comment marks that omission as the historical D-15-pass vs D-10-fail difference). Do not add SifExitCmd to the generic branch.
  • src/elf_loader/loader.c — committed ~6.5 MB bin2c blob of the built loader.elf (symbol loader_elf), regenerated by make elfloader.
  • src/elf_loader/Makefile / src/elf_loader/src/loader/Makefile — the nested two-stage loader build (stage 1 builds loader.elf into BIOS memory; stage 2 bin2c-embeds it and archives libcustom-elf-loader.a).

5. IOP modules (iop/)

  • iop/bdm_query/bdm_query.c — in-tree IOP RPC helper (id 0xB0D10B00 defined bdm_query.c:11, registered bdm_query.c:76; handler bdm_query.c:36) enumerating live block devices via bdm_get_bd(); the EE side classifies each backend by driver-name substring. Built from source (Makefile:241-245).
  • iop/embed/ — pinned/in-tree IRX blobs bin2c'd into the ELF: bdm.irx, bdmfs_fatfs.irx, bdmfs_vfat.irx, mx4sio_bd.irx (+mx4sio_bd_mini.irx), plus the PS2SDK_MX4SIO and BDMASSAULT_MX4SIO source pins. The active mx4sio_bd.irx is pinned from iop/embed/PS2SDK_MX4SIO (Makefile:247-251). Other IRX (iomanX, fileXio, sio2man, mcman, mcserv, padman, libsd, usbd, audsrv, usbmass_bd, cdfs, ps2dev9, ps2atad, ps2hdd-osd, ps2fs, mmceman, ata_bd) resolve from $(PS2SDK)/iop/irx/ (vpath Makefile:216, object list Makefile:88-93).

6. Controller modules (modules/)

  • modules/ds34usb/ and modules/ds34bt/ — DS3/DS4 USB and Bluetooth support, built as EE static libs (EXT_LIBS, Makefile:72) and as IRX (ds34usb.o/ds34bt.o, Makefile:85).
  • modules/pademu/ — pad-emulation IOP module sources (ds34bt/ds34usb/pademu).
  • modules/Rules.bin.make — shared module build rules.

7. On-card payload (bin/POPSLDR/, non-source)

  • POPSTARTER.ELF (the PS1 emulator front-end launched per game), PATCH_5.BIN, boot.adp, APPINFO.PBT, title.cfg, MC icon set (icon.sys/list.icn/copy.icn/del.icn plus .bdma variants), the IMG/ source PNGs, and device-variant IRX (usbd.irx.{usbexfat,mx4sio,mmce}, usbhdfsd.irx.{usbexfat,mx4sio,mmce}).

8. Build / package / CI

  • Makefile — builds the EE ELF, bin2c-embeds all Lua/PNG/IRX assets (BIN2S = $(PS2SDK)/bin/bin2c, Makefile:67; EMBEDDED_RSC Makefile:96-105), links the child-loader lib, strips, and runs ps2-packer to produce bin/POPSLOADER.ELF (Makefile:118-120). make elfloader (Makefile:257-262) force-regenerates the child loader. RESET_IOP = 1 (Makefile:34) compiles in the pre-main IOP reset. default.png is the only OPTIONAL embed: it is added to OPTIONAL_EMBEDDED_RSC and defines -DHAVE_ASSET_DEFAULT_PNG=1 ONLY when the file is present in the checkout (wildcard guard, Makefile:72-75); everything else (incl. the new cover_default.png / cover_missing.png, BIN2S rules Makefile:179-182) is mandatory.
  • EMBED MECHANISM (adding/removing an embedded asset is THREE coordinated places, NOT an auto-glob, and they must be kept in sync MANUALLY — there is no machine-checked embed manifest): (a) Makefile — a BIN2S PNG rule plus the .o in EMBEDDED_RSC; (b) src/embed_assets.cpp — an extern declaration plus an ASSET_ENTRY in BOTH the bare-name and the POPSLDR/IMG/-prefixed mirror tables; (c) bin/POPSLDR/images.lua IMG_REGISTRATIONS (looked up by the bare filename). The MISSING.png removal (−62 KB ELF) touched all three plus the ui.lua draw path and was the reference example of this dance.
  • .github/workflows/compilation.yml — CI on all branches/tags/PRs/dispatch: runs the now-LIVE embedded-Lua syntax gate (apk add lua5.4 + luac5.4 -p on bin/POPSLDR/*.lua + etc/boot.lua, hard-fail on syntax error — it used to silently no-op because the pinned ps2dev image had no luac; catches SYNTAX only, so runtime nil-global / type / load-order errors stay invisible), generates BUILD_INFO.txt, runs make clean elfloader all, enforces embed-identity gates (three string markers in the ELF + loader.c parity), and packages the strict-verified POPSLOADER.zip install bundle as an artifact (no GitHub release). The redistributable POPSTARTER.ELF ships in PS1_POPSLOADER/ next to POPSLOADER.ELF (compilation.yml:161) AND in POPS/ (compilation.yml:171), both on the manifest's required-file list. See STATE.md > CI / release.
  • .github/workflows/rolling-release.yml — on push to dev (rolling-release.yml:6) and PR events: bundles the ELF + full git-tracked source and force-updates the rolling-release prerelease via the GitHub API. The redistributable POPSTARTER.ELF now ships at the ZIP ROOT next to POPSLOADER.ELF (rolling-release.yml:182) AND in POPS/ (rolling-release.yml:192); POPS/PATCH_5.BIN and a POPSTARTER/ pack folder also ship at the root (rolling-release.yml:173). (POPS engine binaries remain NON-redistributable — users supply their own.)
  • .github/workflows/opencode.yml/oc comment bot (DeepSeek). Not part of release packaging or runtime behavior.

Current Feature Surface by Main Menu Option

Dispatch in the MainMenu Play handler (ui.lua:3997), OPT switch ~ui.lua:4227-4404.

  • MMCE (OPT1): implemented.
  • MX4SIO (OPT2): implemented.
  • HDD (PFS) (OPT4): implemented (routes to scene GHDD=5).
  • USB (OPT5): implemented.
  • Disc (DKWDRV) (OPT8): implemented.
  • HDD (exFAT) (OPT3): implemented — scans the exFAT internal drive as a mass: backend (BDMA ata) via InitATAPopsRoot + GetPS1GameLists -> scene GBDMHDD=6 (ui.lua:4349). Classified by exact ioctl driver-name ata. Validating on hardware.
  • i.Link (OPT6): NOT implemented (ui.lua:4462).
  • SMB (v1) (OPT7): implemented — routes to scene GSMBNET=7. SMB / Network settings (server IP, share, user/password, IP assignment DHCP-or-static, port, games path/cwd, link mode; IP addressing only) plus an "SMB modules" install toggle that copies the POPStarter in-game SMB streaming pack (6 IRX: poweroff/ps2dev9/ps2ip/ps2smap/smbman/SMSUTILS) into mc:/POPSTARTER and generates IPCONFIG.DAT + SMBCONFIG.DAT from the settings (backfilled on every settings save while the pack is installed). CONNECT is LAZY (net stack
    • share open only on entering the SMB page or a settings action, never at boot) via the luasystem.cpp EnsureNet/connectSMB bindings; BROWSE scans the share's POPS folder and lists VCDs; a blank Share field opens an in-UI picker driven by GETSHARELIST; LAUNCH hands off to POPStarter with argv0 selector smb:/POPS/SB.<name>.ELF (POPStarter streams the VCD from its own smb:/POPS mount via mc:/POPSTARTER/SMBCONFIG.DAT); DISCONNECT (CLOSESHARE+LOGOFF) on leaving the page. NetBIOS is deferred (address type must be IP). Implemented this cycle, CI+Rolling green, validating on hardware — the exact argv0 device prefix POPStarter accepts (fallbacks mass:/POPS/SB.<name>.ELF then mass:/SB.<name>.ELF), the connect handshake, and the GETSHARELIST DMA are the hardware-only unknowns. See the luasystem.cpp SMB notes.

Preservation Contracts (hardware-load-bearing — do not regress)

  • D-10 (HDD POPSTARTER + HDD game): the BRAM child-loader route via the HDD-partition-context branch (loader.c:381-403, SifExitRpc+SifExitCmd at 396-397) — unmount the pfs prefix, then SifExitRpc + SifExitCmd. (The generic branch loader.c:404-427, which omits SifExitCmd, is the BOOT.ELF/DKWDRV path, not D-10.)
  • D-15 (non-HDD POPSTARTER + HDD game): keep-PFS mask preserves the boot partition's pfs1: slot (elf.c keep-mask, system.lua:1120).
  • DKWDRV from HDD inherits the same embedded-loader path as POPSTARTER (elf.c:628-644); the V3 direct-reset route black-screened on hardware.
  • BRAM metadata struct (partition_context[128], load_path[256], magic POPL) must stay byte-identical between writer (elf.c) and reader (loader.c).
  • Settings sidecar (single-device parity): USB/MC/MMCE/MX4SIO and HDD all use the per-device .pldrs sidecar. HDD installs persist on the HDD boot partition via the PLDR.HDD.EnsureBootPartitionWritable RW mount take-over (system.lua:2159) — there is no mc0: HDD carve-out. That take-over is now load-bearing for HDD settings save and HDD in-app .hide; don't regress it. (Supersedes the old HDD-to-MC exception.) See STATE.md > Preservation Contracts and STATE.md > Settings (single-device parity).

Settle note: Load_HDD_IRX routes through EnsureAtaBdm(), which applies a 1-second settle before loading ata_bd and another after it (see EnsureAtaBdmModulesInner in src/luasystem.cpp). The older "no cold-dev9 settle / fix lives on a separate branch" note is superseded — rolling publishes from dev, which is this branch.

Primary Change Entry Points

  • Settings persistence/apply: bin/POPSLDR/system.lua, bin/POPSLDR/ui.lua.
  • Device detection/classification: bin/POPSLDR/system.lua, src/luasystem.cpp, iop/bdm_query/bdm_query.c.
  • Launch handoff/argv/path: bin/POPSLDR/system.lua, src/luasystem.cpp, src/elf_loader/src/elf.c, src/elf_loader/src/loader/src/loader.c.
  • HDD bring-up/mount: src/luaHDD.cpp, bin/POPSLDR/system.lua.
  • Embedded asset add/resolve: Makefile (bin2c rule + EMBEDDED_RSC), src/embed_assets.cpp, plus the consuming Lua table.
  • Packaging/release: Makefile, .github/workflows/compilation.yml, .github/workflows/rolling-release.yml.