Move utility classes from common to own static lib

This commit is contained in:
Connor McLaughlin
2022-07-08 22:43:38 +10:00
parent d2ca454576
commit b7fbde31a7
101 changed files with 371 additions and 282 deletions

45
src/util/CMakeLists.txt Normal file
View File

@ -0,0 +1,45 @@
add_library(util
audio_stream.cpp
audio_stream.h
cd_image.cpp
cd_image.h
cd_image_bin.cpp
cd_image_cue.cpp
cd_image_chd.cpp
cd_image_device.cpp
cd_image_ecm.cpp
cd_image_hasher.cpp
cd_image_hasher.h
cd_image_m3u.cpp
cd_image_memory.cpp
cd_image_mds.cpp
cd_image_pbp.cpp
cd_image_ppf.cpp
cd_subchannel_replacement.cpp
cd_subchannel_replacement.h
cd_xa.cpp
cd_xa.h
cue_parser.cpp
cue_parser.h
iso_reader.cpp
iso_reader.h
jit_code_buffer.cpp
jit_code_buffer.h
null_audio_stream.cpp
null_audio_stream.h
memory_arena.cpp
memory_arena.h
page_fault_handler.cpp
page_fault_handler.h
shiftjis.cpp
shiftjis.h
state_wrapper.cpp
state_wrapper.h
wav_writer.cpp
wav_writer.h
)
target_include_directories(util PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..")
target_include_directories(util PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..")
target_link_libraries(util PUBLIC common)
target_link_libraries(util PRIVATE libchdr samplerate zlib)

387
src/util/audio_stream.cpp Normal file
View File

@ -0,0 +1,387 @@
#include "audio_stream.h"
#include "assert.h"
#include "common/log.h"
#include "samplerate.h"
#include <algorithm>
#include <cstring>
Log_SetChannel(AudioStream);
AudioStream::AudioStream() = default;
AudioStream::~AudioStream()
{
DestroyResampler();
}
bool AudioStream::Reconfigure(u32 input_sample_rate /* = DefaultInputSampleRate */,
u32 output_sample_rate /* = DefaultOutputSampleRate */, u32 channels /* = 1 */,
u32 buffer_size /* = DefaultBufferSize */)
{
std::unique_lock<std::mutex> buffer_lock(m_buffer_mutex);
std::unique_lock<std::mutex> resampler_Lock(m_resampler_mutex);
DestroyResampler();
if (IsDeviceOpen())
CloseDevice();
m_output_sample_rate = output_sample_rate;
m_channels = channels;
m_buffer_size = buffer_size;
m_buffer_filling.store(m_wait_for_buffer_fill);
m_output_paused = true;
if (!SetBufferSize(buffer_size))
return false;
if (!OpenDevice())
{
LockedEmptyBuffers();
m_buffer_size = 0;
m_output_sample_rate = 0;
m_channels = 0;
return false;
}
CreateResampler();
InternalSetInputSampleRate(input_sample_rate);
return true;
}
void AudioStream::SetInputSampleRate(u32 sample_rate)
{
std::unique_lock<std::mutex> buffer_lock(m_buffer_mutex);
std::unique_lock<std::mutex> resampler_lock(m_resampler_mutex);
InternalSetInputSampleRate(sample_rate);
}
void AudioStream::SetWaitForBufferFill(bool enabled)
{
std::unique_lock<std::mutex> buffer_lock(m_buffer_mutex);
m_wait_for_buffer_fill = enabled;
if (enabled && m_buffer.IsEmpty())
m_buffer_filling.store(true);
}
void AudioStream::InternalSetInputSampleRate(u32 sample_rate)
{
if (m_input_sample_rate == sample_rate)
return;
m_input_sample_rate = sample_rate;
m_resampler_ratio = static_cast<double>(m_output_sample_rate) / static_cast<double>(sample_rate);
src_set_ratio(static_cast<SRC_STATE*>(m_resampler_state), m_resampler_ratio);
ResetResampler();
}
void AudioStream::SetOutputVolume(u32 volume)
{
std::unique_lock<std::mutex> lock(m_buffer_mutex);
m_output_volume = volume;
}
void AudioStream::PauseOutput(bool paused)
{
if (m_output_paused == paused)
return;
PauseDevice(paused);
m_output_paused = paused;
// Empty buffers on pause.
if (paused)
EmptyBuffers();
}
void AudioStream::Shutdown()
{
if (!IsDeviceOpen())
return;
CloseDevice();
EmptyBuffers();
m_buffer_size = 0;
m_output_sample_rate = 0;
m_channels = 0;
m_output_paused = true;
}
void AudioStream::BeginWrite(SampleType** buffer_ptr, u32* num_frames)
{
m_buffer_mutex.lock();
const u32 requested_frames = std::min(*num_frames, m_buffer_size);
EnsureBuffer(requested_frames * m_channels);
*buffer_ptr = m_buffer.GetWritePointer();
*num_frames = std::min(m_buffer_size, m_buffer.GetContiguousSpace() / m_channels);
}
void AudioStream::WriteFrames(const SampleType* frames, u32 num_frames)
{
Assert(num_frames <= m_buffer_size);
const u32 num_samples = num_frames * m_channels;
{
std::unique_lock<std::mutex> lock(m_buffer_mutex);
EnsureBuffer(num_samples);
m_buffer.PushRange(frames, num_samples);
}
FramesAvailable();
}
void AudioStream::EndWrite(u32 num_frames)
{
m_buffer.AdvanceTail(num_frames * m_channels);
if (m_buffer_filling.load())
{
if ((m_buffer.GetSize() / m_channels) >= m_buffer_size)
m_buffer_filling.store(false);
}
m_buffer_mutex.unlock();
FramesAvailable();
}
float AudioStream::GetMaxLatency(u32 sample_rate, u32 buffer_size)
{
return (static_cast<float>(buffer_size) / static_cast<float>(sample_rate));
}
bool AudioStream::SetBufferSize(u32 buffer_size)
{
const u32 buffer_size_in_samples = buffer_size * m_channels;
const u32 max_samples = buffer_size_in_samples * 2u;
if (max_samples > m_buffer.GetCapacity())
return false;
m_buffer_size = buffer_size;
m_max_samples = max_samples;
return true;
}
u32 AudioStream::GetSamplesAvailable() const
{
// TODO: Use atomic loads
u32 available_samples;
{
std::unique_lock<std::mutex> lock(m_buffer_mutex);
available_samples = m_buffer.GetSize();
}
return available_samples / m_channels;
}
u32 AudioStream::GetSamplesAvailableLocked() const
{
return m_buffer.GetSize() / m_channels;
}
void AudioStream::ReadFrames(SampleType* samples, u32 num_frames, bool apply_volume)
{
const u32 total_samples = num_frames * m_channels;
u32 samples_copied = 0;
std::unique_lock<std::mutex> buffer_lock(m_buffer_mutex);
if (!m_buffer_filling.load())
{
if (m_input_sample_rate == m_output_sample_rate)
{
samples_copied = std::min(m_buffer.GetSize(), total_samples);
if (samples_copied > 0)
m_buffer.PopRange(samples, samples_copied);
ReleaseBufferLock(std::move(buffer_lock));
}
else
{
if (m_resampled_buffer.GetSize() < total_samples)
ResampleInput(std::move(buffer_lock));
else
ReleaseBufferLock(std::move(buffer_lock));
samples_copied = std::min(m_resampled_buffer.GetSize(), total_samples);
if (samples_copied > 0)
m_resampled_buffer.PopRange(samples, samples_copied);
}
}
else
{
ReleaseBufferLock(std::move(buffer_lock));
}
if (samples_copied < total_samples)
{
if (samples_copied > 0)
{
m_resample_buffer.resize(samples_copied);
std::memcpy(m_resample_buffer.data(), samples, sizeof(SampleType) * samples_copied);
// super basic resampler - spread the input samples evenly across the output samples. will sound like ass and have
// aliasing, but better than popping by inserting silence.
const u32 increment =
static_cast<u32>(65536.0f * (static_cast<float>(samples_copied / m_channels) / static_cast<float>(num_frames)));
SampleType* out_ptr = samples;
const SampleType* resample_ptr = m_resample_buffer.data();
const u32 copy_stride = sizeof(SampleType) * m_channels;
u32 resample_subpos = 0;
for (u32 i = 0; i < num_frames; i++)
{
std::memcpy(out_ptr, resample_ptr, copy_stride);
out_ptr += m_channels;
resample_subpos += increment;
resample_ptr += (resample_subpos >> 16) * m_channels;
resample_subpos %= 65536u;
}
Log_VerbosePrintf("Audio buffer underflow, resampled %u frames to %u", samples_copied / m_channels, num_frames);
m_underflow_flag.store(true);
}
else
{
// read nothing, so zero-fill
std::memset(samples, 0, sizeof(SampleType) * total_samples);
Log_VerbosePrintf("Audio buffer underflow with no samples, added %u frames silence", num_frames);
m_underflow_flag.store(true);
}
m_buffer_filling.store(m_wait_for_buffer_fill);
}
if (apply_volume && m_output_volume != FullVolume)
{
SampleType* current_ptr = samples;
const SampleType* end_ptr = samples + (num_frames * m_channels);
while (current_ptr != end_ptr)
{
*current_ptr = ApplyVolume(*current_ptr, m_output_volume);
current_ptr++;
}
}
}
void AudioStream::EnsureBuffer(u32 size)
{
DebugAssert(size <= (m_buffer_size * m_channels));
if (GetBufferSpace() >= size)
return;
if (m_sync)
{
std::unique_lock<std::mutex> lock(m_buffer_mutex, std::adopt_lock);
m_buffer_draining_cv.wait(lock, [this, size]() { return GetBufferSpace() >= size; });
lock.release();
}
else
{
m_buffer.Remove(size);
}
}
void AudioStream::DropFrames(u32 count)
{
std::unique_lock<std::mutex> lock(m_buffer_mutex);
m_buffer.Remove(count);
}
void AudioStream::EmptyBuffers()
{
std::unique_lock<std::mutex> lock(m_buffer_mutex);
std::unique_lock<std::mutex> resampler_lock(m_resampler_mutex);
LockedEmptyBuffers();
}
void AudioStream::LockedEmptyBuffers()
{
m_buffer.Clear();
m_underflow_flag.store(false);
m_buffer_filling.store(m_wait_for_buffer_fill);
ResetResampler();
}
void AudioStream::CreateResampler()
{
m_resampler_state = src_new(SRC_SINC_MEDIUM_QUALITY, static_cast<int>(m_channels), nullptr);
if (!m_resampler_state)
Panic("Failed to allocate resampler");
}
void AudioStream::DestroyResampler()
{
if (m_resampler_state)
{
src_delete(static_cast<SRC_STATE*>(m_resampler_state));
m_resampler_state = nullptr;
}
}
void AudioStream::ResetResampler()
{
m_resampled_buffer.Clear();
m_resample_in_buffer.clear();
m_resample_out_buffer.clear();
src_reset(static_cast<SRC_STATE*>(m_resampler_state));
}
void AudioStream::ResampleInput(std::unique_lock<std::mutex> buffer_lock)
{
std::unique_lock<std::mutex> resampler_lock(m_resampler_mutex);
const u32 input_space_from_output = (m_resampled_buffer.GetSpace() * m_output_sample_rate) / m_input_sample_rate;
u32 remaining = std::min(m_buffer.GetSize(), input_space_from_output);
if (m_resample_in_buffer.size() < remaining)
{
remaining -= static_cast<u32>(m_resample_in_buffer.size());
m_resample_in_buffer.reserve(m_resample_in_buffer.size() + remaining);
while (remaining > 0)
{
const u32 read_len = std::min(m_buffer.GetContiguousSize(), remaining);
const size_t old_pos = m_resample_in_buffer.size();
m_resample_in_buffer.resize(m_resample_in_buffer.size() + read_len);
src_short_to_float_array(m_buffer.GetReadPointer(), m_resample_in_buffer.data() + old_pos,
static_cast<int>(read_len));
m_buffer.Remove(read_len);
remaining -= read_len;
}
}
ReleaseBufferLock(std::move(buffer_lock));
const u32 potential_output_size =
(static_cast<u32>(m_resample_in_buffer.size()) * m_input_sample_rate) / m_output_sample_rate;
const u32 output_size = std::min(potential_output_size, m_resampled_buffer.GetSpace());
m_resample_out_buffer.resize(output_size);
SRC_DATA sd = {};
sd.data_in = m_resample_in_buffer.data();
sd.data_out = m_resample_out_buffer.data();
sd.input_frames = static_cast<u32>(m_resample_in_buffer.size()) / m_channels;
sd.output_frames = output_size / m_channels;
sd.src_ratio = m_resampler_ratio;
const int error = src_process(static_cast<SRC_STATE*>(m_resampler_state), &sd);
if (error)
{
Log_ErrorPrintf("Resampler error %d", error);
m_resample_in_buffer.clear();
m_resample_out_buffer.clear();
return;
}
m_resample_in_buffer.erase(m_resample_in_buffer.begin(),
m_resample_in_buffer.begin() + (static_cast<u32>(sd.input_frames_used) * m_channels));
const float* write_ptr = m_resample_out_buffer.data();
remaining = static_cast<u32>(sd.output_frames_gen) * m_channels;
while (remaining > 0)
{
const u32 samples_to_write = std::min(m_resampled_buffer.GetContiguousSpace(), remaining);
src_float_to_short_array(write_ptr, m_resampled_buffer.GetWritePointer(), static_cast<int>(samples_to_write));
m_resampled_buffer.AdvanceTail(samples_to_write);
write_ptr += samples_to_write;
remaining -= samples_to_write;
}
m_resample_out_buffer.erase(m_resample_out_buffer.begin(),
m_resample_out_buffer.begin() + (static_cast<u32>(sd.output_frames_gen) * m_channels));
}

126
src/util/audio_stream.h Normal file
View File

@ -0,0 +1,126 @@
#pragma once
#include "common/fifo_queue.h"
#include "common/types.h"
#include <atomic>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <vector>
// Uses signed 16-bits samples.
class AudioStream
{
public:
using SampleType = s16;
enum : u32
{
DefaultInputSampleRate = 44100,
DefaultOutputSampleRate = 44100,
DefaultBufferSize = 2048,
MaxSamples = 32768,
FullVolume = 100
};
AudioStream();
virtual ~AudioStream();
u32 GetOutputSampleRate() const { return m_output_sample_rate; }
u32 GetChannels() const { return m_channels; }
u32 GetBufferSize() const { return m_buffer_size; }
s32 GetOutputVolume() const { return m_output_volume; }
bool IsSyncing() const { return m_sync; }
bool Reconfigure(u32 input_sample_rate = DefaultInputSampleRate, u32 output_sample_rate = DefaultOutputSampleRate,
u32 channels = 1, u32 buffer_size = DefaultBufferSize);
void SetSync(bool enable) { m_sync = enable; }
void SetInputSampleRate(u32 sample_rate);
void SetWaitForBufferFill(bool enabled);
virtual void SetOutputVolume(u32 volume);
void PauseOutput(bool paused);
void EmptyBuffers();
void Shutdown();
void BeginWrite(SampleType** buffer_ptr, u32* num_frames);
void WriteFrames(const SampleType* frames, u32 num_frames);
void EndWrite(u32 num_frames);
bool DidUnderflow()
{
bool expected = true;
return m_underflow_flag.compare_exchange_strong(expected, false);
}
static std::unique_ptr<AudioStream> CreateNullAudioStream();
// Latency computation - returns values in seconds
static float GetMaxLatency(u32 sample_rate, u32 buffer_size);
protected:
virtual bool OpenDevice() = 0;
virtual void PauseDevice(bool paused) = 0;
virtual void CloseDevice() = 0;
virtual void FramesAvailable() = 0;
ALWAYS_INLINE static SampleType ApplyVolume(SampleType sample, u32 volume)
{
return s16((s32(sample) * s32(volume)) / 100);
}
ALWAYS_INLINE u32 GetBufferSpace() const { return (m_max_samples - m_buffer.GetSize()); }
ALWAYS_INLINE void ReleaseBufferLock(std::unique_lock<std::mutex> lock)
{
// lock is released implicitly by destruction
m_buffer_draining_cv.notify_one();
}
bool SetBufferSize(u32 buffer_size);
bool IsDeviceOpen() const { return (m_output_sample_rate > 0); }
void EnsureBuffer(u32 size);
void LockedEmptyBuffers();
u32 GetSamplesAvailable() const;
u32 GetSamplesAvailableLocked() const;
void ReadFrames(SampleType* samples, u32 num_frames, bool apply_volume);
void DropFrames(u32 count);
void CreateResampler();
void DestroyResampler();
void ResetResampler();
void InternalSetInputSampleRate(u32 sample_rate);
void ResampleInput(std::unique_lock<std::mutex> buffer_lock);
u32 m_input_sample_rate = 0;
u32 m_output_sample_rate = 0;
u32 m_channels = 0;
u32 m_buffer_size = 0;
// volume, 0-100
u32 m_output_volume = FullVolume;
HeapFIFOQueue<SampleType, MaxSamples> m_buffer;
mutable std::mutex m_buffer_mutex;
std::condition_variable m_buffer_draining_cv;
std::vector<SampleType> m_resample_buffer;
std::atomic_bool m_underflow_flag{false};
std::atomic_bool m_buffer_filling{false};
u32 m_max_samples = 0;
bool m_output_paused = true;
bool m_sync = true;
bool m_wait_for_buffer_fill = false;
// Resampling
double m_resampler_ratio = 1.0;
void* m_resampler_state = nullptr;
std::mutex m_resampler_mutex;
HeapFIFOQueue<SampleType, MaxSamples> m_resampled_buffer;
std::vector<float> m_resample_in_buffer;
std::vector<float> m_resample_out_buffer;
};

486
src/util/cd_image.cpp Normal file
View File

@ -0,0 +1,486 @@
#include "cd_image.h"
#include "common/assert.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include "common/string_util.h"
#include <array>
Log_SetChannel(CDImage);
CDImage::CDImage() = default;
CDImage::~CDImage() = default;
u32 CDImage::GetBytesPerSector(TrackMode mode)
{
static constexpr std::array<u32, 8> sizes = {{2352, 2048, 2352, 2336, 2048, 2324, 2332, 2352}};
return sizes[static_cast<u32>(mode)];
}
std::unique_ptr<CDImage> CDImage::Open(const char* filename, Common::Error* error)
{
const char* extension;
#ifdef __ANDROID__
std::string filename_display_name(FileSystem::GetDisplayNameFromPath(filename));
if (filename_display_name.empty())
filename_display_name = filename;
extension = std::strrchr(filename_display_name.c_str(), '.');
#else
extension = std::strrchr(filename, '.');
#endif
if (!extension)
{
Log_ErrorPrintf("Invalid filename: '%s'", filename);
return nullptr;
}
if (StringUtil::Strcasecmp(extension, ".cue") == 0)
{
return OpenCueSheetImage(filename, error);
}
else if (StringUtil::Strcasecmp(extension, ".bin") == 0 || StringUtil::Strcasecmp(extension, ".img") == 0 ||
StringUtil::Strcasecmp(extension, ".iso") == 0)
{
return OpenBinImage(filename, error);
}
else if (StringUtil::Strcasecmp(extension, ".chd") == 0)
{
return OpenCHDImage(filename, error);
}
else if (StringUtil::Strcasecmp(extension, ".ecm") == 0)
{
return OpenEcmImage(filename, error);
}
else if (StringUtil::Strcasecmp(extension, ".mds") == 0)
{
return OpenMdsImage(filename, error);
}
else if (StringUtil::Strcasecmp(extension, ".pbp") == 0)
{
return OpenPBPImage(filename, error);
}
else if (StringUtil::Strcasecmp(extension, ".m3u") == 0)
{
return OpenM3uImage(filename, error);
}
if (IsDeviceName(filename))
return OpenDeviceImage(filename, error);
#undef CASE_COMPARE
Log_ErrorPrintf("Unknown extension '%s' from filename '%s'", extension, filename);
return nullptr;
}
CDImage::LBA CDImage::GetTrackStartPosition(u8 track) const
{
Assert(track > 0 && track <= m_tracks.size());
return m_tracks[track - 1].start_lba;
}
CDImage::Position CDImage::GetTrackStartMSFPosition(u8 track) const
{
Assert(track > 0 && track <= m_tracks.size());
return Position::FromLBA(m_tracks[track - 1].start_lba);
}
CDImage::LBA CDImage::GetTrackLength(u8 track) const
{
Assert(track > 0 && track <= m_tracks.size());
return m_tracks[track - 1].length;
}
CDImage::Position CDImage::GetTrackMSFLength(u8 track) const
{
Assert(track > 0 && track <= m_tracks.size());
return Position::FromLBA(m_tracks[track - 1].length);
}
CDImage::TrackMode CDImage::GetTrackMode(u8 track) const
{
Assert(track > 0 && track <= m_tracks.size());
return m_tracks[track - 1].mode;
}
CDImage::LBA CDImage::GetTrackIndexPosition(u8 track, u8 index) const
{
for (const Index& current_index : m_indices)
{
if (current_index.track_number == track && current_index.index_number == index)
return current_index.start_lba_on_disc;
}
return m_lba_count;
}
CDImage::LBA CDImage::GetTrackIndexLength(u8 track, u8 index) const
{
for (const Index& current_index : m_indices)
{
if (current_index.track_number == track && current_index.index_number == index)
return current_index.length;
}
return 0;
}
const CDImage::CDImage::Track& CDImage::GetTrack(u32 track) const
{
Assert(track > 0 && track <= m_tracks.size());
return m_tracks[track - 1];
}
const CDImage::CDImage::Index& CDImage::GetIndex(u32 i) const
{
return m_indices[i];
}
bool CDImage::Seek(LBA lba)
{
const Index* new_index;
if (m_current_index && lba >= m_current_index->start_lba_on_disc &&
(lba - m_current_index->start_lba_on_disc) < m_current_index->length)
{
new_index = m_current_index;
}
else
{
new_index = GetIndexForDiscPosition(lba);
if (!new_index)
return false;
}
const LBA new_index_offset = lba - new_index->start_lba_on_disc;
if (new_index_offset >= new_index->length)
return false;
m_current_index = new_index;
m_position_on_disc = lba;
m_position_in_index = new_index_offset;
m_position_in_track = new_index->start_lba_in_track + new_index_offset;
return true;
}
bool CDImage::Seek(u32 track_number, const Position& pos_in_track)
{
if (track_number < 1 || track_number > m_tracks.size())
return false;
const Track& track = m_tracks[track_number - 1];
const LBA pos_lba = pos_in_track.ToLBA();
if (pos_lba >= track.length)
return false;
return Seek(track.start_lba + pos_lba);
}
bool CDImage::Seek(const Position& pos)
{
return Seek(pos.ToLBA());
}
bool CDImage::Seek(u32 track_number, LBA lba)
{
if (track_number < 1 || track_number > m_tracks.size())
return false;
const Track& track = m_tracks[track_number - 1];
return Seek(track.start_lba + lba);
}
u32 CDImage::Read(ReadMode read_mode, u32 sector_count, void* buffer)
{
u8* buffer_ptr = static_cast<u8*>(buffer);
u32 sectors_read = 0;
for (; sectors_read < sector_count; sectors_read++)
{
// get raw sector
u8 raw_sector[RAW_SECTOR_SIZE];
if (!ReadRawSector(raw_sector, nullptr))
break;
switch (read_mode)
{
case ReadMode::DataOnly:
std::memcpy(buffer_ptr, raw_sector + 24, DATA_SECTOR_SIZE);
buffer_ptr += DATA_SECTOR_SIZE;
break;
case ReadMode::RawNoSync:
std::memcpy(buffer_ptr, raw_sector + SECTOR_SYNC_SIZE, RAW_SECTOR_SIZE - SECTOR_SYNC_SIZE);
buffer_ptr += RAW_SECTOR_SIZE - SECTOR_SYNC_SIZE;
break;
case ReadMode::RawSector:
std::memcpy(buffer_ptr, raw_sector, RAW_SECTOR_SIZE);
buffer_ptr += RAW_SECTOR_SIZE;
break;
default:
UnreachableCode();
break;
}
}
return sectors_read;
}
bool CDImage::ReadRawSector(void* buffer, SubChannelQ* subq)
{
if (m_position_in_index == m_current_index->length)
{
if (!Seek(m_position_on_disc))
return false;
}
if (buffer)
{
if (m_current_index->file_sector_size > 0)
{
// TODO: This is where we'd reconstruct the header for other mode tracks.
if (!ReadSectorFromIndex(buffer, *m_current_index, m_position_in_index))
{
Log_ErrorPrintf("Read of LBA %u failed", m_position_on_disc);
Seek(m_position_on_disc);
return false;
}
}
else
{
if (m_current_index->track_number == LEAD_OUT_TRACK_NUMBER)
{
// Lead-out area.
std::fill(static_cast<u8*>(buffer), static_cast<u8*>(buffer) + RAW_SECTOR_SIZE, u8(0xAA));
}
else
{
// This in an implicit pregap. Return silence.
std::fill(static_cast<u8*>(buffer), static_cast<u8*>(buffer) + RAW_SECTOR_SIZE, u8(0));
}
}
}
if (subq && !ReadSubChannelQ(subq, *m_current_index, m_position_in_index))
{
Log_ErrorPrintf("Subchannel read of LBA %u failed", m_position_on_disc);
Seek(m_position_on_disc);
return false;
}
m_position_on_disc++;
m_position_in_index++;
m_position_in_track++;
return true;
}
bool CDImage::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index)
{
GenerateSubChannelQ(subq, index, lba_in_index);
return true;
}
bool CDImage::HasNonStandardSubchannel() const
{
return false;
}
std::string CDImage::GetMetadata(const std::string_view& type) const
{
std::string result;
if (type == "title")
{
const std::string display_name(FileSystem::GetDisplayNameFromPath(m_filename));
result = Path::StripExtension(display_name);
}
return result;
}
bool CDImage::HasSubImages() const
{
return false;
}
u32 CDImage::GetSubImageCount() const
{
return 0;
}
u32 CDImage::GetCurrentSubImage() const
{
return 0;
}
bool CDImage::SwitchSubImage(u32 index, Common::Error* error)
{
return false;
}
std::string CDImage::GetSubImageMetadata(u32 index, const std::string_view& type) const
{
return {};
}
CDImage::PrecacheResult CDImage::Precache(ProgressCallback* progress /*= ProgressCallback::NullProgressCallback*/)
{
return PrecacheResult::Unsupported;
}
void CDImage::ClearTOC()
{
m_lba_count = 0;
m_indices.clear();
m_tracks.clear();
m_current_index = nullptr;
m_position_in_index = 0;
m_position_in_track = 0;
m_position_on_disc = 0;
}
void CDImage::CopyTOC(const CDImage* image)
{
m_lba_count = image->m_lba_count;
decltype(m_indices)().swap(m_indices);
decltype(m_tracks)().swap(m_tracks);
m_indices.reserve(image->m_indices.size());
m_tracks.reserve(image->m_tracks.size());
// Damn bitfield copy constructor...
for (const Index& index : image->m_indices)
{
Index new_index;
std::memcpy(&new_index, &index, sizeof(new_index));
m_indices.push_back(new_index);
}
for (const Track& track : image->m_tracks)
{
Track new_track;
std::memcpy(&new_track, &track, sizeof(new_track));
m_tracks.push_back(new_track);
}
m_current_index = nullptr;
m_position_in_index = 0;
m_position_in_track = 0;
m_position_on_disc = 0;
}
const CDImage::Index* CDImage::GetIndexForDiscPosition(LBA pos)
{
for (const Index& index : m_indices)
{
if (pos < index.start_lba_on_disc)
continue;
const LBA index_offset = pos - index.start_lba_on_disc;
if (index_offset >= index.length)
continue;
return &index;
}
return nullptr;
}
const CDImage::Index* CDImage::GetIndexForTrackPosition(u32 track_number, LBA track_pos)
{
if (track_number < 1 || track_number > m_tracks.size())
return nullptr;
const Track& track = m_tracks[track_number - 1];
if (track_pos >= track.length)
return nullptr;
return GetIndexForDiscPosition(track.start_lba + track_pos);
}
bool CDImage::GenerateSubChannelQ(SubChannelQ* subq, LBA lba)
{
const Index* index = GetIndexForDiscPosition(lba);
if (!index)
return false;
const u32 index_offset = index->start_lba_on_disc - lba;
GenerateSubChannelQ(subq, *index, index_offset);
return true;
}
void CDImage::GenerateSubChannelQ(SubChannelQ* subq, const Index& index, u32 index_offset)
{
subq->control_bits = index.control.bits;
subq->track_number_bcd = (index.track_number <= m_tracks.size() ? BinaryToBCD(static_cast<u8>(index.track_number)) :
static_cast<u8>(index.track_number));
subq->index_number_bcd = BinaryToBCD(static_cast<u8>(index.index_number));
Position relative_position;
if (index.is_pregap)
{
// position should count down to the end of the pregap
relative_position = Position::FromLBA(index.length - index_offset - 1);
}
else
{
// count up from the start of the track
relative_position = Position::FromLBA(index.start_lba_in_track + index_offset);
}
std::tie(subq->relative_minute_bcd, subq->relative_second_bcd, subq->relative_frame_bcd) = relative_position.ToBCD();
subq->reserved = 0;
const Position absolute_position = Position::FromLBA(index.start_lba_on_disc + index_offset);
std::tie(subq->absolute_minute_bcd, subq->absolute_second_bcd, subq->absolute_frame_bcd) = absolute_position.ToBCD();
subq->crc = SubChannelQ::ComputeCRC(subq->data);
}
void CDImage::AddLeadOutIndex()
{
Assert(!m_indices.empty());
const Index& last_index = m_indices.back();
Index index = {};
index.start_lba_on_disc = last_index.start_lba_on_disc + last_index.length;
index.length = LEAD_OUT_SECTOR_COUNT;
index.track_number = LEAD_OUT_TRACK_NUMBER;
index.index_number = 0;
index.control.bits = last_index.control.bits;
m_indices.push_back(index);
}
u16 CDImage::SubChannelQ::ComputeCRC(const Data& data)
{
static constexpr std::array<u16, 256> crc16_table = {
{0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD,
0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A,
0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B,
0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861,
0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96,
0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87,
0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A,
0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3,
0x5004, 0x4025, 0x7046, 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290,
0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E,
0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F,
0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D, 0xDB5C,
0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83,
0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74,
0x2E93, 0x3EB2, 0x0ED1, 0x1EF0}};
u16 value = 0;
for (u32 i = 0; i < 10; i++)
value = crc16_table[(value >> 8) ^ data[i]] ^ (value << 8);
return ~(value >> 8) | (~(value) << 8);
}
bool CDImage::SubChannelQ::IsCRCValid() const
{
return crc == ComputeCRC(data);
}

337
src/util/cd_image.h Normal file
View File

@ -0,0 +1,337 @@
#pragma once
#include "common/bitfield.h"
#include "common/progress_callback.h"
#include "common/types.h"
#include <array>
#include <memory>
#include <string>
#include <tuple>
#include <vector>
namespace Common {
class Error;
}
class CDImage
{
public:
CDImage();
virtual ~CDImage();
using LBA = u32;
enum : u32
{
RAW_SECTOR_SIZE = 2352,
DATA_SECTOR_SIZE = 2048,
SECTOR_SYNC_SIZE = 12,
SECTOR_HEADER_SIZE = 4,
FRAMES_PER_SECOND = 75, // "sectors", or "timecode frames" (not "channel frames")
SECONDS_PER_MINUTE = 60,
FRAMES_PER_MINUTE = FRAMES_PER_SECOND * SECONDS_PER_MINUTE,
SUBCHANNEL_BYTES_PER_FRAME = 12,
LEAD_OUT_SECTOR_COUNT = 6750
};
enum : u8
{
LEAD_OUT_TRACK_NUMBER = 0xAA
};
enum class ReadMode : u32
{
DataOnly, // 2048 bytes per sector.
RawSector, // 2352 bytes per sector.
RawNoSync, // 2340 bytes per sector.
};
enum class TrackMode : u32
{
Audio, // 2352 bytes per sector
Mode1, // 2048 bytes per sector
Mode1Raw, // 2352 bytes per sector
Mode2, // 2336 bytes per sector
Mode2Form1, // 2048 bytes per sector
Mode2Form2, // 2324 bytes per sector
Mode2FormMix, // 2332 bytes per sector
Mode2Raw // 2352 bytes per sector
};
enum class PrecacheResult : u8
{
Unsupported,
ReadError,
Success,
};
struct SectorHeader
{
u8 minute;
u8 second;
u8 frame;
u8 sector_mode;
};
struct Position
{
u8 minute;
u8 second;
u8 frame;
static constexpr Position FromBCD(u8 minute, u8 second, u8 frame)
{
return Position{PackedBCDToBinary(minute), PackedBCDToBinary(second), PackedBCDToBinary(frame)};
}
static constexpr Position FromLBA(LBA lba)
{
const u8 frame = Truncate8(lba % FRAMES_PER_SECOND);
lba /= FRAMES_PER_SECOND;
const u8 second = Truncate8(lba % SECONDS_PER_MINUTE);
lba /= SECONDS_PER_MINUTE;
const u8 minute = Truncate8(lba);
return Position{minute, second, frame};
}
LBA ToLBA() const
{
return ZeroExtend32(minute) * FRAMES_PER_MINUTE + ZeroExtend32(second) * FRAMES_PER_SECOND + ZeroExtend32(frame);
}
constexpr std::tuple<u8, u8, u8> ToBCD() const
{
return std::make_tuple<u8, u8, u8>(BinaryToBCD(minute), BinaryToBCD(second), BinaryToBCD(frame));
}
Position operator+(const Position& rhs) { return FromLBA(ToLBA() + rhs.ToLBA()); }
Position& operator+=(const Position& pos)
{
*this = *this + pos;
return *this;
}
#define RELATIONAL_OPERATOR(op) \
bool operator op(const Position& rhs) const \
{ \
return std::tie(minute, second, frame) op std::tie(rhs.minute, rhs.second, rhs.frame); \
}
RELATIONAL_OPERATOR(==);
RELATIONAL_OPERATOR(!=);
RELATIONAL_OPERATOR(<);
RELATIONAL_OPERATOR(<=);
RELATIONAL_OPERATOR(>);
RELATIONAL_OPERATOR(>=);
#undef RELATIONAL_OPERATOR
};
union SubChannelQ
{
using Data = std::array<u8, SUBCHANNEL_BYTES_PER_FRAME>;
union Control
{
u8 bits;
BitField<u8, u8, 0, 4> adr;
BitField<u8, bool, 4, 1> audio_preemphasis;
BitField<u8, bool, 5, 1> digital_copy_permitted;
BitField<u8, bool, 6, 1> data;
BitField<u8, bool, 7, 1> four_channel_audio;
Control& operator=(const Control& rhs)
{
bits = rhs.bits;
return *this;
}
};
struct
{
u8 control_bits;
u8 track_number_bcd;
u8 index_number_bcd;
u8 relative_minute_bcd;
u8 relative_second_bcd;
u8 relative_frame_bcd;
u8 reserved;
u8 absolute_minute_bcd;
u8 absolute_second_bcd;
u8 absolute_frame_bcd;
u16 crc;
};
Data data;
static u16 ComputeCRC(const Data& data);
Control GetControl() const { return Control{control_bits}; }
bool IsData() const { return GetControl().data; }
bool IsCRCValid() const;
SubChannelQ& operator=(const SubChannelQ& q)
{
data = q.data;
return *this;
}
};
static_assert(sizeof(SubChannelQ) == SUBCHANNEL_BYTES_PER_FRAME, "SubChannelQ is correct size");
struct Track
{
u32 track_number;
LBA start_lba;
u32 first_index;
u32 length;
TrackMode mode;
SubChannelQ::Control control;
};
struct Index
{
u64 file_offset;
u32 file_index;
u32 file_sector_size;
LBA start_lba_on_disc;
u32 track_number;
u32 index_number;
LBA start_lba_in_track;
u32 length;
TrackMode mode;
SubChannelQ::Control control;
bool is_pregap;
};
// Helper functions.
static u32 GetBytesPerSector(TrackMode mode);
/// Returns a list of physical CD-ROM devices, .first being the device path, .second being the device name.
static std::vector<std::pair<std::string, std::string>> GetDeviceList();
/// Returns true if the specified filename is a CD-ROM device name.
static bool IsDeviceName(const char* filename);
// Opening disc image.
static std::unique_ptr<CDImage> Open(const char* filename, Common::Error* error);
static std::unique_ptr<CDImage> OpenBinImage(const char* filename, Common::Error* error);
static std::unique_ptr<CDImage> OpenCueSheetImage(const char* filename, Common::Error* error);
static std::unique_ptr<CDImage> OpenCHDImage(const char* filename, Common::Error* error);
static std::unique_ptr<CDImage> OpenEcmImage(const char* filename, Common::Error* error);
static std::unique_ptr<CDImage> OpenMdsImage(const char* filename, Common::Error* error);
static std::unique_ptr<CDImage> OpenPBPImage(const char* filename, Common::Error* error);
static std::unique_ptr<CDImage> OpenM3uImage(const char* filename, Common::Error* error);
static std::unique_ptr<CDImage> OpenDeviceImage(const char* filename, Common::Error* error);
static std::unique_ptr<CDImage>
CreateMemoryImage(CDImage* image, ProgressCallback* progress = ProgressCallback::NullProgressCallback);
static std::unique_ptr<CDImage> OverlayPPFPatch(const char* filename, std::unique_ptr<CDImage> parent_image,
ProgressCallback* progress = ProgressCallback::NullProgressCallback);
// Accessors.
const std::string& GetFileName() const { return m_filename; }
LBA GetPositionOnDisc() const { return m_position_on_disc; }
Position GetMSFPositionOnDisc() const { return Position::FromLBA(m_position_on_disc); }
LBA GetPositionInTrack() const { return m_position_in_track; }
Position GetMSFPositionInTrack() const { return Position::FromLBA(m_position_in_track); }
LBA GetLBACount() const { return m_lba_count; }
u32 GetIndexNumber() const { return m_current_index->index_number; }
u32 GetTrackNumber() const { return m_current_index->track_number; }
u32 GetTrackCount() const { return static_cast<u32>(m_tracks.size()); }
LBA GetTrackStartPosition(u8 track) const;
Position GetTrackStartMSFPosition(u8 track) const;
LBA GetTrackLength(u8 track) const;
Position GetTrackMSFLength(u8 track) const;
TrackMode GetTrackMode(u8 track) const;
LBA GetTrackIndexPosition(u8 track, u8 index) const;
LBA GetTrackIndexLength(u8 track, u8 index) const;
u32 GetFirstTrackNumber() const { return m_tracks.front().track_number; }
u32 GetLastTrackNumber() const { return m_tracks.back().track_number; }
u32 GetIndexCount() const { return static_cast<u32>(m_indices.size()); }
const std::vector<Track>& GetTracks() const { return m_tracks; }
const std::vector<Index>& GetIndices() const { return m_indices; }
const Track& GetTrack(u32 track) const;
const Index& GetIndex(u32 i) const;
// Seek to data LBA.
bool Seek(LBA lba);
// Seek to disc position (MSF).
bool Seek(const Position& pos);
// Seek to track and position.
bool Seek(u32 track_number, const Position& pos_in_track);
// Seek to track and LBA.
bool Seek(u32 track_number, LBA lba);
// Read from the current LBA. Returns the number of sectors read.
u32 Read(ReadMode read_mode, u32 sector_count, void* buffer);
// Read a single raw sector, and subchannel from the current LBA.
bool ReadRawSector(void* buffer, SubChannelQ* subq);
// Reads sub-channel Q for the specified index+LBA.
virtual bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index);
// Returns true if the image has replacement subchannel data.
virtual bool HasNonStandardSubchannel() const;
// Reads a single sector from an index.
virtual bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) = 0;
// Retrieve image metadata.
virtual std::string GetMetadata(const std::string_view& type) const;
// Returns true if this image type has sub-images (e.g. m3u).
virtual bool HasSubImages() const;
// Returns the number of sub-images in this image, if the format supports multiple.
virtual u32 GetSubImageCount() const;
// Returns the current sub-image index, if any.
virtual u32 GetCurrentSubImage() const;
// Changes the current sub-image. If this fails, the image state is unchanged.
virtual bool SwitchSubImage(u32 index, Common::Error* error);
// Retrieve sub-image metadata.
virtual std::string GetSubImageMetadata(u32 index, const std::string_view& type) const;
// Returns true if the source supports precaching, which may be more optimal than an in-memory copy.
virtual PrecacheResult Precache(ProgressCallback* progress = ProgressCallback::NullProgressCallback);
protected:
void ClearTOC();
void CopyTOC(const CDImage* image);
const Index* GetIndexForDiscPosition(LBA pos);
const Index* GetIndexForTrackPosition(u32 track_number, LBA track_pos);
/// Generates sub-channel Q given the specified position.
bool GenerateSubChannelQ(SubChannelQ* subq, LBA lba);
/// Generates sub-channel Q from the given index and index-offset.
void GenerateSubChannelQ(SubChannelQ* subq, const Index& index, u32 index_offset);
/// Synthesis of lead-out data.
void AddLeadOutIndex();
std::string m_filename;
u32 m_lba_count = 0;
std::vector<Track> m_tracks;
std::vector<Index> m_indices;
private:
// Position on disc.
LBA m_position_on_disc = 0;
// Position in track/index.
const Index* m_current_index = nullptr;
LBA m_position_in_index = 0;
LBA m_position_in_track = 0;
};

