mirror of
https://github.com/Retropex/dolphin.git
synced 2025-05-25 03:22:32 +02:00
1472 lines
47 KiB
C++
1472 lines
47 KiB
C++
// Copyright 2008 Dolphin Emulator Project
|
|
// Licensed under GPLv2+
|
|
// Refer to the license.txt file included.
|
|
|
|
#include <algorithm>
|
|
#include <cinttypes>
|
|
#include <cmath>
|
|
#include <memory>
|
|
#include <string>
|
|
|
|
#include "AudioCommon/AudioCommon.h"
|
|
|
|
#include "Common/Align.h"
|
|
#include "Common/ChunkFile.h"
|
|
#include "Common/CommonTypes.h"
|
|
|
|
#include "Core/ConfigManager.h"
|
|
#include "Core/Core.h"
|
|
#include "Core/CoreTiming.h"
|
|
#include "Core/HW/AudioInterface.h"
|
|
#include "Core/HW/DVDInterface.h"
|
|
#include "Core/HW/DVDThread.h"
|
|
#include "Core/HW/MMIO.h"
|
|
#include "Core/HW/Memmap.h"
|
|
#include "Core/HW/ProcessorInterface.h"
|
|
#include "Core/HW/StreamADPCM.h"
|
|
#include "Core/HW/SystemTimers.h"
|
|
#include "Core/IOS/DI/DI.h"
|
|
#include "Core/IOS/IPC.h"
|
|
#include "Core/Movie.h"
|
|
|
|
#include "DiscIO/Enums.h"
|
|
#include "DiscIO/Volume.h"
|
|
#include "DiscIO/VolumeCreator.h"
|
|
#include "DiscIO/VolumeWiiCrypted.h"
|
|
|
|
// The minimum time it takes for the DVD drive to process a command (in
|
|
// microseconds)
|
|
constexpr u64 COMMAND_LATENCY_US = 300;
|
|
|
|
// The size of the streaming buffer.
|
|
constexpr u64 STREAMING_BUFFER_SIZE = 1024 * 1024;
|
|
|
|
// A single DVD disc sector
|
|
constexpr u64 DVD_SECTOR_SIZE = 0x800;
|
|
|
|
// The minimum amount that a drive will read
|
|
constexpr u64 DVD_ECC_BLOCK_SIZE = 16 * DVD_SECTOR_SIZE;
|
|
|
|
// Rate the drive can transfer data to main memory, given the data
|
|
// is already buffered. Measured in bytes per second.
|
|
constexpr u64 BUFFER_TRANSFER_RATE = 32 * 1024 * 1024;
|
|
|
|
// The size of the first Wii disc layer in bytes (2294912 sectors per layer)
|
|
constexpr u64 WII_DISC_LAYER_SIZE = 2294912 * DVD_SECTOR_SIZE;
|
|
|
|
// 24 mm
|
|
constexpr double DVD_INNER_RADIUS = 0.024;
|
|
// 58 mm
|
|
constexpr double WII_DVD_OUTER_RADIUS = 0.058;
|
|
// 38 mm
|
|
constexpr double GC_DVD_OUTER_RADIUS = 0.038;
|
|
|
|
// Approximate read speeds at the inner and outer locations of Wii and GC
|
|
// discs. These speeds are approximations of speeds measured on real Wiis.
|
|
constexpr double GC_DISC_INNER_READ_SPEED = 1024 * 1024 * 2.1; // bytes/s
|
|
constexpr double GC_DISC_OUTER_READ_SPEED = 1024 * 1024 * 3.325; // bytes/s
|
|
constexpr double WII_DISC_INNER_READ_SPEED = 1024 * 1024 * 3.48; // bytes/s
|
|
constexpr double WII_DISC_OUTER_READ_SPEED = 1024 * 1024 * 8.41; // bytes/s
|
|
|
|
// Experimentally measured seek constants. The time to seek appears to be
|
|
// linear, but short seeks appear to be lower velocity.
|
|
constexpr double SHORT_SEEK_MAX_DISTANCE = 0.001; // 1 mm
|
|
constexpr double SHORT_SEEK_CONSTANT = 0.045; // seconds
|
|
constexpr double SHORT_SEEK_VELOCITY_INVERSE = 50; // inverse: s/m
|
|
constexpr double LONG_SEEK_CONSTANT = 0.085; // seconds
|
|
constexpr double LONG_SEEK_VELOCITY_INVERSE = 4.5; // inverse: s/m
|
|
|
|
namespace DVDInterface
|
|
{
|
|
// internal hardware addresses
|
|
enum
|
|
{
|
|
DI_STATUS_REGISTER = 0x00,
|
|
DI_COVER_REGISTER = 0x04,
|
|
DI_COMMAND_0 = 0x08,
|
|
DI_COMMAND_1 = 0x0C,
|
|
DI_COMMAND_2 = 0x10,
|
|
DI_DMA_ADDRESS_REGISTER = 0x14,
|
|
DI_DMA_LENGTH_REGISTER = 0x18,
|
|
DI_DMA_CONTROL_REGISTER = 0x1C,
|
|
DI_IMMEDIATE_DATA_BUFFER = 0x20,
|
|
DI_CONFIG_REGISTER = 0x24
|
|
};
|
|
|
|
// debug commands which may be ORd
|
|
enum
|
|
{
|
|
STOP_DRIVE = 0,
|
|
START_DRIVE = 0x100,
|
|
ACCEPT_COPY = 0x4000,
|
|
DISC_CHECK = 0x8000,
|
|
};
|
|
|
|
// DI Status Register
|
|
union UDISR
|
|
{
|
|
u32 Hex;
|
|
struct
|
|
{
|
|
u32 BREAK : 1; // Stop the Device + Interrupt
|
|
u32 DEINITMASK : 1; // Access Device Error Int Mask
|
|
u32 DEINT : 1; // Access Device Error Int
|
|
u32 TCINTMASK : 1; // Transfer Complete Int Mask
|
|
u32 TCINT : 1; // Transfer Complete Int
|
|
u32 BRKINTMASK : 1;
|
|
u32 BRKINT : 1; // w 1: clear brkint
|
|
u32 : 25;
|
|
};
|
|
UDISR() { Hex = 0; }
|
|
UDISR(u32 _hex) { Hex = _hex; }
|
|
};
|
|
|
|
// DI Cover Register
|
|
union UDICVR
|
|
{
|
|
u32 Hex;
|
|
struct
|
|
{
|
|
u32 CVR : 1; // 0: Cover closed 1: Cover open
|
|
u32 CVRINTMASK : 1; // 1: Interrupt enabled
|
|
u32 CVRINT : 1; // r 1: Interrupt requested w 1: Interrupt clear
|
|
u32 : 29;
|
|
};
|
|
UDICVR() { Hex = 0; }
|
|
UDICVR(u32 _hex) { Hex = _hex; }
|
|
};
|
|
|
|
union UDICMDBUF
|
|
{
|
|
u32 Hex;
|
|
struct
|
|
{
|
|
u8 CMDBYTE3;
|
|
u8 CMDBYTE2;
|
|
u8 CMDBYTE1;
|
|
u8 CMDBYTE0;
|
|
};
|
|
};
|
|
|
|
// DI DMA Address Register
|
|
union UDIMAR
|
|
{
|
|
u32 Hex;
|
|
struct
|
|
{
|
|
u32 Zerobits : 5; // Must be zero (32byte aligned)
|
|
u32 : 27;
|
|
};
|
|
struct
|
|
{
|
|
u32 Address : 26;
|
|
u32 : 6;
|
|
};
|
|
};
|
|
|
|
// DI DMA Address Length Register
|
|
union UDILENGTH
|
|
{
|
|
u32 Hex;
|
|
struct
|
|
{
|
|
u32 Zerobits : 5; // Must be zero (32byte aligned)
|
|
u32 : 27;
|
|
};
|
|
struct
|
|
{
|
|
u32 Length : 26;
|
|
u32 : 6;
|
|
};
|
|
};
|
|
|
|
// DI DMA Control Register
|
|
union UDICR
|
|
{
|
|
u32 Hex;
|
|
struct
|
|
{
|
|
u32 TSTART : 1; // w:1 start r:0 ready
|
|
u32 DMA : 1; // 1: DMA Mode 0: Immediate Mode (can only do Access Register Command)
|
|
u32 RW : 1; // 0: Read Command (DVD to Memory) 1: Write Command (Memory to DVD)
|
|
u32 : 29;
|
|
};
|
|
};
|
|
|
|
union UDIIMMBUF
|
|
{
|
|
u32 Hex;
|
|
struct
|
|
{
|
|
u8 REGVAL3;
|
|
u8 REGVAL2;
|
|
u8 REGVAL1;
|
|
u8 REGVAL0;
|
|
};
|
|
};
|
|
|
|
// DI Config Register
|
|
union UDICFG
|
|
{
|
|
u32 Hex;
|
|
struct
|
|
{
|
|
u32 CONFIG : 8;
|
|
u32 : 24;
|
|
};
|
|
UDICFG() { Hex = 0; }
|
|
UDICFG(u32 _hex) { Hex = _hex; }
|
|
};
|
|
|
|
static std::unique_ptr<DiscIO::IVolume> s_inserted_volume;
|
|
|
|
// STATE_TO_SAVE
|
|
|
|
// Hardware registers
|
|
static UDISR s_DISR;
|
|
static UDICVR s_DICVR;
|
|
static UDICMDBUF s_DICMDBUF[3];
|
|
static UDIMAR s_DIMAR;
|
|
static UDILENGTH s_DILENGTH;
|
|
static UDICR s_DICR;
|
|
static UDIIMMBUF s_DIIMMBUF;
|
|
static UDICFG s_DICFG;
|
|
|
|
// DTK
|
|
static bool s_stream = false;
|
|
static bool s_stop_at_track_end = false;
|
|
static u64 s_audio_position;
|
|
static u64 s_current_start;
|
|
static u32 s_current_length;
|
|
static u64 s_next_start;
|
|
static u32 s_next_length;
|
|
static u32 s_pending_samples;
|
|
|
|
// Disc drive state
|
|
static u32 s_error_code = 0;
|
|
|
|
// Disc drive timing
|
|
static u64 s_read_buffer_start_time;
|
|
static u64 s_read_buffer_end_time;
|
|
static u64 s_read_buffer_start_offset;
|
|
static u64 s_read_buffer_end_offset;
|
|
|
|
// Disc changing
|
|
static std::string s_disc_path_to_insert;
|
|
|
|
// Events
|
|
static CoreTiming::EventType* s_finish_executing_command;
|
|
static CoreTiming::EventType* s_eject_disc;
|
|
static CoreTiming::EventType* s_insert_disc;
|
|
|
|
static void EjectDiscCallback(u64 userdata, s64 cyclesLate);
|
|
static void InsertDiscCallback(u64 userdata, s64 cyclesLate);
|
|
static void FinishExecutingCommandCallback(u64 userdata, s64 cycles_late);
|
|
|
|
void SetLidOpen();
|
|
|
|
void UpdateInterrupts();
|
|
void GenerateDIInterrupt(DIInterruptType _DVDInterrupt);
|
|
|
|
void WriteImmediate(u32 value, u32 output_address, bool reply_to_ios);
|
|
bool ExecuteReadCommand(u64 DVD_offset, u32 output_address, u32 DVD_length, u32 output_length,
|
|
bool decrypt, ReplyType reply_type, DIInterruptType* interrupt_type);
|
|
|
|
u64 PackFinishExecutingCommandUserdata(ReplyType reply_type, DIInterruptType interrupt_type);
|
|
|
|
void ScheduleReads(u64 offset, u32 length, bool decrypt, u32 output_address, ReplyType reply_type);
|
|
double CalculatePhysicalDiscPosition(u64 offset);
|
|
u64 CalculateSeekTime(u64 offset_from, u64 offset_to);
|
|
u64 CalculateRawDiscReadTime(u64 offset, u64 length);
|
|
|
|
void DoState(PointerWrap& p)
|
|
{
|
|
bool disc_inside = IsDiscInside();
|
|
|
|
p.DoPOD(s_DISR);
|
|
p.DoPOD(s_DICVR);
|
|
p.DoArray(s_DICMDBUF);
|
|
p.Do(s_DIMAR);
|
|
p.Do(s_DILENGTH);
|
|
p.Do(s_DICR);
|
|
p.Do(s_DIIMMBUF);
|
|
p.DoPOD(s_DICFG);
|
|
|
|
p.Do(s_stream);
|
|
p.Do(s_stop_at_track_end);
|
|
p.Do(s_audio_position);
|
|
p.Do(s_current_start);
|
|
p.Do(s_current_length);
|
|
p.Do(s_next_start);
|
|
p.Do(s_next_length);
|
|
p.Do(s_pending_samples);
|
|
|
|
p.Do(s_error_code);
|
|
p.Do(disc_inside);
|
|
|
|
p.Do(s_read_buffer_start_time);
|
|
p.Do(s_read_buffer_end_time);
|
|
p.Do(s_read_buffer_start_offset);
|
|
p.Do(s_read_buffer_end_offset);
|
|
|
|
p.Do(s_disc_path_to_insert);
|
|
|
|
DVDThread::DoState(p);
|
|
|
|
// s_inserted_volume isn't savestated (because it points to
|
|
// files on the local system). Instead, we check that the
|
|
// savestated disc_inside matches our IsDiscInside(). This
|
|
// won't catch cases of having the wrong disc inserted, though.
|
|
// TODO: Check the game ID, disc number, revision?
|
|
if (disc_inside != IsDiscInside())
|
|
{
|
|
if (disc_inside)
|
|
PanicAlertT("An inserted disc was expected but not found.");
|
|
else
|
|
s_inserted_volume.reset();
|
|
}
|
|
}
|
|
|
|
static size_t ProcessDTKSamples(std::vector<s16>* temp_pcm, const std::vector<u8>& audio_data)
|
|
{
|
|
size_t samples_processed = 0;
|
|
size_t bytes_processed = 0;
|
|
while (samples_processed < temp_pcm->size() / 2 && bytes_processed < audio_data.size())
|
|
{
|
|
StreamADPCM::DecodeBlock(&(*temp_pcm)[samples_processed * 2], &audio_data[bytes_processed]);
|
|
for (size_t i = 0; i < StreamADPCM::SAMPLES_PER_BLOCK * 2; ++i)
|
|
{
|
|
// TODO: Fix the mixer so it can accept non-byte-swapped samples.
|
|
s16* sample = &(*temp_pcm)[samples_processed * 2 + i];
|
|
*sample = Common::swap16(*sample);
|
|
}
|
|
samples_processed += StreamADPCM::SAMPLES_PER_BLOCK;
|
|
bytes_processed += StreamADPCM::ONE_BLOCK_SIZE;
|
|
}
|
|
return samples_processed;
|
|
}
|
|
|
|
static u32 AdvanceDTK(u32 maximum_samples, u32* samples_to_process)
|
|
{
|
|
u32 bytes_to_process = 0;
|
|
*samples_to_process = 0;
|
|
while (*samples_to_process < maximum_samples)
|
|
{
|
|
if (s_audio_position >= s_current_start + s_current_length)
|
|
{
|
|
DEBUG_LOG(DVDINTERFACE, "AdvanceDTK: NextStart=%08" PRIx64 ", NextLength=%08x, "
|
|
"CurrentStart=%08" PRIx64 ", CurrentLength=%08x, AudioPos=%08" PRIx64,
|
|
s_next_start, s_next_length, s_current_start, s_current_length, s_audio_position);
|
|
|
|
s_audio_position = s_next_start;
|
|
s_current_start = s_next_start;
|
|
s_current_length = s_next_length;
|
|
|
|
if (s_stop_at_track_end)
|
|
{
|
|
s_stop_at_track_end = false;
|
|
s_stream = false;
|
|
break;
|
|
}
|
|
|
|
StreamADPCM::InitFilter();
|
|
}
|
|
|
|
s_audio_position += StreamADPCM::ONE_BLOCK_SIZE;
|
|
bytes_to_process += StreamADPCM::ONE_BLOCK_SIZE;
|
|
*samples_to_process += StreamADPCM::SAMPLES_PER_BLOCK;
|
|
}
|
|
|
|
return bytes_to_process;
|
|
}
|
|
|
|
static void DTKStreamingCallback(const std::vector<u8>& audio_data, s64 cycles_late)
|
|
{
|
|
// Send audio to the mixer.
|
|
std::vector<s16> temp_pcm(s_pending_samples * 2, 0);
|
|
ProcessDTKSamples(&temp_pcm, audio_data);
|
|
g_sound_stream->GetMixer()->PushStreamingSamples(temp_pcm.data(), s_pending_samples);
|
|
|
|
// Determine which audio data to read next.
|
|
static const int MAXIMUM_SAMPLES = 48000 / 2000 * 7; // 3.5ms of 48kHz samples
|
|
u64 read_offset;
|
|
u32 read_length;
|
|
if (s_stream && AudioInterface::IsPlaying())
|
|
{
|
|
read_offset = s_audio_position;
|
|
read_length = AdvanceDTK(MAXIMUM_SAMPLES, &s_pending_samples);
|
|
}
|
|
else
|
|
{
|
|
read_length = 0;
|
|
s_pending_samples = MAXIMUM_SAMPLES;
|
|
}
|
|
|
|
// Read the next chunk of audio data asynchronously.
|
|
s64 ticks_to_dtk = SystemTimers::GetTicksPerSecond() * s64(s_pending_samples) / 48000;
|
|
ticks_to_dtk -= cycles_late;
|
|
if (read_length > 0)
|
|
{
|
|
DVDThread::StartRead(read_offset, read_length, false, ReplyType::DTK, ticks_to_dtk);
|
|
}
|
|
else
|
|
{
|
|
// There's nothing to read, so using DVDThread is unnecessary.
|
|
u64 userdata = PackFinishExecutingCommandUserdata(ReplyType::DTK, DIInterruptType::INT_TCINT);
|
|
CoreTiming::ScheduleEvent(ticks_to_dtk, s_finish_executing_command, userdata);
|
|
}
|
|
}
|
|
|
|
void Init()
|
|
{
|
|
_assert_(!IsDiscInside());
|
|
|
|
DVDThread::Start();
|
|
|
|
Reset();
|
|
s_DICVR.Hex = 1; // Disc Channel relies on cover being open when no disc is inserted
|
|
|
|
s_eject_disc = CoreTiming::RegisterEvent("EjectDisc", EjectDiscCallback);
|
|
s_insert_disc = CoreTiming::RegisterEvent("InsertDisc", InsertDiscCallback);
|
|
|
|
s_finish_executing_command =
|
|
CoreTiming::RegisterEvent("FinishExecutingCommand", FinishExecutingCommandCallback);
|
|
|
|
u64 userdata = PackFinishExecutingCommandUserdata(ReplyType::DTK, DIInterruptType::INT_TCINT);
|
|
CoreTiming::ScheduleEvent(0, s_finish_executing_command, userdata);
|
|
}
|
|
|
|
// This doesn't reset any inserted disc or the cover state.
|
|
void Reset()
|
|
{
|
|
s_DISR.Hex = 0;
|
|
s_DICMDBUF[0].Hex = 0;
|
|
s_DICMDBUF[1].Hex = 0;
|
|
s_DICMDBUF[2].Hex = 0;
|
|
s_DIMAR.Hex = 0;
|
|
s_DILENGTH.Hex = 0;
|
|
s_DICR.Hex = 0;
|
|
s_DIIMMBUF.Hex = 0;
|
|
s_DICFG.Hex = 0;
|
|
s_DICFG.CONFIG = 1; // Disable bootrom descrambler
|
|
|
|
s_stream = false;
|
|
s_stop_at_track_end = false;
|
|
s_audio_position = 0;
|
|
s_next_start = 0;
|
|
s_next_length = 0;
|
|
s_current_start = 0;
|
|
s_current_length = 0;
|
|
s_pending_samples = 0;
|
|
|
|
s_error_code = 0;
|
|
|
|
// The buffer is empty at start
|
|
s_read_buffer_start_offset = 0;
|
|
s_read_buffer_end_offset = 0;
|
|
s_read_buffer_start_time = 0;
|
|
s_read_buffer_end_time = 0;
|
|
|
|
s_disc_path_to_insert.clear();
|
|
}
|
|
|
|
void Shutdown()
|
|
{
|
|
DVDThread::Stop();
|
|
s_inserted_volume.reset();
|
|
}
|
|
|
|
const DiscIO::IVolume& GetVolume()
|
|
{
|
|
_assert_(IsDiscInside());
|
|
return *s_inserted_volume;
|
|
}
|
|
|
|
bool SetVolumeName(const std::string& disc_path)
|
|
{
|
|
DVDThread::WaitUntilIdle();
|
|
s_inserted_volume = DiscIO::CreateVolumeFromFilename(disc_path);
|
|
SetLidOpen();
|
|
return IsDiscInside();
|
|
}
|
|
|
|
bool SetVolumeDirectory(const std::string& full_path, bool is_wii,
|
|
const std::string& apploader_path, const std::string& DOL_path)
|
|
{
|
|
DVDThread::WaitUntilIdle();
|
|
s_inserted_volume =
|
|
DiscIO::CreateVolumeFromDirectory(full_path, is_wii, apploader_path, DOL_path);
|
|
SetLidOpen();
|
|
return IsDiscInside();
|
|
}
|
|
|
|
bool IsDiscInside()
|
|
{
|
|
return s_inserted_volume != nullptr;
|
|
}
|
|
|
|
// Take care of all logic of "swapping discs"
|
|
// We want this in the "backend", NOT the gui
|
|
// any !empty string will be deleted to ensure
|
|
// that the userdata string exists when called
|
|
static void EjectDiscCallback(u64 userdata, s64 cyclesLate)
|
|
{
|
|
DVDThread::WaitUntilIdle();
|
|
s_inserted_volume.reset();
|
|
SetLidOpen();
|
|
}
|
|
|
|
static void InsertDiscCallback(u64 userdata, s64 cyclesLate)
|
|
{
|
|
const std::string& old_path = SConfig::GetInstance().m_strFilename;
|
|
|
|
if (!SetVolumeName(s_disc_path_to_insert))
|
|
{
|
|
// Put back the old one
|
|
SetVolumeName(old_path);
|
|
PanicAlertT("The disc that was about to be inserted couldn't be found.");
|
|
}
|
|
|
|
s_disc_path_to_insert.clear();
|
|
}
|
|
|
|
// Can only be called by the host thread
|
|
void ChangeDiscAsHost(const std::string& new_path)
|
|
{
|
|
bool was_unpaused = Core::PauseAndLock(true);
|
|
|
|
// The host thread is now temporarily the CPU thread
|
|
ChangeDiscAsCPU(new_path);
|
|
|
|
Core::PauseAndLock(false, was_unpaused);
|
|
}
|
|
|
|
// Can only be called by the CPU thread
|
|
void ChangeDiscAsCPU(const std::string& new_path)
|
|
{
|
|
if (!s_disc_path_to_insert.empty())
|
|
{
|
|
PanicAlertT("A disc is already about to be inserted.");
|
|
return;
|
|
}
|
|
|
|
s_disc_path_to_insert = new_path;
|
|
CoreTiming::ScheduleEvent(0, s_eject_disc);
|
|
CoreTiming::ScheduleEvent(SystemTimers::GetTicksPerSecond(), s_insert_disc);
|
|
|
|
Movie::SignalDiscChange(new_path);
|
|
}
|
|
|
|
void SetLidOpen()
|
|
{
|
|
u32 old_value = s_DICVR.CVR;
|
|
s_DICVR.CVR = IsDiscInside() ? 0 : 1;
|
|
if (s_DICVR.CVR != old_value)
|
|
GenerateDIInterrupt(INT_CVRINT);
|
|
}
|
|
|
|
bool ChangePartition(u64 offset)
|
|
{
|
|
DVDThread::WaitUntilIdle();
|
|
return s_inserted_volume->ChangePartition(offset);
|
|
}
|
|
|
|
void RegisterMMIO(MMIO::Mapping* mmio, u32 base)
|
|
{
|
|
mmio->Register(base | DI_STATUS_REGISTER, MMIO::DirectRead<u32>(&s_DISR.Hex),
|
|
MMIO::ComplexWrite<u32>([](u32, u32 val) {
|
|
UDISR tmpStatusReg(val);
|
|
|
|
s_DISR.DEINITMASK = tmpStatusReg.DEINITMASK;
|
|
s_DISR.TCINTMASK = tmpStatusReg.TCINTMASK;
|
|
s_DISR.BRKINTMASK = tmpStatusReg.BRKINTMASK;
|
|
s_DISR.BREAK = tmpStatusReg.BREAK;
|
|
|
|
if (tmpStatusReg.DEINT)
|
|
s_DISR.DEINT = 0;
|
|
|
|
if (tmpStatusReg.TCINT)
|
|
s_DISR.TCINT = 0;
|
|
|
|
if (tmpStatusReg.BRKINT)
|
|
s_DISR.BRKINT = 0;
|
|
|
|
if (s_DISR.BREAK)
|
|
{
|
|
_dbg_assert_(DVDINTERFACE, 0);
|
|
}
|
|
|
|
UpdateInterrupts();
|
|
}));
|
|
|
|
mmio->Register(base | DI_COVER_REGISTER, MMIO::DirectRead<u32>(&s_DICVR.Hex),
|
|
MMIO::ComplexWrite<u32>([](u32, u32 val) {
|
|
UDICVR tmpCoverReg(val);
|
|
|
|
s_DICVR.CVRINTMASK = tmpCoverReg.CVRINTMASK;
|
|
|
|
if (tmpCoverReg.CVRINT)
|
|
s_DICVR.CVRINT = 0;
|
|
|
|
UpdateInterrupts();
|
|
}));
|
|
|
|
// Command registers are very similar and we can register them with a
|
|
// simple loop.
|
|
for (int i = 0; i < 3; ++i)
|
|
mmio->Register(base | (DI_COMMAND_0 + 4 * i), MMIO::DirectRead<u32>(&s_DICMDBUF[i].Hex),
|
|
MMIO::DirectWrite<u32>(&s_DICMDBUF[i].Hex));
|
|
|
|
// DMA related registers. Mostly direct accesses (+ masking for writes to
|
|
// handle things like address alignment) and complex write on the DMA
|
|
// control register that will trigger the DMA.
|
|
mmio->Register(base | DI_DMA_ADDRESS_REGISTER, MMIO::DirectRead<u32>(&s_DIMAR.Hex),
|
|
MMIO::DirectWrite<u32>(&s_DIMAR.Hex, ~0xFC00001F));
|
|
mmio->Register(base | DI_DMA_LENGTH_REGISTER, MMIO::DirectRead<u32>(&s_DILENGTH.Hex),
|
|
MMIO::DirectWrite<u32>(&s_DILENGTH.Hex, ~0x1F));
|
|
mmio->Register(base | DI_DMA_CONTROL_REGISTER, MMIO::DirectRead<u32>(&s_DICR.Hex),
|
|
MMIO::ComplexWrite<u32>([](u32, u32 val) {
|
|
s_DICR.Hex = val & 7;
|
|
if (s_DICR.TSTART)
|
|
{
|
|
ExecuteCommand(s_DICMDBUF[0].Hex, s_DICMDBUF[1].Hex, s_DICMDBUF[2].Hex,
|
|
s_DIMAR.Hex, s_DILENGTH.Hex, false);
|
|
}
|
|
}));
|
|
|
|
mmio->Register(base | DI_IMMEDIATE_DATA_BUFFER, MMIO::DirectRead<u32>(&s_DIIMMBUF.Hex),
|
|
MMIO::DirectWrite<u32>(&s_DIIMMBUF.Hex));
|
|
|
|
// DI config register is read only.
|
|
mmio->Register(base | DI_CONFIG_REGISTER, MMIO::DirectRead<u32>(&s_DICFG.Hex),
|
|
MMIO::InvalidWrite<u32>());
|
|
}
|
|
|
|
void UpdateInterrupts()
|
|
{
|
|
if ((s_DISR.DEINT & s_DISR.DEINITMASK) || (s_DISR.TCINT & s_DISR.TCINTMASK) ||
|
|
(s_DISR.BRKINT & s_DISR.BRKINTMASK) || (s_DICVR.CVRINT & s_DICVR.CVRINTMASK))
|
|
{
|
|
ProcessorInterface::SetInterrupt(ProcessorInterface::INT_CAUSE_DI, true);
|
|
}
|
|
else
|
|
{
|
|
ProcessorInterface::SetInterrupt(ProcessorInterface::INT_CAUSE_DI, false);
|
|
}
|
|
|
|
// Required for Summoner: A Goddess Reborn
|
|
CoreTiming::ForceExceptionCheck(50);
|
|
}
|
|
|
|
void GenerateDIInterrupt(DIInterruptType dvd_interrupt)
|
|
{
|
|
switch (dvd_interrupt)
|
|
{
|
|
case INT_DEINT:
|
|
s_DISR.DEINT = 1;
|
|
break;
|
|
case INT_TCINT:
|
|
s_DISR.TCINT = 1;
|
|
break;
|
|
case INT_BRKINT:
|
|
s_DISR.BRKINT = 1;
|
|
break;
|
|
case INT_CVRINT:
|
|
s_DICVR.CVRINT = 1;
|
|
break;
|
|
}
|
|
|
|
UpdateInterrupts();
|
|
}
|
|
|
|
void WriteImmediate(u32 value, u32 output_address, bool reply_to_ios)
|
|
{
|
|
if (reply_to_ios)
|
|
Memory::Write_U32(value, output_address);
|
|
else
|
|
s_DIIMMBUF.Hex = value;
|
|
}
|
|
|
|
// Iff false is returned, ScheduleEvent must be used to finish executing the command
|
|
bool ExecuteReadCommand(u64 DVD_offset, u32 output_address, u32 DVD_length, u32 output_length,
|
|
bool decrypt, ReplyType reply_type, DIInterruptType* interrupt_type)
|
|
{
|
|
if (!IsDiscInside())
|
|
{
|
|
// Disc read fails
|
|
s_error_code = ERROR_NO_DISK | ERROR_COVER_H;
|
|
*interrupt_type = INT_DEINT;
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// Disc read succeeds
|
|
*interrupt_type = INT_TCINT;
|
|
}
|
|
|
|
if (DVD_length > output_length)
|
|
{
|
|
WARN_LOG(DVDINTERFACE, "Detected an attempt to read more data from the DVD "
|
|
"than what fits inside the out buffer. Clamping.");
|
|
DVD_length = output_length;
|
|
}
|
|
|
|
ScheduleReads(DVD_offset, DVD_length, decrypt, output_address, reply_type);
|
|
return true;
|
|
}
|
|
|
|
// When the command has finished executing, callback_event_type
|
|
// will be called using CoreTiming::ScheduleEvent,
|
|
// with the userdata set to the interrupt type.
|
|
void ExecuteCommand(u32 command_0, u32 command_1, u32 command_2, u32 output_address,
|
|
u32 output_length, bool reply_to_ios)
|
|
{
|
|
ReplyType reply_type = reply_to_ios ? ReplyType::IOS : ReplyType::Interrupt;
|
|
DIInterruptType interrupt_type = INT_TCINT;
|
|
bool command_handled_by_thread = false;
|
|
|
|
// DVDLowRequestError needs access to the error code set by the previous command
|
|
if (command_0 >> 24 != DVDLowRequestError)
|
|
s_error_code = 0;
|
|
|
|
switch (command_0 >> 24)
|
|
{
|
|
// Seems to be used by both GC and Wii
|
|
case DVDLowInquiry:
|
|
// (shuffle2) Taken from my Wii
|
|
Memory::Write_U32(0x00000002, output_address);
|
|
Memory::Write_U32(0x20060526, output_address + 4);
|
|
// This was in the oubuf even though this cmd is only supposed to reply with 64bits
|
|
// However, this and other tests strongly suggest that the buffer is static, and it's never -
|
|
// or rarely cleared.
|
|
Memory::Write_U32(0x41000000, output_address + 8);
|
|
|
|
INFO_LOG(DVDINTERFACE, "DVDLowInquiry (Buffer 0x%08x, 0x%x)", output_address, output_length);
|
|
break;
|
|
|
|
// Only seems to be used from WII_IPC, not through direct access
|
|
case DVDLowReadDiskID:
|
|
INFO_LOG(DVDINTERFACE, "DVDLowReadDiskID");
|
|
command_handled_by_thread = ExecuteReadCommand(0, output_address, 0x20, output_length, false,
|
|
reply_type, &interrupt_type);
|
|
break;
|
|
|
|
// Only used from WII_IPC. This is the only read command that decrypts data
|
|
case DVDLowRead:
|
|
INFO_LOG(DVDINTERFACE, "DVDLowRead: DVDAddr: 0x%09" PRIx64 ", Size: 0x%x", (u64)command_2 << 2,
|
|
command_1);
|
|
command_handled_by_thread =
|
|
ExecuteReadCommand((u64)command_2 << 2, output_address, command_1, output_length, true,
|
|
reply_type, &interrupt_type);
|
|
break;
|
|
|
|
// Probably only used by Wii
|
|
case DVDLowWaitForCoverClose:
|
|
INFO_LOG(DVDINTERFACE, "DVDLowWaitForCoverClose");
|
|
interrupt_type = (DIInterruptType)4; // ???
|
|
break;
|
|
|
|
// "Set Extension"...not sure what it does. GC only?
|
|
case 0x55:
|
|
INFO_LOG(DVDINTERFACE, "SetExtension");
|
|
break;
|
|
|
|
// Probably only used though WII_IPC
|
|
case DVDLowGetCoverReg:
|
|
WriteImmediate(s_DICVR.Hex, output_address, reply_to_ios);
|
|
DEBUG_LOG(DVDINTERFACE, "DVDLowGetCoverReg 0x%08x", s_DICVR.Hex);
|
|
break;
|
|
|
|
// Probably only used by Wii
|
|
case DVDLowNotifyReset:
|
|
ERROR_LOG(DVDINTERFACE, "DVDLowNotifyReset");
|
|
PanicAlert("DVDLowNotifyReset");
|
|
break;
|
|
// Probably only used by Wii
|
|
case DVDLowReadDvdPhysical:
|
|
ERROR_LOG(DVDINTERFACE, "DVDLowReadDvdPhysical");
|
|
PanicAlert("DVDLowReadDvdPhysical");
|
|
break;
|
|
// Probably only used by Wii
|
|
case DVDLowReadDvdCopyright:
|
|
ERROR_LOG(DVDINTERFACE, "DVDLowReadDvdCopyright");
|
|
PanicAlert("DVDLowReadDvdCopyright");
|
|
break;
|
|
// Probably only used by Wii
|
|
case DVDLowReadDvdDiscKey:
|
|
ERROR_LOG(DVDINTERFACE, "DVDLowReadDvdDiscKey");
|
|
PanicAlert("DVDLowReadDvdDiscKey");
|
|
break;
|
|
|
|
// Probably only used by Wii
|
|
case DVDLowClearCoverInterrupt:
|
|
DEBUG_LOG(DVDINTERFACE, "DVDLowClearCoverInterrupt");
|
|
s_DICVR.CVRINT = 0;
|
|
break;
|
|
|
|
// Probably only used by Wii
|
|
case DVDLowGetCoverStatus:
|
|
WriteImmediate(IsDiscInside() ? 2 : 1, output_address, reply_to_ios);
|
|
INFO_LOG(DVDINTERFACE, "DVDLowGetCoverStatus: Disc %sInserted", IsDiscInside() ? "" : "Not ");
|
|
break;
|
|
|
|
// Probably only used by Wii
|
|
case DVDLowReset:
|
|
INFO_LOG(DVDINTERFACE, "DVDLowReset");
|
|
break;
|
|
|
|
// Probably only used by Wii
|
|
case DVDLowClosePartition:
|
|
INFO_LOG(DVDINTERFACE, "DVDLowClosePartition");
|
|
break;
|
|
|
|
// Probably only used by Wii
|
|
case DVDLowUnencryptedRead:
|
|
INFO_LOG(DVDINTERFACE, "DVDLowUnencryptedRead: DVDAddr: 0x%09" PRIx64 ", Size: 0x%x",
|
|
(u64)command_2 << 2, command_1);
|
|
|
|
// We must make sure it is in a valid area! (#001 check)
|
|
// Are these checks correct? They seem to mix 32-bit offsets and 8-bit lengths
|
|
// * 0x00000000 - 0x00014000 (limit of older IOS versions)
|
|
// * 0x460a0000 - 0x460a0008
|
|
// * 0x7ed40000 - 0x7ed40008
|
|
if (((command_2 > 0x00000000 && command_2 < 0x00014000) ||
|
|
(((command_2 + command_1) > 0x00000000) && (command_2 + command_1) < 0x00014000) ||
|
|
(command_2 > 0x460a0000 && command_2 < 0x460a0008) ||
|
|
(((command_2 + command_1) > 0x460a0000) && (command_2 + command_1) < 0x460a0008) ||
|
|
(command_2 > 0x7ed40000 && command_2 < 0x7ed40008) ||
|
|
(((command_2 + command_1) > 0x7ed40000) && (command_2 + command_1) < 0x7ed40008)))
|
|
{
|
|
command_handled_by_thread =
|
|
ExecuteReadCommand((u64)command_2 << 2, output_address, command_1, output_length, false,
|
|
reply_type, &interrupt_type);
|
|
}
|
|
else
|
|
{
|
|
WARN_LOG(DVDINTERFACE, "DVDLowUnencryptedRead: trying to read out of bounds @ %09" PRIx64,
|
|
(u64)command_2 << 2);
|
|
s_error_code = ERROR_READY | ERROR_BLOCK_OOB;
|
|
// Should cause software to call DVDLowRequestError
|
|
interrupt_type = INT_BRKINT;
|
|
}
|
|
|
|
break;
|
|
|
|
// Probably only used by Wii
|
|
case DVDLowEnableDvdVideo:
|
|
ERROR_LOG(DVDINTERFACE, "DVDLowEnableDvdVideo");
|
|
break;
|
|
|
|
// New Super Mario Bros. Wii sends these commands,
|
|
// but it seems we don't need to implement anything.
|
|
// Probably only used by Wii
|
|
case 0x95:
|
|
case 0x96:
|
|
ERROR_LOG(DVDINTERFACE, "Unimplemented BCA command 0x%08x (Buffer 0x%08x, 0x%x)", command_0,
|
|
output_address, output_length);
|
|
break;
|
|
|
|
// Probably only used by Wii
|
|
case DVDLowReportKey:
|
|
INFO_LOG(DVDINTERFACE, "DVDLowReportKey");
|
|
// Does not work on retail discs/drives
|
|
// Retail games send this command to see if they are running on real retail hw
|
|
s_error_code = ERROR_READY | ERROR_INV_CMD;
|
|
interrupt_type = INT_BRKINT;
|
|
break;
|
|
|
|
// DMA Read from Disc. Only seems to be used through direct access, not WII_IPC
|
|
case 0xA8:
|
|
switch (command_0 & 0xFF)
|
|
{
|
|
case 0x00: // Read Sector
|
|
{
|
|
u64 iDVDOffset = (u64)command_1 << 2;
|
|
|
|
INFO_LOG(DVDINTERFACE, "Read: DVDOffset=%08" PRIx64
|
|
", DMABuffer = %08x, SrcLength = %08x, DMALength = %08x",
|
|
iDVDOffset, output_address, command_2, output_length);
|
|
|
|
command_handled_by_thread = ExecuteReadCommand(
|
|
iDVDOffset, output_address, command_2, output_length, false, reply_type, &interrupt_type);
|
|
}
|
|
break;
|
|
|
|
case 0x40: // Read DiscID
|
|
INFO_LOG(DVDINTERFACE, "Read DiscID %08x", Memory::Read_U32(output_address));
|
|
command_handled_by_thread = ExecuteReadCommand(0, output_address, 0x20, output_length, false,
|
|
reply_type, &interrupt_type);
|
|
break;
|
|
|
|
default:
|
|
ERROR_LOG(DVDINTERFACE, "Unknown read subcommand: %08x", command_0);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
// Seems to be used by both GC and Wii
|
|
case DVDLowSeek:
|
|
// Currently unimplemented
|
|
INFO_LOG(DVDINTERFACE, "Seek: offset=%09" PRIx64 " (ignoring)", (u64)command_1 << 2);
|
|
break;
|
|
|
|
// Probably only used by Wii
|
|
case DVDLowReadDvd:
|
|
ERROR_LOG(DVDINTERFACE, "DVDLowReadDvd");
|
|
break;
|
|
// Probably only used by Wii
|
|
case DVDLowReadDvdConfig:
|
|
ERROR_LOG(DVDINTERFACE, "DVDLowReadDvdConfig");
|
|
break;
|
|
// Probably only used by Wii
|
|
case DVDLowStopLaser:
|
|
ERROR_LOG(DVDINTERFACE, "DVDLowStopLaser");
|
|
break;
|
|
// Probably only used by Wii
|
|
case DVDLowOffset:
|
|
ERROR_LOG(DVDINTERFACE, "DVDLowOffset");
|
|
break;
|
|
// Probably only used by Wii
|
|
case DVDLowReadDiskBca:
|
|
WARN_LOG(DVDINTERFACE, "DVDLowReadDiskBca");
|
|
Memory::Write_U32(1, output_address + 0x30);
|
|
break;
|
|
// Probably only used by Wii
|
|
case DVDLowRequestDiscStatus:
|
|
ERROR_LOG(DVDINTERFACE, "DVDLowRequestDiscStatus");
|
|
break;
|
|
// Probably only used by Wii
|
|
case DVDLowRequestRetryNumber:
|
|
ERROR_LOG(DVDINTERFACE, "DVDLowRequestRetryNumber");
|
|
break;
|
|
// Probably only used by Wii
|
|
case DVDLowSetMaximumRotation:
|
|
ERROR_LOG(DVDINTERFACE, "DVDLowSetMaximumRotation");
|
|
break;
|
|
// Probably only used by Wii
|
|
case DVDLowSerMeasControl:
|
|
ERROR_LOG(DVDINTERFACE, "DVDLowSerMeasControl");
|
|
break;
|
|
|
|
// Used by both GC and Wii
|
|
case DVDLowRequestError:
|
|
INFO_LOG(DVDINTERFACE, "Requesting error... (0x%08x)", s_error_code);
|
|
WriteImmediate(s_error_code, output_address, reply_to_ios);
|
|
s_error_code = 0;
|
|
break;
|
|
|
|
// Audio Stream (Immediate). Only seems to be used by some GC games
|
|
// (command_0 >> 16) & 0xFF = Subcommand
|
|
// command_1 << 2 = Offset on disc
|
|
// command_2 = Length of the stream
|
|
case 0xE1:
|
|
{
|
|
u8 cancel_stream = (command_0 >> 16) & 0xFF;
|
|
if (cancel_stream)
|
|
{
|
|
s_stop_at_track_end = false;
|
|
s_stream = false;
|
|
s_audio_position = 0;
|
|
s_next_start = 0;
|
|
s_next_length = 0;
|
|
s_current_start = 0;
|
|
s_current_length = 0;
|
|
}
|
|
else
|
|
{
|
|
if ((command_1 == 0) && (command_2 == 0))
|
|
{
|
|
s_stop_at_track_end = true;
|
|
}
|
|
else if (!s_stop_at_track_end)
|
|
{
|
|
s_next_start = static_cast<u64>(command_1) << 2;
|
|
s_next_length = command_2;
|
|
if (!s_stream)
|
|
{
|
|
s_current_start = s_next_start;
|
|
s_current_length = s_next_length;
|
|
s_audio_position = s_current_start;
|
|
StreamADPCM::InitFilter();
|
|
s_stream = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
INFO_LOG(DVDINTERFACE, "(Audio) Stream cmd: %08x offset: %08" PRIx64 " length: %08x", command_0,
|
|
(u64)command_1 << 2, command_2);
|
|
}
|
|
break;
|
|
|
|
// Request Audio Status (Immediate). Only seems to be used by some GC games
|
|
case 0xE2:
|
|
{
|
|
switch (command_0 >> 16 & 0xFF)
|
|
{
|
|
case 0x00: // Returns streaming status
|
|
INFO_LOG(DVDINTERFACE, "(Audio): Stream Status: Request Audio status "
|
|
"AudioPos:%08" PRIx64 "/%08" PRIx64 " "
|
|
"CurrentStart:%08" PRIx64 " CurrentLength:%08x",
|
|
s_audio_position, s_current_start + s_current_length, s_current_start,
|
|
s_current_length);
|
|
WriteImmediate(s_stream ? 1 : 0, output_address, reply_to_ios);
|
|
break;
|
|
case 0x01: // Returns the current offset
|
|
INFO_LOG(DVDINTERFACE, "(Audio): Stream Status: Request Audio status AudioPos:%08" PRIx64,
|
|
s_audio_position);
|
|
WriteImmediate(static_cast<u32>(s_audio_position >> 2), output_address, reply_to_ios);
|
|
break;
|
|
case 0x02: // Returns the start offset
|
|
INFO_LOG(DVDINTERFACE, "(Audio): Stream Status: Request Audio status CurrentStart:%08" PRIx64,
|
|
s_current_start);
|
|
WriteImmediate(static_cast<u32>(s_current_start >> 2), output_address, reply_to_ios);
|
|
break;
|
|
case 0x03: // Returns the total length
|
|
INFO_LOG(DVDINTERFACE, "(Audio): Stream Status: Request Audio status CurrentLength:%08x",
|
|
s_current_length);
|
|
WriteImmediate(static_cast<u32>(s_current_length >> 2), output_address, reply_to_ios);
|
|
break;
|
|
default:
|
|
INFO_LOG(DVDINTERFACE, "(Audio): Subcommand: %02x Request Audio status %s",
|
|
command_0 >> 16 & 0xFF, s_stream ? "on" : "off");
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case DVDLowStopMotor:
|
|
INFO_LOG(DVDINTERFACE, "DVDLowStopMotor %s %s", command_1 ? "eject" : "",
|
|
command_2 ? "kill!" : "");
|
|
|
|
if (command_1 && !command_2)
|
|
EjectDiscCallback(0, 0);
|
|
break;
|
|
|
|
// DVD Audio Enable/Disable (Immediate). GC uses this, and apparently Wii also does...?
|
|
case DVDLowAudioBufferConfig:
|
|
// For more information: http://www.crazynation.org/GC/GC_DD_TECH/GCTech.htm (dead link?)
|
|
//
|
|
// Upon Power up or reset , 2 commands must be issued for proper use of audio streaming:
|
|
// DVDReadDiskID A8000040,00000000,00000020
|
|
// DVDLowAudioBufferConfig E4xx00yy,00000000,00000020
|
|
//
|
|
// xx=byte 8 [0 or 1] from the disk header retrieved from DVDReadDiskID
|
|
// yy=0 (if xx=0) or 0xA (if xx=1)
|
|
|
|
if ((command_0 >> 16) & 0xFF)
|
|
{
|
|
// TODO: What is this actually supposed to do?
|
|
s_stream = true;
|
|
INFO_LOG(DVDINTERFACE, "(Audio): Audio enabled");
|
|
}
|
|
else
|
|
{
|
|
// TODO: What is this actually supposed to do?
|
|
s_stream = false;
|
|
INFO_LOG(DVDINTERFACE, "(Audio): Audio disabled");
|
|
}
|
|
break;
|
|
|
|
// yet another (GC?) command we prolly don't care about
|
|
case 0xEE:
|
|
INFO_LOG(DVDINTERFACE, "SetStatus");
|
|
break;
|
|
|
|
// Debug commands; see yagcd. We don't really care
|
|
// NOTE: commands to stream data will send...a raw data stream
|
|
// This will appear as unknown commands, unless the check is re-instated to catch such data.
|
|
// Can probably only be used through direct access
|
|
case 0xFE:
|
|
ERROR_LOG(DVDINTERFACE, "Unsupported DVD Drive debug command 0x%08x", command_0);
|
|
break;
|
|
|
|
// Unlock Commands. 1: "MATSHITA" 2: "DVD-GAME"
|
|
// Just for fun
|
|
// Can probably only be used through direct access
|
|
case 0xFF:
|
|
{
|
|
if (command_0 == 0xFF014D41 && command_1 == 0x54534849 && command_2 == 0x54410200)
|
|
{
|
|
INFO_LOG(DVDINTERFACE, "Unlock test 1 passed");
|
|
}
|
|
else if (command_0 == 0xFF004456 && command_1 == 0x442D4741 && command_2 == 0x4D450300)
|
|
{
|
|
INFO_LOG(DVDINTERFACE, "Unlock test 2 passed");
|
|
}
|
|
else
|
|
{
|
|
INFO_LOG(DVDINTERFACE, "Unlock test failed");
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
ERROR_LOG(DVDINTERFACE, "Unknown command 0x%08x (Buffer 0x%08x, 0x%x)", command_0,
|
|
output_address, output_length);
|
|
PanicAlertT("Unknown DVD command %08x - fatal error", command_0);
|
|
break;
|
|
}
|
|
|
|
if (!command_handled_by_thread)
|
|
{
|
|
// TODO: Needs testing to determine if COMMAND_LATENCY_US is accurate for this
|
|
CoreTiming::ScheduleEvent(COMMAND_LATENCY_US * (SystemTimers::GetTicksPerSecond() / 1000000),
|
|
s_finish_executing_command,
|
|
PackFinishExecutingCommandUserdata(reply_type, interrupt_type));
|
|
}
|
|
}
|
|
|
|
u64 PackFinishExecutingCommandUserdata(ReplyType reply_type, DIInterruptType interrupt_type)
|
|
{
|
|
return (static_cast<u64>(reply_type) << 32) + static_cast<u32>(interrupt_type);
|
|
}
|
|
|
|
void FinishExecutingCommandCallback(u64 userdata, s64 cycles_late)
|
|
{
|
|
ReplyType reply_type = static_cast<ReplyType>(userdata >> 32);
|
|
DIInterruptType interrupt_type = static_cast<DIInterruptType>(userdata & 0xFFFFFFFF);
|
|
FinishExecutingCommand(reply_type, interrupt_type, cycles_late);
|
|
}
|
|
|
|
void FinishExecutingCommand(ReplyType reply_type, DIInterruptType interrupt_type, s64 cycles_late,
|
|
const std::vector<u8>& data)
|
|
{
|
|
switch (reply_type)
|
|
{
|
|
case ReplyType::NoReply:
|
|
{
|
|
break;
|
|
}
|
|
|
|
case ReplyType::Interrupt:
|
|
{
|
|
if (s_DICR.TSTART)
|
|
{
|
|
s_DICR.TSTART = 0;
|
|
s_DILENGTH.Length = 0;
|
|
GenerateDIInterrupt(interrupt_type);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ReplyType::IOS:
|
|
{
|
|
auto di = IOS::HLE::GetDeviceByName("/dev/di");
|
|
if (di)
|
|
std::static_pointer_cast<IOS::HLE::Device::DI>(di)->FinishIOCtl(interrupt_type);
|
|
break;
|
|
}
|
|
|
|
case ReplyType::DTK:
|
|
{
|
|
DTKStreamingCallback(data, cycles_late);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determines from a given read request how much of the request is buffered,
|
|
// and how much is required to be read from disc.
|
|
void ScheduleReads(u64 offset, u32 length, bool decrypt, u32 output_address, ReplyType reply_type)
|
|
{
|
|
// The drive continues to read 1 MiB beyond the last read position when idle.
|
|
// If a future read falls within this window, part of the read may be returned
|
|
// from the buffer. Data can be transferred from the buffer at up to 16 MiB/s.
|
|
|
|
// Metroid Prime is a good example of a game that's sensitive to disc timing
|
|
// details; if there isn't enough latency in the right places, doors can open
|
|
// faster than on real hardware, and if there's too much latency in the wrong
|
|
// places, the video before the save-file select screen lags.
|
|
|
|
const u64 current_time = CoreTiming::GetTicks();
|
|
|
|
// Where the DVD read head is (usually parked at the end of the buffer,
|
|
// unless we've interrupted it mid-buffer-read).
|
|
u64 head_position;
|
|
|
|
// Compute the start (inclusive) and end (exclusive) of the buffer.
|
|
// If we fall within its bounds, we get DMA-speed reads.
|
|
u64 buffer_start, buffer_end;
|
|
|
|
// The variable offset uses the same addressing as games do.
|
|
// The variable dvd_offset tracks the actual offset on the DVD
|
|
// that the disc drive starts reading at, which differs in two ways:
|
|
// It's rounded to a whole ECC block and never uses Wii partition addressing.
|
|
u64 dvd_offset = offset;
|
|
if (decrypt)
|
|
dvd_offset = s_inserted_volume->PartitionOffsetToRawOffset(offset);
|
|
dvd_offset = Common::AlignDown(dvd_offset, DVD_ECC_BLOCK_SIZE);
|
|
|
|
if (SConfig::GetInstance().bFastDiscSpeed)
|
|
{
|
|
// The SUDTR setting makes us act as if all reads are buffered
|
|
buffer_start = std::numeric_limits<u64>::min();
|
|
buffer_end = std::numeric_limits<u64>::max();
|
|
head_position = 0;
|
|
}
|
|
else
|
|
{
|
|
if (s_read_buffer_start_time == s_read_buffer_end_time)
|
|
{
|
|
// No buffer
|
|
buffer_start = buffer_end = head_position = 0;
|
|
}
|
|
else
|
|
{
|
|
buffer_start = s_read_buffer_end_offset > STREAMING_BUFFER_SIZE ?
|
|
s_read_buffer_end_offset - STREAMING_BUFFER_SIZE :
|
|
0;
|
|
|
|
DEBUG_LOG(DVDINTERFACE,
|
|
"Buffer: now=0x%" PRIx64 " start time=0x%" PRIx64 " end time=0x%" PRIx64,
|
|
current_time, s_read_buffer_start_time, s_read_buffer_end_time);
|
|
|
|
if (current_time >= s_read_buffer_end_time)
|
|
{
|
|
// Buffer is fully read
|
|
buffer_end = s_read_buffer_end_offset;
|
|
}
|
|
else
|
|
{
|
|
// The amount of data the buffer contains *right now*, rounded to a DVD ECC block.
|
|
buffer_end = s_read_buffer_start_offset +
|
|
Common::AlignDown((current_time - s_read_buffer_start_time) *
|
|
(s_read_buffer_end_offset - s_read_buffer_start_offset) /
|
|
(s_read_buffer_end_time - s_read_buffer_start_time),
|
|
DVD_ECC_BLOCK_SIZE);
|
|
}
|
|
head_position = buffer_end;
|
|
|
|
// Reading before the buffer is not only unbuffered,
|
|
// but also destroys the old buffer for future reads.
|
|
if (dvd_offset < buffer_start)
|
|
{
|
|
// Kill the buffer, but maintain the head position for seeks.
|
|
buffer_start = buffer_end = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
DEBUG_LOG(DVDINTERFACE, "Buffer: start=0x%" PRIx64 " end=0x%" PRIx64 " avail=0x%" PRIx64,
|
|
buffer_start, buffer_end, buffer_end - buffer_start);
|
|
|
|
DEBUG_LOG(DVDINTERFACE,
|
|
"Schedule reads: offset=0x%" PRIx64 " length=0x%" PRIx32 " address=0x%" PRIx32, offset,
|
|
length, output_address);
|
|
|
|
// The DVD drive's minimum turnaround time on a command, based on a hardware test.
|
|
s64 ticks_until_completion = COMMAND_LATENCY_US * (SystemTimers::GetTicksPerSecond() / 1000000);
|
|
|
|
u32 buffered_blocks = 0;
|
|
u32 unbuffered_blocks = 0;
|
|
|
|
const u32 bytes_per_chunk =
|
|
decrypt ? DiscIO::CVolumeWiiCrypted::BLOCK_DATA_SIZE : DVD_ECC_BLOCK_SIZE;
|
|
|
|
while (length > 0)
|
|
{
|
|
// The length of this read - "+1" so that if this read is already
|
|
// aligned to a block we'll read the entire block.
|
|
u32 chunk_length = static_cast<u32>(Common::AlignUp(offset + 1, bytes_per_chunk) - offset);
|
|
|
|
// The last chunk may be short
|
|
chunk_length = std::min(chunk_length, length);
|
|
|
|
if (dvd_offset >= buffer_start && dvd_offset < buffer_end)
|
|
{
|
|
// Number of ticks it takes to transfer the data from the buffer to memory.
|
|
// TODO: This calculation is slightly wrong when decrypt is true - it uses the size of
|
|
// the copy from IOS to PPC but is supposed to model the copy from the disc drive to IOS.
|
|
ticks_until_completion +=
|
|
static_cast<u64>(chunk_length) * SystemTimers::GetTicksPerSecond() / BUFFER_TRANSFER_RATE;
|
|
buffered_blocks++;
|
|
}
|
|
else
|
|
{
|
|
// In practice we'll only ever seek if this is the first time
|
|
// through this loop.
|
|
if (dvd_offset != head_position)
|
|
{
|
|
// Unbuffered seek+read
|
|
ticks_until_completion += CalculateSeekTime(head_position, dvd_offset);
|
|
DEBUG_LOG(DVDINTERFACE, "Seek+read 0x%" PRIx32 " bytes @ 0x%" PRIx64 " ticks=%" PRId64,
|
|
chunk_length, offset, ticks_until_completion);
|
|
}
|
|
else
|
|
{
|
|
// Unbuffered read
|
|
ticks_until_completion += CalculateRawDiscReadTime(dvd_offset, DVD_ECC_BLOCK_SIZE);
|
|
}
|
|
|
|
unbuffered_blocks++;
|
|
head_position = dvd_offset + DVD_ECC_BLOCK_SIZE;
|
|
}
|
|
|
|
// Schedule this read to complete at the appropriate time
|
|
const ReplyType chunk_reply_type = chunk_length == length ? reply_type : ReplyType::NoReply;
|
|
DVDThread::StartReadToEmulatedRAM(output_address, offset, chunk_length, decrypt,
|
|
chunk_reply_type, ticks_until_completion);
|
|
|
|
// Advance the read window
|
|
output_address += chunk_length;
|
|
offset += chunk_length;
|
|
length -= chunk_length;
|
|
dvd_offset += DVD_ECC_BLOCK_SIZE;
|
|
}
|
|
|
|
// Update the buffer based on this read. Based on experimental testing,
|
|
// we will only reuse the old buffer while reading forward. Note that the
|
|
// buffer start we calculate here is not the actual start of the buffer -
|
|
// it is just the start of the portion we need to read.
|
|
const u64 last_block = dvd_offset;
|
|
if (last_block == buffer_start + DVD_ECC_BLOCK_SIZE && buffer_start != buffer_end)
|
|
{
|
|
// Special case: reading less than one block at the start of the
|
|
// buffer won't change the buffer state
|
|
}
|
|
else
|
|
{
|
|
if (last_block >= buffer_end)
|
|
// Full buffer read
|
|
s_read_buffer_start_offset = last_block;
|
|
else
|
|
// Partial buffer read
|
|
s_read_buffer_start_offset = buffer_end;
|
|
|
|
s_read_buffer_end_offset = last_block + STREAMING_BUFFER_SIZE - DVD_ECC_BLOCK_SIZE;
|
|
// Assume the buffer starts reading right after the end of the last operation
|
|
s_read_buffer_start_time = current_time + ticks_until_completion;
|
|
s_read_buffer_end_time =
|
|
s_read_buffer_start_time +
|
|
CalculateRawDiscReadTime(s_read_buffer_start_offset,
|
|
s_read_buffer_end_offset - s_read_buffer_start_offset);
|
|
}
|
|
|
|
DEBUG_LOG(DVDINTERFACE, "Schedule reads: ECC blocks unbuffered=%d, buffered=%d, "
|
|
"ticks=%" PRId64 ", time=%" PRId64 " us",
|
|
unbuffered_blocks, buffered_blocks, ticks_until_completion,
|
|
ticks_until_completion * 1000000 / SystemTimers::GetTicksPerSecond());
|
|
}
|
|
|
|
// We can approximate the relationship between a byte offset on disc and its
|
|
// radial distance from the center by using an approximation for the length of
|
|
// a rolled material, which is the area of the material divided by the pitch
|
|
// (ie: assume that you can squish and deform the area of the disc into a
|
|
// rectangle as thick as the track pitch).
|
|
//
|
|
// In practice this yields good-enough numbers as a more exact formula
|
|
// involving the integral over a polar equation (too complex to describe here)
|
|
// or the approximation of a DVD as a set of concentric circles (which is a
|
|
// better approximation, but makes futher derivations more complicated than
|
|
// they need to be).
|
|
//
|
|
// From the area approximation, we end up with this formula:
|
|
//
|
|
// L = pi*(r.outer^2-r.inner^2)/pitch
|
|
//
|
|
// Where:
|
|
// L = the data track's physical length
|
|
// r.{inner,outer} = the inner/outer radii (24 mm and 58 mm)
|
|
// pitch = the track pitch (.74 um)
|
|
//
|
|
// We can then use this equation to compute the radius for a given sector in
|
|
// the disc by mapping it along the length to a linear position and inverting
|
|
// the equation and solving for r.outer (using the DVD's r.inner and pitch)
|
|
// given that linear position:
|
|
//
|
|
// r.outer = sqrt(L * pitch / pi + r.inner^2)
|
|
//
|
|
// Where:
|
|
// L = the offset's linear position, as offset/density
|
|
// r.outer = the radius for the offset
|
|
// r.inner and pitch are the same as before.
|
|
//
|
|
// The data density of the disc is just the number of bytes addressable on a
|
|
// DVD, divided by the spiral length holding that data. offset/density yields
|
|
// the linear position for a given offset.
|
|
//
|
|
// When we put it all together and simplify, we can compute the radius for a
|
|
// given byte offset as a drastically simplified:
|
|
//
|
|
// r = sqrt(offset/total_bytes*(r.outer^2-r.inner^2) + r.inner^2)
|
|
double CalculatePhysicalDiscPosition(u64 offset)
|
|
{
|
|
// Just in case someone has an overly large disc image
|
|
// that can't exist in reality...
|
|
offset %= WII_DISC_LAYER_SIZE * 2;
|
|
|
|
// Assumption: the layout on the second disc layer is opposite of the first,
|
|
// ie layer 2 starts where layer 1 ends and goes backwards.
|
|
if (offset > WII_DISC_LAYER_SIZE)
|
|
offset = WII_DISC_LAYER_SIZE * 2 - offset;
|
|
|
|
// The track pitch here is 0.74 um, but it cancels out and we don't need it
|
|
|
|
// Note that because Wii and GC discs have identical data densities
|
|
// we can simply use the Wii numbers in both cases
|
|
return std::sqrt(
|
|
static_cast<double>(offset) / WII_DISC_LAYER_SIZE *
|
|
(WII_DVD_OUTER_RADIUS * WII_DVD_OUTER_RADIUS - DVD_INNER_RADIUS * DVD_INNER_RADIUS) +
|
|
DVD_INNER_RADIUS * DVD_INNER_RADIUS);
|
|
}
|
|
|
|
// Returns the number of ticks to move the read head from one offset to
|
|
// another, plus the number of ticks to read one ECC block immediately
|
|
// afterwards. Based on hardware testing, this appears to be a function of the
|
|
// linear distance between the radius of the first and second positions on the
|
|
// disc, though the head speed varies depending on the length of the seek.
|
|
u64 CalculateSeekTime(u64 offset_from, u64 offset_to)
|
|
{
|
|
const double position_from = CalculatePhysicalDiscPosition(offset_from);
|
|
const double position_to = CalculatePhysicalDiscPosition(offset_to);
|
|
|
|
// Seek time is roughly linear based on head distance travelled
|
|
const double distance = fabs(position_from - position_to);
|
|
|
|
double time_in_seconds;
|
|
if (distance < SHORT_SEEK_MAX_DISTANCE)
|
|
time_in_seconds = distance * SHORT_SEEK_VELOCITY_INVERSE + SHORT_SEEK_CONSTANT;
|
|
else
|
|
time_in_seconds = distance * LONG_SEEK_VELOCITY_INVERSE + LONG_SEEK_CONSTANT;
|
|
|
|
return static_cast<u64>(time_in_seconds * SystemTimers::GetTicksPerSecond());
|
|
}
|
|
|
|
// Returns the number of ticks it takes to read an amount of data from a disc,
|
|
// ignoring factors such as seek times. This is the streaming rate of the
|
|
// drive and varies between ~3-8MiB/s for Wii discs. Note that there is technically
|
|
// a DMA delay on top of this, but we model that as part of this read time.
|
|
u64 CalculateRawDiscReadTime(u64 offset, u64 length)
|
|
{
|
|
// The Wii/GC have a CAV drive and the data has a constant pit length
|
|
// regardless of location on disc. This means we can linearly interpolate
|
|
// speed from the inner to outer radius. This matches a hardware test.
|
|
// We're just picking a point halfway into the read as our benchmark for
|
|
// read speed as speeds don't change materially in this small window.
|
|
const double physical_offset = CalculatePhysicalDiscPosition(offset + length / 2);
|
|
|
|
double speed;
|
|
if (s_inserted_volume->GetVolumeType() == DiscIO::Platform::WII_DISC)
|
|
{
|
|
speed = (physical_offset - DVD_INNER_RADIUS) / (WII_DVD_OUTER_RADIUS - DVD_INNER_RADIUS) *
|
|
(WII_DISC_OUTER_READ_SPEED - WII_DISC_INNER_READ_SPEED) +
|
|
WII_DISC_INNER_READ_SPEED;
|
|
}
|
|
else
|
|
{
|
|
speed = (physical_offset - DVD_INNER_RADIUS) / (GC_DVD_OUTER_RADIUS - DVD_INNER_RADIUS) *
|
|
(GC_DISC_OUTER_READ_SPEED - GC_DISC_INNER_READ_SPEED) +
|
|
GC_DISC_INNER_READ_SPEED;
|
|
}
|
|
|
|
DEBUG_LOG(DVDINTERFACE, "Read 0x%" PRIx64 " @ 0x%" PRIx64 " @%lf mm: %lf us, %lf MiB/s", length,
|
|
offset, physical_offset * 1000, length / speed * 1000 * 1000, speed / 1024 / 1024);
|
|
|
|
// (ticks/second) / (bytes/second) * bytes = ticks
|
|
const double ticks = static_cast<double>(SystemTimers::GetTicksPerSecond()) * length / speed;
|
|
|
|
return static_cast<u64>(ticks);
|
|
}
|
|
|
|
} // namespace
|