POPStarter DOCS

Contributing

← POPSLoader docs ยท view on GitHub โ†—

Last updated: 2026-06-21 (BETA-13 in progress)

CONTRIBUTING

Purpose

Practical contributor workflow for POPSLoader changes.

STATE.md is the canonical status doc. The shared/volatile facts โ€” the Known-Issues list, the Preservation Contracts, the Behavioral Invariants, the Settings (single-device parity) behavior, and the Hardware Status โ€” live there and are not restated here; this doc links to the relevant STATE.md section instead. Keep distinct workflow guidance here.

(Scope/runtime-invariant rules formerly in RULES.md are folded into this doc; RULES.md is archived.)

Branch and PR Expectations

Commit Discipline

Scope Discipline

Development Guardrails

The full invariant list is in STATE.md > Behavioral Invariants โ€” preserve all of it unless the task is an explicit, documented migration. Highlights a contributor must not break:
- embedded-Lua boot chain (main.cpp -> runScript("boot.lua") -> require("system")); boot/runtime Lua is embedded-only.
- transactional, per-device settings persistence โ€” edits stage in the UI and commit on Settings/Profile confirm/leave. HDD installs now persist on the HDD boot partition via the EnsureBootPartitionWritable RW take-over; there is no mc0: fallback for an HDD-cwd install (single-device parity). See STATE.md > Settings (single-device parity) and STATE.md > Preservation Contracts.
- mount-driver-based USB/MX4SIO classification (sdc/mx4 ioctl โ†’ MX4SIO; anything else โ†’ USB; mx4sio_bd loads only on explicit MX4SIO evidence and requires usbmass_bd first).
- mc?:/ alias resolution (mc0 then mc1) for executable path probes.
- backend-specific launch policy for USB / MMCE / MX4SIO / HDD.
- the BDMA โŸบ POPSTARTER-MC-folder interlock (BDMA can't be enabled while the folder is off; the folder can't be disabled while BDMA is on) and the destructive folder-disable confirm. See STATE.md > Behavioral Invariants.
- in-app per-game .hide on every device including HDD (L3 toggle, written via the HDD RW mount take-over). See STATE.md > Behavioral Invariants.
- no runtime device-family lock gating (the old inert device-lock subsystem was removed in cef61af; device pages are always enterable).
- Do not add unbounded retries/poll loops in runtime paths; keep probe/retry behavior bounded and deterministic.
- Do not silently change POPStarter selector/argv behavior without explicit migration notes.
- Avoid expensive repeated rescans unless explicitly required.
- Avoid adding runtime logging unless requested.

Packaging Rules

Embedded Assets (adding / removing one)

Every Lua script, font, IRX module, and game-list image is compiled into POPSLOADER.ELF (via bin2c); there is no on-disk asset directory at runtime and the asset list is not auto-globbed. Adding or removing one means editing three coordinated places โ€” miss any one and the build breaks or the asset silently fails to load:
1. Makefile โ€” add a bin2c rule that turns the source file under bin/POPSLDR/... into asset_<name>.c (the symbol name passed to $(BIN2S) is what the C code externs), and add the resulting asset_<name>.o to the EMBEDDED_RSC list so it gets linked. (Optional/large assets can go in OPTIONAL_EMBEDDED_RSC instead, which is concatenated into EMBEDDED_RSC.)
2. src/embed_assets.cpp โ€” extern the asset_<name> symbol and its size_asset_<name>, then add an ASSET_ENTRY("<key>", asset_<name>) in both lookup tables: the bare-name table (e.g. "frame.png") and the path-prefixed table (e.g. "POPSLDR/IMG/frame.png").
3. bin/POPSLDR/images.lua (images only) โ€” add a {accessKey, "<bareFilename>"} row to IMG_REGISTRATIONS. The IMG[...] metatable looks the asset up by the bare filename through System.getEmbeddedAsset, so the second field must match the bare-name key from step 2.

Removal is the same three places in reverse (plus drop any fallback wiring). The recent MISSING.png removal (-62KB ELF) is the worked example: it deleted the bin2c rule + EMBEDDED_RSC entry, the extern + both ASSET_ENTRYs, the images.lua registration, and the now-unused IMG_FALLBACKS fallback. The cover-art placeholders cover_default.png + cover_missing.png are the current additions and follow this same pattern.

Runtime Timing (frame-count, not wall-clock)

Timer.getTime() returns microseconds, not milliseconds (src/luatimer.cpp returns the raw clock() - tick delta; CLOCKS_PER_SEC is 1e6 on the EE toolchain). Code that treats it as ms runs 1000x too fast โ€” that was the root cause of nav auto-repeat "flying" (one press scrolling many rows) before it was reworked.
- The canonical Enceladus idiom for UI cadence is frame-counting, not reading the wall clock โ€” the sibling launchers do the same. New per-direction/per-step timing increments a counter once per frame and fires at frame thresholds derived from the live field rate (nav_fps = (UI.SCR.Y >= 512) and 50 or 60). See resolve_nav / NavHoldFrames (nav auto-repeat) and DescScrollFrames (description scroll) in bin/POPSLDR/ui.lua.
- os.clock() (stock Lua, returns seconds) is the only pre-converted Lua time source and is currently unused. A couple of legacy timers (the action debounce MIN_ACTION_MS, the transition/carousel timers) still sit on the ยตs-as-ms footing but are masked by a per-frame clamp, so they are not visibly broken; prefer frame-counting (or os.clock() seconds) for any new timing rather than adding more Timer.getTime() math.

Settings Plumbing

Settings are write-staged in the UI and committed through a single funnel โ€” PLDR.CommitSettingsChanges(opts) in bin/POPSLDR/system.lua โ€” which serializes the merged state to the per-device config. A new setting touches the whole chain: the parse/serialize round-trip and the next_* merge in system.lua, plus the UI commit call in bin/POPSLDR/ui.lua (e.g. the boot-sound setting flows UI.BootSound โ†’ PLDR.CommitSettingsChanges({ boot_sound = ... }) โ†’ PLDR.BOOT_SOUND). Add the field to every link or it stages but never persists. The user-facing settings behavior (single-device parity, the HDD RW take-over) is canonical in STATE.md > Settings โ€” don't restate it here.

Documentation Sync Rules

Validation Expectations

Docs-only changes

Runtime/build changes

Hardware-sensitive behavior

Good Bug Reports

Include:
- expected behavior,
- actual behavior,
- exact repro steps,
- console/storage/backend layout,
- whether the issue occurs only after another page/backend was initialized,
- whether the result came from a local build or a CI workflow artifact.

Review Checklist