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; }