From 103f68f1992acc860962b9482d5a371ab5ba211a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 3 Nov 2026 01:07:12 +0100 Subject: [PATCH 1/2] sceIo: Implement the memory stick path devctls the pre-3.80 VSH browsers use - Fixes #22890 Before 3.80, the XMB's music browser doesn't list a folder with sceIoDopen/sceIoDread. It asks the FAT driver directly, with the path as a list of UTF-16 components: devctl 0x02415864 starts a directory search and returns a dir fd, read entry by entry with ioctl 0x02425069. 0x02435865 and 0x02435860 are getstat and open, with the path given the same way. None of these were implemented, so firmware 3.71 and older found no tracks. The argument layouts are from fatmsmod.prx and music_browser.prx in 3.71. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceIo.cpp | 251 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) diff --git a/Core/HLE/sceIo.cpp b/Core/HLE/sceIo.cpp index 1216068c31..29879fb410 100644 --- a/Core/HLE/sceIo.cpp +++ b/Core/HLE/sceIo.cpp @@ -24,6 +24,7 @@ #include "Common/Thread/ThreadUtil.h" #include "Common/Profiler/Profiler.h" #include "Common/TimeUtil.h" +#include "Common/Data/Encoding/Utf8.h" #include "Common/File/FileUtil.h" #include "Common/Serialize/SerializeFuncs.h" @@ -1764,6 +1765,10 @@ struct DeviceSize { u32_le sectorCount; }; +static u32 IoOpenFatSearch(u32 argAddr, int argLen); +static u32 IoGetstatFatComponentPath(u32 argAddr, int argLen); +static u32 IoOpenFatComponentPath(u32 argAddr, int argLen); + static u32 sceIoDevctl(const char *name, int cmd, u32 argAddr, int argLen, u32 outPtr, int outLen) { if (strcmp(name, "emulator:")) { DEBUG_LOG(Log::sceIo,"sceIoDevctl(\"%s\", %08x, %08x, %i, %08x, %i)", name, cmd, argAddr, argLen, outPtr, outLen); @@ -2020,6 +2025,15 @@ static u32 sceIoDevctl(const char *name, int cmd, u32 argAddr, int argLen, u32 o case 0x02425856: // Used by VSH, no clue what it should do. Let's just return 0. return hleLogError(Log::sceIo, 0, "Unknown memstick devctl: %08x", cmd); + case 0x02415864: + // Directory search, used by the VSH media browsers before 3.80. + return IoOpenFatSearch(argAddr, argLen); + case 0x02435865: + // Getstat, with the path given the same way as 0x02415864. + return IoGetstatFatComponentPath(argAddr, argLen); + case 0x02435860: + // Open, with the path given the same way as 0x02415864. + return IoOpenFatComponentPath(argAddr, argLen); } } @@ -2748,6 +2762,232 @@ static u32 sceIoDclose(int id) { return hleLogDebug(Log::sceIo, kernelObjects.Destroy(id)); } +// Reads a null-terminated UTF-16 string from guest memory. +static bool ReadGuestUTF16(u32 addr, std::string *out) { + std::u16string str; + for (int i = 0; i < 256; i++) { + if (!Memory::IsValidRange(addr + i * 2, 2)) { + return false; + } + const char16_t c = Memory::ReadUnchecked_U16(addr + i * 2); + if (c == 0) { + *out = ConvertUCS2ToUTF8(str); + return true; + } + str.push_back(c); + } + return false; +} + +// Reads a null-terminated list of pointers to UTF-16 strings. +static bool ReadGuestUTF16List(u32 addr, std::vector *out) { + for (int i = 0; i < 64; i++) { + if (!Memory::IsValid4AlignedAddress(addr + i * 4)) { + return false; + } + const u32 ptr = Memory::ReadUnchecked_U32(addr + i * 4); + if (ptr == 0) { + return true; + } + std::string str; + if (!ReadGuestUTF16(ptr, &str)) { + return false; + } + out->push_back(str); + } + return false; +} + +// A path given as a list of UTF-16 components, like "/", "PSP", "MUSIC". +static bool ReadFatComponentPath(u32 addr, std::string *path) { + std::vector components; + if (!ReadGuestUTF16List(addr, &components)) { + return false; + } + *path = "ms0:"; + for (const std::string &component : components) { + if (component != "/") { + *path += "/" + component; + } + } + if (*path == "ms0:") { + *path += "/"; + } + return true; +} + +// sceIoDevctl("ms0:", 0x02435865): sceIoGetstat with the path given as UTF-16 components (see +// IoOpenFatSearch). The argument is { path list, SceIoStat *, options }. +static u32 IoGetstatFatComponentPath(u32 argAddr, int argLen) { + if (argLen < 12 || !Memory::IsValid4AlignedAddress(argAddr)) { + return hleLogError(Log::sceIo, SCE_KERNEL_ERROR_ERRNO_INVALID_ARGUMENT, "bad args"); + } + const u32 pathListAddr = Memory::ReadUnchecked_U32(argAddr); + const u32 statAddr = Memory::ReadUnchecked_U32(argAddr + 4); + const u32 optionsAddr = Memory::ReadUnchecked_U32(argAddr + 8); + std::string path; + if (!pathListAddr || !optionsAddr || !Memory::IsValidRange(statAddr, sizeof(SceIoStat)) || !ReadFatComponentPath(pathListAddr, &path)) { + return hleLogError(Log::sceIo, SCE_KERNEL_ERROR_ERRNO_INVALID_ARGUMENT, "bad args"); + } + const PSPFileInfo info = pspFileSystem.GetFileInfo(path); + if (!info.exists) { + return hleLogWarning(Log::sceIo, SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND, "%s not found", path.c_str()); + } + __IoGetStat((SceIoStat *)Memory::GetPointerUnchecked(statAddr), info, true); + return hleLogDebug(Log::sceIo, 0, "%s", path.c_str()); +} + +// sceIoDevctl("ms0:", 0x02435860): sceIoOpen with the path given as UTF-16 components (see +// IoOpenFatSearch). The argument is { path list, flags, mode, options }, and the result a file fd. +static u32 IoOpenFatComponentPath(u32 argAddr, int argLen) { + if (argLen < 16 || !Memory::IsValid4AlignedAddress(argAddr)) { + return hleLogError(Log::sceIo, SCE_KERNEL_ERROR_ERRNO_INVALID_ARGUMENT, "bad args"); + } + const u32 pathListAddr = Memory::ReadUnchecked_U32(argAddr); + const int flags = (int)Memory::ReadUnchecked_U32(argAddr + 4); + const int mode = (int)Memory::ReadUnchecked_U32(argAddr + 8); + const u32 optionsAddr = Memory::ReadUnchecked_U32(argAddr + 12); + std::string path; + if (!pathListAddr || !optionsAddr || !ReadFatComponentPath(pathListAddr, &path)) { + return hleLogError(Log::sceIo, SCE_KERNEL_ERROR_ERRNO_INVALID_ARGUMENT, "bad args"); + } + int error = 0; + FileNode *f = __IoOpen(error, path.c_str(), flags, mode); + if (!f) { + return hleLogWarning(Log::sceIo, error, "%s: open failed", path.c_str()); + } + const int id = __IoAllocFd(f); + if (id < 0) { + kernelObjects.Destroy(f->GetUID()); + return hleLogError(Log::sceIo, id, "out of fds"); + } + asyncParams[id].priority = asyncDefaultPriority; + return hleLogDebug(Log::sceIo, id, "%s", path.c_str()); +} + +// Case-insensitive, like FAT. Only * and ?. +static bool FatPatternMatch(const char *pattern, const char *name) { + if (*pattern == '\0') { + return *name == '\0'; + } + if (*pattern == '*') { + for (const char *n = name; ; n++) { + if (FatPatternMatch(pattern + 1, n)) { + return true; + } + if (*n == '\0') { + return false; + } + } + } + if (*name == '\0') { + return false; + } + if (*pattern != '?' && tolower((unsigned char)*pattern) != tolower((unsigned char)*name)) { + return false; + } + return FatPatternMatch(pattern + 1, name + 1); +} + +// The memory stick FAT driver's directory search, which the VSH's media browsers used before +// 3.80 switched them to sceIoDopen/sceIoDread. sceIoDevctl("ms0:", 0x02415864) takes three +// pointers: a null-terminated list of UTF-16 path components ("/", "PSP", "MUSIC"), a list of +// UTF-16 name patterns, and an options struct. It returns a directory fd, read with +// sceIoIoctl(0x02425069) and closed with sceIoDclose. +// From fatmsmod.prx (3.71): the devctl opens "ms0:....****DOPEN MAGIC NAME****...." and passes +// the arguments on to the dir with ioctl 0x0243D85F, which starts a FAT find-first. +static u32 IoOpenFatSearch(u32 argAddr, int argLen) { + if (argLen < 12 || !Memory::IsValid4AlignedAddress(argAddr)) { + return hleLogError(Log::sceIo, SCE_KERNEL_ERROR_ERRNO_INVALID_ARGUMENT, "bad args"); + } + const u32 pathListAddr = Memory::ReadUnchecked_U32(argAddr); + const u32 patternListAddr = Memory::ReadUnchecked_U32(argAddr + 4); + const u32 optionsAddr = Memory::ReadUnchecked_U32(argAddr + 8); + if (!pathListAddr || !patternListAddr || !optionsAddr) { + return hleLogError(Log::sceIo, SCE_KERNEL_ERROR_ERRNO_INVALID_ARGUMENT, "null arg"); + } + + std::string path; + std::vector patterns; + if (!ReadFatComponentPath(pathListAddr, &path) || !ReadGuestUTF16List(patternListAddr, &patterns)) { + return hleLogError(Log::sceIo, SCE_KERNEL_ERROR_ERRNO_INVALID_ARGUMENT, "bad path or pattern list"); + } + + bool exists = false; + std::vector listing = pspFileSystem.GetDirListing(path, &exists); + if (!exists) { + return hleLogWarning(Log::sceIo, SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND, "%s not found", path.c_str()); + } + + DirListing *dir = new DirListing(); + for (const PSPFileInfo &info : listing) { + if (info.name == "." || info.name == "..") { + continue; + } + for (const std::string &pattern : patterns) { + if (FatPatternMatch(pattern.c_str(), info.name.c_str())) { + dir->listing.push_back(info); + break; + } + } + } + dir->index = 0; + dir->name = path; + const SceUID id = kernelObjects.Create(dir); + return hleLogDebug(Log::sceIo, id, "%s: %d matching entries", path.c_str(), (int)dir->listing.size()); +} + +// Reads the next entry of a search started by IoOpenFatSearch. The 0x224 byte argument holds: +// +0x000 SceIoDirent *, filled as by sceIoDread, d_private in the old layout (short name at +// +0, long name at +13) +// +0x004 the 8.3 name in UTF-16, 13 chars +// +0x01E the long name in UTF-16, 256 chars, empty if the entry has none +// +0x220 int *, gets the status of converting the 8.3 name to UTF-16 (1 = lossy, which the +// music browser skips if there's no long name either) +// Returns 1 per entry, 0 at the end. +static int IoReadFatSearchEntry(DirListing *dir, u32 argAddr, u32 argLen) { + if (argLen != 0x224 || !Memory::IsValidRange(argAddr, 0x224)) { + return SCE_KERNEL_ERROR_ERRNO_INVALID_ARGUMENT; + } + const u32 direntAddr = Memory::ReadUnchecked_U32(argAddr); + if (!Memory::IsValidRange(direntAddr, sizeof(SceIoDirEnt))) { + return SCE_KERNEL_ERROR_ERRNO_INVALID_ARGUMENT; + } + if (dir->index >= (int)dir->listing.size()) { + return 0; + } + + const int index = dir->index++; + const PSPFileInfo &info = dir->listing[index]; + const std::string &shortName = dir->ShortName(index); + + SceIoDirEnt *entry = (SceIoDirEnt *)Memory::GetPointerUnchecked(direntAddr); + __IoGetStat(&entry->d_stat, info, true); + strcpy_limit(entry->d_name, info.name.c_str(), ARRAY_SIZE(entry->d_name)); + if (Memory::IsValidRange(entry->d_private, 13 + 256)) { + strcpy_limit((char *)Memory::GetPointerUnchecked(entry->d_private), shortName.c_str(), 13); + strcpy_limit((char *)Memory::GetPointerUnchecked(entry->d_private + 13), entry->d_name, 256); + } + + auto writeUTF16 = [](u32 addr, const std::string &str, int maxChars) { + const std::u16string utf16 = ConvertUTF8ToUCS2(str); + int i = 0; + for (; i < (int)utf16.size() && i < maxChars - 1; i++) { + Memory::WriteUnchecked_U16(utf16[i], addr + i * 2); + } + Memory::WriteUnchecked_U16(0, addr + i * 2); + }; + writeUTF16(argAddr + 0x4, shortName, 13); + // On FAT, a name that's already a valid 8.3 name has no long name entry. + writeUTF16(argAddr + 0x1E, info.name == shortName ? std::string() : info.name, 256); + + const u32 statusAddr = Memory::ReadUnchecked_U32(argAddr + 0x220); + if (Memory::IsValid4AlignedAddress(statusAddr)) { + Memory::WriteUnchecked_U32(0, statusAddr); + } + return 1; +} + int __IoIoctl(u32 id, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 outlen, int &usec) { u32 error; FileNode *f = __IoGetFd(id, error); @@ -3028,6 +3268,17 @@ int __IoIoctl(u32 id, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 out } u32 sceIoIoctl(u32 id, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 outlen) { + if (cmd == 0x02425069) { + // Next entry of a memory stick directory search. That works on a directory fd. + u32 error; + if (DirListing *dir = kernelObjects.Get(id, error)) { + const int result = IoReadFatSearchEntry(dir, indataPtr, inlen); + if (result == 1) { + return hleLogDebug(Log::sceIo, result, "%s", dir->listing[dir->index - 1].name.c_str()); + } + return hleLogDebugOrError(Log::sceIo, result); + } + } int usec = 0; int result = __IoIoctl(id, cmd, indataPtr, inlen, outdataPtr, outlen, usec); if (usec != 0) { From 858b8f48739f3525c9185be1792e0884d958c016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 3 Nov 2026 01:27:02 +0100 Subject: [PATCH 2/2] sceIo: Let the 1.50 XMB's music browser read the memory stick The 1.5x music browser reads PSP/MUSIC through msoem0:, the memory stick with 8.3 names in the OEM code page, which wasn't mounted. It lists folders with ioctl 0x02425051 on an ordinary directory fd (sceIoDread plus the name in UTF-16), and sets a value on fatmsOem0: with devctl 0x02405852 at boot. Formats from fatmsmod.prx and music_plugin.prx in 1.50. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceIo.cpp | 127 +++++++++++++++++++++++++++++++++------------ Core/System.cpp | 2 + 2 files changed, 97 insertions(+), 32 deletions(-) diff --git a/Core/HLE/sceIo.cpp b/Core/HLE/sceIo.cpp index 29879fb410..f2b67832a3 100644 --- a/Core/HLE/sceIo.cpp +++ b/Core/HLE/sceIo.cpp @@ -211,6 +211,8 @@ struct IoAsyncParams { static IoAsyncParams asyncParams[PSP_COUNT_FDS]; static HLEHelperThread *asyncThreads[PSP_COUNT_FDS]{}; static int asyncDefaultPriority = -1; +// See sceIoDevctl("fatmsOem0:"). Not saved in states, since nothing reads it but the guest. +static u32 g_fatmsOemValue = 0; class FileNode : public KernelObject { public: @@ -669,6 +671,7 @@ void __IoInit() { __KernelListenThreadEnd(&TellFsThreadEnded); memset(fds, 0, sizeof(fds)); + g_fatmsOemValue = 0; ioManagerThreadEnabled = true; ioManager.SetThreadEnabled(true); @@ -953,7 +956,7 @@ static bool IsRemovedMemstickPath(const char *path) { if (MemoryStick_State() == PSP_MEMORYSTICK_STATE_INSERTED) { return false; } - return startsWithNoCase(path, "ms0:") || startsWithNoCase(path, "fatms0:"); + return startsWithNoCase(path, "ms0:") || startsWithNoCase(path, "fatms0:") || startsWithNoCase(path, "msoem0:"); } static u32 sceIoGetstat(const char *filename, u32 addr) { @@ -2037,6 +2040,26 @@ static u32 sceIoDevctl(const char *name, int cmd, u32 argAddr, int argLen, u32 o } } + if (!strcmp(name, "fatmsOem0:")) { + // From fatmsmod.prx (1.50): 0x02405852 stores a word in the driver and 0x02405853 reads it + // back into the argument. Probably the OEM code page for 8.3 names (the 1.50 VSH sets it + // at boot). We don't use it. + switch (cmd) { + case 0x02405852: + if (!Memory::IsValid4AlignedAddress(argAddr) || argLen < 4) { + return hleLogError(Log::sceIo, SCE_KERNEL_ERROR_ERRNO_INVALID_ARGUMENT); + } + g_fatmsOemValue = Memory::ReadUnchecked_U32(argAddr); + return hleLogDebug(Log::sceIo, 0, "set %d", g_fatmsOemValue); + case 0x02405853: + if (!Memory::IsValid4AlignedAddress(argAddr) || argLen < 4) { + return hleLogError(Log::sceIo, SCE_KERNEL_ERROR_ERRNO_INVALID_ARGUMENT); + } + Memory::WriteUnchecked_U32(g_fatmsOemValue, argAddr); + return hleLogDebug(Log::sceIo, 0, "get %d", g_fatmsOemValue); + } + } + if (!strcmp(name, "fatms0:")) { switch (cmd) { @@ -2937,9 +2960,44 @@ static u32 IoOpenFatSearch(u32 argAddr, int argLen) { return hleLogDebug(Log::sceIo, id, "%s: %d matching entries", path.c_str(), (int)dir->listing.size()); } +static void WriteGuestUTF16(u32 addr, const std::string &str, int maxChars) { + const std::u16string utf16 = ConvertUTF8ToUCS2(str); + int i = 0; + for (; i < (int)utf16.size() && i < maxChars - 1; i++) { + Memory::WriteUnchecked_U16(utf16[i], addr + i * 2); + } + Memory::WriteUnchecked_U16(0, addr + i * 2); +} + +// Fills a SceIoDirent with the next entry of dir, like sceIoDread, but with d_private always in +// the old layout (8.3 name at +0, long name at +13), which is what fatms writes for these ioctls. +// Returns the entry's index, or -1 at the end. +static int IoReadFatDirent(DirListing *dir, u32 direntAddr) { + if (dir->index >= (int)dir->listing.size()) { + return -1; + } + const int index = dir->index++; + const PSPFileInfo &info = dir->listing[index]; + const std::string &shortName = dir->ShortName(index); + + SceIoDirEnt *entry = (SceIoDirEnt *)Memory::GetPointerUnchecked(direntAddr); + __IoGetStat(&entry->d_stat, info, true); + strcpy_limit(entry->d_name, info.name.c_str(), ARRAY_SIZE(entry->d_name)); + if (Memory::IsValidRange(entry->d_private, 13 + 256)) { + strcpy_limit((char *)Memory::GetPointerUnchecked(entry->d_private), shortName.c_str(), 13); + strcpy_limit((char *)Memory::GetPointerUnchecked(entry->d_private + 13), entry->d_name, 256); + } + return index; +} + +// On FAT, a name that's already a valid 8.3 name has no long name entry. +static std::string FatLongName(DirListing *dir, int index) { + const std::string &name = dir->listing[index].name; + return name == dir->ShortName(index) ? std::string() : name; +} + // Reads the next entry of a search started by IoOpenFatSearch. The 0x224 byte argument holds: -// +0x000 SceIoDirent *, filled as by sceIoDread, d_private in the old layout (short name at -// +0, long name at +13) +// +0x000 SceIoDirent *, filled as by sceIoDread // +0x004 the 8.3 name in UTF-16, 13 chars // +0x01E the long name in UTF-16, 256 chars, empty if the entry has none // +0x220 int *, gets the status of converting the 8.3 name to UTF-16 (1 = lossy, which the @@ -2953,34 +3011,12 @@ static int IoReadFatSearchEntry(DirListing *dir, u32 argAddr, u32 argLen) { if (!Memory::IsValidRange(direntAddr, sizeof(SceIoDirEnt))) { return SCE_KERNEL_ERROR_ERRNO_INVALID_ARGUMENT; } - if (dir->index >= (int)dir->listing.size()) { + const int index = IoReadFatDirent(dir, direntAddr); + if (index < 0) { return 0; } - - const int index = dir->index++; - const PSPFileInfo &info = dir->listing[index]; - const std::string &shortName = dir->ShortName(index); - - SceIoDirEnt *entry = (SceIoDirEnt *)Memory::GetPointerUnchecked(direntAddr); - __IoGetStat(&entry->d_stat, info, true); - strcpy_limit(entry->d_name, info.name.c_str(), ARRAY_SIZE(entry->d_name)); - if (Memory::IsValidRange(entry->d_private, 13 + 256)) { - strcpy_limit((char *)Memory::GetPointerUnchecked(entry->d_private), shortName.c_str(), 13); - strcpy_limit((char *)Memory::GetPointerUnchecked(entry->d_private + 13), entry->d_name, 256); - } - - auto writeUTF16 = [](u32 addr, const std::string &str, int maxChars) { - const std::u16string utf16 = ConvertUTF8ToUCS2(str); - int i = 0; - for (; i < (int)utf16.size() && i < maxChars - 1; i++) { - Memory::WriteUnchecked_U16(utf16[i], addr + i * 2); - } - Memory::WriteUnchecked_U16(0, addr + i * 2); - }; - writeUTF16(argAddr + 0x4, shortName, 13); - // On FAT, a name that's already a valid 8.3 name has no long name entry. - writeUTF16(argAddr + 0x1E, info.name == shortName ? std::string() : info.name, 256); - + WriteGuestUTF16(argAddr + 0x4, dir->ShortName(index), 13); + WriteGuestUTF16(argAddr + 0x1E, FatLongName(dir, index), 256); const u32 statusAddr = Memory::ReadUnchecked_U32(argAddr + 0x220); if (Memory::IsValid4AlignedAddress(statusAddr)) { Memory::WriteUnchecked_U32(0, statusAddr); @@ -2988,6 +3024,33 @@ static int IoReadFatSearchEntry(DirListing *dir, u32 argAddr, u32 argLen) { return 1; } +// ioctl 0x02425051 on a memory stick directory fd: sceIoDread plus the name in UTF-16, which the +// 1.50 XMB's music browser reads folders with. From fatmsmod.prx (1.50). The 0x208 byte argument: +// +0x000 SceIoDirent *, filled as by sceIoDread +// +0x004 the long name in UTF-16, 256 chars. If the entry has none: the 8.3 name if the flag +// at +0x204 is 1, else empty. +// +0x204 int flag, see above +// Returns 1 per entry, 0 at the end. +static int IoReadFatDirentUTF16(DirListing *dir, u32 argAddr, u32 argLen) { + if (argLen < 0x208 || !Memory::IsValidRange(argAddr, 0x208)) { + return SCE_KERNEL_ERROR_ERRNO_INVALID_ARGUMENT; + } + const u32 direntAddr = Memory::ReadUnchecked_U32(argAddr); + if (!Memory::IsValidRange(direntAddr, sizeof(SceIoDirEnt))) { + return SCE_KERNEL_ERROR_ERRNO_INVALID_ARGUMENT; + } + const int index = IoReadFatDirent(dir, direntAddr); + if (index < 0) { + return 0; + } + std::string name = FatLongName(dir, index); + if (name.empty() && Memory::ReadUnchecked_U32(argAddr + 0x204) == 1) { + name = dir->ShortName(index); + } + WriteGuestUTF16(argAddr + 0x4, name, 256); + return 1; +} + int __IoIoctl(u32 id, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 outlen, int &usec) { u32 error; FileNode *f = __IoGetFd(id, error); @@ -3268,11 +3331,11 @@ int __IoIoctl(u32 id, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 out } u32 sceIoIoctl(u32 id, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 outlen) { - if (cmd == 0x02425069) { - // Next entry of a memory stick directory search. That works on a directory fd. + if (cmd == 0x02425069 || cmd == 0x02425051) { + // Reading a memory stick directory, so these work on a directory fd. u32 error; if (DirListing *dir = kernelObjects.Get(id, error)) { - const int result = IoReadFatSearchEntry(dir, indataPtr, inlen); + const int result = cmd == 0x02425069 ? IoReadFatSearchEntry(dir, indataPtr, inlen) : IoReadFatDirentUTF16(dir, indataPtr, inlen); if (result == 1) { return hleLogDebug(Log::sceIo, result, "%s", dir->listing[dir->index - 1].name.c_str()); } diff --git a/Core/System.cpp b/Core/System.cpp index 133f58de20..73606e9c08 100644 --- a/Core/System.cpp +++ b/Core/System.cpp @@ -354,6 +354,8 @@ static void MountFileSystems() { pspFileSystem.Mount("fatms0:", memstickSystem); pspFileSystem.Mount("fatms:", memstickSystem); pspFileSystem.Mount("pfat0:", memstickSystem); + // The memory stick with 8.3 names in the OEM code page. The 1.50 XMB's music browser uses it. + pspFileSystem.Mount("msoem0:", memstickSystem); // TODO: These should be made "lazy" mounts. auto flash0System = std::make_shared(&pspFileSystem, g_Config.nandRootDirectory / "flash0", FileSystemFlags::FLASH);