diff --git a/.gitmodules b/.gitmodules
index 6687ec1553..44d82333b5 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -77,3 +77,6 @@
path = waterbox/gpgx/Genesis-Plus-GX
url = https://github.com/TASEmulators/Genesis-Plus-GX.git
branch = tasvideos-2.1
+[submodule "ExternalProjects/libchdr/libchdr"]
+ path = ExternalProjects/libchdr/libchdr
+ url = https://github.com/rtissera/libchdr.git
diff --git a/Assets/dll/chdr.dll b/Assets/dll/chdr.dll
new file mode 100644
index 0000000000..88b1449b68
Binary files /dev/null and b/Assets/dll/chdr.dll differ
diff --git a/ExternalProjects/libchdr/.gitignore b/ExternalProjects/libchdr/.gitignore
new file mode 100644
index 0000000000..3722ac63ca
--- /dev/null
+++ b/ExternalProjects/libchdr/.gitignore
@@ -0,0 +1 @@
+build/
diff --git a/ExternalProjects/libchdr/build_release.bat b/ExternalProjects/libchdr/build_release.bat
new file mode 100644
index 0000000000..06625b160a
--- /dev/null
+++ b/ExternalProjects/libchdr/build_release.bat
@@ -0,0 +1,10 @@
+rmdir /s /q build
+mkdir build
+cd build
+:: cl must be used as clang fails to compile :(
+cmake ..\libchdr -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=cl -G Ninja ^
+ -DBUILD_LTO=ON -DBUILD_SHARED_LIBS=ON -DINSTALL_STATIC_LIBS=OFF -DWITH_SYSTEM_ZLIB=OFF
+ninja
+xcopy .\chdr.dll ..\..\..\Assets\dll\ /Y
+xcopy .\chdr.dll ..\..\..\output\dll\ /Y
+cd ..
diff --git a/ExternalProjects/libchdr/build_release.sh b/ExternalProjects/libchdr/build_release.sh
new file mode 100755
index 0000000000..a10465fe4c
--- /dev/null
+++ b/ExternalProjects/libchdr/build_release.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -e
+if [ -z "$CC" ]; then export CC="clang"; fi
+
+rm -rf build
+mkdir build
+cd build
+cmake ../libchdr -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=$CC -G Ninja \
+ -DBUILD_LTO=ON -DBUILD_SHARED_LIBS=ON -DINSTALL_STATIC_LIBS=OFF -DWITH_SYSTEM_ZLIB=OFF
+ninja
+cp -t ../../../Assets/dll/ ./libchdr.so
+cp -t ../../../output/dll/ ./libchdr.so
diff --git a/ExternalProjects/libchdr/libchdr b/ExternalProjects/libchdr/libchdr
new file mode 160000
index 0000000000..5c598c2df3
--- /dev/null
+++ b/ExternalProjects/libchdr/libchdr
@@ -0,0 +1 @@
+Subproject commit 5c598c2df3a7717552a76410d79f5af01ff51b1d
diff --git a/src/BizHawk.Client.Common/RomLoader.cs b/src/BizHawk.Client.Common/RomLoader.cs
index d4411bc1bf..009f11ceb1 100644
--- a/src/BizHawk.Client.Common/RomLoader.cs
+++ b/src/BizHawk.Client.Common/RomLoader.cs
@@ -557,6 +557,20 @@ namespace BizHawk.Client.Common
game = rom.GameInfo;
}
+ // HACK due to MAME wanting CHDs as hard drives / handling it on its own (bad design, I know!)
+ // only matters for XML, as CHDs are never the "main" rom for MAME
+ // (in general, this is kind of bad as CHD hard drives might be useful for other future cores?)
+ private static bool IsDiscForXML(string system, string path)
+ {
+ var ext = Path.GetExtension(path);
+ if (system == VSystemID.Raw.Arcade && ext.ToLowerInvariant() == ".chd")
+ {
+ return false;
+ }
+
+ return Disc.IsValidExtension(ext);
+ }
+
private bool LoadXML(string path, CoreComm nextComm, HawkFile file, string forcedCoreName, out IEmulator nextEmulator, out RomGame rom, out GameInfo game)
{
nextEmulator = null;
@@ -573,7 +587,7 @@ namespace BizHawk.Client.Common
Comm = nextComm,
Game = game,
Roms = xmlGame.Assets
- .Where(kvp => !Disc.IsValidExtension(Path.GetExtension(kvp.Key)))
+ .Where(kvp => !IsDiscForXML(system, kvp.Key))
.Select(kvp => (IRomAsset)new RomAsset
{
RomData = kvp.Value,
@@ -584,7 +598,7 @@ namespace BizHawk.Client.Common
})
.ToList(),
Discs = xmlGame.AssetFullPaths
- .Where(p => Disc.IsValidExtension(Path.GetExtension(p)))
+ .Where(p => IsDiscForXML(system, p))
.Select(discPath => (p: discPath, d: DiscExtensions.CreateAnyType(discPath, str => DoLoadErrorCallback(str, system, LoadErrorType.DiscError))))
.Where(a => a.d != null)
.Select(a => (IDiscAsset)new DiscAsset
diff --git a/src/BizHawk.Client.DiscoHawk/MainDiscoForm.Designer.cs b/src/BizHawk.Client.DiscoHawk/MainDiscoForm.Designer.cs
index 66f924c1fe..02a5c8e994 100644
--- a/src/BizHawk.Client.DiscoHawk/MainDiscoForm.Designer.cs
+++ b/src/BizHawk.Client.DiscoHawk/MainDiscoForm.Designer.cs
@@ -154,7 +154,7 @@
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(235, 33);
this.label3.TabIndex = 7;
- this.label3.Text = "- Uses FFMPEG for audio decoding\r\n- Loads ISO, CUE, CCD, CDI, MDS, and NRG";
+ this.label3.Text = "- Uses FFMPEG for audio decoding\r\n- Loads ISO, CUE, CCD, CDI, CHD, MDS, and NRG";
//
// radioButton2
//
diff --git a/src/BizHawk.Emulation.DiscSystem/Disc.cs b/src/BizHawk.Emulation.DiscSystem/Disc.cs
index 1fbd3b9b30..5ce69c5e7f 100644
--- a/src/BizHawk.Emulation.DiscSystem/Disc.cs
+++ b/src/BizHawk.Emulation.DiscSystem/Disc.cs
@@ -122,6 +122,6 @@ namespace BizHawk.Emulation.DiscSystem
{}
public static bool IsValidExtension(string extension)
- => extension.ToLowerInvariant() is ".ccd" or ".cdi" or ".cue" or ".iso" or ".toc" or ".mds" or ".nrg";
+ => extension.ToLowerInvariant() is ".ccd" or ".cdi" or ".chd" or ".cue" or ".iso" or ".toc" or ".mds" or ".nrg";
}
}
\ No newline at end of file
diff --git a/src/BizHawk.Emulation.DiscSystem/DiscFormats/Blobs/Blob_CHD.cs b/src/BizHawk.Emulation.DiscSystem/DiscFormats/Blobs/Blob_CHD.cs
new file mode 100644
index 0000000000..70232deb68
--- /dev/null
+++ b/src/BizHawk.Emulation.DiscSystem/DiscFormats/Blobs/Blob_CHD.cs
@@ -0,0 +1,64 @@
+using System;
+using System.IO;
+
+namespace BizHawk.Emulation.DiscSystem
+{
+ internal class Blob_CHD : IBlob
+ {
+ private LibChdr.CoreFileStreamWrapper _coreFile;
+ private IntPtr _chdFile;
+
+ private readonly uint _hunkSize;
+ private readonly byte[] _hunkCache;
+ private int _currentHunk;
+
+ public Blob_CHD(LibChdr.CoreFileStreamWrapper coreFile, IntPtr chdFile, uint hunkSize)
+ {
+ _coreFile = coreFile;
+ _chdFile = chdFile;
+ _hunkSize = hunkSize;
+ _hunkCache = new byte[hunkSize];
+ _currentHunk = -1;
+ }
+
+ public void Dispose()
+ {
+ if (_chdFile != IntPtr.Zero)
+ {
+ LibChdr.chd_close(_chdFile);
+ _chdFile = IntPtr.Zero;
+ }
+
+ _coreFile?.Dispose();
+ _coreFile = null;
+ }
+
+ public int Read(long byte_pos, byte[] buffer, int offset, int count)
+ {
+ var ret = count;
+ while (count > 0)
+ {
+ var targetHunk = (uint)(byte_pos / _hunkSize);
+ if (targetHunk != _currentHunk)
+ {
+ var err = LibChdr.chd_read(_chdFile, targetHunk, _hunkCache);
+ if (err != LibChdr.chd_error.CHDERR_NONE)
+ {
+ // shouldn't ever happen in practice, unless something has gone terribly wrong
+ throw new IOException($"CHD read failed with error {err}");
+ }
+
+ _currentHunk = (int)targetHunk;
+ }
+
+ var hunkOffset = (uint)(byte_pos - targetHunk * _hunkSize);
+ var bytesToCopy = Math.Min((int)(_hunkSize - hunkOffset), count);
+ Buffer.BlockCopy(_hunkCache, (int)hunkOffset, buffer, offset, bytesToCopy);
+ offset += bytesToCopy;
+ count -= bytesToCopy;
+ }
+
+ return ret;
+ }
+ }
+}
diff --git a/src/BizHawk.Emulation.DiscSystem/DiscFormats/CHD_format.cs b/src/BizHawk.Emulation.DiscSystem/DiscFormats/CHD_format.cs
new file mode 100644
index 0000000000..4bb2daab4a
--- /dev/null
+++ b/src/BizHawk.Emulation.DiscSystem/DiscFormats/CHD_format.cs
@@ -0,0 +1,793 @@
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using BizHawk.Common;
+using BizHawk.Emulation.DiscSystem.CUE;
+
+#pragma warning disable BHI1005
+
+//MAME CHD images, using the standard libchdr for reading
+
+namespace BizHawk.Emulation.DiscSystem
+{
+ public static class CHD_Format
+ {
+ ///
+ /// Represents a CHD file.
+ /// This isn't particularly faithful to the format, but rather it just wraps libchdr's chd_file
+ ///
+ public class CHDFile
+ {
+ ///
+ /// Wrapper of a C# stream to a chd_core_file
+ ///
+ public LibChdr.CoreFileStreamWrapper CoreFile;
+
+ ///
+ /// chd_file* to be used for libchdr functions
+ ///
+ public IntPtr ChdFile;
+
+ ///
+ /// CHD header, interpreted by libchdr
+ ///
+ public LibChdr.chd_header Header;
+
+ ///
+ /// CHD CD metadata for each track
+ ///
+ public readonly IList CdMetadatas = new List();
+ }
+
+ ///
+ /// Results of chd_get_metadata with cdrom track metadata tags
+ ///
+ public class CHDCdMetadata
+ {
+ ///
+ /// Track number (1..99)
+ ///
+ public uint Track;
+
+ ///
+ /// Indicates this is a CDI format
+ /// chd_track_type doesn't have an explicit enum for this
+ /// However, this is still important info for discerning the session format
+ ///
+ public bool IsCDI;
+
+ ///
+ /// Track type
+ ///
+ public LibChdr.chd_track_type TrackType;
+
+ ///
+ /// Subcode type
+ ///
+ public LibChdr.chd_sub_type SubType;
+
+ ///
+ /// Size of each sector
+ ///
+ public uint SectorSize;
+
+ ///
+ /// Subchannel size
+ ///
+ public uint SubSize;
+
+ ///
+ /// Number of frames in this track
+ /// This might include pregap, if that is stored in the chd
+ ///
+ public uint Frames;
+
+ ///
+ /// Number of "padding" frames in this track
+ /// This is done in order to maintain a multiple of 4 frames for each track
+ /// These padding frames aren't representative of the actual disc anyways
+ /// They're only useful to know the offset of the next track within the chd
+ ///
+ public uint Padding;
+
+ ///
+ /// Number of pregap sectors
+ ///
+ public uint Pregap;
+
+ ///
+ /// Pregap track type
+ ///
+ public LibChdr.chd_track_type PregapTrackType;
+
+ ///
+ /// Pregap subcode type
+ ///
+ public LibChdr.chd_sub_type PregapSubType;
+
+ ///
+ /// Indicates whether pregap is in the CHD
+ /// If pregap isn't in the CHD, it needs to be generated where appropriate
+ ///
+ public bool PregapInChd;
+
+ ///
+ /// Number of postgap sectors
+ ///
+ public uint PostGap;
+ }
+
+ public class CHDParseException : Exception
+ {
+ public CHDParseException(string message) : base(message) { }
+ public CHDParseException(string message, Exception ex) : base(message, ex) { }
+ }
+
+ private static LibChdr.chd_track_type GetTrackType(string type)
+ {
+ return type switch
+ {
+ "MODE1" => LibChdr.chd_track_type.CD_TRACK_MODE1,
+ "MODE1/2048" => LibChdr.chd_track_type.CD_TRACK_MODE1,
+ "MODE1_RAW" => LibChdr.chd_track_type.CD_TRACK_MODE1_RAW,
+ "MODE1/2352" => LibChdr.chd_track_type.CD_TRACK_MODE1_RAW,
+ "MODE2" => LibChdr.chd_track_type.CD_TRACK_MODE2,
+ "MODE2/2336" => LibChdr.chd_track_type.CD_TRACK_MODE2,
+ "MODE2_FORM1" => LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1,
+ "MODE2/2048" => LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1,
+ "MODE2_FORM2" => LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2,
+ "MODE2/2324" => LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2,
+ "MODE2_FORM_MIX" => LibChdr.chd_track_type.CD_TRACK_MODE2_FORM_MIX,
+ "MODE2_RAW" => LibChdr.chd_track_type.CD_TRACK_MODE2_RAW,
+ "MODE2/2352" => LibChdr.chd_track_type.CD_TRACK_MODE2_RAW,
+ "CDI/2352" => LibChdr.chd_track_type.CD_TRACK_MODE2_RAW,
+ "AUDIO" => LibChdr.chd_track_type.CD_TRACK_AUDIO,
+ _ => throw new CHDParseException("Malformed CHD format: Invalid track type!"),
+ };
+ }
+
+ private static (LibChdr.chd_track_type TrackType, bool ChdContainsPregap) GetTrackTypeForPregap(string type)
+ {
+ if (type.Length > 0 && type[0] == 'V')
+ {
+ return (GetTrackType(type[1..]), true);
+ }
+
+ return (GetTrackType(type), false);
+ }
+
+ private static uint GetSectorSize(LibChdr.chd_track_type type)
+ {
+ return type switch
+ {
+ LibChdr.chd_track_type.CD_TRACK_MODE1 => 2048,
+ LibChdr.chd_track_type.CD_TRACK_MODE1_RAW => 2352,
+ LibChdr.chd_track_type.CD_TRACK_MODE2 => 2336,
+ LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1 => 2048,
+ LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2 => 2324,
+ LibChdr.chd_track_type.CD_TRACK_MODE2_FORM_MIX => 2336,
+ LibChdr.chd_track_type.CD_TRACK_MODE2_RAW => 2352,
+ LibChdr.chd_track_type.CD_TRACK_AUDIO => 2352,
+ _ => throw new CHDParseException("Malformed CHD format: Invalid track type!"),
+ };
+ }
+
+ private static LibChdr.chd_sub_type GetSubType(string type)
+ {
+ return type switch
+ {
+ "RW" => LibChdr.chd_sub_type.CD_SUB_NORMAL,
+ "RW_RAW" => LibChdr.chd_sub_type.CD_SUB_RAW,
+ "NONE" => LibChdr.chd_sub_type.CD_SUB_NONE,
+ _ => throw new CHDParseException("Malformed CHD format: Invalid sub type!"),
+ };
+ }
+
+ private static uint GetSubSize(LibChdr.chd_sub_type type)
+ {
+ return type switch
+ {
+ LibChdr.chd_sub_type.CD_SUB_NORMAL => 96,
+ LibChdr.chd_sub_type.CD_SUB_RAW => 96,
+ LibChdr.chd_sub_type.CD_SUB_NONE => 0,
+ _ => throw new CHDParseException("Malformed CHD format: Invalid sub type!"),
+ };
+ }
+
+ private static readonly string[] _metadataTags = { "TRACK", "TYPE", "SUBTYPE", "FRAMES", "PREGAP", "PGTYPE", "PGSUB", "POSTGAP" };
+
+ private static CHDCdMetadata ParseMetadata2(string metadata)
+ {
+ var strs = metadata.Split(' ');
+ if (strs.Length != 8)
+ {
+ throw new CHDParseException("Malformed CHD format: Incorrect number of metadata tags");
+ }
+
+ for (var i = 0; i < 8; i++)
+ {
+ var spl = strs[i].Split(':');
+ if (spl.Length != 2 || _metadataTags[i] != spl[0])
+ {
+ throw new CHDParseException("Malformed CHD format: Invalid metadata tag");
+ }
+
+ strs[i] = spl[1];
+ }
+
+ var ret = new CHDCdMetadata();
+ try
+ {
+ ret.Track = uint.Parse(strs[0]);
+ ret.TrackType = GetTrackType(strs[1]);
+ ret.SubType = GetSubType(strs[2]);
+ ret.Frames = uint.Parse(strs[3]);
+ ret.Pregap = uint.Parse(strs[4]);
+ (ret.PregapTrackType, ret.PregapInChd) = GetTrackTypeForPregap(strs[5]);
+ ret.PregapSubType = GetSubType(strs[6]);
+ ret.PostGap = uint.Parse(strs[7]);
+ }
+ catch (Exception ex)
+ {
+ throw ex as CHDParseException ?? new("Malformed CHD format: Metadata parsing threw an exception", ex);
+ }
+
+ if (ret.PregapInChd && ret.Pregap == 0)
+ {
+ throw new CHDParseException("Malformed CHD format: CHD track type indicate it contained pregap data, but no pregap data is present");
+ }
+
+ ret.IsCDI = strs[1] == "CDI/2352";
+ ret.SectorSize = GetSectorSize(ret.TrackType);
+ ret.SubSize = GetSubSize(ret.SubType);
+ ret.Padding = (0 - ret.Frames) & 3;
+ return ret;
+ }
+
+ private static CHDCdMetadata ParseMetadata(string metadata)
+ {
+ var strs = metadata.Split(' ');
+ if (strs.Length != 4)
+ {
+ throw new CHDParseException("Malformed CHD format: Incorrect number of metadata tags");
+ }
+
+ for (var i = 0; i < 4; i++)
+ {
+ var spl = strs[i].Split(':');
+ if (spl.Length != 2 || _metadataTags[i] != spl[0])
+ {
+ throw new CHDParseException("Malformed CHD format: Invalid metadata tag");
+ }
+
+ strs[i] = spl[1];
+ }
+
+ var ret = new CHDCdMetadata();
+ try
+ {
+ ret.Track = uint.Parse(strs[0]);
+ ret.TrackType = GetTrackType(strs[1]);
+ ret.SubType = GetSubType(strs[2]);
+ ret.Frames = uint.Parse(strs[3]);
+ }
+ catch (Exception ex)
+ {
+ throw ex as CHDParseException ?? new("Malformed CHD format: Metadata parsing threw an exception", ex);
+ }
+
+ ret.IsCDI = strs[1] == "CDI/2352";
+ ret.SectorSize = GetSectorSize(ret.TrackType);
+ ret.SubSize = GetSubSize(ret.SubType);
+ ret.Padding = (0 - ret.Frames) & 3;
+ return ret;
+ }
+
+ private static void ParseMetadataOld(ICollection cdMetadatas, Span metadata)
+ {
+ var numTracks = BinaryPrimitives.ReadUInt32LittleEndian(metadata);
+ var bigEndian = numTracks > 99; // apparently old metadata can appear as either little endian or big endian
+ if (bigEndian)
+ {
+ numTracks = BinaryPrimitives.ReverseEndianness(numTracks);
+ }
+
+ if (numTracks > 99)
+ {
+ throw new CHDParseException("Malformed CHD format: Invalid number of tracks");
+ }
+
+ for (var i = 0; i < numTracks; i++)
+ {
+ var track = metadata[(4 + i * 24)..];
+ var cdMetadata = new CHDCdMetadata
+ {
+ Track = (uint)i + 1
+ };
+ if (bigEndian)
+ {
+ cdMetadata.TrackType = (LibChdr.chd_track_type)BinaryPrimitives.ReadUInt32BigEndian(track);
+ cdMetadata.SubType = (LibChdr.chd_sub_type)BinaryPrimitives.ReadUInt32BigEndian(track[..4]);
+ cdMetadata.SectorSize = BinaryPrimitives.ReadUInt32BigEndian(track[..8]);
+ cdMetadata.SubSize = BinaryPrimitives.ReadUInt32BigEndian(track[..12]);
+ cdMetadata.Frames = BinaryPrimitives.ReadUInt32BigEndian(track[..16]);
+ cdMetadata.Padding = BinaryPrimitives.ReadUInt32BigEndian(track[..20]);
+ }
+ else
+ {
+ cdMetadata.TrackType = (LibChdr.chd_track_type)BinaryPrimitives.ReadUInt32LittleEndian(track);
+ cdMetadata.SubType = (LibChdr.chd_sub_type)BinaryPrimitives.ReadUInt32LittleEndian(track[..4]);
+ cdMetadata.SectorSize = BinaryPrimitives.ReadUInt32LittleEndian(track[..8]);
+ cdMetadata.SubSize = BinaryPrimitives.ReadUInt32LittleEndian(track[..12]);
+ cdMetadata.Frames = BinaryPrimitives.ReadUInt32LittleEndian(track[..16]);
+ cdMetadata.Padding = BinaryPrimitives.ReadUInt32LittleEndian(track[..20]);
+ }
+
+ if (cdMetadata.SectorSize != GetSectorSize(cdMetadata.TrackType))
+ {
+ throw new CHDParseException("Malformed CHD format: Invalid sector size");
+ }
+
+ if (cdMetadata.SubSize != GetSubSize(cdMetadata.SubType))
+ {
+ throw new CHDParseException("Malformed CHD format: Invalid sub size");
+ }
+
+ var expectedPadding = (0 - cdMetadata.Frames) & 3;
+ if (cdMetadata.Padding != expectedPadding)
+ {
+ throw new CHDParseException("Malformed CHD format: Invalid padding value");
+ }
+
+ cdMetadatas.Add(cdMetadata);
+ }
+ }
+
+ /// malformed chd format
+ public static CHDFile ParseFrom(Stream stream)
+ {
+ var chdf = new CHDFile();
+ try
+ {
+ chdf.CoreFile = new(stream);
+ var err = LibChdr.chd_open_core_file(chdf.CoreFile.CoreFile, LibChdr.CHD_OPEN_READ, IntPtr.Zero, out chdf.ChdFile);
+ if (err != LibChdr.chd_error.CHDERR_NONE)
+ {
+ throw new CHDParseException($"Malformed CHD format: Failed to open chd, got error {err}");
+ }
+
+ unsafe
+ {
+ var header = (LibChdr.chd_header*)LibChdr.chd_get_header(chdf.ChdFile);
+ chdf.Header = *header;
+ }
+
+ if (chdf.Header.hunkbytes == 0 || chdf.Header.hunkbytes % LibChdr.CD_FRAME_SIZE != 0)
+ {
+ throw new CHDParseException("Malformed CHD format: Invalid hunk size");
+ }
+
+ // libchdr puts the correct value here for older versions of chds which don't have this
+ // for newer chds, it is left as is, which might be invalid
+ if (chdf.Header.unitbytes != LibChdr.CD_FRAME_SIZE)
+ {
+ throw new CHDParseException("Malformed CHD format: Invalid unit size");
+ }
+
+ var metadataOutput = new byte[256];
+ for (uint i = 0; i < 99; i++)
+ {
+ err = LibChdr.chd_get_metadata(chdf.ChdFile, LibChdr.CDROM_TRACK_METADATA2_TAG,
+ i, metadataOutput, (uint)metadataOutput.Length, out var resultLen, out _, out _);
+ if (err == LibChdr.chd_error.CHDERR_NONE)
+ {
+ var metadata = Encoding.ASCII.GetString(metadataOutput, 0, (int)resultLen);
+ chdf.CdMetadatas.Add(ParseMetadata2(metadata));
+ continue;
+ }
+
+ err = LibChdr.chd_get_metadata(chdf.ChdFile, LibChdr.CDROM_TRACK_METADATA_TAG,
+ i, metadataOutput, (uint)metadataOutput.Length, out resultLen, out _, out _);
+ if (err == LibChdr.chd_error.CHDERR_NONE)
+ {
+ var metadata = Encoding.ASCII.GetString(metadataOutput, 0, (int)resultLen);
+ chdf.CdMetadatas.Add(ParseMetadata(metadata));
+ continue;
+ }
+
+ // if no more metadata, we're out of tracks
+ break;
+ }
+
+ // validate track numbers
+ if (chdf.CdMetadatas.Where((t, i) => t.Track != i + 1).Any())
+ {
+ throw new CHDParseException("Malformed CHD format: Invalid track number");
+ }
+
+ if (chdf.CdMetadatas.Count == 0)
+ {
+ // if no metadata was present, we might have "old" metadata instead (which has all track info stored in one entry)
+ metadataOutput = new byte[4 + 24 * 99];
+ err = LibChdr.chd_get_metadata(chdf.ChdFile, LibChdr.CDROM_OLD_METADATA_TAG,
+ 0, metadataOutput, (uint)metadataOutput.Length, out var resultLen, out _, out _);
+ if (err == LibChdr.chd_error.CHDERR_NONE)
+ {
+ if (resultLen != metadataOutput.Length)
+ {
+ throw new CHDParseException("Malformed CHD format: Incorrect length for old metadata");
+ }
+
+ ParseMetadataOld(chdf.CdMetadatas, metadataOutput);
+ }
+ }
+
+ if (chdf.CdMetadatas.Count == 0)
+ {
+ throw new CHDParseException("Malformed CHD format: No tracks present in chd");
+ }
+
+ // validation checks
+ var chdExpectedNumSectors = 0L;
+ foreach (var cdMetadata in chdf.CdMetadatas)
+ {
+ // if pregap is in the chd, then the reported frame count includes both pregap and track data
+ if (cdMetadata.PregapInChd && cdMetadata.Pregap > cdMetadata.Frames)
+ {
+ throw new CHDParseException("Malformed CHD format: Pregap in chd is larger than total sectors in chd track");
+ }
+
+ chdExpectedNumSectors += cdMetadata.Frames + cdMetadata.Padding;
+ }
+
+ var chdActualNumSectors = chdf.Header.hunkcount * (chdf.Header.hunkbytes / LibChdr.CD_FRAME_SIZE);
+ //if (chdExpectedNumSectors != chdActualNumSectors) // i see some chds with 4 extra sectors of padding in the end?
+ if (chdExpectedNumSectors > chdActualNumSectors)
+ {
+ throw new CHDParseException("Malformed CHD format: Mismatch in expected and actual number of sectors present");
+ }
+
+ return chdf;
+ }
+ catch (Exception ex)
+ {
+ if (chdf.ChdFile != IntPtr.Zero)
+ {
+ LibChdr.chd_close(chdf.ChdFile);
+ }
+
+ if (chdf.CoreFile == null)
+ {
+ stream.Dispose();
+ }
+
+ chdf.CoreFile?.Dispose();
+ throw ex as CHDParseException ?? new("Malformed CHD format: An unknown exception was thrown while parsing", ex);
+ }
+ }
+
+ public class LoadResults
+ {
+ public CHDFile ParsedCHDFile;
+ public bool Valid;
+ public CHDParseException FailureException;
+ public string ChdPath;
+ }
+
+ public static LoadResults LoadCHDPath(string path)
+ {
+ var ret = new LoadResults
+ {
+ ChdPath = path
+ };
+ try
+ {
+ if (!File.Exists(path)) throw new CHDParseException("Malformed CHD format: Nonexistent CHD file!");
+
+ var infCHD = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
+ ret.ParsedCHDFile = ParseFrom(infCHD);
+ ret.Valid = true;
+ }
+ catch (CHDParseException ex)
+ {
+ ret.FailureException = ex;
+ }
+
+ return ret;
+ }
+
+ ///
+ /// CHD is dumb and byteswaps audio samples for some reason
+ ///
+ private class SS_CHD_Audio : SS_Base
+ {
+ public override void Synth(SectorSynthJob job)
+ {
+ // read the sector user data
+ if ((job.Parts & ESectorSynthPart.User2352) != 0)
+ {
+ Blob.Read(BlobOffset, job.DestBuffer2448, job.DestOffset, 2352);
+ EndiannessUtils.MutatingByteSwap16(job.DestBuffer2448.AsSpan().Slice(job.DestOffset, 2352));
+ }
+
+ // if subcode is needed, synthesize it
+ SynthSubchannelAsNeed(job);
+ }
+ }
+
+ private class SS_CHD_Sub : ISectorSynthJob2448
+ {
+ private readonly SS_Base _baseSynth;
+ private readonly bool _isInterleaved;
+
+ public SS_CHD_Sub(SS_Base baseSynth, bool isInterleaved)
+ {
+ _baseSynth = baseSynth;
+ _isInterleaved = isInterleaved;
+ }
+
+ public void Synth(SectorSynthJob job)
+ {
+ if ((job.Parts & ESectorSynthPart.SubcodeAny) != 0)
+ {
+ _baseSynth.Blob.Read(_baseSynth.BlobOffset + 2352, job.DestBuffer2448, job.DestOffset + 2352, 96);
+
+ if ((job.Parts & ESectorSynthPart.SubcodeDeinterleave) != 0 && _isInterleaved)
+ {
+ SynthUtils.DeinterleaveSubcodeInplace(job.DestBuffer2448, job.DestOffset + 2352);
+ }
+
+ if ((job.Parts & ESectorSynthPart.SubcodeDeinterleave) == 0 && !_isInterleaved)
+ {
+ SynthUtils.InterleaveSubcodeInplace(job.DestBuffer2448, job.DestOffset + 2352);
+ }
+
+ job.Parts &= ~ESectorSynthPart.SubcodeAny;
+ }
+
+ _baseSynth.Synth(job);
+ }
+ }
+
+ /// file not found
+ public static Disc LoadCHDToDisc(string chdPath, DiscMountPolicy IN_DiscMountPolicy)
+ {
+ var loadResults = LoadCHDPath(chdPath);
+ if (!loadResults.Valid)
+ {
+ throw loadResults.FailureException;
+ }
+
+ var disc = new Disc();
+ try
+ {
+ var chdf = loadResults.ParsedCHDFile;
+ IBlob chdBlob = new Blob_CHD(chdf.CoreFile, chdf.ChdFile, chdf.Header.hunkbytes);
+ disc.DisposableResources.Add(chdBlob);
+
+ // chds only support 1 session
+ var session = new DiscSession { Number = 1 };
+ var chdOffset = 0L;
+ foreach (var cdMetadata in chdf.CdMetadatas)
+ {
+ RawTOCEntry EmitRawTOCEntry()
+ {
+ var q = default(SubchannelQ);
+ //absent some kind of policy for how to set it, this is a safe assumption
+ const byte kADR = 1;
+ var control = cdMetadata.TrackType != LibChdr.chd_track_type.CD_TRACK_AUDIO
+ ? EControlQ.DATA
+ : EControlQ.None;
+ q.SetStatus(kADR, control);
+ q.q_tno = BCD2.FromDecimal(0);
+ q.q_index = BCD2.FromDecimal((int)cdMetadata.Track);
+ q.Timestamp = 0;
+ q.zero = 0;
+ q.AP_Timestamp = disc._Sectors.Count;
+ q.q_crc = 0;
+ return new() { QData = q };
+ }
+
+ static SS_Base CreateSynth(LibChdr.chd_track_type trackType)
+ {
+ return trackType switch
+ {
+ LibChdr.chd_track_type.CD_TRACK_MODE1 => new SS_Mode1_2048(),
+ LibChdr.chd_track_type.CD_TRACK_MODE1_RAW => new SS_2352(),
+ LibChdr.chd_track_type.CD_TRACK_MODE2 => new SS_Mode2_2336(),
+ LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1 => new SS_Mode2_Form1_2048(),
+ LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2 => new SS_Mode2_Form2_2324(),
+ LibChdr.chd_track_type.CD_TRACK_MODE2_FORM_MIX => new SS_Mode2_2336(),
+ LibChdr.chd_track_type.CD_TRACK_MODE2_RAW => new SS_2352(),
+ LibChdr.chd_track_type.CD_TRACK_AUDIO => new SS_CHD_Audio(),
+ _ => throw new InvalidOperationException(),
+ };
+ }
+
+ static CueTrackType ToCueTrackType(LibChdr.chd_track_type chdTrackType, bool isCdi)
+ {
+ // rough matches, not too important if these are somewhat wrong (they're just used for generated gaps)
+ return chdTrackType switch
+ {
+ LibChdr.chd_track_type.CD_TRACK_MODE1 => CueTrackType.Mode1_2048,
+ LibChdr.chd_track_type.CD_TRACK_MODE1_RAW => CueTrackType.Mode1_2352,
+ LibChdr.chd_track_type.CD_TRACK_MODE2 => CueTrackType.Mode2_2336,
+ LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1 => CueTrackType.Mode2_2336,
+ LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2 => CueTrackType.Mode2_2336,
+ LibChdr.chd_track_type.CD_TRACK_MODE2_FORM_MIX => CueTrackType.Mode2_2336,
+ LibChdr.chd_track_type.CD_TRACK_MODE2_RAW when isCdi => CueTrackType.CDI_2352,
+ LibChdr.chd_track_type.CD_TRACK_MODE2_RAW => CueTrackType.Mode2_2352,
+ LibChdr.chd_track_type.CD_TRACK_AUDIO => CueTrackType.Audio,
+ _ => throw new InvalidOperationException(),
+ };
+ }
+
+ var pregapLength = cdMetadata.Pregap;
+ // force 150 sector pregap for the first track if not present in the chd
+ if (!cdMetadata.PregapInChd && cdMetadata.Track == 1)
+ {
+ cdMetadata.PregapTrackType = cdMetadata.TrackType;
+ cdMetadata.PregapSubType = cdMetadata.SubType;
+ pregapLength = 150;
+ }
+
+ var relMSF = -pregapLength;
+ for (var i = 0; i < pregapLength; i++)
+ {
+ SS_Base synth;
+ if (cdMetadata.PregapInChd)
+ {
+ synth = CreateSynth(cdMetadata.PregapTrackType);
+ synth.Blob = chdBlob;
+ synth.BlobOffset = chdOffset;
+ }
+ else
+ {
+ synth = new SS_Gap { TrackType = ToCueTrackType(cdMetadata.PregapTrackType, cdMetadata.IsCDI) };
+ }
+
+ synth.Policy = IN_DiscMountPolicy;
+ const byte kADR = 1;
+ var control = cdMetadata.PregapTrackType != LibChdr.chd_track_type.CD_TRACK_AUDIO
+ ? EControlQ.DATA
+ : EControlQ.None;
+ synth.sq.SetStatus(kADR, control);
+ synth.sq.q_tno = BCD2.FromDecimal((int)cdMetadata.Track);
+ synth.sq.q_index = BCD2.FromDecimal(0);
+ synth.sq.Timestamp = !IN_DiscMountPolicy.CUE_PregapContradictionModeA
+ ? (int)relMSF + 1
+ : (int)relMSF;
+ synth.sq.zero = 0;
+ synth.sq.AP_Timestamp = disc._Sectors.Count;
+ synth.sq.q_crc = 0;
+ synth.Pause = true;
+
+ if (cdMetadata.PregapInChd)
+ {
+ // wrap the base synth with our special synth if we have subcode in the chd
+ ISectorSynthJob2448 chdSynth = cdMetadata.PregapSubType switch
+ {
+ LibChdr.chd_sub_type.CD_SUB_NORMAL => new SS_CHD_Sub(synth, isInterleaved: true),
+ LibChdr.chd_sub_type.CD_SUB_RAW => new SS_CHD_Sub(synth, isInterleaved: false),
+ LibChdr.chd_sub_type.CD_SUB_NONE => synth,
+ _ => throw new InvalidOperationException(),
+ };
+
+ disc._Sectors.Add(chdSynth);
+ chdOffset += LibChdr.CD_FRAME_SIZE;
+ }
+ else
+ {
+ disc._Sectors.Add(synth);
+ }
+
+ relMSF++;
+ }
+
+ session.RawTOCEntries.Add(EmitRawTOCEntry());
+
+ var trackLength = cdMetadata.Frames;
+ if (cdMetadata.PregapInChd)
+ {
+ trackLength -= pregapLength;
+ }
+
+ for (var i = 0; i < trackLength; i++)
+ {
+ var synth = CreateSynth(cdMetadata.TrackType);
+ synth.Blob = chdBlob;
+ synth.BlobOffset = chdOffset;
+ synth.Policy = IN_DiscMountPolicy;
+ const byte kADR = 1;
+ var control = cdMetadata.TrackType != LibChdr.chd_track_type.CD_TRACK_AUDIO
+ ? EControlQ.DATA
+ : EControlQ.None;
+ synth.sq.SetStatus(kADR, control);
+ synth.sq.q_tno = BCD2.FromDecimal((int)cdMetadata.Track);
+ synth.sq.q_index = BCD2.FromDecimal(1);
+ synth.sq.Timestamp = (int)relMSF;
+ synth.sq.zero = 0;
+ synth.sq.AP_Timestamp = disc._Sectors.Count;
+ synth.sq.q_crc = 0;
+ synth.Pause = false;
+ ISectorSynthJob2448 chdSynth = cdMetadata.SubType switch
+ {
+ LibChdr.chd_sub_type.CD_SUB_NORMAL => new SS_CHD_Sub(synth, isInterleaved: true),
+ LibChdr.chd_sub_type.CD_SUB_RAW => new SS_CHD_Sub(synth, isInterleaved: false),
+ LibChdr.chd_sub_type.CD_SUB_NONE => synth,
+ _ => throw new InvalidOperationException(),
+ };
+ disc._Sectors.Add(chdSynth);
+ chdOffset += LibChdr.CD_FRAME_SIZE;
+ relMSF++;
+ }
+
+ chdOffset += cdMetadata.Padding * LibChdr.CD_FRAME_SIZE;
+
+ for (var i = 0; i < cdMetadata.PostGap; i++)
+ {
+ var synth = new SS_Gap
+ {
+ TrackType = ToCueTrackType(cdMetadata.TrackType, cdMetadata.IsCDI),
+ Policy = IN_DiscMountPolicy
+ };
+ const byte kADR = 1;
+ var control = cdMetadata.TrackType != LibChdr.chd_track_type.CD_TRACK_AUDIO
+ ? EControlQ.DATA
+ : EControlQ.None;
+ synth.sq.SetStatus(kADR, control);
+ synth.sq.q_tno = BCD2.FromDecimal((int)cdMetadata.Track);
+ synth.sq.q_index = BCD2.FromDecimal(2);
+ synth.sq.Timestamp = (int)relMSF;
+ synth.sq.zero = 0;
+ synth.sq.AP_Timestamp = disc._Sectors.Count;
+ synth.sq.q_crc = 0;
+ synth.Pause = true;
+ disc._Sectors.Add(synth);
+ relMSF++;
+ }
+ }
+
+ SessionFormat GuessSessionFormat()
+ {
+ foreach (var cdMetadata in chdf.CdMetadatas)
+ {
+ if (cdMetadata.IsCDI)
+ {
+ return SessionFormat.Type10_CDI;
+ }
+
+ if (cdMetadata.TrackType is LibChdr.chd_track_type.CD_TRACK_MODE2
+ or LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1
+ or LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2
+ or LibChdr.chd_track_type.CD_TRACK_MODE2_FORM_MIX
+ or LibChdr.chd_track_type.CD_TRACK_MODE2_RAW)
+ {
+ return SessionFormat.Type20_CDXA;
+ }
+ }
+
+ return SessionFormat.Type00_CDROM_CDDA;
+ }
+
+ var TOCMiscInfo = new Synthesize_A0A1A2_Job(
+ firstRecordedTrackNumber: 1,
+ lastRecordedTrackNumber: chdf.CdMetadatas.Count,
+ sessionFormat: GuessSessionFormat(),
+ leadoutTimestamp: disc._Sectors.Count);
+ TOCMiscInfo.Run(session.RawTOCEntries);
+
+ disc.Sessions.Add(session);
+ return disc;
+ }
+ catch
+ {
+ disc.Dispose();
+ throw;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_SynthExtras.cs b/src/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_SynthExtras.cs
index b42e494af0..151e3fcf85 100644
--- a/src/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_SynthExtras.cs
+++ b/src/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_SynthExtras.cs
@@ -6,7 +6,7 @@ namespace BizHawk.Emulation.DiscSystem.CUE
///
/// Represents a Mode2 Form1 2048-byte sector
- /// Only used by MDS
+ /// Only used by NRG, MDS, and CHD
///
internal class SS_Mode2_Form1_2048 : SS_Base
{
@@ -36,7 +36,7 @@ namespace BizHawk.Emulation.DiscSystem.CUE
///
/// Represents a Mode2 Form1 2324-byte sector
- /// Only used by MDS
+ /// Only used by MDS and CHD
///
internal class SS_Mode2_Form2_2324 : SS_Base
{
@@ -82,7 +82,7 @@ namespace BizHawk.Emulation.DiscSystem.CUE
///
/// Represents a full 2448-byte sector with interleaved subcode
- /// Only used by MDS and CDI
+ /// Only used by MDS, NRG, and CDI
///
internal class SS_2448_Interleaved : SS_Base
{
diff --git a/src/BizHawk.Emulation.DiscSystem/DiscMountJob.cs b/src/BizHawk.Emulation.DiscSystem/DiscMountJob.cs
index 08ef0781c1..e3c053de40 100644
--- a/src/BizHawk.Emulation.DiscSystem/DiscMountJob.cs
+++ b/src/BizHawk.Emulation.DiscSystem/DiscMountJob.cs
@@ -195,6 +195,9 @@ namespace BizHawk.Emulation.DiscSystem
case ".cdi":
OUT_Disc = CDI_Format.LoadCDIToDisc(IN_FromPath, IN_DiscMountPolicy);
break;
+ case ".chd":
+ OUT_Disc = CHD_Format.LoadCHDToDisc(IN_FromPath, IN_DiscMountPolicy);
+ break;
case ".cue":
LoadCue(dir, File.ReadAllText(IN_FromPath));
break;
diff --git a/src/BizHawk.Emulation.DiscSystem/LibChdr.cs b/src/BizHawk.Emulation.DiscSystem/LibChdr.cs
new file mode 100644
index 0000000000..23b3aa5dba
--- /dev/null
+++ b/src/BizHawk.Emulation.DiscSystem/LibChdr.cs
@@ -0,0 +1,266 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+#pragma warning disable IDE1006
+
+// ReSharper disable UnusedMember.Global
+// ReSharper disable UnusedType.Global
+
+namespace BizHawk.Emulation.DiscSystem
+{
+ ///
+ /// libchdr bindings
+ /// TODO: should this be common-ized? chd isn't limited to discs, it could be used for hard disk images (e.g. for MAME)
+ ///
+ public static class LibChdr
+ {
+ public enum chd_error : int
+ {
+ CHDERR_NONE,
+ CHDERR_NO_INTERFACE,
+ CHDERR_OUT_OF_MEMORY,
+ CHDERR_INVALID_FILE,
+ CHDERR_INVALID_PARAMETER,
+ CHDERR_INVALID_DATA,
+ CHDERR_FILE_NOT_FOUND,
+ CHDERR_REQUIRES_PARENT,
+ CHDERR_FILE_NOT_WRITEABLE,
+ CHDERR_READ_ERROR,
+ CHDERR_WRITE_ERROR,
+ CHDERR_CODEC_ERROR,
+ CHDERR_INVALID_PARENT,
+ CHDERR_HUNK_OUT_OF_RANGE,
+ CHDERR_DECOMPRESSION_ERROR,
+ CHDERR_COMPRESSION_ERROR,
+ CHDERR_CANT_CREATE_FILE,
+ CHDERR_CANT_VERIFY,
+ CHDERR_NOT_SUPPORTED,
+ CHDERR_METADATA_NOT_FOUND,
+ CHDERR_INVALID_METADATA_SIZE,
+ CHDERR_UNSUPPORTED_VERSION,
+ CHDERR_VERIFY_INCOMPLETE,
+ CHDERR_INVALID_METADATA,
+ CHDERR_INVALID_STATE,
+ CHDERR_OPERATION_PENDING,
+ CHDERR_NO_ASYNC_OPERATION,
+ CHDERR_UNSUPPORTED_FORMAT
+ }
+
+ public const int CHD_OPEN_READ = 1;
+ public const int CHD_OPEN_READWRITE = 2;
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct chd_core_file
+ {
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate ulong FSizeDelegate(IntPtr file);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate nuint FReadDelegate(IntPtr buffer, nuint size, nuint count, IntPtr file);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate int FCloseDelegate(IntPtr file);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate int FSeekDelegate(IntPtr file, long offset, SeekOrigin origin);
+
+ public IntPtr argp;
+ [MarshalAs(UnmanagedType.FunctionPtr)]
+ public FSizeDelegate fsize;
+ [MarshalAs(UnmanagedType.FunctionPtr)]
+ public FReadDelegate fread;
+ [MarshalAs(UnmanagedType.FunctionPtr)]
+ public FCloseDelegate fclose;
+ [MarshalAs(UnmanagedType.FunctionPtr)]
+ public FSeekDelegate fseek;
+ }
+
+ ///
+ /// Convenience chd_core_file wrapper against a generic Stream
+ ///
+ public class CoreFileStreamWrapper : IDisposable
+ {
+ private const uint READ_BUFFER_LEN = 8 * CD_FRAME_SIZE; // 8 frames, usual uncompressed hunk size
+ private readonly byte[] _readBuffer = new byte[READ_BUFFER_LEN];
+
+ private Stream _s;
+
+ // ReSharper disable once MemberCanBePrivate.Global
+ private readonly chd_core_file _coreFile;
+ public readonly IntPtr CoreFile;
+
+ private ulong FSize(IntPtr file)
+ {
+ try
+ {
+ return (ulong)_s.Length;
+ }
+ catch (Exception e)
+ {
+ Console.Error.WriteLine(e);
+ return unchecked((ulong)-1);
+ }
+ }
+
+ private nuint FRead(IntPtr buffer, nuint size, nuint count, IntPtr file)
+ {
+ nuint ret = 0;
+ try
+ {
+ // note: size will always be 1, so this should never overflow
+ var numBytesToRead = (uint)Math.Min(size * (ulong)count, uint.MaxValue);
+ while (numBytesToRead > 0)
+ {
+ var numRead = _s.Read(_readBuffer, 0, (int)Math.Min(READ_BUFFER_LEN, numBytesToRead));
+ if (numRead == 0)
+ {
+ return ret;
+ }
+
+ Marshal.Copy(_readBuffer, 0, buffer, numRead);
+ buffer += numRead;
+ ret += (uint)numRead;
+ numBytesToRead -= (uint)numRead;
+ }
+
+ return ret;
+ }
+ catch (Exception e)
+ {
+ Console.Error.WriteLine(e);
+ return ret;
+ }
+ }
+
+ private int FClose(IntPtr file)
+ {
+ if (_s == null)
+ {
+ return -1;
+ }
+
+ _s.Dispose();
+ _s = null;
+ return 0;
+ }
+
+ private int FSeek(IntPtr file, long offset, SeekOrigin origin)
+ {
+ try
+ {
+ _s.Seek(offset, origin);
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine(ex);
+ return -1;
+ }
+ }
+
+ public CoreFileStreamWrapper(Stream s)
+ {
+ if (!s.CanRead || !s.CanSeek)
+ {
+ throw new NotSupportedException("The underlying CHD stream must support reading and seeking!");
+ }
+
+ _s = s;
+ _coreFile.fsize = FSize;
+ _coreFile.fread = FRead;
+ _coreFile.fclose = FClose;
+ _coreFile.fseek = FSeek;
+ // the pointer here must stay alloc'd on the unmanaged size
+ // as libchdr expects the memory to not move around
+ CoreFile = Marshal.AllocCoTaskMem(Marshal.SizeOf());
+ Marshal.StructureToPtr(_coreFile, CoreFile, fDeleteOld: false);
+ }
+
+ public void Dispose()
+ {
+ Marshal.DestroyStructure(CoreFile);
+ Marshal.FreeCoTaskMem(CoreFile);
+ _s?.Dispose();
+ }
+ }
+
+ [DllImport("chdr")]
+ public static extern chd_error chd_open_core_file(IntPtr file, int mode, IntPtr parent, out IntPtr chd);
+
+ [DllImport("chdr")]
+ public static extern void chd_close(IntPtr chd);
+
+ public const int CHD_MD5_BYTES = 16;
+ public const int CHD_SHA1_BYTES = 20;
+
+ // extracted chd header (not the same as the one on disk, but rather an interpreted one by libchdr)
+ [StructLayout(LayoutKind.Sequential)]
+ public struct chd_header
+ {
+ public uint length; // length of header data
+ public uint version; // drive format version
+ public uint flags; // flags field
+ public unsafe fixed uint compression[4]; // compression type
+ public uint hunkbytes; // number of bytes per hunk
+ public uint totalhunks; // total # of hunks represented
+ public ulong logicalbytes; // logical size of the data
+ public ulong metaoffset; // offset in file of first metadata
+ public ulong mapoffset; // TOOD V5
+ public unsafe fixed byte md5[CHD_MD5_BYTES]; // overall MD5 checksum
+ public unsafe fixed byte parentmd5[CHD_MD5_BYTES]; // overall MD5 checksum of parent
+ public unsafe fixed byte sha1[CHD_SHA1_BYTES]; // overall SHA1 checksum
+ public unsafe fixed byte rawsha1[CHD_SHA1_BYTES]; // SHA1 checksum of raw data
+ public unsafe fixed byte parentsha1[CHD_SHA1_BYTES]; // overall SHA1 checksum of parent
+ public uint unitbytes; // TODO V5
+ public ulong unitcount; // TODO V5
+ public uint hunkcount; // TODO V5
+ public uint mapentrybytes; // length of each entry in a map (V5)
+ public unsafe byte* rawmap; // raw map data
+ public uint obsolete_cylinders; // obsolete field -- do not use!
+ public uint obsolete_sectors; // obsolete field -- do not use!
+ public uint obsolete_heads; // obsolete field -- do not use!
+ public uint obsolete_hunksize; // obsolete field -- do not use!
+ }
+
+ [DllImport("chdr")]
+ public static extern IntPtr chd_get_header(IntPtr chd);
+
+ [DllImport("chdr")]
+ public static extern chd_error chd_read(IntPtr chd, uint hunknum, byte[] buffer);
+
+ public const uint CDROM_TRACK_METADATA2_TAG = 0x43485432; // CHT2
+ public const uint CDROM_TRACK_METADATA_TAG = 0x43485452; // CHTR
+ public const uint CDROM_OLD_METADATA_TAG = 0x43484344; // CHCD
+
+ // these formats are more for sscanf, they aren't suitable for C#
+ public const string CDROM_TRACK_METADATA2_FORMAT = "TRACK:%d TYPE:%s SUBTYPE:%s FRAMES:%d PREGAP:%d PGTYPE:%s PGSUB:%s POSTGAP:%d";
+ public const string CDROM_TRACK_METADATA_FORMAT = "TRACK:%d TYPE:%s SUBTYPE:%s FRAMES:%d";
+
+ public enum chd_track_type : uint
+ {
+ CD_TRACK_MODE1 = 0, // mode 1 2048 bytes/sector
+ CD_TRACK_MODE1_RAW, // mode 1 2352 bytes/sector
+ CD_TRACK_MODE2, // mode 2 2336 bytes/sector
+ CD_TRACK_MODE2_FORM1, // mode 2 2048 bytes/sector
+ CD_TRACK_MODE2_FORM2, // mode 2 2324 bytes/sector
+ CD_TRACK_MODE2_FORM_MIX, // mode 2 2336 bytes/sector
+ CD_TRACK_MODE2_RAW, // mode 2 2352 bytes/sector
+ CD_TRACK_AUDIO, // redbook audio track 2352 bytes/sector (588 samples)
+ }
+
+ public enum chd_sub_type : uint
+ {
+ CD_SUB_NORMAL = 0, // "cooked" 96 bytes per sector
+ CD_SUB_RAW, // raw uninterleaved 96 bytes per sector
+ CD_SUB_NONE // no subcode data stored
+ }
+
+ // hunks should be a multiple of this for cd chds
+ public const uint CD_FRAME_SIZE = 2352 + 96;
+
+ [DllImport("chdr")]
+ public static extern chd_error chd_get_metadata(
+ IntPtr chd, uint searchtag, uint searchindex, byte[] output, uint outputlen, out uint resultlen, out uint resulttag, out byte resultflags);
+ }
+}