Skip to content

03 architecture

github-actions[bot] edited this page Oct 16, 2026 · 22 revisions

3. Architecture

This page describes the solution structure, the runtime startup sequence, and the high-level data flow of the application. Detailed deep dives live in Conversion Pipeline, Extraction & Verification, Services Reference, and Utilities Reference.


3.1 Solution Structure

CSharp_CHDStudio.sln
├── CHDStudio/                     (Avalonia app, net10.0;net10.0-windows)
│   ├── App.axaml(.cs)                     → startup, Serilog, exception handlers
│   ├── AppConfig.cs                       → central configuration
│   ├── MainWindow.axaml(.cs)              → UI + all batch logic
│   ├── AboutWindow.axaml(.cs)             → about dialog
│   ├── Models/
│   │   ├── FileItem.cs                    → bindable file row (name, size, selected)
│   │   ├── GitHubRelease.cs               → GitHub API release model
│   │   └── PbpExtractionResult.cs         → PBP extraction outcome
│   ├── Services/
│   │   ├── AppHttpClient.cs               → singleton HttpClient (TLS 1.2/1.3)
│   │   ├── ArchiveService.cs              → zip/7z/rar extraction, CSO, 7za fallback
│   │   ├── BugReportApiSink.cs            → Serilog sink → bug API
│   │   ├── BugReportService.cs            → bug report client + exclusion list
│   │   ├── ChdExplorerService.cs          → CHD file-system explorer + Image Info report
│   │   ├── ChdSharpEncoderService.cs      → in-process CHD encoder (CHDSharp library)
│   │   ├── FileEventRecord.cs / FileWatchEventType.cs
│   │   ├── FileWatcherService.cs          → missing-file diagnostics
│   │   ├── LegacyCleanupService.cs        → removes legacy files/folders
│   │   ├── ScreenshotService.cs           → window screenshot capture (RenderTargetBitmap)
│   │   ├── StatsService.cs                → anonymous usage stats
│   │   └── UpdateService.cs               → GitHub update checks
│   └── Utilities/
│       ├── BinCueGenerator.cs             → auto-cue generation for bin-only archives
│       ├── ChdChecksumReport.cs           → whole-image + per-track SHA-1/CRC-32/XXH3 report writer
│       ├── ChdInfoReport.cs               → Explorer Image Info report builder
│       ├── ChdSharpProgressLogger.cs      → 10%-step CHDSharp progress logging
│       ├── CueFileLineTransform.cs / CueFileReference.cs / CueNormalizationResult.cs
│       ├── CueNormalizer.cs               → encoding detection + canonicalization
│       ├── CueWorkDirectory.cs(.Result)   → self-contained ASCII cue work dirs
│       ├── DiscImageKind.cs               → what a file turned out to be
│       ├── DiscImageSignature.cs          → magic-byte content identification
│       ├── FileExtensions.cs              → all extension constants and sets
│       ├── GameFileParser.cs              → cue/gdi/toc referenced-file resolution
│       ├── IMp3Decoder.cs / Mp3ToWavDecoder.cs
│       ├── InputFileFilter.cs             → drops raw images a descriptor already covers
│       ├── IsoSectorValidator.cs          → sector-size alignment checks
│       ├── PathUtils.cs                   → temp dirs, path sanitizing, relative paths
│       ├── RawCdImageDetector.cs          → raw 2352 sector sniffing + cue staging
│       ├── RetryingFileOperations.cs      → retry-with-backoff delete/move
│       ├── TrackBinCueBuilder.cs          → multi-FILE cue for "(Track N)" bin sets
│       └── Ecm/                           → in-process ECM decoding
│           ├── CdSectorEccEdc.cs          → regenerates sector EDC + Reed-Solomon parity
│           ├── EcmImageDecoder.cs         → ECM block-stream decoder
│           └── EcmDecodeResult.cs
├── CHDStudio.Tests/               (xUnit; Fixtures/ holds ecm-sample.ecm, rar-multipart/, MdsV2/ and laserdisc-small.avi)
├── MDSSharp/                                (Alcohol 120% .mds/.mdf parsing; net8.0;net9.0;net10.0)
├── CCDSharp/                                (CloneCD .ccd/.img/.sub parsing; net10.0;net8.0)
├── CSOSharp/                                (CSO/CISO decompression; net10.0;net8.0)
├── PBPSharp/                                (PBP/SFO parsing; net10.0;net8.0)
├── ISZSharp/                                (UltraISO ISZ decompression; net8.0;net9.0;net10.0)
└── References/                              (third-party sources — not part of the build)