143
src/util/cd_image_bin.cpp Normal file
View File

@ -0,0 +1,143 @@
#include "cd_image.h"
#include "cd_subchannel_replacement.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include <cerrno>
Log_SetChannel(CDImageBin);
class CDImageBin : public CDImage
{
public:
CDImageBin();
~CDImageBin() override;
bool Open(const char* filename, Common::Error* error);
bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) override;
bool HasNonStandardSubchannel() const override;
protected:
bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override;
private:
std::FILE* m_fp = nullptr;
u64 m_file_position = 0;
CDSubChannelReplacement m_sbi;
};
CDImageBin::CDImageBin() = default;
CDImageBin::~CDImageBin()
{
if (m_fp)
std::fclose(m_fp);
}
bool CDImageBin::Open(const char* filename, Common::Error* error)
{
m_filename = filename;
m_fp = FileSystem::OpenCFile(filename, "rb");
if (!m_fp)
{
Log_ErrorPrintf("Failed to open binfile '%s': errno %d", filename, errno);
if (error)
error->SetErrno(errno);
return false;
}
const u32 track_sector_size = RAW_SECTOR_SIZE;
// determine the length from the file
std::fseek(m_fp, 0, SEEK_END);
const u32 file_size = static_cast<u32>(std::ftell(m_fp));
std::fseek(m_fp, 0, SEEK_SET);
m_lba_count = file_size / track_sector_size;
SubChannelQ::Control control = {};
TrackMode mode = TrackMode::Mode2Raw;
control.data = mode != TrackMode::Audio;
// Two seconds default pregap.
const u32 pregap_frames = 2 * FRAMES_PER_SECOND;
Index pregap_index = {};
pregap_index.file_sector_size = track_sector_size;
pregap_index.start_lba_on_disc = 0;
pregap_index.start_lba_in_track = static_cast<LBA>(-static_cast<s32>(pregap_frames));
pregap_index.length = pregap_frames;
pregap_index.track_number = 1;
pregap_index.index_number = 0;
pregap_index.mode = mode;
pregap_index.control.bits = control.bits;
pregap_index.is_pregap = true;
m_indices.push_back(pregap_index);
// Data index.
Index data_index = {};
data_index.file_index = 0;
data_index.file_offset = 0;
data_index.file_sector_size = track_sector_size;
data_index.start_lba_on_disc = pregap_index.length;
data_index.track_number = 1;
data_index.index_number = 1;
data_index.start_lba_in_track = 0;
data_index.length = m_lba_count;
data_index.mode = mode;
data_index.control.bits = control.bits;
m_indices.push_back(data_index);
// Assume a single track.
m_tracks.push_back(
Track{static_cast<u32>(1), data_index.start_lba_on_disc, static_cast<u32>(0), m_lba_count, mode, control});
AddLeadOutIndex();
m_sbi.LoadSBIFromImagePath(filename);
return Seek(1, Position{0, 0, 0});
}
bool CDImageBin::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index)
{
if (m_sbi.GetReplacementSubChannelQ(index.start_lba_on_disc + lba_in_index, subq))
return true;
return CDImage::ReadSubChannelQ(subq, index, lba_in_index);
}
bool CDImageBin::HasNonStandardSubchannel() const
{
return (m_sbi.GetReplacementSectorCount() > 0);
}
bool CDImageBin::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index)
{
const u64 file_position = index.file_offset + (static_cast<u64>(lba_in_index) * index.file_sector_size);
if (m_file_position != file_position)
{
if (std::fseek(m_fp, static_cast<long>(file_position), SEEK_SET) != 0)
return false;
m_file_position = file_position;
}
if (std::fread(buffer, index.file_sector_size, 1, m_fp) != 1)
{
std::fseek(m_fp, static_cast<long>(m_file_position), SEEK_SET);
return false;
}
m_file_position += index.file_sector_size;
return true;
}
std::unique_ptr<CDImage> CDImage::OpenBinImage(const char* filename, Common::Error* error)
{
std::unique_ptr<CDImageBin> image = std::make_unique<CDImageBin>();
if (!image->Open(filename, error))
return {};
return image;
}

400
src/util/cd_image_chd.cpp Normal file
View File

@ -0,0 +1,400 @@
#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
#define _CRT_SECURE_NO_WARNINGS
#endif
#include "cd_image.h"
#include "cd_subchannel_replacement.h"
#include "common/align.h"
#include "common/assert.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/platform.h"
#include "libchdr/chd.h"
#include <algorithm>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <limits>
#include <map>
#include <optional>
Log_SetChannel(CDImageCHD);
static std::optional<CDImage::TrackMode> 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, Common::Error* error);
bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) override;
bool HasNonStandardSubchannel() const override;
PrecacheResult Precache(ProgressCallback* progress) override;
protected:
bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override;
private:
enum : u32
{
CHD_CD_SECTOR_DATA_SIZE = 2352 + 96,
CHD_CD_TRACK_ALIGNMENT = 4
};
bool ReadHunk(u32 hunk_index);
std::FILE* m_fp = nullptr;
chd_file* m_chd = nullptr;
u32 m_hunk_size = 0;
u32 m_sectors_per_hunk = 0;
std::vector<u8> m_hunk_buffer;
u32 m_current_hunk_index = static_cast<u32>(-1);
CDSubChannelReplacement m_sbi;
};
CDImageCHD::CDImageCHD() = default;
CDImageCHD::~CDImageCHD()
{
if (m_chd)
chd_close(m_chd);
if (m_fp)
std::fclose(m_fp);
}
bool CDImageCHD::Open(const char* filename, Common::Error* error)
{
Assert(!m_fp);
m_fp = FileSystem::OpenCFile(filename, "rb");
if (!m_fp)
{
Log_ErrorPrintf("Failed to open CHD '%s': errno %d", filename, errno);
if (error)
error->SetErrno(errno);
return false;
}
chd_error err = chd_open_file(m_fp, CHD_OPEN_READ, nullptr, &m_chd);
if (err != CHDERR_NONE)
{
Log_ErrorPrintf("Failed to open CHD '%s': %s", filename, chd_error_string(err));
if (error)
error->SetMessage(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_CD_SECTOR_DATA_SIZE) != 0)
{
Log_ErrorPrintf("Hunk size (%u) is not a multiple of %u", m_hunk_size, CHD_CD_SECTOR_DATA_SIZE);
if (error)
error->SetFormattedMessage("Hunk size (%u) is not a multiple of %u", m_hunk_size, CHD_CD_SECTOR_DATA_SIZE);
return false;
}
m_sectors_per_hunk = m_hunk_size / CHD_CD_SECTOR_DATA_SIZE;
m_hunk_buffer.resize(m_hunk_size);
m_filename = filename;
u32 disc_lba = 0;
u64 file_lba = 0;
// 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, 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);
if (error)
error->SetFormattedMessage("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);
if (error)
error->SetFormattedMessage("Invalid track v2 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);
if (error)
{
error->SetFormattedMessage("Incorrect track number at index %d, expected %d got %d", num_tracks,
(num_tracks + 1), track_num);
}
return false;
}
std::optional<TrackMode> mode = ParseTrackModeString(type_str);
if (!mode.has_value())
{
Log_ErrorPrintf("Invalid track mode: '%s'", type_str);
if (error)
error->SetFormattedMessage("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<LBA>(static_cast<unsigned long>(-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 = 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);
if (error)
error->SetFormattedMessage("Pregap length %u exceeds track length %u", pregap_frames, frames);
return false;
}
pregap_index.file_index = 0;
pregap_index.file_offset = file_lba;
pregap_index.file_sector_size = CHD_CD_SECTOR_DATA_SIZE;
file_lba += 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<u32>(track_num), disc_lba, static_cast<u32>(m_indices.size()),
static_cast<u32>(frames + pregap_frames), mode.value(), control});
// 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_CD_SECTOR_DATA_SIZE;
index.file_offset = file_lba;
index.mode = mode.value();
index.control.bits = control.bits;
index.is_pregap = false;
index.length = static_cast<u32>(frames);
m_indices.push_back(index);
disc_lba += index.length;
file_lba += index.length;
num_tracks++;
// each track is padded to a multiple of 4 frames, see chdman source.
file_lba = Common::AlignUp(file_lba, CHD_CD_TRACK_ALIGNMENT);
}
if (m_tracks.empty())
{
Log_ErrorPrintf("File '%s' contains no tracks", filename);
if (error)
error->SetFormattedMessage("File '%s' contains no tracks", filename);
return false;
}
m_lba_count = disc_lba;
AddLeadOutIndex();
m_sbi.LoadSBIFromImagePath(filename);
return Seek(1, Position{0, 0, 0});
}
bool CDImageCHD::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index)
{
if (m_sbi.GetReplacementSubChannelQ(index.start_lba_on_disc + lba_in_index, subq))
return true;
// TODO: Read subchannel data from CHD
return CDImage::ReadSubChannelQ(subq, index, lba_in_index);
}
bool CDImageCHD::HasNonStandardSubchannel() const
{
return (m_sbi.GetReplacementSectorCount() > 0);
}
CDImage::PrecacheResult CDImageCHD::Precache(ProgressCallback* progress)
{
const std::string_view title(FileSystem::GetDisplayNameFromPath(m_filename));
progress->SetFormattedStatusText("Precaching %.*s...", static_cast<int>(title.size()), title.data());
progress->SetProgressRange(100);
auto callback = [](size_t pos, size_t total, void* param) {
const u32 percent = static_cast<u32>((pos * 100) / total);
static_cast<ProgressCallback*>(param)->SetProgressValue(std::min<u32>(percent, 100));
};
return (chd_precache_progress(m_chd, callback, progress) == CHDERR_NONE) ? CDImage::PrecacheResult::Success :
CDImage::PrecacheResult::ReadError;
}
// 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<u8*>(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<LBA>(index.file_offset) + lba_in_index;
const u32 hunk_index = static_cast<u32>(disc_frame / m_sectors_per_hunk);
const u32 hunk_offset = static_cast<u32>((disc_frame % m_sectors_per_hunk) * CHD_CD_SECTOR_DATA_SIZE);
DebugAssert((m_hunk_size - hunk_offset) >= CHD_CD_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<u32>(-1);
return false;
}
m_current_hunk_index = hunk_index;
return true;
}
std::unique_ptr<CDImage> CDImage::OpenCHDImage(const char* filename, Common::Error* error)
{
std::unique_ptr<CDImageCHD> image = std::make_unique<CDImageCHD>();
if (!image->Open(filename, error))
return {};
return image;
}

340
src/util/cd_image_cue.cpp Normal file
View File

@ -0,0 +1,340 @@
#include "cd_image.h"
#include "cd_subchannel_replacement.h"
#include "common/assert.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include "cue_parser.h"
#include <algorithm>
#include <cerrno>
#include <cinttypes>
#include <map>
Log_SetChannel(CDImageCueSheet);
class CDImageCueSheet : public CDImage
{
public:
CDImageCueSheet();
~CDImageCueSheet() override;
bool OpenAndParse(const char* filename, Common::Error* error);
bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) override;
bool HasNonStandardSubchannel() const override;
protected:
bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override;
private:
struct TrackFile
{
std::string filename;
std::FILE* file;
u64 file_position;
};
std::vector<TrackFile> m_files;
CDSubChannelReplacement m_sbi;
};
CDImageCueSheet::CDImageCueSheet() = default;
CDImageCueSheet::~CDImageCueSheet()
{
std::for_each(m_files.begin(), m_files.end(), [](TrackFile& t) { std::fclose(t.file); });
}
bool CDImageCueSheet::OpenAndParse(const char* filename, Common::Error* error)
{
std::FILE* fp = FileSystem::OpenCFile(filename, "rb");
if (!fp)
{
Log_ErrorPrintf("Failed to open cuesheet '%s': errno %d", filename, errno);
if (error)
error->SetErrno(errno);
return false;
}
CueParser::File parser;
if (!parser.Parse(fp, error))
{
std::fclose(fp);
return false;
}
std::fclose(fp);
m_filename = filename;
u32 disc_lba = 0;
// for each track..
for (u32 track_num = 1; track_num <= CueParser::MAX_TRACK_NUMBER; track_num++)
{
const CueParser::Track* track = parser.GetTrack(track_num);
if (!track)
break;
const std::string track_filename(track->file);
LBA track_start = track->start.ToLBA();
u32 track_file_index = 0;
for (; track_file_index < m_files.size(); track_file_index++)
{
const TrackFile& t = m_files[track_file_index];
if (t.filename == track_filename)
break;
}
if (track_file_index == m_files.size())
{
const std::string track_full_filename(
!Path::IsAbsolute(track_filename) ? Path::BuildRelativePath(m_filename, track_filename) : track_filename);
std::FILE* track_fp = FileSystem::OpenCFile(track_full_filename.c_str(), "rb");
if (!track_fp && track_file_index == 0)
{
// many users have bad cuesheets, or they're renamed the files without updating the cuesheet.
// so, try searching for a bin with the same name as the cue, but only for the first referenced file.
const std::string alternative_filename(Path::ReplaceExtension(filename, "bin"));
track_fp = FileSystem::OpenCFile(alternative_filename.c_str(), "rb");
if (track_fp)
{
Log_WarningPrintf("Your cue sheet references an invalid file '%s', but this was found at '%s' instead.",
track_filename.c_str(), alternative_filename.c_str());
}
}
if (!track_fp)
{
Log_ErrorPrintf("Failed to open track filename '%s' (from '%s' and '%s'): errno %d",
track_full_filename.c_str(), track_filename.c_str(), filename, errno);
if (error)
{
error->SetFormattedMessage("Failed to open track filename '%s' (from '%s' and '%s'): errno %d",
track_full_filename.c_str(), track_filename.c_str(), filename, errno);
}
return false;
}
m_files.push_back(TrackFile{std::move(track_filename), track_fp, 0});
}
// data type determines the sector size
const TrackMode mode = track->mode;
const u32 track_sector_size = GetBytesPerSector(mode);
// precompute subchannel q flags for the whole track
SubChannelQ::Control control{};
control.data = mode != TrackMode::Audio;
control.audio_preemphasis = track->HasFlag(CueParser::TrackFlag::PreEmphasis);
control.digital_copy_permitted = track->HasFlag(CueParser::TrackFlag::CopyPermitted);
control.four_channel_audio = track->HasFlag(CueParser::TrackFlag::FourChannelAudio);
// determine the length from the file
LBA track_length;
if (!track->length.has_value())
{
FileSystem::FSeek64(m_files[track_file_index].file, 0, SEEK_END);
u64 file_size = static_cast<u64>(FileSystem::FTell64(m_files[track_file_index].file));
FileSystem::FSeek64(m_files[track_file_index].file, 0, SEEK_SET);
file_size /= track_sector_size;
if (track_start >= file_size)
{
Log_ErrorPrintf("Failed to open track %u in '%s': track start is out of range (%u vs %" PRIu64 ")", track_num,
filename, track_start, file_size);
if (error)
{
error->SetFormattedMessage("Failed to open track %u in '%s': track start is out of range (%u vs %" PRIu64 ")",
track_num, filename, track_start, file_size);
}
return false;
}
track_length = static_cast<LBA>(file_size - track_start);
}
else
{
track_length = track->length.value().ToLBA();
}
const Position* index0 = track->GetIndex(0);
LBA pregap_frames;
if (index0)
{
// index 1 is always present, so this is safe
pregap_frames = track->GetIndex(1)->ToLBA() - index0->ToLBA();
// Pregap/index 0 is in the file, easy.
Index pregap_index = {};
pregap_index.start_lba_on_disc = disc_lba;
pregap_index.start_lba_in_track = static_cast<LBA>(-static_cast<s32>(pregap_frames));
pregap_index.length = pregap_frames;
pregap_index.track_number = track_num;
pregap_index.index_number = 0;
pregap_index.mode = mode;
pregap_index.control.bits = control.bits;
pregap_index.is_pregap = true;
pregap_index.file_index = track_file_index;
pregap_index.file_offset = static_cast<u64>(static_cast<s64>(track_start - pregap_frames)) * track_sector_size;
pregap_index.file_sector_size = track_sector_size;
m_indices.push_back(pregap_index);
disc_lba += pregap_index.length;
}
else
{
// Two seconds pregap for track 1 is assumed if not specified.
// Some people have broken (older) dumps where a two second pregap was implicit but not specified in the cuesheet.
// The problem is we can't tell between a missing implicit two second pregap and a zero second pregap. Most of
// these seem to be a single bin file for all tracks. So if this is the case, we add the two seconds in if it's
// not specified. If this is an audio CD (likely when track 1 is not data), we don't add these pregaps, and rely
// on the cuesheet. If we did add them, it causes issues in some games (e.g. Dancing Stage featuring DREAMS COME
// TRUE).
const bool is_multi_track_bin = (track_num > 1 && track_file_index == m_indices[0].file_index);
const bool likely_audio_cd = (parser.GetTrack(1)->mode == TrackMode::Audio);
pregap_frames = track->zero_pregap.has_value() ? track->zero_pregap->ToLBA() : 0;
if ((track_num == 1 || is_multi_track_bin) && !track->zero_pregap.has_value() &&
(track_num == 1 || !likely_audio_cd))
{
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<LBA>(-static_cast<s32>(pregap_frames));
pregap_index.length = pregap_frames;
pregap_index.track_number = track_num;
pregap_index.index_number = 0;
pregap_index.mode = mode;
pregap_index.control.bits = control.bits;
pregap_index.is_pregap = true;
m_indices.push_back(pregap_index);
disc_lba += pregap_index.length;
}
}
// add the track itself
m_tracks.push_back(
Track{track_num, disc_lba, static_cast<u32>(m_indices.size()), track_length + pregap_frames, mode, control});
// how many indices in this track?
Index last_index;
last_index.start_lba_on_disc = disc_lba;
last_index.start_lba_in_track = 0;
last_index.track_number = track_num;
last_index.index_number = 1;
last_index.file_index = track_file_index;
last_index.file_sector_size = track_sector_size;
last_index.file_offset = static_cast<u64>(track_start) * track_sector_size;
last_index.mode = mode;
last_index.control.bits = control.bits;
last_index.is_pregap = false;
u32 last_index_offset = track_start;
for (u32 index_num = 1;; index_num++)
{
const Position* pos = track->GetIndex(index_num);
if (!pos)
break;
const u32 index_offset = pos->ToLBA();
// add an index between the track indices
if (index_offset > last_index_offset)
{
last_index.length = index_offset - last_index_offset;
m_indices.push_back(last_index);
disc_lba += last_index.length;
last_index.start_lba_in_track += last_index.length;
last_index.start_lba_on_disc = disc_lba;
last_index.length = 0;
}
last_index.file_offset = index_offset * last_index.file_sector_size;
last_index.index_number = static_cast<u32>(index_num);
last_index_offset = index_offset;
}
// and the last index is added here
const u32 track_end_index = track_start + track_length;
DebugAssert(track_end_index >= last_index_offset);
if (track_end_index > last_index_offset)
{
last_index.length = track_end_index - last_index_offset;
m_indices.push_back(last_index);
disc_lba += last_index.length;
}
}
if (m_tracks.empty())
{
Log_ErrorPrintf("File '%s' contains no tracks", filename);
if (error)
error->SetFormattedMessage("File '%s' contains no tracks", filename);
return false;
}
m_lba_count = disc_lba;
AddLeadOutIndex();
m_sbi.LoadSBIFromImagePath(filename);
return Seek(1, Position{0, 0, 0});
}
bool CDImageCueSheet::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index)
{
if (m_sbi.GetReplacementSubChannelQ(index.start_lba_on_disc + lba_in_index, subq))
return true;
return CDImage::ReadSubChannelQ(subq, index, lba_in_index);
}
bool CDImageCueSheet::HasNonStandardSubchannel() const
{
return (m_sbi.GetReplacementSectorCount() > 0);
}
bool CDImageCueSheet::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index)
{
DebugAssert(index.file_index < m_files.size());
TrackFile& tf = m_files[index.file_index];
const u64 file_position = index.file_offset + (static_cast<u64>(lba_in_index) * index.file_sector_size);
if (tf.file_position != file_position)
{
if (std::fseek(tf.file, static_cast<long>(file_position), SEEK_SET) != 0)
return false;
tf.file_position = file_position;
}
if (std::fread(buffer, index.file_sector_size, 1, tf.file) != 1)
{
std::fseek(tf.file, static_cast<long>(tf.file_position), SEEK_SET);
return false;
}
tf.file_position += index.file_sector_size;
return true;
}
std::unique_ptr<CDImage> CDImage::OpenCueSheetImage(const char* filename, Common::Error* error)
{
std::unique_ptr<CDImageCueSheet> image = std::make_unique<CDImageCueSheet>();
if (!image->OpenAndParse(filename, error))
return {};
return image;
}

View File

