Integrate PR #639 for four-title regression validation

This commit is contained in:
Matthew Stanley
2026-10-12 23:55:21 -07:00
8 changed files with 837 additions and 583 deletions
+10
View File
@@ -1121,6 +1121,16 @@ if(BUILD_TESTING)
target_link_libraries(iso_reader_pregap_test PRIVATE chdr-static)
add_test(NAME iso_reader_pregap_test COMMAND iso_reader_pregap_test)
# Raw vs cooked geometry comes from the cue TRACK mode / sector-0 sync,
# never from file length.
add_executable(iso_reader_geometry_test
tests/test_iso_reader_geometry.cpp
src/iso_reader.cpp
src/cue_sheet.cpp)
target_include_directories(iso_reader_geometry_test PRIVATE include ${PSXRECOMP_ROOT}/recompiler/include)
target_link_libraries(iso_reader_geometry_test PRIVATE chdr-static)
add_test(NAME iso_reader_geometry_test COMMAND iso_reader_geometry_test)
add_executable(iso_reader_sbi_test
tests/test_iso_reader_sbi.cpp
src/iso_reader.cpp
+5
View File
@@ -45,6 +45,11 @@ struct CueTrackRef {
// shifting every later disc LBA (and the TOC) accordingly.
uint32_t pregap = 0;
uint32_t postgap = 0;
// Bytes per stored sector declared by the TRACK mode: 2352 for AUDIO and
// every "/2352" mode, 2048 or 2336 for the cooked modes. 0 = the mode named
// no size (unknown). The cue is the authority for a file's geometry; the
// file length is not (dumps can carry a partial trailing sector).
uint32_t sector_size = 0;
};
struct CueSheet {
+72 -60
View File
@@ -1,12 +1,12 @@
#pragma once
#include <cstdint>
#include <array>
#include <string>
#include <fstream>
#include <memory>
#include <unordered_map>
#include <vector>
#include <cstdint>
#include <array>
#include <string>
#include <fstream>
#include <memory>
#include <unordered_map>
#include <vector>
/**
* ISO/BIN/CUE Reader for PS1 CD-ROM images
@@ -20,10 +20,10 @@
* - Read file contents by path
*/
namespace PS1 {
struct CHDState;
namespace PS1 {
struct CHDState;
/**
* Information about a file entry in the ISO filesystem
*/
@@ -44,21 +44,21 @@ struct RootDirectoryInfo {
/**
* A CD track parsed from the .cue sheet (or the synthesized single data
* track for a bare .bin/.iso). Multi-track discs can carry a
* track for a bare .bin/.iso). Multi-track discs can carry a
* Red Book CD-DA audio track after the data track; the CD model's TOC
* commands (GetTN/GetTD) must report them.
*/
struct CDTrack {
struct CDTrack {
int number; // 1-based track number
bool is_audio; // true = CD-DA audio (Red Book); false = data
uint32_t start_lba; // disc-relative start LBA (cue INDEX 01; track 1 = 0).
uint32_t start_lba; // disc-relative start LBA (cue INDEX 01; track 1 = 0).
// Single-FILE cues: equals the .bin-relative INDEX time.
// Multi-FILE cues (redump "(Track N).bin" dumps): the
// owning file's first disc sector plus its INDEX time.
// Either way, plus every cue PREGAP/POSTGAP before it
// (gaps the dump does not store).
uint32_t pregap_lba; // disc-relative start of the track's pregap: its cue
// PREGAP if any, else INDEX 00, else INDEX 01
// owning file's first disc sector plus its INDEX time.
// Either way, plus every cue PREGAP/POSTGAP before it
// (gaps the dump does not store).
uint32_t pregap_lba; // disc-relative start of the track's pregap: its cue
// PREGAP if any, else INDEX 00, else INDEX 01
};
/**
@@ -70,23 +70,23 @@ struct CDTrack {
struct BinSegment {
std::string path; // resolved on-disk path
std::ifstream file; // opened for the reader's lifetime
uint32_t start_lba; // disc sector of this file's first stored sector
uint32_t start_lba; // disc sector of this file's first stored sector
uint32_t sector_count; // sectors stored in this file
bool raw; // 2352-byte raw sectors (BIN) vs 2048 (ISO)
};
/**
* A contiguous run of disc sectors. A stored span maps onto sectors of one
* BinSegment; a virtual span is a cue PREGAP/POSTGAP that the dump does not
* store and that reads back as zero (silence), as the CHD path already does.
* Without PREGAP/POSTGAP lines each file is exactly one stored span, so the
* disc layout is the plain concatenation of the files.
*/
struct BinSpan {
uint32_t disc_start; // first disc-relative sector
uint32_t sector_count;
int segment; // index into segments_, or -1 for a virtual gap
uint32_t file_sector; // first sector within the segment's file
};
/**
* A contiguous run of disc sectors. A stored span maps onto sectors of one
* BinSegment; a virtual span is a cue PREGAP/POSTGAP that the dump does not
* store and that reads back as zero (silence), as the CHD path already does.
* Without PREGAP/POSTGAP lines each file is exactly one stored span, so the
* disc layout is the plain concatenation of the files.
*/
struct BinSpan {
uint32_t disc_start; // first disc-relative sector
uint32_t sector_count;
int segment; // index into segments_, or -1 for a virtual gap
uint32_t file_sector; // first sector within the segment's file
};
class ISOReader {
@@ -103,11 +103,23 @@ public:
/**
* Open an ISO/BIN file for reading
* @param filename Path to .iso, .bin, .img, .car, .cue, or .chd file
* @param filename Path to .iso, .bin, .img, .car, .cue, or .chd file
* @return true if opened successfully, false otherwise
*/
bool Open(const std::string& filename);
/**
* Sector geometry of the first stored file behind an image, decided the
* same way Open() decides it: a .cue's TRACK mode is authoritative; a bare
* file is raw when sector 0 starts with the CD sync pattern. File length
* is never the evidence (raw dumps can be a multiple of 2048).
* @param path .cue or bare .bin/.iso/.img
* @param raw set to true for 2352-byte raw sectors, false for 2048
* @return false when the image cannot be opened or its geometry is not
* mountable (conflicting or MODE2/2336 track modes)
*/
static bool FirstFileIsRaw(const std::string& path, bool* raw);
/**
* Close the currently open file
*/
@@ -127,15 +139,15 @@ public:
* @param buffer Buffer to read into (must be at least 2352 bytes)
* @return true if the image stores raw sectors and read succeeded
*/
bool ReadRawSector(uint32_t lba, uint8_t* buffer);
/**
* Read generated or SBI-replacement subchannel Q for a disc LBA.
* SBI records intentionally carry invalid CRC data; valid is false for
* those sectors so the CD controller can retain its last valid Q packet.
*/
bool ReadSubChannelQ(uint32_t lba, uint8_t* buffer, bool* valid) const;
bool HasSubChannelReplacements() const { return !subq_replacements_.empty(); }
bool ReadRawSector(uint32_t lba, uint8_t* buffer);
/**
* Read generated or SBI-replacement subchannel Q for a disc LBA.
* SBI records intentionally carry invalid CRC data; valid is false for
* those sectors so the CD controller can retain its last valid Q packet.
*/
bool ReadSubChannelQ(uint32_t lba, uint8_t* buffer, bool* valid) const;
bool HasSubChannelReplacements() const { return !subq_replacements_.empty(); }
/**
* Check if a file is currently open
@@ -163,11 +175,11 @@ public:
/**
* CD-track TOC accessors (multi-track .cue support).
* TrackCount() is >= 1 (a bare image synthesizes one data track).
* TrackStartLBA(n)/TrackPregapLBA(n)/TrackIsAudio(n) take a 1-based track number.
* TrackStartLBA(n)/TrackPregapLBA(n)/TrackIsAudio(n) take a 1-based track number.
*/
int TrackCount() const;
uint32_t TrackStartLBA(int track) const;
uint32_t TrackPregapLBA(int track) const;
int TrackCount() const;
uint32_t TrackStartLBA(int track) const;
uint32_t TrackPregapLBA(int track) const;
bool TrackIsAudio(int track) const;
/**
@@ -237,23 +249,23 @@ private:
std::vector<ISOFileEntry> ListFilesByLBA(uint32_t lba, uint32_t dir_size);
/**
* Helper: find the span containing a disc-relative LBA.
* @return span pointer, or nullptr when lba is past the disc end
* Helper: find the span containing a disc-relative LBA.
* @return span pointer, or nullptr when lba is past the disc end
*/
const BinSpan* SpanForLBA(uint32_t lba) const;
bool LoadSBICompanion(const std::string& image_path);
const BinSpan* SpanForLBA(uint32_t lba) const;
bool LoadSBICompanion(const std::string& image_path);
bool is_open_;
std::string volume_id_;
std::string bin_path_; // first (data) segment; kept for callers
RootDirectoryInfo root_dir_;
std::vector<CDTrack> tracks_; // from the .cue TOC; >=1 entry after Open()
std::vector<BinSegment> segments_; // cue FILE entries in disc order; >=1 after Open()
std::vector<BinSpan> spans_; // disc layout over segments_ + cue gaps
uint32_t disc_sector_count_ = 0; // BIN/CUE: end of the last span
std::unique_ptr<CHDState> chd_; // present only for a directly mounted CHD
std::unordered_map<uint32_t, std::array<uint8_t, 12>> subq_replacements_;
};
std::vector<CDTrack> tracks_; // from the .cue TOC; >=1 entry after Open()
std::vector<BinSegment> segments_; // cue FILE entries in disc order; >=1 after Open()
std::vector<BinSpan> spans_; // disc layout over segments_ + cue gaps
uint32_t disc_sector_count_ = 0; // BIN/CUE: end of the last span
std::unique_ptr<CHDState> chd_; // present only for a directly mounted CHD
std::unordered_map<uint32_t, std::array<uint8_t, 12>> subq_replacements_;
};
} // namespace PS1
+15 -1
View File
@@ -5,6 +5,7 @@
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <sstream>
@@ -125,6 +126,7 @@ CueSheet parse_cue_sheet(const fs::path& cue_path) {
std::string line;
int cur_track_num = -1;
bool cur_track_audio = false;
uint32_t cur_sector_size = 0;
uint32_t cur_index00 = 0;
bool cur_has_index00 = false;
uint32_t cur_pregap = 0;
@@ -162,8 +164,19 @@ CueSheet parse_cue_sheet(const fs::path& cue_path) {
int tn = 0;
char type_buf[32] = {0};
if (std::sscanf(upper.c_str(), " TRACK %d %31s", &tn, type_buf) == 2) {
const std::string type(type_buf);
cur_track_num = tn;
cur_track_audio = (std::string(type_buf).find("AUDIO") != std::string::npos);
cur_track_audio = (type.find("AUDIO") != std::string::npos);
// "MODE2/2352" etc. name the stored sector size; AUDIO is always
// 2352. Anything else (CDG, malformed) leaves it unknown.
cur_sector_size = 0;
const size_t slash = type.find('/');
if (cur_track_audio) {
cur_sector_size = 2352;
} else if (slash != std::string::npos) {
const unsigned long n = std::strtoul(type.c_str() + slash + 1, nullptr, 10);
if (n == 2048 || n == 2336 || n == 2352) cur_sector_size = (uint32_t)n;
}
cur_index00 = 0;
cur_has_index00 = false;
cur_pregap = 0;
@@ -200,6 +213,7 @@ CueSheet parse_cue_sheet(const fs::path& cue_path) {
t.index00 = cur_index00;
t.has_index00 = cur_has_index00;
t.pregap = cur_pregap;
t.sector_size = cur_sector_size;
sheet.tracks.push_back(t);
last_pushed_track = cur_track_num;
cur_track_num = -1;
+588 -520
View File
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -714,8 +714,14 @@ bool sha256_disc_range(const std::filesystem::path& image,
std::array<uint8_t, 2048> bytes{};
size_t remaining = size;
uint64_t at = location;
const bool raw_source = file_size > 0 &&
((uint64_t)file_size % 2352u) == 0;
// Same geometry rule as the mount (cue TRACK mode, else sector-0 sync):
// a raw dump whose length happens to be a multiple of 2048 is still raw.
bool raw_source = false;
if (target == ModPatchTarget::DiscUser &&
!PS1::ISOReader::FirstFileIsRaw(image.string(), &raw_source)) {
if (error) *error = "cannot determine stock image sector geometry: " + image.string();
return false;
}
while (remaining != 0) {
uint64_t physical = at;
size_t chunk = remaining;
+136
View File
@@ -0,0 +1,136 @@
// Raw (2352) vs cooked (2048) sector geometry. The cue's TRACK mode decides;
// a bare file is raw when sector 0 starts with the CD sync pattern. File
// length is never the evidence: the Legend of Dragoon disc 1 dump is a raw
// MODE2/2352 image with 384 trailing bytes whose length is a multiple of 2048,
// and a length test mounted it as a cooked ISO (PVD read back as zeros, BIOS
// SystemError B/906).
#include "iso_reader.h"
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
namespace fs = std::filesystem;
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
static const uint8_t kSync[12] = {0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00};
// Raw MODE2 sectors: sync + header + 8-byte subheader, user data = marker i+1.
static void write_raw(const fs::path& path, uint32_t sectors, size_t trailing) {
std::ofstream out(path, std::ios::binary);
for (uint32_t i = 0; i < sectors; ++i) {
std::vector<uint8_t> s(2352, 0);
std::memcpy(s.data(), kSync, sizeof(kSync));
s[15] = 2;
std::memset(s.data() + 24, (int)(i + 1), 2048);
out.write(reinterpret_cast<const char*>(s.data()), (std::streamsize)s.size());
}
std::vector<uint8_t> tail(trailing, 0xEE);
out.write(reinterpret_cast<const char*>(tail.data()), (std::streamsize)tail.size());
}
// Cooked sectors; sector 0 is an all-zero system-area sector, others = i+1.
static void write_cooked(const fs::path& path, uint32_t sectors) {
std::ofstream out(path, std::ios::binary);
for (uint32_t i = 0; i < sectors; ++i) {
std::vector<uint8_t> s(2048, i == 0 ? 0 : (uint8_t)(i + 1));
out.write(reinterpret_cast<const char*>(s.data()), (std::streamsize)s.size());
}
}
static void write_text(const fs::path& path, const std::string& text) {
std::ofstream out(path);
out << text;
}
static void check_markers(const std::string& image, uint32_t sectors, bool first_is_zero) {
PS1::ISOReader r;
CHECK(r.Open(image));
CHECK(r.GetSectorCount() == sectors);
uint8_t buf[2048];
for (uint32_t i = 0; i < sectors; ++i) {
CHECK(r.ReadSector(i, buf));
const uint8_t want = (i == 0 && first_is_zero) ? 0 : (uint8_t)(i + 1);
if (buf[0] != want || buf[2047] != want) {
std::printf(" %s sector %u: got %02x want %02x\n", image.c_str(), i, buf[0], want);
++g_failures;
}
}
CHECK(!r.ReadSector(sectors, buf));
}
int main() {
const fs::path dir = fs::temp_directory_path() / "psxrecomp-iso-geometry-test";
fs::remove_all(dir);
fs::create_directories(dir);
// 20 raw sectors + 64 trailing bytes = 47104 = 23 * 2048: a length test
// calls this a cooked ISO.
const fs::path odd = dir / "odd.bin";
write_raw(odd, 20, 64);
CHECK(fs::file_size(odd) % 2048 == 0);
CHECK(fs::file_size(odd) % 2352 != 0);
// Cue declares MODE2/2352: raw.
write_text(dir / "odd.cue", "FILE \"odd.bin\" BINARY\n TRACK 01 MODE2/2352\n INDEX 01 00:00:00\n");
check_markers((dir / "odd.cue").string(), 20, false);
bool raw = false;
CHECK(PS1::ISOReader::FirstFileIsRaw((dir / "odd.cue").string(), &raw) && raw);
// Bare: sector-0 sync says raw.
check_markers(odd.string(), 20, false);
raw = false;
CHECK(PS1::ISOReader::FirstFileIsRaw(odd.string(), &raw) && raw);
// A cooked ISO whose length is a multiple of BOTH 2048 and 2352
// (147 * 2048 = 128 * 2352): no sync at sector 0, so cooked.
const fs::path both = dir / "both.iso";
write_cooked(both, 147);
CHECK(fs::file_size(both) % 2352 == 0);
check_markers(both.string(), 147, true);
raw = true;
CHECK(PS1::ISOReader::FirstFileIsRaw(both.string(), &raw) && !raw);
// Cue MODE1/2048 over a cooked file: cooked.
write_text(dir / "both.cue", "FILE \"both.iso\" BINARY\n TRACK 01 MODE1/2048\n INDEX 01 00:00:00\n");
check_markers((dir / "both.cue").string(), 147, true);
// Tracks of one FILE disagreeing on sector size: refuse.
write_text(dir / "conflict.cue",
"FILE \"odd.bin\" BINARY\n TRACK 01 MODE2/2352\n INDEX 01 00:00:00\n"
" TRACK 02 MODE1/2048\n INDEX 01 00:00:10\n");
{
PS1::ISOReader r;
CHECK(!r.Open((dir / "conflict.cue").string()));
CHECK(!PS1::ISOReader::FirstFileIsRaw((dir / "conflict.cue").string(), &raw));
}
// MODE2/2336 has no sync/header; not mountable here: refuse, never misread.
write_text(dir / "m2336.cue", "FILE \"odd.bin\" BINARY\n TRACK 01 MODE2/2336\n INDEX 01 00:00:00\n");
{
PS1::ISOReader r;
CHECK(!r.Open((dir / "m2336.cue").string()));
}
fs::remove_all(dir);
if (g_failures) {
std::printf("iso_reader_geometry_test: %d failure(s)\n", g_failures);
return 1;
}
std::printf("iso_reader_geometry_test: PASS\n");
return 0;
}
+3
View File
@@ -1165,6 +1165,9 @@ int main(int argc, char** argv) {
check(!psx_mod_read_disc_file("S0",nullptr,0,&bytes),"directory rejected");
std::vector<uint8_t> raw_iso(24*2352);
for(unsigned i=0;i<24;++i) {
/* A raw sector starts with the CD sync pattern; geometry is read from
* it, never from the file length. */
raw_iso[i*2352]=0; std::fill_n(raw_iso.begin()+i*2352+1,10,(uint8_t)0xFF); raw_iso[i*2352+11]=0;
raw_iso[i*2352+15]=2;
std::copy_n(iso.begin()+i*2048,2048,raw_iso.begin()+i*2352+24);
}