diff --git a/include/hddsupport.h b/include/hddsupport.h index 7e886cd08..300500a6f 100644 --- a/include/hddsupport.h +++ b/include/hddsupport.h @@ -91,6 +91,8 @@ void hddVcdInvalidateCache(void); void hddInit(item_list_t *itemList); item_list_t *hddGetObject(int initOnly); int hddLoadModules(void); +// Speculative legacy-config probe: identical module load, but expected HDD absence remains log-only. +int hddLoadModulesSilent(void); int hddLoadSupportModules(void); void hddResetModuleState(void); void hddLaunchGame(item_list_t *itemList, int id, config_set_t *configSet); diff --git a/include/opl.h b/include/opl.h index 5116dea2f..4e2a0068a 100644 --- a/include/opl.h +++ b/include/opl.h @@ -215,7 +215,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" diff --git a/src/hddsupport.c b/src/hddsupport.c index 9d0346f03..f734ee518 100644 --- a/src/hddsupport.c +++ b/src/hddsupport.c @@ -212,7 +212,7 @@ static int hddCreateOPLPartition(const char *name) return result; } -int hddLoadModules(void) +static int hddLoadModulesInternal(int reportError) { int retLoadModule; int wait; @@ -271,7 +271,8 @@ int hddLoadModules(void) // terminal hardware stop, not a reversible reference release within the current IOP session. hddModulesLoading = 0; LOG("HDD: No HardDisk Drive detected.\n"); - setErrorMessageWithCode(_STR_HDD_NOT_CONNECTED_ERROR, ERROR_HDD_IF_NOT_DETECTED); + if (reportError) + setErrorMessageWithCode(_STR_HDD_NOT_CONNECTED_ERROR, ERROR_HDD_IF_NOT_DETECTED); return HDD_LOADMODULES_STATUS_ERROR; } @@ -284,6 +285,16 @@ int hddLoadModules(void) return HDD_LOADMODULES_STATUS_NOERROR; } +int hddLoadModules(void) +{ + return hddLoadModulesInternal(1); +} + +int hddLoadModulesSilent(void) +{ + return hddLoadModulesInternal(0); +} + int hddLoadSupportModules(void) { int ret; diff --git a/src/mmcesupport.c b/src/mmcesupport.c index 3bccf192a..b1f310f5b 100644 --- a/src/mmcesupport.c +++ b/src/mmcesupport.c @@ -614,23 +614,23 @@ void mmceLaunchGame(item_list_t *itemList, int id, config_set_t *configSet) return; sbSetBrowseSub(folderGetSub(itemList->mode)); - // VCD view: hand off to POPSTARTER (by name) instead of the disc path below. Menu-launch only. - if (gAutoLaunchBDMGame == NULL && game != NULL && vcdViewActive(itemList->mode)) { - mmceLaunchVcd(itemList, game->name, configSet); - return; - } - + // Quiesce every MMCE art read before either launch path performs card IO. The VCD handoff + // resolves POPSTARTER and may equip BDMA modules from MMCE before deinit(), so allowing its early + // return to bypass this guard can contend with the persistent art worker on the same mmceman channel. if (!cacheAbortMmceImageLoadsTimed(MMCE_ART_ABORT_WAIT_TICKS)) { // #120: the art worker is wedged in a blocking fileXio on a slow/desynced card. Do NOT cacheEnd(1) // here -- its TerminateThread(gArtThreadId) kills the worker MID-RPC and orphans the SHARED mmceman - // channel (TK>0). The launch reads below then FAIL and RETURN to a still-running OPL (unlike a launch - // that LoadExecPS2's away), poisoning every later card read. Abandon-and-retry instead (mirrors - // thmLoad's redesign): toast and bail so the user retries once the card is calm. NEVER a hard - // freeze -- guiWarning is non-blocking. + // channel (TK>0). Abandon-and-retry instead so the user can retry once the card is calm. guiWarning(_l(_STR_ERR_FILE_INVALID), 8); return; } + // VCD view: hand off to POPSTARTER (by name) instead of the disc path below. Menu-launch only. + if (gAutoLaunchBDMGame == NULL && game != NULL && vcdViewActive(itemList->mode)) { + mmceLaunchVcd(itemList, game->name, configSet); + return; + } + void *irx = &mmce_cdvdman_irx; int irx_size = size_mmce_cdvdman_irx; compatmask = sbPrepare(game, configSet, irx_size, irx, &index); diff --git a/src/opl.c b/src/opl.c index 7c8276e44..c212594e2 100644 --- a/src/opl.c +++ b/src/opl.c @@ -1311,8 +1311,9 @@ static int checkLoadConfigBDMHDD(int types) char path[64]; int value; - // Bounded wait so BDM-on-HDD can be detected without long black-screen stalls. - if (hddLoadModules() >= 0 && bdmHDDIsPresent(500)) { + // Legacy config discovery is speculative: expected absence must not post the user-facing 401. + // Keep the scan for compatibility, but load the shared ATA stack silently here. + if (hddLoadModulesSilent() >= 0 && bdmHDDIsPresent(500)) { if (bdmFindPartition(path, CONFIG_OPL_FILENAME, 0) || bdmFindPartition(path, CONFIG_OPL_FILENAME_LEGACY, 0)) { configEnd(); configInit(path); @@ -1333,7 +1334,9 @@ static int checkLoadConfigHDD(int types) int value; char path[64]; - if (hddLoadModules() < 0 || !hddLoadSupportModules()) + // Legacy config discovery is speculative: preserve HDD config lookup without reporting expected + // hardware absence as a startup error on HDD-less consoles. + if (hddLoadModulesSilent() < 0 || !hddLoadSupportModules()) return 0; snprintf(path, sizeof(path), "%s%s", gHDDPrefix, CONFIG_OPL_FILENAME); @@ -1359,12 +1362,13 @@ static int checkLoadConfigHDD(int types) // When this function is called, the current device for loading/saving config is the memory card. static int tryAlternateDevice(int types) { - char pwd[8]; + char pwd[64] = ""; char redirectPath[64]; int value; DIR *dir; - getcwd(pwd, sizeof(pwd)); + if (getcwd(pwd, sizeof(pwd)) == NULL) + pwd[0] = 0; if (readConfigPathRedirect(redirectPath, sizeof(redirectPath))) { configEnd(); @@ -1694,15 +1698,15 @@ static void _loadConfig() // preserving the historical "network BDM wins over SMB" precedence (imported/hand-edited configs // could set both; at boot UDPBD loaded first, so it won). NET_PROTO_UDPFS (filesystem) has no // legacy encoding, so it is only ever reached by an explicit new-format value -- backward-safe. - // Since a NETWORK protocol became the shipped DEFAULT (UDPFS, 2026-07-13), the legacy branch - // must key off the FILE's enable_udpbd, not the defaulted global -- a legacy config must only - // ever derive from what IT expressed, never inherit a defaulted enable flag. + // Fresh installs now default to SMB/Manual. The legacy branch must still key off the FILE's + // enable_udpbd value, not the defaulted global: an older config must derive only from what it + // expressed and must never inherit a newly shipped transport choice accidentally. if (!configGetInt(configOPL, CONFIG_OPL_NETWORK_PROTOCOL, &gNetworkProtocol)) { if (udpbdKeyPresent) gNetworkProtocol = gEnableUDPBD ? ((gNetBootProtocol == NET_BOOT_UDPFS) ? NET_PROTO_UDPFSBD : NET_PROTO_UDPBD) : ((gETHStartMode != START_MODE_DISABLED) ? NET_PROTO_SMB : NET_PROTO_OFF); else if (gETHStartMode != START_MODE_DISABLED) gNetworkProtocol = NET_PROTO_SMB; - // else: the file never expressed ANY network choice -> the shipped default stands (UDPFS) + // else: the file never expressed ANY network choice -> the shipped SMB/Manual default stands } // UDPBD (SUDPBDv2) is a first-class protocol, NOT folded away: it is wire-incompatible with // UDPRDMA (SUDPBDv2 on 0xBDBD vs UDPFS on 0xF5F6), so users still on the older udpbd-server @@ -2863,14 +2867,16 @@ static void setDefaults(void) gXSensitivity = 1; gYSensitivity = 1; + // Device Settings defaults: use Manual wherever the row supports it. This exposes each page + // without auto-starting its hardware stack; binary transport toggles below default Off. gBDMStartMode = START_MODE_MANUAL; - gHDDStartMode = START_MODE_DISABLED; // RiptOPL: APA/PFS HDD OFF by default (was Manual); user opts in - gETHStartMode = START_MODE_DISABLED; // RiptOPL: network/SMB OFF by default (was Manual); user opts in + gHDDStartMode = START_MODE_MANUAL; + gETHStartMode = START_MODE_MANUAL; gAPPStartMode = START_MODE_MANUAL; gMMCEStartMode = START_MODE_MANUAL; gFAVStartMode = START_MODE_MANUAL; - gMMCESlot = 2; //Default to first Auto slot + gMMCESlot = 2; // Default to first Auto slot gMMCEIGRSlot = 3; gMMCEEnableGameID = 1; gApplyGameID = 1; // visual GameID barcode ON by default (Pixel FX/RetroGEM HDMI displays; imperceptible otherwise) @@ -2883,23 +2889,18 @@ static void setDefaults(void) gMMCEAckWaitCycles = 5; gMMCEUseAlarms = 1; - gEnableUSB = 1; + // These Device Settings entries are binary (no Manual state), so default all of them Off. + gEnableUSB = 0; gEnableILK = 0; gEnableMX4SIO = 0; - gEnableBdmHDD = 0; // exFAT BDM HDD OFF by default (the other "HDD type"; APA/PFS is gHDDStartMode above) + gEnableBdmHDD = 0; gEnableUDPBD = 0; // the UDPBD BLOCK device stays opt-in gNetBootProtocol = NET_BOOT_UDPBD; // default transport when network boot is enabled (back-compat) - // Unified network selector defaults to OFF (was UDPFS, Nathan 2026-07-16). The reason is the NIC - // latch: every network stack loads its IOP chain ONCE per boot and never unloads (re-binding the - // UDPRDMA socket bricks UDPFS; smap registers a single SMAP_driver), so whichever protocol is - // active FIRST owns the adapter until a restart -- the settings page even tells you so - // (NETBOOT_RESTART). With UDPFS pre-selected, a user who wanted UDPBD or SMB had to change the - // setting and REBOOT before their choice could load. Defaulting to Off means nothing claims the - // NIC at boot, so the first protocol the user picks in Device Settings comes up live -- the apply - // path re-derives the gEnableUDPBD/gNetBootProtocol shadows and forces a device refresh already. - // Existing installs are unaffected: a saved net protocol in settings_riptopl.cfg overrides this. - gNetworkProtocol = NET_PROTO_OFF; - gNetStartMode = START_MODE_DISABLED; // Off in the 3-row Network setting; migration reconciles old configs + // Network has a Manual start state, so fresh installs expose the SMB page without claiming the NIC + // during boot. Protocol and start mode must be coherent: NET_PROTO_OFF would force the Manual row + // back to Off during config reconciliation. Existing saved settings still override these defaults. + gNetworkProtocol = NET_PROTO_SMB; + gNetStartMode = START_MODE_MANUAL; frameCounter = 0; diff --git a/src/vcdsupport.c b/src/vcdsupport.c index 2297b0123..c3c5763fd 100644 --- a/src/vcdsupport.c +++ b/src/vcdsupport.c @@ -640,6 +640,52 @@ int vcdSafeWriteFile(const char *dstPath, const void *buf, int len) return rc; } +// Keep a complete live BDMA installation in EE RAM while replacing it. Memory-card rename is not +// available, and an on-card backup can exceed the free space of a real 8 MB card. Driver modules are +// small, but keep a hard upper bound so a damaged directory entry cannot cause unbounded allocation. +#define VCD_BACKUP_MAX (2 * 1024 * 1024) +static int vcdReadFileAlloc(const char *path, unsigned char **out, int *outSize) +{ + struct stat st; + int fd, size, total = 0; + unsigned char *buf; + + if (path == NULL || out == NULL || outSize == NULL) + return -1; + *out = NULL; + *outSize = 0; + + if (stat(path, &st) != 0 || st.st_size < 0 || st.st_size > VCD_BACKUP_MAX) + return -1; + if (st.st_size == 0) + return 0; + size = (int)st.st_size; + + fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + buf = (unsigned char *)malloc(size); + if (buf == NULL) { + close(fd); + return -1; + } + + while (total < size) { + int r = read(fd, buf + total, size - total); + if (r <= 0) { + free(buf); + close(fd); + return -1; + } + total += r; + } + close(fd); + + *out = buf; + *outSize = size; + return 0; +} + // ---- BDMA (BDMAssault exFAT driver) equip ------------------------------------------- // POPStarter loads its block-device driver from mc?:/POPSTARTER/{usbd.irx,usbhdfsd.irx}. We let the // user EQUIP one of three exFAT variants (or FAT32 = none) by copying THEIR OWN files from a source @@ -658,6 +704,27 @@ static const char *vcdBdmaModule[2] = {"usbd.irx", "usbhdfsd.irx"}; #define VCD_BDMA_MARKER "bdma_config.txt" +static int vcdBdmaLivePairPresent(const char *mcDir) +{ + char path[96]; + unsigned char byte; + + if (mcDir == NULL || mcDir[0] == 0) + return 0; + + for (int i = 0; i < 2; i++) { + snprintf(path, sizeof(path), "%s/%s", mcDir, vcdBdmaModule[i]); + int fd = open(path, O_RDONLY); + if (fd < 0) + return 0; + int r = read(fd, &byte, 1); + close(fd); + if (r != 1) + return 0; + } + return 1; +} + // Resolve the memory-card POPSTARTER folder (where the modules live). Prefer an existing folder; // otherwise create it on the first present card. A slot-2-only first-time setup must not silently // select absent mc0:, and mkdir/probe failure must reach the caller. @@ -854,12 +921,45 @@ int vcdEquipBdma(int source, int mode, char *diag, int diagSize) return r; } - // Commit by COPY, not rename(): this dir is always on mc0:/mc1:, and the stock mcman.irx OPL embeds - // registers the legacy ioman 'mc' device with NO rename op -- iomanX returns -EUNSUP for every mc - // rename(), so a rename-based swap can never succeed here. If a commit write fails, the CARD is - // refusing IO: normalize to the consistent no-pair state (POPStarter falls back to its built-in - // FAT32 driver, same as the VCD_BDMA_FAT32 path) rather than leave a torn mixed-variant pair. - unlink(dst0); // free the old module's space first; tmp + old + new pairs may not fit a real MC + // Snapshot a complete existing pair before touching either live destination. If the pair exists but + // cannot be backed up, leave it untouched and fail the equip rather than risk destroying it. + unsigned char *old0 = NULL, *old1 = NULL, *oldMarker = NULL; + int old0Size = 0, old1Size = 0, oldMarkerSize = 0; + int hadOldPair = vcdBdmaLivePairPresent(mcDir); + if (hadOldPair && + (vcdReadFileAlloc(dst0, &old0, &old0Size) != 0 || + vcdReadFileAlloc(dst1, &old1, &old1Size) != 0)) { + free(old0); + free(old1); + unlink(tmp0); + unlink(tmp1); + return -3; + } + + char markerPath[96]; + snprintf(markerPath, sizeof(markerPath), "%s/%s", mcDir, VCD_BDMA_MARKER); + int markerFd = open(markerPath, O_RDONLY); + int hadOldMarker = markerFd >= 0; + if (markerFd >= 0) + close(markerFd); + if (hadOldMarker) { + if (vcdReadFileAlloc(markerPath, &oldMarker, &oldMarkerSize) != 0) { + free(old0); + free(old1); + unlink(tmp0); + unlink(tmp1); + return -3; + } + // An empty marker is corrupted state, not something worth restoring. Treat it as absent so a + // replacement can proceed and rollback removes it rather than recreating the empty file. + if (oldMarkerSize == 0) + hadOldMarker = 0; + } + + // Commit by COPY, not rename(): stock mcman exposes no rename operation. The marker is written only + // after both live modules are complete. Any module or marker failure rolls the previous installation + // back; only a setup with no restorable prior pair falls back to the explicit FAT32/no-pair state. + unlink(dst0); r = vcdSafeCopyFile(tmp0, dst0); if (r == 0) { unlink(dst1); @@ -867,15 +967,45 @@ int vcdEquipBdma(int source, int mode, char *diag, int diagSize) } unlink(tmp0); unlink(tmp1); + if (r == 0) + r = vcdWriteBdmaMarker(mcDir, mode); + if (r != 0) { - unlink(dst0); // drop the half-installed pair; vcdSafeCopyFile already removed its partial write + unlink(dst0); unlink(dst1); - vcdWriteBdmaMarker(mcDir, VCD_BDMA_FAT32); + + int pairRestored = 0; + if (hadOldPair && + vcdSafeWriteFile(dst0, old0, old0Size) == 0 && + vcdSafeWriteFile(dst1, old1, old1Size) == 0) { + pairRestored = 1; + + // The driver pair is the functional state. If its old marker cannot be restored, keep the + // working pair and remove the untrustworthy marker so the next launch re-validates/equips it. + if (hadOldMarker) { + if (vcdSafeWriteFile(markerPath, oldMarker, oldMarkerSize) != 0) + unlink(markerPath); + } else { + unlink(markerPath); + } + } + + if (!pairRestored) { + unlink(dst0); + unlink(dst1); + vcdWriteBdmaMarker(mcDir, VCD_BDMA_FAT32); + } + + free(old0); + free(old1); + free(oldMarker); return r; } - int mr = vcdWriteBdmaMarker(mcDir, mode); - return (mr != 0) ? mr : 0; + free(old0); + free(old1); + free(oldMarker); + return 0; } // Best-effort auto-equip of the device-matching BDMA driver before a VCD launch (POPSLoader's @@ -899,8 +1029,11 @@ void vcdEnsureBdmaForLaunch(int source, int mode) return; // user opted to manage the BDMA driver manually (General Settings -> BDMA Source/Mode) if (mode <= VCD_BDMA_FAT32 || mode >= VCD_BDMA_MODE_COUNT) return; // FAT32 / invalid -> POPSTARTER's built-in driver, nothing to equip - if (vcdReadBdmaMode() == mode) - return; // the matching variant is already on the card + char mcDir[64]; + if (vcdReadBdmaMode() == mode && + vcdResolvePopstarterMc(mcDir, sizeof(mcDir)) && + vcdBdmaLivePairPresent(mcDir)) + return; // marker and both non-empty live modules agree int er = vcdEquipBdma(source, mode, diag, sizeof(diag)); if (er == 0)