@ -0,0 +1,543 @@
#include "assert.h"
#include "cd_image.h"
#include "common/error.h"
#include "common/log.h"
#include "common/string_util.h"
#include <algorithm>
#include <cerrno>
#include <cinttypes>
#include <cmath>
Log_SetChannel(CDImageDevice);
static constexpr u32 MAX_TRACK_NUMBER = 99;
static constexpr int ALL_SUBCODE_SIZE = 96;
static u32 BEToU32(const u8* val)
{
return (static_cast<u32>(val[0]) << 24) | (static_cast<u32>(val[1]) << 16) | (static_cast<u32>(val[2]) << 8) |
static_cast<u32>(val[3]);
}
static void U16ToBE(u8* beval, u16 leval)
{
beval[0] = static_cast<u8>(leval >> 8);
beval[1] = static_cast<u8>(leval);
}
// Adapted from
// https://github.com/saramibreak/DiscImageCreator/blob/5a8fe21730872d67991211f1319c87f0780f2d0f/DiscImageCreator/convert.cpp
static void DeinterleaveSubcode(const u8* subcode_in, u8* subcode_out)
{
std::memset(subcode_out, 0, ALL_SUBCODE_SIZE);
int row = 0;
for (int bitNum = 0; bitNum < 8; bitNum++)
{
for (int nColumn = 0; nColumn < ALL_SUBCODE_SIZE; row++)
{
u32 mask = 0x80;
for (int nShift = 0; nShift < 8; nShift++, nColumn++)
{
const int n = nShift - bitNum;
if (n > 0)
{
subcode_out[row] |= static_cast<u8>((subcode_in[nColumn] >> n) & mask);
}
else
{
subcode_out[row] |= static_cast<u8>((subcode_in[nColumn] << std::abs(n)) & mask);
}
mask >>= 1;
}
}
}
}
#if defined(_WIN32) && !defined(_UWP)
// The include order here is critical.
// clang-format off
#include "common/windows_headers.h"
#include <winioctl.h>
#include <ntddcdrm.h>
#include <ntddscsi.h>
// clang-format on
class CDImageDeviceWin32 : public CDImage
{
public:
CDImageDeviceWin32();
~CDImageDeviceWin32() override;
bool Open(const char* filename, Common::Error* error);
bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) override;
bool HasNonStandardSubchannel() const override;
protected:
bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override;
private:
struct SPTDBuffer
{
SCSI_PASS_THROUGH_DIRECT cmd;
u8 sense[20];
};
static void FillSPTD(SPTDBuffer* sptd, u32 sector_number, bool include_subq, void* buffer);
bool ReadSectorToBuffer(u64 offset);
bool DetermineReadMode();
HANDLE m_hDevice = INVALID_HANDLE_VALUE;
u64 m_buffer_offset = ~static_cast<u64>(0);
bool m_use_sptd = true;
bool m_read_subcode = false;
std::array<u8, CD_RAW_SECTOR_WITH_SUBCODE_SIZE> m_buffer;
std::array<u8, ALL_SUBCODE_SIZE> m_deinterleaved_subcode;
std::array<u8, SUBCHANNEL_BYTES_PER_FRAME> m_subq;
};
CDImageDeviceWin32::CDImageDeviceWin32() = default;
CDImageDeviceWin32::~CDImageDeviceWin32()
{
if (m_hDevice != INVALID_HANDLE_VALUE)
CloseHandle(m_hDevice);
}
bool CDImageDeviceWin32::Open(const char* filename, Common::Error* error)
{
m_filename = filename;
m_hDevice = CreateFile(filename, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
OPEN_EXISTING, 0, NULL);
if (m_hDevice == INVALID_HANDLE_VALUE)
{
m_hDevice = CreateFile(filename, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, NULL);
if (m_hDevice != INVALID_HANDLE_VALUE)
{
m_use_sptd = false;
}
else
{
Log_ErrorPrintf("CreateFile('%s') failed: %08X", filename, GetLastError());
if (error)
error->SetWin32(GetLastError());
return false;
}
}
// Set it to 4x speed. A good balance between readahead and spinning up way too high.
static constexpr u32 READ_SPEED_MULTIPLIER = 4;
static constexpr u32 READ_SPEED_KBS = (DATA_SECTOR_SIZE * FRAMES_PER_SECOND * 8) / 1024;
CDROM_SET_SPEED set_speed = {CdromSetSpeed, READ_SPEED_KBS, 0, CdromDefaultRotation};
if (!DeviceIoControl(m_hDevice, IOCTL_CDROM_SET_SPEED, &set_speed, sizeof(set_speed), nullptr, 0, nullptr, nullptr))
Log_WarningPrintf("DeviceIoControl(IOCTL_CDROM_SET_SPEED) failed: %08X", GetLastError());
CDROM_READ_TOC_EX read_toc_ex = {};
read_toc_ex.Format = CDROM_READ_TOC_EX_FORMAT_TOC;
read_toc_ex.Msf = 0;
read_toc_ex.SessionTrack = 1;
CDROM_TOC toc = {};
U16ToBE(toc.Length, sizeof(toc) - sizeof(UCHAR) * 2);
DWORD bytes_returned;
if (!DeviceIoControl(m_hDevice, IOCTL_CDROM_READ_TOC_EX, &read_toc_ex, sizeof(read_toc_ex), &toc, sizeof(toc),
&bytes_returned, nullptr) ||
toc.LastTrack < toc.FirstTrack)
{
Log_ErrorPrintf("DeviceIoCtl(IOCTL_CDROM_READ_TOC_EX) failed: %08X", GetLastError());
if (error)
error->SetWin32(GetLastError());
return false;
}
DWORD last_track_address = 0;
LBA disc_lba = 0;
Log_DevPrintf("FirstTrack=%u, LastTrack=%u", toc.FirstTrack, toc.LastTrack);
const u32 num_tracks_to_check = (toc.LastTrack - toc.FirstTrack) + 1 + 1;
for (u32 track_index = 0; track_index < num_tracks_to_check; track_index++)
{
const TRACK_DATA& td = toc.TrackData[track_index];
const u8 track_num = td.TrackNumber;
const DWORD track_address = BEToU32(td.Address);
Log_DevPrintf(" [%u]: Num=%02X, Address=%u", track_index, track_num, track_address);
// fill in the previous track's length
if (!m_tracks.empty())
{
if (track_num < m_tracks.back().track_number)
{
Log_ErrorPrintf("Invalid TOC, track %u less than %u", track_num, m_tracks.back().track_number);
return false;
}
const LBA previous_track_length = static_cast<LBA>(track_address - last_track_address);
m_tracks.back().length += previous_track_length;
m_indices.back().length += previous_track_length;
disc_lba += previous_track_length;
}
last_track_address = track_address;
if (track_num == LEAD_OUT_TRACK_NUMBER)
{
AddLeadOutIndex();
break;
}
// precompute subchannel q flags for the whole track
SubChannelQ::Control control{};
control.bits = td.Adr | (td.Control << 4);
const LBA track_lba = static_cast<LBA>(track_address);
const TrackMode track_mode = control.data ? CDImage::TrackMode::Mode2Raw : CDImage::TrackMode::Audio;
// TODO: How the hell do we handle pregaps here?
const u32 pregap_frames = (control.data && track_index == 0) ? 150 : 0;
if (pregap_frames > 0)
{
Index pregap_index = {};
pregap_index.start_lba_on_disc = disc_lba;
pregap_index.start_lba_in_track = static_cast<LBA>(-static_cast<s32>(pregap_frames));
pregap_index.length = pregap_frames;
pregap_index.track_number = track_num;
pregap_index.index_number = 0;
pregap_index.mode = track_mode;
pregap_index.control.bits = control.bits;
pregap_index.is_pregap = true;
m_indices.push_back(pregap_index);
disc_lba += pregap_frames;
}
// index 1, will be filled in next iteration
if (track_num <= MAX_TRACK_NUMBER)
{
// add the track itself
m_tracks.push_back(Track{track_num, disc_lba, static_cast<u32>(m_indices.size()), 0, track_mode, control});
Index index1;
index1.start_lba_on_disc = disc_lba;
index1.start_lba_in_track = 0;
index1.length = 0;
index1.track_number = track_num;
index1.index_number = 1;
index1.file_index = 0;
index1.file_sector_size = 2048;
index1.file_offset = static_cast<u64>(track_address) * index1.file_sector_size;
index1.mode = track_mode;
index1.control.bits = control.bits;
index1.is_pregap = false;
m_indices.push_back(index1);
}
}
if (m_tracks.empty())
{
Log_ErrorPrintf("File '%s' contains no tracks", filename);
if (error)
error->SetFormattedMessage("File '%s' contains no tracks", filename);
return false;
}
m_lba_count = disc_lba;
Log_DevPrintf("%u tracks, %u indices, %u lbas", static_cast<u32>(m_tracks.size()), static_cast<u32>(m_indices.size()),
static_cast<u32>(m_lba_count));
for (u32 i = 0; i < m_tracks.size(); i++)
{
Log_DevPrintf(" Track %u: Start %u, length %u, mode %u, control 0x%02X", static_cast<u32>(m_tracks[i].track_number),
static_cast<u32>(m_tracks[i].start_lba), static_cast<u32>(m_tracks[i].length),
static_cast<u32>(m_tracks[i].mode), static_cast<u32>(m_tracks[i].control.bits));
}
for (u32 i = 0; i < m_indices.size(); i++)
{
Log_DevPrintf(" Index %u: Track %u, Index %u, Start %u, length %u, file sector size %u, file offset %" PRIu64, i,
static_cast<u32>(m_indices[i].track_number), static_cast<u32>(m_indices[i].index_number),
static_cast<u32>(m_indices[i].start_lba_on_disc), static_cast<u32>(m_indices[i].length),
static_cast<u32>(m_indices[i].file_sector_size), m_indices[i].file_offset);
}
if (!DetermineReadMode())
{
Log_ErrorPrintf("Could not determine read mode");
if (error)
error->SetMessage("Could not determine read mode");
return false;
}
return Seek(1, Position{0, 0, 0});
}
bool CDImageDeviceWin32::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index)
{
if (index.file_sector_size == 0 || !m_read_subcode)
return CDImage::ReadSubChannelQ(subq, index, lba_in_index);
const u64 offset = index.file_offset + static_cast<u64>(lba_in_index) * index.file_sector_size;
if (m_buffer_offset != offset && !ReadSectorToBuffer(offset))
return false;
// P, Q, ...
std::memcpy(subq->data.data(), m_subq.data(), SUBCHANNEL_BYTES_PER_FRAME);
return true;
}
bool CDImageDeviceWin32::HasNonStandardSubchannel() const
{
return true;
}
bool CDImageDeviceWin32::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index)
{
if (index.file_sector_size == 0)
return false;
const u64 offset = index.file_offset + static_cast<u64>(lba_in_index) * index.file_sector_size;
if (m_buffer_offset != offset && !ReadSectorToBuffer(offset))
return false;
std::memcpy(buffer, m_buffer.data(), RAW_SECTOR_SIZE);
return true;
}
void CDImageDeviceWin32::FillSPTD(SPTDBuffer* sptd, u32 sector_number, bool include_subq, void* buffer)
{
std::memset(sptd, 0, sizeof(SPTDBuffer));
sptd->cmd.Length = sizeof(sptd->cmd);
sptd->cmd.CdbLength = 12;
sptd->cmd.SenseInfoLength = sizeof(sptd->sense);
sptd->cmd.DataIn = SCSI_IOCTL_DATA_IN;
sptd->cmd.DataTransferLength = include_subq ? (RAW_SECTOR_SIZE + SUBCHANNEL_BYTES_PER_FRAME) : RAW_SECTOR_SIZE;
sptd->cmd.TimeOutValue = 10;
sptd->cmd.SenseInfoOffset = offsetof(SPTDBuffer, sense);
sptd->cmd.DataBuffer = buffer;
sptd->cmd.Cdb[0] = 0xBE; // READ CD
sptd->cmd.Cdb[1] = 0x00; // sector type
sptd->cmd.Cdb[2] = Truncate8(sector_number >> 24); // Starting LBA
sptd->cmd.Cdb[3] = Truncate8(sector_number >> 16);
sptd->cmd.Cdb[4] = Truncate8(sector_number >> 8);
sptd->cmd.Cdb[5] = Truncate8(sector_number);
sptd->cmd.Cdb[6] = 0x00; // Transfer Count
sptd->cmd.Cdb[7] = 0x00;
sptd->cmd.Cdb[8] = 0x01;
sptd->cmd.Cdb[9] = (1 << 7) | // include sync
(0b11 << 5) | // include header codes
(1 << 4) | // include user data
(1 << 3) | // edc/ecc
(0 << 2); // don't include C2 data
sptd->cmd.Cdb[10] = (include_subq ? (0b010 << 0) : (0b000 << 0)); // subq selection
}
bool CDImageDeviceWin32::ReadSectorToBuffer(u64 offset)
{
if (m_use_sptd)
{
const u32 sector_number = static_cast<u32>(offset / 2048);
SPTDBuffer sptd = {};
FillSPTD(&sptd, sector_number, m_read_subcode, m_buffer.data());
const u32 expected_bytes = sptd.cmd.DataTransferLength;
DWORD bytes_returned;
if (!DeviceIoControl(m_hDevice, IOCTL_SCSI_PASS_THROUGH_DIRECT, &sptd, sizeof(sptd), &sptd, sizeof(sptd),
&bytes_returned, nullptr) &&
sptd.cmd.ScsiStatus == 0x00)
{
Log_ErrorPrintf("DeviceIoControl(IOCTL_SCSI_PASS_THROUGH_DIRECT) for offset %" PRIu64
" failed: %08X Status 0x%02X",
offset, GetLastError(), sptd.cmd.ScsiStatus);
return false;
}
if (sptd.cmd.DataTransferLength != expected_bytes)
Log_WarningPrintf("Only read %u of %u bytes", static_cast<u32>(sptd.cmd.DataTransferLength), expected_bytes);
if (m_read_subcode)
std::memcpy(m_subq.data(), &m_buffer[RAW_SECTOR_SIZE], SUBCHANNEL_BYTES_PER_FRAME);
}
else
{
RAW_READ_INFO rri;
rri.DiskOffset.QuadPart = offset;
rri.SectorCount = 1;
rri.TrackMode = RawWithSubCode;
DWORD bytes_returned;
if (!DeviceIoControl(m_hDevice, IOCTL_CDROM_RAW_READ, &rri, sizeof(rri), m_buffer.data(),
static_cast<DWORD>(m_buffer.size()), &bytes_returned, nullptr))
{
Log_ErrorPrintf("DeviceIoControl(IOCTL_CDROM_RAW_READ) for offset %" PRIu64 " failed: %08X", offset,
GetLastError());
return false;
}
if (bytes_returned != m_buffer.size())
Log_WarningPrintf("Only read %u of %u bytes", bytes_returned, static_cast<unsigned>(m_buffer.size()));
// P, Q, ...
DeinterleaveSubcode(&m_buffer[RAW_SECTOR_SIZE], m_deinterleaved_subcode.data());
std::memcpy(m_subq.data(), &m_deinterleaved_subcode[SUBCHANNEL_BYTES_PER_FRAME], SUBCHANNEL_BYTES_PER_FRAME);
}
m_buffer_offset = offset;
return true;
}
bool CDImageDeviceWin32::DetermineReadMode()
{
// Prefer raw reads if we can use them
RAW_READ_INFO rri;
rri.DiskOffset.QuadPart = 0;
rri.SectorCount = 1;
rri.TrackMode = RawWithSubCode;
DWORD bytes_returned;
if (DeviceIoControl(m_hDevice, IOCTL_CDROM_RAW_READ, &rri, sizeof(rri), m_buffer.data(),
static_cast<DWORD>(m_buffer.size()), &bytes_returned, nullptr) &&
bytes_returned == CD_RAW_SECTOR_WITH_SUBCODE_SIZE)
{
SubChannelQ subq;
DeinterleaveSubcode(&m_buffer[RAW_SECTOR_SIZE], m_deinterleaved_subcode.data());
std::memcpy(&subq, &m_deinterleaved_subcode[SUBCHANNEL_BYTES_PER_FRAME], SUBCHANNEL_BYTES_PER_FRAME);
m_use_sptd = false;
m_read_subcode = true;
if (subq.IsCRCValid())
{
Log_DevPrintf("Raw read returned invalid SubQ CRC (got %02X expected %02X)", static_cast<unsigned>(subq.crc),
static_cast<unsigned>(SubChannelQ::ComputeCRC(subq.data)));
m_read_subcode = false;
}
else
{
Log_DevPrintf("Using raw reads with subcode");
}
return true;
}
Log_DevPrintf("DeviceIoControl(IOCTL_CDROM_RAW_READ) failed: %08X, %u bytes returned, trying SPTD", GetLastError(),
bytes_returned);
SPTDBuffer sptd = {};
FillSPTD(&sptd, 0, true, m_buffer.data());
if (DeviceIoControl(m_hDevice, IOCTL_SCSI_PASS_THROUGH_DIRECT, &sptd, sizeof(sptd), &sptd, sizeof(sptd),
&bytes_returned, nullptr) &&
sptd.cmd.ScsiStatus == 0x00)
{
// check the validity of the subchannel data. this assumes that the first sector has a valid subq, which it should
// in all PS1 games.
SubChannelQ subq;
std::memcpy(&subq, &m_buffer[RAW_SECTOR_SIZE], sizeof(subq));
if (subq.IsCRCValid())
{
Log_DevPrintf("Using SPTD reads with subq (%u, status 0x%02X)", sptd.cmd.DataTransferLength, sptd.cmd.ScsiStatus);
m_read_subcode = true;
m_use_sptd = true;
return true;
}
else
{
Log_DevPrintf("SPTD read returned invalid SubQ CRC (got %02X expected %02X)", static_cast<unsigned>(subq.crc),
static_cast<unsigned>(SubChannelQ::ComputeCRC(subq.data)));
}
}
// try without subcode
FillSPTD(&sptd, 0, false, m_buffer.data());
if (DeviceIoControl(m_hDevice, IOCTL_SCSI_PASS_THROUGH_DIRECT, &sptd, sizeof(sptd), &sptd, sizeof(sptd),
&bytes_returned, nullptr) &&
sptd.cmd.ScsiStatus == 0x00)
{
Log_DevPrintf("Using SPTD reads without subq (%u, status 0x%02X)", sptd.cmd.DataTransferLength,
sptd.cmd.ScsiStatus);
m_read_subcode = false;
m_use_sptd = true;
return true;
}
Log_ErrorPrintf("No working read mode found (status 0x%02X, err %08X)", sptd.cmd.ScsiStatus, GetLastError());
return false;
}
std::unique_ptr<CDImage> CDImage::OpenDeviceImage(const char* filename, Common::Error* error)
{
std::unique_ptr<CDImageDeviceWin32> image = std::make_unique<CDImageDeviceWin32>();
if (!image->Open(filename, error))
return {};
return image;
}
std::vector<std::pair<std::string, std::string>> CDImage::GetDeviceList()
{
std::vector<std::pair<std::string, std::string>> ret;
char buf[256];
if (GetLogicalDriveStringsA(sizeof(buf), buf) != 0)
{
const char* ptr = buf;
while (*ptr != '\0')
{
std::size_t len = std::strlen(ptr);
const DWORD type = GetDriveTypeA(ptr);
if (type != DRIVE_CDROM)
{
ptr += len + 1u;
continue;
}
// Drop the trailing slash.
const std::size_t append_len = (ptr[len - 1] == '\\') ? (len - 1) : len;
std::string path;
path.append("\\\\.\\");
path.append(ptr, append_len);
std::string name(ptr, append_len);
ret.emplace_back(std::move(path), std::move(name));
ptr += len + 1u;
}
}
return ret;
}
bool CDImage::IsDeviceName(const char* filename)
{
return StringUtil::StartsWith(filename, "\\\\.\\");
}
#else
std::unique_ptr<CDImage> CDImage::OpenDeviceImage(const char* filename, Common::Error* error)
{
return {};
}
std::vector<std::pair<std::string, std::string>> CDImage::GetDeviceList()
{
return {};
}
bool CDImage::IsDeviceName(const char* filename)
{
return false;
}
#endif

556
src/util/cd_image_ecm.cpp Normal file
View File

@ -0,0 +1,556 @@
#include "cd_image.h"
#include "cd_subchannel_replacement.h"
#include "common/assert.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include <array>
#include <cerrno>
#include <map>
Log_SetChannel(CDImageEcm);
// unecm.c by Neill Corlett (c) 2002, GPL licensed
/* LUTs used for computing ECC/EDC */
static constexpr std::array<u8, 256> ComputeECCFLUT()
{
std::array<u8, 256> ecc_lut{};
for (u32 i = 0; i < 256; i++)
{
u32 j = (i << 1) ^ (i & 0x80 ? 0x11D : 0);
ecc_lut[i] = static_cast<u8>(j);
}
return ecc_lut;
}
static constexpr std::array<u8, 256> ComputeECCBLUT()
{
std::array<u8, 256> ecc_lut{};
for (u32 i = 0; i < 256; i++)
{
u32 j = (i << 1) ^ (i & 0x80 ? 0x11D : 0);
ecc_lut[i ^ j] = static_cast<u8>(i);
}
return ecc_lut;
}
static constexpr std::array<u32, 256> ComputeEDCLUT()
{
std::array<u32, 256> edc_lut{};
for (u32 i = 0; i < 256; i++)
{
u32 edc = i;
for (u32 k = 0; k < 8; k++)
edc = (edc >> 1) ^ (edc & 1 ? 0xD8018001 : 0);
edc_lut[i] = edc;
}
return edc_lut;
}
static constexpr std::array<u8, 256> ecc_f_lut = ComputeECCFLUT();
static constexpr std::array<u8, 256> ecc_b_lut = ComputeECCBLUT();
static constexpr std::array<u32, 256> edc_lut = ComputeEDCLUT();
/***************************************************************************/
/*
** Compute EDC for a block
*/
static u32 edc_partial_computeblock(u32 edc, const u8* src, u16 size)
{
while (size--)
edc = (edc >> 8) ^ edc_lut[(edc ^ (*src++)) & 0xFF];
return edc;
}
static void edc_computeblock(const u8* src, u16 size, u8* dest)
{
u32 edc = edc_partial_computeblock(0, src, size);
dest[0] = (edc >> 0) & 0xFF;
dest[1] = (edc >> 8) & 0xFF;
dest[2] = (edc >> 16) & 0xFF;
dest[3] = (edc >> 24) & 0xFF;
}
/***************************************************************************/
/*
** Compute ECC for a block (can do either P or Q)
*/
static void ecc_computeblock(u8* src, u32 major_count, u32 minor_count, u32 major_mult, u32 minor_inc, u8* dest)
{
u32 size = major_count * minor_count;
u32 major, minor;
for (major = 0; major < major_count; major++)
{
u32 index = (major >> 1) * major_mult + (major & 1);
u8 ecc_a = 0;
u8 ecc_b = 0;
for (minor = 0; minor < minor_count; minor++)
{
u8 temp = src[index];
index += minor_inc;
if (index >= size)
index -= size;
ecc_a ^= temp;
ecc_b ^= temp;
ecc_a = ecc_f_lut[ecc_a];
}
ecc_a = ecc_b_lut[ecc_f_lut[ecc_a] ^ ecc_b];
dest[major] = ecc_a;
dest[major + major_count] = ecc_a ^ ecc_b;
}
}
/*
** Generate ECC P and Q codes for a block
*/
static void ecc_generate(u8* sector, int zeroaddress)
{
u8 address[4], i;
/* Save the address and zero it out */
if (zeroaddress)
for (i = 0; i < 4; i++)
{
address[i] = sector[12 + i];
sector[12 + i] = 0;
}
/* Compute ECC P code */
ecc_computeblock(sector + 0xC, 86, 24, 2, 86, sector + 0x81C);
/* Compute ECC Q code */
ecc_computeblock(sector + 0xC, 52, 43, 86, 88, sector + 0x8C8);
/* Restore the address */
if (zeroaddress)
for (i = 0; i < 4; i++)
sector[12 + i] = address[i];
}
/***************************************************************************/
/*
** Generate ECC/EDC information for a sector (must be 2352 = 0x930 bytes)
** Returns 0 on success
*/
static void eccedc_generate(u8* sector, int type)
{
switch (type)
{
case 1: /* Mode 1 */
/* Compute EDC */
edc_computeblock(sector + 0x00, 0x810, sector + 0x810);
/* Write out zero bytes */
for (u32 i = 0; i < 8; i++)
sector[0x814 + i] = 0;
/* Generate ECC P/Q codes */
ecc_generate(sector, 0);
break;
case 2: /* Mode 2 form 1 */
/* Compute EDC */
edc_computeblock(sector + 0x10, 0x808, sector + 0x818);
/* Generate ECC P/Q codes */
ecc_generate(sector, 1);
break;
case 3: /* Mode 2 form 2 */
/* Compute EDC */
edc_computeblock(sector + 0x10, 0x91C, sector + 0x92C);
break;
}
}
class CDImageEcm : public CDImage
{
public:
CDImageEcm();
~CDImageEcm() override;
bool Open(const char* filename, Common::Error* error);
bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) override;
bool HasNonStandardSubchannel() const override;
protected:
bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override;
private:
bool ReadChunks(u32 disc_offset, u32 size);
std::FILE* m_fp = nullptr;
enum class SectorType : u32
{
Raw = 0x00,
Mode1 = 0x01,
Mode2Form1 = 0x02,
Mode2Form2 = 0x03,
Count,
};
static constexpr std::array<u32, static_cast<u32>(SectorType::Count)> s_sector_sizes = {
0x930, // raw
0x803, // mode1
0x804, // mode2form1
0x918, // mode2form2
};
static constexpr std::array<u32, static_cast<u32>(SectorType::Count)> s_chunk_sizes = {
0, // raw
2352, // mode1
2336, // mode2form1
2336, // mode2form2
};
struct SectorEntry
{
u32 file_offset;
u32 chunk_size;
SectorType type;
};
using DataMap = std::map<u32, SectorEntry>;
DataMap m_data_map;
std::vector<u8> m_chunk_buffer;
u32 m_chunk_start = 0;
CDSubChannelReplacement m_sbi;
};
CDImageEcm::CDImageEcm() = default;
CDImageEcm::~CDImageEcm()
{
if (m_fp)
std::fclose(m_fp);
}
bool CDImageEcm::Open(const char* filename, Common::Error* error)
{
m_filename = filename;
m_fp = FileSystem::OpenCFile(filename, "rb");
if (!m_fp)
{
Log_ErrorPrintf("Failed to open binfile '%s': errno %d", filename, errno);
if (error)
error->SetErrno(errno);
return false;
}
s64 file_size;
if (FileSystem::FSeek64(m_fp, 0, SEEK_END) != 0 || (file_size = FileSystem::FTell64(m_fp)) <= 0 ||
FileSystem::FSeek64(m_fp, 0, SEEK_SET) != 0)
{
Log_ErrorPrintf("Get file size failed: errno %d", errno);
if (error)
error->SetErrno(errno);
return false;
}
char header[4];
if (std::fread(header, sizeof(header), 1, m_fp) != 1 || header[0] != 'E' || header[1] != 'C' || header[2] != 'M' ||
header[3] != 0)
{
Log_ErrorPrintf("Failed to read/invalid header");
if (error)
error->SetMessage("Failed to read/invalid header");
return false;
}
// build sector map
u32 file_offset = static_cast<u32>(std::ftell(m_fp));
u32 disc_offset = 0;
for (;;)
{
int bits = std::fgetc(m_fp);
if (bits == EOF)
{
Log_ErrorPrintf("Unexpected EOF after %zu chunks", m_data_map.size());
if (error)
error->SetFormattedMessage("Unexpected EOF after %zu chunks", m_data_map.size());
return false;
}
file_offset++;
const SectorType type = static_cast<SectorType>(static_cast<u32>(bits) & 0x03u);
u32 count = (static_cast<u32>(bits) >> 2) & 0x1F;
u32 shift = 5;
while (bits & 0x80)
{
bits = std::fgetc(m_fp);
if (bits == EOF)
{
Log_ErrorPrintf("Unexpected EOF after %zu chunks", m_data_map.size());
if (error)
error->SetFormattedMessage("Unexpected EOF after %zu chunks", m_data_map.size());
return false;
}
count |= (static_cast<u32>(bits) & 0x7F) << shift;
shift += 7;
file_offset++;
}
if (count == 0xFFFFFFFFu)
break;
// for this sector
count++;
if (count >= 0x80000000u)
{
Log_ErrorPrintf("Corrupted header after %zu chunks", m_data_map.size());
if (error)
error->SetFormattedMessage("Corrupted header after %zu chunks", m_data_map.size());
return false;
}
if (type == SectorType::Raw)
{
while (count > 0)
{
const u32 size = std::min<u32>(count, 2352);
m_data_map.emplace(disc_offset, SectorEntry{file_offset, size, type});
disc_offset += size;
file_offset += size;
count -= size;
if (static_cast<s64>(file_offset) > file_size)
{
Log_ErrorPrintf("Out of file bounds after %zu chunks", m_data_map.size());
if (error)
error->SetFormattedMessage("Out of file bounds after %zu chunks", m_data_map.size());
}
}
}
else
{
const u32 size = s_sector_sizes[static_cast<u32>(type)];
const u32 chunk_size = s_chunk_sizes[static_cast<u32>(type)];
for (u32 i = 0; i < count; i++)
{
m_data_map.emplace(disc_offset, SectorEntry{file_offset, chunk_size, type});
disc_offset += chunk_size;
file_offset += size;
if (static_cast<s64>(file_offset) > file_size)
{
Log_ErrorPrintf("Out of file bounds after %zu chunks", m_data_map.size());
if (error)
error->SetFormattedMessage("Out of file bounds after %zu chunks", m_data_map.size());
}
}
}
if (std::fseek(m_fp, file_offset, SEEK_SET) != 0)
{
Log_ErrorPrintf("Failed to seek to offset %u after %zu chunks", file_offset, m_data_map.size());
if (error)
error->SetFormattedMessage("Failed to seek to offset %u after %zu chunks", file_offset, m_data_map.size());
return false;
}
}
if (m_data_map.empty())
{
Log_ErrorPrintf("No data in image '%s'", filename);
if (error)
error->SetFormattedMessage("No data in image '%s'", filename);
return false;
}
m_lba_count = disc_offset / RAW_SECTOR_SIZE;
if ((disc_offset % RAW_SECTOR_SIZE) != 0)
Log_WarningPrintf("ECM image is misaligned with offset %u", disc_offset);
if (m_lba_count == 0)
return false;
SubChannelQ::Control control = {};
TrackMode mode = TrackMode::Mode2Raw;
control.data = mode != TrackMode::Audio;
// Two seconds default pregap.
const u32 pregap_frames = 2 * FRAMES_PER_SECOND;
Index pregap_index = {};
pregap_index.file_sector_size = RAW_SECTOR_SIZE;
pregap_index.start_lba_on_disc = 0;
pregap_index.start_lba_in_track = static_cast<LBA>(-static_cast<s32>(pregap_frames));
pregap_index.length = pregap_frames;
pregap_index.track_number = 1;
pregap_index.index_number = 0;
pregap_index.mode = mode;
pregap_index.control.bits = control.bits;
pregap_index.is_pregap = true;
m_indices.push_back(pregap_index);
// Data index.
Index data_index = {};
data_index.file_index = 0;
data_index.file_offset = 0;
data_index.file_sector_size = RAW_SECTOR_SIZE;
data_index.start_lba_on_disc = pregap_index.length;
data_index.track_number = 1;
data_index.index_number = 1;
data_index.start_lba_in_track = 0;
data_index.length = m_lba_count;
data_index.mode = mode;
data_index.control.bits = control.bits;
m_indices.push_back(data_index);
// Assume a single track.
m_tracks.push_back(
Track{static_cast<u32>(1), data_index.start_lba_on_disc, static_cast<u32>(0), m_lba_count, mode, control});
AddLeadOutIndex();
m_sbi.LoadSBIFromImagePath(filename);
m_chunk_buffer.reserve(RAW_SECTOR_SIZE * 2);
return Seek(1, Position{0, 0, 0});
}
bool CDImageEcm::ReadChunks(u32 disc_offset, u32 size)
{
DataMap::iterator next =
m_data_map.lower_bound((disc_offset > RAW_SECTOR_SIZE) ? (disc_offset - RAW_SECTOR_SIZE) : 0);
DataMap::iterator current = m_data_map.begin();
while (next != m_data_map.end() && next->first <= disc_offset)
current = next++;
// extra bytes if we need to buffer some at the start
m_chunk_start = current->first;
m_chunk_buffer.clear();
if (m_chunk_start < disc_offset)
size += (disc_offset - current->first);
u32 total_bytes_read = 0;
while (total_bytes_read < size)
{
if (current == m_data_map.end() || std::fseek(m_fp, current->second.file_offset, SEEK_SET) != 0)
return false;
const u32 chunk_size = current->second.chunk_size;
const u32 chunk_start = static_cast<u32>(m_chunk_buffer.size());
m_chunk_buffer.resize(chunk_start + chunk_size);
if (current->second.type == SectorType::Raw)
{
if (std::fread(&m_chunk_buffer[chunk_start], chunk_size, 1, m_fp) != 1)
return false;
total_bytes_read += chunk_size;
}
else
{
// u8* sector = &m_chunk_buffer[chunk_start];
u8 sector[RAW_SECTOR_SIZE];
// TODO: needed?
std::memset(sector, 0, RAW_SECTOR_SIZE);
std::memset(sector + 1, 0xFF, 10);
u32 skip;
switch (current->second.type)
{
case SectorType::Mode1:
{
sector[0x0F] = 0x01;
if (std::fread(sector + 0x00C, 0x003, 1, m_fp) != 1 || std::fread(sector + 0x010, 0x800, 1, m_fp) != 1)
return false;
eccedc_generate(sector, 1);
skip = 0;
}
break;
case SectorType::Mode2Form1:
{
sector[0x0F] = 0x02;
if (std::fread(sector + 0x014, 0x804, 1, m_fp) != 1)
return false;
sector[0x10] = sector[0x14];
sector[0x11] = sector[0x15];
sector[0x12] = sector[0x16];
sector[0x13] = sector[0x17];
eccedc_generate(sector, 2);
skip = 0x10;
}
break;
case SectorType::Mode2Form2:
{
sector[0x0F] = 0x02;
if (std::fread(sector + 0x014, 0x918, 1, m_fp) != 1)
return false;
sector[0x10] = sector[0x14];
sector[0x11] = sector[0x15];
sector[0x12] = sector[0x16];
sector[0x13] = sector[0x17];
eccedc_generate(sector, 3);
skip = 0x10;
}
break;
default:
UnreachableCode();
return false;
}
std::memcpy(&m_chunk_buffer[chunk_start], sector + skip, chunk_size);
total_bytes_read += chunk_size;
}
++current;
}
return true;
}
bool CDImageEcm::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index)
{
if (m_sbi.GetReplacementSubChannelQ(index.start_lba_on_disc + lba_in_index, subq))
return true;
return CDImage::ReadSubChannelQ(subq, index, lba_in_index);
}
bool CDImageEcm::HasNonStandardSubchannel() const
{
return (m_sbi.GetReplacementSectorCount() > 0);
}
bool CDImageEcm::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index)
{
const u32 file_start = static_cast<u32>(index.file_offset) + (lba_in_index * index.file_sector_size);
const u32 file_end = file_start + RAW_SECTOR_SIZE;
if (file_start < m_chunk_start || file_end > (m_chunk_start + m_chunk_buffer.size()))
{
if (!ReadChunks(file_start, RAW_SECTOR_SIZE))
return false;
}
DebugAssert(file_start >= m_chunk_start && file_end <= (m_chunk_start + m_chunk_buffer.size()));
const size_t chunk_offset = static_cast<size_t>(file_start - m_chunk_start);
std::memcpy(buffer, &m_chunk_buffer[chunk_offset], RAW_SECTOR_SIZE);
return true;
}
std::unique_ptr<CDImage> CDImage::OpenEcmImage(const char* filename, Common::Error* error)
{
std::unique_ptr<CDImageEcm> image = std::make_unique<CDImageEcm>();
if (!image->Open(filename, error))
return {};
return image;
}

