Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions include/hddsupport.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion include/opl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
15 changes: 13 additions & 2 deletions src/hddsupport.c
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ static int hddCreateOPLPartition(const char *name)
return result;
}

int hddLoadModules(void)
static int hddLoadModulesInternal(int reportError)
{
int retLoadModule;
int wait;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
Expand Down
20 changes: 10 additions & 10 deletions src/mmcesupport.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
51 changes: 26 additions & 25 deletions src/opl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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;

Expand Down
157 changes: 145 additions & 12 deletions src/vcdsupport.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -854,28 +921,91 @@ 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);
r = vcdSafeCopyFile(tmp1, dst1);
}
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
Expand All @@ -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)
Expand Down