CHDSharp 1.5.1
See the version list below for details.
dotnet add package CHDSharp --version 1.5.1
NuGet\Install-Package CHDSharp -Version 1.5.1
<PackageReference Include="CHDSharp" Version="1.5.1" />
<PackageVersion Include="CHDSharp" Version="1.5.1" />
<PackageReference Include="CHDSharp" />
paket add CHDSharp --version 1.5.1
#r "nuget: CHDSharp, 1.5.1"
#:package CHDSharp@1.5.1
#addin nuget:?package=CHDSharp&version=1.5.1
#tool nuget:?package=CHDSharp&version=1.5.1
CHDSharpLib
Pure C# CHD (Compressed Hunks of Data) reader and writer — V1–V5, all 10 codecs, parent/child chaining, parallel verification, 100% match with MAME chdman.
Fork of RomVault/CHDSharp by Gordon Jefferyes, extended with Zstd, AVHuff, V5 compressed map, random-access API, parent/child chaining, parallel verification, seekable stream, span reads, read-ahead decompression, and lazy parent resolution.
What's New in v1.5.0
- Release pipeline - v1.5.0 ships from a single version tag via GitHub Actions: six platform CLI bundles (
CHDSharp_<rid>_v1.5.0.zip), the NuGet package, the wiki and the docs site (scripts/package-release.ps1,scripts/prepare-wiki.ps1,.github/workflows/release.yml). - Update-check fix - the CLI's "new version available" notice now resolves the direct per-platform bundle download (asset name built from the release version plus OS and architecture).
- No library API changes in this release - the package is unchanged apart from the version and a refreshed README.
What's New in v1.5.1
- Single-bin multisession cues with an unusable lead-out now encode exactly like chdman - a
REM LEAD-OUTat or before the session's last track (REM LEAD-OUT 00:00:00is common in real IsoBuster/ImgBurn rips) used to trim the track to a negative length and abort (the session lead-out values leave a negative length (-11400 frames)). MAME compares auint32_tframe count against a signedint32_tend frame, so the negative value wraps and the trim is skipped; the parser now performs the same unsigned comparison. Byte-identical tochdman createcdon the three reported failure shapes. - A pad larger than its track no longer zeroes the track - the CD reader now mirrors chdman's unsigned pad offset (
chdman.cpp:471-472): when the pad is larger than the track the offset wraps below the track start, the pad region is unreachable, and every frame is read from the source file. The GD-ROM data/pad/split ordering now matches chdman as well. - No public API changes - behavior only; verified with 3 new
chdman createcdbyte-parity fixtures, 454/454 encoder tests and 4214/4214 library tests.
What's New in v1.4.5
- Multisession cue parity - ports MAME's
REM SESSION/REM LEAD-IN/REM LEAD-OUT/ session-pregap layout rules (IsoBuster/ImgBurn single-bin trim + zero-filled inter-session gaps, Redump/DiscImageCreator synthesized lead-in/out and.imgheuristic). Multisession cues now produce byte-identical CHDs tochdman createcd; malformed lead-out values are rejected with a clear error instead of being encoded as a wrong image. Verification: 3 chdman byte-parity fixtures + unit tests, suite 451/451.
What's New in v1.4.4
- Redump multi-cue Dreamcast parity - a
.cuewithREM SINGLE-DENSITY AREA/REM HIGH-DENSITY AREAmarkers (MAME'sis_gdicue) now gets the full GD-ROM layout: pregaps move onto the previous track as split regions, the last single-density track pads up to LBA 45000, and the CHD is written withCHGDmetadata. Verified byte-identical tochdman createcdon a real 1.19 GB Redump multi-cue, with a byte-perfectchdman extractcdround-trip of all three tracks. - cdrdao TOC
STARTparity - a bareSTARTline (cdrdao's documented form) is now accepted as "no pregap", matching MAME 0.289'sparse_tocexactly (msf_to_frames("")is 0). Previously it threwMalformed START command, failing cdrdao.tocfiles thatchdmanconverts fine. Locked down with a parse regression test and byte-parity tests againstchdman createcdfor bothSTARTandSTART 00:00:00.
What's New in v1.4.3
Consolidates all changes since v1.4.2 - the last chdman byte-parity gaps are closed:
- Partial-tail raw-encode fix -
EncodeRawon inputs of ≥ 513 hunks whose length is not hunk-aligned could produce a self-consistent-but-wrong CHD (stale compression ring-buffer slots). Fixed and locked down with guardrail tests: a synthetic probe, partial-last-hunk unit tests, and a resized long-tail battle case. - FLAC encoder byte parity - fixed-predictor analysis window + native
cosftukey-window rounding. The vendored FLAC encode is now byte-identical tochdmanon the verification corpora: a 7.8 GB XGD2 image (1,912,816 hunks, default codec list), a 100 MB partial-tail slice, and three single-hunk repros. - Earlier byte-parity fixes in the cycle - stale work-buffer tail + LZMA match-finder insert parity.
- Diagnostics - detailed decompression-failure messages (hunk index, codec, CRC/truncation reasons) and chdman-style progress + speed-meter output.
- Status -
CHDSharpTest12,648/12,648 (1,308/1,308 per-TFM aggregate),CHDSharpBattleTest3174/3174 vs MAME 0.289.
What's New in v1.4.2
Final byte-parity gaps closed against MAME 0.289 — battle harness still green (2907/2907 synthetic + 3003/3003 real-world):
- LZMA raw-encode byte parity — the encoder now replicates
chd.cpp's 1 MiB compression work buffer (256-hunk ring, 128-hunk batches) exactly: partial final hunks keep the stale slot bytes from the previous cycle and the raw SHA-1 folds only the valid bytes (m_compsha1.append(dest, numbytes)parity).createraw/createhd -ion non-hunk-aligned inputs now produce byte-identical files and hashes. createhdsize / CHS quirks — size parsing matcheschdmansscanf("%I64u")(leading digits only:"512K"= 512 bytes) and sub-geometry sizes round up to the guessed-CHS product; newChdEncodeOptions.LogicalLengthBytesoverride.- Laserdisc AVI byte parity —
createld/extractldoutput is byte-identical tochdman. - DVD metadata correction —
DVDpayload is exactly one NUL byte (length 1) perchd.h:351std::stringlength()+1behaviour (corrects the v1.4.1 note). - Strict CLI validation —
info/verifynow validate the full option list (duplicate-idetection, per-command sets) and returnchdman-consistent exit codes.
What's New in v1.4.1
Complete chdman parity against MAME 0.289 — 16 discrepancies fixed and battle-verified (2907/2907 synthetic + 3003/3003 real-world):
createhd/createrawparity —createhd -inow synthesizesGDDDgeometry (51-byte delta gone),createhd --identextracts CHS from ATA bytes 2/6/12, blank images enforce-c none,createrawvalidates--unitsize/ hunk size (16 B–1 MiB, multiple of unit), andlisttemplatesshows all 17 templates. Parent/ident fallback andguess_chsnow matchchdman.extractcdcooked vs raw —ExtractToDirectory(..., cooked: true)writes cooked sectors (track.DataSize, subcode omitted, audio byte-swapped) and matcheschdman extractcdfor all 43 CDs + 3 GD-ROMs;cooked: false(default) keeps 2448-byte raw frames with library compat. CLIextractcddefaults to cooked (--rawto keep raw). Extraction now uses a 32 MiB aligned buffer with batch writes and correct per-mode audio swap (CUEBIN always, GDI onlyVersion>4).- GD-ROM Redump —
has_physical_pregap/padframes/splitframesfixup ported;ChdTrackInfogainsSplitFrames/PhysFrameOfs,REM SINGLE-DENSITY/HIGH-DENSITYemitted,MODE_GDIvsMODE_CUEBINhandled perchdman. copyand parent handling —copynow picks per-type defaults (get_compression_defaults:2426), parent hunk-size inheritance and factor check (hunk % input && input % hunk) forcreateraw/createhd/createcd/createdvd/copy, andinfo --verboseper-codecSELF/PARENT/MINIstats with 0.5 s throttling.- DVD and metadata —
DVDtag is now empty (length 0) perchdman,addmeta --valuetextno trailing NUL, andextractcd --outputbinrequires%twhenis_splitbin. - CLI strictness — unknown/duplicate/missing-parameter errors, per-command valid sets,
isb/ish/ib/ihmutual exclusion and10MBtrailing-Bhandling now matchchdmanverbatim. - Docs — new
docs/chd-deep-reference.mdaudited against MAME 0.289 with 9⚠ Corrections;docs/chd-format.mdfixed (RLE, 0.289).Meziantou.Analyzer 3.0.190, zero warnings. Targetsnet8.0/net9.0/net10.0— pure C#, no native dependencies.
What's New in v1.4.0
- Byte-for-byte parity with
chdmanfor every codec — includingcreateld(AVHuff),cdzs(CD Zstd), andzstd. The encoder's output is now byte-identical to MAME for all writable codecs. VendoredZSTDreplacesZstdSharp.Port— the library now ships its own pure C# port of the zstd 1.5.5 tree MAME bundles (encoder + decoder). Zero native dependencies — every compression codec is vendored in pure C#. The only runtime NuGet dependency is the optionalMicrosoft.Extensions.Logging.Abstractionslogging abstraction (used only when you enable logging).- 38 bugs fixed from a deep code review — including thread-safe parallel verification,
correct CLI exit codes,
ReadHunk(Span)hunk caching,MemoryMappedFiledisposal on view failure, andLzmaStream.Seekposition fixes.
Parity tables: the complete chdman-vs-CHDSharp comparison (encoder byte-identical matrices, decoder, delta, copy, CLI suites — 2907/2907 checks) lives in docs/chdman-parity.md.
What's New in v1.3.0
- Full CHD encoding/writing —
ChdEncoderis now part ofCHDSharpLib. Create CHDs from raw binaries, CD images (CUE/GDI/ISO/TOC/NRG), or blank HD templates. Re-compress existing CHDs with new codecs. Create delta/parent CHDs. All 10 codecs with best-per-hunk selection, parallel compression (1–64 workers), and 100% byte-identical output vs MAMEchdman. No separate package needed. - Hard disk ident metadata (
IDNT) — Read/writeIDNTmetadata (ATA IDENTIFY DEVICE response, 512 bytes) preserving original drive model, serial, CHS geometry, and firmware revision. Access viaChdFile.IdentDataproperty.--ident <path>flag oncreatehdCLI. Automatically preserved duringChdEncoder.Copy(). - Hard disk encryption key metadata — Read/write
KEYmetadata (encryption key) used by OG Xbox and other platforms with encrypted HDD contents. Access viaChdFile.KeyDataproperty. Automatically preserved duringChdEncoder.Copy(). - PCMCIA CIS metadata — Read/write
CISmetadata (Card Information Structure) used by PC Engine CD and other PCMCIA platforms. Access viaChdFile.PcmciaCisDataproperty. Automatically preserved duringChdEncoder.Copy(). - Bounded metadata string parsing — Hardened track metadata parsing against crafted payloads (libchdr #165). TYPE/SUBTYPE/PGTYPE/PGSUB fields are capped at 15 characters matching MAME's stack buffer limits. Track metadata payloads > 4 KiB are rejected. Embedded null bytes in payloads are rejected. Metadata entries > 64 KiB are rejected at the storage layer. Malformed entries are silently skipped rather than crashing.
- Deflate decoder infinite-loop guard — Added
here.bits == 0guards in the inflate state machine (Len,Dist,CodeLensstates) andInflateFast(dolen,dodistpaths). When a Huffman table entry hasbits=0, the decoder transitions toBadmode and returnsZ_DATA_ERRORinstead of looping indefinitely (libchdr #168, miniz 3.1.2 fix parity). ZstdSharp audited — uses a different format not susceptible to this bug. ChdImageStream— seekableStreamover decompressed image —ChdFile.OpenAsStream()returns a read-only, seekableStreamwrapping the decompressed CHD. SupportsRead,ReadAsync,Seek,Position, andLength. Dispose disposes the parentChdFileby default. Available via filename, filename+parent, or from an existingChdFileinstance.- Span<byte> read overloads —
ReadHunk(uint, Span<byte>)andRead(ulong, Span<byte>, int)enable callers to usestackalloc,ArrayPool, or pinned memory without allocating a temporarybyte[]. Internally reuses the existing hunk cache.ChdImageStream.Read(Span<byte>)now uses the span path directly (zero-copy on .NET 7+). - Threaded read-ahead decompression —
ChdFile.ConfigureReadAhead(int lookAhead)enables background pre-decompression of upcoming hunks. After eachReadHunk, the next N hunks are decompressed in the background usingReadHunkConcurrent. Results are cached in aConcurrentDictionary(L2) checked before the LRU cache (L1). UsesSemaphoreSlimto cap concurrency andThreadLocal<ChdCodecState>for thread-safe codec access.FlushReadAhead()clears stale entries after seeks. Ideal for sequential streaming and verification workloads. - Lazy parent resolution (
ParentResolver) — Open child CHDs without providing the parent path upfront. Supply aParentResolvercallback that resolves the parent by SHA1/MD5 hash on first read. The resolved parent is cached. Also available onChd.CheckFileWithParent. - CD/GD-ROM track (TOC) parsing — Full track layout via
Tracksproperty backed byChdTocParser, exposingChdTrackInfowith track type, sector sizes, pregap/postgap, and GD-ROM support. Legacy GD-ROMs (CHGT/CD_FLAG_GDROMLE) are detected viaIsLittleEndianAudioand their AUDIO tracks byte-swapped during extraction. IncludesGenerateCueSheet(),GenerateGdiDescriptor(),ExportToc(),ExtractToDirectory(). - LBA/MSF sector reads —
ChdFile.ReadSector(lba),ReadSectorMsf(m, s, f), andReadFrame(lba)read CD/GD-ROM sectors or full 2448-byte frames by logical block address, mapped through the track table (pregap-aware).CdRomAddressconverts between BCD MSF and LBA (with and without the 150-frame lead-in offset). UnitBytesproperty — Derives sector size from metadata for all CHD versions: V5 reads from header, V1-V4 detects HDD (512B) or CD (2448B) from metadata tags- New enums —
ChdTrackType(matches MAMEcdrom.h: Mode1, Mode2, Audio, etc.) andChdSubType(None, Normal, Raw) - Deterministic reproducible builds — Byte-for-byte reproducible via
<Deterministic>true</Deterministic>with embedded SourceLink and debug symbols
Installation
dotnet add package CHDSharp
Targets net8.0, net9.0, and net10.0. No native dependencies — all codecs (including Zstd via the in-repo pure-C# VendoredZSTD port) are implemented from scratch in C#. The only runtime NuGet dependency is Microsoft.Extensions.Logging.Abstractions (optional logging).
Quick Start
Verify a standalone CHD (parallel, fast)
using CHDSharp;
using CHDSharp.Models;
using Stream s = File.OpenRead("game.chd");
var result = Chd.CheckFile(s, "game.chd", deepCheck: true);
if (result.IsSuccess)
Console.WriteLine($"V{result.Version} — SHA1: {result.Sha1Hex}");
else
Console.WriteLine($"Error: {result.Error.GetMessage()}");
Verify a child (differential) CHD against its parent
var result = Chd.CheckFileWithParent("child.chd", "parent.chd");
Open a child CHD with lazy parent resolution
Instead of providing an explicit parent path, you can supply a ParentResolver callback
that resolves the parent by SHA1/MD5 hash at read time. This is useful for frontends that
manage their own parent lookup (ROM set scanning, database queries, etc.).
ParentResolver resolver = (sha1, md5) =>
{
// Your custom lookup logic here (database, filesystem search, etc.)
var parentPath = FindParentByHash(sha1);
if (parentPath == null) return null;
var err = ChdFile.Open(parentPath, out var parent);
return err == ChdError.Chderrnone ? parent : null;
};
var err = ChdFile.Open("child.chd", resolver, out var chd);
// Parent is resolved lazily on the first ReadHunk call that needs it,
// then cached for all subsequent reads.
Random-access reading
var err = ChdFile.Open("game.chd", out var chd);
if (err != ChdError.Chderrnone) return;
using (chd)
{
// Inspect metadata (game name, disc label, etc.)
foreach (var meta in chd.Metadata)
Console.WriteLine(meta.ToString());
// Read a single decompressed hunk
byte[] hunk = new byte[chd.HunkBytes];
chd.ReadHunk(42, hunk);
// Read arbitrary byte range (handles hunk boundaries)
byte[] buf = new byte[1024];
chd.Read(offset: 0x10000, buf, 0, buf.Length);
}
Async random-access reading
var (err, chd) = await ChdFile.OpenAsync("game.chd");
if (err != ChdError.Chderrnone) return;
await using (chd)
{
byte[] hunk = new byte[chd.HunkBytes];
await chd.ReadHunkAsync(42, hunk);
}
Quick file checking
bool isChd = Chd.IsChdFile("game.chd", out uint version);
// isChd=true, version=5 for a V5 CHD
// Or just yes/no:
bool yesNo = Chd.IsChdFile("game.chd");
Read the full header without opening the file (libchdr chd_read_header parity)
var err = Chd.ReadHeader("game.chd", out ChdHeaderInfo? header);
if (err == ChdError.Chderrnone)
{
Console.WriteLine($"V{header.Version}, {header.TotalBytes:N0} bytes, " +
$"{header.TotalHunks} hunks x {header.HunkBytes}");
Console.WriteLine($"Codecs: {string.Join(", ", header.Compression)}");
Console.WriteLine($"Parent required: {header.HasParent}");
}
// Async + stream variants:
var (aerr, aHeader) = await Chd.ReadHeaderAsync("game.chd");
Chd.ReadHeader(File.OpenRead("game.chd"), out ChdHeaderInfo? sHeader); // stream left open
The file is opened, parsed, and closed again — no handle is kept alive. Stream and async variants are also available.
Decompress entire image to a byte array
ChdFile.Open("game.chd", out var chd);
using (chd)
{
chd.ReadAllBytes(out byte[] image);
// image now contains the full decompressed image
}
Get CD/GD-ROM track layout (TOC)
ChdFile.Open("game.chd", out var chd);
using (chd)
{
if (chd.Tracks is not { } tracks) return;
foreach (var track in tracks)
{
Console.WriteLine($"Track {track.TrackNumber}: {track.GetTypeString()} " +
$"{track.Frames} frames, pregap={track.PreGap}");
}
}
Read sectors by LBA / MSF (CD/GD-ROM)
using CHDSharp.Utils;
ChdFile.Open("game.chd", out var chd);
using (chd)
{
// 2352-byte sector at LBA 0 (MSF 00:02:00 — the first track's INDEX 01)
byte[] sector = new byte[2352];
chd.ReadSector(0, sector);
// Same sector, addressed by BCD MSF (0x02 minutes = "02")
chd.ReadSectorMsf(0x00, 0x02, 0x00, sector);
// Full 2448-byte frame (2352 data + 96 subcode)
byte[] frame = new byte[chd.UnitBytes];
chd.ReadFrame(0, frame);
// Convert between BCD MSF and LBA (with or without the 150-frame lead-in)
int lba = CdRomAddress.MsfToLba(0x02, 0x00, 0x00); // 8850
(byte m, byte s, byte f) = CdRomAddress.LbaToMsf(lba); // (0x02, 0x00, 0x00)
}
LBA 0 maps to the first data track's INDEX 01: PreGap frames into the image when the pregap is stored physically (metadata PGTYPE:V...), and at image frame 0 otherwise (Redump-style CUEs, NRG, TOC, GDI). Non-CD images return Chderrinvaliddata.
Iterate hunks one at a time
ChdFile.Open("game.chd", out var chd);
using (chd)
{
foreach (byte[] hunk in chd.EnumerateHunks())
{
// Process each decompressed hunk; buffer is reused — copy if needed
}
}
Writing CHDs (Encoding)
The encoder is part of the same library — no separate package needed. All encoder types live in the CHDSharp.Encoder namespace.
Raw Binary → CHD
using CHDSharp.Encoder;
// Simplest form — default codec (zlib), auto hunk/unit sizes
ChdEncoder.EncodeRaw("game.bin", "game.chd");
// Custom codecs (tried per hunk; smallest output wins)
ChdEncoder.EncodeRaw("game.bin", "game.chd",
codecTags: ChdCodecs.ParseCodecTags("zlib,zstd,lzma"));
// Custom hunk/unit sizes
ChdEncoder.EncodeRaw("game.bin", "game.chd",
hunkBytes: 65536, unitBytes: 4096);
// Uncompressed CHD (-c none)
ChdEncoder.EncodeRaw("game.bin", "game.chd",
codecTags: [CodecTags.None]);
CD Image → CHD
// From CUE sheet
ChdEncoder.EncodeCd("game.cue", "game.chd");
// From GDI, ISO, TOC, or NRG
ChdEncoder.EncodeCd("game.gdi", "game.chd");
ChdEncoder.EncodeCd("game.iso", "game.chd");
Blank HD CHD
// Zero-filled CHD with auto-derived CHS geometry
ChdEncoder.CreateBlank("blank.chd", 100 * 1024 * 1024UL); // 100 MB
// Explicit CHS geometry
ChdEncoder.CreateBlankWithChs("blank.chd",
cylinders: 1024, heads: 16, sectors: 63, sectorSize: 512);
Re-compress Existing CHD
// Re-compress with Zstd
ChdEncoder.Copy("old.chd", "new.chd",
codecTags: [CodecTags.Zstd]);
// Preserve legacy metadata (no upgrade)
ChdEncoder.Copy("old.chd", "new.chd",
codecTags: [CodecTags.Zstd],
options: new ChdEncodeOptions { NoMetadataUpgrade = true });
Delta (Parent) CHD
// Create a differential child against a parent
ChdEncoder.EncodeRaw("game.bin", "game.chd",
options: new ChdEncodeOptions { ParentPath = "base.chd" });
Progress Reporting During Encoding
var options = new ChdEncodeOptions
{
TaskCount = 8,
HunkCompleted = p => Console.WriteLine(
$"hunk {p.HunkIndex,6}/{p.HunkCount} {p.CodecName,-5} " +
$"{p.RawBytes,8} -> {p.StoredBytes,8} B ({p.Ratio:P1})")
};
ChdEncoder.EncodeRaw("game.bin", "game.chd", options: options);
Extraction
// Extract CHD tracks to a directory
ChdFile.Open("game.chd", out var chd);
using (chd)
{
var files = chd.ExtractToDirectory("output_folder", "game");
foreach (var f in files)
Console.WriteLine($"Extracted: {f}");
}
// Generate CUE sheet for CD CHDs
ChdFile.Open("game.chd", out var chd2);
using (chd2)
{
string cue = chd2.GenerateCueSheet("game.bin");
File.WriteAllText("game.cue", cue);
}
Logging
The library uses Microsoft.Extensions.Logging.Abstractions. By default, logging is discarded. To enable logging (e.g., with Serilog):
using Serilog;
using Serilog.Extensions.Logging;
var serilogLogger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
Chd.LoggerFactory = new SerilogLoggerFactory(serilogLogger);
// All subsequent Chd/ChdFile operations will log through Serilog
You can use any ILoggerFactory-compatible provider (NLog, Microsoft.Extensions.Logging.Console, etc.).
API Reference
Chd — Static class
| Member | Signature | Description |
|---|---|---|
| LoggerFactory | ILoggerFactory? (static property) |
Set to enable internal logging. |
| TaskCount | int (static property, default 8) |
Number of parallel workers for CheckFile (1-64). Change before calling. |
| CheckFile | ChdResult CheckFile(Stream, string, bool, IProgress<ChdProgress>? = null, CancellationToken = default) |
Full parallel verification. Returns error, version, SHA1, MD5. Reports progress per hunk; cancellable. |
| CheckFileWithParent | ChdResult CheckFileWithParent(string, string, IProgress<ChdProgress>? = null, CancellationToken = default) |
Verify child CHD against parent. Pass null for second arg for standalone. Reports progress per hunk; cancellable. |
| CheckHeader | bool CheckHeader(Stream, out uint length, out uint version) |
Sniff magic + version. Stream must be at position 0. |
| IsChdFile | bool IsChdFile(string) / bool IsChdFile(string, out uint) |
Quick check if a file is a valid CHD. |
| ReadHeader | ChdError ReadHeader(string, out ChdHeaderInfo?) / ChdError ReadHeader(Stream, out ChdHeaderInfo?) / Task<(ChdError, ChdHeaderInfo?)> ReadHeaderAsync(string) |
Parse the full header DTO (version, flags, codec slots, sizes, hashes, unit info, parent linkage) without opening the file for reads or keeping a handle. libchdr chd_read_header parity. |
ChdResult — Verification result
| Property | Type | Description |
|---|---|---|
Error |
ChdError |
Error code (ChderrNone on success). |
Version |
uint? |
CHD version (1-5). |
Sha1 |
byte[]? |
SHA1 hash from header. |
Md5 |
byte[]? |
MD5 hash from header. |
IsSuccess |
bool |
True if Error == ChderrNone. |
Sha1Hex |
string |
SHA1 as lowercase hex, or "(none)". |
Md5Hex |
string |
MD5 as lowercase hex, or "(none)". |
Supports deconstruction: var (err, ver, sha1, md5) = result;
ChdHeaderInfo — Full header DTO
Returned by Chd.ReadHeader(...). A snapshot of the CHD header without keeping the file open.
| Property | Type | Description |
|---|---|---|
Length |
uint |
On-disk header length (76/80/120/108/124 for V1-V5). |
Version |
uint |
CHD format version (1-5). |
Flags |
uint |
Raw flags (V1-V4): bit 0 = has parent, bit 1 = writable. 0 for V5. |
Compression |
ChdCodec[] |
Codec slots (up to 4 for V5). |
HunkBytes / TotalHunks |
uint |
Hunk size / hunk count. |
TotalBytes |
ulong |
Decompressed image size. |
MetaOffset / MapOffset |
ulong |
Metadata / V5 map file offsets. |
Md5 / ParentMd5 |
byte[]? |
MD5 hashes (V1-V3). |
Sha1 / RawSha1 / ParentSha1 |
byte[]? |
SHA1 hashes (V3-V5). |
UnitBytes / UnitCount |
uint / ulong |
Unit size / count (matches ChdFile.UnitBytes). |
HasParent |
bool |
True if a differential child requiring a parent. |
ObsoleteCylinders/Heads/Sectors/Hunksize |
uint |
Obsolete V1/V2 hard-disk geometry. |
ChdProgress — Long-operation progress
Pass an IProgress<ChdProgress> to Chd.CheckFile, Chd.CheckFileWithParent, ChdFile.ReadAllBytes, ChdFile.EnumerateHunks, or ChdFile.ExtractToDirectory to receive a report after every decompressed hunk.
| Property | Type | Description |
|---|---|---|
CurrentHunk |
long |
Hunks processed so far (1-based; equals TotalHunks when done). |
TotalHunks |
long |
Total hunks in the image. |
BytesProcessed |
long |
Decompressed bytes processed so far. |
TotalBytes |
long |
Total decompressed image size. |
Elapsed |
TimeSpan |
Wall-clock time since the operation started. |
Percent |
double |
Percentage completed (0–100). |
var progress = new Progress<ChdProgress>(p =>
Console.WriteLine($"{p.Percent:F0}% — {p.BytesProcessed:N0}/{p.TotalBytes:N0} bytes ({p.Elapsed.TotalSeconds:F1}s)"));
var result = Chd.CheckFile(File.OpenRead("game.chd"), "game.chd", deepCheck: true, progress);
All parameters default to null, so existing callers are unaffected. For Chd.CheckFile(deepCheck: true), reports arrive in hunk order from the internal hashing thread; new Progress<ChdProgress>(...) marshals them back to the capturing context automatically.
Cancellation
All long-running methods take an optional trailing CancellationToken (default default) and throw OperationCanceledException on cancellation: Chd.CheckFile, Chd.CheckFileWithParent, ChdFile.Open/OpenAsync (all overloads), ReadHunk/ReadHunkAsync, Read/ReadAsync, ReadAllBytes, and ExtractToDirectory/ExtractToDirectoryWithReporting. For deep verification the token is linked into the pipeline's internal CancellationTokenSource, so cancel stops the workers immediately and the method throws OCE instead of reporting a bogus partial-hash mismatch. Async twins also pass the token to Task.Run (a pre-cancelled token yields a cancelled task). Cancellation is never swallowed into an error result.
using var cts = new CancellationTokenSource();
var result = Chd.CheckFile(File.OpenRead("game.chd"), "game.chd", deepCheck: true, cancellationToken: cts.Token);
ChdFile — Random-access reader
All Open overloads seek from the start. The reader is not thread-safe — serialize all calls.
Static factory methods
| Overload | Description |
|---|---|
Open(string path, out ChdFile? chd, CancellationToken = default) |
Standalone CHD from disk. |
Open(string path, string parentPath, out ChdFile? chd, CancellationToken = default) |
Child CHD; parent opened and owned internally. |
Open(string path, ChdFile? parent, out ChdFile? chd, CancellationToken = default) |
Child with external parent. Pass null for standalone. |
Open(Stream s, bool leaveOpen, out ChdFile? chd, CancellationToken = default) |
From seekable stream. |
Open(Stream s, bool leaveOpen, ChdFile? parent, out ChdFile? chd, CancellationToken = default) |
From stream with external parent. |
OpenAsync(...) |
Async overloads for all Open variants, each with an optional trailing CancellationToken. |
Instance methods
| Method | Signature | Description |
|---|---|---|
| ReadHunk | ChdError ReadHunk(uint, byte[], CancellationToken = default) |
Decompress a single hunk. Serves cached hunks when CacheSize > 1. |
| Read | ChdError Read(ulong, byte[], int, int, CancellationToken = default) |
Read byte range. Caches last hunk. |
| ReadSector | ChdError ReadSector(uint lba, byte[], CancellationToken = default) |
Read the 2352-byte sector data at an LBA (CD/GD-ROM only; pregap-aware mapping). |
| ReadSectorMsf | ChdError ReadSectorMsf(byte m, byte s, byte f, byte[], CancellationToken = default) |
Read the 2352-byte sector at a BCD MSF address (e.g. (0x00, 0x02, 0x00) = LBA 0). |
| ReadFrame | ChdError ReadFrame(uint lba, byte[], CancellationToken = default) |
Read the full 2448-byte frame (data + subcode) at an LBA. |
| ReadAllBytes | ChdError ReadAllBytes(out byte[], IProgress<ChdProgress>? = null, CancellationToken = default) |
Decompress entire image to a byte[]. Reports progress per hunk. |
| ConfigureCache | void ConfigureCache(int) |
Set the multi-hunk LRU cache size. <= 1 disables it (single-slot behaviour). |
| Precache | ChdError Precache() |
Load the entire compressed file into memory for fast random access (libchdr chd_precache parity). Idempotent. |
| GetMetadata | ChdError GetMetadata(string?, uint, out ChdMetadataEntry?) |
Search metadata by tag + occurrence index (null/empty tag = wildcard). Returns Chderrmetadatanotfound when absent. |
| EnumerateHunks | IEnumerable<byte[]> EnumerateHunks(IProgress<ChdProgress>? = null) |
Yield each decompressed hunk. Buffer reused — copy if needed. Reports progress per hunk. |
| ReadHunkAsync | Task<ChdError> ReadHunkAsync(uint, byte[], CancellationToken = default) |
Async hunk read; cancellable. |
| ReadAsync | Task<ChdError> ReadAsync(ulong, byte[], int, int, CancellationToken = default) |
Async byte range read; cancellable. |
| GenerateCueSheet | string GenerateCueSheet(string) |
Generate CUE sheet for CD CHDs. |
| GenerateGdiDescriptor | string GenerateGdiDescriptor(string[]) |
Generate GDI descriptor for GD-ROM CHDs. |
| ExportToc | string ExportToc() |
Export TOC as human-readable text. |
| ExtractToDirectory | List<string> ExtractToDirectory(string, string, IProgress<ChdProgress>? = null, CancellationToken = default) |
Extract CHD tracks to directory. Returns file paths. Reports progress per hunk; cancellable. |
| OpenAsStream | ChdImageStream OpenAsStream(bool leaveOpen = false) |
Returns a read-only, seekable Stream wrapping the decompressed CHD. Supports Read, ReadAsync, Seek, Position, Length. |
| ConfigureReadAhead | void ConfigureReadAhead(int lookAhead) |
Enable background pre-decompression of the next N hunks. Uses ThreadLocal<ChdCodecState> for thread-safe codec access. |
| FlushReadAhead | void FlushReadAhead() |
Clear stale read-ahead cache entries after seeks. |
| Dispose / DisposeAsync | void Dispose() / ValueTask DisposeAsync() |
Release stream and parent. |
Properties
| Property | Type | Description |
|---|---|---|
Version |
uint |
CHD format version (1–5). |
TotalBytes |
ulong |
Decompressed image size. |
HunkBytes |
uint |
Size of one hunk. |
CacheSize |
int |
Number of decompressed hunks retained by the multi-hunk LRU cache (default 1). Set via ConfigureCache(int). Memory capped at CacheSize * HunkBytes. |
MaxCompressedBlockBytes |
uint |
Max allowed on-disk length of one compressed hunk. Defaults to HunkBytes * 2; a hunk claiming more is rejected with Chderrinvaliddata before allocation (OOM guard). Settable; floors at HunkBytes, set to 0 to reset. |
HunkCount |
uint |
Total number of hunks. |
UnitBytes |
uint |
Unit size for parent block address translation. V5 reads from header; V1-V4 derives from metadata (HDD BPS, CD 2448, or HunkBytes). |
Sha1 |
byte[]? |
Combined SHA1 (image + metadata). |
RawSha1 |
byte[]? |
Raw image data SHA1. |
Md5 |
byte[]? |
Raw image MD5. |
RequiresParent |
bool |
True if differential child. |
IsChild |
bool |
Alias for RequiresParent. |
Tracks |
IReadOnlyList<ChdTrackInfo>? |
CD/GD-ROM track layout. null if not a CD/GD-ROM image. |
IsCd |
bool |
True if CD-ROM track metadata present. |
IsGdRom |
bool |
True if GD-ROM (Sega Dreamcast) image. |
IsLittleEndianAudio |
bool |
True for legacy GD-ROMs (CHGT tag / CD_FLAG_GDROMLE) whose CDDA audio tracks are stored little-endian. AUDIO tracks are byte-swapped during extraction. |
IsDvd |
bool |
True if DVD metadata present. |
IsHdd |
bool |
True if hard disk geometry metadata present. |
IdentData |
byte[]? |
ATA IDENTIFY DEVICE data (512 bytes) from IDNT metadata. null if not present. |
KeyData |
byte[]? |
Encryption key data from KEY metadata. null if not present. |
PcmciaCisData |
byte[]? |
PCMCIA Card Information Structure from CIS metadata. null if not present. |
Metadata |
IReadOnlyList<ChdMetadataEntry> |
CHD metadata entries (game name, disc type, etc.). Lazy-loaded. V1/V2 files include a synthesized GDDD entry. |
ChdMetadataEntry — Metadata record
| Property | Type | Description |
|---|---|---|
Tag |
string |
4-char tag (e.g. "GAME", "DISC", "HARD"). |
Data |
byte[] |
Raw metadata bytes. |
Flags |
byte |
Entry flags from the header (bit 0 = checksummed). |
IsText |
bool |
True if data is printable ASCII. |
GetText() |
string |
ASCII text representation. |
ToString() |
string |
Human-readable: GAME: gauntlet. |
ChdTrackInfo — Track record
| Property | Type | Description |
|---|---|---|
TrackNumber |
int |
1-based track number. |
TrackType |
ChdTrackType |
CD track data type (Mode1, Audio, etc.). |
SubType |
ChdSubType |
Subcode type for this track. |
DataSize |
int |
Bytes per sector (2048, 2352, etc.). |
SubSize |
int |
Subcode bytes per sector (0 or 96). |
Frames |
int |
Number of frames in this track. |
ExtraFrames |
int |
Padding frames for 4-frame alignment. |
PreGap |
int |
Pregap frames (index 00 to index 01). |
PostGap |
int |
Postgap frames. |
PreGapType |
ChdTrackType |
Track type of pregap sectors. |
PreGapSubType |
ChdSubType |
Subcode type of pregap sectors. |
PreGapDataSize |
int |
Bytes per sector for pregap data. |
PreGapSubSize |
int |
Subcode bytes per sector for pregap. |
PadFrames |
int |
GD-ROM pad frames (GD-ROM only). |
StartFrame |
ulong |
CHD frame offset where this track starts. |
GetTypeString() |
string |
e.g. "MODE1/2048", "AUDIO". |
GetSubTypeString() |
string |
e.g. "RW", "RW_RAW", "NONE". |
CdRomAddress — MSF ↔ LBA conversion (static)
CHDSharp.Utils namespace. MSF values are BCD-encoded (as found in CD sector headers): 0x02 = 2 minutes, 0x10 = 10 minutes. LBA 0 = MSF 00:02:00; LbaToMsfAlt/MsfToLbaAlt omit the 150-frame lead-in (Sega CD / PC Engine addressing).
| Member | Signature | Description |
|---|---|---|
MsfToLba |
int MsfToLba(byte m, byte s, byte f) |
BCD MSF → LBA ((m*60 + s)*75 + f - 150). Negative for addresses before 00:02:00. |
MsfToLbaAlt |
int MsfToLbaAlt(byte m, byte s, byte f) |
BCD MSF → absolute frame count (no lead-in offset). |
LbaToMsf |
(byte m, byte s, byte f) LbaToMsf(int lba) |
LBA → BCD MSF (adds the 150-frame lead-in). |
LbaToMsfAlt |
(byte m, byte s, byte f) LbaToMsfAlt(int lba) |
Frame count → BCD MSF (no lead-in offset). |
FramesPerSecond / SecondsPerMinute / PregapFrames |
const int |
75 / 60 / 150. |
Invalid BCD nibbles and positions past the 99-minute BCD limit throw ArgumentOutOfRangeException.
ChdError.GetMessage() — Extension method
ChdError err = ChdFile.Open("bad.chd", out _);
Console.WriteLine(err.GetMessage());
// "File not found"
ChdEncoder — Static encoder class
CHDSharp.Encoder namespace. All methods produce CHD v5 files with byte-identical output to MAME chdman.
| Method | Signature | Description |
|---|---|---|
| EncodeRaw | void EncodeRaw(string input, string output, string? codecTags = null, int? hunkBytes = null, int? unitBytes = null, ChdEncodeOptions? options = null) |
Create CHD from a raw binary file. Default codec: zlib. Auto hunk/unit sizes if omitted. |
| EncodeCd | void EncodeCd(string input, string output, string? codecTags = null, ChdEncodeOptions? options = null) |
Create CD CHD from CUE, GDI, ISO, TOC, or NRG. Handles audio byte-swap, 4-frame padding, CHT2 metadata. |
| CreateBlank | void CreateBlank(string output, ulong totalBytes, string? codecTags = null, ChdEncodeOptions? options = null) |
Create a zero-filled HD CHD with auto-derived CHS geometry. |
| CreateBlankWithChs | void CreateBlankWithChs(string output, uint cylinders, uint heads, uint sectors, uint sectorSize = 512, string? codecTags = null, ChdEncodeOptions? options = null) |
Create a zero-filled HD CHD with explicit CHS geometry. |
| Copy | void Copy(string input, string output, string? codecTags = null, ChdEncodeOptions? options = null) |
Re-compress an existing CHD. Clones all metadata. Upgrades legacy CD/GD tags unless NoMetadataUpgrade is set. |
ChdEncodeOptions — Encoder options
| Property | Type | Default | Description |
|---|---|---|---|
TaskCount |
int |
8 | Parallel compression workers (1–64). |
ParentPath |
string? |
null | Parent CHD path for delta encoding. |
NoMetadataUpgrade |
bool |
false | Preserve legacy CHCD/CHTR/CHGT tags during copy. |
HunkCompleted |
Action<HunkProgress>? |
null | Per-hunk progress callback with codec name, sizes, and ratio. |
ChdCodecs — Codec tag helper
| Method | Description |
|---|---|
ParseCodecTags(string) |
Parse comma-separated codec names (e.g. "zlib,zstd,lzma") into tag array. |
CodecTags.None |
Uncompressed. |
CodecTags.Zlib / .Zstd / .Lzma / .Huff / .Flac |
Individual codec tags. |
Supported Formats
CHD Versions
| Version | Header | Map Type | Status |
|---|---|---|---|
| V1 | 76 bytes | Self-hunk dedup via offset | ✅ |
| V2 | 80 bytes | Self-hunk dedup via offset | ✅ |
| V3 | 120 bytes | CRC32 map, self-hunk | ✅ |
| V4 | 108 bytes | CRC32 map, parent chain | ✅ |
| V5 | 124 bytes | CRC16 map, compressed/uncompressed map, RLE, parent/unit chain | ✅ |
Compression Codecs
| Codec | FourCC | CD Variant | Implementation |
|---|---|---|---|
| Zlib (Deflate) | zlib |
cdzl |
System.IO.Compression (managed) |
| LZMA | lzma |
cdlz |
Custom pure C# LZMA decoder |
| Huffman | huff |
— | Custom pure C# Huffman decoder |
| FLAC | flac |
cdfl |
Custom pure C# FLAC decoder (16-bit stereo/mono) |
| Zstd | zstd |
cdzs |
VendoredZSTD (in-repo pure C# port of zstd 1.5.5) |
| AVHuff | avhu |
— | Custom pure C# AV Huffman decoder |
Common Usage Patterns
Pattern 1: Fast batch verification
var files = Directory.GetFiles(@"D:\CHD", "*.chd");
foreach (var path in files)
{
using var s = File.OpenRead(path);
var result = Chd.CheckFile(s, Path.GetFileName(path), deepCheck: true);
Console.WriteLine($"{Path.GetFileName(path)}: {result.Error.GetMessage()}");
}
Pattern 2: Universal verification (standalone or child)
static ChdError UniversalVerify(string path, string? parentPath = null)
{
if (parentPath != null)
{
var r = Chd.CheckFileWithParent(path, parentPath);
return r.Error;
}
using var s = File.OpenRead(path);
var result = Chd.CheckFile(s, Path.GetFileName(path), deepCheck: true);
if (result.Error == ChdError.Chderrrequiresparent)
Console.WriteLine(" -> requires parent CHD");
return result.Error;
}
Pattern 3: Working with child (differential) CHDs
// Option A: Let the library manage parent lifetime
ChdFile.Open("child.chd", "parent.chd", out var child);
child?.Dispose();
// Option B: Share parent across multiple children
ChdFile.Open("parent.chd", out var parent);
using (parent)
{
foreach (var childPath in new[] { "child1.chd", "child2.chd" })
{
ChdFile.Open(childPath, parent, out var c);
using (c) { /* read hunks */ }
}
}
Pattern 4: Computing SHA1 while streaming
using var sha1 = System.Security.Cryptography.SHA1.Create();
ChdFile.Open("game.chd", out var chd);
using (chd)
{
var buf = new byte[chd.HunkBytes];
var remaining = chd.TotalBytes;
ulong offset = 0;
while (remaining > 0)
{
var chunk = (int)Math.Min((ulong)buf.Length, remaining);
chd.Read(offset, buf, 0, chunk);
sha1.TransformBlock(buf, 0, chunk, null, 0);
offset += (ulong)chunk;
remaining -= (ulong)chunk;
}
sha1.TransformFinalBlock([], 0, 0);
Console.WriteLine($"SHA1: {Convert.ToHexString(sha1.Hash!).ToLower()}");
}
Performance
| Scenario | Throughput | Notes |
|---|---|---|
CheckFile(deepCheck: true) |
~200–400 MB/s | 8 parallel threads, bounded memory |
CheckFile(deepCheck: false) |
> 1 GB/s | Header-only |
ChdFile.Read() sequential |
~150–300 MB/s | Single-threaded, hunk-cached |
ChdFile.ReadHunk() random |
~50–150 MB/s | Per-hunk re-decompression |
Tuning parallelism
Chd.TaskCount = 16; // set before calling CheckFile
var result = Chd.CheckFile(s, name, deepCheck: true);
Architecture
┌────────────────────────────────────────────────────┐
│ Public API │
│ Chd.CheckFile() ChdFile.Open() ChdFile.Read() │
│ ChdEncoder.EncodeRaw/Copy/CreateBlank/EncodeCd │
├────────────────────────────────────────────────────┤
│ CHDHeaders → Parse V1–V5 headers + maps │
│ CHDBlockRead → Dispatch hunk → codec delegate │
│ CHDReaders → Decompression delegates (10) │
│ CHDCodec → Per-codec reusable state │
│ CHDMetaData → Metadata traversal + SHA1 check │
├────────────────────────────────────────────────────┤
│ Encoder/ │
│ ChdEncoder · HunkProcessor · MapCompressor · │
│ ParentMap · ChdCodec · MetadataWriter · │
│ CdImageParser · CueParser · GdiParser · NrgParser │
├────────────────────────────────────────────────────┤
│ Utils/ │
│ CRC · CRC16 · BitStream · HuffmanDecoder · │
│ HuffmanDecoderRLE · BigEndian · ArrayPool · cdRom │
├────────────────────────────────────────────────────┤
│ LZMA/ │
│ LzmaStream · LzmaDecoder · RangeCoder · │
│ LzBinTree · LzInWindow · LzOutWindow │
├────────────────────────────────────────────────────┤
│ Flac/ │
│ AudioDecoder · FlacFrame · FlacSubframe · │
│ BitReader · LPC · RiceContext · WindowFunction │
├────────────────────────────────────────────────────┤
│ VendoredZSTD (in-repo pure C# zstd 1.5.5 port) │
└────────────────────────────────────────────────────┘
Building
dotnet build CHDSharpLib/CHDSharpLib.csproj -c Release
dotnet pack CHDSharpLib/CHDSharpLib.csproj -c Release
Dependencies
| Package | Version | Purpose |
|---|---|---|
| Microsoft.Extensions.Logging.Abstractions | 10.0.11 (all TFMs: net8.0 / net9.0 / net10.0) | Pluggable logging (optional) |
All codec implementations are vendored in-repo as project references (no external runtime NuGet dependencies):
| Project | Purpose |
|---|---|
VendoredZLib |
Pure C# zlib (deflate/inflate) |
VendoredLZMA |
Pure C# LZMA SDK port |
VendoredFlac |
Pure C# FLAC encoder/decoder |
VendoredZSTD |
Pure C# zstd 1.5.5 encoder/decoder (C-to-C# port of MAME's bundled tree) |
Limits
- Not thread-safe per instance —
ChdFileinstances must be used from a single thread. UseReadHunkConcurrentor separate instances for parallel work. - No lossy video — Lossy AVHuff video variants are not supported
- Stream must be seekable — for
ChdFile.Openstream overloads - V6+ not supported — MAME has not released a V6 format
License
This is a combined work: the project code is MIT; VendoredFlac is LGPL-2.1; VendoredZLib is zlib-licensed; VendoredLZMA is public domain; VendoredZSTD is MIT (based on Facebook zstd, BSD-3-Clause). See LICENSE.txt for the full third-party notice and obligations.
Acknowledgments
- Gordon Jefferyes — original C# CHDSharp implementation
- MAME — CHD format specification and
chdmanreference - libchdr — C reference library by Romain Tisseraud
- ZstdSharp — original pure C# Zstd port by Oleg Stepanischev, vendored in-repo as
VendoredZSTD(ZstdSharp 0.7.6 = libzstd 1.5.5 source)
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.13)
- System.IO.Hashing (>= 10.0.13)
-
net8.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.13)
- System.IO.Hashing (>= 10.0.13)
-
net9.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.13)
- System.IO.Hashing (>= 10.0.13)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on CHDSharp:
| Package | Downloads |
|---|---|
|
VideoGameFileSystemParser
Video game file system parsing library for console disc images. Supports ISO 9660, UDF, XDVDFS, OperaFS, CD-i, and other formats through CHD, ISO and raw sector readers. |
|
|
RetroAchievementsSharp
A native C# port of the RetroAchievements hashing engine (rcheevos/RAHasher, MIT) that produces 100% identical hashes to the original for every supported console. Supports cartridge, disc (.cue/.bin/.iso/.gdi/.chd/.rvz/.wia), zip, m3u, Arcade, DOS and 3DS inputs. |
GitHub repositories
This package is not used by any popular GitHub repositories.
v1.5.1: fixes two cue-layout divergences from chdman. A single-bin multisession cue whose REM LEAD-OUT sits at or before the session's last track (REM LEAD-OUT 00:00:00 is common in real rips) no longer trims that track to a negative length and abort: the frame comparison is now unsigned, exactly like MAME's uint32_t vs int32_t comparison, so chdman's layout is reproduced byte for byte. The CD reader also mirrors chdman's unsigned pad offset, so a pad larger than its track no longer zeroes the track (it reads the source data, as chdman does), including the GD-ROM data/pad/split ordering. Verified byte-identical to chdman createcd on the three reported failure shapes; encoder suite 454/454 and 4214/4214 library tests. Pure C# - zero native dependencies. Targets net8.0/net9.0/net10.0.