CHDSharp 1.4.3

dotnet add package CHDSharp --version 1.4.3
                    
NuGet\Install-Package CHDSharp -Version 1.4.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CHDSharp" Version="1.4.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CHDSharp" Version="1.4.3" />
                    
Directory.Packages.props
<PackageReference Include="CHDSharp" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CHDSharp --version 1.4.3
                    
#r "nuget: CHDSharp, 1.4.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CHDSharp@1.4.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CHDSharp&version=1.4.3
                    
Install as a Cake Addin
#tool nuget:?package=CHDSharp&version=1.4.3
                    
Install as a Cake Tool

.NET NuGet

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.4.3

Consolidates all changes since v1.4.2 - the last chdman byte-parity gaps are closed:

  • Partial-tail raw-encode fix - EncodeRaw on 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 cosf tukey-window rounding. The vendored FLAC encode is now byte-identical to chdman on 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 - CHDSharpTest 12,648/12,648 (1,308/1,308 per-TFM aggregate), CHDSharpBattleTest 3174/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 -i on non-hunk-aligned inputs now produce byte-identical files and hashes.
  • createhd size / CHS quirks — size parsing matches chdman sscanf("%I64u") (leading digits only: "512K" = 512 bytes) and sub-geometry sizes round up to the guessed-CHS product; new ChdEncodeOptions.LogicalLengthBytes override.
  • Laserdisc AVI byte paritycreateld/extractld output is byte-identical to chdman.
  • DVD metadata correctionDVD payload is exactly one NUL byte (length 1) per chd.h:351 std::string length()+1 behaviour (corrects the v1.4.1 note).
  • Strict CLI validationinfo/verify now validate the full option list (duplicate -i detection, per-command sets) and return chdman-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 / createraw paritycreatehd -i now synthesizes GDDD geometry (51-byte delta gone), createhd --ident extracts CHS from ATA bytes 2/6/12, blank images enforce -c none, createraw validates --unitsize / hunk size (16 B–1 MiB, multiple of unit), and listtemplates shows all 17 templates. Parent/ident fallback and guess_chs now match chdman.
  • extractcd cooked vs rawExtractToDirectory(..., cooked: true) writes cooked sectors (track.DataSize, subcode omitted, audio byte-swapped) and matches chdman extractcd for all 43 CDs + 3 GD-ROMs; cooked: false (default) keeps 2448-byte raw frames with library compat. CLI extractcd defaults to cooked (--raw to keep raw). Extraction now uses a 32 MiB aligned buffer with batch writes and correct per-mode audio swap (CUEBIN always, GDI only Version>4).
  • GD-ROM Redumphas_physical_pregap / padframes / splitframes fixup ported; ChdTrackInfo gains SplitFrames/PhysFrameOfs, REM SINGLE-DENSITY / HIGH-DENSITY emitted, MODE_GDI vs MODE_CUEBIN handled per chdman.
  • copy and parent handlingcopy now picks per-type defaults (get_compression_defaults:2426), parent hunk-size inheritance and factor check (hunk % input && input % hunk) for createraw/createhd/createcd/createdvd/copy, and info --verbose per-codec SELF/PARENT/MINI stats with 0.5 s throttling.
  • DVD and metadataDVD tag is now empty (length 0) per chdman, addmeta --valuetext no trailing NUL, and extractcd --outputbin requires %t when is_splitbin.
  • CLI strictness — unknown/duplicate/missing-parameter errors, per-command valid sets, isb/ish/ib/ih mutual exclusion and 10MB trailing-B handling now match chdman verbatim.
  • Docs — new docs/chd-deep-reference.md audited against MAME 0.289 with 9 ⚠ Corrections; docs/chd-format.md fixed (RLE, 0.289). Meziantou.Analyzer 3.0.190, zero warnings. Targets net8.0 / net9.0 / net10.0 — pure C#, no native dependencies.

