Skip to content

Commit 8f2a105

Browse files
feat(favourites): APA-HDD VCD favourites -- launch PS1 games off the internal HDD from Favourites
Completes VCD-favourite coverage: the APA-formatted internal HDD (HDD_MODE) now implements itemLaunchVcd, so its PS1/.VCD games can be favourited and launched from the Favourites tab like every other device. (Previously deferred -- R3 was a no-op there.) The challenge is that the HDD VCD list (hddVcdGames + the index-parallel hddVcdParts, which records each game's owning __.POPS partition) is only built while the HDD page is in its VCD view, so from the Favourites tab the owning partition for a given name may be unknown. hddLaunchVcd resolves it by name: - hddDoLaunchVcd() is the existing in-view POPSTARTER handoff, extracted verbatim (selector + hdd0:<part>: target built on stack BEFORE deinit frees the list); the in-view branch now calls it too -- one code path, behaviour-identical. - hddLaunchVcd() finds the name in hddVcdGames; the WARM path (visited the HDD VCD view this session -> the list persists across the L3 switch) needs no rescan. The COLD path (re)scans via hddBuildVcdGameList -- the SAME single-pfs0:-slot partition walk the VCD view uses (mounts each __.POPS on pfs0:, scans, restores pfs0:); it never opens a 2nd pfs slot, so the documented pfs1: single-slot wedge can't fire. FREEZE GUARD (adversarial review caught this): the cold rescan runs on the launch (main) thread, where -- unlike the in-view rescan (IO thread, gated on no pending art/IO) -- the cache worker can still hold an open fd reading a cover from pfs0:. Umounting pfs0: under it is the HDD-freeze class. So hddLaunchVcd quiesces the art + IO workers (cacheAbortMmceImageLoadsTimed + cacheCancelPendingImageLoadsTimed + ioBlockOps) before the rescan, exactly as deinit() does, then restores. Verified no use-after-free (name/part copied to stack before deinit) and graceful failure when the HDD isn't mounted (scan returns 0 -> "POPSTARTER not found", no crash). favResolve/favGetConfig/favGetImage already cover HDD generically via itemGetPrefix (gHDDPrefix=pfs0:OPL/): HDD VCD favourites get the SAME pfs0:OPL/ART cover art as the HDD VCD view + the PS1 #DiscType badge. The opl.c itemLaunchVcd==NULL guard stays as a future-proof backstop. docs/VCD.md updated (caveat removed). REMAINING: HW validation (HDD POPSTARTER launch + the cold-path rescan; no emulator coverage for the HDD POPSTARTER path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bf99d9d commit 8f2a105

5 files changed

Lines changed: 79 additions & 28 deletions

File tree

docs/VCD.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ A VCD favourite launches through POPSTARTER straight from the Favourites tab —
4444
source device page is currently showing its disc list — and carries its PS1 cover art and
4545
disc badge with it.
4646

47-
This works for VCDs on **USB, MX4SIO, the internal exFAT HDD, SMB, and MMCE**. PS1 favourites
48-
for games on an **APA-formatted internal HDD** aren't supported yet (R3 there does nothing on a
49-
VCD), since that layout keeps each PS1 game on its own `__.POPS` partition; the common internal
50-
exFAT HDD is covered.
47+
This works on **every device with a VCD view**USB, MX4SIO, the internal exFAT HDD, SMB, MMCE,
48+
and the **APA-formatted internal HDD**. (On the APA HDD each PS1 game lives on its own `__.POPS`
49+
partition, so opening one of its VCD favourites re-scans those partitions to find the game — the
50+
first launch may take a moment.)
5151

5252
## 3. How PS1 games launch (POPSTARTER only)
5353

include/iosupport.h

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,9 @@ typedef struct _item_list_t
156156
int (*itemIconId)(item_list_t *itemList);
157157

158158
/// Launch a PS1/.VCD entry BY NAME via POPSTARTER, independent of the device's current ISO/VCD
159-
/// view. NULL for devices without a VCD view (and for the APA HDD, whose __.POPS partition model
160-
/// is not name-resolvable here). Used by the Favourites tab to launch a VCD favourite while its
161-
/// source device page may be in ISO view -- the id-based itemLaunch's VCD branch is gated on the
162-
/// device being live in VCD view, which the Favourites tab cannot guarantee.
159+
/// view. NULL for devices without a VCD view. Used by the Favourites tab to launch a VCD favourite
160+
/// while its source device page may be in ISO view -- the id-based itemLaunch's VCD branch is gated
161+
/// on the device being live in VCD view, which the Favourites tab cannot guarantee.
163162
void (*itemLaunchVcd)(item_list_t *itemList, const char *vcdName, config_set_t *configSet);
164163
} item_list_t;
165164