View File

@ -0,0 +1,131 @@
#include "cd_image_hasher.h"
#include "cd_image.h"
#include "common/md5_digest.h"
#include "common/string_util.h"
namespace CDImageHasher {
static bool ReadIndex(CDImage* image, u8 track, u8 index, MD5Digest* digest, ProgressCallback* progress_callback)
{
const CDImage::LBA index_start = image->GetTrackIndexPosition(track, index);
const u32 index_length = image->GetTrackIndexLength(track, index);
const u32 update_interval = std::max<u32>(index_length / 100u, 1u);
progress_callback->SetFormattedStatusText("Computing hash for track %u/index %u...", track, index);
progress_callback->SetProgressRange(index_length);
if (!image->Seek(index_start))
{
progress_callback->DisplayFormattedModalError("Failed to seek to sector %u for track %u index %u", index_start,
track, index);
return false;
}
std::array<u8, CDImage::RAW_SECTOR_SIZE> sector;
for (u32 lba = 0; lba < index_length; lba++)
{
if ((lba % update_interval) == 0)
progress_callback->SetProgressValue(lba);
if (!image->ReadRawSector(sector.data(), nullptr))
{
progress_callback->DisplayFormattedModalError("Failed to read sector %u from image", image->GetPositionOnDisc());
return false;
}
digest->Update(sector.data(), static_cast<u32>(sector.size()));
}
progress_callback->SetProgressValue(index_length);
return true;
}
static bool ReadTrack(CDImage* image, u8 track, MD5Digest* digest, ProgressCallback* progress_callback)
{
static constexpr u8 INDICES_TO_READ = 2;
progress_callback->PushState();
const bool dataTrack = track == 1;
progress_callback->SetProgressRange(dataTrack ? 1 : 2);
u8 progress = 0;
for (u8 index = 0; index < INDICES_TO_READ; index++)
{
progress_callback->SetProgressValue(progress);
// skip index 0 if data track
if (dataTrack && index == 0)
continue;
progress++;
progress_callback->PushState();
if (!ReadIndex(image, track, index, digest, progress_callback))
{
progress_callback->PopState();
progress_callback->PopState();
return false;
}
progress_callback->PopState();
}
progress_callback->SetProgressValue(progress);
progress_callback->PopState();
return true;
}
std::string HashToString(const Hash& hash)
{
return StringUtil::StdStringFromFormat("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", hash[0],
hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7], hash[8],
hash[9], hash[10], hash[11], hash[12], hash[13], hash[14], hash[15]);
}
std::optional<Hash> HashFromString(const std::string_view& str) {
auto decoded = StringUtil::DecodeHex(str);
if (decoded && decoded->size() == std::tuple_size_v<Hash>)
{
Hash result;
std::copy(decoded->begin(), decoded->end(), result.begin());
return result;
}
return std::nullopt;
}
bool GetImageHash(CDImage* image, Hash* out_hash,
ProgressCallback* progress_callback /*= ProgressCallback::NullProgressCallback*/)
{
MD5Digest digest;
progress_callback->SetProgressRange(image->GetTrackCount());
progress_callback->SetProgressValue(0);
progress_callback->PushState();
for (u32 i = 1; i <= image->GetTrackCount(); i++)
{
progress_callback->SetProgressValue(i - 1);
if (!ReadTrack(image, static_cast<u8>(i), &digest, progress_callback))
{
progress_callback->PopState();
return false;
}
}
progress_callback->SetProgressValue(image->GetTrackCount());
digest.Final(out_hash->data());
return true;
}
bool GetTrackHash(CDImage* image, u8 track, Hash* out_hash,
ProgressCallback* progress_callback /*= ProgressCallback::NullProgressCallback*/)
{
MD5Digest digest;
if (!ReadTrack(image, track, &digest, progress_callback))
return false;
digest.Final(out_hash->data());
return true;
}
} // namespace CDImageHasher

View File

@ -0,0 +1,21 @@
#pragma once
#include "common/progress_callback.h"
#include "common/types.h"
#include <array>
#include <optional>
#include <string>
class CDImage;
namespace CDImageHasher {
using Hash = std::array<u8, 16>;
std::string HashToString(const Hash& hash);
std::optional<Hash> HashFromString(const std::string_view& str);
bool GetImageHash(CDImage* image, Hash* out_hash,
ProgressCallback* progress_callback = ProgressCallback::NullProgressCallback);
bool GetTrackHash(CDImage* image, u8 track, Hash* out_hash,
ProgressCallback* progress_callback = ProgressCallback::NullProgressCallback);
} // namespace CDImageHasher

180
src/util/cd_image_m3u.cpp Normal file
View File

@ -0,0 +1,180 @@
#include "cd_image.h"
#include "cd_subchannel_replacement.h"
#include "common/assert.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include <algorithm>
#include <cerrno>
#include <map>
#include <sstream>
Log_SetChannel(CDImageMemory);
class CDImageM3u : public CDImage
{
public:
CDImageM3u();
~CDImageM3u() override;
bool Open(const char* path, Common::Error* Error);
bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) override;
bool HasNonStandardSubchannel() const override;
bool HasSubImages() const override;
u32 GetSubImageCount() const override;
u32 GetCurrentSubImage() const override;
std::string GetSubImageMetadata(u32 index, const std::string_view& type) const override;
bool SwitchSubImage(u32 index, Common::Error* error) override;
protected:
bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override;
private:
struct Entry
{
// TODO: Worth storing any other data?
std::string filename;
std::string title;
};
std::vector<Entry> m_entries;
std::unique_ptr<CDImage> m_current_image;
u32 m_current_image_index = UINT32_C(0xFFFFFFFF);
};
CDImageM3u::CDImageM3u() = default;
CDImageM3u::~CDImageM3u() = default;
bool CDImageM3u::Open(const char* path, Common::Error* error)
{
std::FILE* fp = FileSystem::OpenCFile(path, "rb");
if (!fp)
return false;
std::optional<std::string> m3u_file(FileSystem::ReadFileToString(fp));
std::fclose(fp);
if (!m3u_file.has_value() || m3u_file->empty())
{
if (error)
error->SetMessage("Failed to read M3u file");
return false;
}
std::istringstream ifs(m3u_file.value());
m_filename = path;
std::vector<std::string> entries;
std::string line;
while (std::getline(ifs, line))
{
u32 start_offset = 0;
while (start_offset < line.size() && std::isspace(line[start_offset]))
start_offset++;
// skip comments
if (start_offset == line.size() || line[start_offset] == '#')
continue;
// strip ending whitespace
u32 end_offset = static_cast<u32>(line.size()) - 1;
while (std::isspace(line[end_offset]) && end_offset > start_offset)
end_offset--;
// anything?
if (start_offset == end_offset)
continue;
Entry entry;
std::string entry_filename(line.begin() + start_offset, line.begin() + end_offset + 1);
entry.title = Path::GetFileTitle(entry_filename);
if (!Path::IsAbsolute(entry_filename))
entry.filename = Path::BuildRelativePath(path, entry_filename);
else
entry.filename = std::move(entry_filename);
Log_DevPrintf("Read path from m3u: '%s'", entry.filename.c_str());
m_entries.push_back(std::move(entry));
}
Log_InfoPrintf("Loaded %zu paths from m3u '%s'", m_entries.size(), path);
return !m_entries.empty() && SwitchSubImage(0, error);
}
bool CDImageM3u::HasNonStandardSubchannel() const
{
return m_current_image->HasNonStandardSubchannel();
}
bool CDImageM3u::HasSubImages() const
{
return true;
}
u32 CDImageM3u::GetSubImageCount() const
{
return static_cast<u32>(m_entries.size());
}
u32 CDImageM3u::GetCurrentSubImage() const
{
return m_current_image_index;
}
bool CDImageM3u::SwitchSubImage(u32 index, Common::Error* error)
{
if (index >= m_entries.size())
return false;
else if (index == m_current_image_index)
return true;
const Entry& entry = m_entries[index];
std::unique_ptr<CDImage> new_image = CDImage::Open(entry.filename.c_str(), error);
if (!new_image)
{
Log_ErrorPrintf("Failed to load subimage %u (%s)", index, entry.filename.c_str());
return false;
}
CopyTOC(new_image.get());
m_current_image = std::move(new_image);
m_current_image_index = index;
if (!Seek(1, Position{0, 0, 0}))
Panic("Failed to seek to start after sub-image change.");
return true;
}
std::string CDImageM3u::GetSubImageMetadata(u32 index, const std::string_view& type) const
{
if (index > m_entries.size())
return {};
if (type == "title")
return m_entries[index].title;
else if (type == "file_title")
return std::string(Path::GetFileTitle(m_entries[index].filename));
return CDImage::GetSubImageMetadata(index, type);
}
bool CDImageM3u::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index)
{
return m_current_image->ReadSectorFromIndex(buffer, index, lba_in_index);
}
bool CDImageM3u::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index)
{
return m_current_image->ReadSubChannelQ(subq, index, lba_in_index);
}
std::unique_ptr<CDImage> CDImage::OpenM3uImage(const char* filename, Common::Error* error)
{
std::unique_ptr<CDImageM3u> image = std::make_unique<CDImageM3u>();
if (!image->Open(filename, error))
return {};
return image;
}

304
src/util/cd_image_mds.cpp Normal file
View File

@ -0,0 +1,304 @@
#include "assert.h"
#include "cd_image.h"
#include "cd_subchannel_replacement.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include <algorithm>
#include <cerrno>
#include <map>
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, Common::Error* error);
bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) 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, Common::Error* error)
{
std::FILE* mds_fp = FileSystem::OpenCFile(filename, "rb");
if (!mds_fp)
{
Log_ErrorPrintf("Failed to open mds '%s': errno %d", filename, errno);
if (error)
error->SetErrno(errno);
return false;
}
std::optional<std::vector<u8>> 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);
if (error)
error->SetFormattedMessage("Failed to read mds file '%s'", filename);
return false;
}
std::string mdf_filename(Path::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);
if (error)
error->SetFormattedMessage("Failed to open mdf file '%s': errno %d", mdf_filename.c_str(), errno);
return false;
}
const std::vector<u8>& 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);
if (error)
error->SetFormattedMessage("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);
if (error)
error->SetFormattedMessage("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);
if (error)
error->SetFormattedMessage("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);
if (error)
error->SetFormattedMessage("End of file in '%s' at track %u", filename, track_number);
return false;
}
TrackEntry track;
std::memcpy(&track, &mds[track_offset], sizeof(track));
track_offset += sizeof(TrackEntry);
if (PackedBCDToBinary(track.track_number) != track_number)
{
Log_ErrorPrintf("Unexpected track number 0x%02X in track %u", track.track_number, track_number);
if (error)
error->SetFormattedMessage("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);
if (error)
error->SetFormattedMessage("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);
if (error)
error->SetFormattedMessage("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<LBA>(-static_cast<s32>(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<u32>(track_number), track_start_lba, static_cast<u32>(m_indices.size()),
static_cast<u32>(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);
if (error)
error->SetFormattedMessage("File '%s' contains no tracks", filename);
return false;
}
m_lba_count = m_tracks.back().start_lba + m_tracks.back().length;
AddLeadOutIndex();
m_sbi.LoadSBIFromImagePath(filename);
return Seek(1, Position{0, 0, 0});
}
bool CDImageMds::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index)
{
if (m_sbi.GetReplacementSubChannelQ(index.start_lba_on_disc + lba_in_index, subq))
return true;
return CDImage::ReadSubChannelQ(subq, index, lba_in_index);
}
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<u64>(lba_in_index) * index.file_sector_size);
if (m_mdf_file_position != file_position)
{
if (std::fseek(m_mdf_file, static_cast<long>(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<long>(m_mdf_file_position), SEEK_SET);
return false;
}
m_mdf_file_position += read_size;
return true;
}
std::unique_ptr<CDImage> CDImage::OpenMdsImage(const char* filename, Common::Error* error)
{
std::unique_ptr<CDImageMds> image = std::make_unique<CDImageMds>();
if (!image->OpenAndParse(filename, error))
return {};
return image;
}

View File

@ -0,0 +1,152 @@
#include "cd_image.h"
#include "cd_subchannel_replacement.h"
#include "common/assert.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include <algorithm>
#include <cerrno>
Log_SetChannel(CDImageMemory);
class CDImageMemory : public CDImage
{
public:
CDImageMemory();
~CDImageMemory() override;
bool CopyImage(CDImage* image, ProgressCallback* progress);
bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) override;
bool HasNonStandardSubchannel() const override;
protected:
bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override;
private:
u8* m_memory = nullptr;
u32 m_memory_sectors = 0;
CDSubChannelReplacement m_sbi;
};
CDImageMemory::CDImageMemory() = default;
CDImageMemory::~CDImageMemory()
{
if (m_memory)
std::free(m_memory);
}
bool CDImageMemory::CopyImage(CDImage* image, ProgressCallback* progress)
{
// figure out the total number of sectors (not including blank pregaps)
m_memory_sectors = 0;
for (u32 i = 0; i < image->GetIndexCount(); i++)
{
const Index& index = image->GetIndex(i);
if (index.file_sector_size > 0)
m_memory_sectors += image->GetIndex(i).length;
}
if ((static_cast<u64>(RAW_SECTOR_SIZE) * static_cast<u64>(m_memory_sectors)) >=
static_cast<u64>(std::numeric_limits<size_t>::max()))
{
progress->DisplayFormattedModalError("Insufficient address space");
return false;
}
progress->SetFormattedStatusText("Allocating memory for %u sectors...", m_memory_sectors);
m_memory =
static_cast<u8*>(std::malloc(static_cast<size_t>(RAW_SECTOR_SIZE) * static_cast<size_t>(m_memory_sectors)));
if (!m_memory)
{
progress->DisplayFormattedModalError("Failed to allocate memory for %u sectors", m_memory_sectors);
return false;
}
progress->SetStatusText("Preloading CD image to RAM...");
progress->SetProgressRange(m_memory_sectors);
progress->SetProgressValue(0);
u8* memory_ptr = m_memory;
u32 sectors_read = 0;
for (u32 i = 0; i < image->GetIndexCount(); i++)
{
const Index& index = image->GetIndex(i);
if (index.file_sector_size == 0)
continue;
for (u32 lba = 0; lba < index.length; lba++)
{
if (!image->ReadSectorFromIndex(memory_ptr, index, lba))
{
Log_ErrorPrintf("Failed to read LBA %u in index %u", lba, i);
return false;
}
progress->SetProgressValue(sectors_read);
memory_ptr += RAW_SECTOR_SIZE;
sectors_read++;
}
}
for (u32 i = 1; i <= image->GetTrackCount(); i++)
m_tracks.push_back(image->GetTrack(i));
u32 current_offset = 0;
for (u32 i = 0; i < image->GetIndexCount(); i++)
{
Index new_index = image->GetIndex(i);
new_index.file_index = 0;
if (new_index.file_sector_size > 0)
{
new_index.file_offset = current_offset;
current_offset += new_index.length;
}
m_indices.push_back(new_index);
}
Assert(current_offset == m_memory_sectors);
m_filename = image->GetFileName();
m_lba_count = image->GetLBACount();
m_sbi.LoadSBI(Path::ReplaceExtension(m_filename, "sbi").c_str());
return Seek(1, Position{0, 0, 0});
}
bool CDImageMemory::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index)
{
if (m_sbi.GetReplacementSubChannelQ(index.start_lba_on_disc + lba_in_index, subq))
return true;
return CDImage::ReadSubChannelQ(subq, index, lba_in_index);
}
bool CDImageMemory::HasNonStandardSubchannel() const
{
return (m_sbi.GetReplacementSectorCount() > 0);
}
bool CDImageMemory::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index)
{
DebugAssert(index.file_index == 0);
const u64 sector_number = index.file_offset + lba_in_index;
if (sector_number >= m_memory_sectors)
return false;
const size_t file_offset = static_cast<size_t>(sector_number) * static_cast<size_t>(RAW_SECTOR_SIZE);
std::memcpy(buffer, &m_memory[file_offset], RAW_SECTOR_SIZE);
return true;
}
std::unique_ptr<CDImage>
CDImage::CreateMemoryImage(CDImage* image, ProgressCallback* progress /* = ProgressCallback::NullProgressCallback */)
{
std::unique_ptr<CDImageMemory> memory_image = std::make_unique<CDImageMemory>();
if (!memory_image->CopyImage(image, progress))
return {};
return memory_image;
}

902
src/util/cd_image_pbp.cpp Normal file
View File

@ -0,0 +1,902 @@
#include "cd_image.h"
#include "cd_subchannel_replacement.h"
#include "common/assert.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include "common/string_util.h"
#include "pbp_types.h"
#include "string.h"
#include "zlib.h"
#include <array>
#include <cstdio>
#include <vector>
Log_SetChannel(CDImagePBP);
using namespace PBP;
using FileSystem::FSeek64;
using FileSystem::FTell64;
class CDImagePBP final : public CDImage
{
public:
CDImagePBP() = default;
~CDImagePBP() override;
bool Open(const char* filename, Common::Error* error);
bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) override;
bool HasNonStandardSubchannel() const override;
bool HasSubImages() const override;
u32 GetSubImageCount() const override;
u32 GetCurrentSubImage() const override;
bool SwitchSubImage(u32 index, Common::Error* error) override;
std::string GetMetadata(const std::string_view& type) const override;
std::string GetSubImageMetadata(u32 index, const std::string_view& type) const override;
protected:
bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override;
private:
struct BlockInfo
{
u32 offset; // Absolute offset from start of file
u16 size;
};
#if _DEBUG
static void PrintPBPHeaderInfo(const PBPHeader& pbp_header);
static void PrintSFOHeaderInfo(const SFOHeader& sfo_header);
static void PrintSFOIndexTableEntry(const SFOIndexTableEntry& sfo_index_table_entry, size_t i);
static void PrintSFOTable(const SFOTable& sfo_table);
#endif
bool LoadPBPHeader();
bool LoadSFOHeader();
bool LoadSFOIndexTable();
bool LoadSFOTable();
bool IsValidEboot(Common::Error* error);
bool InitDecompressionStream();
bool DecompressBlock(const BlockInfo& block_info);
bool OpenDisc(u32 index, Common::Error* error);
static const std::string* LookupStringSFOTableEntry(const char* key, const SFOTable& table);
FILE* m_file = nullptr;
PBPHeader m_pbp_header;
SFOHeader m_sfo_header;
SFOTable m_sfo_table;
SFOIndexTable m_sfo_index_table;
// Absolute offsets to ISO headers, size is the number of discs in the file
std::vector<u32> m_disc_offsets;
u32 m_current_disc = 0;
// Absolute offsets and sizes of blocks in m_file
std::array<BlockInfo, BLOCK_TABLE_NUM_ENTRIES> m_blockinfo_table;
std::array<TOCEntry, TOC_NUM_ENTRIES> m_toc;
u32 m_current_block = static_cast<u32>(-1);
std::array<u8, DECOMPRESSED_BLOCK_SIZE> m_decompressed_block;
std::vector<u8> m_compressed_block;
z_stream m_inflate_stream;
CDSubChannelReplacement m_sbi;
};
namespace EndianHelper {
static constexpr bool HostIsLittleEndian()
{
constexpr union
{
u8 a[4];
u32 b;
} test_val = {{1}};
return test_val.a[0] == 1;
}
template<typename T>
static void SwapByteOrder(T& val)
{
union
{
T t;
std::array<u8, sizeof(T)> arr;
} swap_val;
swap_val.t = val;
std::reverse(std::begin(swap_val.arr), std::end(swap_val.arr));
val = swap_val.t;
}
} // namespace EndianHelper
CDImagePBP::~CDImagePBP()
{
if (m_file)
fclose(m_file);
inflateEnd(&m_inflate_stream);
}
bool CDImagePBP::LoadPBPHeader()
{
if (!m_file)
return false;
if (FSeek64(m_file, 0, SEEK_END) != 0)
return false;
if (FTell64(m_file) < 0)
return false;
if (FSeek64(m_file, 0, SEEK_SET) != 0)
return false;
if (fread(&m_pbp_header, sizeof(PBPHeader), 1, m_file) != 1)
{
Log_ErrorPrint("Unable to read PBP header");
return false;
}
if (strncmp((char*)m_pbp_header.magic, "\0PBP", 4) != 0)
{
Log_ErrorPrint("PBP magic number mismatch");
return false;
}
#if _DEBUG
PrintPBPHeaderInfo(m_pbp_header);
#endif
return true;
}
bool CDImagePBP::LoadSFOHeader()
{
if (FSeek64(m_file, m_pbp_header.param_sfo_offset, SEEK_SET) != 0)
return false;
if (fread(&m_sfo_header, sizeof(SFOHeader), 1, m_file) != 1)
return false;
if (strncmp((char*)m_sfo_header.magic, "\0PSF", 4) != 0)
{
Log_ErrorPrint("SFO magic number mismatch");
return false;
}
#if _DEBUG
PrintSFOHeaderInfo(m_sfo_header);
#endif
return true;
}
bool CDImagePBP::LoadSFOIndexTable()
{
m_sfo_index_table.clear();
m_sfo_index_table.resize(m_sfo_header.num_table_entries);
if (FSeek64(m_file, m_pbp_header.param_sfo_offset + sizeof(m_sfo_header), SEEK_SET) != 0)
return false;
if (fread(m_sfo_index_table.data(), sizeof(SFOIndexTableEntry), m_sfo_header.num_table_entries, m_file) !=
m_sfo_header.num_table_entries)
return false;
#if _DEBUG
for (size_t i = 0; i < static_cast<size_t>(m_sfo_header.num_table_entries); ++i)
PrintSFOIndexTableEntry(m_sfo_index_table[i], i);
#endif
return true;
}
bool CDImagePBP::LoadSFOTable()
{
m_sfo_table.clear();
for (size_t i = 0; i < static_cast<size_t>(m_sfo_header.num_table_entries); ++i)
{
u32 abs_key_offset =
m_pbp_header.param_sfo_offset + m_sfo_header.key_table_offset + m_sfo_index_table[i].key_offset;
u32 abs_data_offset =
m_pbp_header.param_sfo_offset + m_sfo_header.data_table_offset + m_sfo_index_table[i].data_offset;
if (FSeek64(m_file, abs_key_offset, SEEK_SET) != 0)
{
Log_ErrorPrintf("Failed seek to key for SFO table entry %zu", i);
return false;
}
// Longest known key string is 20 characters total, including the null character
char key_cstr[20] = {};
if (fgets(key_cstr, sizeof(key_cstr), m_file) == nullptr)
{
Log_ErrorPrintf("Failed to read key string for SFO table entry %zu", i);
return false;
}
if (FSeek64(m_file, abs_data_offset, SEEK_SET) != 0)
{
Log_ErrorPrintf("Failed seek to data for SFO table entry %zu", i);
return false;
}
if (m_sfo_index_table[i].data_type == 0x0004) // "special mode" UTF-8 (not null terminated)
{
Log_ErrorPrintf("Unhandled special mode UTF-8 type found in SFO table for entry %zu", i);
return false;
}
else if (m_sfo_index_table[i].data_type == 0x0204) // null-terminated UTF-8 character string
{
std::vector<char> data_cstr(m_sfo_index_table[i].data_size);
if (fgets(data_cstr.data(), static_cast<int>(data_cstr.size() * sizeof(char)), m_file) == nullptr)
{
Log_ErrorPrintf("Failed to read data string for SFO table entry %zu", i);
return false;
}
m_sfo_table.emplace(std::string(key_cstr), std::string(data_cstr.data()));
}
else if (m_sfo_index_table[i].data_type == 0x0404) // uint32_t
{
u32 val;
if (fread(&val, sizeof(u32), 1, m_file) != 1)
{
Log_ErrorPrintf("Failed to read unsigned data value for SFO table entry %zu", i);
return false;
}
m_sfo_table.emplace(std::string(key_cstr), val);
}
else
{
Log_ErrorPrintf("Unhandled SFO data type 0x%04X found in SFO table for entry %zu", m_sfo_index_table[i].data_type,
i);
return false;
}
}
#if _DEBUG
PrintSFOTable(m_sfo_table);
#endif
return true;
}
bool CDImagePBP::IsValidEboot(Common::Error* error)
{
// Check some fields to make sure this is a valid PS1 EBOOT.PBP
auto a_it = m_sfo_table.find("BOOTABLE");
if (a_it != m_sfo_table.end())
{
SFOTableDataValue data_value = a_it->second;
if (!std::holds_alternative<u32>(data_value) || std::get<u32>(data_value) != 1)
{
Log_ErrorPrint("Invalid BOOTABLE value");
if (error)
error->SetMessage("Invalid BOOTABLE value");
return false;
}
}
else
{
Log_ErrorPrint("No BOOTABLE value found");
if (error)
error->SetMessage("No BOOTABLE value found");
return false;
}
a_it = m_sfo_table.find("CATEGORY");
if (a_it != m_sfo_table.end())
{
SFOTableDataValue data_value = a_it->second;
if (!std::holds_alternative<std::string>(data_value) || std::get<std::string>(data_value) != "ME")
{
Log_ErrorPrint("Invalid CATEGORY value");
if (error)
error->SetMessage("Invalid CATEGORY value");
return false;
}
}
else
{
Log_ErrorPrint("No CATEGORY value found");
if (error)
error->SetMessage("No CATEGORY value found");
return false;
}
return true;
}
bool CDImagePBP::Open(const char* filename, Common::Error* error)
{
if (!EndianHelper::HostIsLittleEndian())
{
Log_ErrorPrint("Big endian hosts not currently supported");
return false;
}
m_file = FileSystem::OpenCFile(filename, "rb");
if (!m_file)
{
if (error)
error->SetErrno(errno);
return false;
}
m_filename = filename;
// Read in PBP header
if (!LoadPBPHeader())
{
Log_ErrorPrint("Failed to load PBP header");
if (error)
error->SetMessage("Failed to load PBP header");
return false;
}
// Read in SFO header
if (!LoadSFOHeader())
{
Log_ErrorPrint("Failed to load SFO header");
if (error)
error->SetMessage("Failed to load SFO header");
return false;
}
// Read in SFO index table
if (!LoadSFOIndexTable())
{
Log_ErrorPrint("Failed to load SFO index table");
if (error)
error->SetMessage("Failed to load SFO index table");
return false;
}
// Read in SFO table
if (!LoadSFOTable())
{
Log_ErrorPrint("Failed to load SFO table");
if (error)
error->SetMessage("Failed to load SFO table");
return false;
}
// Since PBP files can store things that aren't PS1 CD images, make sure we're loading the right kind
if (!IsValidEboot(error))
{
Log_ErrorPrint("Couldn't validate EBOOT");
return false;
}
// Start parsing ISO stuff
if (FSeek64(m_file, m_pbp_header.data_psar_offset, SEEK_SET) != 0)
return false;
// Check "PSTITLEIMG000000" for multi-disc
char data_psar_magic[16] = {};
if (fread(data_psar_magic, sizeof(data_psar_magic), 1, m_file) != 1)
return false;
if (strncmp(data_psar_magic, "PSTITLEIMG000000", 16) == 0) // Multi-disc header found
{
// For multi-disc, the five disc offsets are located at data_psar_offset + 0x200. Non-present discs have an offset
// of 0. There are also some disc hashes, a serial (from one of the discs, but used as an identifier for the entire
// "title image" header), and some other offsets, but we don't really need to check those
if (FSeek64(m_file, m_pbp_header.data_psar_offset + 0x200, SEEK_SET) != 0)
return false;
u32 disc_table[DISC_TABLE_NUM_ENTRIES] = {};
if (fread(disc_table, sizeof(u32), DISC_TABLE_NUM_ENTRIES, m_file) != DISC_TABLE_NUM_ENTRIES)
return false;
// Ignore encrypted files
if (disc_table[0] == 0x44475000) // "\0PGD"
{
Log_ErrorPrintf("Encrypted PBP images are not supported, skipping %s", m_filename.c_str());
if (error)
error->SetMessage("Encrypted PBP images are not supported");
return false;
}
// Convert relative offsets to absolute offsets for available discs
for (u32 i = 0; i < DISC_TABLE_NUM_ENTRIES; i++)
{
if (disc_table[i] != 0)
m_disc_offsets.push_back(m_pbp_header.data_psar_offset + disc_table[i]);
else
break;
}
if (m_disc_offsets.size() < 1)
{
Log_ErrorPrintf("Invalid number of discs (%u) in multi-disc PBP file", static_cast<u32>(m_disc_offsets.size()));
return false;
}
}
else // Single-disc
{
m_disc_offsets.push_back(m_pbp_header.data_psar_offset);
}
// Default to first disc for now
return OpenDisc(0, error);
}
bool CDImagePBP::OpenDisc(u32 index, Common::Error* error)
{
if (index >= m_disc_offsets.size())
{
Log_ErrorPrintf("File does not contain disc %u", index + 1);
if (error)
error->SetMessage(TinyString::FromFormat("File does not contain disc %u", index + 1));
return false;
}
m_current_block = static_cast<u32>(-1);
m_blockinfo_table.fill({});
m_toc.fill({});
m_decompressed_block.fill(0x00);
m_compressed_block.clear();
// Go to ISO header
const u32 iso_header_start = m_disc_offsets[index];
if (FSeek64(m_file, iso_header_start, SEEK_SET) != 0)
return false;
char iso_header_magic[12] = {};
if (fread(iso_header_magic, sizeof(iso_header_magic), 1, m_file) != 1)
return false;
if (strncmp(iso_header_magic, "PSISOIMG0000", 12) != 0)
{
Log_ErrorPrint("ISO header magic number mismatch");
return false;
}
// Ignore encrypted files
u32 pgd_magic;
if (FSeek64(m_file, iso_header_start + 0x400, SEEK_SET) != 0)
return false;
if (fread(&pgd_magic, sizeof(pgd_magic), 1, m_file) != 1)
return false;
if (pgd_magic == 0x44475000) // "\0PGD"
{
Log_ErrorPrintf("Encrypted PBP images are not supported, skipping %s", m_filename.c_str());
if (error)
error->SetMessage("Encrypted PBP images are not supported");
return false;
}
// Read in the TOC
if (FSeek64(m_file, iso_header_start + 0x800, SEEK_SET) != 0)
return false;
for (u32 i = 0; i < TOC_NUM_ENTRIES; i++)
{
if (fread(&m_toc[i], sizeof(m_toc[i]), 1, m_file) != 1)
return false;
}
// For homebrew EBOOTs, audio track table doesn't exist -- the data track block table will point to compressed blocks
// for both data and audio
// Get the offset of the compressed iso
if (FSeek64(m_file, iso_header_start + 0xBFC, SEEK_SET) != 0)
return false;
u32 iso_offset;
if (fread(&iso_offset, sizeof(iso_offset), 1, m_file) != 1)
return false;
// Generate block info table
if (FSeek64(m_file, iso_header_start + 0x4000, SEEK_SET) != 0)
return false;
for (u32 i = 0; i < BLOCK_TABLE_NUM_ENTRIES; i++)
{
BlockTableEntry bte;
if (fread(&bte, sizeof(bte), 1, m_file) != 1)
return false;
// Only store absolute file offset into a BlockInfo if this is a valid block
m_blockinfo_table[i] = {(bte.size != 0) ? (iso_header_start + iso_offset + bte.offset) : 0, bte.size};
// printf("Block %u, file offset %u, size %u\n", i, m_blockinfo_table[i].offset, m_blockinfo_table[i].size);
}
// iso_header_start + 0x12D4, 0x12D6, 0x12D8 supposedly contain data on block size, num clusters, and num blocks
// Might be useful for error checking, but probably not that important as of now
// Ignore track types for first three TOC entries, these don't seem to be consistent, but check that the points are
// valid. Not sure what m_toc[0].userdata_start.s encodes on homebrew EBOOTs though, so ignore that
if (m_toc[0].point != 0xA0 || m_toc[1].point != 0xA1 || m_toc[2].point != 0xA2)
{
Log_ErrorPrint("Invalid points on information tracks");
return false;
}
const u8 first_track = PackedBCDToBinary(m_toc[0].userdata_start.m);
const u8 last_track = PackedBCDToBinary(m_toc[1].userdata_start.m);
const LBA sectors_on_file =
Position::FromBCD(m_toc[2].userdata_start.m, m_toc[2].userdata_start.s, m_toc[2].userdata_start.f).ToLBA();
if (first_track != 1 || last_track < first_track)
{
Log_ErrorPrint("Invalid starting track number or track count");
return false;
}
// We assume that the pregap for the data track (track 1) is not on file, but pregaps for any additional tracks are on
// file. Also, homebrew tools seem to create 2 second pregaps for audio tracks, even when the audio track has a pregap
// that isn't 2 seconds long. We don't have a good way to validate this, and have to assume the TOC is giving us
// correct pregap lengths...
ClearTOC();
m_lba_count = sectors_on_file;
LBA track1_pregap_frames = 0;
for (u32 curr_track = 1; curr_track <= last_track; curr_track++)
{
// Load in all the user stuff to m_tracks and m_indices
const TOCEntry& t = m_toc[static_cast<size_t>(curr_track) + 2];
const u8 track_num = PackedBCDToBinary(t.point);
if (track_num != curr_track)
Log_WarningPrintf("Mismatched TOC track number, expected %u but got %u", static_cast<u32>(curr_track), track_num);
const bool is_audio_track = t.type == 0x01;
const bool is_first_track = curr_track == 1;
const bool is_last_track = curr_track == last_track;
const TrackMode track_mode = is_audio_track ? TrackMode::Audio : TrackMode::Mode2Raw;
const u32 track_sector_size = GetBytesPerSector(track_mode);
SubChannelQ::Control track_control = {};
track_control.data = !is_audio_track;
LBA pregap_start = Position::FromBCD(t.pregap_start.m, t.pregap_start.s, t.pregap_start.f).ToLBA();
LBA userdata_start = Position::FromBCD(t.userdata_start.m, t.userdata_start.s, t.userdata_start.f).ToLBA();
LBA pregap_frames;
u32 pregap_sector_size;
if (userdata_start < pregap_start)
{
if (!is_first_track || is_audio_track)
{
Log_ErrorPrintf("Invalid TOC entry at index %u, user data (%u) should not start before pregap (%u)",
static_cast<u32>(curr_track), userdata_start, pregap_start);
return false;
}
Log_WarningPrintf(
"Invalid TOC entry at index %u, user data (%u) should not start before pregap (%u), assuming not in file.",
static_cast<u32>(curr_track), userdata_start, pregap_start);
pregap_start = 0;
pregap_frames = userdata_start;
pregap_sector_size = 0;
}
else
{
pregap_frames = userdata_start - pregap_start;
pregap_sector_size = track_sector_size;
}
if (is_first_track)
{
m_lba_count += pregap_frames;
track1_pregap_frames = pregap_frames;
}
Index pregap_index = {};
pregap_index.file_offset =
is_first_track ? 0 : (static_cast<u64>(pregap_start - track1_pregap_frames) * pregap_sector_size);
pregap_index.file_index = 0;
pregap_index.file_sector_size = pregap_sector_size;
pregap_index.start_lba_on_disc = pregap_start;
pregap_index.track_number = curr_track;
pregap_index.index_number = 0;
pregap_index.start_lba_in_track = static_cast<LBA>(-static_cast<s32>(pregap_frames));
pregap_index.length = pregap_frames;
pregap_index.mode = track_mode;
pregap_index.control.bits = track_control.bits;
pregap_index.is_pregap = true;
m_indices.push_back(pregap_index);
Index userdata_index = {};
userdata_index.file_offset = static_cast<u64>(userdata_start - track1_pregap_frames) * track_sector_size;
userdata_index.file_index = 0;
userdata_index.file_sector_size = track_sector_size;
userdata_index.start_lba_on_disc = userdata_start;
userdata_index.track_number = curr_track;
userdata_index.index_number = 1;
userdata_index.start_lba_in_track = 0;
userdata_index.mode = track_mode;
userdata_index.control.bits = track_control.bits;
userdata_index.is_pregap = false;
if (is_last_track)
{
if (userdata_start >= m_lba_count)
{
Log_ErrorPrintf("Last user data index on disc for TOC entry %u should not be 0 or less in length",
static_cast<u32>(curr_track));
return false;
}
userdata_index.length = m_lba_count - userdata_start;
}
else
{
const TOCEntry& next_track = m_toc[static_cast<size_t>(curr_track) + 3];
const LBA next_track_start =
Position::FromBCD(next_track.pregap_start.m, next_track.pregap_start.s, next_track.pregap_start.f).ToLBA();
const u8 next_track_num = PackedBCDToBinary(next_track.point);
if (next_track_num != curr_track + 1 || next_track_start < userdata_start)
{
Log_ErrorPrintf("Unable to calculate user data index length for TOC entry %u", static_cast<u32>(curr_track));
return false;
}
userdata_index.length = next_track_start - userdata_start;
}
m_indices.push_back(userdata_index);
m_tracks.push_back(Track{curr_track, userdata_start, 2 * curr_track - 1,
pregap_index.length + userdata_index.length, track_mode, track_control});
}
AddLeadOutIndex();
// Initialize zlib stream
if (!InitDecompressionStream())
{
Log_ErrorPrint("Failed to initialize zlib decompression stream");
return false;
}
if (m_disc_offsets.size() > 1)
{
std::string sbi_path(Path::StripExtension(m_filename));
sbi_path += TinyString::FromFormat("_%u.sbi", index + 1);
m_sbi.LoadSBI(sbi_path.c_str());
}
else
m_sbi.LoadSBI(Path::ReplaceExtension(m_filename, "sbi").c_str());
m_current_disc = index;
return Seek(1, Position{0, 0, 0});
}
const std::string* CDImagePBP::LookupStringSFOTableEntry(const char* key, const SFOTable& table)
{
auto iter = table.find(key);
if (iter == table.end())
return nullptr;
const SFOTableDataValue& data_value = iter->second;
if (!std::holds_alternative<std::string>(data_value))
return nullptr;
return &std::get<std::string>(data_value);
}
bool CDImagePBP::InitDecompressionStream()
{
m_inflate_stream = {};
m_inflate_stream.next_in = Z_NULL;
m_inflate_stream.avail_in = 0;
m_inflate_stream.zalloc = Z_NULL;
m_inflate_stream.zfree = Z_NULL;
m_inflate_stream.opaque = Z_NULL;
int ret = inflateInit2(&m_inflate_stream, -MAX_WBITS);
return ret == Z_OK;
}
bool CDImagePBP::DecompressBlock(const BlockInfo& block_info)
{
if (FSeek64(m_file, block_info.offset, SEEK_SET) != 0)
return false;
// Compression level 0 has compressed size == decompressed size.
if (block_info.size == m_decompressed_block.size())
{
return (fread(m_decompressed_block.data(), sizeof(u8), m_decompressed_block.size(), m_file) ==
m_decompressed_block.size());
}
m_compressed_block.resize(block_info.size);
if (fread(m_compressed_block.data(), sizeof(u8), m_compressed_block.size(), m_file) != m_compressed_block.size())
return false;
m_inflate_stream.next_in = m_compressed_block.data();
m_inflate_stream.avail_in = static_cast<uInt>(m_compressed_block.size());
m_inflate_stream.next_out = m_decompressed_block.data();
m_inflate_stream.avail_out = static_cast<uInt>(m_decompressed_block.size());
if (inflateReset(&m_inflate_stream) != Z_OK)
return false;
int err = inflate(&m_inflate_stream, Z_FINISH);
if (err != Z_STREAM_END)
{
Log_ErrorPrintf("Inflate error %d", err);
return false;
}
return true;
}
bool CDImagePBP::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index)
{
if (m_sbi.GetReplacementSubChannelQ(index.start_lba_on_disc + lba_in_index, subq))
return true;
return CDImage::ReadSubChannelQ(subq, index, lba_in_index);
}
bool CDImagePBP::HasNonStandardSubchannel() const
{
return (m_sbi.GetReplacementSectorCount() > 0);
}
bool CDImagePBP::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index)
{
const u32 offset_in_file = static_cast<u32>(index.file_offset) + (lba_in_index * index.file_sector_size);
const u32 offset_in_block = offset_in_file % DECOMPRESSED_BLOCK_SIZE;
const u32 requested_block = offset_in_file / DECOMPRESSED_BLOCK_SIZE;
BlockInfo& bi = m_blockinfo_table[requested_block];
if (bi.size == 0)
{
Log_ErrorPrintf("Invalid block %u requested", requested_block);
return false;
}
if (m_current_block != requested_block && !DecompressBlock(bi))
{
Log_ErrorPrintf("Failed to decompress block %u", requested_block);
return false;
}
std::memcpy(buffer, &m_decompressed_block[offset_in_block], RAW_SECTOR_SIZE);
return true;
}
#if _DEBUG
void CDImagePBP::PrintPBPHeaderInfo(const PBPHeader& pbp_header)
{
printf("PBP header info\n");
printf("PBP format version 0x%08X\n", pbp_header.version);
printf("File offsets\n");
printf("PARAM.SFO 0x%08X PARSE\n", pbp_header.param_sfo_offset);
printf("ICON0.PNG 0x%08X IGNORE\n", pbp_header.icon0_png_offset);
printf("ICON1.PNG 0x%08X IGNORE\n", pbp_header.icon1_png_offset);
printf("PIC0.PNG 0x%08X IGNORE\n", pbp_header.pic0_png_offset);
printf("PIC1.PNG 0x%08X IGNORE\n", pbp_header.pic1_png_offset);
printf("SND0.AT3 0x%08X IGNORE\n", pbp_header.snd0_at3_offset);
printf("DATA.PSP 0x%08X IGNORE\n", pbp_header.data_psp_offset);
printf("DATA.PSAR 0x%08X PARSE\n", pbp_header.data_psar_offset);
printf("\n");
}
void CDImagePBP::PrintSFOHeaderInfo(const SFOHeader& sfo_header)
{
printf("SFO header info\n");
printf("SFO format version 0x%08X\n", sfo_header.version);
printf("SFO key table offset 0x%08X\n", sfo_header.key_table_offset);
printf("SFO data table offset 0x%08X\n", sfo_header.data_table_offset);
printf("SFO table entry count 0x%08X\n", sfo_header.num_table_entries);
printf("\n");
}
void CDImagePBP::PrintSFOIndexTableEntry(const SFOIndexTableEntry& sfo_index_table_entry, size_t i)
{
printf("SFO index table entry %zu\n", i);
printf("Key offset 0x%08X\n", sfo_index_table_entry.key_offset);
printf("Data type 0x%08X\n", sfo_index_table_entry.data_type);
printf("Data size 0x%08X\n", sfo_index_table_entry.data_size);
printf("Total data size 0x%08X\n", sfo_index_table_entry.data_total_size);
printf("Data offset 0x%08X\n", sfo_index_table_entry.data_offset);
printf("\n");
}
void CDImagePBP::PrintSFOTable(const SFOTable& sfo_table)
{
for (auto it = sfo_table.begin(); it != sfo_table.end(); ++it)
{
std::string key_value = it->first;
SFOTableDataValue data_value = it->second;
if (std::holds_alternative<std::string>(data_value))
printf("Key: %s, Data: %s\n", key_value.c_str(), std::get<std::string>(data_value).c_str());
else if (std::holds_alternative<u32>(data_value))
printf("Key: %s, Data: %u\n", key_value.c_str(), std::get<u32>(data_value));
}
}
#endif
bool CDImagePBP::HasSubImages() const
{
return m_disc_offsets.size() > 1;
}
std::string CDImagePBP::GetMetadata(const std::string_view& type) const
{
if (type == "title")
{
const std::string* title = LookupStringSFOTableEntry("TITLE", m_sfo_table);
if (title && !title->empty())
return *title;
}
return CDImage::GetMetadata(type);
}
u32 CDImagePBP::GetSubImageCount() const
{
return static_cast<u32>(m_disc_offsets.size());
}
u32 CDImagePBP::GetCurrentSubImage() const
{
return m_current_disc;
}
bool CDImagePBP::SwitchSubImage(u32 index, Common::Error* error)
{
if (index >= m_disc_offsets.size())
return false;
const u32 old_disc = m_current_disc;
if (!OpenDisc(index, error))
{
// return to old disc, this should never fail... in theory.
if (!OpenDisc(old_disc, nullptr))
Panic("Failed to reopen old disc after switch.");
}
return true;
}
std::string CDImagePBP::GetSubImageMetadata(u32 index, const std::string_view& type) const
{
if (type == "title")
{
const std::string* title = LookupStringSFOTableEntry("TITLE", m_sfo_table);
if (title && !title->empty())
return StringUtil::StdStringFromFormat("%s (Disc %u)", title->c_str(), index + 1);
}
return CDImage::GetSubImageMetadata(index, type);
}
std::unique_ptr<CDImage> CDImage::OpenPBPImage(const char* filename, Common::Error* error)
{
std::unique_ptr<CDImagePBP> image = std::make_unique<CDImagePBP>();
if (!image->Open(filename, error))
return {};
return image;
}

