mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-03-27 12:35:45 -05:00
Advantages: * Simpler code in general * No extra volume objects created * Now actually notices if the disc or partition gets changed while the core is running * No longer picks up on disc access done by the GUI (it used to do so as long as the core was running) * Gets rid of a Core dependency in DiscIO There are two performance disadvantages: * FileMonitor is now a bit slower when used with VolumeDirectory because FileMonitor now always uses the FileSystemGCWii code for finding filenames instead of VolumeDirectory finding the filename on its own and directly hooking into FileMonitor. But this isn't such a big deal, because it's happening on the DVD thread, and my currently unmerged file system PR will make FileSystemGCWii's file finding code about as fast as VolumeDirectory's. * FileMonitor's creation of the file system object is now done on the CPU thread instead of the DVD thread, and it will be done even if FileMonitor logging is disabled. This will be fixed in the next commit.
1484 lines
47 KiB
C++
1484 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/FileMonitor.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();
|
|
FileMonitor::SetFileSystem(nullptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
FileMonitor::SetFileSystem(nullptr);
|
|
}
|
|
|
|
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);
|
|
FileMonitor::SetFileSystem(s_inserted_volume.get());
|
|
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);
|
|
FileMonitor::SetFileSystem(s_inserted_volume.get());
|
|
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();
|
|
FileMonitor::SetFileSystem(s_inserted_volume.get());
|
|
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();
|
|
const bool success = s_inserted_volume->ChangePartition(offset);
|
|
FileMonitor::SetFileSystem(s_inserted_volume.get());
|
|
return success;
|
|
}
|
|
|
|
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
|