mirror of
https://github.com/WinampDesktop/winamp.git
synced 2025-06-18 19:45:47 -04:00
Move utility classes from common to own static lib
This commit is contained in:
45
src/util/CMakeLists.txt
Normal file
45
src/util/CMakeLists.txt
Normal 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
387
src/util/audio_stream.cpp
Normal 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
126
src/util/audio_stream.h
Normal 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
486
src/util/cd_image.cpp
Normal 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
337
src/util/cd_image.h
Normal 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
143
src/util/cd_image_bin.cpp
Normal 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
400
src/util/cd_image_chd.cpp
Normal 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
340
src/util/cd_image_cue.cpp
Normal 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;
|
||||
}
|
543
src/util/cd_image_device.cpp
Normal file
543
src/util/cd_image_device.cpp
Normal 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
556
src/util/cd_image_ecm.cpp
Normal 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;
|
||||
}
|
131
src/util/cd_image_hasher.cpp
Normal file
131
src/util/cd_image_hasher.cpp
Normal 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
|
21
src/util/cd_image_hasher.h
Normal file
21
src/util/cd_image_hasher.h
Normal 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
180
src/util/cd_image_m3u.cpp
Normal 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
304
src/util/cd_image_mds.cpp
Normal 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;
|
||||
}
|
152
src/util/cd_image_memory.cpp
Normal file
152
src/util/cd_image_memory.cpp
Normal 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
902
src/util/cd_image_pbp.cpp
Normal 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
440
src/util/cd_image_ppf.cpp
Normal 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;
|
||||
}
|
115
src/util/cd_subchannel_replacement.cpp
Normal file
115
src/util/cd_subchannel_replacement.cpp
Normal 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;
|
||||
}
|
32
src/util/cd_subchannel_replacement.h
Normal file
32
src/util/cd_subchannel_replacement.h
Normal 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
98
src/util/cd_xa.cpp
Normal 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
70
src/util/cd_xa.h
Normal 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
477
src/util/cue_parser.cpp
Normal 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 ⁢
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Track* File::GetMutableTrack(u32 n)
|
||||
{
|
||||
for (auto& it : m_tracks)
|
||||
{
|
||||
if (it.number == n)
|
||||
return ⁢
|
||||
}
|
||||
|
||||
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
86
src/util/cue_parser.h
Normal 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
292
src/util/iso_reader.cpp
Normal 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*>(§or_buffer[sector_offset]);
|
||||
const char* de_filename =
|
||||
reinterpret_cast<const char*>(§or_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*>(§or_buffer[sector_offset]);
|
||||
const char* de_filename =
|
||||
reinterpret_cast<const char*>(§or_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
155
src/util/iso_reader.h
Normal 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 = {};
|
||||
};
|
312
src/util/jit_code_buffer.cpp
Normal file
312
src/util/jit_code_buffer.cpp
Normal 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
|
61
src/util/jit_code_buffer.h
Normal file
61
src/util/jit_code_buffer.h
Normal 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
410
src/util/memory_arena.cpp
Normal 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
74
src/util/memory_arena.h
Normal 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
|
25
src/util/null_audio_stream.cpp
Normal file
25
src/util/null_audio_stream.cpp
Normal 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>();
|
||||
}
|
15
src/util/null_audio_stream.h
Normal file
15
src/util/null_audio_stream.h
Normal 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;
|
||||
};
|
461
src/util/page_fault_handler.cpp
Normal file
461
src/util/page_fault_handler.cpp
Normal 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
|
19
src/util/page_fault_handler.h
Normal file
19
src/util/page_fault_handler.h
Normal 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
110
src/util/pbp_types.h
Normal 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
1596
src/util/shiftjis.cpp
Normal file
File diff suppressed because it is too large
Load Diff
4
src/util/shiftjis.h
Normal file
4
src/util/shiftjis.h
Normal file
@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
void sjis2ascii(char* bData);
|
||||
char* sjis2utf8(char* input);
|
80
src/util/state_wrapper.cpp
Normal file
80
src/util/state_wrapper.cpp
Normal 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
208
src/util/state_wrapper.h
Normal 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
16
src/util/util.props
Normal 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
58
src/util/util.vcxproj
Normal 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>
|
46
src/util/util.vcxproj.filters
Normal file
46
src/util/util.vcxproj.filters
Normal 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
115
src/util/wav_writer.cpp
Normal 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
34
src/util/wav_writer.h
Normal 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
|
Reference in New Issue
Block a user