Components
← POPSLoader docs · view on GitHub ↗
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
devrolling 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 /strUtilssource (commit f83dbbb) — have since
been removed from the tree and are documented as removed below. (The old
orphansrc/luaSMB.cppis gone; SMB (v1) is now a real, wired feature — a C
binding inside theSystem.*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 BEFOREmain()inside newlib's
memory-init hook. Gated on-DRESET_IOP(set in Makefile:34, applied
Makefile:59-61). It performsSifExitRpc -> 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 plainSifIopReset
(ps2sdk #425). Anyone readingmain()top-down will miss this reset.detectBootDeviceHintFromArgv0()(main.cpp:134) derives an advisory pre-Lua
boot-device hint fromargv[0]. HDD variantshdd/pfs/ata/apaall map to
"HDD"(main.cpp:155-160); bothmassandusbmap 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=
(-modeis a pure alias, both writelaunch_arg_page, main.cpp:220-226),
-game=,-debug.main()(main.cpp:439) parses launch args, installs SBV patches, then loads
the embedded IRX stack viaSifExecModuleBuffer(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 fromluasystem.cpp/luaHDD.cpp.- After IRX bring-up
main()sets boot path/app dir, inits gsKit + pad, then
enters a loop running embeddedboot.luaviarunScript.
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.luawas
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, clearspackage.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 separateluaSMB_init—
the SMB (v1) network client lives inside theSystem.*surface
(initSMB/connectSMB/disconnectSMB, registered byluaSystem_init;
the Lua-sidePLDR.InitSMBPopsRootwraps them), not a standalone module.
The old orphansrc/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 lookupGetMassMountDriverNameBySlot
(luasystem.cpp:312). - MX4SIO init enforces USB mass first:
lua_mx4sio_init(luasystem.cpp:1298)
callsEnsureUsbMass()(luasystem.cpp:1316) before loadingmx4sio_bd.irx
(luasystem.cpp:1318). - External-ELF launch bindings:
lua_loadELF(luasystem.cpp:974),
lua_loadELFWithPartition(luasystem.cpp:1020, requiresreboot_iop != 0and
anhdd?: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
bindinglua_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 stackEnsureNet
(luasystem.cpp:1412): brings up dev9 once via the sharedEnsureDev9/
g_dev9_loadedguard, then loadsnetman+smap+ps2ips+smbmanand
ps2ipand callsNetManInit— NEVER at boot, only on a menu/settings action.
Bindings registered in theSystem.*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 theps2smb.hdevctls (LOGON/ECHO/OPENSHARE) onsmb0:. A
blank Share field triggersSMB_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.irxis
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 onlyremove()s the source if
the copy returned 0 (luasystem.cpp:760-762).copy_file_contentsreturns -1
on an open/short-write/mid-stream-read error, so a failed copy no longer
deletes the source. (There is no longer a separatelua_movefile; only
lua_renameis registered, luasystem.cpp:1330.copyFileis 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 asHDD.Initializeat luaHDD.cpp:173)
callsEnsureAtaBdm()(dev9 -> bdm -> bdmfs_fatfs ->ata_bd, i.e. ps2atad
built withATA_ENABLE_BDM=1so one atad instance serves APA/PFS and exFAT),
then ps2hdd-osd -> ps2fs, with HDD args-o 4 -n 20and PFS args
-m 4 -o 10 -n 40. Plainps2atadis no longer loaded. There ARE settles now:
sleep(1)before theata_bdload andsleep(1)after it
(luasystem.cppEnsureAtaBdmModulesInner), matching NHDDL/wLaunchELF ordering.GetHDDStatusviaHDIOC_STATUS(luaHDD.cpp:93-96); on-demand mount
MountPart/mnt(luaHDD.cpp:21-92) producingpfs%d:/mount points.
HDD.MountPartitionreturns(ok, rc)— the second value is the raw
fileXioMountrc (2026-07-09 diagnostics;mntno 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, ornil, rcbefore ps2hdd loads. Consumed by
PLDR.HDD.DiscoverPartitionGames(partition-installedPP./__.games).
src/embed_assets.cpp — runtime name -> embedded blob resolver
embedded_get()(embed_assets.cpp:195) normalizes paths (stripsembed:/,
embed_assets.cpp:205;./, embed_assets.cpp:209; leading/,
embed_assets.cpp:212) and resolves against the staticg_embedded_assetstable
(embed_assets.cpp:93-181) viaembedded_find(embed_assets.cpp:184). There is
NO icon/MISSING.pngfallback — 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.pngis 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.pngplaceholders 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.loadImageEmbeddedpath used byimages.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 OPLrmSetOverscanexactly (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_overscanis 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 surfaceScreen.setOverscan(permille)/Screen.getOverscan()
(lua_set_overscanluaScreen.cpp:64 /lua_get_overscanluaScreen.cpp:71;
registered luaScreen.cpp:162-163) wrapping graphics.cpp'sset_overscan/
get_overscan.src/luacontrols.cpp(luaControls_init) — pad input, registered under the
globalPads(NOTControls;Pads_functionsluacontrols.cpp:273,
lua_setglobal "Pads"luacontrols.cpp:289).Pads.getMode()
(lua_getmodeluacontrols.cpp:70; registered luacontrols.cpp:278) returns the
LIVE negotiated controller mode viapadInfoMode(port,0,PAD_MODECURID,0)
(luacontrols.cpp:78) — high nibble 0x5 analog / 0x7 DualShock / 0x4 digital / 0
no-data. The pre-existingPads.getType()(lua_gettypeluacontrols.cpp:9;
registered luacontrols.cpp:277) readsPAD_MODETABLE(a capability-table entry,
luacontrols.cpp:17) and is NOT the live mode —ui.lua's stick-fold gate must
usegetMode.getLeftStick/getRightStick(lua_getleftluacontrols.cpp:82
/lua_getrightluacontrols.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_DUALSHOCKglobals 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 theSystem.*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 underResolveBootContext.
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, barebdma->no-op page value),PLDR.LAUNCH_ARGS,
carousel page auto-navpage_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
.pldrsat APP_DIR for every device; HDD installs now persist on the HDD
boot partition via thePLDR.HDD.EnsureBootPartitionWritableRW mount
take-over (system.lua:2159) — nomc0:carve-out.mc0:/POPSTARTER/.pldrs
remains only a legacy fallback. SeeSTATE.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
(CreateCachesystem.lua:4880 /ReadCachesystem.lua:4927) is gated on the
PLDR.GAMELIST_CACHEsetting (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 sameGAMELIST_CACHE
gate covers the USB/MMCE/MX4SIO list cache (SaveGameListCache
system.lua:4816; there is no separateReadGameListCachefunction — the HDD
reader isPLDR.HDD.ReadCachesystem.lua:4927). - Launch engine:
LaunchEngine(system.lua:5436),RunPOPStarterGame
(system.lua:5653),BuildPopstarterLaunchCommand(system.lua:5631, sets
per-devicereboot_iop). HDD pre-exec gateValidateHddPopstarterExecGate
(system.lua:1655). Keep-PFS-mask prepPrepareForExternalELFLaunch
(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.ELFwhen it exists (lets a per-device build — e.g.
a USB-delay POPSTARTER dropped in the USB drive'sPOPSfolder — be used
WITHOUT forcing it); 3.POPSTARTER.ELFin the launcher's own folder (cwd,
ResolveCwdSidecarPopstarter); 4. themc0:/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_PATHis 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-scenePlay()+UI.flip()).
bin/POPSLDR/ui.lua — the entire UI table, no main loop
- Defines one
UItable literal (ui.lua:446) andreturn 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 MainMenuPlayhandler at ui.lua:3997, OPT switch
~4227-4404). Game listUI.GameList(table ui.lua:2324), launch trigger
LaunchSelectedGame(ui.lua:2750). Settings pageUI.ProfileQuery(table
ui.lua:2976,Playui.lua:2979). DKWDRV/BOOT.ELF/exit handoffsOpenDKWDRV
(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_msgates 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.NavHoldFramescounter (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_defaultwithIMG.cover_missingoverlaid (ui.lua:2585-2586);
a LIVE cover uses its own COVER_W inset (ui.lua:2573-2576). The default, the
missing overlay, andIMG.frameall 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 everyceil(_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):BuildCoverCandidatesreturns a
single candidate<device-root>/ART/<gamefilename>_COV.png. The
Cover/details folder setting was removed in EXP35 (PLDR.ART_LOCATIONstill
parses but is INERT), there is noPOPS/ARTmode, 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.pngvia
PLDR.ResolveHddPartitionReadablePath. The.txtdetails 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.
GetOrLoadloads
viaGraphics.loadImage/fopendirectly with NOdoesFileExist/open()
pre-probe -- a redundant-syscall cleanup, since ps2sdk routesopenandfopen
through the same libcglue_open(nested reads work; OPL readsmass:/ART/the
same way). A missingPOPS/ARTcover is a filename/location issue, and the list
view's "No cover. Looked for:" caption and its last_cover_probestate
were REMOVED (EXP42) -- a missing cover just draws the placeholder. Cover drawing
is gated by the persistedCOVER_ARTsetting (UI.CoverPreviewEnabled, applied
viaUI.SetCoverPreview); the Square toggle is gone. - WRITE-GUARD GOTCHA:
__newindexmetatables onUI.MainMenuandUI
(ui.lua:4957 & 4979) silently DROP writes toUI.MainMenu.OPT(unless
Carousel.allowOptWrite, ui.lua:4960) andUI.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
legacydefault, and the cover placeholderscover_default+cover_missing;
the oldmissingkey is GONE).IMG_SOURCESmaps each key to its bare filename
(images.lua:39-44); thecover_default/cover_missingcovers are consumed by
the ui.lua preview box viaIMG.cover_default/IMG.cover_missing. LazyIMG
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 -> missingfallback was removed withMISSING.png, so an unresolved
key just returns nil (marked inIMG_FAILED). This is UI chrome, NOT per-game
box art (game covers areUI.CoverCachein ui.lua).
3. Boot script (etc/)
etc/boot.lua(HDD-boot branch etc/boot.lua:37) mounts the HDD boot partition
topfs1:(warning "NEVER USE IT FOR ANYTHING ELSE", boot.lua:48), normalizes
cwd topfs1:(boot.lua:63-64), loads fonts, thenrequire("system")
(boot.lua:181).System.sleep(2)(boot.lua:47) is SECONDS, not ms (the binding
calls Csleep, luasystem.cpp:774-780), so it is a full 2-second HDD settle.
CI requires this file end with a0x0Anewline.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 forkLoadELFFromFileExecPS2RebootIOPWithPartition(elf.c:618): HDD
partition AND filename both HDD-backed ->ExecuteHddBackedViaEmbeddedLoader
(elf.c:648); resolved-path/partition HDD-backed -> same (elf.c:656); else
directSifLoadElf(elf.c:661) -> unmount-pfs (keep-mask) -> SifIopReset ->
reload rom0:SIO2MAN/MCMAN/MCSERV -> ExecPS2. BOOT.ELF and DKWDRV-HDD
special-cases inLoadELFFromFileWithPartition(elf.c:481, BOOT.ELF mc-case
elf.c:499, DKWDRV elf.c:505/519).ExecuteViaEmbeddedLoader(elf.c:397) writes
BRAM metadata (magicPOPL, addr0x00083C00; 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 at0x00083C00(loader.c:144-145) and branches
three ways beforeExecPS2: filexio-direct (loader.c:373-379); the
HDD-partition-context branch for D-10 (loader.c:381-403 — unmounts the pfs
prefix, thenSifExitRpc+SifExitCmdat loader.c:396-397); and the
generic/empty-context branch for BOOT.ELF/DKWDRV (loader.c:404-427 —
SifExitRpconly at loader.c:404, INTENTIONALLY omitsSifExitCmd, comment
loader.c:405; the comment marks that omission as the historical D-15-pass vs
D-10-fail difference). Do not addSifExitCmdto the generic branch.src/elf_loader/loader.c— committed ~6.5 MB bin2c blob of the built
loader.elf(symbolloader_elf), regenerated bymake elfloader.src/elf_loader/Makefile/src/elf_loader/src/loader/Makefile— the nested
two-stage loader build (stage 1 buildsloader.elfinto BIOS memory; stage 2
bin2c-embeds it and archiveslibcustom-elf-loader.a).
5. IOP modules (iop/)
iop/bdm_query/bdm_query.c— in-tree IOP RPC helper (id0xB0D10B00defined
bdm_query.c:11, registered bdm_query.c:76; handler bdm_query.c:36) enumerating
live block devices viabdm_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 thePS2SDK_MX4SIOandBDMASSAULT_MX4SIO
source pins. The activemx4sio_bd.irxis 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/andmodules/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.icnplus.bdmavariants), theIMG/
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_RSCMakefile:96-105),
links the child-loader lib, strips, and runsps2-packerto 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.pngis the only OPTIONAL embed: it is added to
OPTIONAL_EMBEDDED_RSCand defines-DHAVE_ASSET_DEFAULT_PNG=1ONLY when the
file is present in the checkout (wildcard guard, Makefile:72-75); everything else
(incl. the newcover_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— aBIN2SPNG rule plus the
.oinEMBEDDED_RSC; (b)src/embed_assets.cpp— anexterndeclaration plus
anASSET_ENTRYin BOTH the bare-name and thePOPSLDR/IMG/-prefixed mirror
tables; (c)bin/POPSLDR/images.luaIMG_REGISTRATIONS(looked up by the bare
filename). TheMISSING.pngremoval (−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
onbin/POPSLDR/*.lua+etc/boot.lua, hard-fail on syntax error — it used to
silently no-op because the pinned ps2dev image had noluac; catches SYNTAX
only, so runtime nil-global / type / load-order errors stay invisible),
generatesBUILD_INFO.txt, runsmake clean elfloader all, enforces
embed-identity gates (three string markers in the ELF + loader.c parity), and
packages the strict-verifiedPOPSLOADER.zipinstall bundle as an artifact (no
GitHub release). The redistributablePOPSTARTER.ELFships inPS1_POPSLOADER/
next toPOPSLOADER.ELF(compilation.yml:161) AND inPOPS/
(compilation.yml:171), both on the manifest's required-file list.
SeeSTATE.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 therolling-releaseprerelease via the GitHub API. The
redistributablePOPSTARTER.ELFnow ships at the ZIP ROOT next to
POPSLOADER.ELF(rolling-release.yml:182) AND inPOPS/
(rolling-release.yml:192);POPS/PATCH_5.BINand aPOPSTARTER/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—/occomment 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, thenSifExitRpc+SifExitCmd. (The
generic branch loader.c:404-427, which omitsSifExitCmd, is the
BOOT.ELF/DKWDRV path, not D-10.) - D-15 (non-HDD POPSTARTER + HDD game): keep-PFS mask preserves the boot
partition'spfs1: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], magicPOPL)
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.pldrssidecar. HDD installs persist on the HDD boot partition
via thePLDR.HDD.EnsureBootPartitionWritableRW mount take-over
(system.lua:2159) — there is nomc0: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.) SeeSTATE.md > Preservation Contracts
andSTATE.md > Settings (single-device parity).
Settle note:
Load_HDD_IRXroutes throughEnsureAtaBdm(), which applies a
1-second settle before loadingata_bdand another after it (see
EnsureAtaBdmModulesInnerinsrc/luasystem.cpp). The older "no cold-dev9
settle / fix lives on a separate branch" note is superseded — rolling publishes
fromdev, 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.