CHDSharp 1.4.3
dotnet add package CHDSharp --version 1.4.3
NuGet\Install-Package CHDSharp -Version 1.4.3
<PackageReference Include="CHDSharp" Version="1.4.3" />
<PackageVersion Include="CHDSharp" Version="1.4.3" />
<PackageReference Include="CHDSharp" />
paket add CHDSharp --version 1.4.3
#r "nuget: CHDSharp, 1.4.3"
#:package CHDSharp@1.4.3
#addin nuget:?package=CHDSharp&version=1.4.3
#tool nuget:?package=CHDSharp&version=1.4.3
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 -
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.11)
- System.IO.Hashing (>= 10.0.11)
-
net8.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- System.IO.Hashing (>= 10.0.11)
-
net9.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- System.IO.Hashing (>= 10.0.11)
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.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.