From e09980e6cdcc24b642f3c2fa87953dba12518eb1 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 24 Sep 2026 14:38:03 -0700 Subject: [PATCH 1/2] fix: honour cue PREGAP/POSTGAP in the BIN/CUE mount Gaps the dump does not store were dropped, so every later track (and GetTD) sat 150 sectors early; games that seek via GetTD landed in the previous track. Co-Authored-By: Claude Opus 5.5 (1M context) --- runtime/CMakeLists.txt | 9 + runtime/include/cue_sheet.h | 5 + runtime/include/iso_reader.h | 29 +++- runtime/src/cue_sheet.cpp | 21 +++ runtime/src/iso_reader.cpp | 119 ++++++++++--- runtime/tests/test_iso_reader_pregap.cpp | 210 +++++++++++++++++++++++ 6 files changed, 361 insertions(+), 32 deletions(-) create mode 100644 runtime/tests/test_iso_reader_pregap.cpp diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index d39d56e1..f199c03c 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -388,6 +388,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 diff --git a/runtime/include/cue_sheet.h b/runtime/include/cue_sheet.h index 33f060bc..7900c35d 100644 --- a/runtime/include/cue_sheet.h +++ b/runtime/include/cue_sheet.h @@ -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 { diff --git a/runtime/include/iso_reader.h b/runtime/include/iso_reader.h index 7111c0fb..ff75e9c3 100644 --- a/runtime/include/iso_reader.h +++ b/runtime/include/iso_reader.h @@ -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 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 tracks_; // from the .cue TOC; >=1 entry after Open() std::vector segments_; // cue FILE entries in disc order; >=1 after Open() + std::vector spans_; // disc layout over segments_ + cue gaps + uint32_t disc_sector_count_ = 0; // BIN/CUE: end of the last span std::unique_ptr chd_; // present only for a directly mounted CHD std::unordered_map> subq_replacements_; }; diff --git a/runtime/src/cue_sheet.cpp b/runtime/src/cue_sheet.cpp index bcfd6b54..340608a4 100644 --- a/runtime/src/cue_sheet.cpp +++ b/runtime/src/cue_sheet.cpp @@ -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; } } diff --git a/runtime/src/iso_reader.cpp b/runtime/src/iso_reader.cpp index c4774bb1..af866320 100644 --- a/runtime/src/iso_reader.cpp +++ b/runtime/src/iso_reader.cpp @@ -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(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(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 { diff --git a/runtime/tests/test_iso_reader_pregap.cpp b/runtime/tests/test_iso_reader_pregap.cpp new file mode 100644 index 00000000..03cf426b --- /dev/null +++ b/runtime/tests/test_iso_reader_pregap.cpp @@ -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 +#include +#include +#include +#include +#include + +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& markers) { + std::ofstream out(path, std::ios::binary); + for (uint8_t m : markers) { + std::vector sector(2352, m); + out.write(reinterpret_cast(sector.data()), sector.size()); + } +} + +static void write_text(const fs::path& path, const char* text) { + std::ofstream out(path); + out << text; +} + +static std::vector run(uint8_t first, int count) { + std::vector v; + for (int i = 0; i < count; ++i) v.push_back((uint8_t)(first + i)); + return v; +} + +static std::vector cat(std::vector a, const std::vector& 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 data = run(0x10, 10); // file sectors 0..9 + const std::vector aud2 = run(0x40, 3); // file sectors 10..12 + const std::vector 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(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(2, 0), run(0x40, 3))); + write_bin(dir / "t3_stored.bin", cat(std::vector(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; +} From 0e75a37f7cfd838a029874bfb330058bfe18a6c0 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 24 Sep 2026 14:38:03 -0700 Subject: [PATCH 2/2] debug: record immediate CD responses in the command history ring Co-Authored-By: Claude Opus 5.5 (1M context) --- runtime/include/cdrom.h | 4 ++++ runtime/src/cdrom.c | 7 +++++++ runtime/src/debug_server.c | 11 ++++++++--- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/runtime/include/cdrom.h b/runtime/include/cdrom.h index df5cb346..3f23357d 100644 --- a/runtime/include/cdrom.h +++ b/runtime/include/cdrom.h @@ -288,6 +288,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 { diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index 73884657..d146d977 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -824,6 +824,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) { diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index b4dee829..528a5847 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -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++; }