440
src/util/cd_image_ppf.cpp Normal file
View File

@ -0,0 +1,440 @@
#include "cd_image.h"
#include "cd_subchannel_replacement.h"
#include "common/assert.h"
#include "common/file_system.h"
#include "common/log.h"
#include <algorithm>
#include <cerrno>
#include <map>
#include <unordered_map>
Log_SetChannel(CDImagePPF);
enum : u32
{
DESC_SIZE = 50,
BLOCKCHECK_SIZE = 1024
};
class CDImagePPF : public CDImage
{
public:
CDImagePPF();
~CDImagePPF() override;
bool Open(const char* filename, std::unique_ptr<CDImage> parent_image);
bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) override;
bool HasNonStandardSubchannel() const override;
std::string GetMetadata(const std::string_view& type) const override;
std::string GetSubImageMetadata(u32 index, const std::string_view& type) const override;
protected:
bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override;
private:
bool ReadV1Patch(std::FILE* fp);
bool ReadV2Patch(std::FILE* fp);
bool ReadV3Patch(std::FILE* fp);
u32 ReadFileIDDiz(std::FILE* fp, u32 version);
bool AddPatch(u64 offset, const u8* patch, u32 patch_size);
std::unique_ptr<CDImage> m_parent_image;
std::vector<u8> m_replacement_data;
std::unordered_map<u32, u32> m_replacement_map;
u32 m_replacement_offset = 0;
};
CDImagePPF::CDImagePPF() = default;
CDImagePPF::~CDImagePPF() = default;
bool CDImagePPF::Open(const char* filename, std::unique_ptr<CDImage> parent_image)
{
auto fp = FileSystem::OpenManagedCFile(filename, "rb");
if (!fp)
{
Log_ErrorPrintf("Failed to open '%s'", filename);
return false;
}
u32 magic;
if (std::fread(&magic, sizeof(magic), 1, fp.get()) != 1)
{
Log_ErrorPrintf("Failed to read magic from '%s'", filename);
return false;
}
// work out the offset from the start of the parent image which we need to patch
// i.e. the two second implicit pregap on data sectors
if (parent_image->GetTrack(1).mode != TrackMode::Audio)
m_replacement_offset = parent_image->GetIndex(1).start_lba_on_disc;
// copy all the stuff from the parent image
m_filename = parent_image->GetFileName();
m_tracks = parent_image->GetTracks();
m_indices = parent_image->GetIndices();
m_parent_image = std::move(parent_image);
if (magic == 0x33465050) // PPF3
return ReadV3Patch(fp.get());
else if (magic == 0x32465050) // PPF2
return ReadV2Patch(fp.get());
else if (magic == 0x31465050) // PPF1
return ReadV1Patch(fp.get());
Log_ErrorPrintf("Unknown PPF magic %08X", magic);
return false;
}
u32 CDImagePPF::ReadFileIDDiz(std::FILE* fp, u32 version)
{
const int lenidx = (version == 2) ? 4 : 2;
u32 magic;
if (std::fseek(fp, -(lenidx + 4), SEEK_END) != 0 || std::fread(&magic, sizeof(magic), 1, fp) != 1)
{
Log_WarningPrintf("Failed to read diz magic");
return 0;
}
if (magic != 0x5A49442E) // .DIZ
return 0;
u32 dlen = 0;
if (std::fseek(fp, -lenidx, SEEK_END) != 0 || std::fread(&dlen, lenidx, 1, fp) != 1)
{
Log_WarningPrintf("Failed to read diz length");
return 0;
}
if (dlen > static_cast<u32>(std::ftell(fp)))
{
Log_WarningPrintf("diz length out of range");
return 0;
}
std::string fdiz;
fdiz.resize(dlen);
if (std::fseek(fp, -(lenidx + 16 + static_cast<int>(dlen)), SEEK_END) != 0 ||
std::fread(fdiz.data(), 1, dlen, fp) != dlen)
{
Log_WarningPrintf("Failed to read fdiz");
return 0;
}
Log_InfoPrintf("File_Id.diz: %s", fdiz.c_str());
return dlen;
}
bool CDImagePPF::ReadV1Patch(std::FILE* fp)
{
char desc[DESC_SIZE + 1] = {};
if (std::fseek(fp, 6, SEEK_SET) != 0 || std::fread(desc, sizeof(char), DESC_SIZE, fp) != DESC_SIZE)
{
Log_ErrorPrintf("Failed to read description");
return false;
}
u32 filelen;
if (std::fseek(fp, 0, SEEK_END) != 0 || (filelen = static_cast<u32>(std::ftell(fp))) == 0 || filelen < 56)
{
Log_ErrorPrintf("Invalid ppf file");
return false;
}
u32 count = filelen - 56;
if (count <= 0)
return false;
if (std::fseek(fp, 56, SEEK_SET) != 0)
return false;
std::vector<u8> temp;
while (count > 0)
{
u32 offset;
u8 chunk_size;
if (std::fread(&offset, sizeof(offset), 1, fp) != 1 || std::fread(&chunk_size, sizeof(chunk_size), 1, fp) != 1)
{
Log_ErrorPrintf("Incomplete ppf");
return false;
}
temp.resize(chunk_size);
if (std::fread(temp.data(), 1, chunk_size, fp) != chunk_size)
{
Log_ErrorPrintf("Failed to read patch data");
return false;
}
if (!AddPatch(offset, temp.data(), chunk_size))
return false;
count -= sizeof(offset) + sizeof(chunk_size) + chunk_size;
}
Log_InfoPrintf("Loaded %zu replacement sectors from version 1 PPF", m_replacement_map.size());
return true;
}
bool CDImagePPF::ReadV2Patch(std::FILE* fp)
{
char desc[DESC_SIZE + 1] = {};
if (std::fseek(fp, 6, SEEK_SET) != 0 || std::fread(desc, sizeof(char), DESC_SIZE, fp) != DESC_SIZE)
{
Log_ErrorPrintf("Failed to read description");
return false;
}
Log_InfoPrintf("Patch description: %s", desc);
const u32 idlen = ReadFileIDDiz(fp, 2);
u32 origlen;
if (std::fseek(fp, 56, SEEK_SET) != 0 || std::fread(&origlen, sizeof(origlen), 1, fp) != 1)
{
Log_ErrorPrintf("Failed to read size");
return false;
}
std::vector<u8> temp;
temp.resize(BLOCKCHECK_SIZE);
if (std::fread(temp.data(), 1, BLOCKCHECK_SIZE, fp) != BLOCKCHECK_SIZE)
{
Log_ErrorPrintf("Failed to read blockcheck data");
return false;
}
// do blockcheck
{
u32 blockcheck_src_sector = 16 + m_replacement_offset;
u32 blockcheck_src_offset = 32;
std::vector<u8> src_sector(RAW_SECTOR_SIZE);
if (m_parent_image->Seek(blockcheck_src_sector) && m_parent_image->ReadRawSector(src_sector.data(), nullptr))
{
if (std::memcmp(&src_sector[blockcheck_src_offset], temp.data(), BLOCKCHECK_SIZE) != 0)
Log_WarningPrintf("Blockcheck failed. The patch may not apply correctly.");
}
else
{
Log_WarningPrintf("Failed to read blockcheck sector %u", blockcheck_src_sector);
}
}
u32 filelen;
if (std::fseek(fp, 0, SEEK_END) != 0 || (filelen = static_cast<u32>(std::ftell(fp))) == 0 || filelen < 1084)
{
Log_ErrorPrintf("Invalid ppf file");
return false;
}
u32 count = filelen - 1084;
if (idlen > 0)
count -= (idlen + 38);
if (count <= 0)
return false;
if (std::fseek(fp, 1084, SEEK_SET) != 0)
return false;
while (count > 0)
{
u32 offset;
u8 chunk_size;
if (std::fread(&offset, sizeof(offset), 1, fp) != 1 || std::fread(&chunk_size, sizeof(chunk_size), 1, fp) != 1)
{
Log_ErrorPrintf("Incomplete ppf");
return false;
}
temp.resize(chunk_size);
if (std::fread(temp.data(), 1, chunk_size, fp) != chunk_size)
{
Log_ErrorPrintf("Failed to read patch data");
return false;
}
if (!AddPatch(offset, temp.data(), chunk_size))
return false;
count -= sizeof(offset) + sizeof(chunk_size) + chunk_size;
}
Log_InfoPrintf("Loaded %zu replacement sectors from version 2 PPF", m_replacement_map.size());
return true;
}
bool CDImagePPF::ReadV3Patch(std::FILE* fp)
{
char desc[DESC_SIZE + 1] = {};
if (std::fseek(fp, 6, SEEK_SET) != 0 || std::fread(desc, sizeof(char), DESC_SIZE, fp) != DESC_SIZE)
{
Log_ErrorPrintf("Failed to read description");
return false;
}
Log_InfoPrintf("Patch description: %s", desc);
u32 idlen = ReadFileIDDiz(fp, 3);
u8 image_type;
u8 block_check;
u8 undo;
if (std::fseek(fp, 56, SEEK_SET) != 0 || std::fread(&image_type, sizeof(image_type), 1, fp) != 1 ||
std::fread(&block_check, sizeof(block_check), 1, fp) != 1 || std::fread(&undo, sizeof(undo), 1, fp) != 1)
{
Log_ErrorPrintf("Failed to read headers");
return false;
}
// TODO: Blockcheck
std::fseek(fp, 0, SEEK_END);
u32 count = static_cast<u32>(std::ftell(fp));
u32 seekpos = (block_check) ? 1084 : 60;
if (seekpos >= count)
{
Log_ErrorPrintf("File is too short");
return false;
}
count -= seekpos;
if (idlen > 0)
{
const u32 extralen = idlen + 18 + 16 + 2;
if (count < extralen)
{
Log_ErrorPrintf("File is too short (diz)");
return false;
}
count -= extralen;
}
if (std::fseek(fp, seekpos, SEEK_SET) != 0)
return false;
std::vector<u8> temp;
while (count > 0)
{
u64 offset;
u8 chunk_size;
if (std::fread(&offset, sizeof(offset), 1, fp) != 1 || std::fread(&chunk_size, sizeof(chunk_size), 1, fp) != 1)
{
Log_ErrorPrintf("Incomplete ppf");
return false;
}
temp.resize(chunk_size);
if (std::fread(temp.data(), 1, chunk_size, fp) != chunk_size)
{
Log_ErrorPrintf("Failed to read patch data");
return false;
}
if (!AddPatch(offset, temp.data(), chunk_size))
return false;
count -= sizeof(offset) + sizeof(chunk_size) + chunk_size;
}
Log_InfoPrintf("Loaded %zu replacement sectors from version 3 PPF", m_replacement_map.size());
return true;
}
bool CDImagePPF::AddPatch(u64 offset, const u8* patch, u32 patch_size)
{
Log_DebugPrintf("Starting applying patch of %u bytes at at offset %" PRIu64, patch_size, offset);
while (patch_size > 0)
{
const u32 sector_index = Truncate32(offset / RAW_SECTOR_SIZE) + m_replacement_offset;
const u32 sector_offset = Truncate32(offset % RAW_SECTOR_SIZE);
if (sector_index >= m_parent_image->GetLBACount())
{
Log_ErrorPrintf("Sector %u in patch is out of range", sector_index);
return false;
}
const u32 bytes_to_patch = std::min(patch_size, RAW_SECTOR_SIZE - sector_offset);
auto iter = m_replacement_map.find(sector_index);
if (iter == m_replacement_map.end())
{
const u32 replacement_buffer_start = static_cast<u32>(m_replacement_data.size());
m_replacement_data.resize(m_replacement_data.size() + RAW_SECTOR_SIZE);
if (!m_parent_image->Seek(sector_index) ||
!m_parent_image->ReadRawSector(&m_replacement_data[replacement_buffer_start], nullptr))
{
Log_ErrorPrintf("Failed to read sector %u from parent image", sector_index);
return false;
}
iter = m_replacement_map.emplace(sector_index, replacement_buffer_start).first;
}
// patch it!
Log_DebugPrintf(" Patching %u bytes at sector %u offset %u", bytes_to_patch, sector_index, sector_offset);
std::memcpy(&m_replacement_data[iter->second + sector_offset], patch, bytes_to_patch);
offset += bytes_to_patch;
patch += bytes_to_patch;
patch_size -= bytes_to_patch;
}
return true;
}
bool CDImagePPF::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index)
{
return m_parent_image->ReadSubChannelQ(subq, index, lba_in_index);
}
bool CDImagePPF::HasNonStandardSubchannel() const
{
return m_parent_image->HasNonStandardSubchannel();
}
std::string CDImagePPF::GetMetadata(const std::string_view& type) const
{
return m_parent_image->GetMetadata(type);
}
std::string CDImagePPF::GetSubImageMetadata(u32 index, const std::string_view& type) const
{
// We only support a single sub-image for patched games.
std::string ret;
if (index == 0)
ret = m_parent_image->GetSubImageMetadata(index, type);
return ret;
}
bool CDImagePPF::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index)
{
DebugAssert(index.file_index == 0);
const u32 sector_number = index.start_lba_on_disc + lba_in_index;
const auto it = m_replacement_map.find(sector_number);
if (it == m_replacement_map.end())
return m_parent_image->ReadSectorFromIndex(buffer, index, lba_in_index);
std::memcpy(buffer, &m_replacement_data[it->second], RAW_SECTOR_SIZE);
return true;
}
std::unique_ptr<CDImage>
CDImage::OverlayPPFPatch(const char* filename, std::unique_ptr<CDImage> parent_image,
ProgressCallback* progress /* = ProgressCallback::NullProgressCallback */)
{
std::unique_ptr<CDImagePPF> ppf_image = std::make_unique<CDImagePPF>();
if (!ppf_image->Open(filename, std::move(parent_image)))
return {};
return ppf_image;
}

View File

@ -0,0 +1,115 @@
#include "cd_subchannel_replacement.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include <algorithm>
#include <memory>
Log_SetChannel(CDSubChannelReplacement);
#pragma pack(push, 1)
struct SBIFileEntry
{
u8 minute_bcd;
u8 second_bcd;
u8 frame_bcd;
u8 type;
u8 data[10];
};
#pragma pack(pop)
CDSubChannelReplacement::CDSubChannelReplacement() = default;
CDSubChannelReplacement::~CDSubChannelReplacement() = default;
static constexpr u32 MSFToLBA(u8 minute_bcd, u8 second_bcd, u8 frame_bcd)
{
const u8 minute = PackedBCDToBinary(minute_bcd);
const u8 second = PackedBCDToBinary(second_bcd);
const u8 frame = PackedBCDToBinary(frame_bcd);
return (ZeroExtend32(minute) * 60 * 75) + (ZeroExtend32(second) * 75) + ZeroExtend32(frame);
}
bool CDSubChannelReplacement::LoadSBI(const char* path)
{
auto fp = FileSystem::OpenManagedCFile(path, "rb");
if (!fp)
return false;
char header[4];
if (std::fread(header, sizeof(header), 1, fp.get()) != 1)
{
Log_ErrorPrintf("Failed to read header for '%s'", path);
return true;
}
static constexpr char expected_header[] = {'S', 'B', 'I', '\0'};
if (std::memcmp(header, expected_header, sizeof(header)) != 0)
{
Log_ErrorPrintf("Invalid header in '%s'", path);
return true;
}
SBIFileEntry entry;
while (std::fread(&entry, sizeof(entry), 1, fp.get()) == 1)
{
if (!IsValidPackedBCD(entry.minute_bcd) || !IsValidPackedBCD(entry.second_bcd) ||
!IsValidPackedBCD(entry.frame_bcd))
{
Log_ErrorPrintf("Invalid position [%02x:%02x:%02x] in '%s'", entry.minute_bcd, entry.second_bcd, entry.frame_bcd,
path);
return false;
}
if (entry.type != 1)
{
Log_ErrorPrintf("Invalid type 0x%02X in '%s'", entry.type, path);
return false;
}
const u32 lba = MSFToLBA(entry.minute_bcd, entry.second_bcd, entry.frame_bcd);
CDImage::SubChannelQ subq;
std::copy_n(entry.data, countof(entry.data), subq.data.data());
// generate an invalid crc by flipping all bits from the valid crc (will never collide)
const u16 crc = subq.ComputeCRC(subq.data) ^ 0xFFFF;
subq.data[10] = Truncate8(crc);
subq.data[11] = Truncate8(crc >> 8);
m_replacement_subq.emplace(lba, subq);
}
Log_InfoPrintf("Loaded %zu replacement sectors from '%s'", m_replacement_subq.size(), path);
return true;
}
bool CDSubChannelReplacement::LoadSBIFromImagePath(const char* image_path)
{
return LoadSBI(Path::ReplaceExtension(image_path, "sbi").c_str());
}
void CDSubChannelReplacement::AddReplacementSubChannelQ(u32 lba, const CDImage::SubChannelQ& subq)
{
auto iter = m_replacement_subq.find(lba);
if (iter != m_replacement_subq.end())
iter->second.data = subq.data;
else
m_replacement_subq.emplace(lba, subq);
}
bool CDSubChannelReplacement::GetReplacementSubChannelQ(u8 minute_bcd, u8 second_bcd, u8 frame_bcd,
CDImage::SubChannelQ* subq) const
{
return GetReplacementSubChannelQ(MSFToLBA(minute_bcd, second_bcd, frame_bcd), subq);
}
bool CDSubChannelReplacement::GetReplacementSubChannelQ(u32 lba, CDImage::SubChannelQ* subq) const
{
const auto iter = m_replacement_subq.find(lba);
if (iter == m_replacement_subq.cend())
return false;
*subq = iter->second;
return true;
}

View File

@ -0,0 +1,32 @@
#pragma once
#include "cd_image.h"
#include "common/types.h"
#include <array>
#include <cstdio>
#include <unordered_map>
class CDSubChannelReplacement
{
public:
CDSubChannelReplacement();
~CDSubChannelReplacement();
u32 GetReplacementSectorCount() const { return static_cast<u32>(m_replacement_subq.size()); }
bool LoadSBI(const char* path);
bool LoadSBIFromImagePath(const char* image_path);
/// Adds a sector to the replacement map.
void AddReplacementSubChannelQ(u32 lba, const CDImage::SubChannelQ& subq);
/// Returns the replacement subchannel data for the specified position (in BCD).
bool GetReplacementSubChannelQ(u8 minute_bcd, u8 second_bcd, u8 frame_bcd, CDImage::SubChannelQ* subq) const;
/// Returns the replacement subchannel data for the specified sector.
bool GetReplacementSubChannelQ(u32 lba, CDImage::SubChannelQ* subq) const;
private:
using ReplacementMap = std::unordered_map<u32, CDImage::SubChannelQ>;
ReplacementMap m_replacement_subq;
};

98
src/util/cd_xa.cpp Normal file
View File

