diff --git a/src/common/cd_image.h b/src/common/cd_image.h index 707cafc46..fa7e8ef5c 100644 --- a/src/common/cd_image.h +++ b/src/common/cd_image.h @@ -159,6 +159,7 @@ public: static std::unique_ptr Open(const char* filename); static std::unique_ptr OpenBinImage(const char* filename); static std::unique_ptr OpenCueSheetImage(const char* filename); + static std::unique_ptr OpenCHDImage(const char* filename); // Accessors. const std::string& GetFileName() const { return m_filename; } diff --git a/src/common/cd_image_chd.cpp b/src/common/cd_image_chd.cpp new file mode 100644 index 000000000..3b4cb89eb --- /dev/null +++ b/src/common/cd_image_chd.cpp @@ -0,0 +1,327 @@ +#ifdef _MSC_VER +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include "assert.h" +#include "cd_image.h" +#include "cd_subchannel_replacement.h" +#include "cpu_detect.h" +#include "file_system.h" +#include "libchdr/chd.h" +#include "log.h" +#include +#include +#include +#include +#include +Log_SetChannel(CDImageCHD); + +static std::optional ParseTrackModeString(const char* str) +{ + if (std::strncmp(str, "MODE2_FORM_MIX", 14) == 0) + return CDImage::TrackMode::Mode2FormMix; + else if (std::strncmp(str, "MODE2_FORM1", 10) == 0) + return CDImage::TrackMode::Mode2Form1; + else if (std::strncmp(str, "MODE2_FORM2", 10) == 0) + return CDImage::TrackMode::Mode2Form2; + else if (std::strncmp(str, "MODE2_RAW", 9) == 0) + return CDImage::TrackMode::Mode2Raw; + else if (std::strncmp(str, "MODE1_RAW", 9) == 0) + return CDImage::TrackMode::Mode1Raw; + else if (std::strncmp(str, "MODE1", 5) == 0) + return CDImage::TrackMode::Mode1; + else if (std::strncmp(str, "MODE2", 5) == 0) + return CDImage::TrackMode::Mode2; + else if (std::strncmp(str, "AUDIO", 5) == 0) + return CDImage::TrackMode::Audio; + else + return std::nullopt; +} + +class CDImageCHD : public CDImage +{ +public: + CDImageCHD(); + ~CDImageCHD() override; + + bool Open(const char* filename); + + bool ReadSubChannelQ(SubChannelQ* subq) override; + +protected: + bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override; + +private: + enum : u32 + { + CHD_SECTOR_DATA_SIZE = 2352 + 96, + }; + + bool ReadHunk(u32 hunk_index); + + chd_file* m_chd = nullptr; + u32 m_hunk_size = 0; + u32 m_sectors_per_hunk = 0; + + std::vector m_hunk_buffer; + u32 m_current_hunk_index = static_cast(-1); + + CDSubChannelReplacement m_sbi; +}; + +CDImageCHD::CDImageCHD() = default; + +CDImageCHD::~CDImageCHD() +{ + if (m_chd) + chd_close(m_chd); +} + +bool CDImageCHD::Open(const char* filename) +{ + chd_error err = chd_open(filename, CHD_OPEN_READ, nullptr, &m_chd); + if (err != CHDERR_NONE) + { + Log_ErrorPrintf("Failed to open CHD '%s': %s", chd_error_string(err)); + return false; + } + + const chd_header* header = chd_get_header(m_chd); + m_hunk_size = header->hunkbytes; + if ((m_hunk_size % CHD_SECTOR_DATA_SIZE) != 0) + { + Log_ErrorPrintf("Hunk size (%u) is not a multiple of %u", m_hunk_size, CHD_SECTOR_DATA_SIZE); + return false; + } + + m_sectors_per_hunk = m_hunk_size / CHD_SECTOR_DATA_SIZE; + m_hunk_buffer.resize(m_hunk_size); + m_filename = filename; + + u32 disc_lba = 0; + u64 disc_frame = 0; + + // "last track" subchannel q - used for the pregap + SubChannelQ::Control last_track_control{}; + + // for each track.. + int num_tracks = 0; + for (;;) + { + char metadata_str[256]; + char type_str[256]; + char subtype_str[256]; + char pgtype_str[256]; + char pgsub_str[256]; + u32 metadata_length; + + int track_num = 0, frames = 0, pad = 0, pregap_frames = 0, postgap_frames = 0; + err = chd_get_metadata(m_chd, CDROM_TRACK_METADATA2_TAG, num_tracks, metadata_str, sizeof(metadata_str), + &metadata_length, nullptr, nullptr); + if (err == CHDERR_NONE) + { + if (std::sscanf(metadata_str, CDROM_TRACK_METADATA2_FORMAT, &track_num, type_str, subtype_str, &frames, + &pregap_frames, pgtype_str, pgsub_str, &postgap_frames) != 8) + { + Log_ErrorPrintf("Invalid track v2 metadata: '%s'", metadata_str); + return false; + } + } + else + { + // try old version + err = chd_get_metadata(m_chd, CDROM_TRACK_METADATA_TAG, num_tracks, metadata_str, sizeof(metadata_str), + &metadata_length, nullptr, nullptr); + if (err != CHDERR_NONE) + { + // not found, so no more tracks + break; + } + + if (std::sscanf(metadata_str, CDROM_TRACK_METADATA_FORMAT, &track_num, type_str, subtype_str, &frames) != 4) + { + Log_ErrorPrintf("Invalid track metadata: '%s'", metadata_str); + return false; + } + } + + if (track_num != (num_tracks + 1)) + { + Log_ErrorPrintf("Incorrect track number at index %d, expected %d got %d", num_tracks, (num_tracks + 1), + track_num); + return false; + } + + std::optional mode = ParseTrackModeString(type_str); + if (!mode.has_value()) + { + Log_ErrorPrintf("Invalid track mode: '%s'", type_str); + return false; + } + + // precompute subchannel q flags for the whole track + SubChannelQ::Control control{}; + control.data = mode.value() != TrackMode::Audio; + + // two seconds pregap for track 1 is assumed if not specified + const bool pregap_in_file = (pregap_frames > 0 && pgtype_str[0] == 'V'); + if (pregap_frames <= 0 && mode != TrackMode::Audio) + pregap_frames = 2 * FRAMES_PER_SECOND; + + // create the index for the pregap + if (pregap_frames > 0) + { + Index pregap_index = {}; + pregap_index.start_lba_on_disc = disc_lba; + pregap_index.start_lba_in_track = static_cast(static_cast(-pregap_frames)); + pregap_index.length = pregap_frames; + pregap_index.track_number = track_num; + pregap_index.index_number = 0; + pregap_index.mode = mode.value(); + pregap_index.control.bits = (track_num > 1) ? last_track_control.bits : control.bits; + pregap_index.is_pregap = true; + + if (pregap_in_file) + { + if (pregap_frames > frames) + { + Log_ErrorPrintf("Pregap length %u exceeds track length %u", pregap_frames, frames); + return false; + } + + pregap_index.file_index = 0; + pregap_index.file_offset = disc_lba; + pregap_index.file_sector_size = CHD_SECTOR_DATA_SIZE; + disc_frame += pregap_frames; + frames -= pregap_frames; + } + + m_indices.push_back(pregap_index); + disc_lba += pregap_frames; + } + + // add the track itself + m_tracks.push_back(Track{static_cast(track_num), disc_lba, static_cast(m_indices.size()), + static_cast(frames), mode.value(), control}); + last_track_control.bits = control.bits; + + // how many indices in this track? + Index index = {}; + index.start_lba_on_disc = disc_lba; + index.start_lba_in_track = 0; + index.track_number = track_num; + index.index_number = 1; + index.file_index = 0; + index.file_sector_size = CHD_SECTOR_DATA_SIZE; + index.file_offset = disc_frame; + index.mode = mode.value(); + index.control.bits = control.bits; + index.is_pregap = false; + index.length = static_cast(frames); + m_indices.push_back(index); + + disc_lba += index.length; + disc_frame += index.length; + num_tracks++; + } + + m_lba_count = disc_lba; + + m_sbi.LoadSBI(FileSystem::ReplaceExtension(filename, "sbi").c_str()); + + return Seek(1, Position{0, 0, 0}); +} + +bool CDImageCHD::ReadSubChannelQ(SubChannelQ* subq) +{ + if (m_sbi.GetReplacementSubChannelQ(m_position_on_disc, subq->data)) + return true; + + // TODO: Read subchannel data from CHD + + return CDImage::ReadSubChannelQ(subq); +} + +// There's probably a more efficient way of doing this with vectorization... +ALWAYS_INLINE static void CopyAndSwap(void* dst_ptr, const u8* src_ptr, u32 data_size) +{ + u8* dst_ptr_byte = static_cast(dst_ptr); +#if defined(CPU_X64) || defined(CPU_AARCH64) + const u32 num_values = data_size / 8; + for (u32 i = 0; i < num_values; i++) + { + u64 value; + std::memcpy(&value, src_ptr, sizeof(value)); + value = ((value >> 8) & UINT64_C(0x00FF00FF00FF00FF)) | ((value << 8) & UINT64_C(0xFF00FF00FF00FF00)); + std::memcpy(dst_ptr_byte, &value, sizeof(value)); + src_ptr += sizeof(value); + dst_ptr_byte += sizeof(value); + } +#elif defined(CPU_X86) || defined(CPU_ARM) + const u32 num_values = data_size / 4; + for (u32 i = 0; i < num_values; i++) + { + u32 value; + std::memcpy(&value, src_ptr, sizeof(value)); + value = ((value >> 8) & UINT32_C(0x00FF00FF)) | ((value << 8) & UINT32_C(0xFF00FF00)); + std::memcpy(dst_ptr_byte, &value, sizeof(value)); + src_ptr += sizeof(value); + dst_ptr_byte += sizeof(value); + } +#else + const u32 num_values = data_size / sizeof(u16); + for (u32 i = 0; i < num_values; i++) + { + u16 value; + std::memcpy(&value, src_ptr, sizeof(value)); + value = (value << 8) | (value >> 8); + std::memcpy(dst_ptr_byte, &value, sizeof(value)); + src_ptr += sizeof(value); + dst_ptr_byte += sizeof(value); + } +#endif +} + +bool CDImageCHD::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) +{ + const u32 disc_frame = static_cast(index.file_offset) + lba_in_index; + const u32 hunk_index = static_cast(disc_frame / m_sectors_per_hunk); + const u32 hunk_offset = static_cast((disc_frame % m_sectors_per_hunk) * CHD_SECTOR_DATA_SIZE); + DebugAssert((m_hunk_size - hunk_offset) >= CHD_SECTOR_DATA_SIZE); + + if (m_current_hunk_index != hunk_index && !ReadHunk(hunk_index)) + return false; + + // Audio data is in big-endian, so we have to swap it for little endian hosts... + if (index.mode == TrackMode::Audio) + CopyAndSwap(buffer, &m_hunk_buffer[hunk_offset], RAW_SECTOR_SIZE); + else + std::memcpy(buffer, &m_hunk_buffer[hunk_offset], RAW_SECTOR_SIZE); + + return true; +} + +bool CDImageCHD::ReadHunk(u32 hunk_index) +{ + const chd_error err = chd_read(m_chd, hunk_index, m_hunk_buffer.data()); + if (err != CHDERR_NONE) + { + Log_ErrorPrintf("chd_read(%u) failed: %s", hunk_index, chd_error_string(err)); + + // data might have been partially written + m_current_hunk_index = static_cast(-1); + return false; + } + + m_current_hunk_index = hunk_index; + return true; +} + +std::unique_ptr CDImage::OpenCHDImage(const char* filename) +{ + std::unique_ptr image = std::make_unique(); + if (!image->Open(filename)) + return {}; + + return image; +} diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj index 5836429b3..8a993f302 100644 --- a/src/common/common.vcxproj +++ b/src/common/common.vcxproj @@ -76,6 +76,7 @@ + @@ -110,6 +111,9 @@ {43540154-9e1e-409c-834f-b84be5621388} + + {425d6c99-d1c8-43c2-b8ac-4d7b1d941017} + {6a4208ed-e3dc-41e1-81cd-f61025fc285a} @@ -251,7 +255,7 @@ WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\glad\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\glad\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 true @@ -277,7 +281,7 @@ _ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\glad\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\glad\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) Default false true @@ -306,7 +310,7 @@ WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\glad\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\glad\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 true @@ -332,7 +336,7 @@ _ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\glad\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\glad\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) Default false true @@ -362,7 +366,7 @@ true WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\glad\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\glad\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 false @@ -392,7 +396,7 @@ true WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\glad\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\glad\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true stdcpp17 @@ -422,7 +426,7 @@ true WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\glad\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\glad\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 false @@ -452,7 +456,7 @@ true WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\glad\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\glad\include;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true stdcpp17 diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters index bb91af0fc..84877fa9d 100644 --- a/src/common/common.vcxproj.filters +++ b/src/common/common.vcxproj.filters @@ -97,6 +97,7 @@ d3d11 +