diff --git a/include/opl.h b/include/opl.h index 90ed1f3007..f2dbb05bc0 100644 --- a/include/opl.h +++ b/include/opl.h @@ -216,8 +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 showHddReconcilePopup; // boot toast: APA + exFAT(BDM) HDD were both enabled -- one was auto-disabled (#154) +extern int showNetDhcpPopup; // boot toast: UDP transport selected while IP Type = DHCP (needs static IP) #ifdef IGS #define IGS_VERSION "0.1" diff --git a/modules/hdd/xhdd/xhdd.c b/modules/hdd/xhdd/xhdd.c index 7dbbb0597e..495fa15716 100644 --- a/modules/hdd/xhdd/xhdd.c +++ b/modules/hdd/xhdd/xhdd.c @@ -52,6 +52,18 @@ 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. + 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/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..8f3d2d60b3 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,12 @@ 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; + } else if (nonSony > 0) { + hddSupportErrToasted = 0; // probe SUCCEEDED (genuine MBR/GPT) -- a future failure is new news + } return; } @@ -341,14 +349,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 +370,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 +584,20 @@ 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) { + // 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)) // 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. } }