Skip to the content.

5. Conversion Pipeline (Technical)

This page is a deep dive into the conversion machinery. All references are to BatchConvertToCHD/MainWindow.xaml.cs unless noted.


5.1 Entry Point & Batch Orchestration

StartConversionButton_ClickAsync (:1025) validates both folder paths (PathUtils.ValidateAndNormalizePath), reads the option flags, renews the cancellation token source, disables the UI, and calls PerformBatchConversionAsync (:1292).

PerformBatchConversionAsync does, in order:

  1. Executable validationValidateExecutableAccessAsync (:345): file exists, is .exe, is not locked exclusively, and (when not running as admin) is not read-only. ValidateChdmanCompatibilityAsync (:424) runs chdman help and gives specific guidance for old-Windows “not a valid application” errors (Win32 error 193) and access-denied (error 5).
  2. Sorting — when “process smaller files first” is set, files are ordered by ascending size (:1300–1313).
  3. Disk space checkCheckDiskSpace (:3292): warns when the output drive’s free space is below 50 % of the total input size for conversion (100 % for extraction), and separately checks the temp drive when it differs from the output drive.
  4. Per-file loopProcessSingleFileForConversionAsync (:1395), with Interlocked ok/failed counters and progress/speed updates.

5.2 Per-File Routing

ProcessSingleFileForConversionAsync decides the output path (source subfolder structure preserved via PathUtils.GetSafeRelativePath + SanitizeFileName), then routes in two stages: content inspection first, extension dispatch second.

Stage 1 — content resolution

TryResolveByContentAsync reads the input’s leading bytes and returns one of three answers:

The order matters:

  1. Text descriptors (.cue, .gdi, .toc, .ccd, .mds) are skipped — they have their own handlers and there is nothing to sniff.
  2. An archive extension whose content really is an archive returns null (the archive handler owns it).
  3. SplitImageJoiner.TryGetVolumeSet — a .001/.i00 first volume is rejoined via ResolveSplitVolumeSetAsync, then classified.
  4. DiscImageSignature.Detect decides the rest: EcmResolveEcmAsync, IszResolveIszAsync, Chd → skip (“this file is already a CHD”).
  5. An extension promising a container (.zip/.7z/.rar/.isz) whose content is a plain image → ResolveMislabelledContainerAsync.

ClassifyRecoveredImageAsync is the shared tail for anything recovered into a temp directory (joined from parts, decoded from ECM, decompressed from ISZ): raw CD sectors get a generated cue and createcd; a whole number of 2048-byte sectors converts as a DVD image; anything else is skipped with a reason naming the likely cause.

Stage 2 — extension dispatch

Extension Handler What it does
.cso ProcessCsoFileForConversionAsync _archiveService.ExtractCsoAsync decompresses to a temp .iso, then converts.
.zip/.7z/.rar ProcessArchiveFileForConversionAsync Extracts to a temp dir; drops raw images already covered by a descriptor in the archive (InputFileFilter.RemoveCompanionDataFilesAsync); maps auto-cue outputs; validates cue/gdi/toc dependencies; converts each supported file. .ccd goes through CCDSharp, .mds through the Alcohol parser, .isz through ConvertIszViaImageAsync.
.pbp ProcessPbpFileForConversionAsync ExtractPbpToCueBinAsync via PBPSharp; PbpError.InvalidPsarHeader → informational skip; converts each disc cue.
.ccd ProcessCcdFileForConversionAsync CcdConverter.Parse + ConvertToCueBin into a temp dir, then converts the cue.
.mds ProcessMdsFileForConversionAsync MdsParser + MdsInputPreparer produce a cue (or a stripped image, or a DVD image), then convert.
everything else direct path TryStageCueForRawImageAsync may generate a cue for a raw CD image, then ValidateDependentFilesAsyncTryDirectConversionAsync.

Missing input file: logs “File not found, skipping:” and asks FileWatcherService.GetContextForMissingFile for a diagnostic (deleted / renamed / created-then-gone / outside watch / drive disconnected).

Converting in place

The source and output folders may be the same, and the output may sit inside the source tree. Three properties make that safe for conversion:

