libgui 2.01
libgui is a GUI library for the GameCube, Wii, and Wii U
 
Loading...
Searching...
No Matches
WutFileSystemDriver.cpp
1/****************************************************************************
2 * Platform Abstraction Layer (WUT driver)
3 * Daryl Borth 2026
4 * WutFileSystemDriver.cpp
5 *
6 * Wii U storage device enumeration + mounting: SD via a plain
7 * WHBMountSdCard() FSA mount, done exactly once in init() and treated as
8 * permanently present from then on
9 * USB1/2/3 via libmocha's raw disc interface + libdvm (see dvm_wut.c/h).
10 ***************************************************************************/
11#include <whb/sdcard.h>
12#include <mocha/mocha.h>
13#include <mocha/disc_interface.h>
14#include <string.h>
15#include <stdio.h>
16#include <unistd.h>
17
18#include "WutFileSystemDriver.h"
19#include "dvm_wut.h"
20#include "../Logger.h"
21
23static const char * const SD_FALLBACK_PREFIX = "/vol/external01/";
24
26static const int SD_MOUNT_ATTEMPTS = 3;
27static const useconds_t SD_MOUNT_RETRY_DELAY_US = 100000;
28
31static void NormalizeSdPrefix(char prefix[32])
32{
33 const char * sdPath = WHBGetSdCardMountPath();
34
35 if(sdPath && sdPath[0])
36 {
37 strncpy(prefix, sdPath, 32 - 2); // leave room for '/' + NUL
38 prefix[32 - 2] = '\0';
39
40 size_t len = strlen(prefix);
41 if(len == 0 || prefix[len - 1] != '/')
42 {
43 prefix[len] = '/';
44 prefix[len + 1] = '\0';
45 }
46 }
47 else
48 {
49 strncpy(prefix, SD_FALLBACK_PREFIX, 32 - 1);
50 prefix[32 - 1] = '\0';
51 }
52}
53
55{
56 memset(devices, 0, sizeof(devices));
57 deviceCount = 0;
58
59 // USB goes through Mocha's raw DISC_INTERFACE + libdvm. If this fails -
60 // not booted under Aroma/compatible CFW, or Mocha not installed (it's
61 // optional there) - USB is unavailable entirely.
62 mochaReady = (Mocha_InitLibrary() == MOCHA_RESULT_SUCCESS);
63
64 // Independent of Mocha - dvmWutInit() just registers libdvm's vfat/exfat/ntfs filesystem drivers, which don't touch hardware themselves.
65 dvmWutInit();
66
67 // storageSlots[] is USB only: [0..2] = USB1..3, i.e. devices[slotUSB1 + i]
68 storageSlots[0] = { &Mocha_usb1_disc_interface, "usb1", 0, 0 };
69 storageSlots[1] = { &Mocha_usb2_disc_interface, "usb2", 0, 0 };
70 storageSlots[2] = { &Mocha_usb3_disc_interface, "usb3", 0, 0 };
71
72 // SD: plain FSA mount, attempted once
73 WutDeviceState & sd = devices[slotSD];
74 memset(&sd, 0, sizeof(sd));
75 sd.id = DEVICE_SD;
76 strcpy(sd.name, "SD Card");
77
78 bool sdMounted = false;
79 for(int attempt = 0; attempt < SD_MOUNT_ATTEMPTS && !sdMounted; attempt++)
80 {
81 if(attempt > 0)
82 usleep(SD_MOUNT_RETRY_DELAY_US);
83 sdMounted = WHBMountSdCard();
84 }
85
86 NormalizeSdPrefix(sd.stablePrefix);
87 strcpy(sd.prefix, sd.stablePrefix);
88 sd.isPresent = true;
89 sd.isMounted = true;
90
91 // USB 1/2/3 setup
92 WutDeviceState & usb1 = devices[slotUSB1];
93 memset(&usb1, 0, sizeof(usb1));
94 usb1.id = DEVICE_USB;
95 strcpy(usb1.name, "USB Storage 1");
96 snprintf(usb1.stablePrefix, sizeof(usb1.stablePrefix), "%s:/", storageSlots[0].mountName);
97
98 WutDeviceState & usb2 = devices[slotUSB2];
99 memset(&usb2, 0, sizeof(usb2));
100 usb2.id = DEVICE_USB2;
101 strcpy(usb2.name, "USB Storage 2");
102 snprintf(usb2.stablePrefix, sizeof(usb2.stablePrefix), "%s:/", storageSlots[1].mountName);
103
104 WutDeviceState & usb3 = devices[slotUSB3];
105 memset(&usb3, 0, sizeof(usb3));
106 usb3.id = DEVICE_USB3;
107 strcpy(usb3.name, "USB Storage 3");
108 snprintf(usb3.stablePrefix, sizeof(usb3.stablePrefix), "%s:/", storageSlots[2].mountName);
109
110 smbDriver.init();
111
112 WutDeviceState & smb = devices[slotSMB];
113 memset(&smb, 0, sizeof(smb));
114 smb.id = DEVICE_SMB;
115 strcpy(smb.name, "Network Share");
116 strcpy(smb.stablePrefix, "smb:/");
117
118 deviceCount = slotCount;
119}
120
122{
123 smbDriver.shutdown();
124
125 WHBUnmountSdCard();
126
127 // unmountStorageSlot() only touches whichever slots were actually
128 // mounted. Mocha_usbN_shutdown() is a safe no-op on an interface that
129 // was never started, so no special-casing needed.
130 for(int i = 0; i < storageSlotCount; i++)
131 unmountStorageSlot(i);
132
133 for(int i = 0; i < storageSlotCount; i++)
134 if(storageSlots[i].iface)
135 storageSlots[i].iface->shutdown();
136
137 if(mochaReady)
138 {
139 Mocha_DeInitLibrary();
140 mochaReady = false;
141 }
142
143 memset(devices, 0, sizeof(devices));
144 deviceCount = 0;
145}
146
147int WutFileSystemDriver::findDeviceIndex(int deviceId) const
148{
149 for(int i = 0; i < deviceCount; i++)
150 if(devices[i].id == deviceId)
151 return i;
152 return -1;
153}
154
155void WutFileSystemDriver::getVolumeLabel(WutDeviceState & dev, const char * dvmName)
156{
157 // Only ever looked up once per mount
158 if(dev.labelFetched)
159 return;
160
161 dev.volumeLabel[0] = '\0';
162
163 if(dvmName) // null when there's no dvm volume behind the device - no label source then
164 {
165 char path[16];
166 snprintf(path, sizeof(path), "%s:", dvmName);
167 dvmWutGetVolumeLabel(path, dev.volumeLabel, sizeof(dev.volumeLabel));
168 }
169
170 dev.labelFetched = true;
171}
172
173bool WutFileSystemDriver::tryMountStorageSlot(int slotIdx)
174{
175 if(slotIdx < 0 || slotIdx >= storageSlotCount)
176 return false;
177
178 WutDeviceState & dev = devices[slotUSB1 + slotIdx];
179 WutStorageSlot & slot = storageSlots[slotIdx];
180
181 if(dev.isMounted)
182 return true;
183
184 if(dev.unmountRequired)
185 unmountStorageSlot(slotIdx);
186
187 if(!mochaReady)
188 return false;
189
190 // Backing off after repeated failures against an unremoved device
191 if(slot.backoffPollsLeft > 0)
192 {
193 slot.backoffPollsLeft--;
194 return false;
195 }
196
197 // dvmWutMountVolume() takes a non-const DISC_INTERFACE* but never mutates it
198 if(dvmWutMountVolume(slot.mountName, (DISC_INTERFACE *) slot.iface, cachePages, sectorsPerPage))
199 {
200 slot.failCount = 0;
201 dev.isPresent = true;
202 dev.isMounted = true;
203 dev.unmountRequired = false;
204 snprintf(dev.prefix, sizeof(dev.prefix), "%s:/", slot.mountName);
205 getVolumeLabel(dev, slot.mountName);
206 return true;
207 }
208
209 // Mount failed. Always leave the interface shutdown() here rather than leaving the fd open across attempts.
210 slot.iface->shutdown();
211
212 slot.failCount++;
213 if(slot.failCount < maxQuickRetries)
214 slot.backoffPollsLeft = 0; // could be transient (eg. drive still spinning up) - try again next cycle
215 else
216 slot.backoffPollsLeft = backoffPolls; // give up on this device for a while - never permanently
217
218 dev.isPresent = false;
219 dev.isMounted = false;
220 return false;
221}
222
223void WutFileSystemDriver::unmountStorageSlot(int slotIdx)
224{
225 if(slotIdx < 0 || slotIdx >= storageSlotCount)
226 return;
227
228 WutDeviceState & dev = devices[slotUSB1 + slotIdx];
229 WutStorageSlot & slot = storageSlots[slotIdx];
230
231 if(dev.isMounted)
232 dvmWutUnmountVolume(slot.mountName);
233
234 slot.failCount = 0;
235 slot.backoffPollsLeft = 0;
236 dev.isPresent = false;
237 dev.isMounted = false;
238 dev.unmountRequired = false;
239 dev.labelFetched = false;
240 dev.prefix[0] = '\0';
241 dev.volumeLabel[0] = '\0';
242}
243
244bool WutFileSystemDriver::slotStillPresent(int slotIdx)
245{
246 if(slotIdx < 0 || slotIdx >= storageSlotCount || !mochaReady)
247 return false;
248
249 WutDeviceState & dev = devices[slotUSB1 + slotIdx];
250 if(!dev.isMounted)
251 return false;
252
253 // Forces a genuine, uncached raw sector read through the mounted disc
254 return dvmWutVolumeStillPresent(storageSlots[slotIdx].mountName);
255}
256
257void WutFileSystemDriver::refreshSmbSlot()
258{
259 WutDeviceState & smb = devices[slotSMB];
260 bool connected = smbDriver.isConnected();
261 smb.isPresent = true;
262 smb.isMounted = connected;
263
264 if(connected)
265 {
266 strncpy(smb.prefix, smbDriver.getMountPath(), sizeof(smb.prefix) - 1);
267 smb.prefix[sizeof(smb.prefix) - 1] = '\0';
268 }
269 else
270 {
271 smb.prefix[0] = '\0';
272 smb.volumeLabel[0] = '\0';
273 }
274}
275
277{
278 refreshSmbSlot();
279
280 int count = 0;
281 for(int i = 0; i < deviceCount && count < MAX_STORAGE_DEVICES; i++)
282 {
283 if(!devices[i].isPresent)
284 continue;
285
286 StorageDevice & out = outDevices[count];
287 out.id = devices[i].id;
288 strncpy(out.name, devices[i].name, sizeof(out.name) - 1);
289 out.name[sizeof(out.name) - 1] = '\0';
290 strncpy(out.volumeLabel, devices[i].volumeLabel, sizeof(out.volumeLabel) - 1);
291 out.volumeLabel[sizeof(out.volumeLabel) - 1] = '\0';
292 const char * browsePrefix = (devices[i].id == DEVICE_SMB) ? devices[i].stablePrefix : devices[i].prefix;
293 strncpy(out.prefix, browsePrefix, sizeof(out.prefix) - 1);
294 out.prefix[sizeof(out.prefix) - 1] = '\0';
295 // SD (mounted in init(), never polled) and SMB (needs explicit getSmb()->connect() first) are neither removable nor auto-mounted
296 bool fixedDevice = (devices[i].id == DEVICE_SD || devices[i].id == DEVICE_SMB);
297 out.removable = !fixedDevice;
298 out.autoMountAtStartup = !fixedDevice;
299 out.alwaysListed = (devices[i].id == DEVICE_SMB);
300 count++;
301 }
302 return count;
303}
304
306{
307 if(deviceId == DEVICE_SMB)
308 return smbDriver.isConnected(); // informational only - SMB is alwaysListed
309
310 int idx = findDeviceIndex(deviceId);
311 return idx >= 0 && devices[idx].isPresent;
312}
313
315{
316 int idx = findDeviceIndex(deviceId);
317 if(idx < 0)
318 return MountResult::DeviceNotFound; // not ours
319
320 if(deviceId == DEVICE_SMB)
321 {
322 refreshSmbSlot();
323 return smbDriver.isConnected() ? MountResult::Success : MountResult::DeviceNotFound;
324 }
325
326 // SD is always present and always mounted
327 if(deviceId == DEVICE_SD)
328 return MountResult::Success;
329
330 // USB1/2/3 - backed by storageSlots[]
331 return tryMountStorageSlot(idx - slotUSB1) ? MountResult::Success : MountResult::DeviceNotFound;
332}
333
334const char * WutFileSystemDriver::mountResultMessage(int deviceId, MountResult result)
335{
336 if(result == MountResult::MountFailed)
337 return "Unable to mount device.";
338
339 if(deviceId == DEVICE_SD)
340 return "SD card not found!";
341 else if(deviceId == DEVICE_USB || deviceId == DEVICE_USB2 || deviceId == DEVICE_USB3)
342 return "USB drive not found!";
343 else if(deviceId == DEVICE_SMB)
344 return "Network share not connected!";
345
346 return "Storage device not found!";
347}
348
350{
351 if(deviceId == DEVICE_SMB)
352 {
353 smbDriver.disconnect();
354 refreshSmbSlot();
355 return;
356 }
357
358 if(deviceId == DEVICE_SD)
359 return; // SD never goes away
360
361 int idx = findDeviceIndex(deviceId);
362 if(idx < 0)
363 return;
364
365 devices[idx].isMounted = false;
366 devices[idx].unmountRequired = true;
367 devices[idx].labelFetched = false;
368}
369
370void WutFileSystemDriver::pollStorageDevices(int removedIds[MAX_STORAGE_DEVICES], int & outRemovedCount, bool & deviceListChanged)
371{
372 outRemovedCount = 0;
373 deviceListChanged = false;
374
375 // SD is assumed always present and is not polled at all.
376
377 // Cheap, read-only nsysuhs scan. Detects a real hardware-level
378 // attach/detach independently of whether Mocha's mount attempt has
379 // succeeded. On a change, a fresh insertion should be tried this same
380 // cycle.
381 UsbHardwareSignature currentHwSig;
382 ScanUsbHardwareSignature(currentHwSig);
383
384 if(UsbHardwareSignatureChanged(currentHwSig, usbHwSignature))
385 {
386 // Only backoffPollsLeft, not failCount: a real hardware change
387 // anywhere is worth one fresh look at every USB slot right now
388 // (that's what backoffPollsLeft=0 buys), but a slot sitting on a
389 // permanently-unmountable device (eg. an unsupported filesystem)
390 // still needs to reach maxQuickRetries and back off properly.
391 for(int i = 0; i < storageSlotCount; i++)
392 storageSlots[i].backoffPollsLeft = 0;
393 }
394
395 usbHwSignature = currentHwSig;
396
397 for(int slotIdx = 0; slotIdx < storageSlotCount; slotIdx++)
398 {
399 WutDeviceState & dev = devices[slotUSB1 + slotIdx];
400
401 if(dev.isMounted)
402 {
403 if(!slotStillPresent(slotIdx))
404 {
405 unmountStorageSlot(slotIdx);
406
407 if(outRemovedCount < MAX_STORAGE_DEVICES)
408 removedIds[outRemovedCount++] = dev.id;
409 deviceListChanged = true;
410 }
411 }
412 else if(tryMountStorageSlot(slotIdx))
413 {
414 deviceListChanged = true;
415 }
416 }
417}
418
419const char * WutFileSystemDriver::getDevicePrefix(int device) const
420{
421 int idx = findDeviceIndex(device);
422 return idx < 0 ? "" : devices[idx].stablePrefix;
423}
424
425const char * WutFileSystemDriver::getMountPath(int device) const
426{
427 if(device == DEVICE_SMB)
428 const_cast<WutFileSystemDriver *>(this)->refreshSmbSlot();
429
430 int idx = findDeviceIndex(device);
431 if(idx < 0 || !devices[idx].isMounted || devices[idx].prefix[0] == '\0')
432 return "";
433 return devices[idx].prefix;
434}
435
436const int * WutFileSystemDriver::getValidLoadDevices(int & outCount) const
437{
438 static const int devices[] = { DEVICE_AUTO, DEVICE_SD, DEVICE_USB, DEVICE_USB2, DEVICE_USB3, DEVICE_SMB };
439 outCount = sizeof(devices) / sizeof(devices[0]);
440 return devices;
441}
442
443const int * WutFileSystemDriver::getValidSaveDevices(int & outCount) const
444{
445 static const int devices[] = { DEVICE_AUTO, DEVICE_SD, DEVICE_USB, DEVICE_USB2, DEVICE_USB3, DEVICE_SMB };
446 outCount = sizeof(devices) / sizeof(devices[0]);
447 return devices;
448}
SD: a plain WHBMountSdCard() FSA mount, done once in init().
const char * getDevicePrefix(int device) const override
The devoptab prefix this device would use (eg.
const char * getMountPath(int device) const override
devoptab-style mount path for device (eg.
void shutdown() override
Unmounts every device and releases the driver's resources.
void pollStorageDevices(int removedIds[MAX_STORAGE_DEVICES], int &outRemovedCount, bool &deviceListChanged) override
Called once per device-checking thread cycle.
int enumerateStorageDevices(StorageDevice outDevices[MAX_STORAGE_DEVICES]) override
Fills outDevices (size MAX_STORAGE_DEVICES) and returns the device count.
void init() override
Initializes the driver and auto-mounts devices flagged autoMountAtStartup.
const int * getValidSaveDevices(int &outCount) const override
Devices (Device enum values, in priority order) that are offered when choosing where to save to on th...
const char * mountResultMessage(int deviceId, MountResult result) override
A short, user-displayable reason for a non-Success MountResult (eg.
bool isDevicePresent(int deviceId) const override
Lightweight, cached hardware-presence check for a single device - does NOT mount and does no invasive...
MountResult mountStorageDevice(int deviceId) override
Attempts to mount deviceId exactly once.
void invalidateStorageDevice(int deviceId) override
Marks deviceId as needing a fresh mount next time mountStorageDevice() is called, eg.
const int * getValidLoadDevices(int &outCount) const override
Devices (Device enum values, in priority order) that are offered when choosing where to load from on ...
MountResult
Result of a single mount attempt.
@ MountFailed
present, but couldn't be mounted (eg.
@ DeviceNotFound
not physically present / not inserted
Description of one storage device: id, display name, mount prefix, volume label and flags such as rem...
bool autoMountAtStartup
silently attempted at boot
bool removable
can this device disappear at runtime?
int id
Device enum value (DEVICE_SD, DEVICE_USB, ...)
bool alwaysListed
show in a device listing unconditionally, regardless of isDevicePresent()
char volumeLabel[16]
Volume label of the mounted filesystem, or empty if unknown.
char prefix[32]
devoptab mount prefix, eg.
char name[20]
Short display name for device lists.
The set of USBCLASS_STORAGE interfaces nsysuhs currently reports across every controller_num probed.
Definition WutUsbProbe.h:28
State tracker for a single storage device slot.
int id
Device enum value (DEVICE_SD, DEVICE_USB, ...)
char name[16]
human-readable base name, eg.
bool isMounted
A filesystem is currently mounted on this slot.
char volumeLabel[16]
volume label, best-effort - empty if none could be read
bool labelFetched
volume label already looked up since the last mount/removal - see getVolumeLabel()
char stablePrefix[32]
same string as prefix, but set once in init() and never cleared on unmount - this device's identity f...
bool unmountRequired
The slot was removed or invalidated and must be unmounted before the next mount attempt.
char prefix[32]
devoptab mount prefix, eg.
bool isPresent
found on the last poll
int failCount
consecutive mount failures since the last success or hardware change - see tryMountStorageSlot()
int backoffPollsLeft
polls left to skip before the next probe attempt (0 = probe now)
const DISC_INTERFACE * iface
&Mocha_usb1_disc_interface .
const char * mountName
devoptab basename, eg.