Dependency graph

                 ┌──────────────────────────────────┐
                 │         CHDStudio        │  (Avalonia app)
                 └───┬───────┬───────┬───────┬──────┘
      Project refs   │       │       │       │
         ┌───────────▼─┐ ┌───▼──────▼──┐ ┌──▼──────────┐
         │  CCDSharp    │ │  CSOSharp   │ │  PBPSharp   │
         └──────────────┘ └─────────────┘ └─────────────┘
         ┌──────────────────────┐  ┌─────────────────────┐
         │       MDSSharp       │  │       ISZSharp      │
         └──────────────────────┘  └─────────────────────┘
     NuGet: CHDSharp 1.4.5, Avalonia, SharpCompress, NAudio, Serilog
  • The app references MDSSharp, CCDSharp, CSOSharp, PBPSharp and ISZSharp as project references.
  • All five libraries are packable and expose internals to CHDStudio.Tests via InternalsVisibleTo; MDSSharp, CCDSharp, CSOSharp, PBPSharp and ISZSharp all multi-target net8.0;net9.0;net10.0.
  • CHDStudio.Tests references the app (internals visible) plus MDSSharp, CCDSharp, CSOSharp, PBPSharp and ISZSharp; CcdParserTests/CcdModelTests cover the CloneCD parser and writer (see Testing).

Why ISZ and Alcohol support moved into libraries. ISZSharp and MDSSharp were split out of the app's utilities into standalone packable projects (they are self-contained formats with redistributable value), while ECM decoding remains in-app because it is tightly coupled to the cue staging flow. The test project references both new libraries directly.


3.2 Startup Sequence

App ctor
 ├─ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)   ← legacy codepages (CP932/CP949/CP1251...)
 ├─ new BugReportService(...)  → App.SharedBugReportService
 ├─ new StatsService(...)
 ├─ ConfigureSerilog()
 │    ├─ file sink: %LocalAppData%\CHDStudio\logs\CHDStudio-.log (daily, 7 retained)
 │    ├─ debug sink
 │    └─ BugReportApiSink (forwards Warning+ to the bug API)
 └─ subscribe: AppDomain.UnhandledException, Dispatcher.UIThread.UnhandledException,
                TaskScheduler.UnobservedTaskException, Exit

OnStartup
 ├─ acquire global mutex "Global\CHDStudio_SingleInstance" (second instance → exit)
 ├─ ShutdownMode = OnMainWindowClose
 ├─ apply dark Fluent theme (Avalonia)
 ├─ delete legacy 7z_x64.dll / 7z_arm64.dll
 ├─ _statsService.RecordUsageAsync()        (fire-and-forget)
 └─ type preloading on background thread

MainWindow ctor
 ├─ probe chdman/7za (app directory first, then PATH); the CHDSharp encoder is built in
 ├─ construct services (ArchiveService, ScreenshotService, FileWatcherService)
 ├─ wire F8 screenshot hotkey (window KeyDown)
 ├─ InitializeStatusBar
 ├─ after 2 s: CleanupLeftoverTempDirectories + LegacyCleanupService.RunInBackground
 └─ log environment details

MainWindow Loaded
 ├─ create performance counters (write/read speed)
 ├─ apply CLI folder argument if present
 ├─ CheckDependenciesAndNotifyUser (chdman presence on Windows; built-in CHDSharp always available)
 └─ UpdateService.CheckForNewVersionAsync (background)

Line references: App.axaml.cs:35–145, MainWindow.axaml.cs:87–172.


3.3 Runtime Data Flow — Conversion

