diff --git a/src/BizHawk.Client.Common/RomLoader.cs b/src/BizHawk.Client.Common/RomLoader.cs
index 2506868be8..ee7a152d28 100644
--- a/src/BizHawk.Client.Common/RomLoader.cs
+++ b/src/BizHawk.Client.Common/RomLoader.cs
@@ -898,7 +898,7 @@ namespace BizHawk.Client.Common
/// TODO add and handle (you can drag-and-drop scripts and there are already non-rom things in this list, so why not?)
public static readonly FilesystemFilterSet RomFilter = new(
new FilesystemFilter("Music Files", Array.Empty(), devBuildExtraExts: new[] { "psf", "minipsf", "sid", "nsf", "gbs" }),
- new FilesystemFilter("Disc Images", new[] { "cue", "ccd", "mds", "m3u" }),
+ new FilesystemFilter("Disc Images", new[] { "cue", "ccd", "cdi", "mds", "m3u" }),
new FilesystemFilter("NES", RomFileExtensions.NES.Concat(new[] { "nsf" }).ToList(), addArchiveExts: true),
new FilesystemFilter("Super NES", RomFileExtensions.SNES, addArchiveExts: true),
new FilesystemFilter("PlayStation", new[] { "cue", "ccd", "mds", "m3u" }),
diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs
index c9f27ddf96..92e6dc5b30 100644
--- a/src/BizHawk.Client.EmuHawk/MainForm.cs
+++ b/src/BizHawk.Client.EmuHawk/MainForm.cs
@@ -3880,7 +3880,7 @@ namespace BizHawk.Client.EmuHawk
var ext = Path.GetExtension(xmlGame.AssetFullPaths[xg])?.ToLowerInvariant();
var (filename, data) = xmlGame.Assets[xg];
- if (ext == ".cue" || ext == ".ccd" || ext == ".toc" || ext == ".mds")
+ if (ext is ".cue" or ".ccd" or ".cdi" or ".toc" or ".mds")
{
xSw.WriteLine(Path.GetFileNameWithoutExtension(filename));
xSw.WriteLine("SHA1:N/A");
diff --git a/src/BizHawk.Emulation.DiscSystem/Disc.cs b/src/BizHawk.Emulation.DiscSystem/Disc.cs
index 328a2a3857..f4a14cc6eb 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 ".cue" or ".iso" or ".mds";
+ => extension.ToLowerInvariant() is ".ccd" or ".cdi" or ".cue" or ".iso" or ".mds";
}
}
\ No newline at end of file
diff --git a/src/BizHawk.Emulation.DiscSystem/DiscFormats/CDI_format.cs b/src/BizHawk.Emulation.DiscSystem/DiscFormats/CDI_format.cs
new file mode 100644
index 0000000000..1deb039471
--- /dev/null
+++ b/src/BizHawk.Emulation.DiscSystem/DiscFormats/CDI_format.cs
@@ -0,0 +1,599 @@
+using System;
+using System.IO;
+using System.Globalization;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography;
+using BizHawk.Common;
+using BizHawk.Common.IOExtensions;
+using BizHawk.Emulation.DiscSystem.CUE;
+
+//DiscJuggler CDI images
+//https://problemkaputt.de/psxspx-cdrom-disk-images-cdi-discjuggler.htm
+//https://github.com/cdemu/cdemu/blob/1f90f74/libmirage/images/image-cdi/parser.c
+
+namespace BizHawk.Emulation.DiscSystem
+{
+ public class CDI_Format
+ {
+ ///
+ /// Represents a CDI file, faithfully. Minimal interpretation of the data happens.
+ ///
+ public class CDIFile
+ {
+ ///
+ /// Number of sessions
+ ///
+ public byte NumSessions;
+
+ ///
+ /// The session blocks
+ ///
+ public readonly IList Sessions = new List();
+
+ ///
+ /// The track blocks
+ ///
+ public readonly IList Tracks = new List();
+
+ ///
+ /// The disc info block
+ ///
+ public readonly CDIDiscInfo DiscInfo = new();
+
+ ///
+ /// Footer size in bytes
+ ///
+ public uint Entrypoint;
+ }
+
+ ///
+ /// Represents a session block from a CDI file
+ ///
+ public class CDISession
+ {
+ ///
+ /// Number of tracks in session (1..99) (or 0 = no more sessions)
+ ///
+ public byte NumTracks;
+ }
+
+ ///
+ /// Represents a track/disc info block header from a CDI track
+ ///
+ public class CDITrackHeader
+ {
+ ///
+ /// Number of tracks on disc (1..99)
+ ///
+ public byte NumTracks;
+
+ ///
+ /// Full Path/Filename (may be empty)
+ ///
+ public string Path;
+
+ ///
+ /// 0x0098 = CD-ROM, 0x0038 = DVD-ROM
+ ///
+ public ushort MediumType;
+ }
+
+ ///
+ /// Represents a CD text block from a CDI track
+ ///
+ public class CDICDText
+ {
+ ///
+ /// A CD text block has 0-18 strings, each of variable length
+ ///
+ public readonly IList CdTexts = new List();
+ }
+
+ ///
+ /// Represents a track block from a CDI file
+ ///
+ public class CDITrack : CDITrackHeader
+ {
+ ///
+ /// The sector count of each index specified for the track
+ ///
+ public readonly IList IndexSectorCounts = new List();
+
+ ///
+ /// CD text blocks
+ ///
+ public readonly IList CdTextBlocks = new List();
+
+ ///
+ /// The specified track mode (0 = Audio, 1 = Mode1, 2 = Mode2/Mixed)
+ ///
+ public byte TrackMode;
+
+ ///
+ /// Session number (0-indexed)
+ ///
+ public uint SessionNumber;
+
+ ///
+ /// Track number (0-indexed, releative to session)
+ ///
+ public uint TrackNumber;
+
+ ///
+ /// Track start address
+ ///
+ public uint TrackStartAddress;
+
+ ///
+ /// Track length, in sectors
+ ///
+ public uint TrackLength;
+
+ ///
+ /// The specified read mode (0 = Mode1, 1 = Mode2, 2 = Audio, 3 = Raw+Q, 4 = Raw+PQRSTUVW)
+ ///
+ public uint ReadMode;
+
+ ///
+ /// Upper 4 bits of ADR/Control
+ ///
+ public uint Control;
+
+ ///
+ /// 12-letter/digit string (may be empty)
+ ///
+ public string IsrcCode;
+
+ ///
+ /// Any non-zero is valid?
+ ///
+ public uint IsrcValidFlag;
+
+ ///
+ /// Only present on last track of a session (0 = Audio/CD-DA, 1 = Mode1/CD-ROM, 2 = Mode2/CD-XA)
+ ///
+ public uint SessionType;
+ }
+
+ ///
+ /// Represents a disc info block from a CDI file
+ ///
+ public class CDIDiscInfo : CDITrackHeader
+ {
+ ///
+ /// Total number of sectors
+ ///
+ public uint DiscSize;
+
+ ///
+ /// probably junk for non-ISO data discs
+ ///
+ public string VolumeId;
+
+ ///
+ /// 13-digit string (may be empty)
+ ///
+ public string Ean13Code;
+
+ ///
+ /// Any non-zero is valid?
+ ///
+ public uint Ean13CodeValid;
+
+ ///
+ /// CD text (for lead-in?)
+ ///
+ public string CdText;
+ }
+
+ public class CDIParseException : Exception
+ {
+ public CDIParseException(string message) : base(message) { }
+ }
+
+ /// malformed cdi format
+ public static CDIFile ParseFrom(Stream stream)
+ {
+ var cdif = new CDIFile();
+ using var br = new BinaryReader(stream);
+
+ try
+ {
+ stream.Seek(-4, SeekOrigin.End);
+ cdif.Entrypoint = br.ReadUInt32();
+
+ stream.Seek(-cdif.Entrypoint, SeekOrigin.End);
+
+ cdif.NumSessions = br.ReadByte();
+ if (cdif.NumSessions == 0)
+ {
+ throw new CDIParseException("Malformed CDI format: 0 sessions!");
+ }
+
+ void ParseTrackHeader(CDITrackHeader header)
+ {
+ stream.Seek(15, SeekOrigin.Current); // unknown bytes
+ header.NumTracks = br.ReadByte();
+ var pathLen = br.ReadByte();
+ header.Path = br.ReadStringFixedUtf8(pathLen);
+ stream.Seek(29, SeekOrigin.Current); // unknown bytes
+ header.MediumType = br.ReadUInt16();
+ switch (header.MediumType)
+ {
+ case 0x0038:
+ throw new CDIParseException("Malformed CDI format: DVD was specified, but this is not supported!");
+ case 0x0098:
+ return;
+ default:
+ throw new CDIParseException("Malformed CDI format: Invalid medium type!");
+ }
+ }
+
+ for (var i = 0; i <= cdif.NumSessions; i++)
+ {
+ var session = new CDISession();
+ stream.Seek(1, SeekOrigin.Current); // unknown byte
+ session.NumTracks = br.ReadByte();
+ stream.Seek(13, SeekOrigin.Current); // unknown bytes
+ cdif.Sessions.Add(session);
+
+ // the last session block should have 0 tracks (as it indicates no more sessions)
+ if (session.NumTracks == 0 && i != cdif.NumSessions)
+ {
+ throw new CDIParseException("Malformed CDI format: No tracks in session!");
+ }
+
+ if (session.NumTracks + cdif.Tracks.Count > 99)
+ {
+ throw new CDIParseException("Malformed CDI format: More than 99 tracks on disc!");
+ }
+
+ for (var j = 0; j < session.NumTracks; j++)
+ {
+ var track = new CDITrack();
+ ParseTrackHeader(track);
+
+ var indexes = br.ReadUInt16();
+ if (indexes < 2) // We should have at least 2 indexes (one pre-gap, and one "real" one)
+ {
+ throw new CDIParseException("Malformed CDI format: Less than 2 indexes in track!");
+ }
+ for (var k = 0; k < indexes; k++)
+ {
+ track.IndexSectorCounts.Add(br.ReadUInt32());
+ }
+
+ var numCdTextBlocks = br.ReadUInt32();
+ for (var k = 0; k < numCdTextBlocks; k++)
+ {
+ var cdTextBlock = new CDICDText();
+ for (var l = 0; l < 18; l++)
+ {
+ var cdTextLen = br.ReadByte();
+ if (cdTextLen > 0)
+ {
+ cdTextBlock.CdTexts.Add(br.ReadStringFixedUtf8(cdTextLen));
+ }
+ }
+ track.CdTextBlocks.Add(cdTextBlock);
+ }
+
+ stream.Seek(2, SeekOrigin.Current); // unknown bytes
+ track.TrackMode = br.ReadByte();
+ if (track.TrackMode > 2)
+ {
+ throw new CDIParseException("Malformed CDI format: Invalid track mode!");
+ }
+
+ stream.Seek(7, SeekOrigin.Current); // unknown bytes
+ track.SessionNumber = br.ReadUInt32();
+ if (track.SessionNumber != i)
+ {
+ throw new CDIParseException("Malformed CDI format: Session number mismatch!");
+ }
+
+ track.TrackNumber = br.ReadUInt32();
+ if (track.TrackNumber != j) // I think this is relative to the session?
+ {
+ throw new CDIParseException("Malformed CDI format: Track number mismatch!");
+ }
+
+ track.TrackStartAddress = br.ReadUInt32();
+ track.TrackLength = br.ReadUInt32();
+ stream.Seek(16, SeekOrigin.Current); // unknown bytes
+
+ track.ReadMode = br.ReadUInt32();
+ if (track.ReadMode > 4)
+ {
+ throw new CDIParseException("Malformed CDI format: Invalid read mode!");
+ }
+
+ track.Control = br.ReadUInt32();
+ if ((track.Control & ~0xF) != 0)
+ {
+ throw new CDIParseException("Malformed CDI format: Invalid control!");
+ }
+
+ stream.Seek(1, SeekOrigin.Current); // unknown byte
+ var redundantTrackLen = br.ReadUInt32();
+ if (track.TrackLength != redundantTrackLen)
+ {
+ throw new CDIParseException("Malformed CDI format: Track length mismatch!");
+ }
+
+ stream.Seek(4, SeekOrigin.Current); // unknown bytes
+ track.IsrcCode = br.ReadStringFixedUtf8(12);
+ track.IsrcValidFlag = br.ReadUInt32();
+ if (track.IsrcValidFlag == 0)
+ {
+ track.IsrcCode = string.Empty;
+ }
+
+ stream.Seek(87, SeekOrigin.Current); // unknown bytes
+ track.SessionType = br.ReadByte();
+ switch (track.SessionType)
+ {
+ case > 2:
+ throw new CDIParseException("Malformed CDI format: Invalid session type!");
+ case > 0 when j != session.NumTracks - 1:
+ throw new CDIParseException("Malformed CDI format: Session type was specified, but this is only supposed to be present on the last track!");
+ }
+
+ stream.Seek(5, SeekOrigin.Current); // unknown bytes
+ var notLastTrackInSession = br.ReadByte();
+ switch (notLastTrackInSession)
+ {
+ case 0 when j != session.NumTracks - 1:
+ throw new CDIParseException("Malformed CDI format: Track was specified to be the last track of the session, but more tracks are available!");
+ case > 1:
+ throw new CDIParseException("Malformed CDI format: Invalid not last track of session flag!");
+ }
+
+ stream.Seek(5, SeekOrigin.Current); // unknown bytes
+ // well, the last 4 bytes here are said to be the "address for last track of a session? (otherwise 00,00,FF,FF)"
+ // except I'm not sure what the address is meant to be, by bytes? by sectors? relative to file? relative session start?
+ // for now I just ignore it
+
+ cdif.Tracks.Add(track);
+ }
+ }
+
+ ParseTrackHeader(cdif.DiscInfo);
+ cdif.DiscInfo.DiscSize = br.ReadUInt32();
+ if (cdif.DiscInfo.DiscSize != cdif.Tracks.Sum(t => t.TrackLength))
+ {
+ //throw new CDIParseException("Malformed CDI format: Disc size mismatch!");
+ //this seems to be wrong?
+ }
+
+ var volumeIdLen = br.ReadByte();
+ cdif.DiscInfo.VolumeId = br.ReadStringFixedUtf8(volumeIdLen);
+ stream.Seek(9, SeekOrigin.Current); // unknown bytes
+
+ cdif.DiscInfo.Ean13Code = br.ReadStringFixedUtf8(13);
+ cdif.DiscInfo.Ean13CodeValid = br.ReadUInt32();
+ if (cdif.DiscInfo.Ean13CodeValid == 0)
+ {
+ cdif.DiscInfo.Ean13Code = string.Empty;
+ }
+
+ var cdTextLengh = br.ReadUInt32();
+ if (cdTextLengh > int.MaxValue)
+ {
+ // suppose technically this might not be considered too large purely going off the format
+ // but it's a bit silly to have a >2GB string so this is probably not valid
+ throw new CDIParseException("Malformed CDI format: CD text too large!");
+ }
+
+ cdif.DiscInfo.CdText = br.ReadStringFixedUtf8((int)cdTextLengh);
+ stream.Seek(12, SeekOrigin.Current); // unknown bytes
+
+ if (cdif.Tracks.Any(track => track.NumTracks != cdif.Tracks.Count) || cdif.DiscInfo.NumTracks != cdif.Tracks.Count)
+ {
+ throw new CDIParseException("Malformed CDI format: Total track number mismatch!");
+ }
+
+ if (stream.Position != stream.Length - 4)
+ {
+ throw new CDIParseException("Malformed CDI format: Did not reach end of footer after parsing!");
+ }
+
+ return cdif;
+ }
+ catch (EndOfStreamException)
+ {
+ throw new CDIParseException("Malformed CDI format: Unexpected stream end!");
+ }
+ }
+
+ public class LoadResults
+ {
+ public CDIFile ParsedCDIFile;
+ public bool Valid;
+ public CDIParseException FailureException;
+ public string CdiPath;
+ }
+
+ public static LoadResults LoadCDIPath(string path)
+ {
+ var ret = new LoadResults
+ {
+ CdiPath = path
+ };
+ try
+ {
+ if (!File.Exists(path)) throw new CDIParseException("Malformed CDI format: nonexistent CDI file!");
+
+ CDIFile cdif;
+ using (var infCDI = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
+ cdif = ParseFrom(infCDI);
+
+ ret.ParsedCDIFile = cdif;
+ ret.Valid = true;
+ }
+ catch (CDIParseException ex)
+ {
+ ret.FailureException = ex;
+ }
+
+ return ret;
+ }
+
+ private class SS_CDI_RawQ : SS_Base
+ {
+ public override void Synth(SectorSynthJob job)
+ {
+ Blob.Read(BlobOffset, job.DestBuffer2448, job.DestOffset, 2352);
+
+ if ((job.Parts & ESectorSynthPart.SubchannelP) != 0)
+ {
+ SynthUtils.SubP(job.DestBuffer2448, job.DestOffset + 2352, Pause);
+ }
+
+ // Q is present in the blob and non-interleaved
+ if ((job.Parts & ESectorSynthPart.SubchannelQ) != 0)
+ {
+ Blob.Read(BlobOffset, job.DestBuffer2448, job.DestOffset + 2352 + 12, 12);
+ }
+
+ //clear R-W if needed
+ if ((job.Parts & ESectorSynthPart.Subchannel_RSTUVW) != 0)
+ {
+ Array.Clear(job.DestBuffer2448, job.DestOffset + 2352 + 12 + 12, 12 * 6);
+ }
+
+ //subcode has been generated deinterleaved; we may still need to interleave it
+ if ((job.Parts & ESectorSynthPart.SubcodeAny) != 0 && (job.Parts & ESectorSynthPart.SubcodeDeinterleave) == 0)
+ {
+ SynthUtils.InterleaveSubcodeInplace(job.DestBuffer2448, job.DestOffset + 2352);
+ }
+ }
+ }
+
+ private class SS_CDI_RawPQRSTUVW : SS_Base
+ {
+ public override void Synth(SectorSynthJob job)
+ {
+ // all subcode is present and interleaved, just read it all
+ Blob.Read(BlobOffset, job.DestBuffer2448, job.DestOffset, 2448);
+
+ // deinterleave it if needed
+ if ((job.Parts & ESectorSynthPart.SubcodeDeinterleave) != 0)
+ {
+ SynthUtils.DeinterleaveSubcodeInplace(job.DestBuffer2448, job.DestOffset + 2352);
+ }
+ }
+ }
+
+ /// file not found
+ public static Disc LoadCDIToDisc(string cdiPath, DiscMountPolicy IN_DiscMountPolicy)
+ {
+ var loadResults = LoadCDIPath(cdiPath);
+ if (!loadResults.Valid)
+ throw loadResults.FailureException;
+
+ var disc = new Disc();
+ var cdif = loadResults.ParsedCDIFile;
+
+ IBlob cdiBlob = new Blob_RawFile { PhysicalPath = cdiPath };
+ disc.DisposableResources.Add(cdiBlob);
+
+ var trackOffset = 0;
+ var blobOffset = 0;
+ for (var i = 0; i < cdif.NumSessions; i++)
+ {
+ var session = new DiscSession { Number = i + 1 };
+ for (var j = 0; j < cdif.Sessions[i].NumTracks; j++)
+ {
+ var track = cdif.Tracks[trackOffset + j];
+
+ 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;
+ q.SetStatus(kADR, (EControlQ)track.Control);
+ q.q_tno = BCD2.FromDecimal(0);
+ q.q_index = BCD2.FromDecimal(trackOffset + j + 1);
+ q.Timestamp = 0;
+ q.zero = 0;
+ q.AP_Timestamp = disc._Sectors.Count;
+ q.q_crc = 0;
+ return new() { QData = q };
+ }
+
+ var sectorSize = track.ReadMode switch
+ {
+ 0 => 2048,
+ 1 => 2336,
+ 2 => 2352,
+ 3 => 2368,
+ 4 => 2448,
+ _ => throw new InvalidOperationException()
+ };
+ var curIndex = 0;
+ var relMSF = -track.IndexSectorCounts[0];
+ var indexSectorOffset = 0U;
+ for (var k = 0; k < track.TrackLength; k++)
+ {
+ if (track.IndexSectorCounts[curIndex] == k - indexSectorOffset)
+ {
+ indexSectorOffset += track.IndexSectorCounts[curIndex];
+ curIndex++;
+ if (track.IndexSectorCounts.Count == curIndex)
+ {
+ throw new CDIParseException("Malformed CDI Format: Reached end of index list unexpectedly");
+ }
+ if (curIndex == 1)
+ {
+ session.RawTOCEntries.Add(EmitRawTOCEntry());
+ }
+ }
+ //note that CDIs contain the pregap data themselves...
+ SS_Base synth = track.ReadMode switch
+ {
+ 0 => new SS_Mode1_2048(),
+ 1 => throw new NotSupportedException("Mode2/2336"), // TODO
+ 2 => new SS_2352(),
+ 3 => new SS_CDI_RawQ(),
+ 4 => new SS_CDI_RawPQRSTUVW(),
+ _ => throw new InvalidOperationException()
+ };
+ synth.Blob = cdiBlob;
+ synth.BlobOffset = blobOffset;
+ synth.Policy = IN_DiscMountPolicy;
+ //TODO: subchannel here is all wrong for gaps, probably
+ const byte kADR = 1;
+ synth.sq.SetStatus(kADR, (EControlQ)track.Control);
+ synth.sq.q_tno = BCD2.FromDecimal(trackOffset + j + 1);
+ synth.sq.q_index = BCD2.FromDecimal(curIndex);
+ synth.sq.Timestamp = (int)relMSF;
+ synth.sq.zero = 0;
+ synth.sq.AP_Timestamp = disc._Sectors.Count;
+ synth.sq.q_crc = 0;
+ synth.Pause = curIndex == 0;
+ disc._Sectors.Add(synth);
+ blobOffset += sectorSize;
+ if (curIndex != 0)
+ {
+ relMSF++;
+ }
+ }
+ }
+
+ var TOCMiscInfo = new Synthesize_A0A1A2_Job(
+ firstRecordedTrackNumber: trackOffset + 1,
+ lastRecordedTrackNumber: trackOffset + cdif.Sessions[i].NumTracks + 1,
+ sessionFormat: (SessionFormat)(cdif.Tracks[trackOffset + cdif.Sessions[i].NumTracks - 1].SessionType * 0x10),
+ leadoutTimestamp: disc._Sectors.Count);
+ TOCMiscInfo.Run(session.RawTOCEntries);
+
+ disc.Sessions.Add(session);
+ trackOffset += cdif.Sessions[i].NumTracks;
+ }
+
+ return disc;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BizHawk.Emulation.DiscSystem/DiscIdentifier.cs b/src/BizHawk.Emulation.DiscSystem/DiscIdentifier.cs
index bbd74d7063..60be54f3b4 100644
--- a/src/BizHawk.Emulation.DiscSystem/DiscIdentifier.cs
+++ b/src/BizHawk.Emulation.DiscSystem/DiscIdentifier.cs
@@ -403,9 +403,15 @@ namespace BizHawk.Emulation.DiscSystem
if (_disc.Sessions.Count > 2 && !_disc.Sessions[2].TOC.TOCItems[_disc.Sessions[2].TOC.FirstRecordedTrackNumber].IsData)
{
var data = new byte[2352];
- _dsr.ReadLBA_2352(_disc.Sessions[2].Tracks[1].LBA, data, 0);
- var s = Encoding.ASCII.GetString(data);
- return s.Contains("ATARI APPROVED DATA HEADER ATRI") || s.Contains("TARA IPARPVODED TA AEHDAREA RT");
+ for (var i = 0; i < 2; i++)
+ {
+ _dsr.ReadLBA_2352(_disc.Sessions[2].Tracks[1].LBA + i, data, 0);
+ var s = Encoding.ASCII.GetString(data);
+ if (s.Contains("ATARI APPROVED DATA HEADER ATRI") || s.Contains("TARA IPARPVODED TA AEHDAREA RT"))
+ {
+ return true;
+ }
+ }
}
return false;
diff --git a/src/BizHawk.Emulation.DiscSystem/DiscMountJob.cs b/src/BizHawk.Emulation.DiscSystem/DiscMountJob.cs
index 2b2e7ec383..5c1b371609 100644
--- a/src/BizHawk.Emulation.DiscSystem/DiscMountJob.cs
+++ b/src/BizHawk.Emulation.DiscSystem/DiscMountJob.cs
@@ -193,6 +193,9 @@ namespace BizHawk.Emulation.DiscSystem
case ".ccd":
OUT_Disc = CCD_Format.LoadCCDToDisc(IN_FromPath, IN_DiscMountPolicy);
break;
+ case ".cdi":
+ OUT_Disc = CDI_Format.LoadCDIToDisc(IN_FromPath, IN_DiscMountPolicy);
+ break;
case ".cue":
LoadCue(dir, File.ReadAllText(IN_FromPath));
break;