From f73b27c69d8137f9f06e44b3e196aa5928d59600 Mon Sep 17 00:00:00 2001 From: "whatev.indus" <77seventy77@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:44:49 -0700 Subject: [PATCH] Support loading PSP prototype DVD-R disc dumps directly Some preserved PSP prototype builds were distributed as DVD-R images that contain the actual UMD data inside USER_L0.IMG and, for dual-layer titles, USER_L1.IMG. These images previously required manual extraction, renaming, or concatenation before PPSSPP could load them. Add support for recognizing these DVD-R wrapper layouts and exposing the embedded UMD image through the normal disc loading path. This includes both ISO-based wrappers and UDF-based wrappers, so preserved prototype dumps can be opened directly without conversion. This makes PPSSPP compatible with a wider range of preserved developer disc images without relying on title-specific handling. The primary importance of this patch is to encourage preserving 1:1, perfect disc image dumps in the manner of Redump.org. This patch has been successfully tested on the following DVD-R ISOs: https://hiddenpalace.org/Rock_Band_Unplugged_(Dec_10,_2008_prototype) https://hiddenpalace.org/WipEout_Pulse_(May_4,_2007_prototype) https://hiddenpalace.org/Lara_Croft_Tomb_Raider:_Anniversary_(May_19,_2007_prototype) https://hiddenpalace.org/Heatseeker_(Jan_15,_2007_prototype) Fixes #15547. --- Core/FileSystems/BlockDevices.cpp | 352 ++++++++++++++++++++++++++++++ Core/FileSystems/BlockDevices.h | 47 ++++ 2 files changed, 399 insertions(+) diff --git a/Core/FileSystems/BlockDevices.cpp b/Core/FileSystems/BlockDevices.cpp index 4512e8b1e3..de9970a845 100644 --- a/Core/FileSystems/BlockDevices.cpp +++ b/Core/FileSystems/BlockDevices.cpp @@ -16,7 +16,9 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include +#include #include +#include #include "Common/Data/Text/I18n.h" #include "Common/System/OSD.h" @@ -28,6 +30,7 @@ #include "Common/StringUtils.h" #include "Core/Loaders.h" #include "Core/FileSystems/BlockDevices.h" +#include "Core/FileSystems/ISOFileSystem.h" #include "Core/Util/PathUtil.h" #include "libchdr/chd.h" @@ -38,6 +41,184 @@ extern "C" #include "ext/libkirk/kirk_engine.h" }; +static u16 ReadLE16(const u8 *ptr) { + return ptr[0] | (ptr[1] << 8); +} + +static u32 ReadLE32(const u8 *ptr) { + return ptr[0] | (ptr[1] << 8) | (ptr[2] << 16) | (ptr[3] << 24); +} + +static std::string DecodeUDFFileName(const u8 *data, size_t size) { + if (size == 0) + return ""; + + std::string result; + if (data[0] == 8) { + result.assign((const char *)(data + 1), size - 1); + } else if (data[0] == 16) { + for (size_t i = 1; i + 1 < size; i += 2) { + result.push_back((char)data[i + 1]); + } + } + return result; +} + +struct UDFShortAd { + u32 length = 0; + u32 position = 0; +}; + +struct UDFLongAd { + u32 length = 0; + u32 position = 0; + u16 partition = 0; +}; + +static bool ReadDescriptorSector(FileLoader *fileLoader, u32 sector, std::array *out) { + return fileLoader->ReadAt((u64)sector * 2048, 1, out->size(), out->data()) == out->size(); +} + +static bool ParseUDFLongAd(const u8 *data, UDFLongAd *out) { + out->length = ReadLE32(data); + out->position = ReadLE32(data + 4); + out->partition = ReadLE16(data + 8); + return true; +} + +static bool ParseUDFShortAd(const u8 *data, UDFShortAd *out) { + out->length = ReadLE32(data) & 0x3FFFFFFF; + out->position = ReadLE32(data + 4); + return out->length != 0; +} + +static bool ParseUDFFileEntryExtent(FileLoader *fileLoader, u32 sector, UDFShortAd *extent) { + std::array block{}; + if (!ReadDescriptorSector(fileLoader, sector, &block)) + return false; + if (ReadLE16(block.data()) != 0x0105) + return false; + + u32 extAttrLen = ReadLE32(block.data() + 0xA8); + u32 allocDescLen = ReadLE32(block.data() + 0xAC); + u32 allocDescOffset = 0xB0 + extAttrLen; + if (allocDescLen < 8 || allocDescOffset + 8 > block.size()) + return false; + + return ParseUDFShortAd(block.data() + allocDescOffset, extent); +} + +static bool ParseUDFRootDirectory(FileLoader *fileLoader, u32 sector, u32 partitionStart, std::vector *dirData) { + UDFShortAd extent{}; + if (!ParseUDFFileEntryExtent(fileLoader, sector, &extent)) + return false; + if (extent.length == 0) + return false; + + dirData->resize(extent.length); + const u64 offset = (u64)(partitionStart + extent.position) * 2048; + return fileLoader->ReadAt(offset, 1, dirData->size(), dirData->data()) == dirData->size(); +} + +static bool FindUDFRootFileEntry(FileLoader *fileLoader, u32 *rootSector, u32 *partitionStart) { + std::array avdp{}; + if (!ReadDescriptorSector(fileLoader, 256, &avdp)) + return false; + if (ReadLE16(avdp.data()) != 0x0002) + return false; + + u32 mvdsLength = ReadLE32(avdp.data() + 16); + u32 mvdsLocation = ReadLE32(avdp.data() + 20); + if (mvdsLength < 2048) + return false; + + std::array block{}; + bool foundPartition = false; + bool foundRoot = false; + u32 fsdLocation = 0; + u32 fsdPartition = 0; + + for (u32 sector = mvdsLocation; sector < mvdsLocation + mvdsLength / 2048; ++sector) { + if (!ReadDescriptorSector(fileLoader, sector, &block)) + return false; + + switch (ReadLE16(block.data())) { + case 0x0005: + // Partition Descriptor. + fsdPartition = ReadLE32(block.data() + 188); + foundPartition = true; + break; + case 0x0006: + // Logical Volume Descriptor. The file set descriptor sequence is stored + // in logicalVolumeContentsUse as an extent_ad. + fsdLocation = ReadLE32(block.data() + 252); + foundRoot = true; + break; + default: + break; + } + } + + if (!foundPartition || !foundRoot) + return false; + + std::array fsd{}; + if (!ReadDescriptorSector(fileLoader, fsdPartition + fsdLocation, &fsd)) + return false; + if (ReadLE16(fsd.data()) != 0x0100) + return false; + + UDFLongAd rootIcb{}; + ParseUDFLongAd(fsd.data() + 400, &rootIcb); + if (rootIcb.partition != 0) + return false; + + *partitionStart = fsdPartition; + *rootSector = fsdPartition + rootIcb.position; + return true; +} + +static bool FindUDFLayerFileEntrySectors(FileLoader *fileLoader, u32 rootSector, u32 partitionStart, u32 *layer0Sector, u32 *layer1Sector) { + std::vector dirData; + if (!ParseUDFRootDirectory(fileLoader, rootSector, partitionStart, &dirData)) + return false; + + bool found0 = false; + bool found1 = false; + for (size_t offset = 0; offset + 16 <= dirData.size();) { + u16 tag = ReadLE16(&dirData[offset]); + u16 crcLen = ReadLE16(&dirData[offset + 10]); + size_t entryLen = 16 + crcLen; + entryLen = (entryLen + 3) & ~size_t(3); + if (entryLen == 0 || offset + entryLen > dirData.size()) + break; + + if (tag == 0x0101 && crcLen >= 20) { + u8 fileIdLen = dirData[offset + 19]; + u16 implUseLen = ReadLE16(&dirData[offset + 36]); + size_t nameOffset = offset + 38 + implUseLen; + if (nameOffset + fileIdLen <= dirData.size()) { + std::string name = DecodeUDFFileName(&dirData[nameOffset], fileIdLen); + UDFLongAd icb{}; + ParseUDFLongAd(&dirData[offset + 20], &icb); + if (icb.partition == 0) { + if (name == "USER_L0.IMG") { + *layer0Sector = partitionStart + icb.position; + found0 = true; + } else if (name == "USER_L1.IMG") { + *layer1Sector = partitionStart + icb.position; + found1 = true; + } + } + } + } + + offset += entryLen; + } + + return found0 || found1; +} + BlockDevice *ConstructBlockDevice(FileLoader *fileLoader, std::string *errorString) { if (!fileLoader->Exists()) { // Shouldn't get here really. @@ -73,6 +254,22 @@ BlockDevice *ConstructBlockDevice(FileLoader *fileLoader, std::string *errorStri device = new CHDFileBlockDevice(fileLoader); } + if (!device) { + device = new UDFFileBlockDevice(fileLoader); + if (!device->IsOK()) { + delete device; + device = nullptr; + } + } + + if (!device) { + device = new ISOContainerFileBlockDevice(fileLoader); + if (!device->IsOK()) { + delete device; + device = nullptr; + } + } + // No check above passed, should be just a regular ISO file. Let's open it as a plain block device and let the other systems take over. if (!device) { device = new FileBlockDevice(fileLoader); @@ -121,6 +318,161 @@ bool FileBlockDevice::ReadBlocks(u32 minBlock, int count, u8 *outPtr) { return true; } +UDFFileBlockDevice::UDFFileBlockDevice(FileLoader *fileLoader) + : BlockDevice(fileLoader) { + u32 partitionStart = 0; + u32 rootSector = 0; + if (!FindUDFRootFileEntry(fileLoader, &rootSector, &partitionStart)) { + errorString_ = "Not a supported UDF disc image"; + return; + } + + u32 layer0Sector = 0; + u32 layer1Sector = 0; + if (!FindUDFLayerFileEntrySectors(fileLoader, rootSector, partitionStart, &layer0Sector, &layer1Sector) || layer0Sector == 0) { + errorString_ = "Not a PSP UDF disc image"; + return; + } + + UDFShortAd layer0Extent{}; + if (!ParseUDFFileEntryExtent(fileLoader, layer0Sector, &layer0Extent)) { + errorString_ = "Failed to read USER_L0.IMG entry"; + return; + } + layer0_.startBlock = partitionStart + layer0Extent.position; + layer0_.numBlocks = layer0Extent.length / GetBlockSize(); + numBlocks_ = layer0_.numBlocks; + + if (layer1Sector != 0) { + UDFShortAd layer1Extent{}; + if (ParseUDFFileEntryExtent(fileLoader, layer1Sector, &layer1Extent)) { + layer1_.startBlock = partitionStart + layer1Extent.position; + layer1_.numBlocks = layer1Extent.length / GetBlockSize(); + numBlocks_ += layer1_.numBlocks; + } + } + + if (numBlocks_ == 0) { + errorString_ = "UDF disc image had no readable UMD layers"; + return; + } + + INFO_LOG(Log::Loader, "Detected PSP DVD-R wrapper: USER_L0=%u blocks at %u, USER_L1=%u blocks at %u", + layer0_.numBlocks, layer0_.startBlock, layer1_.numBlocks, layer1_.startBlock); +} + +UDFFileBlockDevice::~UDFFileBlockDevice() = default; + +bool UDFFileBlockDevice::ReadBlock(int blockNumber, u8 *outPtr, bool uncached) { + if ((u32)blockNumber >= numBlocks_) { + memset(outPtr, 0, GetBlockSize()); + return false; + } + + u32 sourceBlock = 0; + if ((u32)blockNumber < layer0_.numBlocks) { + sourceBlock = layer0_.startBlock + blockNumber; + } else { + u32 layer1Block = (u32)blockNumber - layer0_.numBlocks; + if (layer1Block >= layer1_.numBlocks) { + memset(outPtr, 0, GetBlockSize()); + return false; + } + sourceBlock = layer1_.startBlock + layer1Block; + } + + FileLoader::Flags flags = uncached ? FileLoader::Flags::HINT_UNCACHED : FileLoader::Flags::NONE; + size_t retval = fileLoader_->ReadAt((u64)sourceBlock * (u64)GetBlockSize(), 1, GetBlockSize(), outPtr, flags); + if (retval != GetBlockSize()) { + DEBUG_LOG(Log::FileSystem, "Could not read UDF-wrapped block %d from source block %u", blockNumber, sourceBlock); + return false; + } + return true; +} + +bool UDFFileBlockDevice::ReadBlocks(u32 minBlock, int count, u8 *outPtr) { + for (int i = 0; i < count; ++i) { + if (!ReadBlock(minBlock + i, outPtr)) { + return false; + } + outPtr += GetBlockSize(); + } + return true; +} + +ISOContainerFileBlockDevice::ISOContainerFileBlockDevice(FileLoader *fileLoader) + : BlockDevice(fileLoader) { + outerBlockDevice_ = std::make_shared(fileLoader); + if (!outerBlockDevice_->IsOK()) { + errorString_ = outerBlockDevice_->ErrorString(); + outerBlockDevice_.reset(); + return; + } + + SequentialHandleAllocator alloc; + ISOFileSystem iso(&alloc, outerBlockDevice_); + + PSPFileInfo layer0Info = iso.GetFileInfo("/USER_L0.IMG"); + if (!layer0Info.exists) { + errorString_ = "Not a PSP ISO container image"; + outerBlockDevice_.reset(); + return; + } + + layer0_.startBlock = layer0Info.startSector; + layer0_.numBlocks = (u32)((layer0Info.size + GetBlockSize() - 1) / GetBlockSize()); + numBlocks_ = layer0_.numBlocks; + + PSPFileInfo layer1Info = iso.GetFileInfo("/USER_L1.IMG"); + if (layer1Info.exists) { + layer1_.startBlock = layer1Info.startSector; + layer1_.numBlocks = (u32)((layer1Info.size + GetBlockSize() - 1) / GetBlockSize()); + numBlocks_ += layer1_.numBlocks; + } + + if (numBlocks_ == 0) { + errorString_ = "ISO container image had no readable UMD layers"; + outerBlockDevice_.reset(); + return; + } + + INFO_LOG(Log::Loader, "Detected PSP ISO wrapper: USER_L0=%u blocks at %u, USER_L1=%u blocks at %u", + layer0_.numBlocks, layer0_.startBlock, layer1_.numBlocks, layer1_.startBlock); +} + +ISOContainerFileBlockDevice::~ISOContainerFileBlockDevice() = default; + +bool ISOContainerFileBlockDevice::ReadBlock(int blockNumber, u8 *outPtr, bool uncached) { + if ((u32)blockNumber >= numBlocks_ || !outerBlockDevice_) { + memset(outPtr, 0, GetBlockSize()); + return false; + } + + u32 sourceBlock = 0; + if ((u32)blockNumber < layer0_.numBlocks) { + sourceBlock = layer0_.startBlock + blockNumber; + } else { + u32 layer1Block = (u32)blockNumber - layer0_.numBlocks; + if (layer1Block >= layer1_.numBlocks) { + memset(outPtr, 0, GetBlockSize()); + return false; + } + sourceBlock = layer1_.startBlock + layer1Block; + } + + return outerBlockDevice_->ReadBlock(sourceBlock, outPtr, uncached); +} + +bool ISOContainerFileBlockDevice::ReadBlocks(u32 minBlock, int count, u8 *outPtr) { + for (int i = 0; i < count; ++i) { + if (!ReadBlock(minBlock + i, outPtr)) { + return false; + } + outPtr += GetBlockSize(); + } + return true; +} + // .CSO format // compressed ISO(9660) header format diff --git a/Core/FileSystems/BlockDevices.h b/Core/FileSystems/BlockDevices.h index 5b765ca31c..6bf39698c1 100644 --- a/Core/FileSystems/BlockDevices.h +++ b/Core/FileSystems/BlockDevices.h @@ -102,6 +102,53 @@ private: u64 filesize_; }; +class UDFFileBlockDevice : public BlockDevice { +public: + UDFFileBlockDevice(FileLoader *fileLoader); + ~UDFFileBlockDevice(); + bool ReadBlock(int blockNumber, u8 *outPtr, bool uncached = false) override; + bool ReadBlocks(u32 minBlock, int count, u8 *outPtr) override; + u32 GetNumBlocks() const override { return numBlocks_; } + bool IsDisc() const override { return true; } + u64 GetUncompressedSize() const override { + return (u64)numBlocks_ * (u64)GetBlockSize(); + } + +private: + struct Extent { + u32 startBlock = 0; + u32 numBlocks = 0; + }; + + Extent layer0_{}; + Extent layer1_{}; + u32 numBlocks_ = 0; +}; + +class ISOContainerFileBlockDevice : public BlockDevice { +public: + ISOContainerFileBlockDevice(FileLoader *fileLoader); + ~ISOContainerFileBlockDevice(); + bool ReadBlock(int blockNumber, u8 *outPtr, bool uncached = false) override; + bool ReadBlocks(u32 minBlock, int count, u8 *outPtr) override; + u32 GetNumBlocks() const override { return numBlocks_; } + bool IsDisc() const override { return true; } + u64 GetUncompressedSize() const override { + return (u64)numBlocks_ * (u64)GetBlockSize(); + } + +private: + struct Extent { + u32 startBlock = 0; + u32 numBlocks = 0; + }; + + std::shared_ptr outerBlockDevice_; + Extent layer0_{}; + Extent layer1_{}; + u32 numBlocks_ = 0; +}; + // For encrypted ISOs in PBP files.