What's New in v1.4.0

  • Byte-for-byte parity with chdman for every codec — including createld (AVHuff), cdzs (CD Zstd), and zstd. The encoder's output is now byte-identical to MAME for all writable codecs.
  • VendoredZSTD replaces ZstdSharp.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 optional Microsoft.Extensions.Logging.Abstractions logging 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, MemoryMappedFile disposal on view failure, and LzmaStream.Seek position 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/writingChdEncoder is now part of CHDSharpLib. 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 MAME chdman. No separate package needed.
  • Hard disk ident metadata (IDNT) — Read/write IDNT metadata (ATA IDENTIFY DEVICE response, 512 bytes) preserving original drive model, serial, CHS geometry, and firmware revision. Access via ChdFile.IdentData property. --ident <path> flag on createhd CLI. Automatically preserved during ChdEncoder.Copy().
  • Hard disk encryption key metadata — Read/write KEY metadata (encryption key) used by OG Xbox and other platforms with encrypted HDD contents. Access via ChdFile.KeyData property. Automatically preserved during ChdEncoder.Copy().
  • PCMCIA CIS metadata — Read/write CIS metadata (Card Information Structure) used by PC Engine CD and other PCMCIA platforms. Access via ChdFile.PcmciaCisData property. Automatically preserved during ChdEncoder.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 == 0 guards in the inflate state machine (Len, Dist, CodeLens states) and InflateFast (dolen, dodist paths). When a Huffman table entry has bits=0, the decoder transitions to Bad mode and returns Z_DATA_ERROR instead of looping indefinitely (libchdr #168, miniz 3.1.2 fix parity). ZstdSharp audited — uses a different format not susceptible to this bug.
  • ChdImageStream — seekable Stream over decompressed imageChdFile.OpenAsStream() returns a read-only, seekable Stream wrapping the decompressed CHD. Supports Read, ReadAsync, Seek, Position, and Length. Dispose disposes the parent ChdFile by default. Available via filename, filename+parent, or from an existing ChdFile instance.
  • Span<byte> read overloadsReadHunk(uint, Span<byte>) and Read(ulong, Span<byte>, int) enable callers to use stackalloc, ArrayPool, or pinned memory without allocating a temporary byte[]. Internally reuses the existing hunk cache. ChdImageStream.Read(Span<byte>) now uses the span path directly (zero-copy on .NET 7+).
  • Threaded read-ahead decompressionChdFile.ConfigureReadAhead(int lookAhead) enables background pre-decompression of upcoming hunks. After each ReadHunk, the next N hunks are decompressed in the background using ReadHunkConcurrent. Results are cached in a ConcurrentDictionary (L2) checked before the LRU cache (L1). Uses SemaphoreSlim to cap concurrency and ThreadLocal<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 a ParentResolver callback that resolves the parent by SHA1/MD5 hash on first read. The resolved parent is cached. Also available on Chd.CheckFileWithParent.
  • CD/GD-ROM track (TOC) parsing — Full track layout via Tracks property backed by ChdTocParser, exposing ChdTrackInfo with track type, sector sizes, pregap/postgap, and GD-ROM support. Legacy GD-ROMs (CHGT / CD_FLAG_GDROMLE) are detected via IsLittleEndianAudio and their AUDIO tracks byte-swapped during extraction. Includes GenerateCueSheet(), GenerateGdiDescriptor(), ExportToc(), ExtractToDirectory().
  • LBA/MSF sector readsChdFile.ReadSector(lba), ReadSectorMsf(m, s, f), and ReadFrame(lba) read CD/GD-ROM sectors or full 2448-byte frames by logical block address, mapped through the track table (pregap-aware). CdRomAddress converts between BCD MSF and LBA (with and without the 150-frame lead-in offset).
  • UnitBytes property — 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 enumsChdTrackType (matches MAME cdrom.h: Mode1, Mode2, Audio, etc.) and ChdSubType (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 instanceChdFile instances must be used from a single thread. Use ReadHunkConcurrent or separate instances for parallel work.
  • No lossy video — Lossy AVHuff video variants are not supported
  • Stream must be seekable — for ChdFile.Open stream 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 chdman reference
  • 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.

Version Downloads Last Updated
1.4.3 108 9/2/2026
1.4.2 108 8/29/2026
1.4.1 99 8/28/2026
1.4.0 97 8/26/2026
1.3.0 108 8/23/2026
1.2.0 223 7/23/2026
1.1.0 116 7/18/2026
1.0.0 119 7/17/2026

v1.4.3: consolidates all changes since v1.4.2 - closes the last chdman byte-parity gaps (stale work-buffer tail + LZMA match-finder insert, stale-ring corruption on partial-tail raw encodes of 513+ hunks, vendored FLAC byte parity via fixed-predictor analysis window + native cosf tukey window; byte-identical to chdman on the 7.8 GB XGD2 verification image), detailed decompression-failure diagnostics (hunk index, codec, CRC/truncation reasons), chdman-style progress + speed meter, single merged battle harness (3174/3174 vs MAME 0.289). Pure C# - zero native dependencies. Targets net8.0/net9.0/net10.0.