Merge pull request #389 from RetroPortingToolKit/fix/cue-pregap

Honour cue PREGAP/POSTGAP in the BIN/CUE mount (fixes #388)
This commit is contained in:
Matt Stanley
2026-09-24 15:42:52 -07:00
committed by GitHub
9 changed files with 380 additions and 35 deletions
+9
View File
@@ -393,6 +393,15 @@ if(BUILD_TESTING)
target_link_libraries(iso_reader_cdda_test PRIVATE chdr-static)
add_test(NAME iso_reader_cdda_test COMMAND iso_reader_cdda_test)
# Cue PREGAP/POSTGAP (gaps the dump does not store) shift the disc layout.
add_executable(iso_reader_pregap_test
tests/test_iso_reader_pregap.cpp
src/iso_reader.cpp
src/cue_sheet.cpp)
target_include_directories(iso_reader_pregap_test PRIVATE include ${PSXRECOMP_ROOT}/recompiler/include)
target_link_libraries(iso_reader_pregap_test PRIVATE chdr-static)
add_test(NAME iso_reader_pregap_test COMMAND iso_reader_pregap_test)
add_executable(iso_reader_sbi_test
tests/test_iso_reader_sbi.cpp
src/iso_reader.cpp
+4
View File
@@ -309,6 +309,10 @@ typedef struct CDROMCommandHistoryEntry {
uint8_t pending_pending;
uint8_t queued_cmd;
uint8_t queued_pending;
/* Immediate response FIFO as the guest will read it (e.g. the GetTD MSF,
* the GetTN track range), so a TOC can be checked from the ring. */
uint8_t response_count;
uint8_t response[16];
} CDROMCommandHistoryEntry;
typedef struct CDROMSectorHistoryEntry {
+5
View File
@@ -40,6 +40,11 @@ struct CueTrackRef {
uint32_t index01 = 0; // track start, file-relative LBA
uint32_t index00 = 0; // pregap start, file-relative LBA
bool has_index00 = false;
// PREGAP / POSTGAP lengths in sectors. Unlike INDEX 00, these gaps are
// NOT stored in the file: the mount must insert them as silent sectors,
// shifting every later disc LBA (and the TOC) accordingly.
uint32_t pregap = 0;
uint32_t postgap = 0;
};
struct CueSheet {
+24 -5
View File
@@ -55,7 +55,10 @@ struct CDTrack {
// 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.
uint32_t pregap_lba; // disc-relative INDEX 00, or INDEX 01 when absent
// 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
};
/**
@@ -67,9 +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; // first disc-relative sector of this file
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
};
class ISOReader {
@@ -220,10 +237,10 @@ private:
std::vector<ISOFileEntry> ListFilesByLBA(uint32_t lba, uint32_t dir_size);
/**
* Helper: find the BinSegment containing a disc-relative LBA.
* @return segment 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
*/
BinSegment* SegmentForLBA(uint32_t lba);
const BinSpan* SpanForLBA(uint32_t lba) const;
bool LoadSBICompanion(const std::string& image_path);
@@ -233,6 +250,8 @@ private:
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_;
};
+7
View File
@@ -835,6 +835,13 @@ static void record_command_history(uint8_t kind, uint8_t cmd,
e->pending_pending = (uint8_t)(pending.pending ? 1 : 0);
e->queued_cmd = queued_cmd.cmd;
e->queued_pending = (uint8_t)(queued_cmd.pending ? 1 : 0);
{
int rn = response_count;
if (rn < 0) rn = 0;
if (rn > (int)sizeof(e->response)) rn = (int)sizeof(e->response);
e->response_count = (uint8_t)rn;
memcpy(e->response, response_fifo, (size_t)rn);
}
}
static int xa_is_audio_realtime(const CDROMSectorDelivery *d) {
+21
View File
@@ -127,6 +127,10 @@ CueSheet parse_cue_sheet(const fs::path& cue_path) {
bool cur_track_audio = false;
uint32_t cur_index00 = 0;
bool cur_has_index00 = false;
uint32_t cur_pregap = 0;
// A TRACK whose INDEX 01 has been recorded, so a trailing POSTGAP line
// (which follows INDEX 01) still attaches to it.
int last_pushed_track = -1;
while (std::getline(cue, line)) {
// Strip a trailing CR so cues with CRLF endings parse on POSIX.
@@ -150,6 +154,8 @@ CueSheet parse_cue_sheet(const fs::path& cue_path) {
cur_track_num = -1;
cur_has_index00 = false;
cur_index00 = 0;
cur_pregap = 0;
last_pushed_track = -1;
continue;
}
@@ -160,10 +166,23 @@ CueSheet parse_cue_sheet(const fs::path& cue_path) {
cur_track_audio = (std::string(type_buf).find("AUDIO") != std::string::npos);
cur_index00 = 0;
cur_has_index00 = false;
cur_pregap = 0;
last_pushed_track = -1;
continue;
}
int idx = 0, mm = 0, ss = 0, ff = 0;
if (std::sscanf(upper.c_str(), " PREGAP %d:%d:%d", &mm, &ss, &ff) == 3) {
if (cur_track_num >= 1)
cur_pregap = (uint32_t)(((mm * 60 + ss) * 75) + ff);
continue;
}
if (std::sscanf(upper.c_str(), " POSTGAP %d:%d:%d", &mm, &ss, &ff) == 3) {
if (last_pushed_track >= 0 && !sheet.tracks.empty() &&
sheet.tracks.back().number == last_pushed_track)
sheet.tracks.back().postgap = (uint32_t)(((mm * 60 + ss) * 75) + ff);
continue;
}
if (std::sscanf(upper.c_str(), " INDEX %d %d:%d:%d", &idx, &mm, &ss, &ff) == 4 &&
cur_track_num >= 1 && !sheet.files.empty()) {
const uint32_t index_lba = (uint32_t)(((mm * 60 + ss) * 75) + ff);
@@ -180,7 +199,9 @@ CueSheet parse_cue_sheet(const fs::path& cue_path) {
t.index01 = index_lba;
t.index00 = cur_index00;
t.has_index00 = cur_has_index00;
t.pregap = cur_pregap;
sheet.tracks.push_back(t);
last_pushed_track = cur_track_num;
cur_track_num = -1;
}
}
+8 -3
View File
@@ -5768,7 +5768,7 @@ static void handle_cdrom_command_history(int id, const char *json)
uint64_t oldest = (total > CDROM_COMMAND_HISTORY_CAP)
? total - CDROM_COMMAND_HISTORY_CAP : 0;
size_t bufsz = 256u + (size_t)count * 640u;
size_t bufsz = 256u + (size_t)count * 800u;
char *buf = (char *)malloc(bufsz);
if (!buf) { send_err(id, "oom"); return; }
@@ -5781,7 +5781,7 @@ static void handle_cdrom_command_history(int id, const char *json)
(unsigned long long)oldest);
uint64_t seq = total;
while (seq > oldest && emitted < count && pos < bufsz - 640) {
while (seq > oldest && emitted < count && pos < bufsz - 800) {
seq--;
const CDROMCommandHistoryEntry *e =
&entries[seq % CDROM_COMMAND_HISTORY_CAP];
@@ -5809,13 +5809,18 @@ static void handle_cdrom_command_history(int id, const char *json)
"\"reading\":%u,\"pending_cmd\":\"0x%02X\","
"\"pending\":%u,\"queued_cmd\":\"0x%02X\","
"\"queued\":%u,\"func\":\"0x%08X\",\"pc\":\"0x%08X\","
"\"i_stat\":\"0x%08X\"}",
"\"i_stat\":\"0x%08X\",\"response\":[",
e->stat, e->request, e->irq_enable, e->irq_flag,
e->mode, e->seek_min, e->seek_sec, e->seek_sect,
e->read_min, e->read_sec, e->read_sect, e->read_cmd,
e->reading, e->pending_cmd, e->pending_pending,
e->queued_cmd, e->queued_pending, e->func, e->pc,
e->i_stat);
for (uint8_t i = 0; i < e->response_count && i < 16 && pos < bufsz - 16; i++) {
pos += snprintf(buf + pos, bufsz - pos,
"%s\"0x%02X\"", i ? "," : "", e->response[i]);
}
pos += snprintf(buf + pos, bufsz - pos, "]}");
emitted++;
}
+92 -27
View File
@@ -310,7 +310,6 @@ bool ISOReader::Open(const std::string& filename) {
// Build the segment table: open every file and lay it out at the next
// disc-relative sector. Every file must open — a multi-file dump with a
// missing track file would otherwise silently read the wrong sectors.
uint32_t next_lba = 0;
for (const std::string& bin_name : bin_files) {
BinSegment seg;
seg.path = bin_name;
@@ -330,21 +329,71 @@ bool ISOReader::Open(const std::string& filename) {
seg.raw = (size % RAW_SECTOR_SIZE) == 0;
seg.sector_count = static_cast<uint32_t>(size / (seg.raw ? RAW_SECTOR_SIZE
: SECTOR_SIZE));
seg.start_lba = next_lba;
next_lba += seg.sector_count;
seg.start_lba = 0; // assigned by the layout pass below
segments_.push_back(std::move(seg));
}
// Convert the pending cue tracks to disc-relative LBAs.
for (const PendingTrack& p : pending_tracks) {
CDTrack t;
t.number = p.number;
t.is_audio = p.is_audio;
t.start_lba = segments_[p.file_index].start_lba + p.index01;
t.pregap_lba = segments_[p.file_index].start_lba +
(p.has_index00 ? p.index00 : p.index01);
tracks_.push_back(t);
// Lay the files out on the disc and convert the pending cue tracks to
// disc-relative LBAs. INDEX times are file-relative; a cue PREGAP (before
// a track's INDEX 00/01) or POSTGAP (after its data) is a run of sectors
// the dump does not store, so it becomes a virtual span and moves every
// later disc LBA. Real discs carry those sectors, and games locate tracks
// through the TOC (GetTD), so dropping them shifts every later track
// earlier than the game expects. Without gap lines each file is one stored
// span and the layout is the plain concatenation of the files.
uint32_t disc = 0;
auto push_stored = [&](size_t seg_index, uint32_t from, uint32_t to) {
if (to <= from) return;
spans_.push_back({disc, to - from, (int)seg_index, from});
disc += to - from;
};
auto push_virtual = [&](uint32_t count) {
if (count == 0) return;
spans_.push_back({disc, count, -1, 0});
disc += count;
};
for (size_t si = 0; si < segments_.size(); ++si) {
BinSegment& seg = segments_[si];
const uint32_t n = seg.sector_count;
seg.start_lba = disc;
uint32_t cursor = 0;
for (size_t pi = 0; pi < pending_tracks.size(); ++pi) {
const PendingTrack& p = pending_tracks[pi];
if (p.file_index != si) continue;
// The track's region in the file starts at INDEX 00 when it has
// one; its PREGAP (if any) sits just before that point.
const uint32_t region =
std::min(std::max(p.has_index00 ? p.index00 : p.index01, cursor), n);
push_stored(si, cursor, region);
cursor = region;
CDTrack t;
t.number = p.number;
t.is_audio = p.is_audio;
t.pregap_lba = disc;
push_virtual(p.pregap);
t.start_lba = disc + (p.index01 > region ? p.index01 - region : 0);
tracks_.push_back(t);
if (p.postgap) {
// POSTGAP follows the track's stored data: the next track of
// this file starts its region there, else the file ends.
uint32_t end = n;
for (size_t qi = pi + 1; qi < pending_tracks.size(); ++qi) {
const PendingTrack& q = pending_tracks[qi];
if (q.file_index != si) continue;
end = std::min(std::max(q.has_index00 ? q.index00 : q.index01,
cursor), n);
break;
}
push_stored(si, cursor, end);
cursor = end;
push_virtual(p.postgap);
}
}
push_stored(si, cursor, n);
}
disc_sector_count_ = disc;
// Synthesize a single data track for a bare .bin/.iso or a .cue with no
// parseable TRACK entries, so TrackCount() is always >= 1.
@@ -384,6 +433,8 @@ void ISOReader::Close() {
}
}
segments_.clear();
spans_.clear();
disc_sector_count_ = 0;
tracks_.clear();
subq_replacements_.clear();
bin_path_.clear();
@@ -466,11 +517,12 @@ bool ISOReader::ReadSubChannelQ(uint32_t lba, uint8_t* buffer, bool* valid) cons
return true;
}
BinSegment* ISOReader::SegmentForLBA(uint32_t lba) {
// Linear scan: images carry a handful of segments (one per track file).
for (BinSegment& seg : segments_) {
if (lba >= seg.start_lba && lba - seg.start_lba < seg.sector_count) {
return &seg;
const BinSpan* ISOReader::SpanForLBA(uint32_t lba) const {
// Linear scan: images carry a handful of spans (one per track file, plus
// one per cue gap).
for (const BinSpan& span : spans_) {
if (lba >= span.disc_start && lba - span.disc_start < span.sector_count) {
return &span;
}
}
return nullptr;
@@ -492,11 +544,17 @@ bool ISOReader::ReadSector(uint32_t lba, uint8_t* buffer) {
return true;
}
BinSegment* seg = SegmentForLBA(lba);
if (!seg) {
const BinSpan* span = SpanForLBA(lba);
if (!span) {
return false;
}
const uint32_t local_lba = lba - seg->start_lba;
if (span->segment < 0) {
// Cue PREGAP/POSTGAP: not stored in the dump; reads back as zero.
std::memset(buffer, 0, SECTOR_SIZE);
return true;
}
BinSegment* seg = &segments_[(size_t)span->segment];
const uint32_t local_lba = span->file_sector + (lba - span->disc_start);
// Clear any error flags from a previous failed read
seg->file.clear();
@@ -542,11 +600,21 @@ bool ISOReader::ReadRawSector(uint32_t lba, uint8_t* buffer) {
if (chd_) return read_chd_raw(*chd_, lba, buffer, nullptr);
BinSegment* seg = SegmentForLBA(lba);
if (!seg || !seg->raw) {
const BinSpan* span = SpanForLBA(lba);
if (!span) {
return false;
}
const uint32_t local_lba = lba - seg->start_lba;
if (span->segment < 0) {
// Cue PREGAP/POSTGAP: not stored in the dump; digital silence, the
// same as the CHD path's virtual pregap.
std::memset(buffer, 0, RAW_SECTOR_SIZE);
return true;
}
BinSegment* seg = &segments_[(size_t)span->segment];
if (!seg->raw) {
return false;
}
const uint32_t local_lba = span->file_sector + (lba - span->disc_start);
seg->file.clear();
std::streampos offset = static_cast<std::streampos>(local_lba) * RAW_SECTOR_SIZE;
@@ -580,10 +648,7 @@ uint32_t ISOReader::GetSectorCount() {
return 0;
}
if (chd_) return chd_->disc_sector_count;
if (segments_.empty()) return 0;
const BinSegment& last = segments_.back();
return last.start_lba + last.sector_count;
return disc_sector_count_;
}
int ISOReader::TrackCount() const {
+210
View File
@@ -0,0 +1,210 @@
// Cue PREGAP / POSTGAP are gaps the dump does NOT store. The mount must insert
// them as silent sectors so the disc layout (and the TOC games read through
// GetTD) matches the physical disc. The oracle here is layout equivalence: a
// cue that stores its pregap in the file (INDEX 00) and one that declares it
// with PREGAP describe the same disc, so every track LBA, the sector count and
// every sector's contents must agree.
#include "iso_reader.h"
#include <cstdint>
#include <cstdio>
#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)
// Sector i of a file is filled with byte `markers[i]` (0 = a silent sector).
static void write_bin(const fs::path& path, const std::vector<uint8_t>& markers) {
std::ofstream out(path, std::ios::binary);
for (uint8_t m : markers) {
std::vector<uint8_t> sector(2352, m);
out.write(reinterpret_cast<const char*>(sector.data()), sector.size());
}
}
static void write_text(const fs::path& path, const char* text) {
std::ofstream out(path);
out << text;
}
static std::vector<uint8_t> run(uint8_t first, int count) {
std::vector<uint8_t> v;
for (int i = 0; i < count; ++i) v.push_back((uint8_t)(first + i));
return v;
}
static std::vector<uint8_t> cat(std::vector<uint8_t> a, const std::vector<uint8_t>& b) {
a.insert(a.end(), b.begin(), b.end());
return a;
}
// Every observable of the two mounts must agree.
static void check_same_disc(const fs::path& a_cue, const fs::path& b_cue) {
PS1::ISOReader a, b;
CHECK(a.Open(a_cue.string()));
CHECK(b.Open(b_cue.string()));
CHECK(a.TrackCount() == b.TrackCount());
CHECK(a.GetSectorCount() == b.GetSectorCount());
for (int t = 1; t <= a.TrackCount(); ++t) {
CHECK(a.TrackStartLBA(t) == b.TrackStartLBA(t));
CHECK(a.TrackPregapLBA(t) == b.TrackPregapLBA(t));
CHECK(a.TrackIsAudio(t) == b.TrackIsAudio(t));
}
uint8_t ra[2352], rb[2352];
for (uint32_t lba = 0; lba < a.GetSectorCount(); ++lba) {
const bool oa = a.ReadRawSector(lba, ra);
const bool ob = b.ReadRawSector(lba, rb);
CHECK(oa && ob);
if (oa && ob && ra[0] != rb[0]) {
std::printf(" sector %u differs: %02x vs %02x\n", lba, ra[0], rb[0]);
++g_failures;
}
}
CHECK(!a.ReadRawSector(a.GetSectorCount(), ra));
}
int main() {
const fs::path dir = fs::temp_directory_path() / "psxrecomp-cue-pregap-test";
fs::remove_all(dir);
fs::create_directories(dir);
// Single-file dump shaped like Xi (issue #388): data track, then the first
// audio track with a 3-sector PREGAP that is not in the file, then an audio
// track whose pregap IS in the file (INDEX 00).
const std::vector<uint8_t> data = run(0x10, 10); // file sectors 0..9
const std::vector<uint8_t> aud2 = run(0x40, 3); // file sectors 10..12
const std::vector<uint8_t> aud3 = run(0x60, 3); // file sectors 13..15
write_bin(dir / "gap.bin", cat(cat(data, aud2), aud3));
write_text(dir / "gap.cue",
"FILE \"gap.bin\" BINARY\n"
" TRACK 01 MODE2/2352\n"
" INDEX 01 00:00:00\n"
" TRACK 02 AUDIO\n"
" PREGAP 00:00:03\n"
" INDEX 01 00:00:10\n"
" TRACK 03 AUDIO\n"
" INDEX 00 00:00:13\n"
" INDEX 01 00:00:14\n");
{
PS1::ISOReader r;
CHECK(r.Open((dir / "gap.cue").string()));
CHECK(r.TrackCount() == 3);
CHECK(r.TrackStartLBA(1) == 0);
CHECK(r.TrackPregapLBA(2) == 10); // the virtual gap starts here
CHECK(r.TrackStartLBA(2) == 13); // INDEX 01 moved by the gap
CHECK(r.TrackPregapLBA(3) == 16);
CHECK(r.TrackStartLBA(3) == 17);
CHECK(r.GetSectorCount() == 19);
uint8_t raw[2352];
CHECK(r.ReadRawSector(9, raw) && raw[0] == 0x19);
CHECK(r.ReadRawSector(10, raw) && raw[0] == 0x00 && raw[2351] == 0x00);
CHECK(r.ReadRawSector(12, raw) && raw[0] == 0x00);
CHECK(r.ReadRawSector(13, raw) && raw[0] == 0x40);
CHECK(r.ReadRawSector(18, raw) && raw[0] == 0x62);
uint8_t user[2048];
CHECK(r.ReadSector(11, user) && user[0] == 0x00);
}
// The same physical disc, with the pregap stored in the file as INDEX 00.
write_bin(dir / "stored.bin",
cat(cat(cat(data, std::vector<uint8_t>(3, 0)), aud2), aud3));
write_text(dir / "stored.cue",
"FILE \"stored.bin\" BINARY\n"
" TRACK 01 MODE2/2352\n"
" INDEX 01 00:00:00\n"
" TRACK 02 AUDIO\n"
" INDEX 00 00:00:10\n"
" INDEX 01 00:00:13\n"
" TRACK 03 AUDIO\n"
" INDEX 00 00:00:16\n"
" INDEX 01 00:00:17\n");
check_same_disc(dir / "gap.cue", dir / "stored.cue");
// Multi-file (redump layout) with every audio pregap stripped from its
// file and declared as PREGAP, versus the redump original, plus a POSTGAP
// on the data track.
write_bin(dir / "t1.bin", run(0x10, 4));
write_bin(dir / "t2_stored.bin", cat(std::vector<uint8_t>(2, 0), run(0x40, 3)));
write_bin(dir / "t3_stored.bin", cat(std::vector<uint8_t>(2, 0), run(0x60, 2)));
write_bin(dir / "t2_gap.bin", run(0x40, 3));
write_bin(dir / "t3_gap.bin", run(0x60, 2));
write_text(dir / "multi_stored.cue",
"FILE \"t1.bin\" BINARY\n"
" TRACK 01 MODE2/2352\n"
" INDEX 01 00:00:00\n"
"FILE \"t2_stored.bin\" BINARY\n"
" TRACK 02 AUDIO\n"
" INDEX 00 00:00:00\n"
" INDEX 01 00:00:02\n"
"FILE \"t3_stored.bin\" BINARY\n"
" TRACK 03 AUDIO\n"
" INDEX 00 00:00:00\n"
" INDEX 01 00:00:02\n");
write_text(dir / "multi_gap.cue",
"FILE \"t1.bin\" BINARY\n"
" TRACK 01 MODE2/2352\n"
" INDEX 01 00:00:00\n"
"FILE \"t2_gap.bin\" BINARY\n"
" TRACK 02 AUDIO\n"
" PREGAP 00:00:02\n"
" INDEX 01 00:00:00\n"
"FILE \"t3_gap.bin\" BINARY\n"
" TRACK 03 AUDIO\n"
" PREGAP 00:00:02\n"
" INDEX 01 00:00:00\n");
check_same_disc(dir / "multi_gap.cue", dir / "multi_stored.cue");
// POSTGAP follows the track's stored data and precedes the next track.
write_text(dir / "postgap.cue",
"FILE \"t1.bin\" BINARY\n"
" TRACK 01 MODE2/2352\n"
" INDEX 01 00:00:00\n"
" POSTGAP 00:00:02\n"
"FILE \"t2_gap.bin\" BINARY\n"
" TRACK 02 AUDIO\n"
" INDEX 01 00:00:00\n");
{
PS1::ISOReader r;
CHECK(r.Open((dir / "postgap.cue").string()));
CHECK(r.TrackPregapLBA(2) == 6);
CHECK(r.TrackStartLBA(2) == 6);
CHECK(r.GetSectorCount() == 9);
uint8_t raw[2352];
CHECK(r.ReadRawSector(3, raw) && raw[0] == 0x13);
CHECK(r.ReadRawSector(4, raw) && raw[0] == 0x00);
CHECK(r.ReadRawSector(5, raw) && raw[0] == 0x00);
CHECK(r.ReadRawSector(6, raw) && raw[0] == 0x40);
}
// No gap lines: the layout is the plain concatenation, as before.
{
PS1::ISOReader r;
CHECK(r.Open((dir / "multi_stored.cue").string()));
CHECK(r.TrackPregapLBA(2) == 4);
CHECK(r.TrackStartLBA(2) == 6);
CHECK(r.TrackPregapLBA(3) == 9);
CHECK(r.TrackStartLBA(3) == 11);
CHECK(r.GetSectorCount() == 13);
}
fs::remove_all(dir);
if (g_failures) {
std::printf("iso_reader_pregap_test: %d failure(s)\n", g_failures);
return 1;
}
std::printf("iso_reader_pregap_test: OK\n");
return 0;
}