Skip to content
Open
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
137 changes: 128 additions & 9 deletions ps2xIOP/src/emulator/imports/iop_cdvd.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<uint8_t, kSectorSize> descriptor{};
if (!readImageBytes(static_cast<uint64_t>(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<uint32_t>(source[0]) | (static_cast<uint32_t>(source[1]) << 8u) |
(static_cast<uint32_t>(source[2]) << 16u) | (static_cast<uint32_t>(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<uint8_t> directory(current.size);
if (!readImageBytes(static_cast<uint64_t>(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<size_t>(directory[offset + 32u], recordLength - 33u);
const std::string identifier(reinterpret_cast<const char *>(&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<uint8_t, 32u> 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<size_t>(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<uint8_t, 32u> 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<size_t>(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)
Expand Down
151 changes: 149 additions & 2 deletions ps2xIOP/tests/iop_emulator_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<size_t>(std::min<uint64_t>(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<uint8_t> guest;
std::vector<std::string> logs;
std::string cdRoot;
std::vector<uint8_t> cdImage;
};

#pragma pack(push, 1)
Expand Down Expand Up @@ -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<uint8_t> makeCdImage()
{
constexpr uint32_t sectorSize = 2048u;
std::vector<uint8_t> 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<uint8_t>(value >> (i * 8u));
image[offset + 4u + i] = static_cast<uint8_t>(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<size_t>(1u);
image[record] = static_cast<uint8_t>(length);
putBoth32(record + 2u, lsn);
putBoth32(record + 10u, size);
image[record + 25u] = directory ? 2u : 0u;
image[record + 32u] = static_cast<uint8_t>(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<uint8_t> 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)
Expand Down Expand Up @@ -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;
}