duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

cd_image_chd.cpp (18517B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "cd_image.h"
      5 #include "cd_subchannel_replacement.h"
      6 
      7 #include "common/align.h"
      8 #include "common/assert.h"
      9 #include "common/error.h"
     10 #include "common/file_system.h"
     11 #include "common/gsvector.h"
     12 #include "common/hash_combine.h"
     13 #include "common/heap_array.h"
     14 #include "common/log.h"
     15 #include "common/path.h"
     16 #include "common/string_util.h"
     17 
     18 #include "fmt/format.h"
     19 #include "libchdr/cdrom.h"
     20 #include "libchdr/chd.h"
     21 
     22 #include <algorithm>
     23 #include <cerrno>
     24 #include <cstdio>
     25 #include <cstring>
     26 #include <limits>
     27 #include <mutex>
     28 #include <optional>
     29 
     30 Log_SetChannel(CDImageCHD);
     31 
     32 namespace {
     33 
     34 static std::optional<CDImage::TrackMode> ParseTrackModeString(const std::string_view str)
     35 {
     36   if (str == "MODE2_FORM_MIX")
     37     return CDImage::TrackMode::Mode2FormMix;
     38   else if (str == "MODE2_FORM1")
     39     return CDImage::TrackMode::Mode2Form1;
     40   else if (str == "MODE2_FORM2")
     41     return CDImage::TrackMode::Mode2Form2;
     42   else if (str == "MODE2_RAW")
     43     return CDImage::TrackMode::Mode2Raw;
     44   else if (str == "MODE1_RAW")
     45     return CDImage::TrackMode::Mode1Raw;
     46   else if (str == "MODE1")
     47     return CDImage::TrackMode::Mode1;
     48   else if (str == "MODE2")
     49     return CDImage::TrackMode::Mode2;
     50   else if (str == "AUDIO")
     51     return CDImage::TrackMode::Audio;
     52   else
     53     return std::nullopt;
     54 }
     55 
     56 static std::vector<std::pair<std::string, chd_header>> s_chd_hash_cache; // <filename, header>
     57 static std::recursive_mutex s_chd_hash_cache_mutex;
     58 
     59 class CDImageCHD : public CDImage
     60 {
     61 public:
     62   CDImageCHD();
     63   ~CDImageCHD() override;
     64 
     65   bool Open(const char* filename, Error* error);
     66 
     67   bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) override;
     68   bool HasNonStandardSubchannel() const override;
     69   PrecacheResult Precache(ProgressCallback* progress) override;
     70   bool IsPrecached() const override;
     71   s64 GetSizeOnDisk() const override;
     72 
     73 protected:
     74   bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override;
     75 
     76 private:
     77   static constexpr u32 CHD_CD_SECTOR_DATA_SIZE = 2352 + 96;
     78   static constexpr u32 CHD_CD_TRACK_ALIGNMENT = 4;
     79   static constexpr u32 MAX_PARENTS = 32; // Surely someone wouldn't be insane enough to go beyond this...
     80 
     81   chd_file* OpenCHD(std::string_view filename, FileSystem::ManagedCFilePtr fp, Error* error, u32 recursion_level);
     82   bool UpdateHunkBuffer(const Index& index, LBA lba_in_index, u32& hunk_offset);
     83 
     84   static void CopyAndSwap(void* dst_ptr, const u8* src_ptr);
     85 
     86   chd_file* m_chd = nullptr;
     87   u32 m_hunk_size = 0;
     88   u32 m_sectors_per_hunk = 0;
     89 
     90   DynamicHeapArray<u8, 16> m_hunk_buffer;
     91   u32 m_current_hunk_index = static_cast<u32>(-1);
     92   bool m_precached = false;
     93 
     94   CDSubChannelReplacement m_sbi;
     95 };
     96 } // namespace
     97 
     98 CDImageCHD::CDImageCHD() = default;
     99 
    100 CDImageCHD::~CDImageCHD()
    101 {
    102   if (m_chd)
    103     chd_close(m_chd);
    104 }
    105 
    106 chd_file* CDImageCHD::OpenCHD(std::string_view filename, FileSystem::ManagedCFilePtr fp, Error* error,
    107                               u32 recursion_level)
    108 {
    109   chd_file* chd;
    110   chd_error err = chd_open_file(fp.get(), CHD_OPEN_READ | CHD_OPEN_TRANSFER_FILE, nullptr, &chd);
    111   if (err == CHDERR_NONE)
    112   {
    113     // fp is now managed by libchdr
    114     fp.release();
    115     return chd;
    116   }
    117   else if (err != CHDERR_REQUIRES_PARENT)
    118   {
    119     ERROR_LOG("Failed to open CHD '{}': {}", filename, chd_error_string(err));
    120     Error::SetString(error, chd_error_string(err));
    121     return nullptr;
    122   }
    123 
    124   if (recursion_level >= MAX_PARENTS)
    125   {
    126     ERROR_LOG("Failed to open CHD '{}': Too many parent files", filename);
    127     Error::SetString(error, "Too many parent files");
    128     return nullptr;
    129   }
    130 
    131   // Need to get the sha1 to look for.
    132   chd_header header;
    133   err = chd_read_header_file(fp.get(), &header);
    134   if (err != CHDERR_NONE)
    135   {
    136     ERROR_LOG("Failed to read CHD header '{}': {}", filename, chd_error_string(err));
    137     Error::SetString(error, chd_error_string(err));
    138     return nullptr;
    139   }
    140 
    141   // Find a chd with a matching sha1 in the same directory.
    142   // Have to do *.* and filter on the extension manually because Linux is case sensitive.
    143   chd_file* parent_chd = nullptr;
    144   const std::string parent_dir(Path::GetDirectory(filename));
    145   const std::unique_lock hash_cache_lock(s_chd_hash_cache_mutex);
    146 
    147   // Memoize which hashes came from what files, to avoid reading them repeatedly.
    148   for (auto it = s_chd_hash_cache.begin(); it != s_chd_hash_cache.end(); ++it)
    149   {
    150     if (!StringUtil::EqualNoCase(parent_dir, Path::GetDirectory(it->first)))
    151       continue;
    152 
    153     if (!chd_is_matching_parent(&header, &it->second))
    154       continue;
    155 
    156     // Re-check the header, it might have changed since we last opened.
    157     chd_header parent_header;
    158     auto parent_fp = FileSystem::OpenManagedSharedCFile(it->first.c_str(), "rb", FileSystem::FileShareMode::DenyWrite);
    159     if (parent_fp && chd_read_header_file(parent_fp.get(), &parent_header) == CHDERR_NONE &&
    160         chd_is_matching_parent(&header, &parent_header))
    161     {
    162       // Need to take a copy of the string, because the parent might add to the list and invalidate the iterator.
    163       const std::string filename_to_open = it->first;
    164 
    165       // Match! Open this one.
    166       parent_chd = OpenCHD(filename_to_open, std::move(parent_fp), error, recursion_level + 1);
    167       if (parent_chd)
    168       {
    169         VERBOSE_LOG("Using parent CHD '{}' from cache for '{}'.", Path::GetFileName(filename_to_open),
    170                     Path::GetFileName(filename));
    171       }
    172     }
    173 
    174     // No point checking any others. Since we recursively call OpenCHD(), the iterator is invalidated anyway.
    175     break;
    176   }
    177   if (!parent_chd)
    178   {
    179     // Look for files in the same directory as the chd.
    180     FileSystem::FindResultsArray parent_files;
    181     FileSystem::FindFiles(parent_dir.c_str(), "*.*",
    182                           FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES | FILESYSTEM_FIND_KEEP_ARRAY,
    183                           &parent_files);
    184     for (FILESYSTEM_FIND_DATA& fd : parent_files)
    185     {
    186       if (StringUtil::EndsWithNoCase(Path::GetExtension(fd.FileName), ".chd"))
    187         continue;
    188 
    189       // Re-check the header, it might have changed since we last opened.
    190       chd_header parent_header;
    191       auto parent_fp =
    192         FileSystem::OpenManagedSharedCFile(fd.FileName.c_str(), "rb", FileSystem::FileShareMode::DenyWrite);
    193       if (!parent_fp || chd_read_header_file(parent_fp.get(), &parent_header) != CHDERR_NONE)
    194         continue;
    195 
    196       // Don't duplicate in the cache. But update it, in case the file changed.
    197       auto cache_it = std::find_if(s_chd_hash_cache.begin(), s_chd_hash_cache.end(),
    198                                    [&fd](const auto& it) { return it.first == fd.FileName; });
    199       if (cache_it != s_chd_hash_cache.end())
    200         std::memcpy(&cache_it->second, &parent_header, sizeof(parent_header));
    201       else
    202         s_chd_hash_cache.emplace_back(fd.FileName, parent_header);
    203 
    204       if (!chd_is_matching_parent(&header, &parent_header))
    205         continue;
    206 
    207       // Match! Open this one.
    208       parent_chd = OpenCHD(fd.FileName, std::move(parent_fp), error, recursion_level + 1);
    209       if (parent_chd)
    210       {
    211         VERBOSE_LOG("Using parent CHD '{}' for '{}'.", Path::GetFileName(fd.FileName), Path::GetFileName(filename));
    212         break;
    213       }
    214     }
    215   }
    216   if (!parent_chd)
    217   {
    218     ERROR_LOG("Failed to open CHD '{}': Failed to find parent CHD, it must be in the same directory.", filename);
    219     Error::SetString(error, "Failed to find parent CHD, it must be in the same directory.");
    220     return nullptr;
    221   }
    222 
    223   // Now try re-opening with the parent.
    224   err = chd_open_file(fp.get(), CHD_OPEN_READ | CHD_OPEN_TRANSFER_FILE, parent_chd, &chd);
    225   if (err != CHDERR_NONE)
    226   {
    227     ERROR_LOG("Failed to open CHD '{}': {}", filename, chd_error_string(err));
    228     Error::SetString(error, chd_error_string(err));
    229     return nullptr;
    230   }
    231 
    232   // fp now owned by libchdr
    233   fp.release();
    234   return chd;
    235 }
    236 
    237 bool CDImageCHD::Open(const char* filename, Error* error)
    238 {
    239   auto fp = FileSystem::OpenManagedSharedCFile(filename, "rb", FileSystem::FileShareMode::DenyWrite);
    240   if (!fp)
    241   {
    242     ERROR_LOG("Failed to open CHD '{}': errno {}", filename, errno);
    243     if (error)
    244       error->SetErrno(errno);
    245 
    246     return false;
    247   }
    248 
    249   m_chd = OpenCHD(filename, std::move(fp), error, 0);
    250   if (!m_chd)
    251     return false;
    252 
    253   const chd_header* header = chd_get_header(m_chd);
    254   m_hunk_size = header->hunkbytes;
    255   if ((m_hunk_size % CHD_CD_SECTOR_DATA_SIZE) != 0)
    256   {
    257     ERROR_LOG("Hunk size ({}) is not a multiple of {}", m_hunk_size, CHD_CD_SECTOR_DATA_SIZE);
    258     Error::SetString(error, fmt::format("Hunk size ({}) is not a multiple of {}", m_hunk_size,
    259                                         static_cast<u32>(CHD_CD_SECTOR_DATA_SIZE)));
    260     return false;
    261   }
    262 
    263   m_sectors_per_hunk = m_hunk_size / CHD_CD_SECTOR_DATA_SIZE;
    264   m_hunk_buffer.resize(m_hunk_size);
    265   m_filename = filename;
    266 
    267   u32 disc_lba = 0;
    268   u64 file_lba = 0;
    269 
    270   // for each track..
    271   int num_tracks = 0;
    272   for (;;)
    273   {
    274     char metadata_str[256];
    275     char type_str[256];
    276     char subtype_str[256];
    277     char pgtype_str[256];
    278     char pgsub_str[256];
    279     u32 metadata_length;
    280 
    281     int track_num = 0, frames = 0, pregap_frames = 0, postgap_frames = 0;
    282     chd_error err = chd_get_metadata(m_chd, CDROM_TRACK_METADATA2_TAG, num_tracks, metadata_str, sizeof(metadata_str),
    283                                      &metadata_length, nullptr, nullptr);
    284     if (err == CHDERR_NONE)
    285     {
    286       if (std::sscanf(metadata_str, CDROM_TRACK_METADATA2_FORMAT, &track_num, type_str, subtype_str, &frames,
    287                       &pregap_frames, pgtype_str, pgsub_str, &postgap_frames) != 8)
    288       {
    289         ERROR_LOG("Invalid track v2 metadata: '{}'", metadata_str);
    290         Error::SetString(error, fmt::format("Invalid track v2 metadata: '{}'", metadata_str));
    291         return false;
    292       }
    293     }
    294     else
    295     {
    296       // try old version
    297       err = chd_get_metadata(m_chd, CDROM_TRACK_METADATA_TAG, num_tracks, metadata_str, sizeof(metadata_str),
    298                              &metadata_length, nullptr, nullptr);
    299       if (err != CHDERR_NONE)
    300       {
    301         // not found, so no more tracks
    302         break;
    303       }
    304 
    305       if (std::sscanf(metadata_str, CDROM_TRACK_METADATA_FORMAT, &track_num, type_str, subtype_str, &frames) != 4)
    306       {
    307         ERROR_LOG("Invalid track metadata: '{}'", metadata_str);
    308         Error::SetString(error, fmt::format("Invalid track v2 metadata: '{}'", metadata_str));
    309         return false;
    310       }
    311     }
    312 
    313     u32 csubtype, csubsize;
    314     if (!cdrom_parse_subtype_string(subtype_str, &csubtype, &csubsize))
    315     {
    316       csubtype = CD_SUB_NONE;
    317       csubsize = 0;
    318     }
    319 
    320     if (track_num != (num_tracks + 1))
    321     {
    322       ERROR_LOG("Incorrect track number at index {}, expected {} got {}", num_tracks, (num_tracks + 1), track_num);
    323       Error::SetString(error, fmt::format("Incorrect track number at index {}, expected {} got {}", num_tracks,
    324                                           (num_tracks + 1), track_num));
    325       return false;
    326     }
    327 
    328     std::optional<TrackMode> mode = ParseTrackModeString(type_str);
    329     if (!mode.has_value())
    330     {
    331       ERROR_LOG("Invalid track mode: '{}'", type_str);
    332       Error::SetString(error, fmt::format("Invalid track mode: '{}'", type_str));
    333       return false;
    334     }
    335 
    336     // precompute subchannel q flags for the whole track
    337     SubChannelQ::Control control{};
    338     control.data = mode.value() != TrackMode::Audio;
    339 
    340     // two seconds pregap for track 1 is assumed if not specified
    341     const bool pregap_in_file = (pregap_frames > 0 && pgtype_str[0] == 'V');
    342     if (pregap_frames <= 0 && mode != TrackMode::Audio)
    343       pregap_frames = 2 * FRAMES_PER_SECOND;
    344 
    345     // create the index for the pregap
    346     if (pregap_frames > 0)
    347     {
    348       Index pregap_index = {};
    349       pregap_index.start_lba_on_disc = disc_lba;
    350       pregap_index.start_lba_in_track = static_cast<LBA>(static_cast<unsigned long>(-pregap_frames));
    351       pregap_index.length = pregap_frames;
    352       pregap_index.track_number = track_num;
    353       pregap_index.index_number = 0;
    354       pregap_index.mode = mode.value();
    355       pregap_index.submode = static_cast<SubchannelMode>(csubtype);
    356       pregap_index.control.bits = control.bits;
    357       pregap_index.is_pregap = true;
    358 
    359       if (pregap_in_file)
    360       {
    361         if (pregap_frames > frames)
    362         {
    363           ERROR_LOG("Pregap length {} exceeds track length {}", pregap_frames, frames);
    364           Error::SetString(error, fmt::format("Pregap length {} exceeds track length {}", pregap_frames, frames));
    365           return false;
    366         }
    367 
    368         pregap_index.file_index = 0;
    369         pregap_index.file_offset = file_lba;
    370         pregap_index.file_sector_size = CHD_CD_SECTOR_DATA_SIZE;
    371         file_lba += pregap_frames;
    372         frames -= pregap_frames;
    373       }
    374 
    375       m_indices.push_back(pregap_index);
    376       disc_lba += pregap_frames;
    377     }
    378 
    379     // add the track itself
    380     m_tracks.push_back(Track{static_cast<u32>(track_num), disc_lba, static_cast<u32>(m_indices.size()),
    381                              static_cast<u32>(frames + pregap_frames), mode.value(),
    382                              static_cast<SubchannelMode>(csubtype), control});
    383 
    384     // how many indices in this track?
    385     Index index = {};
    386     index.start_lba_on_disc = disc_lba;
    387     index.start_lba_in_track = 0;
    388     index.track_number = track_num;
    389     index.index_number = 1;
    390     index.file_index = 0;
    391     index.file_sector_size = CHD_CD_SECTOR_DATA_SIZE;
    392     index.file_offset = file_lba;
    393     index.mode = mode.value();
    394     index.submode = static_cast<SubchannelMode>(csubtype);
    395     index.control.bits = control.bits;
    396     index.is_pregap = false;
    397     index.length = static_cast<u32>(frames);
    398     m_indices.push_back(index);
    399 
    400     disc_lba += index.length;
    401     file_lba += index.length;
    402     num_tracks++;
    403 
    404     // each track is padded to a multiple of 4 frames, see chdman source.
    405     file_lba = Common::AlignUp(file_lba, CHD_CD_TRACK_ALIGNMENT);
    406   }
    407 
    408   if (m_tracks.empty())
    409   {
    410     ERROR_LOG("File '{}' contains no tracks", filename);
    411     Error::SetString(error, fmt::format("File '{}' contains no tracks", filename));
    412     return false;
    413   }
    414 
    415   m_lba_count = disc_lba;
    416   AddLeadOutIndex();
    417 
    418   m_sbi.LoadFromImagePath(filename);
    419 
    420   return Seek(1, Position{0, 0, 0});
    421 }
    422 
    423 bool CDImageCHD::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index)
    424 {
    425   if (m_sbi.GetReplacementSubChannelQ(index.start_lba_on_disc + lba_in_index, subq))
    426     return true;
    427 
    428   if (index.submode == CDImage::SubchannelMode::None)
    429     return CDImage::ReadSubChannelQ(subq, index, lba_in_index);
    430 
    431   u32 hunk_offset;
    432   if (!UpdateHunkBuffer(index, lba_in_index, hunk_offset))
    433     return false;
    434 
    435   u8 deinterleaved_subchannel_data[96];
    436   const u8* raw_subchannel_data = &m_hunk_buffer[hunk_offset + RAW_SECTOR_SIZE];
    437   const u8* real_subchannel_data = raw_subchannel_data;
    438   if (index.submode == CDImage::SubchannelMode::RawInterleaved)
    439   {
    440     DeinterleaveSubcode(raw_subchannel_data, deinterleaved_subchannel_data);
    441     real_subchannel_data = deinterleaved_subchannel_data;
    442   }
    443 
    444   // P, Q, R, S, T, U, V, W
    445   std::memcpy(subq->data.data(), real_subchannel_data + (1 * SUBCHANNEL_BYTES_PER_FRAME), SUBCHANNEL_BYTES_PER_FRAME);
    446   return true;
    447 }
    448 
    449 bool CDImageCHD::HasNonStandardSubchannel() const
    450 {
    451   // Just look at the first track for in-CHD subq.
    452   return (m_sbi.GetReplacementSectorCount() > 0 || m_tracks.front().submode != CDImage::SubchannelMode::None);
    453 }
    454 
    455 CDImage::PrecacheResult CDImageCHD::Precache(ProgressCallback* progress)
    456 {
    457   if (m_precached)
    458     return CDImage::PrecacheResult::Success;
    459 
    460   progress->SetStatusText(fmt::format("Precaching {}...", FileSystem::GetDisplayNameFromPath(m_filename)).c_str());
    461   progress->SetProgressRange(100);
    462 
    463   auto callback = [](size_t pos, size_t total, void* param) {
    464     const u32 percent = static_cast<u32>((pos * 100) / total);
    465     static_cast<ProgressCallback*>(param)->SetProgressValue(std::min<u32>(percent, 100));
    466   };
    467 
    468   if (chd_precache_progress(m_chd, callback, progress) != CHDERR_NONE)
    469     return CDImage::PrecacheResult::ReadError;
    470 
    471   m_precached = true;
    472   return CDImage::PrecacheResult::Success;
    473 }
    474 
    475 bool CDImageCHD::IsPrecached() const
    476 {
    477   return m_precached;
    478 }
    479 
    480 ALWAYS_INLINE_RELEASE void CDImageCHD::CopyAndSwap(void* dst_ptr, const u8* src_ptr)
    481 {
    482   constexpr u32 data_size = RAW_SECTOR_SIZE;
    483 
    484   u8* dst_ptr_byte = static_cast<u8*>(dst_ptr);
    485   static_assert((data_size % 16) == 0);
    486   constexpr u32 num_values = data_size / 16;
    487 
    488   constexpr GSVector4i mask = GSVector4i::cxpr8(1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10, 13, 12, 15, 14);
    489   for (u32 i = 0; i < num_values; i++)
    490   {
    491     GSVector4i value = GSVector4i::load<false>(src_ptr);
    492     value = value.shuffle8(mask);
    493     GSVector4i::store<false>(dst_ptr_byte, value);
    494     src_ptr += sizeof(value);
    495     dst_ptr_byte += sizeof(value);
    496   }
    497 }
    498 
    499 bool CDImageCHD::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index)
    500 {
    501   u32 hunk_offset;
    502   if (!UpdateHunkBuffer(index, lba_in_index, hunk_offset))
    503     return false;
    504 
    505   // Audio data is in big-endian, so we have to swap it for little endian hosts...
    506   if (index.mode == TrackMode::Audio)
    507     CopyAndSwap(buffer, &m_hunk_buffer[hunk_offset]);
    508   else
    509     std::memcpy(buffer, &m_hunk_buffer[hunk_offset], RAW_SECTOR_SIZE);
    510 
    511   return true;
    512 }
    513 
    514 ALWAYS_INLINE_RELEASE bool CDImageCHD::UpdateHunkBuffer(const Index& index, LBA lba_in_index, u32& hunk_offset)
    515 {
    516   const u32 disc_frame = static_cast<LBA>(index.file_offset) + lba_in_index;
    517   const u32 hunk_index = static_cast<u32>(disc_frame / m_sectors_per_hunk);
    518   hunk_offset = static_cast<u32>((disc_frame % m_sectors_per_hunk) * CHD_CD_SECTOR_DATA_SIZE);
    519   DebugAssert((m_hunk_size - hunk_offset) >= CHD_CD_SECTOR_DATA_SIZE);
    520 
    521   if (m_current_hunk_index == hunk_index)
    522     return true;
    523 
    524   const chd_error err = chd_read(m_chd, hunk_index, m_hunk_buffer.data());
    525   if (err != CHDERR_NONE)
    526   {
    527     ERROR_LOG("chd_read({}) failed: {}", hunk_index, chd_error_string(err));
    528 
    529     // data might have been partially written
    530     m_current_hunk_index = static_cast<u32>(-1);
    531     return false;
    532   }
    533 
    534   m_current_hunk_index = hunk_index;
    535   return true;
    536 }
    537 
    538 s64 CDImageCHD::GetSizeOnDisk() const
    539 {
    540   return static_cast<s64>(chd_get_compressed_size(m_chd));
    541 }
    542 
    543 std::unique_ptr<CDImage> CDImage::OpenCHDImage(const char* filename, Error* error)
    544 {
    545   std::unique_ptr<CDImageCHD> image = std::make_unique<CDImageCHD>();
    546   if (!image->Open(filename, error))
    547     return {};
    548 
    549   return image;
    550 }