Skip to content

cue: synthesize the multisession gap across a FILE boundary - #244

Merged
danifunker merged 1 commit into
danifunker:mainfrom
iTechMedic:multibin-session-gap
Aug 3, 2026
Merged

cue: synthesize the multisession gap across a FILE boundary#244
danifunker merged 1 commit into
danifunker:mainfrom
iTechMedic:multibin-session-gap

Conversation

@iTechMedic

@iTechMedic iTechMedic commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #242. Ordinary multi-BIN cues worked, but an Enhanced CD split into one BIN per track exposed two related gaps in the model: session 2 was placed too early, and the unbacked space between sessions could alias bytes from the next BIN.

Root cause

Each BIN restarts its CUE INDEX times at 00:00:00. When REM SESSION 02 is also a FILE boundary, the sheet omits the physical distance between sessions: 6750 lead-out frames, 4500 lead-in frames, and—when the cue does not state one—the next track's 150-frame pregap.

For Akumajou Dracula - MIDI Collection (Japan), track 21 was reported at LBA 267988 instead of INDEX 01 at LBA 279388. Once that layout was corrected, a hardware trace exposed the backing problem: the host requested CD-DA at LBA 268045, 57 frames into the valid session gap, but physically concatenated BINs turned that into MODE2 sector 57 of track 21 and produced modem-like noise.

The 57 is not a missing constant. It is simply where the host read inside the 11250-frame gap. Combined images work because those gap frames physically exist in the single BIN.

Fix

  • At a session change that is also a FILE change, add the 11250-frame lead-out/lead-in distance to disc addresses.
  • Add a 150-frame unstored pregap only when the cue does not state its own pregap.
  • Keep file_start and file_offset unchanged, so session 2 INDEX 01 still maps to byte 0 of its BIN.
  • Build a sparse virtual seek layout once when the CUE files are opened.
  • Represent unbacked inter-file frames as holes that CCueBinFileDevice::Read() serves as digital zeros.
  • Preserve backed prefixes and resume at the correct later file when a read crosses a hole.
  • Fail malformed or unrepresentable layouts closed rather than abutting their files.

Per-file physical sizes remain available for parsing, lead-out, and validation. GetSize() now describes the addressable virtual seek space for split CUE images. Ordinary contiguous multi-BIN files retain adjacent bases and the existing cache/fast-seek path.

No disc-format logic or per-batch track scan is added to tcdstate_update.cpp; that generic transfer hot path is byte-identical to main.

Verification

Twenty new tests cover session layout, stored and unstored pregaps, zero-length INDEX 00, TOC and both lead-outs, READ(10) of CD001, sparse gap silence, audio/gap/data spanning reads, exact INDEX 01 mapping, ordinary split-audio boundaries, image limits, malformed-layout rejection, failed-file ownership rollback, invalid LBA sense data, and overflow-safe READ CD bounds.

  • Host regression suite: 204 passed, 0 failed
  • Host regression suite with CHD: 207 passed, 0 failed
  • CodeRabbit completed review: zero findings after minor test-guard corrections
  • Sparse implementation hardware-tested by iTechMedic on the original Akumajou reproduction: Song 20 ends cleanly and session-2 data remains accessible
  • Polynectar independently exercised the earlier gap-silence behavior across numerous images; the sparse refactor preserves that behavior without modifying the generic transfer loop

One unrelated pre-existing issue remains out of scope: READ SUB-CHANNEL relative addressing around unstored pregaps is tracked separately in #245.

@iTechMedic
iTechMedic force-pushed the multibin-session-gap branch from 4dba1af to 73859dc Compare August 2, 2026 00:48
@iTechMedic

iTechMedic commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Hardware verification

  • The original failure was captured as READ CD requests inside the unbacked session gap returning track 21 MODE2 bytes as CD-DA.
  • The sparse CUE/BIN implementation now represents that gap in the image device and returns digital silence without changing tcdstate_update.cpp.
  • iTechMedic tested the sparse implementation on the original Akumajou image: Song 20 ends cleanly and session-2 Windows/Mac data remains accessible.
  • Ordinary multi-BIN audio images and Alien Trilogy continued to work in testing.
  • Polynectar independently tested the earlier gap-silence behavior across numerous images and reported perfect results; the final sparse refactor preserves the same host-visible behavior outside malformed/error paths.
  • Final host suite: 204 passed; 207 passed with CHD.
  • CodeRabbit completed review: zero findings after minor test-guard corrections.
  • Final PR commit: 0202bc9.

A split rip gives each track its own BIN and restarts INDEX times at
00:00:00 in every one, so a "REM SESSION 02" that is also a FILE boundary
never says where session 2 begins. The parser placed the data track directly
after session 1's last audio sector, 11400 frames early, and hosts looking
for the volume descriptor read the wrong sectors and mounted the disc as
audio-only. Akumajou Dracula - MIDI Collection (Japan) is the reported case:
20 audio BINs totalling 267988 sectors, then a MODE2/2352 BIN whose own
sector 16 header reads 62:07:29 -- LBA 279404, so INDEX 01 belongs at 279388.