@ -0,0 +1,98 @@
#include "cd_xa.h"
#include "cd_image.h"
#include <algorithm>
#include <array>
namespace CDXA {
static constexpr std::array<s32, 4> s_xa_adpcm_filter_table_pos = {{0, 60, 115, 98}};
static constexpr std::array<s32, 4> s_xa_adpcm_filter_table_neg = {{0, 0, -52, -55}};
template<bool IS_STEREO, bool IS_8BIT>
static void DecodeXA_ADPCMChunk(const u8* chunk_ptr, s16* samples, s32* last_samples)
{
// The data layout is annoying here. Each word of data is interleaved with the other blocks, requiring multiple
// passes to decode the whole chunk.
constexpr u32 NUM_BLOCKS = IS_8BIT ? 4 : 8;
constexpr u32 WORDS_PER_BLOCK = 28;
const u8* headers_ptr = chunk_ptr + 4;
const u8* words_ptr = chunk_ptr + 16;
for (u32 block = 0; block < NUM_BLOCKS; block++)
{
const XA_ADPCMBlockHeader block_header{headers_ptr[block]};
const u8 shift = block_header.GetShift();
const u8 filter = block_header.GetFilter();
const s32 filter_pos = s_xa_adpcm_filter_table_pos[filter];
const s32 filter_neg = s_xa_adpcm_filter_table_neg[filter];
s16* out_samples_ptr =
IS_STEREO ? &samples[(block / 2) * (WORDS_PER_BLOCK * 2) + (block % 2)] : &samples[block * WORDS_PER_BLOCK];
constexpr u32 out_samples_increment = IS_STEREO ? 2 : 1;
for (u32 word = 0; word < 28; word++)
{
// NOTE: assumes LE
u32 word_data;
std::memcpy(&word_data, &words_ptr[word * sizeof(u32)], sizeof(word_data));
// extract nibble from block
const u32 nibble = IS_8BIT ? ((word_data >> (block * 8)) & 0xFF) : ((word_data >> (block * 4)) & 0x0F);
const s16 sample = static_cast<s16>(Truncate16(nibble << 12)) >> shift;
// mix in previous values
s32* prev = IS_STEREO ? &last_samples[(block & 1) * 2] : last_samples;
const s32 interp_sample = s32(sample) + ((prev[0] * filter_pos) + (prev[1] * filter_neg) + 32) / 64;
// update previous values
prev[1] = prev[0];
prev[0] = interp_sample;
*out_samples_ptr = static_cast<s16>(std::clamp<s32>(interp_sample, -0x8000, 0x7FFF));
out_samples_ptr += out_samples_increment;
}
}
}
template<bool IS_STEREO, bool IS_8BIT>
static void DecodeXA_ADPCMChunks(const u8* chunk_ptr, s16* samples, s32* last_samples)
{
constexpr u32 NUM_CHUNKS = 18;
constexpr u32 CHUNK_SIZE_IN_BYTES = 128;
constexpr u32 WORDS_PER_CHUNK = 28;
constexpr u32 SAMPLES_PER_CHUNK = WORDS_PER_CHUNK * (IS_8BIT ? 4 : 8);
for (u32 i = 0; i < NUM_CHUNKS; i++)
{
DecodeXA_ADPCMChunk<IS_STEREO, IS_8BIT>(chunk_ptr, samples, last_samples);
samples += SAMPLES_PER_CHUNK;
chunk_ptr += CHUNK_SIZE_IN_BYTES;
}
}
void DecodeADPCMSector(const void* data, s16* samples, s32* last_samples)
{
const XASubHeader* subheader = reinterpret_cast<const XASubHeader*>(
reinterpret_cast<const u8*>(data) + CDImage::SECTOR_SYNC_SIZE + sizeof(CDImage::SectorHeader));
// The XA subheader is repeated?
const u8* chunk_ptr = reinterpret_cast<const u8*>(data) + CDImage::SECTOR_SYNC_SIZE + sizeof(CDImage::SectorHeader) +
sizeof(XASubHeader) + 4;
if (subheader->codinginfo.bits_per_sample != 1)
{
if (subheader->codinginfo.mono_stereo != 1)
DecodeXA_ADPCMChunks<false, false>(chunk_ptr, samples, last_samples);
else
DecodeXA_ADPCMChunks<true, false>(chunk_ptr, samples, last_samples);
}
else
{
if (subheader->codinginfo.mono_stereo != 1)
DecodeXA_ADPCMChunks<false, true>(chunk_ptr, samples, last_samples);
else
DecodeXA_ADPCMChunks<true, true>(chunk_ptr, samples, last_samples);
}
}
} // namespace CDXA

70
src/util/cd_xa.h Normal file
View File

@ -0,0 +1,70 @@
#pragma once
#include "common/bitfield.h"
#include "common/types.h"
namespace CDXA {
enum
{
XA_SUBHEADER_SIZE = 4,
XA_ADPCM_SAMPLES_PER_SECTOR_4BIT = 4032, // 28 words * 8 nibbles per word * 18 chunks
XA_ADPCM_SAMPLES_PER_SECTOR_8BIT = 2016 // 28 words * 4 bytes per word * 18 chunks
};
struct XASubHeader
{
u8 file_number;
u8 channel_number;
union Submode
{
u8 bits;
BitField<u8, bool, 0, 1> eor;
BitField<u8, bool, 1, 1> video;
BitField<u8, bool, 2, 1> audio;
BitField<u8, bool, 3, 1> data;
BitField<u8, bool, 4, 1> trigger;
BitField<u8, bool, 5, 1> form2;
BitField<u8, bool, 6, 1> realtime;
BitField<u8, bool, 7, 1> eof;
} submode;
union Codinginfo
{
u8 bits;
BitField<u8, u8, 0, 2> mono_stereo;
BitField<u8, u8, 2, 2> sample_rate;
BitField<u8, u8, 4, 2> bits_per_sample;
BitField<u8, bool, 6, 1> emphasis;
bool IsStereo() const { return mono_stereo == 1; }
bool IsHalfSampleRate() const { return sample_rate == 1; }
u32 GetSampleRate() const { return sample_rate == 1 ? 18900 : 37800; }
u32 GetBitsPerSample() const { return bits_per_sample == 1 ? 8 : 4; }
u32 GetSamplesPerSector() const
{
return bits_per_sample == 1 ? XA_ADPCM_SAMPLES_PER_SECTOR_8BIT : XA_ADPCM_SAMPLES_PER_SECTOR_4BIT;
}
} codinginfo;
};
union XA_ADPCMBlockHeader
{
u8 bits;
BitField<u8, u8, 0, 4> shift;
BitField<u8, u8, 4, 2> filter;
// For both 4bit and 8bit ADPCM, reserved shift values 13..15 will act same as shift=9).
u8 GetShift() const
{
const u8 shift_value = shift;
return (shift_value > 12) ? 9 : shift_value;
}
u8 GetFilter() const { return filter; }
};
static_assert(sizeof(XA_ADPCMBlockHeader) == 1, "XA-ADPCM block header is one byte");
// Decodes XA-ADPCM samples in an audio sector. Stereo samples are interleaved with left first.
void DecodeADPCMSector(const void* data, s16* samples, s32* last_samples);
} // namespace CDXA

477
src/util/cue_parser.cpp Normal file
View File

@ -0,0 +1,477 @@
#include "cue_parser.h"
#include "common/error.h"
#include "common/log.h"
#include "common/string_util.h"
#include <cstdarg>
Log_SetChannel(CueParser);
namespace CueParser {
static bool TokenMatch(const std::string_view& s1, const char* token)
{
const size_t token_len = std::strlen(token);
if (s1.length() != token_len)
return false;
return (StringUtil::Strncasecmp(s1.data(), token, token_len) == 0);
}
File::File() = default;
File::~File() = default;
const Track* File::GetTrack(u32 n) const
{
for (const auto& it : m_tracks)
{
if (it.number == n)
return &it;
}
return nullptr;
}
Track* File::GetMutableTrack(u32 n)
{
for (auto& it : m_tracks)
{
if (it.number == n)
return &it;
}
return nullptr;
}
bool File::Parse(std::FILE* fp, Common::Error* error)
{
char line[1024];
u32 line_number = 1;
while (std::fgets(line, sizeof(line), fp))
{
if (!ParseLine(line, line_number, error))
return false;
line_number++;
}
if (!CompleteLastTrack(line_number, error))
return false;
if (!SetTrackLengths(line_number, error))
return false;
return true;
}
void File::SetError(u32 line_number, Common::Error* error, const char* format, ...)
{
std::va_list ap;
SmallString str;
va_start(ap, format);
str.FormatVA(format, ap);
va_end(ap);
Log_ErrorPrintf("Cue parse error at line %u: %s", line_number, str.GetCharArray());
if (error)
error->SetFormattedMessage("Cue parse error at line %u: %s", line_number, str.GetCharArray());
}
std::string_view File::GetToken(const char*& line)
{
std::string_view ret;
const char* start = line;
while (std::isspace(*start) && *start != '\0')
start++;
if (*start == '\0')
return ret;
const char* end;
const bool quoted = *start == '\"';
if (quoted)
{
start++;
end = start;
while (*end != '\"' && *end != '\0')
end++;
if (*end != '\"')
return ret;
ret = std::string_view(start, static_cast<size_t>(end - start));
// eat closing "
end++;
}
else
{
end = start;
while (!std::isspace(*end) && *end != '\0')
end++;
ret = std::string_view(start, static_cast<size_t>(end - start));
}
line = end;
return ret;
}
std::optional<MSF> File::GetMSF(const std::string_view& token)
{
static const s32 max_values[] = {std::numeric_limits<s32>::max(), 60, 75};
u32 parts[3] = {};
u32 part = 0;
u32 start = 0;
for (;;)
{
while (start < token.length() && token[start] < '0' && token[start] <= '9')
start++;
if (start == token.length())
return std::nullopt;
u32 end = start;
while (end < token.length() && token[end] >= '0' && token[end] <= '9')
end++;
const std::optional<s32> value = StringUtil::FromChars<s32>(token.substr(start, end - start));
if (!value.has_value() || value.value() < 0 || value.value() > max_values[part])
return std::nullopt;
parts[part] = static_cast<u32>(value.value());
part++;
if (part == 3)
break;
while (end < token.length() && std::isspace(token[end]))
end++;
if (end == token.length() || token[end] != ':')
return std::nullopt;
start = end + 1;
}
MSF ret;
ret.minute = static_cast<u8>(parts[0]);
ret.second = static_cast<u8>(parts[1]);
ret.frame = static_cast<u8>(parts[2]);
return ret;
}
bool File::ParseLine(const char* line, u32 line_number, Common::Error* error)
{
const std::string_view command(GetToken(line));
if (command.empty())
return true;
if (TokenMatch(command, "REM"))
{
// comment, eat it
return true;
}
if (TokenMatch(command, "FILE"))
return HandleFileCommand(line, line_number, error);
else if (TokenMatch(command, "TRACK"))
return HandleTrackCommand(line, line_number, error);
else if (TokenMatch(command, "INDEX"))
return HandleIndexCommand(line, line_number, error);
else if (TokenMatch(command, "PREGAP"))
return HandlePregapCommand(line, line_number, error);
else if (TokenMatch(command, "FLAGS"))
return HandleFlagCommand(line, line_number, error);
if (TokenMatch(command, "POSTGAP"))
{
Log_WarningPrintf("Ignoring '%*s' command", static_cast<int>(command.size()), command.data());
return true;
}
// stuff we definitely ignore
if (TokenMatch(command, "CATALOG") || TokenMatch(command, "CDTEXTFILE") || TokenMatch(command, "ISRC") ||
TokenMatch(command, "TRACK_ISRC") || TokenMatch(command, "TITLE") || TokenMatch(command, "PERFORMER") ||
TokenMatch(command, "SONGWRITER") || TokenMatch(command, "COMPOSER") || TokenMatch(command, "ARRANGER") ||
TokenMatch(command, "MESSAGE") || TokenMatch(command, "DISC_ID") || TokenMatch(command, "GENRE") ||
TokenMatch(command, "TOC_INFO1") || TokenMatch(command, "TOC_INFO2") || TokenMatch(command, "UPC_EAN") ||
TokenMatch(command, "SIZE_INFO"))
{
return true;
}
SetError(line_number, error, "Invalid command '%*s'", static_cast<int>(command.size()), command.data());
return false;
}
bool File::HandleFileCommand(const char* line, u32 line_number, Common::Error* error)
{
const std::string_view filename(GetToken(line));
const std::string_view mode(GetToken(line));
if (filename.empty())
{
SetError(line_number, error, "Missing filename");
return false;
}
if (!TokenMatch(mode, "BINARY"))
{
SetError(line_number, error, "Only BINARY modes are supported");
return false;
}
m_current_file = filename;
Log_DebugPrintf("File '%s'", m_current_file->c_str());
return true;
}
bool File::HandleTrackCommand(const char* line, u32 line_number, Common::Error* error)
{
if (!CompleteLastTrack(line_number, error))
return false;
if (!m_current_file.has_value())
{
SetError(line_number, error, "Starting a track declaration without a file set");
return false;
}
const std::string_view track_number_str(GetToken(line));
if (track_number_str.empty())
{
SetError(line_number, error, "Missing track number");
return false;
}
const std::optional<s32> track_number = StringUtil::FromChars<s32>(track_number_str);
if (track_number.value_or(0) < MIN_TRACK_NUMBER || track_number.value_or(0) > MAX_TRACK_NUMBER)
{
SetError(line_number, error, "Invalid track number %d", track_number.value_or(0));
return false;
}
const std::string_view mode_str = GetToken(line);
TrackMode mode;
if (TokenMatch(mode_str, "AUDIO"))
mode = TrackMode::Audio;
else if (TokenMatch(mode_str, "MODE1/2048"))
mode = TrackMode::Mode1;
else if (TokenMatch(mode_str, "MODE1/2352"))
mode = TrackMode::Mode1Raw;
else if (TokenMatch(mode_str, "MODE2/2336"))
mode = TrackMode::Mode2;
else if (TokenMatch(mode_str, "MODE2/2048"))
mode = TrackMode::Mode2Form1;
else if (TokenMatch(mode_str, "MODE2/2342"))
mode = TrackMode::Mode2Form2;
else if (TokenMatch(mode_str, "MODE2/2332"))
mode = TrackMode::Mode2FormMix;
else if (TokenMatch(mode_str, "MODE2/2352"))
mode = TrackMode::Mode2Raw;
else
{
SetError(line_number, error, "Invalid mode: '%*s'", static_cast<int>(mode_str.length()), mode_str.data());
return false;
}
m_current_track = Track();
m_current_track->number = static_cast<u32>(track_number.value());
m_current_track->file = m_current_file.value();
m_current_track->mode = mode;
return true;
}
bool File::HandleIndexCommand(const char* line, u32 line_number, Common::Error* error)
{
if (!m_current_track.has_value())
{
SetError(line_number, error, "Setting index without track");
return false;
}
const std::string_view index_number_str(GetToken(line));
if (index_number_str.empty())
{
SetError(line_number, error, "Missing index number");
return false;
}
const std::optional<s32> index_number = StringUtil::FromChars<s32>(index_number_str);
if (index_number.value_or(-1) < MIN_INDEX_NUMBER || index_number.value_or(-1) > MAX_INDEX_NUMBER)
{
SetError(line_number, error, "Invalid index number %d", index_number.value_or(-1));
return false;
}
if (m_current_track->GetIndex(static_cast<u32>(index_number.value())) != nullptr)
{
SetError(line_number, error, "Duplicate index %d", index_number.value());
return false;
}
const std::string_view msf_str(GetToken(line));
if (msf_str.empty())
{
SetError(line_number, error, "Missing index location");
return false;
}
const std::optional<MSF> msf(GetMSF(msf_str));
if (!msf.has_value())
{
SetError(line_number, error, "Invalid index location '%*s'", static_cast<int>(msf_str.size()), msf_str.data());
return false;
}
m_current_track->indices.emplace_back(static_cast<u32>(index_number.value()), msf.value());
return true;
}
bool File::HandlePregapCommand(const char* line, u32 line_number, Common::Error* error)
{
if (!m_current_track.has_value())
{
SetError(line_number, error, "Setting pregap without track");
return false;
}
if (m_current_track->zero_pregap.has_value())
{
SetError(line_number, error, "Pregap already specified for track %u", m_current_track->number);
return false;
}
const std::string_view msf_str(GetToken(line));
if (msf_str.empty())
{
SetError(line_number, error, "Missing pregap location");
return false;
}
const std::optional<MSF> msf(GetMSF(msf_str));
if (!msf.has_value())
{
SetError(line_number, error, "Invalid pregap location '%*s'", static_cast<int>(msf_str.size()), msf_str.data());
return false;
}
m_current_track->zero_pregap = std::move(msf);
return true;
}
bool File::HandleFlagCommand(const char* line, u32 line_number, Common::Error* error)
{
if (!m_current_track.has_value())
{
SetError(line_number, error, "Flags command outside of track");
return false;
}
for (;;)
{
const std::string_view token(GetToken(line));
if (token.empty())
break;
if (TokenMatch(token, "PRE"))
m_current_track->SetFlag(TrackFlag::PreEmphasis);
else if (TokenMatch(token, "DCP"))
m_current_track->SetFlag(TrackFlag::CopyPermitted);
else if (TokenMatch(token, "4CH"))
m_current_track->SetFlag(TrackFlag::FourChannelAudio);
else if (TokenMatch(token, "SCMS"))
m_current_track->SetFlag(TrackFlag::SerialCopyManagement);
else
Log_WarningPrintf("Unknown track flag '%*s'", static_cast<int>(token.size()), token.data());
}
return true;
}
bool File::CompleteLastTrack(u32 line_number, Common::Error* error)
{
if (!m_current_track.has_value())
return true;
const MSF* index1 = m_current_track->GetIndex(1);
if (!index1)
{
SetError(line_number, error, "Track %u is missing index 1", m_current_track->number);
return false;
}
// check indices
for (const auto& [index_number, index_msf] : m_current_track->indices)
{
if (index_number == 0)
continue;
const MSF* prev_index = m_current_track->GetIndex(index_number - 1);
if (prev_index && *prev_index > index_msf)
{
SetError(line_number, error, "Index %u is after index %u in track %u", index_number - 1, index_number,
m_current_track->number);
return false;
}
}
const MSF* index0 = m_current_track->GetIndex(0);
if (index0 && m_current_track->zero_pregap.has_value())
{
Log_WarningPrintf("Zero pregap and index 0 specified in track %u, ignoring zero pregap", m_current_track->number);
m_current_track->zero_pregap.reset();
}
m_current_track->start = *index1;
m_tracks.push_back(std::move(m_current_track.value()));
m_current_track.reset();
return true;
}
bool File::SetTrackLengths(u32 line_number, Common::Error* error)
{
for (const Track& track : m_tracks)
{
if (track.number > 1)
{
// set the length of the previous track based on this track's start, if they're the same file
Track* previous_track = GetMutableTrack(track.number - 1);
if (previous_track && previous_track->file == track.file)
{
if (previous_track->start > track.start)
{
SetError(line_number, error, "Track %u start greater than track %u start", previous_track->number,
track.number);
return false;
}
// Use index 0, otherwise index 1.
const MSF* start_index = track.GetIndex(0);
if (!start_index)
start_index = track.GetIndex(1);
previous_track->length = MSF::FromLBA(start_index->ToLBA() - previous_track->start.ToLBA());
}
}
}
return true;
}
const CueParser::MSF* Track::GetIndex(u32 n) const
{
for (const auto& it : indices)
{
if (it.first == n)
return &it.second;
}
return nullptr;
}
} // namespace CueParser

86
src/util/cue_parser.h Normal file
View File

@ -0,0 +1,86 @@
#pragma once
#include "cd_image.h"
#include "common/types.h"
#include <optional>
#include <string_view>
#include <utility>
#include <vector>
namespace Common {
class Error;
}
namespace CueParser {
using TrackMode = CDImage::TrackMode;
using MSF = CDImage::Position;
enum : s32
{
MIN_TRACK_NUMBER = 1,
MAX_TRACK_NUMBER = 99,
MIN_INDEX_NUMBER = 0,
MAX_INDEX_NUMBER = 99
};
enum class TrackFlag : u32
{
PreEmphasis = (1 << 0),
CopyPermitted = (1 << 1),
FourChannelAudio = (1 << 2),
SerialCopyManagement = (1 << 3),
};
struct Track
{
u32 number;
u32 flags;
std::string file;
std::vector<std::pair<u32, MSF>> indices;
TrackMode mode;
MSF start;
std::optional<MSF> length;
std::optional<MSF> zero_pregap;
const MSF* GetIndex(u32 n) const;
ALWAYS_INLINE bool HasFlag(TrackFlag flag) const { return (flags & static_cast<u32>(flag)) != 0; }
ALWAYS_INLINE void SetFlag(TrackFlag flag) { flags |= static_cast<u32>(flag); }
ALWAYS_INLINE void RemoveFlag(TrackFlag flag) { flags &= ~static_cast<u32>(flag); }
};
class File
{
public:
File();
~File();
const Track* GetTrack(u32 n) const;
bool Parse(std::FILE* fp, Common::Error* error);
private:
Track* GetMutableTrack(u32 n);
void SetError(u32 line_number, Common::Error* error, const char* format, ...);
static std::string_view GetToken(const char*& line);
static std::optional<MSF> GetMSF(const std::string_view& token);
bool ParseLine(const char* line, u32 line_number, Common::Error* error);
bool HandleFileCommand(const char* line, u32 line_number, Common::Error* error);
bool HandleTrackCommand(const char* line, u32 line_number, Common::Error* error);
bool HandleIndexCommand(const char* line, u32 line_number, Common::Error* error);
bool HandlePregapCommand(const char* line, u32 line_number, Common::Error* error);
bool HandleFlagCommand(const char* line, u32 line_number, Common::Error* error);
bool CompleteLastTrack(u32 line_number, Common::Error* error);
bool SetTrackLengths(u32 line_number, Common::Error* error);
std::vector<Track> m_tracks;
std::optional<std::string> m_current_file;
std::optional<Track> m_current_track;
};
} // namespace CueParser

292
src/util/iso_reader.cpp Normal file
View File

@ -0,0 +1,292 @@
#include "iso_reader.h"
#include "cd_image.h"
#include "common/log.h"
#include <cctype>
Log_SetChannel(ISOReader);
static bool FilenamesEqual(const char* a, const char* b, u32 length)
{
u32 pos = 0;
for (; pos < length && *a != '\0' && *b != '\0'; pos++)
{
if (std::tolower(*(a++)) != std::tolower(*(b++)))
return false;
}
return true;
}
ISOReader::ISOReader() = default;
ISOReader::~ISOReader() = default;
bool ISOReader::Open(CDImage* image, u32 track_number)
{
m_image = image;
m_track_number = track_number;
if (!ReadPVD())
return false;
return true;
}
bool ISOReader::ReadPVD()
{
// volume descriptor start at sector 16
if (!m_image->Seek(m_track_number, 16))
return false;
// try only a maximum of 256 volume descriptors
for (u32 i = 0; i < 256; i++)
{
u8 buffer[SECTOR_SIZE];
if (m_image->Read(CDImage::ReadMode::DataOnly, 1, buffer) != 1)
return false;
const ISOVolumeDescriptorHeader* header = reinterpret_cast<ISOVolumeDescriptorHeader*>(buffer);
if (header->type_code != 1)
continue;
else if (header->type_code == 255)
break;
std::memcpy(&m_pvd, buffer, sizeof(ISOPrimaryVolumeDescriptor));
Log_DebugPrintf("PVD found at index %u", i);
return true;
}
Log_ErrorPrint("PVD not found");
return false;
}
std::optional<ISOReader::ISODirectoryEntry> ISOReader::LocateFile(const char* path)
{
u8 sector_buffer[SECTOR_SIZE];
const ISODirectoryEntry* root_de = reinterpret_cast<const ISODirectoryEntry*>(m_pvd.root_directory_entry);
if (*path == '\0' || std::strcmp(path, "/") == 0)
{
// locating the root directory
return *root_de;
}
// start at the root directory
return LocateFile(path, sector_buffer, root_de->location_le, root_de->length_le);
}
std::optional<ISOReader::ISODirectoryEntry> ISOReader::LocateFile(const char* path, u8* sector_buffer,
u32 directory_record_lba, u32 directory_record_size)
{
if (directory_record_size == 0)
{
Log_ErrorPrintf("Directory entry record size 0 while looking for '%s'", path);
return std::nullopt;
}
// strip any leading slashes
const char* path_component_start = path;
while (*path_component_start == '/' || *path_component_start == '\\')
path_component_start++;
u32 path_component_length = 0;
const char* path_component_end = path_component_start;
while (*path_component_end != '\0' && *path_component_end != '/' && *path_component_end != '\\')
{
path_component_length++;
path_component_end++;
}
// start reading directory entries
const u32 num_sectors = (directory_record_size + (SECTOR_SIZE - 1)) / SECTOR_SIZE;
if (!m_image->Seek(m_track_number, directory_record_lba))
{
Log_ErrorPrintf("Seek to LBA %u failed", directory_record_lba);
return std::nullopt;
}
for (u32 i = 0; i < num_sectors; i++)
{
if (m_image->Read(CDImage::ReadMode::DataOnly, 1, sector_buffer) != 1)
{
Log_ErrorPrintf("Failed to read LBA %u", directory_record_lba + i);
return std::nullopt;
}
u32 sector_offset = 0;
while ((sector_offset + sizeof(ISODirectoryEntry)) < SECTOR_SIZE)
{
const ISODirectoryEntry* de = reinterpret_cast<const ISODirectoryEntry*>(&sector_buffer[sector_offset]);
const char* de_filename =
reinterpret_cast<const char*>(&sector_buffer[sector_offset + sizeof(ISODirectoryEntry)]);
if ((sector_offset + de->entry_length) > SECTOR_SIZE || de->filename_length > de->entry_length ||
de->entry_length < sizeof(ISODirectoryEntry))
{
break;
}
sector_offset += de->entry_length;
// skip current/parent directory
if (de->filename_length == 1 && (*de_filename == '\x0' || *de_filename == '\x1'))
continue;
// check filename length
if (de->filename_length < path_component_length)
continue;
if (de->flags & ISODirectoryEntryFlag_Directory)
{
// directories don't have the version? so check the length instead
if (de->filename_length != path_component_length ||
!FilenamesEqual(de_filename, path_component_start, path_component_length))
{
continue;
}
}
else
{
// compare filename
if (!FilenamesEqual(de_filename, path_component_start, path_component_length) ||
de_filename[path_component_length] != ';')
{
continue;
}
}
// found it. is this the file we're looking for?
if (*path_component_end == '\0')
return *de;
// if it is a directory, recurse into it
if (de->flags & ISODirectoryEntryFlag_Directory)
return LocateFile(path_component_end, sector_buffer, de->location_le, de->length_le);
// we're looking for a directory but got a file
Log_ErrorPrintf("Looking for directory but got file");
return std::nullopt;
}
}
std::string temp(path_component_start, path_component_length);
Log_ErrorPrintf("Path component '%s' not found", temp.c_str());
return std::nullopt;
}
std::vector<std::string> ISOReader::GetFilesInDirectory(const char* path)
{
std::string base_path = path;
u32 directory_record_lba;
u32 directory_record_length;
if (base_path.empty())
{
// root directory
const ISODirectoryEntry* root_de = reinterpret_cast<const ISODirectoryEntry*>(m_pvd.root_directory_entry);
directory_record_lba = root_de->location_le;
directory_record_length = root_de->length_le;
}
else
{
auto directory_de = LocateFile(base_path.c_str());
if (!directory_de)
{
Log_ErrorPrintf("Directory entry not found for '%s'", path);
return {};
}
if ((directory_de->flags & ISODirectoryEntryFlag_Directory) == 0)
{
Log_ErrorPrintf("Path '%s' is not a directory, can't list", path);
return {};
}
directory_record_lba = directory_de->location_le;
directory_record_length = directory_de->length_le;
if (base_path[base_path.size() - 1] != '/')
base_path += '/';
}
// start reading directory entries
const u32 num_sectors = (directory_record_length + (SECTOR_SIZE - 1)) / SECTOR_SIZE;
if (!m_image->Seek(m_track_number, directory_record_lba))
{
Log_ErrorPrintf("Seek to LBA %u failed", directory_record_lba);
return {};
}
std::vector<std::string> files;
u8 sector_buffer[SECTOR_SIZE];
for (u32 i = 0; i < num_sectors; i++)
{
if (m_image->Read(CDImage::ReadMode::DataOnly, 1, sector_buffer) != 1)
{
Log_ErrorPrintf("Failed to read LBA %u", directory_record_lba + i);
break;
}
u32 sector_offset = 0;
while ((sector_offset + sizeof(ISODirectoryEntry)) < SECTOR_SIZE)
{
const ISODirectoryEntry* de = reinterpret_cast<const ISODirectoryEntry*>(&sector_buffer[sector_offset]);
const char* de_filename =
reinterpret_cast<const char*>(&sector_buffer[sector_offset + sizeof(ISODirectoryEntry)]);
if ((sector_offset + de->entry_length) > SECTOR_SIZE || de->filename_length > de->entry_length ||
de->entry_length < sizeof(ISODirectoryEntry))
{
break;
}
sector_offset += de->entry_length;
// skip current/parent directory
if (de->filename_length == 1 && (*de_filename == '\x0' || *de_filename == '\x1'))
continue;
// strip off terminator/file version
std::string filename(de_filename, de->filename_length);
std::string::size_type pos = filename.rfind(';');
if (pos == std::string::npos)
{
Log_ErrorPrintf("Invalid filename '%s'", filename.c_str());
continue;
}
filename.erase(pos);
if (!filename.empty())
files.push_back(base_path + filename);
}
}
return files;
}
bool ISOReader::ReadFile(const char* path, std::vector<u8>* data)
{
auto de = LocateFile(path);
if (!de)
{
Log_ErrorPrintf("File not found: '%s'", path);
return false;
}
if (de->flags & ISODirectoryEntryFlag_Directory)
{
Log_ErrorPrintf("File is a directory: '%s'", path);
return false;
}
if (!m_image->Seek(m_track_number, de->location_le))
return false;
if (de->length_le == 0)
{
data->clear();
return true;
}
const u32 num_sectors = (de->length_le + (SECTOR_SIZE - 1)) / SECTOR_SIZE;
data->resize(num_sectors * u64(SECTOR_SIZE));
if (m_image->Read(CDImage::ReadMode::DataOnly, num_sectors, data->data()) != num_sectors)
return false;
data->resize(de->length_le);
return true;
}

155
src/util/iso_reader.h Normal file
View File

@ -0,0 +1,155 @@
#pragma once
#include "common/types.h"
#include <memory>
#include <optional>
#include <string>
#include <vector>
class CDImage;
class ISOReader
{
public:
enum : u32
{
SECTOR_SIZE = 2048
};
#pragma pack(push, 1)
struct ISOVolumeDescriptorHeader
{
u8 type_code;
char standard_identifier[5];
u8 version;
};
static_assert(sizeof(ISOVolumeDescriptorHeader) == 7);
struct ISOBootRecord
{
ISOVolumeDescriptorHeader header;
char boot_system_identifier[32];
char boot_identifier[32];
u8 data[1977];
};
static_assert(sizeof(ISOBootRecord) == 2048);
struct ISOPVDDateTime
{
char year[4];
char month[2];
char day[2];
char hour[2];
char minute[2];
char second[2];
char milliseconds[2];
s8 gmt_offset;
};
static_assert(sizeof(ISOPVDDateTime) == 17);
struct ISOPrimaryVolumeDescriptor
{
ISOVolumeDescriptorHeader header;
u8 unused;
char system_identifier[32];
char volume_identifier[32];
char unused2[8];
u32 total_sectors_le;
u32 total_sectors_be;
char unused3[32];
u16 volume_set_size_le;
u16 volume_set_size_be;
u16 volume_sequence_number_le;
u16 volume_sequence_number_be;
u16 block_size_le;
u16 block_size_be;
u32 path_table_size_le;
u32 path_table_size_be;
u32 path_table_location_le;
u32 optional_path_table_location_le;
u32 path_table_location_be;
u32 optional_path_table_location_be;
u8 root_directory_entry[34];
char volume_set_identifier[128];
char publisher_identifier[128];
char data_preparer_identifier[128];
char application_identifier[128];
char copyright_file_identifier[38];
char abstract_file_identifier[36];
char bibliographic_file_identifier[37];
ISOPVDDateTime volume_creation_time;
ISOPVDDateTime volume_modification_time;
ISOPVDDateTime volume_expiration_time;
ISOPVDDateTime volume_effective_time;
u8 structure_version;
u8 unused4;
u8 application_used[512];
u8 reserved[653];
};
static_assert(sizeof(ISOPrimaryVolumeDescriptor) == 2048);
struct ISODirectoryEntryDateTime
{
u8 years_since_1900;
u8 month;
u8 day;
u8 hour;
u8 minute;
u8 second;
s8 gmt_offset;
};
enum ISODirectoryEntryFlags : u8
{
ISODirectoryEntryFlag_Hidden = (1 << 0),
ISODirectoryEntryFlag_Directory = (1 << 1),
ISODirectoryEntryFlag_AssociatedFile = (1 << 2),
ISODirectoryEntryFlag_ExtendedAttributePresent = (1 << 3),
ISODirectoryEntryFlag_OwnerGroupPermissions = (1 << 4),
ISODirectoryEntryFlag_MoreExtents = (1 << 7),
};
struct ISODirectoryEntry
{
u8 entry_length;
u8 extended_attribute_length;
u32 location_le;
u32 location_be;
u32 length_le;
u32 length_be;
ISODirectoryEntryDateTime recoding_time;
ISODirectoryEntryFlags flags;
u8 interleaved_unit_size;
u8 interleaved_gap_size;
u16 sequence_le;
u16 sequence_be;
u8 filename_length;
};
#pragma pack(pop)
ISOReader();
~ISOReader();
ALWAYS_INLINE const CDImage* GetImage() const { return m_image; }
ALWAYS_INLINE u32 GetTrackNumber() const { return m_track_number; }
ALWAYS_INLINE const ISOPrimaryVolumeDescriptor& GetPVD() const { return m_pvd; }
bool Open(CDImage* image, u32 track_number);
std::vector<std::string> GetFilesInDirectory(const char* path);
bool ReadFile(const char* path, std::vector<u8>* data);
private:
bool ReadPVD();
std::optional<ISODirectoryEntry> LocateFile(const char* path);
std::optional<ISODirectoryEntry> LocateFile(const char* path, u8* sector_buffer, u32 directory_record_lba,
u32 directory_record_size);
CDImage* m_image;
u32 m_track_number;
ISOPrimaryVolumeDescriptor m_pvd = {};
};

