From 5252fe4d940467c2ef414a75ed895b9b3b5ef19e Mon Sep 17 00:00:00 2001 From: drako <98249188+drakolordx7@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:17:18 -0500 Subject: [PATCH] Search files in the configured disc image and add sceCdTrayReq With a disc image configured (HostPathKind::CdImage), the cdvdman emulation reads sectors from the image but resolved sceCdSearchFile against the virtual ISO9660 layout built from the host folder. The two assign different LSNs, so a guest that looks a file up and then reads it through sceCdRead (an IRX streaming from disc, for example) read the wrong sectors, or failed the search when there is no host folder. Walk the image's own ISO9660 directory for sceCdSearchFile instead: read the primary volume descriptor, then the directory record of each path component (honouring the padding at sector ends and ;version suffixes), and return the record's LSN, size and directory flag. If there is no image or it has no ISO9660 volume descriptor, the virtual layout is used as before. The result buffer writing is shared by both paths. Also implement sceCdTrayReq (cdvdman #14), which was an unhandled import: the tray never moves, so it stores a tray count of 0 and returns 1. Killzone's PFILE_R.IRX calls it about 2.7k times per boot. The new test runs a module that searches an in-memory ISO9660 image (file in a subdirectory, file missing) and calls sceCdTrayReq; the search part fails without the change. Made by drakolord and assisted with Claude Code. --- ps2xIOP/src/emulator/imports/iop_cdvd.cpp | 137 ++++++++++++++++++-- ps2xIOP/tests/iop_emulator_tests.cpp | 151 +++++++++++++++++++++- 2 files changed, 277 insertions(+), 11 deletions(-) diff --git a/ps2xIOP/src/emulator/imports/iop_cdvd.cpp b/ps2xIOP/src/emulator/imports/iop_cdvd.cpp index ad58fb4e4..0e4361d71 100644 --- a/ps2xIOP/src/emulator/imports/iop_cdvd.cpp +++ b/ps2xIOP/src/emulator/imports/iop_cdvd.cpp @@ -39,6 +39,7 @@ namespace ps2x::iop::detail constexpr uint32_t kCdvdStreamTimeout = 5000u; constexpr uint32_t kCdvdSyncTimeout = 15000u; constexpr uint32_t kCdvdmanVersion = 0x0226u; + constexpr uint32_t kMaxImageDirectoryBytes = 1u << 20; uint32_t alignSectors(uint64_t bytes) { @@ -280,6 +281,15 @@ namespace ps2x::iop::detail cpu.gpr[2] = kCdvdReadyComplete; return true; + case 14: // sceCdTrayReq(mode, u32 *traycnt): the tray never moves + if (a1 != 0u) + { + const uint32_t trayCount = 0u; + (void)memory.writeRam(a1, &trayCount, sizeof(trayCount)); + } + cpu.gpr[2] = 1u; + return true; + case 28: // sceCdStatus cpu.gpr[2] = kCdvdStatusPause; return true; @@ -594,24 +604,133 @@ namespace ps2x::iop::detail return &nodes[current]; } + // With a disc image configured, sectors are read from it, so files have to be looked up in its own ISO9660 + // directory: the virtual layout built from the host folder assigns different LSNs. + struct ImageEntry + { + uint32_t lsn = 0u; + uint32_t size = 0u; + bool directory = false; + std::string identifier; + }; + + enum class ImageSearch + { + Unavailable, // no image, or it has no ISO9660 volume descriptor + NotFound, + Found, + }; + + bool readImageBytes(uint64_t offset, void *destination, size_t size) + { + const std::string imagePath = host.hostPath(HostPathKind::CdImage); + if (imagePath.empty()) + return false; + if (imageHandle == 0u) + imageHandle = host.openHostFile(imagePath); + size_t bytesRead = 0u; + return imageHandle != 0u && host.readHostFile(imageHandle, offset, destination, size, bytesRead) && bytesRead == size; + } + + ImageSearch searchImage(std::string_view guestPath, ImageEntry &out) + { + std::array descriptor{}; + if (!readImageBytes(static_cast(kPrimaryVolumeDescriptorLsn) * kSectorSize, descriptor.data(), descriptor.size()) || + descriptor[0] != 1u || std::memcmp(descriptor.data() + 1, "CD001", 5) != 0) + return ImageSearch::Unavailable; + + const ParsedPs2Path parsed = parsePs2Path(guestPath); + if (!parsed || parsed.device != Ps2PathDevice::Cdrom) + return ImageSearch::NotFound; + + const auto readLe32 = [](const uint8_t *source) + { + return static_cast(source[0]) | (static_cast(source[1]) << 8u) | + (static_cast(source[2]) << 16u) | (static_cast(source[3]) << 24u); + }; + // The root directory record sits at offset 156 of the volume descriptor. + ImageEntry current{readLe32(descriptor.data() + 158u), readLe32(descriptor.data() + 166u), true, {}}; + size_t begin = 0u; + while (begin <= parsed.path.size()) + { + const size_t end = parsed.path.find('/', begin); + const size_t length = (end == std::string::npos) ? parsed.path.size() - begin : end - begin; + const std::string_view component(parsed.path.data() + begin, length); + begin = (end == std::string::npos) ? parsed.path.size() + 1u : end + 1u; + + if (component.empty() || component == ".") + continue; + if (component == ".." || !current.directory || current.size == 0u || current.size > kMaxImageDirectoryBytes) + return ImageSearch::NotFound; + + std::vector directory(current.size); + if (!readImageBytes(static_cast(current.lsn) * kSectorSize, directory.data(), directory.size())) + return ImageSearch::NotFound; + + const std::string wanted = normalizedIsoComponent(component); + bool found = false; + for (size_t offset = 0u; offset < directory.size();) + { + const uint8_t recordLength = directory[offset]; + if (recordLength == 0u) + { + // Records do not cross sector boundaries; the rest of the sector is padding. + offset = (offset / kSectorSize + 1u) * kSectorSize; + continue; + } + if (recordLength < 33u || offset + recordLength > directory.size()) + break; + const size_t nameLength = std::min(directory[offset + 32u], recordLength - 33u); + const std::string identifier(reinterpret_cast(&directory[offset + 33u]), nameLength); + if (normalizedIsoComponent(identifier) == wanted) + { + current = ImageEntry{readLe32(&directory[offset + 2u]), readLe32(&directory[offset + 10u]), + (directory[offset + 25u] & 2u) != 0u, identifier}; + found = true; + break; + } + offset += recordLength; + } + if (!found) + return ImageSearch::NotFound; + } + out = std::move(current); + return ImageSearch::Found; + } + + // sceCdlFILE: lsn, size, name[16], date/flags[8]. + bool writeSearchResult(uint32_t resultAddress, uint32_t lsn, uint32_t size, std::string_view identifier, bool directory) + { + std::array result{}; + writeLe32(result.data(), lsn); + writeLe32(result.data() + 4u, size); + const std::string leaf = normalizedIsoComponent(identifier); + std::memcpy(result.data() + 8u, leaf.data(), std::min(16u, leaf.size())); + result[24u] = directory ? 2u : 0u; + return memory.writeRam(resultAddress, result.data(), result.size()); + } + bool searchFile(uint32_t resultAddress, uint32_t nameAddress) { if (resultAddress == 0u || nameAddress == 0u) return false; const std::string guestPath = memory.readString(nameAddress, 1024u); + ImageEntry entry; + switch (searchImage(guestPath, entry)) + { + case ImageSearch::Found: + return writeSearchResult(resultAddress, entry.lsn, entry.size, entry.identifier, entry.directory); + case ImageSearch::NotFound: + return false; + case ImageSearch::Unavailable: + break; + } + IsoNode *node = findVirtualIsoNode(guestPath); if (!node) return false; - - // sceCdlFILE: lsn, size, name[16], date/flags[8]. - std::array result{}; - writeLe32(result.data(), node->lsn); - writeLe32(result.data() + 4u, node->size); - const std::string leaf = normalizedIsoComponent(node->identifier); - std::memcpy(result.data() + 8u, leaf.data(), std::min(16u, leaf.size())); - result[24u] = node->directory ? 2u : 0u; - return memory.writeRam(resultAddress, result.data(), result.size()); + return writeSearchResult(resultAddress, node->lsn, node->size, node->identifier, node->directory); } IsoNode *fileForSector(uint32_t lsn) diff --git a/ps2xIOP/tests/iop_emulator_tests.cpp b/ps2xIOP/tests/iop_emulator_tests.cpp index 44c249499..733f9fdfa 100644 --- a/ps2xIOP/tests/iop_emulator_tests.cpp +++ b/ps2xIOP/tests/iop_emulator_tests.cpp @@ -61,21 +61,38 @@ namespace void audioCommand(uint32_t, uint32_t, GuestBuffer, GuestBuffer) override {} std::string hostPath(HostPathKind kind) const override { + if (kind == HostPathKind::CdImage) + return cdImage.empty() ? std::string{} : std::string(kCdImagePath); return kind == HostPathKind::CdRoot ? cdRoot : std::string{}; } std::string translateGuestPath(std::string_view path) const override { return std::string(path); } - uint64_t openHostFile(std::string_view) override { return 0u; } + uint64_t openHostFile(std::string_view path) override + { + return path == kCdImagePath && !cdImage.empty() ? kCdImageHandle : 0u; + } bool hostFileSize(uint64_t, uint64_t &) const override { return false; } - bool readHostFile(uint64_t, uint64_t, void *, size_t, size_t &) override { return false; } + bool readHostFile(uint64_t handle, uint64_t offset, void *destination, size_t size, size_t &bytesRead) override + { + bytesRead = 0u; + if (handle != kCdImageHandle || offset >= cdImage.size()) + return false; + bytesRead = static_cast(std::min(size, cdImage.size() - offset)); + std::memcpy(destination, cdImage.data() + offset, bytesRead); + return true; + } void closeHostFile(uint64_t) override {} int32_t memoryCard(const MemoryCardRequest &) override { return 0; } bool hasGuestFunction(uint32_t) const override { return false; } bool invokeGuestFunction(uint64_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t *) override { return false; } void log(LogLevel, std::string_view message) override { logs.emplace_back(message); } + static constexpr std::string_view kCdImagePath = "test.iso"; + static constexpr uint64_t kCdImageHandle = 2u; + std::vector guest; std::vector logs; std::string cdRoot; + std::vector cdImage; }; #pragma pack(push, 1) @@ -883,6 +900,110 @@ namespace std::memcpy(host.guest.data() + address + codeOffset, segment.data(), segment.size()); } + // A small ISO9660 image: /FILE.BIN (LSN 30) and /DIR/DATA.BIN (LSN 25, 0x1234 bytes). The LSNs differ from what + // the virtual layout of a host folder would assign. + std::vector makeCdImage() + { + constexpr uint32_t sectorSize = 2048u; + std::vector image(40u * sectorSize, 0u); + const auto putBoth32 = [&image](size_t offset, uint32_t value) + { + for (uint32_t i = 0u; i < 4u; ++i) + { + image[offset + i] = static_cast(value >> (i * 8u)); + image[offset + 4u + i] = static_cast(value >> ((3u - i) * 8u)); + } + }; + const auto addRecord = [&](size_t base, size_t &cursor, std::string_view identifier, uint32_t lsn, + uint32_t size, bool directory) + { + const size_t record = base + cursor; + const size_t length = (33u + identifier.size() + 1u) & ~static_cast(1u); + image[record] = static_cast(length); + putBoth32(record + 2u, lsn); + putBoth32(record + 10u, size); + image[record + 25u] = directory ? 2u : 0u; + image[record + 32u] = static_cast(identifier.size()); + std::memcpy(&image[record + 33u], identifier.data(), identifier.size()); + cursor += length; + }; + const std::string_view dot("\0", 1u); + const std::string_view dotDot("\1", 1u); + + const size_t volumeDescriptor = 16u * sectorSize; + image[volumeDescriptor] = 1u; + std::memcpy(&image[volumeDescriptor + 1u], "CD001", 5u); + image[volumeDescriptor + 6u] = 1u; + size_t rootRecord = 156u; + addRecord(volumeDescriptor, rootRecord, dot, 18u, sectorSize, true); + + size_t cursor = 0u; + addRecord(18u * sectorSize, cursor, dot, 18u, sectorSize, true); + addRecord(18u * sectorSize, cursor, dotDot, 18u, sectorSize, true); + addRecord(18u * sectorSize, cursor, "FILE.BIN;1", 30u, 0x800u, false); + addRecord(18u * sectorSize, cursor, "DIR", 19u, sectorSize, true); + cursor = 0u; + addRecord(19u * sectorSize, cursor, dot, 19u, sectorSize, true); + addRecord(19u * sectorSize, cursor, dotDot, 18u, sectorSize, true); + addRecord(19u * sectorSize, cursor, "DATA.BIN;1", 25u, 0x1234u, false); + return image; + } + + // Start routine: sceCdSearchFile("cdrom0:/DIR/DATA.BIN;1") into 0x10500 (result at 0x10540), sceCdTrayReq(2, + // 0x10520) (result at 0x10544), sceCdSearchFile("cdrom0:/MISSING.BIN;1") (result at 0x10548). + void writeCdvdSearchIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t importTableAddress = loadAddress + 0x100u; + constexpr uint32_t searchStub = importTableAddress + 20u; + constexpr uint32_t trayStub = searchStub + 8u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0x700u; program.memsz = 0x700u; program.flags = 7u; program.align = 4u; + + const auto jal = [](uint32_t target) { return 0x0C000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const uint32_t entry[] = { + 0x27BDFFF0u, 0xAFBF000Cu, + 0x3C040001u, 0x34840500u, 0x3C050001u, 0x34A50600u, jal(searchStub), 0x00000000u, // found file + 0x3C080001u, 0x35080540u, 0xAD020000u, // [0x10540] = v0 + 0x24040002u, 0x3C050001u, 0x34A50520u, jal(trayStub), 0x00000000u, // sceCdTrayReq(2, 0x10520) + 0x3C080001u, 0x35080540u, 0xAD020004u, // [0x10544] = v0 + 0x3C040001u, 0x34840500u, 0x3C050001u, 0x34A50680u, jal(searchStub), 0x00000000u, // missing file + 0x3C080001u, 0x35080540u, 0xAD020008u, // [0x10548] = v0 + 0x8FBF000Cu, 0x00001021u, 0x27BD0010u, 0x03E00008u, 0x00000000u, + }; + const uint32_t imports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x64766463u, 0x006E616Du, // "cdvdman" + 0x03E00008u, 0x2400000Au, // sceCdSearchFile + 0x03E00008u, 0x2400000Eu, // sceCdTrayReq + 0u, 0u, + }; + const uint32_t trayCountInit = 0xFFFFFFFFu; + const std::string_view found = "cdrom0:/DIR/DATA.BIN;1"; + const std::string_view missing = "cdrom0:/MISSING.BIN;1"; + + std::vector segment(program.filesz, 0u); + std::memcpy(segment.data(), entry, sizeof(entry)); + std::memcpy(segment.data() + 0x100u, imports, sizeof(imports)); + std::memcpy(segment.data() + 0x520u, &trayCountInit, sizeof(trayCountInit)); + std::memcpy(segment.data() + 0x600u, found.data(), found.size()); + std::memcpy(segment.data() + 0x680u, missing.data(), missing.size()); + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, segment.data(), segment.size()); + } + bool expect(bool value, const char *message) { if (!value) @@ -1142,6 +1263,32 @@ int main() if (!expect(!emittedUnhandledSeek, "sceCdSeek still emitted an unhandled IOP import")) return 1; + iop.reset(); + host.logs.clear(); + host.cdImage = makeCdImage(); + writeCdvdSearchIrx(host, 0x100u); + const ModuleLoadResult cdvdSearch = iop.loadModuleBuffer(0x100u); + host.cdImage.clear(); + uint32_t cdvdSearchResults[3] = {}; + uint32_t cdvdSearchFile[2] = {}; + uint32_t cdvdTrayCount = 0u; + if (!expect(cdvdSearch.handled && cdvdSearch.startResult == 0, "CDVD search module did not start")) return 1; + if (!expect(iop.readMemory(0x00010540u, cdvdSearchResults, sizeof(cdvdSearchResults)) && + iop.readMemory(0x00010500u, cdvdSearchFile, sizeof(cdvdSearchFile)) && + iop.readMemory(0x00010520u, &cdvdTrayCount, sizeof(cdvdTrayCount)), + "Could not read the CDVD search results")) return 1; + if (!expect(cdvdSearchResults[0] == 1u, "sceCdSearchFile did not find a file in the disc image")) return 1; + if (!expect(cdvdSearchFile[0] == 25u && cdvdSearchFile[1] == 0x1234u, + "sceCdSearchFile did not report the LSN and size from the disc image's ISO9660 directory")) return 1; + if (!expect(cdvdSearchResults[1] == 1u && cdvdTrayCount == 0u, + "sceCdTrayReq did not succeed with a tray count of 0")) return 1; + if (!expect(cdvdSearchResults[2] == 0u, "sceCdSearchFile found a file that is not in the disc image")) return 1; + const bool emittedUnhandledTrayReq = std::any_of( + host.logs.begin(), host.logs.end(), + [](const std::string &message) + { return message.find("unhandled import cdvdman:14") != std::string::npos; }); + if (!expect(!emittedUnhandledTrayReq, "sceCdTrayReq still emitted an unhandled IOP import")) return 1; + std::cout << "ps2xIOP emulator smoke tests passed\n"; return 0; }