The missing distance is the Orange Book gap, 90 s of lead-out (6750 frames)
plus 60 s of lead-in (4500), and then the track's own 150-frame pregap. So
track_start goes 11250 past the previous session's content and INDEX 01 a
further 150 on. Where the sheet states a pregap itself, by INDEX 00 or
PREGAP, only the 11250 is synthesized; an INDEX 00 equal to INDEX 01 states
a pregap of no length and counts as stating none.

The 11250 rides on cumulative_offset and the 150 on unstored_pregap_length,
the levers an ordinary unstored PREGAP already uses, so both carry into
later tracks while file_start and file_offset stay put -- CueResolveLBA at
the corrected LBA still lands on byte 0 of the session-2 BIN. Single-BIN
multisession cues carry absolute INDEX times and are untouched: the gap
needs a session advance, a FILE change and a previous track. Nothing in
GetSessionLeadoutLBA changes, because track_start lands exactly the 11250
frames past session 1's content that it already subtracts.

Those synthesized addresses have no bytes behind them, and the split device
could not say so. Its seek space was the .bin files physically concatenated,
so an address that ran off the end of session 1's file was session 2's first
byte: a host ripping the last audio track read into the gap and got Mode 2
sectors served as CD-DA, about a second of modem noise. Confirmed on
hardware -- READ CD (0xBE) for 92 sectors from LBA 268045, 57 past the
stored end of track 20, every one answered successfully with the wrong data.
Those 57 sectors are not a layout constant anyone forgot; the request simply
landed inside the 11250-frame gap, and concatenation aliased it onto BIN
sector 57 of the next file. A single-BIN rip of the same disc has no bug
because its gap sectors physically exist.

So CCueBinFileDevice's seek space becomes sparse, which is where a question
about the backing layout belongs. Once every FILE is open, one pass over the
sheet records where each file's stored frames begin and end on the disc.
Where the next file starts later than the previous one ends, the two virtual
bases are separated by that many frames' worth of bytes instead of abutting,
and the hole is recorded. GetByteOffsetForLBA answers inside the hole for a
frame in it, Read serves holes as zeros without opening, seeking or caching
any .bin, and a read crossing one keeps its real prefix and resumes on the
far side at the right offset. Files that abut on the disc still get adjacent
bases and cross directly, so an ordinary split rip is byte-for-byte what it
was, and per-file physical sizes stay in GetDataFileSizes for the parser,
lead-out and validation.

Construction fails closed. A boundary the layout cannot represent -- a hole
wider than a disc, a track with no sector length, an open file no track
places, an address space that would overflow -- rejects the image rather
than abutting the files, because abutting them is the aliasing this is
fixing. The layout is staged in locals and committed only once every
boundary passes, so a rejection leaves the previous one intact; AddDataFile
returns false without adopting the FIL, drops it from m_Files so the
destructor cannot close it a second time, rebuilds the shorter layout, and
leaves the caller to close what it still owns. The loader reports an
unusable disc layout rather than a file-count limit.

GetSize therefore means addressable bytes of the seek space, not stored
bytes. Both its in-tree consumers want that: the Seek bound and the ReadCD
end-of-image bound are both bounds on this address space, and GetLeadoutLBA
already reads per-file sizes for split images. ReadCD now compares by
subtraction against that size and rejects a failed translation outright with
LOGICAL BLOCK ADDRESS OUT OF RANGE, so neither the (u64)-1 sentinel nor a
host-chosen byte count can wrap the bound. Nothing was
added to tcdstate_update.cpp, which is the generic transfer hot path and is
unchanged from main; a backed image pays one extra comparison per read and
an image with no holes pays nothing.

20 new tests, 204 total (207 with CHD), over the direct INDEX 01, stored
INDEX 00, explicit PREGAP, zero-length INDEX 00 and no-session-boundary
layouts, plus TOC, READ(10), both lead-outs, gap-as-silence, a read crossing
into the gap, a read spanning audio then gap then data, INDEX 01's own first
stored sector, file and image boundaries, an oversized hole that refuses to
mount, a rejected AddDataFile leaving neither a half-built layout nor a
double-closed file, a failed translation answering 05/21/00 with no read
left pending, a range ending exactly at the addressable end, and an ordinary
split audio boundary that must still cross into the next .bin. The
fixture is 150 sectors of session 1 audio and a generated 40-sector Mode 2
Form 1 BIN with a CD001 descriptor at local sector 16, built at test time.
@iTechMedic
iTechMedic force-pushed the multibin-session-gap branch from 73859dc to 0202bc9 Compare August 2, 2026 16:02
@danifunker
danifunker merged commit 6e8699d into danifunker:main Aug 3, 2026
1 check passed
@iTechMedic
iTechMedic deleted the multibin-session-gap branch August 7, 2026 18:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants