From 3827e0cb410d465aaa6abfd1e90950c6920809c2 Mon Sep 17 00:00:00 2001 From: "NathanNeurotic (Ripto)" <109461996+NathanNeurotic@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:58:56 -0700 Subject: [PATCH 1/3] feat(hdd): APA and BDM-ATA coexist -- remove the exclusivity policy + give the APA page real retry (HW batch S1+S2) Maintainer directive (2026-07-21): "APA and ATA should be able to coexist. POPSLoader has them coexisting and populating - we have no excuse." Root-caused by the triage workflow (verified): the mutual block was 100% OUR EE-side policy, not a driver conflict -- OPL's "ps2atad" is the SDK's ata_bd build, which registers the drive with BDM while exporting the atad library ps2hdd links against; one shared stack already serves both, exactly like wLaunchELF-R3Z3N, NHDDL and POPSLoader. REMOVED (S1): the Device-Settings interlock (gui.c seed lines + the guiDeviceUpdater live re-grey), the #120-audit F-12 load-time reconcile that force-zeroed one flag (opl.c, + its showHddReconcilePopup plumbing; the _STR_HDD_BACKEND_RECONCILED label STAYS in _base.yml -- labels are positional and append-only), and the forced APA-disable on an ATA boot (the boot-device auto-enable stays). Per-DRIVE arbitration is untouched and remains the doctrine: hddDetectNonSonyFileSystem keeps APA off MBR/GPT/exFAT drives (corruption guard), BDM publishes massN: only where a FAT/exFAT partition exists. ADDED (S2, the empty-APA-page session): the APA list was ONE-SHOT at boot -- an hddInitModules first touch that raced drive spin-up (MC boot touches ATA seconds after power-on) left the page empty with NOTHING retrying for the whole session (and no massN: for BDM-ATA either, which reads on HW as "they block each other"). Now: - hddUpdateGameList self-heals: if the support stack is not loaded, re-run hddLoadModulesReady() + hddLoadSupportModules() before scanning (both idempotent; the failed-load count is retryable post-#241) -- wLaunchELF-R3Z3N parity: latch only on success, retry per use. One toast per failure streak (hddSupportErrToasted), reset on recovery. - xhdd's READ_PARTITION_SECTOR devctl (the EE side's readiness probe) re-probes via sceAtaInit before the transfer instead of DMA-ing against a dead devinfo -- a latched no-op on healthy paths. KNOWN RESIDUAL (documented, next PR if HW still shows it): the SDK ata_bd latches its device-info init even on a failed probe, so an in-flight spin-up race at _start can still need a reboot to clear -- fully fixing that needs a fork-vendored/patched atad (same .patch mechanism as the mmceman fixes). The retries above cover every post-probe failure; ship and see what HW says. Concurrency note carried from the review: with both stacks live, pfs and bdmfs reads can interleave ATA commands (the three reference implementations never issue concurrent IO) -- if hybrid-drive instability appears, the fix is a semaphore in the atad patch, NOT re-adding the exclusivity policy. Do NOT revert #241 (BUSYLOADING-as-failure verified correct; reverting recreates the Vapor poison). Builds clean (make opl.elf, exit 0). HW-pending: Nathan's dual-HDD rig. Co-Authored-By: Claude Fable 5 --- include/opl.h | 1 - modules/hdd/xhdd/xhdd.c | 9 +++++++++ src/gui.c | 22 +++++----------------- src/hddsupport.c | 35 +++++++++++++++++++++++++++++++---- src/opl.c | 40 ++++++++++++---------------------------- 5 files changed, 57 insertions(+), 50 deletions(-) diff --git a/include/opl.h b/include/opl.h index 90ed1f3007..c27d1bae0c 100644 --- a/include/opl.h +++ b/include/opl.h @@ -217,7 +217,6 @@ extern int gOSDLanguageSource; extern int showCfgPopup; extern int showNetDhcpPopup; // boot toast: UDP transport selected while IP Type = DHCP (needs static IP) -extern int showHddReconcilePopup; // boot toast: APA + exFAT(BDM) HDD were both enabled -- one was auto-disabled (#154) #ifdef IGS #define IGS_VERSION "0.1" diff --git a/modules/hdd/xhdd/xhdd.c b/modules/hdd/xhdd/xhdd.c index 7dbbb0597e..e6a7be717a 100644 --- a/modules/hdd/xhdd/xhdd.c +++ b/modules/hdd/xhdd/xhdd.c @@ -52,6 +52,15 @@ static int xhddDevctl(iop_file_t *fd, const char *name, int cmd, void *arg, unsi if (buflen % 512 != 0) return -EINVAL; + // Re-probe before the transfer: a boot-time atad init that raced drive spin-up leaves a + // dead devinfo behind, and this devctl is the EE side's readiness gate (the + // hddDetectNonSonyFileSystem probe). sceAtaInit is a latched no-op once a probe has + // succeeded, so a healthy path pays one call; a dead one gets a real second attempt + // (with an atad whose init is retryable) instead of an unconditional -1 for the session. + if ((devinfo = sceAtaInit(fd->unit)) == NULL || !devinfo->exists) + if ((devinfo = sceAtaInit(fd->unit)) == NULL || !devinfo->exists) + return -ENODEV; + return sceAtaDmaTransfer(fd->unit, buf, 0, buflen / 512, ATA_DIR_READ); } diff --git a/src/gui.c b/src/gui.c index e2c94cfbfa..b54bf5b727 100644 --- a/src/gui.c +++ b/src/gui.c @@ -302,7 +302,7 @@ static void guiShowNotifications(void) int y = 10; int yadd = 35; - if (showPartPopup || showThmPopup || showLngPopup || showCfgPopup || showNetDhcpPopup || showHddReconcilePopup) { + if (showPartPopup || showThmPopup || showLngPopup || showCfgPopup || showNetDhcpPopup) { if (!popupTimer) { popupTimer = clock() + 5000 * (CLOCKS_PER_SEC / 1000); sfxPlay(SFX_MESSAGE); @@ -351,18 +351,11 @@ static void guiShowNotifications(void) y += yadd; } - // One-time notice set at config load (#154): APA + exFAT(BDM) internal HDD were both - // enabled and one was auto-disabled. Rendered here (not toasted from _loadConfig) so _l() - // resolves AFTER the language pack loads -- Gemini review of #167. - if (showHddReconcilePopup) - guiRenderNotifications(_l(_STR_HDD_BACKEND_RECONCILED), y); - if (clock() >= popupTimer) { guiResetNotifications(); showPartPopup = 0; showCfgPopup = 0; showNetDhcpPopup = 0; - showHddReconcilePopup = 0; } } } @@ -1231,13 +1224,8 @@ void guiShowNetConfig(void) static int guiDeviceUpdater(int modified) { if (modified) { - int hddMode, bdmHdd; - diaGetInt(diaDeviceConfig, CFG_HDDMODE, &hddMode); - diaGetInt(diaDeviceConfig, CFG_ENABLEBDMHDD, &bdmHdd); - // BDM HDD (GPT/MBR) and the APA HDD mode are mutually exclusive; keep the - // two interlocked live now that both live on the same page. - diaSetEnabled(diaDeviceConfig, CFG_HDDMODE, !bdmHdd); - diaSetEnabled(diaDeviceConfig, CFG_ENABLEBDMHDD, hddMode == 0); + // APA + BDM-HDD interlock REMOVED (coexistence directive 2026-07-21): both rows stay + // enabled; per-drive arbitration lives in hddDetectNonSonyFileSystem, not the dialog. // Network 3-row live logic. Row 1 Start (Off/Manual/Auto), Row 2 Protocol (SMB/UDPFS/UDPBD), // Row 3 Access (Files/IMG). While Start=Off, grey Protocol + Access. Lock Access to Files for @@ -1291,8 +1279,8 @@ void guiShowDeviceConfig(void) diaSetInt(diaDeviceConfig, CFG_ENABLEILK, gEnableILK); diaSetInt(diaDeviceConfig, CFG_ENABLEMX4SIO, gEnableMX4SIO); diaSetInt(diaDeviceConfig, CFG_ENABLEBDMHDD, gEnableBdmHDD); - diaSetEnabled(diaDeviceConfig, CFG_ENABLEBDMHDD, !gHDDStartMode); - diaSetEnabled(diaDeviceConfig, CFG_HDDMODE, !gEnableBdmHDD); + diaSetEnabled(diaDeviceConfig, CFG_ENABLEBDMHDD, 1); // coexists with APA (directive 2026-07-21) + diaSetEnabled(diaDeviceConfig, CFG_HDDMODE, 1); // Network: 3 orthogonal rows seeded from the authoritative gNetworkProtocol + gNetStartMode. // Row 1 Start: Off/Manual/Auto == gNetStartMode (START_MODE_*) // Row 2 Protocol: SMB(0)/UDPFS(1)/UDPBD(2) -- OFF and UDPFSBD both collapse to their protocol diff --git a/src/hddsupport.c b/src/hddsupport.c index 5e11fbeec5..e7b79adbc6 100644 --- a/src/hddsupport.c +++ b/src/hddsupport.c @@ -35,6 +35,10 @@ static unsigned char hddHDProKitDetected = 0; static unsigned char hddModulesLoadCount = 0; static unsigned char hddModulesLoaded = 0; static unsigned char hddSupportModulesLoaded = 0; +// One toast per failure streak: hddUpdateGameList now RETRIES the support-module load every refresh +// while it keeps failing, and re-toasting the same error box each pass would bury the UI. Reset on +// success so a later, different failure toasts again. +static unsigned char hddSupportErrToasted = 0; static char *hddPrefix = "pfs0:"; static hdl_games_list_t hddGames; @@ -331,8 +335,10 @@ void hddLoadSupportModules(void) // just looks like a dead drive for the whole session. Surface it. The 1 (genuine MBR/GPT/exFAT) // branch stays silent on purpose -- that is the NORMAL coexistence case for BDM-HDD users and // must not toast at every boot. - if (nonSony < 0) + if (nonSony < 0 && !hddSupportErrToasted) { setErrorMessageWithCode(_STR_HDD_NOT_CONNECTED_ERROR, ERROR_HDD_NOT_DETECTED); + hddSupportErrToasted = 1; + } return; } @@ -341,14 +347,20 @@ void hddLoadSupportModules(void) int ret = sysLoadModuleBuffer(&ps2hdd_irx, size_ps2hdd_irx, sizeof(hddarg), hddarg); if (ret < 0) { LOG("HDD: No HardDisk Drive detected.\n"); - setErrorMessageWithCode(_STR_HDD_NOT_CONNECTED_ERROR, ERROR_HDD_MODULE_HDD_FAILURE); + if (!hddSupportErrToasted) { + setErrorMessageWithCode(_STR_HDD_NOT_CONNECTED_ERROR, ERROR_HDD_MODULE_HDD_FAILURE); + hddSupportErrToasted = 1; + } return; } // Check if a HDD unit is connected if (hddCheck() < 0) { LOG("HDD: No HardDisk Drive detected.\n"); - setErrorMessageWithCode(_STR_HDD_NOT_CONNECTED_ERROR, ERROR_HDD_NOT_DETECTED); + if (!hddSupportErrToasted) { + setErrorMessageWithCode(_STR_HDD_NOT_CONNECTED_ERROR, ERROR_HDD_NOT_DETECTED); + hddSupportErrToasted = 1; + } return; } @@ -356,11 +368,15 @@ void hddLoadSupportModules(void) ret = sysLoadModuleBuffer(&ps2fs_irx, size_ps2fs_irx, sizeof(pfsarg), pfsarg); if (ret < 0) { LOG("HDD: HardDisk Drive not formatted (PFS).\n"); - setErrorMessageWithCode(_STR_HDD_NOT_FORMATTED_ERROR, ERROR_HDD_MODULE_PFS_FAILURE); + if (!hddSupportErrToasted) { + setErrorMessageWithCode(_STR_HDD_NOT_FORMATTED_ERROR, ERROR_HDD_MODULE_PFS_FAILURE); + hddSupportErrToasted = 1; + } return; } hddSupportModulesLoaded = 1; + hddSupportErrToasted = 0; // recovered -- a future, different failure gets its own toast LOG("HDDSUPPORT modules loaded\n"); if (gOPLPart[0] == '\0') @@ -566,6 +582,17 @@ static int hddNeedsUpdate(item_list_t *itemList) static int hddUpdateGameList(item_list_t *itemList) { + // Self-heal (wLaunchELF-R3Z3N parity: latch only on success, retry per use): a boot-time first + // touch that raced drive spin-up used to leave the APA page EMPTY for the whole session -- the + // one-shot hddInitModules never retried, and nothing else reloads the support stack. Both calls + // are idempotent (hddLoadModules dedupes via ALREADYLOADED and its failed count is retryable + // post-#241; hddLoadSupportModules no-ops once hddSupportModulesLoaded), so tab entry / refresh + // becomes a real second attempt. Runs on the IO worker like the original init. + if (!hddSupportModulesLoaded) { + hddLoadModulesReady(); + hddLoadSupportModules(); + } + if (vcdViewActive(itemList->mode)) // Reuse the session's built list on view flips; hddBuildVcdGameList runs only when never // built, invalidated (first-disc-only change), or freed by teardown (hddFreeVcdGameList). diff --git a/src/opl.c b/src/opl.c index e4c7019600..31e3070867 100644 --- a/src/opl.c +++ b/src/opl.c @@ -195,8 +195,7 @@ int gFadeDelay; int toggleSfx; int showCfgPopup; // Boot toast (rendered by guiShowNotifications alongside showCfgPopup): -int showNetDhcpPopup; // a UDP transport is selected but IP Type is DHCP -- ministack needs a static IP -int showHddReconcilePopup; // APA + exFAT(BDM) HDD were both enabled -- one was auto-disabled at load (#154) +int showNetDhcpPopup; // a UDP transport is selected but IP Type is DHCP -- ministack needs a static IP #ifdef PADEMU int gEnablePadEmu; int gPadEmuSettings; @@ -1704,26 +1703,14 @@ static void _loadConfig() configGetInt(configOPL, CONFIG_OPL_ENABLE_ILINK, &gEnableILK); configGetInt(configOPL, CONFIG_OPL_ENABLE_MX4SIO, &gEnableMX4SIO); configGetInt(configOPL, CONFIG_OPL_ENABLE_BDMHDD, &gEnableBdmHDD); - // #120 audit F-12: APA (gHDDStartMode) and BDM-ATA (gEnableBdmHDD) are mutually exclusive by - // design, but the Device-Settings interlock only guards values changed THROUGH the dialog. A - // legacy, hand-edited or cross-version config can load both, and then BOTH internal-HDD - // stacks initialize against the one drive. Normalize at load: the backend matching the boot - // device wins; otherwise the APA start mode (the older, more deliberate setting) is kept. - // configSetInt the loser so the next save persists the reconciled pair. - if (gEnableBdmHDD && gHDDStartMode != START_MODE_DISABLED) { - if (gBootDirBdmType == BDM_TYPE_ATA) - gHDDStartMode = START_MODE_DISABLED; - else - gEnableBdmHDD = 0; - configSetInt(configOPL, CONFIG_OPL_HDD_MODE, gHDDStartMode); - configSetInt(configOPL, CONFIG_OPL_ENABLE_BDMHDD, gEnableBdmHDD); - // #154 forensics: this reconciliation was SILENT -- a user whose internal-exFAT (or - // APA) page vanished after a hand-edit/cross-version config had no clue why. Flag it - // for the notification popup (same pattern as showNetDhcpPopup): the render site - // resolves _l() per frame, so the message localizes correctly even though the - // language pack loads AFTER this point (applyConfig at the end of _loadConfig). - showHddReconcilePopup = 1; - } + // APA (gHDDStartMode) and BDM-ATA (gEnableBdmHDD) COEXIST -- maintainer directive + // 2026-07-21, overruling the old #120-audit F-12 exclusivity that force-disabled one here. + // There is no driver conflict to arbitrate: OPL's "ps2atad" is the SDK's ata_bd build, + // which registers the drive with BDM while exporting the atad library ps2hdd links + // against -- one shared stack serves both, the same approach wLaunchELF-R3Z3N, NHDDL and + // POPSLoader ship. Per-DRIVE arbitration stays where it belongs: + // hddDetectNonSonyFileSystem() keeps APA off an MBR/GPT/exFAT drive (corruption guard), + // and BDM only publishes massN: where a FAT/exFAT partition actually exists. int udpbdKeyPresent = configGetInt(configOPL, CONFIG_OPL_ENABLE_UDPBD, &gEnableUDPBD); configGetInt(configOPL, CONFIG_OPL_NET_BOOT_PROTOCOL, &gNetBootProtocol); // Unified network-protocol selector (single SMAP NIC -> at most one transport per session). @@ -1815,12 +1802,9 @@ static void _loadConfig() if (gBootDirBdmType == BDM_TYPE_ATA && !gEnableBdmHDD) { gEnableBdmHDD = 1; configSetInt(configGetByType(CONFIG_OPL), CONFIG_OPL_ENABLE_BDMHDD, gEnableBdmHDD); - // #120 audit F-12: don't leave a loaded APA start mode fighting the just-enabled BDM-ATA - // backend (the drive we booted from is exFAT, so APA is definitionally wrong for it). - if (gHDDStartMode != START_MODE_DISABLED) { - gHDDStartMode = START_MODE_DISABLED; - configSetInt(configGetByType(CONFIG_OPL), CONFIG_OPL_HDD_MODE, gHDDStartMode); - } + // A loaded APA start mode is left ALONE (coexistence directive 2026-07-21): a second, + // APA-formatted drive is a real setup, and on this single exFAT boot drive the + // hddDetectNonSonyFileSystem guard makes the APA leg a harmless no-op anyway. } } From b74cd26edc1a58d8f4f064ee6a1069f533c29908 Mon Sep 17 00:00:00 2001 From: "NathanNeurotic (Ripto)" <109461996+NathanNeurotic@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:14:12 -0700 Subject: [PATCH 2/3] style(opl): align comment per CI clang-format 12 Co-Authored-By: Claude Fable 5 --- include/opl.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/opl.h b/include/opl.h index c27d1bae0c..f2dbb05bc0 100644 --- a/include/opl.h +++ b/include/opl.h @@ -216,7 +216,7 @@ extern int gOSDLanguageEnable; extern int gOSDLanguageSource; extern int showCfgPopup; -extern int showNetDhcpPopup; // boot toast: UDP transport selected while IP Type = DHCP (needs static IP) +extern int showNetDhcpPopup; // boot toast: UDP transport selected while IP Type = DHCP (needs static IP) #ifdef IGS #define IGS_VERSION "0.1" From f9676ad3464e6fae6840c08be7149ca4aaa08caf Mon Sep 17 00:00:00 2001 From: "NathanNeurotic (Ripto)" <109461996+NathanNeurotic@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:27:07 -0700 Subject: [PATCH 3/3] fix(hdd): honor module readiness before support load + latch reset on definitive probe + xhdd retry braces (bot sweep of #249 -- all vetted) - hddUpdateGameList retry now gates hddLoadSupportModules on hddLoadModulesReady() -- a failed base load no longer double-toasts on its first pass (Gemini high + CodeRabbit major, same finding). - A SUCCESSFUL non-Sony probe (genuine MBR/GPT) clears hddSupportErrToasted -- a later, different failure is new news and deserves its own toast (CodeRabbit minor). - xhdd re-probe restructured with explicit braces, assignment out of the condition (Gemini style). Builds clean. Co-Authored-By: Claude Fable 5 --- modules/hdd/xhdd/xhdd.c | 7 +++++-- src/hddsupport.c | 9 +++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/modules/hdd/xhdd/xhdd.c b/modules/hdd/xhdd/xhdd.c index e6a7be717a..495fa15716 100644 --- a/modules/hdd/xhdd/xhdd.c +++ b/modules/hdd/xhdd/xhdd.c @@ -57,9 +57,12 @@ static int xhddDevctl(iop_file_t *fd, const char *name, int cmd, void *arg, unsi // hddDetectNonSonyFileSystem probe). sceAtaInit is a latched no-op once a probe has // succeeded, so a healthy path pays one call; a dead one gets a real second attempt // (with an atad whose init is retryable) instead of an unconditional -1 for the session. - if ((devinfo = sceAtaInit(fd->unit)) == NULL || !devinfo->exists) - if ((devinfo = sceAtaInit(fd->unit)) == NULL || !devinfo->exists) + devinfo = sceAtaInit(fd->unit); + if (devinfo == NULL || !devinfo->exists) { + devinfo = sceAtaInit(fd->unit); // one bounded retry + if (devinfo == NULL || !devinfo->exists) return -ENODEV; + } return sceAtaDmaTransfer(fd->unit, buf, 0, buflen / 512, ATA_DIR_READ); } diff --git a/src/hddsupport.c b/src/hddsupport.c index e7b79adbc6..8f3d2d60b3 100644 --- a/src/hddsupport.c +++ b/src/hddsupport.c @@ -338,6 +338,8 @@ void hddLoadSupportModules(void) if (nonSony < 0 && !hddSupportErrToasted) { setErrorMessageWithCode(_STR_HDD_NOT_CONNECTED_ERROR, ERROR_HDD_NOT_DETECTED); hddSupportErrToasted = 1; + } else if (nonSony > 0) { + hddSupportErrToasted = 0; // probe SUCCEEDED (genuine MBR/GPT) -- a future failure is new news } return; } @@ -589,8 +591,11 @@ static int hddUpdateGameList(item_list_t *itemList) // post-#241; hddLoadSupportModules no-ops once hddSupportModulesLoaded), so tab entry / refresh // becomes a real second attempt. Runs on the IO worker like the original init. if (!hddSupportModulesLoaded) { - hddLoadModulesReady(); - hddLoadSupportModules(); + // Only chase the support stack when the base modules are actually resident -- calling it + // anyway made a failed base load toast TWICE (base failure + the doomed non-Sony probe's) + // on the first pass (Gemini + CodeRabbit review of #249, vetted). + if (hddLoadModulesReady()) + hddLoadSupportModules(); } if (vcdViewActive(itemList->mode))