PathUtils.IsSameOrInsideDirectory detects the situation and the log notes it once. Extraction is the tab where in-place needs care, because its output shares the source’s base name — see Extraction & Verification.

Batch preflight

Before the per-file loop, WarnAboutOutputCollisions reports inputs that would map to the same output .chd (the name comes from the input’s base name alone, so Game.cue, Game.zip and Game.ccd in one folder all target Game.chd). This is a warning, not a block — but combined with the staging file in §5.3 it means the second conversion can no longer destroy the first one’s output.

Retry-via-temp-copy fallback

If the direct conversion of the original file fails, TryRetryConversionViaTempCopyAsync (:1863) copies the input (plus referenced files for cue/gdi/toc via GameFileParser) into a temp directory and converts there. This handles network paths and file-locking quirks. It also:

Result handling

HandleConversionResultAsync: on success, optionally deletes originals (see below) and prunes now-empty subfolders (TryDeleteEmptySubfolderAsync). On failure the destination is left alone — a failure says nothing about the CHD already sitting at that path, which may be a good conversion from another input. Temp dirs are always cleaned in a finally block.

Why the destination is no longer deleted on failure. chdman is invoked with -f and truncates its output file before it can fail, so a second input mapping to the same output name used to wipe out a working CHD. Conversions now write to a <name>.<8hex>.chdtmp staging file and are moved into place only after success (see §5.3), and the old unconditional “delete the partial output” calls were removed.

Deleting originalsDeleteOriginalGameFilesAsync (:3134): for .cue/.gdi/.toc it also deletes every referenced data file (GameFileParser); for .ccd it deletes the .img/.sub/.cdt companions. All deletions go through RetryingFileOperations.TryDeleteAsync via TryDeleteFileAsync (:3362), which additionally kills stray chdman processes after the second failed attempt (KillChdmanProcesses, :3380).

5.3 ConvertToChdAsync — the chdman Wrapper

ConvertToChdAsync is the single funnel for every chdman invocation.

Output staging

The conversion writes to <name>.<8hex>.chdtmp next to the destination and moves it into place only after chdman reports success. chdman ignores the output file’s extension — verified: createraw -o out.chdtmp exits 0 and writes a valid CHD — and keeping the staging name off .chd also means a leftover staging file is never mistaken for a finished CHD by the verification and extraction tabs.

Command & argument selection

command = forceCd || hasCue || (!forceDvd && !isIso && !isImg && !isRaw) ? "createcd"
        : forceDvd || isIso                                          ? "createdvd"
        : isImg                                                      ? "createhd"
        :                                                              "createraw";

Pre-flight validations

  1. Sector-size warning for DVD: IsoSectorValidator.GetSectorSizeWarning flags sizes not divisible by 2352/2048/2336/2324/2448/2368, but conversion proceeds — the hard gate is the post-failure check (some legitimate images use non-standard layouts).
  2. Cue work-dir preparation for .cue/.toc (:2503–2527): PrepareCueWorkDirAsync (:2417) → CueWorkDirectory.PrepareAsync (see Utilities). If MP3 tracks exist and decoding failed, conversion is aborted with a clear message instead of handing chdman an MP3 cue.
  3. ASCII temp work dir (:2508–2546): if the input or output filename contains non-ASCII characters, the input is copied into a GUID-named temp dir and the output is written there too; after success the output is moved to the real destination with RetryingFileOperations.TryMoveAsync.

Process execution

Exit-code handling

5.4 Cue Normalization & Work Directories

Two cooperating mechanisms ensure chdman never sees malformed cues:

  1. CueNormalizer.NormalizeAsync — detects the file encoding (BOMs → strict UTF-8 → legacy codepages scored by resolvable references), strips BOMs, unquotes/rewrites FILE lines, resolves references case-insensitively and with zero-padding tolerance ((Track 2)(Track 02)), and produces a canonical UTF-8 (no BOM) CRLF cue.
  2. CueWorkDirectory.PrepareAsync — when the cue needs rewriting (BOM, non-UTF-8, non-ASCII names, MP3 tracks, corrected names), builds an isolated ASCII work directory with the canonical cue and every referenced file under safe trackNN.ext names; MP3 tracks are decoded to WAV. BOM-only cues with ASCII names use an in-place fast path: a game.cue referencing bins via relative paths, avoiding multi-hundred-MB copies.