User clicks Start Conversion
  └─ StartConversionButton_ClickAsync              (MainWindow.axaml.cs:1275)
       ├─ validate paths (ValidateAndNormalizePath)
       ├─ read options (delete originals, smaller-first, force CD/DVD, timeout)
       ├─ RenewCancellationTokenSource
       ├─ SetControlsState(false)
       └─ PerformBatchConversionAsync              (:1684)
             ├─ encoder preflight (Windows): probe chdman access + compatibility;
             │   continue on the built-in CHDSharp encoder when missing or failing
             │   (on Linux/macOS the built-in encoder is used directly)
             ├─ optional sort by size (smaller first)
             ├─ CheckDiskSpace (free space warnings)
             ├─ InputFileFilter + ResolveOutputCollisions (batch preflight)
             └─ per file: ProcessSingleFileForConversionAsync
                  ├─ missing file? → FileWatcherService diagnostics
                  ├─ TryResolveByContentAsync  ← content before extension
                  │    ├─ split volume set → SplitImageJoiner (MDSSharp) → classify
                  │    ├─ Isz  → ResolveIszAsync   (ISZSharp)       → classify
                  │    ├─ Ecm  → ResolveEcmAsync   (Utilities/Ecm)  → classify
                  │    ├─ Chd  → skip ("already a CHD")
                  │    └─ container extension, plain image inside → generated cue
                  ├─ else route by extension:
                  │    .cso   → ProcessCsoFileForConversionAsync
                  │    archive→ ProcessArchiveFileForConversionAsync
                  │    .pbp   → ProcessPbpFileForConversionAsync (InvalidHeader → content-routed)
                  │    .ccd   → ProcessCcdFileForConversionAsync
                  │    .mds   → ProcessMdsFileForConversionAsync   (MDSSharp)
                  │    other  → TryStageCueForRawImageAsync → direct conversion
                  ├─ ValidateDependentFilesAsync (cue/gdi/toc)
                  ├─ TryDirectConversionAsync
                  │    └─ ConvertToChdAsync  → chdman primary on Windows with the
                  │                             built-in CHDSharp fallback; built-in only
                  │                             on Linux/macOS; writes <name>.<hex>.chdtmp,
                  │                             moves on success
                  ├─ fallback: TryRetryConversionViaTempCopyAsync
                  └─ HandleConversionResultAsync
                       ├─ success → optionally delete originals + prune empty dirs
                       └─ failure → leave the destination alone, keep source

3.4 Runtime Data Flow — Extraction & Verification

Extraction: StartExtractionButton_ClickAsync (:915)
  └─ PerformBatchExtractionAsync (:1761)
       └─ per file: ExtractChdAsync (:4142)
            ├─ pick command: auto-detect via CHD metadata, or explicit CD/DVD/HDD
            ├─ ChdFile.Open (CHDSharp) — corrupt CHD → clear error, continue
            ├─ DVD/HDD → ExtractChdToSingleFile (streamed 4 MB buffer)
            └─ CD/GDI → ExtractChdTracksToDirectory (temp dir → retrying moves)

Verification: StartVerificationButton_ClickAsync (:1413)
  └─ PerformBatchVerificationAsync (:4005)
       └─ per file: VerifyChdAsync (:6004) — CHDSharp Chd.CheckFile
            └─ optional move to Success/Failed via MoveVerifiedFileAsync (:4086)
                 └─ RetryingFileOperations.TryMoveAsync (retries ~45 s on locks)

3.5 Concurrency & Threading Model

  • UI thread: all Avalonia controls; dispatcher invocations are used from worker contexts (Dispatcher.UIThread.Invoke/InvokeAsync, with DispatcherPriority.Background for chunked list loading).
  • Worker threads: Task.Run for file scanning, archive extraction, chdman process orchestration, in-process CHDSharp encoding, screenshot rendering.
  • Cancellation: one CancellationTokenSource per operation, guarded by a Lock (_cts, _ctsLock, MainWindow.axaml.cs:31–32); cancellation is observed at every loop iteration and propagated into chdman via a linked timeout CTS.
  • Chdman process: stdout/stderr are redirected and parsed asynchronously (OutputDataReceived/ErrorDataReceived); the process is killed (process.Kill(true)) on cancellation/timeout, and the app waits 300 ms before temp cleanup so file handles are released.
  • Speed telemetry: PerformanceCounter-based disk write/read rates sampled every second (AppConfig.WriteSpeedUpdateIntervalMs = 1000).
  • Operation state: an interlocked _operationRunningState plus SetControlsState guards the UI against re-entrancy; a _pendingClose flag lets the window close gracefully mid-operation.

3.6 Logging Pipeline

LogMessage / LogWarning / LogError (MainWindow)
   └─ Serilog (Log.Information/Warning/Error)
        ├─ Debug sink
        ├─ File sink   → %LocalAppData%\CHDStudio\logs\CHDStudio-YYYYMMDD.log
        └─ BugReportApiSink → BugReportService.SendBugReportAsync
             (Warning+ only; exclusion patterns drop known-noise; single in-flight send)

See Bug Reporting System for the full contract.

Clone this wiki locally