mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-05-11 13:07:08 +03:00
1079 lines
28 KiB
C++
1079 lines
28 KiB
C++
// Copyright 2013 Dolphin Emulator Project
|
|
// Licensed under GPLv2
|
|
// Refer to the license.txt file included.
|
|
|
|
#include <cinttypes>
|
|
|
|
#include "AudioCommon/AudioCommon.h"
|
|
|
|
#include "Common/ChunkFile.h"
|
|
#include "Common/Common.h"
|
|
#include "Common/Thread.h"
|
|
|
|
#include "Core/ConfigManager.h"
|
|
#include "Core/CoreTiming.h"
|
|
#include "Core/Movie.h"
|
|
#include "Core/VolumeHandler.h"
|
|
#include "Core/HW/AudioInterface.h"
|
|
#include "Core/HW/DVDInterface.h"
|
|
#include "Core/HW/Memmap.h"
|
|
#include "Core/HW/MMIO.h"
|
|
#include "Core/HW/ProcessorInterface.h"
|
|
#include "Core/HW/StreamADPCM.h"
|
|
#include "Core/HW/SystemTimers.h"
|
|
#include "Core/PowerPC/PowerPC.h"
|
|
|
|
// A GameCube disc can be read at somewhere between
|
|
// 2 and 3MB/sec, depending on the location on disk. Wii disks
|
|
// not yet tested.
|
|
static const u32 DISC_TRANSFER_RATE_GC = 3 * 1024 * 1024;
|
|
|
|
// Rate the drive can transfer data to main memory, given the data
|
|
// is already buffered.
|
|
static const u32 BUFFER_TRANSFER_RATE_GC = 16 * 1024 * 1024;
|
|
|
|
// Disc access time measured in milliseconds
|
|
static const u32 DISC_ACCESS_TIME_MS = 50;
|
|
|
|
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
|
|
};
|
|
|
|
|
|
// DVD IntteruptTypes
|
|
enum DI_InterruptType
|
|
{
|
|
INT_DEINT = 0,
|
|
INT_TCINT = 1,
|
|
INT_BRKINT = 2,
|
|
INT_CVRINT = 3,
|
|
};
|
|
|
|
// 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;}
|
|
};
|
|
|
|
|
|
// STATE_TO_SAVE
|
|
// hardware registers
|
|
static UDISR m_DISR;
|
|
static UDICVR m_DICVR;
|
|
static UDICMDBUF m_DICMDBUF[3];
|
|
static UDIMAR m_DIMAR;
|
|
static UDILENGTH m_DILENGTH;
|
|
static UDICR m_DICR;
|
|
static UDIIMMBUF m_DIIMMBUF;
|
|
static UDICFG m_DICFG;
|
|
|
|
static u32 LoopStart;
|
|
static u32 AudioPos;
|
|
static u32 CurrentStart;
|
|
static u32 LoopLength;
|
|
static u32 CurrentLength;
|
|
|
|
u32 g_ErrorCode = 0;
|
|
bool g_bDiscInside = false;
|
|
bool g_bStream = false;
|
|
int tc = 0;
|
|
int dtk = 0;
|
|
|
|
static u64 g_last_read_offset;
|
|
static u64 g_last_read_time;
|
|
|
|
// GC-AM only
|
|
static unsigned char media_buffer[0x40];
|
|
|
|
static int ejectDisc;
|
|
static int insertDisc;
|
|
|
|
void EjectDiscCallback(u64 userdata, int cyclesLate);
|
|
void InsertDiscCallback(u64 userdata, int cyclesLate);
|
|
|
|
void UpdateInterrupts();
|
|
void GenerateDIInterrupt(DI_InterruptType _DVDInterrupt);
|
|
void ExecuteCommand();
|
|
void FinishExecuteRead();
|
|
|
|
void DoState(PointerWrap &p)
|
|
{
|
|
p.DoPOD(m_DISR);
|
|
p.DoPOD(m_DICVR);
|
|
p.DoArray(m_DICMDBUF, 3);
|
|
p.Do(m_DIMAR);
|
|
p.Do(m_DILENGTH);
|
|
p.Do(m_DICR);
|
|
p.Do(m_DIIMMBUF);
|
|
p.DoPOD(m_DICFG);
|
|
|
|
p.Do(LoopStart);
|
|
p.Do(AudioPos);
|
|
p.Do(LoopLength);
|
|
|
|
p.Do(g_ErrorCode);
|
|
p.Do(g_bDiscInside);
|
|
p.Do(g_bStream);
|
|
|
|
p.Do(CurrentStart);
|
|
p.Do(CurrentLength);
|
|
|
|
p.Do(g_last_read_offset);
|
|
p.Do(g_last_read_time);
|
|
}
|
|
|
|
static void TransferComplete(u64 userdata, int cyclesLate)
|
|
{
|
|
if (m_DICR.TSTART)
|
|
FinishExecuteRead();
|
|
}
|
|
|
|
static u32 ProcessDTKSamples(short *tempPCM, u32 num_samples)
|
|
{
|
|
u32 samples_processed = 0;
|
|
do
|
|
{
|
|
if (AudioPos >= CurrentStart + CurrentLength)
|
|
{
|
|
AudioPos = LoopStart;
|
|
CurrentStart = LoopStart;
|
|
CurrentLength = LoopLength;
|
|
NGCADPCM::InitFilter();
|
|
AudioInterface::GenerateAISInterrupt();
|
|
|
|
// If there isn't any audio to stream, stop streaming.
|
|
if (AudioPos >= CurrentStart + CurrentLength)
|
|
{
|
|
g_bStream = false;
|
|
}
|
|
break;
|
|
}
|
|
|
|
u8 tempADPCM[NGCADPCM::ONE_BLOCK_SIZE];
|
|
// TODO: What if we can't read from AudioPos?
|
|
VolumeHandler::ReadToPtr(tempADPCM, AudioPos, sizeof(tempADPCM));
|
|
AudioPos += sizeof(tempADPCM);
|
|
NGCADPCM::DecodeBlock(tempPCM + samples_processed * 2, tempADPCM);
|
|
samples_processed += NGCADPCM::SAMPLES_PER_BLOCK;
|
|
} while (samples_processed < num_samples);
|
|
for (unsigned i = 0; i < samples_processed * 2; ++i)
|
|
{
|
|
// TODO: Fix the mixer so it can accept non-byte-swapped samples.
|
|
tempPCM[i] = Common::swap16(tempPCM[i]);
|
|
}
|
|
return samples_processed;
|
|
}
|
|
|
|
static void DTKStreamingCallback(u64 userdata, int cyclesLate)
|
|
{
|
|
// Send audio to the mixer.
|
|
static const int NUM_SAMPLES = 48000 / 2000 * 7; // 3.5ms of 48kHz samples
|
|
short tempPCM[NUM_SAMPLES * 2];
|
|
unsigned samples_processed;
|
|
if (g_bStream)
|
|
{
|
|
samples_processed = ProcessDTKSamples(tempPCM, NUM_SAMPLES);
|
|
}
|
|
else
|
|
{
|
|
memset(tempPCM, 0, sizeof(tempPCM));
|
|
samples_processed = NUM_SAMPLES;
|
|
}
|
|
soundStream->GetMixer()->PushStreamingSamples(tempPCM, samples_processed);
|
|
|
|
int ticks_to_dtk = int(SystemTimers::GetTicksPerSecond() * u64(samples_processed) / 48000);
|
|
CoreTiming::ScheduleEvent(ticks_to_dtk - cyclesLate, dtk);
|
|
}
|
|
|
|
void Init()
|
|
{
|
|
m_DISR.Hex = 0;
|
|
m_DICVR.Hex = 0;
|
|
m_DICMDBUF[0].Hex = 0;
|
|
m_DICMDBUF[1].Hex = 0;
|
|
m_DICMDBUF[2].Hex = 0;
|
|
m_DIMAR.Hex = 0;
|
|
m_DILENGTH.Hex = 0;
|
|
m_DICR.Hex = 0;
|
|
m_DIIMMBUF.Hex = 0;
|
|
m_DICFG.Hex = 0;
|
|
m_DICFG.CONFIG = 1; // Disable bootrom descrambler
|
|
|
|
AudioPos = 0;
|
|
LoopStart = 0;
|
|
LoopLength = 0;
|
|
CurrentStart = 0;
|
|
CurrentLength = 0;
|
|
|
|
g_bStream = false;
|
|
|
|
ejectDisc = CoreTiming::RegisterEvent("EjectDisc", EjectDiscCallback);
|
|
insertDisc = CoreTiming::RegisterEvent("InsertDisc", InsertDiscCallback);
|
|
|
|
tc = CoreTiming::RegisterEvent("TransferComplete", TransferComplete);
|
|
dtk = CoreTiming::RegisterEvent("StreamingTimer", DTKStreamingCallback);
|
|
|
|
CoreTiming::ScheduleEvent(0, dtk);
|
|
}
|
|
|
|
void Shutdown()
|
|
{
|
|
}
|
|
|
|
void SetDiscInside(bool _DiscInside)
|
|
{
|
|
g_bDiscInside = _DiscInside;
|
|
}
|
|
|
|
bool IsDiscInside()
|
|
{
|
|
return g_bDiscInside;
|
|
}
|
|
|
|
// 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
|
|
void EjectDiscCallback(u64 userdata, int cyclesLate)
|
|
{
|
|
// Empty the drive
|
|
SetDiscInside(false);
|
|
SetLidOpen();
|
|
VolumeHandler::EjectVolume();
|
|
}
|
|
|
|
void InsertDiscCallback(u64 userdata, int cyclesLate)
|
|
{
|
|
std::string& SavedFileName = SConfig::GetInstance().m_LocalCoreStartupParameter.m_strFilename;
|
|
std::string *_FileName = (std::string *)userdata;
|
|
|
|
if (!VolumeHandler::SetVolumeName(*_FileName))
|
|
{
|
|
// Put back the old one
|
|
VolumeHandler::SetVolumeName(SavedFileName);
|
|
PanicAlertT("Invalid file");
|
|
}
|
|
SetLidOpen(false);
|
|
SetDiscInside(VolumeHandler::IsValid());
|
|
delete _FileName;
|
|
}
|
|
|
|
void ChangeDisc(const std::string& newFileName)
|
|
{
|
|
std::string* _FileName = new std::string(newFileName);
|
|
CoreTiming::ScheduleEvent_Threadsafe(0, ejectDisc);
|
|
CoreTiming::ScheduleEvent_Threadsafe(500000000, insertDisc, (u64)_FileName);
|
|
if (Movie::IsRecordingInput())
|
|
{
|
|
Movie::g_bDiscChange = true;
|
|
std::string fileName = newFileName;
|
|
auto sizeofpath = fileName.find_last_of("/\\") + 1;
|
|
if (fileName.substr(sizeofpath).length() > 40)
|
|
{
|
|
PanicAlert("Saving iso filename to .dtm failed; max file name length is 40 characters.");
|
|
}
|
|
Movie::g_discChange = fileName.substr(sizeofpath);
|
|
}
|
|
}
|
|
|
|
void SetLidOpen(bool _bOpen)
|
|
{
|
|
m_DICVR.CVR = _bOpen ? 1 : 0;
|
|
|
|
GenerateDIInterrupt(INT_CVRINT);
|
|
}
|
|
|
|
bool IsLidOpen()
|
|
{
|
|
return (m_DICVR.CVR == 1);
|
|
}
|
|
|
|
void ClearCoverInterrupt()
|
|
{
|
|
m_DICVR.CVRINT = 0;
|
|
}
|
|
|
|
bool DVDRead(u32 _iDVDOffset, u32 _iRamAddress, u32 _iLength)
|
|
{
|
|
return VolumeHandler::ReadToPtr(Memory::GetPointer(_iRamAddress), _iDVDOffset, _iLength);
|
|
}
|
|
|
|
void RegisterMMIO(MMIO::Mapping* mmio, u32 base)
|
|
{
|
|
mmio->Register(base | DI_STATUS_REGISTER,
|
|
MMIO::DirectRead<u32>(&m_DISR.Hex),
|
|
MMIO::ComplexWrite<u32>([](u32, u32 val) {
|
|
UDISR tmpStatusReg(val);
|
|
|
|
m_DISR.DEINITMASK = tmpStatusReg.DEINITMASK;
|
|
m_DISR.TCINTMASK = tmpStatusReg.TCINTMASK;
|
|
m_DISR.BRKINTMASK = tmpStatusReg.BRKINTMASK;
|
|
m_DISR.BREAK = tmpStatusReg.BREAK;
|
|
|
|
if (tmpStatusReg.DEINT)
|
|
m_DISR.DEINT = 0;
|
|
|
|
if (tmpStatusReg.TCINT)
|
|
m_DISR.TCINT = 0;
|
|
|
|
if (tmpStatusReg.BRKINT)
|
|
m_DISR.BRKINT = 0;
|
|
|
|
if (m_DISR.BREAK)
|
|
{
|
|
_dbg_assert_(DVDINTERFACE, 0);
|
|
}
|
|
|
|
UpdateInterrupts();
|
|
})
|
|
);
|
|
|
|
mmio->Register(base | DI_COVER_REGISTER,
|
|
MMIO::DirectRead<u32>(&m_DICVR.Hex),
|
|
MMIO::ComplexWrite<u32>([](u32, u32 val) {
|
|
UDICVR tmpCoverReg(val);
|
|
|
|
m_DICVR.CVRINTMASK = tmpCoverReg.CVRINTMASK;
|
|
|
|
if (tmpCoverReg.CVRINT)
|
|
m_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>(&m_DICMDBUF[i].Hex),
|
|
MMIO::DirectWrite<u32>(&m_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>(&m_DIMAR.Hex),
|
|
MMIO::DirectWrite<u32>(&m_DIMAR.Hex, ~0xFC00001F)
|
|
);
|
|
mmio->Register(base | DI_DMA_LENGTH_REGISTER,
|
|
MMIO::DirectRead<u32>(&m_DILENGTH.Hex),
|
|
MMIO::DirectWrite<u32>(&m_DILENGTH.Hex, ~0x1F)
|
|
);
|
|
mmio->Register(base | DI_DMA_CONTROL_REGISTER,
|
|
MMIO::DirectRead<u32>(&m_DICR.Hex),
|
|
MMIO::ComplexWrite<u32>([](u32, u32 val) {
|
|
m_DICR.Hex = val & 7;
|
|
if (m_DICR.TSTART)
|
|
{
|
|
ExecuteCommand();
|
|
}
|
|
})
|
|
);
|
|
|
|
mmio->Register(base | DI_IMMEDIATE_DATA_BUFFER,
|
|
MMIO::DirectRead<u32>(&m_DIIMMBUF.Hex),
|
|
MMIO::DirectWrite<u32>(&m_DIIMMBUF.Hex)
|
|
);
|
|
|
|
// DI config register is read only.
|
|
mmio->Register(base | DI_CONFIG_REGISTER,
|
|
MMIO::DirectRead<u32>(&m_DICFG.Hex),
|
|
MMIO::InvalidWrite<u32>()
|
|
);
|
|
}
|
|
|
|
void UpdateInterrupts()
|
|
{
|
|
if ((m_DISR.DEINT & m_DISR.DEINITMASK) ||
|
|
(m_DISR.TCINT & m_DISR.TCINTMASK) ||
|
|
(m_DISR.BRKINT & m_DISR.BRKINTMASK) ||
|
|
(m_DICVR.CVRINT & m_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(DI_InterruptType _DVDInterrupt)
|
|
{
|
|
switch (_DVDInterrupt)
|
|
{
|
|
case INT_DEINT: m_DISR.DEINT = 1; break;
|
|
case INT_TCINT: m_DISR.TCINT = 1; break;
|
|
case INT_BRKINT: m_DISR.BRKINT = 1; break;
|
|
case INT_CVRINT: m_DICVR.CVRINT = 1; break;
|
|
}
|
|
|
|
UpdateInterrupts();
|
|
}
|
|
|
|
void ExecuteCommand()
|
|
{
|
|
// _dbg_assert_(DVDINTERFACE, _DICR.RW == 0); // only DVD to Memory
|
|
int GCAM = ((SConfig::GetInstance().m_SIDevice[0] == SIDEVICE_AM_BASEBOARD) &&
|
|
(SConfig::GetInstance().m_EXIDevice[2] == EXIDEVICE_AM_BASEBOARD))
|
|
? 1 : 0;
|
|
|
|
if (GCAM)
|
|
{
|
|
ERROR_LOG(DVDINTERFACE,
|
|
"DVD: %08x, %08x, %08x, DMA=addr:%08x,len:%08x,ctrl:%08x",
|
|
m_DICMDBUF[0].Hex, m_DICMDBUF[1].Hex, m_DICMDBUF[2].Hex,
|
|
m_DIMAR.Hex, m_DILENGTH.Hex, m_DICR.Hex);
|
|
// decrypt command. But we have a zero key, that simplifies things a lot.
|
|
// If you get crazy dvd command errors, make sure 0x80000000 - 0x8000000c is zero'd
|
|
m_DICMDBUF[0].Hex <<= 24;
|
|
}
|
|
|
|
|
|
switch (m_DICMDBUF[0].CMDBYTE0)
|
|
{
|
|
case DVDLowInquiry:
|
|
if (GCAM)
|
|
{
|
|
// 0x29484100...
|
|
// was 21 i'm not entirely sure about this, but it works well.
|
|
m_DIIMMBUF.Hex = 0x21000000;
|
|
}
|
|
else
|
|
{
|
|
// small safety check, dunno if it's needed
|
|
if ((m_DICMDBUF[1].Hex == 0) && (m_DILENGTH.Length == 0x20))
|
|
{
|
|
u8* driveInfo = Memory::GetPointer(m_DIMAR.Address);
|
|
// gives the correct output in GCOS - 06 2001/08 (61)
|
|
// there may be other stuff missing ?
|
|
driveInfo[4] = 0x20;
|
|
driveInfo[5] = 0x01;
|
|
driveInfo[6] = 0x06;
|
|
driveInfo[7] = 0x08;
|
|
driveInfo[8] = 0x61;
|
|
|
|
// Just for fun
|
|
INFO_LOG(DVDINTERFACE, "Drive Info: %02x %02x%02x/%02x (%02x)",
|
|
driveInfo[6], driveInfo[4], driveInfo[5], driveInfo[7], driveInfo[8]);
|
|
}
|
|
}
|
|
break;
|
|
|
|
// "Set Extension"...not sure what it does
|
|
case 0x55:
|
|
INFO_LOG(DVDINTERFACE, "SetExtension");
|
|
break;
|
|
|
|
// DMA Read from Disc
|
|
case 0xA8:
|
|
if (g_bDiscInside)
|
|
{
|
|
switch (m_DICMDBUF[0].CMDBYTE3)
|
|
{
|
|
case 0x00: // Read Sector
|
|
{
|
|
u32 iDVDOffset = m_DICMDBUF[1].Hex << 2;
|
|
|
|
DEBUG_LOG(DVDINTERFACE, "Read: DVDOffset=%08x, DMABuffer=%08x, SrcLength=%08x, DMALength=%08x",
|
|
iDVDOffset, m_DIMAR.Address, m_DICMDBUF[2].Hex, m_DILENGTH.Length);
|
|
_dbg_assert_(DVDINTERFACE, m_DICMDBUF[2].Hex == m_DILENGTH.Length);
|
|
|
|
if (GCAM)
|
|
{
|
|
if (iDVDOffset & 0x80000000) // read request to hardware buffer
|
|
{
|
|
u32 len = m_DILENGTH.Length / 4;
|
|
switch (iDVDOffset)
|
|
{
|
|
case 0x80000000:
|
|
ERROR_LOG(DVDINTERFACE, "GC-AM: READ MEDIA BOARD STATUS (80000000)");
|
|
for (u32 i = 0; i < len; i++)
|
|
Memory::Write_U32(0, m_DIMAR.Address + i * 4);
|
|
break;
|
|
case 0x80000040:
|
|
ERROR_LOG(DVDINTERFACE, "GC-AM: READ MEDIA BOARD STATUS (2) (80000040)");
|
|
for (u32 i = 0; i < len; i++)
|
|
Memory::Write_U32(~0, m_DIMAR.Address + i * 4);
|
|
Memory::Write_U32(0x00000020, m_DIMAR.Address); // DIMM SIZE, LE
|
|
Memory::Write_U32(0x4743414D, m_DIMAR.Address + 4); // GCAM signature
|
|
break;
|
|
case 0x80000120:
|
|
ERROR_LOG(DVDINTERFACE, "GC-AM: READ FIRMWARE STATUS (80000120)");
|
|
for (u32 i = 0; i < len; i++)
|
|
Memory::Write_U32(0x01010101, m_DIMAR.Address + i * 4);
|
|
break;
|
|
case 0x80000140:
|
|
ERROR_LOG(DVDINTERFACE, "GC-AM: READ FIRMWARE STATUS (80000140)");
|
|
for (u32 i = 0; i < len; i++)
|
|
Memory::Write_U32(0x01010101, m_DIMAR.Address + i * 4);
|
|
break;
|
|
case 0x84000020:
|
|
ERROR_LOG(DVDINTERFACE, "GC-AM: READ MEDIA BOARD STATUS (1) (84000020)");
|
|
for (u32 i = 0; i < len; i++)
|
|
Memory::Write_U32(0x00000000, m_DIMAR.Address + i * 4);
|
|
break;
|
|
default:
|
|
ERROR_LOG(DVDINTERFACE, "GC-AM: UNKNOWN MEDIA BOARD LOCATION %x", iDVDOffset);
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
else if ((iDVDOffset == 0x1f900000) || (iDVDOffset == 0x1f900020))
|
|
{
|
|
ERROR_LOG(DVDINTERFACE, "GC-AM: READ MEDIA BOARD COMM AREA (1f900020)");
|
|
memcpy(Memory::GetPointer(m_DIMAR.Address), media_buffer + iDVDOffset - 0x1f900000, m_DILENGTH.Length);
|
|
for (u32 i = 0; i < m_DILENGTH.Length; i += 4)
|
|
ERROR_LOG(DVDINTERFACE, "GC-AM: %08x", Memory::Read_U32(m_DIMAR.Address + i));
|
|
break;
|
|
}
|
|
}
|
|
|
|
u64 ticksUntilTC = 0;
|
|
|
|
// The drive buffers 1MB (?) of data after every read request;
|
|
// if a read request is covered by this buffer (or if it's
|
|
// faster to wait for the data to be buffered), the drive
|
|
// doesn't seek; it returns buffered data. Data can be
|
|
// transferred from the buffer at up to 16MB/sec.
|
|
//
|
|
// If the drive has to seek, the time this takes varies a lot.
|
|
// A short seek is around 50ms; a long seek is around 150ms.
|
|
// However, the time isn't purely dependent on the distance; the
|
|
// pattern of previous seeks seems to matter in a way I'm
|
|
// not sure how to explain.
|
|
//
|
|
// Metroid Prime is a good example of a game that's sensitive to
|
|
// all of these details; if there isn't enough latency in the
|
|
// right places, doors open too quickly, and if there's too
|
|
// much latency in the wrong places, the video before the
|
|
// save-file select screen lags.
|
|
//
|
|
// For now, just use a very rough approximation: 50ms seek
|
|
// and 3MB/sec for reads outside 1MB, acceleated reads
|
|
// within 1MB. We can refine this if someone comes up
|
|
// with a more complete model for seek times.
|
|
|
|
u64 cur_time = CoreTiming::GetTicks();
|
|
// Number of ticks it takes to seek and read directly from the disk.
|
|
u64 disk_read_duration = m_DILENGTH.Length *
|
|
(SystemTimers::GetTicksPerSecond() / DISC_TRANSFER_RATE_GC) +
|
|
SystemTimers::GetTicksPerSecond() / 1000 * DISC_ACCESS_TIME_MS;
|
|
|
|
if (iDVDOffset + m_DILENGTH.Length - g_last_read_offset > 1024 * 1024)
|
|
{
|
|
// No buffer; just use the simple seek time + read time.
|
|
DEBUG_LOG(DVDINTERFACE, "Seeking %" PRId64 " bytes", s64(g_last_read_offset) - s64(iDVDOffset));
|
|
ticksUntilTC = disk_read_duration;
|
|
g_last_read_time = cur_time + ticksUntilTC;
|
|
}
|
|
else
|
|
{
|
|
// Possibly buffered; use the buffer if it saves time.
|
|
// It's not proven that the buffer actually behaves like this, but
|
|
// it appears to be a decent approximation.
|
|
|
|
// Time at which the buffer will contain the data we need.
|
|
u64 buffer_fill_time = (iDVDOffset + m_DILENGTH.Length - g_last_read_offset) *
|
|
(SystemTimers::GetTicksPerSecond() / DISC_TRANSFER_RATE_GC) +
|
|
g_last_read_time;
|
|
// Number of ticks it takes to transfer the data from the buffer to memory.
|
|
u64 buffer_read_duration = m_DILENGTH.Length *
|
|
(SystemTimers::GetTicksPerSecond() / BUFFER_TRANSFER_RATE_GC);
|
|
|
|
if (cur_time > buffer_fill_time)
|
|
{
|
|
DEBUG_LOG(DVDINTERFACE, "Fast buffer read at %" PRId64, s64(iDVDOffset));
|
|
ticksUntilTC = buffer_read_duration;
|
|
g_last_read_time = buffer_fill_time;
|
|
}
|
|
else if (cur_time + disk_read_duration > buffer_fill_time)
|
|
{
|
|
DEBUG_LOG(DVDINTERFACE, "Slow buffer read at %" PRId64, s64(iDVDOffset));
|
|
ticksUntilTC = std::max(buffer_fill_time - cur_time, buffer_read_duration);
|
|
g_last_read_time = buffer_fill_time;
|
|
}
|
|
else
|
|
{
|
|
DEBUG_LOG(DVDINTERFACE, "Short seek %" PRId64 " bytes", s64(g_last_read_offset) - s64(iDVDOffset));
|
|
ticksUntilTC = disk_read_duration;
|
|
g_last_read_time = cur_time + ticksUntilTC;
|
|
}
|
|
}
|
|
g_last_read_offset = (iDVDOffset + m_DILENGTH.Length - 2048) & ~2047;
|
|
|
|
if (SConfig::GetInstance().m_LocalCoreStartupParameter.bFastDiscSpeed)
|
|
{
|
|
// Make sure fast disc speed performs "instant" reads; in addition
|
|
// to being used to speed up games, fast disc speed is used as a
|
|
// workaround for crashes in certain games, including Star Wars
|
|
// Rogue Leader.
|
|
FinishExecuteRead();
|
|
return;
|
|
}
|
|
|
|
CoreTiming::ScheduleEvent((int)ticksUntilTC, tc);
|
|
|
|
// Early return; we'll finish executing the command in FinishExecuteRead.
|
|
return;
|
|
}
|
|
break;
|
|
|
|
case 0x40: // Read DiscID
|
|
_dbg_assert_(DVDINTERFACE, m_DICMDBUF[1].Hex == 0);
|
|
_dbg_assert_(DVDINTERFACE, m_DICMDBUF[2].Hex == m_DILENGTH.Length);
|
|
_dbg_assert_(DVDINTERFACE, m_DILENGTH.Length == 0x20);
|
|
if (!DVDRead(m_DICMDBUF[1].Hex, m_DIMAR.Address, m_DILENGTH.Length))
|
|
PanicAlertT("Can't read from DVD_Plugin - DVD-Interface: Fatal Error");
|
|
WARN_LOG(DVDINTERFACE, "Read DiscID %08x", Memory::Read_U32(m_DIMAR.Address));
|
|
break;
|
|
|
|
default:
|
|
_dbg_assert_msg_(DVDINTERFACE, 0, "Unknown Read Subcommand");
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// there is no disc to read
|
|
m_DICR.TSTART = 0;
|
|
m_DILENGTH.Length = 0;
|
|
g_ErrorCode = ERROR_NO_DISK | ERROR_COVER_H;
|
|
GenerateDIInterrupt(INT_DEINT);
|
|
return;
|
|
}
|
|
break;
|
|
|
|
// GC-AM
|
|
case 0xAA:
|
|
if (GCAM)
|
|
{
|
|
ERROR_LOG(DVDINTERFACE, "GC-AM: 0xAA, DMABuffer=%08x, DMALength=%08x", m_DIMAR.Address, m_DILENGTH.Length);
|
|
u32 iDVDOffset = m_DICMDBUF[1].Hex << 2;
|
|
unsigned int len = m_DILENGTH.Length;
|
|
int offset = iDVDOffset - 0x1F900000;
|
|
/*
|
|
if (iDVDOffset == 0x84800000)
|
|
{
|
|
ERROR_LOG(DVDINTERFACE, "Firmware upload");
|
|
}
|
|
else*/
|
|
if ((offset < 0) || ((offset + len) > 0x40) || len > 0x40)
|
|
{
|
|
u32 addr = m_DIMAR.Address;
|
|
if (iDVDOffset == 0x84800000)
|
|
{
|
|
ERROR_LOG(DVDINTERFACE, "FIRMWARE UPLOAD");
|
|
}
|
|
else
|
|
{
|
|
ERROR_LOG(DVDINTERFACE, "ILLEGAL MEDIA WRITE");
|
|
}
|
|
|
|
while (len >= 4)
|
|
{
|
|
ERROR_LOG(DVDINTERFACE, "GC-AM Media Board WRITE (0xAA): %08x: %08x", iDVDOffset, Memory::Read_U32(addr));
|
|
addr += 4;
|
|
len -= 4;
|
|
iDVDOffset += 4;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
u32 addr = m_DIMAR.Address;
|
|
memcpy(media_buffer + offset, Memory::GetPointer(addr), len);
|
|
while (len >= 4)
|
|
{
|
|
ERROR_LOG(DVDINTERFACE, "GC-AM Media Board WRITE (0xAA): %08x: %08x", iDVDOffset, Memory::Read_U32(addr));
|
|
addr += 4;
|
|
len -= 4;
|
|
iDVDOffset += 4;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
// Seek (immediate)
|
|
case DVDLowSeek:
|
|
if (!GCAM)
|
|
{
|
|
// We don't care :)
|
|
DEBUG_LOG(DVDINTERFACE, "Seek: offset=%08x (ignoring)", m_DICMDBUF[1].Hex << 2);
|
|
}
|
|
else
|
|
{
|
|
memset(media_buffer, 0, 0x20);
|
|
media_buffer[0] = media_buffer[0x20]; // ID
|
|
media_buffer[2] = media_buffer[0x22];
|
|
media_buffer[3] = media_buffer[0x23] | 0x80;
|
|
int cmd = (media_buffer[0x23]<<8)|media_buffer[0x22];
|
|
ERROR_LOG(DVDINTERFACE, "GC-AM: execute buffer, cmd=%04x", cmd);
|
|
switch (cmd)
|
|
{
|
|
case 0x00:
|
|
media_buffer[4] = 1;
|
|
break;
|
|
case 0x1:
|
|
media_buffer[7] = 0x20; // DIMM Size
|
|
break;
|
|
case 0x100:
|
|
{
|
|
// urgh
|
|
static int percentage = 0;
|
|
static int status = 0;
|
|
percentage++;
|
|
if (percentage > 100)
|
|
{
|
|
status++;
|
|
percentage = 0;
|
|
}
|
|
media_buffer[4] = status;
|
|
/* status:
|
|
0 - "Initializing media board. Please wait.."
|
|
1 - "Checking network. Please wait..."
|
|
2 - "Found a system disc. Insert a game disc"
|
|
3 - "Testing a game program. %d%%"
|
|
4 - "Loading a game program. %d%%"
|
|
5 - go
|
|
6 - error xx
|
|
*/
|
|
media_buffer[8] = percentage;
|
|
media_buffer[4] = 0x05;
|
|
media_buffer[8] = 0x64;
|
|
break;
|
|
}
|
|
case 0x101:
|
|
media_buffer[4] = 3; // version
|
|
media_buffer[5] = 3;
|
|
media_buffer[6] = 1; // xxx
|
|
media_buffer[8] = 1;
|
|
media_buffer[16] = 0xFF;
|
|
media_buffer[17] = 0xFF;
|
|
media_buffer[18] = 0xFF;
|
|
media_buffer[19] = 0xFF;
|
|
break;
|
|
case 0x102: // get error code
|
|
media_buffer[4] = 1; // 0: download incomplete (31), 1: corrupted, other error 1
|
|
media_buffer[5] = 0;
|
|
break;
|
|
case 0x103:
|
|
memcpy(media_buffer + 4, "A89E27A50364511", 15); // serial
|
|
break;
|
|
#if 0
|
|
case 0x301: // unknown
|
|
memcpy(media_buffer + 4, media_buffer + 0x24, 0x1c);
|
|
break;
|
|
case 0x302:
|
|
break;
|
|
#endif
|
|
default:
|
|
ERROR_LOG(DVDINTERFACE, "GC-AM: execute buffer (unknown)");
|
|
break;
|
|
}
|
|
memset(media_buffer + 0x20, 0, 0x20);
|
|
m_DIIMMBUF.Hex = 0x66556677; // just a random value that works.
|
|
}
|
|
break;
|
|
|
|
case DVDLowOffset:
|
|
DEBUG_LOG(DVDINTERFACE, "DVDLowOffset: ignoring...");
|
|
break;
|
|
|
|
// Request Error Code
|
|
case DVDLowRequestError:
|
|
ERROR_LOG(DVDINTERFACE, "Requesting error... (0x%08x)", g_ErrorCode);
|
|
m_DIIMMBUF.Hex = g_ErrorCode;
|
|
break;
|
|
|
|
// Audio Stream (Immediate)
|
|
// m_DICMDBUF[0].CMDBYTE1 = Subcommand
|
|
// m_DICMDBUF[1].Hex << 2 = Offset on disc
|
|
// m_DICMDBUF[2].Hex = Length of the stream
|
|
case 0xE1:
|
|
{
|
|
u32 pos = m_DICMDBUF[1].Hex << 2;
|
|
u32 length = m_DICMDBUF[2].Hex;
|
|
|
|
// Start playing
|
|
if (!g_bStream && m_DICMDBUF[0].CMDBYTE1 == 0 && pos != 0 && length != 0)
|
|
{
|
|
AudioPos = pos;
|
|
CurrentStart = pos;
|
|
CurrentLength = length;
|
|
NGCADPCM::InitFilter();
|
|
}
|
|
|
|
LoopStart = pos;
|
|
LoopLength = length;
|
|
g_bStream = (m_DICMDBUF[0].CMDBYTE1 == 0); // This command can start/stop the stream
|
|
|
|
// Stop stream
|
|
if (m_DICMDBUF[0].CMDBYTE1 == 1)
|
|
{
|
|
AudioPos = 0;
|
|
LoopStart = 0;
|
|
LoopLength = 0;
|
|
CurrentStart = 0;
|
|
CurrentLength = 0;
|
|
}
|
|
|
|
WARN_LOG(DVDINTERFACE, "(Audio) Stream subcmd = %08x offset = %08x length=%08x",
|
|
m_DICMDBUF[0].Hex, m_DICMDBUF[1].Hex << 2, m_DICMDBUF[2].Hex);
|
|
}
|
|
break;
|
|
|
|
// Request Audio Status (Immediate)
|
|
case 0xE2:
|
|
{
|
|
switch (m_DICMDBUF[0].CMDBYTE1)
|
|
{
|
|
case 0x00: // Returns streaming status
|
|
m_DIIMMBUF.Hex = (AudioPos == 0) ? 0 : 1;
|
|
break;
|
|
case 0x01: // Returns the current offset
|
|
if (g_bStream)
|
|
m_DIIMMBUF.Hex = (AudioPos - CurrentStart) >> 2;
|
|
else
|
|
m_DIIMMBUF.Hex = 0;
|
|
break;
|
|
case 0x02: // Returns the start offset
|
|
if (g_bStream)
|
|
m_DIIMMBUF.Hex = CurrentStart >> 2;
|
|
else
|
|
m_DIIMMBUF.Hex = 0;
|
|
break;
|
|
case 0x03: // Returns the total length
|
|
if (g_bStream)
|
|
m_DIIMMBUF.Hex = CurrentLength;
|
|
else
|
|
m_DIIMMBUF.Hex = 0;
|
|
break;
|
|
default:
|
|
WARN_LOG(DVDINTERFACE, "(Audio): Subcommand: %02x Request Audio status %s", m_DICMDBUF[0].CMDBYTE1, g_bStream? "on":"off");
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case DVDLowStopMotor:
|
|
DEBUG_LOG(DVDINTERFACE, "Stop motor");
|
|
break;
|
|
|
|
// DVD Audio Enable/Disable (Immediate)
|
|
case DVDLowAudioBufferConfig:
|
|
if (m_DICMDBUF[0].CMDBYTE1 == 1)
|
|
{
|
|
// TODO: What is this actually supposed to do?
|
|
g_bStream = true;
|
|
WARN_LOG(DVDINTERFACE, "(Audio): Audio enabled");
|
|
}
|
|
else
|
|
{
|
|
// TODO: What is this actually supposed to do?
|
|
g_bStream = false;
|
|
WARN_LOG(DVDINTERFACE, "(Audio): Audio disabled");
|
|
}
|
|
break;
|
|
|
|
// yet another command we prolly don't care about
|
|
case 0xEE:
|
|
DEBUG_LOG(DVDINTERFACE, "SetStatus - Unimplemented");
|
|
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.
|
|
case 0xFE:
|
|
INFO_LOG(DVDINTERFACE, "Unsupported DVD Drive debug command 0x%08x", m_DICMDBUF[0].Hex);
|
|
break;
|
|
|
|
// Unlock Commands. 1: "MATSHITA" 2: "DVD-GAME"
|
|
// Just for fun
|
|
case 0xFF:
|
|
{
|
|
if (m_DICMDBUF[0].Hex == 0xFF014D41 &&
|
|
m_DICMDBUF[1].Hex == 0x54534849 &&
|
|
m_DICMDBUF[2].Hex == 0x54410200)
|
|
{
|
|
INFO_LOG(DVDINTERFACE, "Unlock test 1 passed");
|
|
}
|
|
else if (m_DICMDBUF[0].Hex == 0xFF004456 &&
|
|
m_DICMDBUF[1].Hex == 0x442D4741 &&
|
|
m_DICMDBUF[2].Hex == 0x4D450300)
|
|
{
|
|
INFO_LOG(DVDINTERFACE, "Unlock test 2 passed");
|
|
}
|
|
else
|
|
{
|
|
INFO_LOG(DVDINTERFACE, "Unlock test failed");
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
PanicAlertT("Unknown DVD command %08x - fatal error", m_DICMDBUF[0].Hex);
|
|
_dbg_assert_(DVDINTERFACE, 0);
|
|
break;
|
|
}
|
|
|
|
// transfer is done
|
|
m_DICR.TSTART = 0;
|
|
m_DILENGTH.Length = 0;
|
|
GenerateDIInterrupt(INT_TCINT);
|
|
g_ErrorCode = 0;
|
|
}
|
|
|
|
void FinishExecuteRead()
|
|
{
|
|
u32 iDVDOffset = m_DICMDBUF[1].Hex << 2;
|
|
|
|
if (!DVDRead(iDVDOffset, m_DIMAR.Address, m_DILENGTH.Length))
|
|
{
|
|
PanicAlertT("Can't read from DVD_Plugin - DVD-Interface: Fatal Error");
|
|
}
|
|
|
|
// transfer is done
|
|
m_DICR.TSTART = 0;
|
|
m_DILENGTH.Length = 0;
|
|
GenerateDIInterrupt(INT_TCINT);
|
|
g_ErrorCode = 0;
|
|
}
|
|
|
|
} // namespace
|