Add CHD CD support

This commit is contained in:
CasualPokePlayer 2024-04-29 22:20:24 -07:00
parent 9dcb84336e
commit 21b6bd331b
14 changed files with 1174 additions and 7 deletions

3
.gitmodules vendored
View File

@ -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

BIN
Assets/dll/chdr.dll Normal file

Binary file not shown.

1
ExternalProjects/libchdr/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
build/

View File

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

View File

@ -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

@ -0,0 +1 @@
Subproject commit 5c598c2df3a7717552a76410d79f5af01ff51b1d

View File

@ -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

View File

@ -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
//

View File

@ -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";
}
}

View File

@ -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;
}
}
}

View File

@ -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
{
/// <summary>
/// Represents a CHD file.
/// This isn't particularly faithful to the format, but rather it just wraps libchdr's chd_file
/// </summary>
public class CHDFile
{
/// <summary>
/// Wrapper of a C# stream to a chd_core_file
/// </summary>
public LibChdr.CoreFileStreamWrapper CoreFile;
/// <summary>
/// chd_file* to be used for libchdr functions
/// </summary>
public IntPtr ChdFile;
/// <summary>
/// CHD header, interpreted by libchdr
/// </summary>
public LibChdr.chd_header Header;
/// <summary>
/// CHD CD metadata for each track
/// </summary>
public readonly IList<CHDCdMetadata> CdMetadatas = new List<CHDCdMetadata>();
}
/// <summary>
/// Results of chd_get_metadata with cdrom track metadata tags
/// </summary>
public class CHDCdMetadata
{
/// <summary>
/// Track number (1..99)
/// </summary>
public uint Track;
/// <summary>
/// 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
/// </summary>
public bool IsCDI;
/// <summary>
/// Track type
/// </summary>
public LibChdr.chd_track_type TrackType;
/// <summary>
/// Subcode type
/// </summary>
public LibChdr.chd_sub_type SubType;
/// <summary>
/// Size of each sector
/// </summary>
public uint SectorSize;
/// <summary>
/// Subchannel size
/// </summary>
public uint SubSize;
/// <summary>
/// Number of frames in this track
/// This might include pregap, if that is stored in the chd
/// </summary>
public uint Frames;
/// <summary>
/// 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
/// </summary>
public uint Padding;
/// <summary>
/// Number of pregap sectors
/// </summary>
public uint Pregap;
/// <summary>
/// Pregap track type
/// </summary>
public LibChdr.chd_track_type PregapTrackType;
/// <summary>
/// Pregap subcode type
/// </summary>
public LibChdr.chd_sub_type PregapSubType;
/// <summary>
/// Indicates whether pregap is in the CHD
/// If pregap isn't in the CHD, it needs to be generated where appropriate
/// </summary>
public bool PregapInChd;
/// <summary>
/// Number of postgap sectors
/// </summary>
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<CHDCdMetadata> cdMetadatas, Span<byte> 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);
}
}
/// <exception cref="CHDParseException">malformed chd format</exception>
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;
}
/// <summary>
/// CHD is dumb and byteswaps audio samples for some reason
/// </summary>
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);
}
}
/// <exception cref="CHDParseException">file <paramref name="chdPath"/> not found</exception>
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;
}
}
}
}

View File

@ -6,7 +6,7 @@ namespace BizHawk.Emulation.DiscSystem.CUE
/// <summary>
/// Represents a Mode2 Form1 2048-byte sector
/// Only used by MDS
/// Only used by NRG, MDS, and CHD
/// </summary>
internal class SS_Mode2_Form1_2048 : SS_Base
{
@ -36,7 +36,7 @@ namespace BizHawk.Emulation.DiscSystem.CUE
/// <summary>
/// Represents a Mode2 Form1 2324-byte sector
/// Only used by MDS
/// Only used by MDS and CHD
/// </summary>
internal class SS_Mode2_Form2_2324 : SS_Base
{
@ -82,7 +82,7 @@ namespace BizHawk.Emulation.DiscSystem.CUE
/// <summary>
/// Represents a full 2448-byte sector with interleaved subcode
/// Only used by MDS and CDI
/// Only used by MDS, NRG, and CDI
/// </summary>
internal class SS_2448_Interleaved : SS_Base
{

View File

@ -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;

View File

@ -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
{
/// <summary>
/// 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)
/// </summary>
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;
}
/// <summary>
/// Convenience chd_core_file wrapper against a generic Stream
/// </summary>
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<chd_core_file>());
Marshal.StructureToPtr(_coreFile, CoreFile, fDeleteOld: false);
}
public void Dispose()
{
Marshal.DestroyStructure<chd_core_file>(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);
}
}