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