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 }