From 753dd0480fcbe75a6dc3e346338dbf94ce31c374 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 7 Mar 2021 16:26:12 +1000 Subject: [PATCH] CDImage: Add support for loading mds/mdf images --- src/common/CMakeLists.txt | 1 + src/common/cd_image.cpp | 4 + src/common/cd_image.h | 1 + src/common/cd_image_mds.cpp | 267 ++++++++++++++++++++++++++ src/common/common.vcxproj | 1 + src/common/common.vcxproj.filters | 1 + src/core/system.cpp | 8 +- src/duckstation-qt/mainwindow.cpp | 7 +- src/frontend-common/fullscreen_ui.cpp | 3 +- 9 files changed, 285 insertions(+), 8 deletions(-) create mode 100644 src/common/cd_image_mds.cpp diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 38ec64e52..5a7cd1da6 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -17,6 +17,7 @@ add_library(common cd_image_hasher.cpp cd_image_hasher.h cd_image_memory.cpp + cd_image_mds.cpp cd_subchannel_replacement.cpp cd_subchannel_replacement.h cd_xa.cpp diff --git a/src/common/cd_image.cpp b/src/common/cd_image.cpp index 9e266b585..20a12812f 100644 --- a/src/common/cd_image.cpp +++ b/src/common/cd_image.cpp @@ -46,6 +46,10 @@ std::unique_ptr CDImage::Open(const char* filename) { return OpenEcmImage(filename); } + else if (CASE_COMPARE(extension, ".mds") == 0) + { + return OpenMdsImage(filename); + } #undef CASE_COMPARE diff --git a/src/common/cd_image.h b/src/common/cd_image.h index b6ae5c086..42493ec6b 100644 --- a/src/common/cd_image.h +++ b/src/common/cd_image.h @@ -196,6 +196,7 @@ public: static std::unique_ptr OpenCueSheetImage(const char* filename); static std::unique_ptr OpenCHDImage(const char* filename); static std::unique_ptr OpenEcmImage(const char* filename); + static std::unique_ptr OpenMdsImage(const char* filename); static std::unique_ptr CreateMemoryImage(CDImage* image, ProgressCallback* progress = ProgressCallback::NullProgressCallback); diff --git a/src/common/cd_image_mds.cpp b/src/common/cd_image_mds.cpp new file mode 100644 index 000000000..e589ec48b --- /dev/null +++ b/src/common/cd_image_mds.cpp @@ -0,0 +1,267 @@ +#include "assert.h" +#include "cd_image.h" +#include "cd_subchannel_replacement.h" +#include "file_system.h" +#include "log.h" +#include +#include +#include +Log_SetChannel(CDImageMds); + +#pragma pack(push, 1) +struct TrackEntry +{ + u8 track_type; + u8 has_subchannel_data; + u8 unk1; + u8 unk2; + u8 track_number; + u8 unk3[4]; + u8 start_m; + u8 start_s; + u8 start_f; + u32 extra_offset; + u8 unk4[24]; + u32 track_offset_in_mdf; + u8 unk5[36]; +}; +static_assert(sizeof(TrackEntry) == 0x50, "TrackEntry is 0x50 bytes"); +#pragma pack(pop) + +class CDImageMds : public CDImage +{ +public: + CDImageMds(); + ~CDImageMds() override; + + bool OpenAndParse(const char* filename); + + bool ReadSubChannelQ(SubChannelQ* subq) override; + bool HasNonStandardSubchannel() const override; + +protected: + bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override; + +private: + std::FILE* m_mdf_file = nullptr; + u64 m_mdf_file_position = 0; + CDSubChannelReplacement m_sbi; +}; + +CDImageMds::CDImageMds() = default; + +CDImageMds::~CDImageMds() +{ + if (m_mdf_file) + std::fclose(m_mdf_file); +} + +bool CDImageMds::OpenAndParse(const char* filename) +{ + std::FILE* mds_fp = FileSystem::OpenCFile(filename, "rb"); + if (!mds_fp) + { + Log_ErrorPrintf("Failed to open mds '%s': errno %d", filename, errno); + return false; + } + + std::optional> mds_data_opt(FileSystem::ReadBinaryFile(mds_fp)); + std::fclose(mds_fp); + if (!mds_data_opt.has_value() || mds_data_opt->size() < 0x54) + { + Log_ErrorPrintf("Failed to read mds file '%s'", filename); + return false; + } + + std::string mdf_filename(FileSystem::ReplaceExtension(filename, "mdf")); + m_mdf_file = FileSystem::OpenCFile(mdf_filename.c_str(), "rb"); + if (!m_mdf_file) + { + Log_ErrorPrintf("Failed to open mdf file '%s': errno %d", mdf_filename.c_str(), errno); + return false; + } + + const std::vector& mds = mds_data_opt.value(); + static constexpr char expected_signature[] = "MEDIA DESCRIPTOR"; + if (std::memcmp(&mds[0], expected_signature, sizeof(expected_signature) - 1) != 0) + { + Log_ErrorPrintf("Incorrect signature in '%s'", filename); + return false; + } + + u32 session_offset; + std::memcpy(&session_offset, &mds[0x50], sizeof(session_offset)); + if ((session_offset + 24) > mds.size()) + { + Log_ErrorPrintf("Invalid session offset in '%s'", filename); + return false; + } + + u16 track_count; + u32 track_offset; + std::memcpy(&track_count, &mds[session_offset + 14], sizeof(track_count)); + std::memcpy(&track_offset, &mds[session_offset + 20], sizeof(track_offset)); + if (track_count > 99 || track_offset >= mds.size()) + { + Log_ErrorPrintf("Invalid track count/block offset %u/%u in '%s'", track_count, track_offset, filename); + return false; + } + + while ((track_offset + sizeof(TrackEntry)) <= mds.size()) + { + TrackEntry track; + std::memcpy(&track, &mds[track_offset], sizeof(track)); + if (track.track_number < 0xA0) + break; + + track_offset += sizeof(TrackEntry); + } + + for (u32 track_number = 1; track_number <= track_count; track_number++) + { + if ((track_offset + sizeof(TrackEntry)) > mds.size()) + { + Log_ErrorPrintf("End of file in '%s' at track %u", filename, track_number); + return false; + } + + TrackEntry track; + std::memcpy(&track, &mds[track_offset], sizeof(track)); + if (PackedBCDToBinary(track.track_number) != track_number) + { + Log_ErrorPrintf("Unexpected track number 0x%02X in track %u", track.track_number, track_number); + return false; + } + + const bool contains_subchannel = (track.has_subchannel_data != 0); + const u32 track_sector_size = (contains_subchannel ? 2448 : RAW_SECTOR_SIZE); + const TrackMode mode = (track.track_type == 0xA9) ? TrackMode::Audio : TrackMode::Mode2Raw; + + if ((track.extra_offset + sizeof(u32) + sizeof(u32)) > mds.size()) + { + Log_ErrorPrintf("Invalid extra offset %u in track %u", track.extra_offset, track_number); + return false; + } + + u32 track_start_lba = Position::FromBCD(track.start_m, track.start_s, track.start_f).ToLBA(); + u32 track_file_offset = track.track_offset_in_mdf; + + u32 track_pregap; + u32 track_length; + std::memcpy(&track_pregap, &mds[track.extra_offset], sizeof(track_pregap)); + std::memcpy(&track_length, &mds[track.extra_offset + sizeof(u32)], sizeof(track_length)); + + // precompute subchannel q flags for the whole track + // todo: pull from mds? + SubChannelQ::Control control{}; + control.data = mode != TrackMode::Audio; + + // create the index for the pregap + if (track_pregap > 0) + { + if (track_pregap > track_start_lba) + { + Log_ErrorPrintf("Track pregap %u is too large for start lba %u", track_pregap, track_start_lba); + return false; + } + + Index pregap_index = {}; + pregap_index.start_lba_on_disc = track_start_lba - track_pregap; + pregap_index.start_lba_in_track = static_cast(-static_cast(track_pregap)); + pregap_index.length = track_pregap; + pregap_index.track_number = track_number; + pregap_index.index_number = 0; + pregap_index.mode = mode; + pregap_index.control.bits = control.bits; + pregap_index.is_pregap = true; + + const bool pregap_in_file = (track_number > 1); + if (pregap_in_file) + { + pregap_index.file_index = 0; + pregap_index.file_offset = track_file_offset; + pregap_index.file_sector_size = track_sector_size; + track_file_offset += track_pregap * track_sector_size; + } + + m_indices.push_back(pregap_index); + } + + // add the track itself + m_tracks.push_back(Track{static_cast(track_number), track_start_lba, static_cast(m_indices.size()), + static_cast(track_length), mode, control}); + + // how many indices in this track? + Index last_index; + last_index.start_lba_on_disc = track_start_lba; + last_index.start_lba_in_track = 0; + last_index.track_number = track_number; + last_index.index_number = 1; + last_index.file_index = 0; + last_index.file_sector_size = track_sector_size; + last_index.file_offset = track_file_offset; + last_index.mode = mode; + last_index.control.bits = control.bits; + last_index.is_pregap = false; + last_index.length = track_length; + m_indices.push_back(last_index); + } + + if (m_tracks.empty()) + { + Log_ErrorPrintf("File '%s' contains no tracks", filename); + return false; + } + + m_lba_count = m_tracks.back().start_lba + m_tracks.back().length; + AddLeadOutIndex(); + + m_sbi.LoadSBI(FileSystem::ReplaceExtension(filename, "sbi").c_str()); + + return Seek(1, Position{0, 0, 0}); +} + +bool CDImageMds::ReadSubChannelQ(SubChannelQ* subq) +{ + if (m_sbi.GetReplacementSubChannelQ(m_position_on_disc, subq)) + return true; + + return CDImage::ReadSubChannelQ(subq); +} + +bool CDImageMds::HasNonStandardSubchannel() const +{ + return (m_sbi.GetReplacementSectorCount() > 0); +} + +bool CDImageMds::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) +{ + const u64 file_position = index.file_offset + (static_cast(lba_in_index) * index.file_sector_size); + if (m_mdf_file_position != file_position) + { + if (std::fseek(m_mdf_file, static_cast(file_position), SEEK_SET) != 0) + return false; + + m_mdf_file_position = file_position; + } + + // we don't want the subchannel data + const u32 read_size = RAW_SECTOR_SIZE; + if (std::fread(buffer, read_size, 1, m_mdf_file) != 1) + { + std::fseek(m_mdf_file, static_cast(m_mdf_file_position), SEEK_SET); + return false; + } + + m_mdf_file_position += read_size; + return true; +} + +std::unique_ptr CDImage::OpenMdsImage(const char* filename) +{ + std::unique_ptr image = std::make_unique(); + if (!image->OpenAndParse(filename)) + return {}; + + return image; +} diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj index 99a32b6f7..da63992a4 100644 --- a/src/common/common.vcxproj +++ b/src/common/common.vcxproj @@ -132,6 +132,7 @@ + diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters index 1273c0f38..124f84eb0 100644 --- a/src/common/common.vcxproj.filters +++ b/src/common/common.vcxproj.filters @@ -211,6 +211,7 @@ + diff --git a/src/core/system.cpp b/src/core/system.cpp index 0dcab2e75..f19405be1 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -296,10 +296,10 @@ bool IsM3UFileName(const char* path) bool IsLoadableFilename(const char* path) { - static constexpr auto extensions = make_array(".bin", ".cue", ".img", ".iso", ".chd", ".ecm", // discs - ".exe", ".psexe", // exes - ".psf", ".minipsf", // psf - ".m3u" // playlists + static constexpr auto extensions = make_array(".bin", ".cue", ".img", ".iso", ".chd", ".ecm", ".mds", // discs + ".exe", ".psexe", // exes + ".psf", ".minipsf", // psf + ".m3u" // playlists ); const char* extension = std::strrchr(path, '.'); if (!extension) diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 1708d09b0..b84d94b75 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -33,9 +33,10 @@ static constexpr char DISC_IMAGE_FILTER[] = QT_TRANSLATE_NOOP( "MainWindow", - "All File Types (*.bin *.img *.iso *.cue *.chd *.ecm *.exe *.psexe *.psf *.minipsf *.m3u);;Single-Track Raw Images " - "(*.bin *.img *.iso);;Cue Sheets (*.cue);;MAME CHD Images (*.chd);;Error Code Modeler Images (*.ecm);;PlayStation " - "Executables (*.exe *.psexe);;Portable Sound Format Files (*.psf *.minipsf);;Playlists (*.m3u)"); + "All File Types (*.bin *.img *.iso *.cue *.chd *.ecm *.mds *.exe *.psexe *.psf *.minipsf *.m3u);;Single-Track Raw " + "Images (*.bin *.img *.iso);;Cue Sheets (*.cue);;MAME CHD Images (*.chd);;Error Code Modeler Images (*.ecm);;Media " + "Descriptor Sidecar Images (*.mds);;PlayStation Executables (*.exe *.psexe);;Portable Sound Format Files (*.psf " + "*.minipsf);;Playlists (*.m3u)"); ALWAYS_INLINE static QString getWindowTitle() { diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp index 7e4b1e362..2410cfece 100644 --- a/src/frontend-common/fullscreen_ui.cpp +++ b/src/frontend-common/fullscreen_ui.cpp @@ -513,7 +513,8 @@ bool InvalidateCachedTexture(const std::string& path) static ImGuiFullscreen::FileSelectorFilters GetDiscImageFilters() { - return {"*.bin", "*.cue", "*.iso", "*.img", "*.chd", "*.ecm", "*.psexe", "*.exe", "*.psf", "*.minipsf", "*.m3u"}; + return {"*.bin", "*.cue", "*.iso", "*.img", "*.chd", "*.ecm", + "*.mds", "*.psexe", "*.exe", "*.psf", "*.minipsf", "*.m3u"}; } static void DoStartPath(const std::string& path, bool allow_resume)