View File

@ -0,0 +1,312 @@
#include "jit_code_buffer.h"
#include "common/align.h"
#include "common/assert.h"
#include "common/log.h"
#include "common/platform.h"
#include <algorithm>
Log_SetChannel(JitCodeBuffer);
#if defined(_WIN32)
#include "common/windows_headers.h"
#else
#include <errno.h>
#include <sys/mman.h>
#endif
#if defined(__APPLE__) && defined(__aarch64__)
// pthread_jit_write_protect_np()
#include <pthread.h>
#endif
JitCodeBuffer::JitCodeBuffer() = default;
JitCodeBuffer::JitCodeBuffer(u32 size, u32 far_code_size)
{
if (!Allocate(size, far_code_size))
Panic("Failed to allocate code space");
}
JitCodeBuffer::JitCodeBuffer(void* buffer, u32 size, u32 far_code_size, u32 guard_pages)
{
if (!Initialize(buffer, size, far_code_size))
Panic("Failed to initialize code space");
}
JitCodeBuffer::~JitCodeBuffer()
{
Destroy();
}
bool JitCodeBuffer::Allocate(u32 size /* = 64 * 1024 * 1024 */, u32 far_code_size /* = 0 */)
{
Destroy();
m_total_size = size + far_code_size;
#if defined(_WIN32)
#if !defined(_UWP)
m_code_ptr = static_cast<u8*>(VirtualAlloc(nullptr, m_total_size, MEM_COMMIT, PAGE_EXECUTE_READWRITE));
#else
m_code_ptr = static_cast<u8*>(
VirtualAlloc2FromApp(GetCurrentProcess(), nullptr, m_total_size, MEM_COMMIT, PAGE_READWRITE, nullptr, 0));
if (m_code_ptr)
{
ULONG old_protection;
if (!VirtualProtectFromApp(m_code_ptr, m_total_size, PAGE_EXECUTE_READWRITE, &old_protection))
{
VirtualFree(m_code_ptr, m_total_size, MEM_RELEASE);
return false;
}
}
#endif
if (!m_code_ptr)
{
Log_ErrorPrintf("VirtualAlloc(RWX, %u) for internal buffer failed: %u", m_total_size, GetLastError());
return false;
}
#elif defined(__linux__) || defined(__ANDROID__) || defined(__APPLE__) || defined(__HAIKU__) || defined(__FreeBSD__)
int flags = MAP_PRIVATE | MAP_ANONYMOUS;
#if defined(__APPLE__) && defined(__aarch64__)
// MAP_JIT and toggleable write protection is required on Apple Silicon.
flags |= MAP_JIT;
#endif
m_code_ptr = static_cast<u8*>(mmap(nullptr, m_total_size, PROT_READ | PROT_WRITE | PROT_EXEC, flags, -1, 0));
if (!m_code_ptr)
{
Log_ErrorPrintf("mmap(RWX, %u) for internal buffer failed: %d", m_total_size, errno);
return false;
}
#else
return false;
#endif
m_free_code_ptr = m_code_ptr;
m_code_size = size;
m_code_used = 0;
m_far_code_ptr = static_cast<u8*>(m_code_ptr) + size;
m_free_far_code_ptr = m_far_code_ptr;
m_far_code_size = far_code_size;
m_far_code_used = 0;
m_old_protection = 0;
m_owns_buffer = true;
return true;
}
bool JitCodeBuffer::Initialize(void* buffer, u32 size, u32 far_code_size /* = 0 */, u32 guard_size /* = 0 */)
{
Destroy();
if ((far_code_size > 0 && guard_size >= far_code_size) || (far_code_size + (guard_size * 2)) > size)
return false;
#if defined(_WIN32)
DWORD old_protect = 0;
if (!VirtualProtect(buffer, size, PAGE_EXECUTE_READWRITE, &old_protect))
{
Log_ErrorPrintf("VirtualProtect(RWX) for external buffer failed: %u", GetLastError());
return false;
}
if (guard_size > 0)
{
DWORD old_guard_protect = 0;
u8* guard_at_end = (static_cast<u8*>(buffer) + size) - guard_size;
if (!VirtualProtect(buffer, guard_size, PAGE_NOACCESS, &old_guard_protect) ||
!VirtualProtect(guard_at_end, guard_size, PAGE_NOACCESS, &old_guard_protect))
{
Log_ErrorPrintf("VirtualProtect(NOACCESS) for guard page failed: %u", GetLastError());
return false;
}
}
m_code_ptr = static_cast<u8*>(buffer);
m_old_protection = static_cast<u32>(old_protect);
#elif defined(__linux__) || defined(__ANDROID__) || defined(__APPLE__) || defined(__HAIKU__) || defined(__FreeBSD__)
if (mprotect(buffer, size, PROT_READ | PROT_WRITE | PROT_EXEC) != 0)
{
Log_ErrorPrintf("mprotect(RWX) for external buffer failed: %d", errno);
return false;
}
if (guard_size > 0)
{
u8* guard_at_end = (static_cast<u8*>(buffer) + size) - guard_size;
if (mprotect(buffer, guard_size, PROT_NONE) != 0 || mprotect(guard_at_end, guard_size, PROT_NONE) != 0)
{
Log_ErrorPrintf("mprotect(NONE) for guard page failed: %d", errno);
return false;
}
}
// reasonable default?
m_code_ptr = static_cast<u8*>(buffer);
m_old_protection = PROT_READ | PROT_WRITE;
#else
m_code_ptr = nullptr;
#endif
if (!m_code_ptr)
return false;
m_total_size = size;
m_free_code_ptr = m_code_ptr + guard_size;
m_code_size = size - far_code_size - (guard_size * 2);
m_code_used = 0;
m_far_code_ptr = static_cast<u8*>(m_code_ptr) + m_code_size;
m_free_far_code_ptr = m_far_code_ptr;
m_far_code_size = far_code_size - guard_size;
m_far_code_used = 0;
m_guard_size = guard_size;
m_owns_buffer = false;
return true;
}
void JitCodeBuffer::Destroy()
{
if (m_owns_buffer)
{
#if defined(_WIN32)
if (!VirtualFree(m_code_ptr, 0, MEM_RELEASE))
Log_ErrorPrintf("Failed to free code pointer %p", m_code_ptr);
#elif defined(__linux__) || defined(__ANDROID__) || defined(__APPLE__) || defined(__HAIKU__) || defined(__FreeBSD__)
if (munmap(m_code_ptr, m_total_size) != 0)
Log_ErrorPrintf("Failed to free code pointer %p", m_code_ptr);
#endif
}
else if (m_code_ptr)
{
#if defined(_WIN32)
DWORD old_protect = 0;
if (!VirtualProtect(m_code_ptr, m_total_size, m_old_protection, &old_protect))
Log_ErrorPrintf("Failed to restore protection on %p", m_code_ptr);
#else
if (mprotect(m_code_ptr, m_total_size, m_old_protection) != 0)
Log_ErrorPrintf("Failed to restore protection on %p", m_code_ptr);
#endif
}
m_code_ptr = nullptr;
m_free_code_ptr = nullptr;
m_code_size = 0;
m_code_reserve_size = 0;
m_code_used = 0;
m_far_code_ptr = nullptr;
m_free_far_code_ptr = nullptr;
m_far_code_size = 0;
m_far_code_used = 0;
m_total_size = 0;
m_guard_size = 0;
m_old_protection = 0;
m_owns_buffer = false;
}
void JitCodeBuffer::ReserveCode(u32 size)
{
Assert(m_code_used == 0);
Assert(size < m_code_size);
m_code_reserve_size += size;
m_free_code_ptr += size;
m_code_size -= size;
}
void JitCodeBuffer::CommitCode(u32 length)
{
if (length == 0)
return;
#if defined(CPU_AARCH32) || defined(CPU_AARCH64)
// ARM instruction and data caches are not coherent, we need to flush after every block.
FlushInstructionCache(m_free_code_ptr, length);
#endif
Assert(length <= (m_code_size - m_code_used));
m_free_code_ptr += length;
m_code_used += length;
}
void JitCodeBuffer::CommitFarCode(u32 length)
{
if (length == 0)
return;
#if defined(CPU_AARCH32) || defined(CPU_AARCH64)
// ARM instruction and data caches are not coherent, we need to flush after every block.
FlushInstructionCache(m_free_far_code_ptr, length);
#endif
Assert(length <= (m_far_code_size - m_far_code_used));
m_free_far_code_ptr += length;
m_far_code_used += length;
}
void JitCodeBuffer::Reset()
{
WriteProtect(false);
m_free_code_ptr = m_code_ptr + m_guard_size + m_code_reserve_size;
m_code_used = 0;
std::memset(m_free_code_ptr, 0, m_code_size);
FlushInstructionCache(m_free_code_ptr, m_code_size);
if (m_far_code_size > 0)
{
m_free_far_code_ptr = m_far_code_ptr;
m_far_code_used = 0;
std::memset(m_free_far_code_ptr, 0, m_far_code_size);
FlushInstructionCache(m_free_far_code_ptr, m_far_code_size);
}
WriteProtect(true);
}
void JitCodeBuffer::Align(u32 alignment, u8 padding_value)
{
DebugAssert(Common::IsPow2(alignment));
const u32 num_padding_bytes =
std::min(static_cast<u32>(Common::AlignUpPow2(reinterpret_cast<uintptr_t>(m_free_code_ptr), alignment) -
reinterpret_cast<uintptr_t>(m_free_code_ptr)),
GetFreeCodeSpace());
std::memset(m_free_code_ptr, padding_value, num_padding_bytes);
m_free_code_ptr += num_padding_bytes;
m_code_used += num_padding_bytes;
}
void JitCodeBuffer::FlushInstructionCache(void* address, u32 size)
{
#if defined(_WIN32)
::FlushInstructionCache(GetCurrentProcess(), address, size);
#elif defined(__GNUC__) || defined(__clang__)
__builtin___clear_cache(reinterpret_cast<char*>(address), reinterpret_cast<char*>(address) + size);
#else
#error Unknown platform.
#endif
}
#if defined(__APPLE__) && defined(__aarch64__)
void JitCodeBuffer::WriteProtect(bool enabled)
{
static bool initialized = false;
static bool needs_write_protect = false;
if (!initialized)
{
initialized = true;
needs_write_protect = (pthread_jit_write_protect_supported_np() != 0);
if (needs_write_protect)
Log_InfoPrint("pthread_jit_write_protect_np() will be used before writing to JIT space.");
}
if (!needs_write_protect)
return;
pthread_jit_write_protect_np(enabled ? 1 : 0);
}
#endif

View File

@ -0,0 +1,61 @@
#pragma once
#include "common/types.h"
class JitCodeBuffer
{
public:
JitCodeBuffer();
JitCodeBuffer(u32 size, u32 far_code_size);
JitCodeBuffer(void* buffer, u32 size, u32 far_code_size, u32 guard_size);
~JitCodeBuffer();
bool IsValid() const { return (m_code_ptr != nullptr); }
bool Allocate(u32 size = 64 * 1024 * 1024, u32 far_code_size = 0);
bool Initialize(void* buffer, u32 size, u32 far_code_size = 0, u32 guard_size = 0);
void Destroy();
void Reset();
ALWAYS_INLINE u8* GetCodePointer() const { return m_code_ptr; }
ALWAYS_INLINE u32 GetTotalSize() const { return m_total_size; }
ALWAYS_INLINE u8* GetFreeCodePointer() const { return m_free_code_ptr; }
ALWAYS_INLINE u32 GetFreeCodeSpace() const { return static_cast<u32>(m_code_size - m_code_used); }
void ReserveCode(u32 size);
void CommitCode(u32 length);
ALWAYS_INLINE u8* GetFreeFarCodePointer() const { return m_free_far_code_ptr; }
ALWAYS_INLINE u32 GetFreeFarCodeSpace() const { return static_cast<u32>(m_far_code_size - m_far_code_used); }
void CommitFarCode(u32 length);
/// Adjusts the free code pointer to the specified alignment, padding with bytes.
/// Assumes alignment is a power-of-two.
void Align(u32 alignment, u8 padding_value);
/// Flushes the instruction cache on the host for the specified range.
static void FlushInstructionCache(void* address, u32 size);
/// For Apple Silicon - Toggles write protection on the JIT space.
#if defined(__APPLE__) && defined(__aarch64__)
static void WriteProtect(bool enabled);
#else
ALWAYS_INLINE static void WriteProtect(bool enabled) {}
#endif
private:
u8* m_code_ptr = nullptr;
u8* m_free_code_ptr = nullptr;
u32 m_code_size = 0;
u32 m_code_reserve_size = 0;
u32 m_code_used = 0;
u8* m_far_code_ptr = nullptr;
u8* m_free_far_code_ptr = nullptr;
u32 m_far_code_size = 0;
u32 m_far_code_used = 0;
u32 m_total_size = 0;
u32 m_guard_size = 0;
u32 m_old_protection = 0;
bool m_owns_buffer = false;
};

410
src/util/memory_arena.cpp Normal file
View File

@ -0,0 +1,410 @@
#include "memory_arena.h"
#include "common/assert.h"
#include "common/log.h"
#include "common/string_util.h"
Log_SetChannel(Common::MemoryArena);
#if defined(_WIN32)
#include "common/windows_headers.h"
#elif defined(ANDROID)
#include <dlfcn.h>
#include <fcntl.h>
#include <linux/ashmem.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
#include <cerrno>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#endif
namespace Common {
// Borrowed from Dolphin
#ifdef ANDROID
#define ASHMEM_DEVICE "/dev/ashmem"
static int AshmemCreateFileMapping(const char* name, size_t size)
{
// ASharedMemory path - works on API >= 26 and falls through on API < 26:
// We can't call ASharedMemory_create the normal way without increasing the
// minimum version requirement to API 26, so we use dlopen/dlsym instead
static void* libandroid = dlopen("libandroid.so", RTLD_LAZY | RTLD_LOCAL);
static auto shared_memory_create =
reinterpret_cast<int (*)(const char*, size_t)>(dlsym(libandroid, "ASharedMemory_create"));
if (shared_memory_create)
return shared_memory_create(name, size);
// /dev/ashmem path - works on API < 29:
int fd, ret;
fd = open(ASHMEM_DEVICE, O_RDWR);
if (fd < 0)
return fd;
// We don't really care if we can't set the name, it is optional
ioctl(fd, ASHMEM_SET_NAME, name);
ret = ioctl(fd, ASHMEM_SET_SIZE, size);
if (ret < 0)
{
close(fd);
Log_ErrorPrintf("Ashmem returned error: 0x%08x", ret);
return ret;
}
return fd;
}
#endif
MemoryArena::MemoryArena() = default;
MemoryArena::~MemoryArena()
{
Destroy();
}
void* MemoryArena::FindBaseAddressForMapping(size_t size)
{
void* base_address;
#if defined(_WIN32)
base_address = VirtualAlloc(nullptr, size, MEM_RESERVE, PAGE_READWRITE);
if (base_address)
VirtualFree(base_address, 0, MEM_RELEASE);
#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
base_address = mmap(nullptr, size, PROT_NONE, MAP_ANON | MAP_PRIVATE, -1, 0);
if (base_address)
munmap(base_address, size);
#elif defined(__ANDROID__)
base_address = mmap(nullptr, size, PROT_NONE, MAP_ANON | MAP_SHARED, -1, 0);
if (base_address)
munmap(base_address, size);
#else
base_address = nullptr;
#endif
if (!base_address)
{
Log_ErrorPrintf("Failed to get base address for memory mapping of size %zu", size);
return nullptr;
}
return base_address;
}
bool MemoryArena::IsValid() const
{
#if defined(_WIN32)
return m_file_handle != nullptr;
#else
return m_shmem_fd >= 0;
#endif
}
static std::string GetFileMappingName()
{
#if defined(_WIN32)
const unsigned pid = GetCurrentProcessId();
#elif defined(__ANDROID__) || defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
const unsigned pid = static_cast<unsigned>(getpid());
#else
#error Unknown platform.
#endif
const std::string ret(StringUtil::StdStringFromFormat("duckstation_%u", pid));
Log_InfoPrintf("File mapping name: %s", ret.c_str());
return ret;
}
bool MemoryArena::Create(size_t size, bool writable, bool executable)
{
if (IsValid())
Destroy();
const std::string file_mapping_name(GetFileMappingName());
#if defined(_WIN32)
const DWORD protect = (writable ? (executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE) : PAGE_READONLY);
#ifndef _UWP
m_file_handle = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, protect, Truncate32(size >> 32), Truncate32(size),
file_mapping_name.c_str());
#else
m_file_handle = CreateFileMappingFromApp(INVALID_HANDLE_VALUE, nullptr, protect, size,
StringUtil::UTF8StringToWideString(file_mapping_name).c_str());
#endif
if (!m_file_handle)
{
Log_ErrorPrintf("CreateFileMapping failed: %u", GetLastError());
return false;
}
m_size = size;
m_writable = writable;
m_executable = executable;
return true;
#elif defined(__ANDROID__)
m_shmem_fd = AshmemCreateFileMapping(file_mapping_name.c_str(), size);
if (m_shmem_fd < 0)
{
Log_ErrorPrintf("AshmemCreateFileMapping failed: %d %d", m_shmem_fd, errno);
return false;
}
m_size = size;
m_writable = writable;
m_executable = executable;
return true;
#elif defined(__linux__)
m_shmem_fd = shm_open(file_mapping_name.c_str(), O_CREAT | O_EXCL | (writable ? O_RDWR : O_RDONLY), 0600);
if (m_shmem_fd < 0)
{
Log_ErrorPrintf("shm_open failed: %d", errno);
return false;
}
// we're not going to be opening this mapping in other processes, so remove the file
shm_unlink(file_mapping_name.c_str());
// ensure it's the correct size
if (ftruncate64(m_shmem_fd, static_cast<off64_t>(size)) < 0)
{
Log_ErrorPrintf("ftruncate64(%zu) failed: %d", size, errno);
return false;
}
m_size = size;
m_writable = writable;
m_executable = executable;
return true;
#elif defined(__APPLE__) || defined(__FreeBSD__)
#if defined(__APPLE__)
m_shmem_fd = shm_open(file_mapping_name.c_str(), O_CREAT | O_EXCL | (writable ? O_RDWR : O_RDONLY), 0600);
#else
m_shmem_fd = shm_open(SHM_ANON, O_CREAT | O_EXCL | (writable ? O_RDWR : O_RDONLY), 0600);
#endif
if (m_shmem_fd < 0)
{
Log_ErrorPrintf("shm_open failed: %d", errno);
return false;
}
#ifdef __APPLE__
// we're not going to be opening this mapping in other processes, so remove the file
shm_unlink(file_mapping_name.c_str());
#endif
// ensure it's the correct size
if (ftruncate(m_shmem_fd, static_cast<off_t>(size)) < 0)
{
Log_ErrorPrintf("ftruncate(%zu) failed: %d", size, errno);
return false;
}
m_size = size;
m_writable = writable;
m_executable = executable;
return true;
#else
return false;
#endif
}
void MemoryArena::Destroy()
{
#if defined(_WIN32)
if (m_file_handle)
{
CloseHandle(m_file_handle);
m_file_handle = nullptr;
}
#elif defined(__linux__) || defined(__FreeBSD__)
if (m_shmem_fd > 0)
{
close(m_shmem_fd);
m_shmem_fd = -1;
}
#endif
}
std::optional<MemoryArena::View> MemoryArena::CreateView(size_t offset, size_t size, bool writable, bool executable,
void* fixed_address)
{
void* base_pointer = CreateViewPtr(offset, size, writable, executable, fixed_address);
if (!base_pointer)
return std::nullopt;
return View(this, base_pointer, offset, size, writable);
}
std::optional<MemoryArena::View> MemoryArena::CreateReservedView(size_t size, void* fixed_address /*= nullptr*/)
{
void* base_pointer = CreateReservedPtr(size, fixed_address);
if (!base_pointer)
return std::nullopt;
return View(this, base_pointer, View::RESERVED_REGION_OFFSET, size, false);
}
void* MemoryArena::CreateViewPtr(size_t offset, size_t size, bool writable, bool executable,
void* fixed_address /*= nullptr*/)
{
void* base_pointer;
#if defined(_WIN32)
const DWORD desired_access = FILE_MAP_READ | (writable ? FILE_MAP_WRITE : 0) | (executable ? FILE_MAP_EXECUTE : 0);
#ifndef _UWP
base_pointer =
MapViewOfFileEx(m_file_handle, desired_access, Truncate32(offset >> 32), Truncate32(offset), size, fixed_address);
#else
// UWP does not support fixed mappings.
if (!fixed_address)
base_pointer = MapViewOfFileFromApp(m_file_handle, desired_access, offset, size);
else
base_pointer = nullptr;
#endif
if (!base_pointer)
return nullptr;
#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
const int flags = (fixed_address != nullptr) ? (MAP_SHARED | MAP_FIXED) : MAP_SHARED;
const int prot = PROT_READ | (writable ? PROT_WRITE : 0) | (executable ? PROT_EXEC : 0);
base_pointer = mmap(fixed_address, size, prot, flags, m_shmem_fd, static_cast<off_t>(offset));
if (base_pointer == reinterpret_cast<void*>(-1))
return nullptr;
#else
return nullptr;
#endif
m_num_views.fetch_add(1);
return base_pointer;
}
bool MemoryArena::FlushViewPtr(void* address, size_t size)
{
#if defined(_WIN32)
return FlushViewOfFile(address, size);
#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
return (msync(address, size, 0) >= 0);
#else
return false;
#endif
}
bool MemoryArena::ReleaseViewPtr(void* address, size_t size)
{
bool result;
#if defined(_WIN32)
result = static_cast<bool>(UnmapViewOfFile(address));
#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
result = (munmap(address, size) >= 0);
#else
result = false;
#endif
if (!result)
{
Log_ErrorPrintf("Failed to unmap previously-created view at %p", address);
return false;
}
const size_t prev_count = m_num_views.fetch_sub(1);
Assert(prev_count > 0);
return true;
}
void* MemoryArena::CreateReservedPtr(size_t size, void* fixed_address /*= nullptr*/)
{
void* base_pointer;
#if defined(_WIN32)
base_pointer = VirtualAlloc(fixed_address, size, MEM_RESERVE, PAGE_NOACCESS);
#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
const int flags =
(fixed_address != nullptr) ? (MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED) : (MAP_PRIVATE | MAP_ANONYMOUS);
base_pointer = mmap(fixed_address, size, PROT_NONE, flags, -1, 0);
if (base_pointer == reinterpret_cast<void*>(-1))
return nullptr;
#else
return nullptr;
#endif
m_num_views.fetch_add(1);
return base_pointer;
}
bool MemoryArena::ReleaseReservedPtr(void* address, size_t size)
{
bool result;
#if defined(_WIN32)
result = static_cast<bool>(VirtualFree(address, 0, MEM_RELEASE));
#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
result = (munmap(address, size) >= 0);
#else
result = false;
#endif
if (!result)
{
Log_ErrorPrintf("Failed to release previously-created view at %p", address);
return false;
}
const size_t prev_count = m_num_views.fetch_sub(1);
Assert(prev_count > 0);
return true;
}
bool MemoryArena::SetPageProtection(void* address, size_t length, bool readable, bool writable, bool executable)
{
#if defined(_WIN32)
static constexpr DWORD protection_table[2][2][2] = {
{{PAGE_NOACCESS, PAGE_EXECUTE}, {PAGE_WRITECOPY, PAGE_EXECUTE_WRITECOPY}},
{{PAGE_READONLY, PAGE_EXECUTE_READ}, {PAGE_READWRITE, PAGE_EXECUTE_READWRITE}}};
DWORD old_protect;
return static_cast<bool>(
VirtualProtect(address, length, protection_table[readable][writable][executable], &old_protect));
#elif defined(__linux__) || defined(__ANDROID__) || defined(__APPLE__) || defined(__FreeBSD__)
const int prot = (readable ? PROT_READ : 0) | (writable ? PROT_WRITE : 0) | (executable ? PROT_EXEC : 0);
return (mprotect(address, length, prot) >= 0);
#else
return false;
#endif
}
MemoryArena::View::View(MemoryArena* parent, void* base_pointer, size_t arena_offset, size_t mapping_size,
bool writable)
: m_parent(parent), m_base_pointer(base_pointer), m_arena_offset(arena_offset), m_mapping_size(mapping_size),
m_writable(writable)
{
}
MemoryArena::View::View(View&& view)
: m_parent(view.m_parent), m_base_pointer(view.m_base_pointer), m_arena_offset(view.m_arena_offset),
m_mapping_size(view.m_mapping_size)
{
view.m_parent = nullptr;
view.m_base_pointer = nullptr;
view.m_arena_offset = 0;
view.m_mapping_size = 0;
}
MemoryArena::View::~View()
{
if (m_parent)
{
if (m_arena_offset != RESERVED_REGION_OFFSET)
{
if (m_writable && !m_parent->FlushViewPtr(m_base_pointer, m_mapping_size))
Panic("Failed to flush previously-created view");
if (!m_parent->ReleaseViewPtr(m_base_pointer, m_mapping_size))
Panic("Failed to unmap previously-created view");
}
else
{
if (!m_parent->ReleaseReservedPtr(m_base_pointer, m_mapping_size))
Panic("Failed to release previously-created view");
}
}
}
} // namespace Common

74
src/util/memory_arena.h Normal file
View File

@ -0,0 +1,74 @@
#pragma once
#include "common/types.h"
#include <atomic>
#include <optional>
namespace Common {
class MemoryArena
{
public:
class View
{
public:
enum : size_t
{
RESERVED_REGION_OFFSET = static_cast<size_t>(-1)
};
View(MemoryArena* parent, void* base_pointer, size_t arena_offset, size_t mapping_size, bool writable);
View(View&& view);
~View();
void* GetBasePointer() const { return m_base_pointer; }
size_t GetArenaOffset() const { return m_arena_offset; }
size_t GetMappingSize() const { return m_mapping_size; }
bool IsWritable() const { return m_writable; }
private:
MemoryArena* m_parent;
void* m_base_pointer;
size_t m_arena_offset;
size_t m_mapping_size;
bool m_writable;
};
MemoryArena();
~MemoryArena();
static void* FindBaseAddressForMapping(size_t size);
ALWAYS_INLINE size_t GetSize() const { return m_size; }
ALWAYS_INLINE bool IsWritable() const { return m_writable; }
ALWAYS_INLINE bool IsExecutable() const { return m_executable; }
bool IsValid() const;
bool Create(size_t size, bool writable, bool executable);
void Destroy();
std::optional<View> CreateView(size_t offset, size_t size, bool writable, bool executable,
void* fixed_address = nullptr);
std::optional<View> CreateReservedView(size_t size, void* fixed_address = nullptr);
void* CreateViewPtr(size_t offset, size_t size, bool writable, bool executable, void* fixed_address = nullptr);
bool FlushViewPtr(void* address, size_t size);
bool ReleaseViewPtr(void* address, size_t size);
void* CreateReservedPtr(size_t size, void* fixed_address = nullptr);
bool ReleaseReservedPtr(void* address, size_t size);
static bool SetPageProtection(void* address, size_t length, bool readable, bool writable, bool executable);
private:
#if defined(_WIN32)
void* m_file_handle = nullptr;
#elif defined(__linux__) || defined(ANDROID) || defined(__APPLE__) || defined(__FreeBSD__)
int m_shmem_fd = -1;
#endif
std::atomic_size_t m_num_views{0};
size_t m_size = 0;
bool m_writable = false;
bool m_executable = false;
};
} // namespace Common

View File

@ -0,0 +1,25 @@
#include "null_audio_stream.h"
NullAudioStream::NullAudioStream() = default;
NullAudioStream::~NullAudioStream() = default;
bool NullAudioStream::OpenDevice()
{
return true;
}
void NullAudioStream::PauseDevice(bool paused) {}
void NullAudioStream::CloseDevice() {}
void NullAudioStream::FramesAvailable()
{
// drop any buffer as soon as they're available
DropFrames(GetSamplesAvailable());
}
std::unique_ptr<AudioStream> AudioStream::CreateNullAudioStream()
{
return std::make_unique<NullAudioStream>();
}

View File

@ -0,0 +1,15 @@
#pragma once
#include "audio_stream.h"
class NullAudioStream final : public AudioStream
{
public:
NullAudioStream();
~NullAudioStream();
protected:
bool OpenDevice() override;
void PauseDevice(bool paused) override;
void CloseDevice() override;
void FramesAvailable() override;
};

View File