src/favsupport.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,9 +344,8 @@ static item_list_t *favResolve(int mode, int id, const char *text, int isVcd, in
344344
// ISO view right now (its submenu/games array holds discs, not VCDs), yet we must still surface +
345345
// launch the PS1 favourite. So we only need a loaded device that can launch a VCD (itemLaunchVcd
346346
// != NULL); art/config/launch all key off the stored name + the device's prefix (itemGetPrefix).
347-
// A device whose VCD list lives off the browse prefix and provides no itemLaunchVcd (APA HDD __.POPS
348-
// partitions) is intentionally skipped here -- the common internal exFAT HDD is a BDM massN: device
349-
// and is covered through the BDM branch.
347+
// A device that provides no itemLaunchVcd is skipped here -- a future-proof backstop, since every
348+
// VCD-capable device (BDM/ETH/MMCE/HDD) implements it today.
350349
if (isVcd) {
351350
if (mode >= BDM_MODE && mode <= BDM_MODE_LAST) {
352351
// Prefer the stored slot; else the first loaded BDM slot (a stick can change slots).

src/hddsupport.c

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "include/themes.h"
1010
#include "include/textures.h"
1111
#include "include/ioman.h"
12+
#include "include/texcache.h" // cache quiesce before the launch-path VCD partition rescan (freeze guard)
1213
#include "include/system.h"
1314
#include "include/extern_irx.h"
1415
#include "include/cheatman.h"
@@ -532,6 +533,68 @@ static void hddRenameGame(item_list_t *itemList, int id, char *newName)
532533
hddForceUpdate = 1;
533534
}
534535

536+
// Index of the VCD whose basename matches vcdName in the current hddVcdGames list, or -1.
537+
static int hddFindVcdByName(const char *vcdName)
538+
{
539+
for (int i = 0; i < hddVcdGameCount; i++) {
540+
if (strcmp(hddVcdGames[i].name, vcdName) == 0)
541+
return i;
542+
}
543+
return -1;
544+
}
545+
546+
// Shared POPSTARTER handoff for an HDD VCD. POPSTARTER's argv[0] is just the VCD name; the owning
547+
// __.POPS* partition (`part`) is passed OUT OF BAND so POPSTARTER self-mounts it after the IOP reset.
548+
// Everything is built on stack BEFORE deinit() frees the VCD list. Used by both the in-view menu launch
549+
// and the Favourites tab (hddLaunchVcd).
550+
static void hddDoLaunchVcd(item_list_t *itemList, const char *vcdName, const char *part)
551+
{
552+
char vcdElf[256], vcdSelector[320], vcdPart[64];
553+
554+
if (vcdName == NULL || vcdName[0] == '\0' || !strcmp(vcdName, "POPSTARTER"))
555+
return;
556+
if (!vcdResolvePopstarter(gHDDPrefix, vcdElf, sizeof(vcdElf))) {
557+
guiMsgBox(_l(_STR_POPSTARTER_NOT_FOUND), 0, NULL);
558+
return;
559+
}
560+
snprintf(vcdSelector, sizeof(vcdSelector), "%s.ELF", vcdName); // POPSTARTER's argv[0] = the name
561+
snprintf(vcdPart, sizeof(vcdPart), "hdd0:%s:", part); // self-mount target, hdd0:PART:
562+
deinit(UNMOUNT_EXCEPTION, itemList->mode);
563+
sysLaunchPopstarter(vcdElf, vcdSelector, vcdPart);
564+
}
565+
566+
// Launch an HDD PS1/.VCD entry BY NAME -- the Favourites tab's view-independent entry point. The
567+
// per-game __.POPS* partition lives in hddVcdParts, which is only populated while the HDD page is in
568+
// its VCD view; from Favourites it may be empty/stale, so (re)scan via the SAME safe partition walk the
569+
// VCD view uses (hddBuildVcdGameList mounts each __.POPS on pfs0:, scans, restores pfs0: to the default
570+
// OPL partition) to resolve name -> partition, then hand off exactly as the in-view launch does.
571+
static void hddLaunchVcd(item_list_t *itemList, const char *vcdName, config_set_t *configSet)
572+
{
573+
if (vcdName == NULL || vcdName[0] == '\0' || !strcmp(vcdName, "POPSTARTER"))
574+
return;
575+
576+
int idx = hddFindVcdByName(vcdName);
577+
if (idx < 0) {
578+
// Cold path: the per-game __.POPS partition is unknown (hddVcdGames is only built while the HDD
579+
// page is in VCD view). hddBuildVcdGameList remounts the single ps2fs slot (pfs0:), and
580+
// umounting it while the cache worker holds an open fd reading a cover from pfs0: is the
581+
// documented HDD-freeze hazard. The equivalent in-view rescan runs on the IO thread only when no
582+
// art/IO is pending (opl.c menuUpdateHook); on the launch path we enforce the same invariant by
583+
// quiescing the art + IO workers first, exactly as deinit() does before its own teardown IO.
584+
cacheAbortMmceImageLoadsTimed(0);
585+
(void)cacheCancelPendingImageLoadsTimed(0);
586+
ioBlockOps(1);
587+
hddBuildVcdGameList(); // restores pfs0: to the default OPL partition when done
588+
ioBlockOps(0);
589+
idx = hddFindVcdByName(vcdName);
590+
}
591+
if (idx < 0) {
592+
guiMsgBox(_l(_STR_POPSTARTER_NOT_FOUND), 0, NULL);
593+
return;
594+
}
595+
hddDoLaunchVcd(itemList, hddVcdGames[idx].name, hddVcdParts[idx]);
596+
}
597+
535598
void hddLaunchGame(item_list_t *itemList, int id, config_set_t *configSet)
536599
{
537600
int i, size_irx = 0;
@@ -549,18 +612,7 @@ void hddLaunchGame(item_list_t *itemList, int id, config_set_t *configSet)
549612
// selector/partition contract is hardware-testable -- POPSLoader proved the shape with a vendored
550613
// loader; we use the stock ps2sdk loader, the same one the shipping USB/MMCE/SMB VCD launch uses.
551614
if (gAutoLaunchGame == NULL && vcdViewActive(itemList->mode)) {
552-
char vcdName[VCD_NAME_MAX], vcdElf[256], vcdSelector[320], vcdPart[64];
553-
snprintf(vcdName, sizeof(vcdName), "%s", hddVcdGames[id].name);
554-
if (vcdName[0] == '\0' || !strcmp(vcdName, "POPSTARTER"))
555-
return;
556-
if (!vcdResolvePopstarter(gHDDPrefix, vcdElf, sizeof(vcdElf))) {
557-
guiMsgBox(_l(_STR_POPSTARTER_NOT_FOUND), 0, NULL);
558-
return;
559-
}
560-
snprintf(vcdSelector, sizeof(vcdSelector), "%s.ELF", vcdName); // POPSTARTER's argv[0] = the name
561-
snprintf(vcdPart, sizeof(vcdPart), "hdd0:%s:", hddVcdParts[id]); // self-mount target, hdd0:PART:
562-
deinit(UNMOUNT_EXCEPTION, itemList->mode);
563-
sysLaunchPopstarter(vcdElf, vcdSelector, vcdPart);
615+
hddDoLaunchVcd(itemList, hddVcdGames[id].name, hddVcdParts[id]);
564616
return;
565617
}
566618

@@ -1029,4 +1081,4 @@ static char *hddGetPrefix(item_list_t *itemList)
10291081
static item_list_t hddGameList = {
10301082
HDD_MODE, 0, 0, MODE_FLAG_COMPAT_DMA, MENU_MIN_INACTIVE_FRAMES, HDD_MODE_UPDATE_DELAY, NULL, NULL, &hddGetTextId, &hddGetPrefix, &hddInit, &hddNeedsUpdate, &hddUpdateGameList,
10311083
&hddGetGameCount, &hddGetGame, &hddGetGameName, &hddGetGameNameLength, &hddGetGameStartup, &hddDeleteGame, &hddRenameGame,
1032-
&hddLaunchGame, &hddGetConfig, &hddGetImage, &hddCleanUp, &hddShutdown, &hddCheckVMC, &hddGetIconId};
1084+
&hddLaunchGame, &hddGetConfig, &hddGetImage, &hddCleanUp, &hddShutdown, &hddCheckVMC, &hddGetIconId, &hddLaunchVcd};

src/opl.c

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -406,10 +406,11 @@ static void itemExecFav(struct menu_item *curMenu)
406406
// it resolves + launches as POPSTARTER and lands in the Favourites tab's own VCD view.
407407
int isVcd = vcdViewActive(support->mode) ? 1 : 0;
408408
// ...but only on a device whose VCD favourites can actually be resolved/launched later
409-
// (itemLaunchVcd present). The APA HDD's __.POPS partition model isn't name-resolvable yet, so
410-
// storing a VCD favourite there would be permanently hidden + unlaunchable -- make R3 an honest
411-
// no-op (no record, no star, no confirm sound) rather than a misleading "saved". Disc favourites
412-
// on the same device are unaffected (isVcd == 0 there).
409+
// (itemLaunchVcd present). Storing a VCD favourite on a device without it would leave a
410+
// permanently-hidden, unlaunchable record, so make R3 an honest no-op (no record, no star, no
411+
// confirm sound) rather than a misleading "saved". Every VCD-capable device (BDM/ETH/MMCE/HDD)
412+
// implements itemLaunchVcd today -- this is a future-proof backstop. Disc favourites are
413+
// unaffected (isVcd == 0 there).
413414
if (isVcd && support->itemLaunchVcd == NULL)
414415
return;
415416
if (it->favourited) {

0 commit comments

Comments
 (0)