Details in Utilities Reference.

5.5 Exception Classifiers

Centralized classification used across the pipeline (:3227–3290):

Helper Matches
IsCancellationException OperationCanceledException
IsDiskSpaceException IOException HResult -2147024784 (ERROR_DISK_FULL) or -2147024783 (ERROR_SEM_TIMEOUT)
IsCrcErrorException IOException HResult -2147024809 (ERROR_CRC) or message containing “cyclic redundancy check”/”data error”
IsCorruptionException InvalidDataException, IndexOutOfRangeException, NullReferenceException, CryptographicException, or SharpCompress archive-corruption types (IncompleteArchive, ArchiveOperation, InvalidFormat, LZMA DataError)
IsDiskSpaceError (string) chdman output keywords listed above

5.6 Archive Processing (Summary)

See Services Reference → ArchiveService for the full extraction semantics. Highlights relevant to the pipeline:

Still unhandled: an .ecm inside an archive. .ecm is not an archive primary target, so an archive containing only .ecm files reports No supported primary files found in archive. .isz is a primary target and shows the pattern to follow.


5.7 Recovered-Image Formats

These all converge on ClassifyRecoveredImageAsync (§5.2) once the image has been reconstructed.

Split volume sets — SplitImageJoiner

.001/.002… and .i00/.i01… sets are concatenated into one temp file. Only the first volume is a registered input, so a set is offered once rather than once per piece. A multi-part archive is detected and refused separately, with instructions, since that needs different tooling. A set whose parts do not join to a whole number of sectors is reported as needing re-download rather than converted.

ISZ — Utilities/Isz/

Written against EZB Systems’ ISZ File Format Specification 1.00. IszHeader parses the packed 48-byte header; IszDecoder walks the chunk table, splitting each entry’s top two bits into the storage kind (ADI_ZERO, ADI_DATA, ADI_ZLIB, ADI_BZ2) and the remainder into the stored length, then decompresses through ZLibStream and SharpCompress’s BZip2Stream.

Note that .isz covers two unrelated things in practice: a real ISZ starts with IsZ! and is decompressed, while ordinary images also get renamed to .isz and are routed by content like any other mislabelled file.

ECM — Utilities/Ecm/

ECM shrinks a raw CD image by discarding each sector’s EDC checksum and Reed-Solomon parity, which are derivable from the user data, and recording only what kind of sector each one was. EcmImageDecoder parses the block stream (literal, Mode 1, Mode 2 Form 1, Mode 2 Form 2) and CdSectorEccEdc regenerates the discarded fields.

Alcohol 120% — Utilities/Mds/

MdsParser reads the descriptor’s session and track tables; MdsInputPreparer picks one of three shapes:

Split .i00 data files are joined first. A descriptor whose session count is implausible, or whose track modes cannot be expressed in a cue, is refused with the reason. A .mdf is never converted on its own: the .mds drives everything, which is why only .mds is a registered input.


5.8 Generated Cues and the Same-Volume Constraint

Several paths above generate a cue that references a disc image where it already lies, rather than copying a multi-hundred-megabyte file. That works because chdman resolves a cue’s FILE entry against the cue’s own directory.

The catch, verified against chdman 0.285: chdman joins the FILE string to the cue’s directory unconditionally. An absolute path is therefore looked for at C:\temp\x\D:\game.iso and reported as ERROR: couldn't find bin file [...]. A bare file name with the image elsewhere fails the same way.

So a generated cue has to sit on the same volume as its image. StageCueForImageAsync uses PathUtils.CreateTempDirectoryOnSameVolume for this, not GetBestTempDirectory — the latter deliberately picks the roomiest drive, which is right for writing a whole image and wrong for a few hundred bytes of cue with a hard placement constraint. When no writable location exists on the image’s volume the image is converted as-is with a warning.

The .mds path has the same constraint and resolves it differently: it falls back to copying the .mdf into the work directory, which is correct but slow.