@ -0,0 +1,461 @@
#include "page_fault_handler.h"
#include "common/log.h"
#include "common/platform.h"
#include <algorithm>
#include <cstring>
#include <mutex>
#include <vector>
Log_SetChannel(Common::PageFaultHandler);
#if defined(_WIN32)
#include "common/windows_headers.h"
#elif defined(__linux__) || defined(__ANDROID__)
#include <signal.h>
#include <ucontext.h>
#include <unistd.h>
#define USE_SIGSEGV 1
#elif defined(__APPLE__) || defined(__FreeBSD__)
#include <signal.h>
#include <unistd.h>
#define USE_SIGSEGV 1
#endif
namespace Common::PageFaultHandler {
struct RegisteredHandler
{
Callback callback;
const void* owner;
void* start_pc;
u32 code_size;
};
static std::vector<RegisteredHandler> m_handlers;
static std::mutex m_handler_lock;
static thread_local bool s_in_handler;
#if defined(CPU_AARCH32)
static bool IsStoreInstruction(const void* ptr)
{
u32 bits;
std::memcpy(&bits, ptr, sizeof(bits));
// TODO
return false;
}
#elif defined(CPU_AARCH64)
static bool IsStoreInstruction(const void* ptr)
{
u32 bits;
std::memcpy(&bits, ptr, sizeof(bits));
// Based on vixl's disassembler Instruction::IsStore().
// if (Mask(LoadStoreAnyFMask) != LoadStoreAnyFixed)
if ((bits & 0x0a000000) != 0x08000000)
return false;
// if (Mask(LoadStorePairAnyFMask) == LoadStorePairAnyFixed)
if ((bits & 0x3a000000) == 0x28000000)
{
// return Mask(LoadStorePairLBit) == 0
return (bits & (1 << 22)) == 0;
}
switch (bits & 0xC4C00000)
{
case 0x00000000: // STRB_w
case 0x40000000: // STRH_w
case 0x80000000: // STR_w
case 0xC0000000: // STR_x
case 0x04000000: // STR_b
case 0x44000000: // STR_h
case 0x84000000: // STR_s
case 0xC4000000: // STR_d
case 0x04800000: // STR_q
return true;
default:
return false;
}
}
#endif
#if defined(_WIN32) && !defined(_UWP) && (defined(CPU_X64) || defined(CPU_AARCH64))
static PVOID s_veh_handle;
static LONG ExceptionHandler(PEXCEPTION_POINTERS exi)
{
if (exi->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION || s_in_handler)
return EXCEPTION_CONTINUE_SEARCH;
s_in_handler = true;
#if defined(_M_AMD64)
void* const exception_pc = reinterpret_cast<void*>(exi->ContextRecord->Rip);
#elif defined(_M_ARM64)
void* const exception_pc = reinterpret_cast<void*>(exi->ContextRecord->Pc);
#else
void* const exception_pc = nullptr;
#endif
void* const exception_address = reinterpret_cast<void*>(exi->ExceptionRecord->ExceptionInformation[1]);
bool const is_write = exi->ExceptionRecord->ExceptionInformation[0] == 1;
std::lock_guard<std::mutex> guard(m_handler_lock);
for (const RegisteredHandler& rh : m_handlers)
{
if (rh.callback(exception_pc, exception_address, is_write) == HandlerResult::ContinueExecution)
{
s_in_handler = false;
return EXCEPTION_CONTINUE_EXECUTION;
}
}
s_in_handler = false;
return EXCEPTION_CONTINUE_SEARCH;
}
u32 GetHandlerCodeSize()
{
return 0;
}
#elif defined(_UWP)
// https://docs.microsoft.com/en-us/cpp/build/exception-handling-x64?view=msvc-160
struct UNWIND_INFO
{
BYTE version : 3;
BYTE flags : 5;
BYTE size_of_prologue;
BYTE count_of_unwind_codes;
BYTE frame_register : 4;
BYTE frame_offset_scaled : 4;
ULONG exception_handler_address;
};
struct UnwindHandler
{
RUNTIME_FUNCTION runtime_function;
UNWIND_INFO unwind_info;
uint8_t exception_handler_code[32];
};
static constexpr size_t UNWIND_HANDLER_ALLOC_SIZE = 4096;
static_assert(sizeof(UnwindHandler) <= UNWIND_HANDLER_ALLOC_SIZE);
static EXCEPTION_DISPOSITION UnwindExceptionHandler(PEXCEPTION_RECORD ExceptionRecord, ULONG64 EstablisherFrame,
PCONTEXT ContextRecord, PDISPATCHER_CONTEXT DispatcherContext)
{
if (s_in_handler)
return ExceptionContinueSearch;
s_in_handler = true;
void* const exception_pc = reinterpret_cast<void*>(DispatcherContext->ControlPc);
void* const exception_address = reinterpret_cast<void*>(ExceptionRecord->ExceptionInformation[1]);
bool const is_write = ExceptionRecord->ExceptionInformation[0] == 1;
std::lock_guard<std::mutex> guard(m_handler_lock);
for (const RegisteredHandler& rh : m_handlers)
{
if (static_cast<const u8*>(exception_pc) >= static_cast<const u8*>(rh.start_pc) &&
static_cast<const u8*>(exception_pc) <= (static_cast<const u8*>(rh.start_pc) + rh.code_size))
{
if (rh.callback(exception_pc, exception_address, is_write) == HandlerResult::ContinueExecution)
{
s_in_handler = false;
return ExceptionContinueExecution;
}
}
}
s_in_handler = false;
return ExceptionContinueSearch;
}
static PRUNTIME_FUNCTION GetRuntimeFunctionCallback(DWORD64 ControlPc, PVOID Context)
{
std::lock_guard<std::mutex> guard(m_handler_lock);
for (const RegisteredHandler& rh : m_handlers)
{
if (ControlPc >= reinterpret_cast<DWORD64>(rh.start_pc) &&
ControlPc <= (reinterpret_cast<DWORD64>(rh.start_pc) + rh.code_size))
{
return reinterpret_cast<PRUNTIME_FUNCTION>(rh.start_pc);
}
}
return nullptr;
}
static bool InstallFunctionTableCallback(const void* owner, void* start_pc, u32 code_size)
{
if (code_size < UNWIND_HANDLER_ALLOC_SIZE)
{
Log_ErrorPrintf("Invalid code size: %u @ %p", code_size, UNWIND_HANDLER_ALLOC_SIZE);
return false;
}
if (!RtlInstallFunctionTableCallback(reinterpret_cast<DWORD64>(owner) | 0x3, reinterpret_cast<DWORD64>(start_pc),
static_cast<DWORD>(code_size), &GetRuntimeFunctionCallback, nullptr, nullptr))
{
Log_ErrorPrintf("RtlInstallFunctionTableCallback() failed: %08X", GetLastError());
return false;
}
// This is only valid on x86 for now.
#ifndef CPU_X64
Log_ErrorPrint("Exception unwind codegen not implemented");
return false;
#else
UnwindHandler* uh = static_cast<UnwindHandler*>(start_pc);
ULONG old_protection;
if (!VirtualProtectFromApp(uh, UNWIND_HANDLER_ALLOC_SIZE, PAGE_READWRITE, &old_protection))
{
Log_ErrorPrintf("VirtualProtectFromApp(RW) for exception handler failed: %08X", GetLastError());
return false;
}
uh->runtime_function.BeginAddress = UNWIND_HANDLER_ALLOC_SIZE;
uh->runtime_function.EndAddress = code_size;
uh->runtime_function.UnwindInfoAddress = offsetof(UnwindHandler, unwind_info);
uh->unwind_info.version = 1;
uh->unwind_info.flags = UNW_FLAG_EHANDLER;
uh->unwind_info.size_of_prologue = 0;
uh->unwind_info.count_of_unwind_codes = 0;
uh->unwind_info.frame_register = 0;
uh->unwind_info.frame_offset_scaled = 0;
uh->unwind_info.exception_handler_address = offsetof(UnwindHandler, exception_handler_code);
// mov rax, handler
const void* handler = UnwindExceptionHandler;
uh->exception_handler_code[0] = 0x48;
uh->exception_handler_code[1] = 0xb8;
std::memcpy(&uh->exception_handler_code[2], &handler, sizeof(handler));
// jmp rax
uh->exception_handler_code[10] = 0xff;
uh->exception_handler_code[11] = 0xe0;
if (!VirtualProtectFromApp(uh, UNWIND_HANDLER_ALLOC_SIZE, PAGE_EXECUTE_READ, &old_protection))
{
Log_ErrorPrintf("VirtualProtectFromApp(RX) for exception handler failed: %08X", GetLastError());
return false;
}
return true;
#endif
}
u32 GetHandlerCodeSize()
{
return UNWIND_HANDLER_ALLOC_SIZE;
}
#elif defined(USE_SIGSEGV)
static struct sigaction s_old_sigsegv_action;
#if defined(__APPLE__) || defined(__aarch64__)
static struct sigaction s_old_sigbus_action;
#endif
static void SIGSEGVHandler(int sig, siginfo_t* info, void* ctx)
{
if ((info->si_code != SEGV_MAPERR && info->si_code != SEGV_ACCERR) || s_in_handler)
return;
#if defined(__linux__) || defined(__ANDROID__)
void* const exception_address = reinterpret_cast<void*>(info->si_addr);
#if defined(CPU_X64)
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext.gregs[REG_RIP]);
const bool is_write = (static_cast<ucontext_t*>(ctx)->uc_mcontext.gregs[REG_ERR] & 2) != 0;
#elif defined(CPU_AARCH32)
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext.arm_pc);
const bool is_write = IsStoreInstruction(exception_pc);
#elif defined(CPU_AARCH64)
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext.pc);
const bool is_write = IsStoreInstruction(exception_pc);
#else
void* const exception_pc = nullptr;
const bool is_write = false;
#endif
#elif defined(__APPLE__)
#if defined(CPU_X64)
void* const exception_address =
reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext->__es.__faultvaddr);
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext->__ss.__rip);
const bool is_write = (static_cast<ucontext_t*>(ctx)->uc_mcontext->__es.__err & 2) != 0;
#elif defined(CPU_AARCH64)
void* const exception_address = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext->__es.__far);
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext->__ss.__pc);
const bool is_write = IsStoreInstruction(exception_pc);
#else
void* const exception_address = reinterpret_cast<void*>(info->si_addr);
void* const exception_pc = nullptr;
const bool is_write = false;
#endif
#elif defined(__FreeBSD__)
#if defined(CPU_X64)
void* const exception_address = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext.mc_addr);
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext.mc_rip);
const bool is_write = (static_cast<ucontext_t*>(ctx)->uc_mcontext.mc_err & 2) != 0;
#elif defined(CPU_AARCH64)
void* const exception_address = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext->__es.__far);
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext->__ss.__pc);
const bool is_write = IsStoreInstruction(exception_pc);
#else
void* const exception_address = reinterpret_cast<void*>(info->si_addr);
void* const exception_pc = nullptr;
const bool is_write = false;
#endif
#endif
std::lock_guard<std::mutex> guard(m_handler_lock);
for (const RegisteredHandler& rh : m_handlers)
{
if (rh.callback(exception_pc, exception_address, is_write) == HandlerResult::ContinueExecution)
{
s_in_handler = false;
return;
}
}
// call old signal handler
#if !defined(__APPLE__) && !defined(__aarch64__)
const struct sigaction& sa = s_old_sigsegv_action;
#else
const struct sigaction& sa = (sig == SIGBUS) ? s_old_sigbus_action : s_old_sigsegv_action;
#endif
if (sa.sa_flags & SA_SIGINFO)
sa.sa_sigaction(sig, info, ctx);
else if (sa.sa_handler == SIG_DFL)
signal(sig, SIG_DFL);
else if (sa.sa_handler == SIG_IGN)
return;
else
sa.sa_handler(sig);
}
u32 GetHandlerCodeSize()
{
return 0;
}
#else
u32 GetHandlerCodeSize()
{
return 0;
}
#endif
bool InstallHandler(const void* owner, void* start_pc, u32 code_size, Callback callback)
{
bool was_empty;
{
std::lock_guard<std::mutex> guard(m_handler_lock);
if (std::find_if(m_handlers.begin(), m_handlers.end(),
[owner](const RegisteredHandler& rh) { return rh.owner == owner; }) != m_handlers.end())
{
return false;
}
was_empty = m_handlers.empty();
}
if (was_empty)
{
#if defined(_WIN32) && !defined(_UWP) && (defined(CPU_X64) || defined(CPU_AARCH64))
s_veh_handle = AddVectoredExceptionHandler(1, ExceptionHandler);
if (!s_veh_handle)
{
Log_ErrorPrint("Failed to add vectored exception handler");
return false;
}
#elif defined(_UWP)
if (!InstallFunctionTableCallback(owner, start_pc, code_size))
{
Log_ErrorPrint("Failed to install function table callback");
return false;
}
#elif defined(USE_SIGSEGV)
struct sigaction sa = {};
sa.sa_sigaction = SIGSEGVHandler;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGSEGV, &sa, &s_old_sigsegv_action) < 0)
{
Log_ErrorPrintf("sigaction(SIGSEGV) failed: %d", errno);
return false;
}
#if defined(__APPLE__) || defined(__aarch64__)
if (sigaction(SIGBUS, &sa, &s_old_sigbus_action) < 0)
{
Log_ErrorPrintf("sigaction(SIGBUS) failed: %d", errno);
return false;
}
#endif
#else
return false;
#endif
}
m_handlers.push_back(RegisteredHandler{callback, owner, start_pc, code_size});
return true;
}
bool RemoveHandler(const void* owner)
{
std::lock_guard<std::mutex> guard(m_handler_lock);
auto it = std::find_if(m_handlers.begin(), m_handlers.end(),
[owner](const RegisteredHandler& rh) { return rh.owner == owner; });
if (it == m_handlers.end())
return false;
m_handlers.erase(it);
if (m_handlers.empty())
{
#if defined(_WIN32) && !defined(_UWP) && (defined(CPU_X64) || defined(CPU_AARCH64))
RemoveVectoredExceptionHandler(s_veh_handle);
s_veh_handle = nullptr;
#elif defined(_UWP)
// nothing to do here, any unregistered regions will be ignored
#elif defined(USE_SIGSEGV)
// restore old signal handler
#if defined(__APPLE__) || defined(__aarch64__)
if (sigaction(SIGBUS, &s_old_sigbus_action, nullptr) < 0)
{
Log_ErrorPrintf("sigaction(SIGBUS) failed: %d", errno);
return false;
}
s_old_sigbus_action = {};
#endif
if (sigaction(SIGSEGV, &s_old_sigsegv_action, nullptr) < 0)
{
Log_ErrorPrintf("sigaction(SIGSEGV) failed: %d", errno);
return false;
}
s_old_sigsegv_action = {};
#else
return false;
#endif
}
return true;
}
} // namespace Common::PageFaultHandler

View File

@ -0,0 +1,19 @@
#pragma once
#include "common/types.h"
namespace Common::PageFaultHandler {
enum class HandlerResult
{
ContinueExecution,
ExecuteNextHandler,
};
using Callback = HandlerResult (*)(void* exception_pc, void* fault_address, bool is_write);
using Handle = void*;
u32 GetHandlerCodeSize();
bool InstallHandler(const void* owner, void* start_pc, u32 code_size, Callback callback);
bool RemoveHandler(const void* owner);
} // namespace Common::PageFaultHandler

110
src/util/pbp_types.h Normal file
View File

@ -0,0 +1,110 @@
#pragma once
#include "common/types.h"
#include <map>
#include <string>
#include <variant>
#include <vector>
namespace PBP {
enum : u32
{
PBP_HEADER_OFFSET_COUNT = 8u,
TOC_NUM_ENTRIES = 102u,
BLOCK_TABLE_NUM_ENTRIES = 32256u,
DISC_TABLE_NUM_ENTRIES = 5u,
DECOMPRESSED_BLOCK_SIZE = 37632u // 2352 bytes per sector * 16 sectors per block
};
#pragma pack(push, 1)
struct PBPHeader
{
u8 magic[4]; // "\0PBP"
u32 version;
union
{
u32 offsets[PBP_HEADER_OFFSET_COUNT];
struct
{
u32 param_sfo_offset; // 0x00000028
u32 icon0_png_offset;
u32 icon1_png_offset;
u32 pic0_png_offset;
u32 pic1_png_offset;
u32 snd0_at3_offset;
u32 data_psp_offset;
u32 data_psar_offset;
};
};
};
static_assert(sizeof(PBPHeader) == 0x28);
struct SFOHeader
{
u8 magic[4]; // "\0PSF"
u32 version;
u32 key_table_offset; // Relative to start of SFOHeader, 0x000000A4 expected
u32 data_table_offset; // Relative to start of SFOHeader, 0x00000100 expected
u32 num_table_entries; // 0x00000009
};
static_assert(sizeof(SFOHeader) == 0x14);
struct SFOIndexTableEntry
{
u16 key_offset; // Relative to key_table_offset
u16 data_type;
u32 data_size; // Size of actual data in bytes
u32 data_total_size; // Size of data field in bytes, data_total_size >= data_size
u32 data_offset; // Relative to data_table_offset
};
static_assert(sizeof(SFOIndexTableEntry) == 0x10);
using SFOIndexTable = std::vector<SFOIndexTableEntry>;
using SFOTableDataValue = std::variant<std::string, u32>;
using SFOTable = std::map<std::string, SFOTableDataValue>;
struct BlockTableEntry
{
u32 offset;
u16 size;
u16 marker;
u8 checksum[0x10];
u64 padding;
};
static_assert(sizeof(BlockTableEntry) == 0x20);
struct TOCEntry
{
struct Timecode
{
u8 m;
u8 s;
u8 f;
};
u8 type;
u8 unknown;
u8 point;
Timecode pregap_start;
u8 zero;
Timecode userdata_start;
};
static_assert(sizeof(TOCEntry) == 0x0A);
#if 0
struct AudioTrackTableEntry
{
u32 block_offset;
u32 block_size;
u32 block_padding;
u32 block_checksum;
};
static_assert(sizeof(CDDATrackTableEntry) == 0x10);
#endif
#pragma pack(pop)
} // namespace PBP

1596
src/util/shiftjis.cpp Normal file

File diff suppressed because it is too large Load Diff

4
src/util/shiftjis.h Normal file
View File

@ -0,0 +1,4 @@
#pragma once
void sjis2ascii(char* bData);
char* sjis2utf8(char* input);

View File

@ -0,0 +1,80 @@
#include "state_wrapper.h"
#include "common/log.h"
#include "common/string.h"
#include <cinttypes>
#include <cstring>
Log_SetChannel(StateWrapper);
StateWrapper::StateWrapper(ByteStream* stream, Mode mode, u32 version)
: m_stream(stream), m_mode(mode), m_version(version)
{
}
StateWrapper::~StateWrapper() = default;
void StateWrapper::DoBytes(void* data, size_t length)
{
if (m_mode == Mode::Read)
{
if (m_error || (m_error |= !m_stream->Read2(data, static_cast<u32>(length))) == true)
std::memset(data, 0, length);
}
else
{
if (!m_error)
m_error |= !m_stream->Write2(data, static_cast<u32>(length));
}
}
void StateWrapper::Do(bool* value_ptr)
{
if (m_mode == Mode::Read)
{
u8 value = 0;
if (!m_error)
m_error |= !m_stream->ReadByte(&value);
*value_ptr = (value != 0);
}
else
{
u8 value = static_cast<u8>(*value_ptr);
if (!m_error)
m_error |= !m_stream->WriteByte(value);
}
}
void StateWrapper::Do(std::string* value_ptr)
{
u32 length = static_cast<u32>(value_ptr->length());
Do(&length);
if (m_mode == Mode::Read)
value_ptr->resize(length);
DoBytes(&(*value_ptr)[0], length);
value_ptr->resize(std::strlen(&(*value_ptr)[0]));
}
void StateWrapper::Do(String* value_ptr)
{
u32 length = static_cast<u32>(value_ptr->GetLength());
Do(&length);
if (m_mode == Mode::Read)
value_ptr->Resize(length);
DoBytes(value_ptr->GetWriteableCharArray(), length);
value_ptr->UpdateSize();
}
bool StateWrapper::DoMarker(const char* marker)
{
SmallString file_value(marker);
Do(&file_value);
if (m_error)
return false;
if (m_mode == Mode::Write || file_value.Compare(marker))
return true;
Log_ErrorPrintf("Marker mismatch at offset %" PRIu64 ": found '%s' expected '%s'", m_stream->GetPosition(),
file_value.GetCharArray(), marker);
return false;
}

208
src/util/state_wrapper.h Normal file
View File

@ -0,0 +1,208 @@
#pragma once
#include "common/byte_stream.h"
#include "common/fifo_queue.h"
#include "common/heap_array.h"
#include "common/types.h"
#include <cstring>
#include <deque>
#include <string>
#include <type_traits>
#include <vector>
class String;
class StateWrapper
{
public:
enum class Mode
{
Read,
Write
};
StateWrapper(ByteStream* stream, Mode mode, u32 version);
StateWrapper(const StateWrapper&) = delete;
~StateWrapper();
ByteStream* GetStream() const { return m_stream; }
bool HasError() const { return m_error; }
bool IsReading() const { return (m_mode == Mode::Read); }
bool IsWriting() const { return (m_mode == Mode::Write); }
Mode GetMode() const { return m_mode; }
void SetMode(Mode mode) { m_mode = mode; }
u32 GetVersion() const { return m_version; }
/// Overload for integral or floating-point types. Writes bytes as-is.
template<typename T, std::enable_if_t<std::is_integral_v<T> || std::is_floating_point_v<T>, int> = 0>
void Do(T* value_ptr)
{
if (m_mode == Mode::Read)
{
if (m_error || (m_error |= !m_stream->Read2(value_ptr, sizeof(T))) == true)
*value_ptr = static_cast<T>(0);
}
else
{
if (!m_error)
m_error |= !m_stream->Write2(value_ptr, sizeof(T));
}
}
/// Overload for enum types. Uses the underlying type.
template<typename T, std::enable_if_t<std::is_enum_v<T>, int> = 0>
void Do(T* value_ptr)
{
using TType = std::underlying_type_t<T>;
if (m_mode == Mode::Read)
{
TType temp;
if (m_error || (m_error |= !m_stream->Read2(&temp, sizeof(TType))) == true)
temp = static_cast<TType>(0);
*value_ptr = static_cast<T>(temp);
}
else
{
TType temp;
std::memcpy(&temp, value_ptr, sizeof(TType));
if (!m_error)
m_error |= !m_stream->Write2(&temp, sizeof(TType));
}
}
/// Overload for POD types, such as structs.
template<typename T, std::enable_if_t<std::is_pod_v<T>, int> = 0>
void DoPOD(T* value_ptr)
{
if (m_mode == Mode::Read)
{
if (m_error || (m_error |= !m_stream->Read2(value_ptr, sizeof(T))) == true)
std::memset(value_ptr, 0, sizeof(*value_ptr));
}
else
{
if (!m_error)
m_error |= !m_stream->Write2(value_ptr, sizeof(T));
}
}
template<typename T>
void DoArray(T* values, size_t count)
{
for (size_t i = 0; i < count; i++)
Do(&values[i]);
}
template<typename T>
void DoPODArray(T* values, size_t count)
{
for (size_t i = 0; i < count; i++)
DoPOD(&values[i]);
}
void DoBytes(void* data, size_t length);
void Do(bool* value_ptr);
void Do(std::string* value_ptr);
void Do(String* value_ptr);
template<typename T, size_t N>
void Do(std::array<T, N>* data)
{
DoArray(data->data(), data->size());
}
template<typename T, size_t N>
void Do(HeapArray<T, N>* data)
{
DoArray(data->data(), data->size());
}
template<typename T>
void Do(std::vector<T>* data)
{
u32 length = static_cast<u32>(data->size());
Do(&length);
if (m_mode == Mode::Read)
data->resize(length);
DoArray(data->data(), data->size());
}
template<typename T>
void Do(std::deque<T>* data)
{
u32 length = static_cast<u32>(data->size());
Do(&length);
if (m_mode == Mode::Read)
{
data->clear();
for (u32 i = 0; i < length; i++)
{
T value;
Do(&value);
data->push_back(value);
}
}
else
{
for (u32 i = 0; i < length; i++)
Do(&data[i]);
}
}
template<typename T, u32 CAPACITY>
void Do(FIFOQueue<T, CAPACITY>* data)
{
u32 size = data->GetSize();
Do(&size);
if (m_mode == Mode::Read)
{
T* temp = new T[size];
DoArray(temp, size);
data->Clear();
data->PushRange(temp, size);
delete[] temp;
}
else
{
for (u32 i = 0; i < size; i++)
{
T temp(data->Peek(i));
Do(&temp);
}
}
}
bool DoMarker(const char* marker);
template<typename T>
void DoEx(T* data, u32 version_introduced, T default_value)
{
if (m_version < version_introduced)
{
*data = std::move(default_value);
return;
}
Do(data);
}
void SkipBytes(size_t count)
{
if (m_mode != Mode::Read)
{
m_error = true;
return;
}
if (!m_error)
m_error = !m_stream->SeekRelative(static_cast<s64>(count));
}
private:
ByteStream* m_stream;
Mode m_mode;
u32 m_version;
bool m_error = false;
};

16
src/util/util.props Normal file
View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\common\common.props" />
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>$(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\libchdr\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<Link>
<AdditionalDependencies>$(RootBuildDir)libchdr\libchdr.lib;$(RootBuildDir)libsamplerate\libsamplerate.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
</Project>

58
src/util/util.vcxproj Normal file
View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\dep\msvc\vsprops\Configurations.props" />
<ItemGroup>
<ClInclude Include="audio_stream.h" />
<ClInclude Include="cd_image.h" />
<ClInclude Include="cd_image_hasher.h" />
<ClInclude Include="cue_parser.h" />
<ClInclude Include="iso_reader.h" />
<ClInclude Include="jit_code_buffer.h" />
<ClInclude Include="null_audio_stream.h" />
<ClInclude Include="pbp_types.h" />
<ClInclude Include="memory_arena.h" />
<ClInclude Include="page_fault_handler.h" />
<ClInclude Include="cd_subchannel_replacement.h" />
<ClInclude Include="shiftjis.h" />
<ClInclude Include="state_wrapper.h" />
<ClInclude Include="cd_xa.h" />
<ClInclude Include="wav_writer.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="audio_stream.cpp" />
<ClCompile Include="cd_image.cpp" />
<ClCompile Include="cd_image_bin.cpp" />
<ClCompile Include="cd_image_chd.cpp" />
<ClCompile Include="cd_image_cue.cpp" />
<ClCompile Include="cd_image_device.cpp" />
<ClCompile Include="cd_image_ecm.cpp" />
<ClCompile Include="cd_image_hasher.cpp" />
<ClCompile Include="cd_image_m3u.cpp" />
<ClCompile Include="cd_image_mds.cpp" />
<ClCompile Include="cd_image_memory.cpp" />
<ClCompile Include="cd_image_pbp.cpp" />
<ClCompile Include="cue_parser.cpp" />
<ClCompile Include="cd_image_ppf.cpp" />
<ClCompile Include="iso_reader.cpp" />
<ClCompile Include="jit_code_buffer.cpp" />
<ClCompile Include="cd_subchannel_replacement.cpp" />
<ClCompile Include="null_audio_stream.cpp" />
<ClCompile Include="shiftjis.cpp" />
<ClCompile Include="memory_arena.cpp" />
<ClCompile Include="page_fault_handler.cpp" />
<ClCompile Include="state_wrapper.cpp" />
<ClCompile Include="cd_xa.cpp" />
<ClCompile Include="wav_writer.cpp" />
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{57F6206D-F264-4B07-BAF8-11B9BBE1F455}</ProjectGuid>
</PropertyGroup>
<Import Project="..\..\dep\msvc\vsprops\StaticLibrary.props" />
<Import Project="util.props" />
<ItemDefinitionGroup>
<ClCompile>
<ObjectFileName>$(IntDir)/%(RelativeDir)/</ObjectFileName>
</ClCompile>
</ItemDefinitionGroup>
<Import Project="..\..\dep\msvc\vsprops\Targets.props" />
</Project>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ClInclude Include="jit_code_buffer.h" />
<ClInclude Include="state_wrapper.h" />
<ClInclude Include="audio_stream.h" />
<ClInclude Include="cd_xa.h" />
<ClInclude Include="iso_reader.h" />
<ClInclude Include="cd_image.h" />
<ClInclude Include="cd_subchannel_replacement.h" />
<ClInclude Include="null_audio_stream.h" />
<ClInclude Include="wav_writer.h" />
<ClInclude Include="cd_image_hasher.h" />
<ClInclude Include="shiftjis.h" />
<ClInclude Include="memory_arena.h" />
<ClInclude Include="page_fault_handler.h" />
<ClInclude Include="pbp_types.h" />
<ClInclude Include="cue_parser.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="jit_code_buffer.cpp" />
<ClCompile Include="state_wrapper.cpp" />
<ClCompile Include="cd_image.cpp" />
<ClCompile Include="audio_stream.cpp" />
<ClCompile Include="cd_xa.cpp" />
<ClCompile Include="cd_image_cue.cpp" />
<ClCompile Include="cd_image_bin.cpp" />
<ClCompile Include="iso_reader.cpp" />
<ClCompile Include="cd_subchannel_replacement.cpp" />
<ClCompile Include="null_audio_stream.cpp" />
<ClCompile Include="cd_image_chd.cpp" />
<ClCompile Include="wav_writer.cpp" />
<ClCompile Include="cd_image_hasher.cpp" />
<ClCompile Include="cd_image_memory.cpp" />
<ClCompile Include="shiftjis.cpp" />
<ClCompile Include="memory_arena.cpp" />
<ClCompile Include="page_fault_handler.cpp" />
<ClCompile Include="cd_image_ecm.cpp" />
<ClCompile Include="cd_image_mds.cpp" />
<ClCompile Include="cd_image_pbp.cpp" />
<ClCompile Include="cd_image_m3u.cpp" />
<ClCompile Include="cue_parser.cpp" />
<ClCompile Include="cd_image_ppf.cpp" />
<ClCompile Include="cd_image_device.cpp" />
</ItemGroup>
</Project>

115
src/util/wav_writer.cpp Normal file
View File

@ -0,0 +1,115 @@
#include "wav_writer.h"
#include "common/file_system.h"
#include "common/log.h"
Log_SetChannel(WAVWriter);
#pragma pack(push, 1)
struct WAV_HEADER
{
u32 chunk_id; // RIFF
u32 chunk_size;
u32 format; // WAVE
struct FormatChunk
{
u32 chunk_id; // "fmt "
u32 chunk_size;
u16 audio_format; // pcm = 1
u16 num_channels;
u32 sample_rate;
u32 byte_rate;
u16 block_align;
u16 bits_per_sample;
} fmt_chunk;
struct DataChunkHeader
{
u32 chunk_id; // "data "
u32 chunk_size;
} data_chunk_header;
};
#pragma pack(pop)
namespace Common {
WAVWriter::WAVWriter() = default;
WAVWriter::~WAVWriter()
{
if (IsOpen())
Close();
}
bool WAVWriter::Open(const char* filename, u32 sample_rate, u32 num_channels)
{
if (IsOpen())
Close();
m_file = FileSystem::OpenCFile(filename, "wb");
if (!m_file)
return false;
m_sample_rate = sample_rate;
m_num_channels = num_channels;
if (!WriteHeader())
{
Log_ErrorPrintf("Failed to write header to file");
m_sample_rate = 0;
m_num_channels = 0;
std::fclose(m_file);
m_file = nullptr;
return false;
}
return true;
}
void WAVWriter::Close()
{
if (!IsOpen())
return;
if (std::fseek(m_file, 0, SEEK_SET) != 0 || !WriteHeader())
Log_ErrorPrintf("Failed to re-write header on file, file may be unplayable");
std::fclose(m_file);
m_file = nullptr;
m_sample_rate = 0;
m_num_channels = 0;
m_num_frames = 0;
}
void WAVWriter::WriteFrames(const s16* samples, u32 num_frames)
{
const u32 num_frames_written =
static_cast<u32>(std::fwrite(samples, sizeof(s16) * m_num_channels, num_frames, m_file));
if (num_frames_written != num_frames)
Log_ErrorPrintf("Only wrote %u of %u frames to output file", num_frames_written, num_frames);
m_num_frames += num_frames_written;
}
bool WAVWriter::WriteHeader()
{
const u32 data_size = sizeof(SampleType) * m_num_channels * m_num_frames;
WAV_HEADER header = {};
header.chunk_id = 0x46464952; // 0x52494646
header.chunk_size = sizeof(WAV_HEADER) - 8 + data_size;
header.format = 0x45564157; // 0x57415645
header.fmt_chunk.chunk_id = 0x20746d66; // 0x666d7420
header.fmt_chunk.chunk_size = sizeof(header.fmt_chunk) - 8;
header.fmt_chunk.audio_format = 1;
header.fmt_chunk.num_channels = static_cast<u16>(m_num_channels);
header.fmt_chunk.sample_rate = m_sample_rate;
header.fmt_chunk.byte_rate = m_sample_rate * m_num_channels * sizeof(SampleType);
header.fmt_chunk.block_align = static_cast<u16>(m_num_channels * sizeof(SampleType));
header.fmt_chunk.bits_per_sample = 16;
header.data_chunk_header.chunk_id = 0x61746164; // 0x64617461
header.data_chunk_header.chunk_size = data_size;
return (std::fwrite(&header, sizeof(header), 1, m_file) == 1);
}
} // namespace Common

34
src/util/wav_writer.h Normal file
View File

@ -0,0 +1,34 @@
#pragma once
#include "common/types.h"
#include <cstdio>
namespace Common {
class WAVWriter
{
public:
WAVWriter();
~WAVWriter();
ALWAYS_INLINE u32 GetSampleRate() const { return m_sample_rate; }
ALWAYS_INLINE u32 GetNumChannels() const { return m_num_channels; }
ALWAYS_INLINE u32 GetNumFrames() const { return m_num_frames; }
ALWAYS_INLINE bool IsOpen() const { return (m_file != nullptr); }
bool Open(const char* filename, u32 sample_rate, u32 num_channels);
void Close();
void WriteFrames(const s16* samples, u32 num_frames);
private:
using SampleType = s16;
bool WriteHeader();
std::FILE* m_file = nullptr;
u32 m_sample_rate = 0;
u32 m_num_channels = 0;
u32 m_num_frames = 0;
};
} // namespace Common