mirror of
https://github.com/SourMesen/Mesen2.git
synced 2025-04-02 10:21:44 -04:00
365 lines
11 KiB
C++
365 lines
11 KiB
C++
#include "pch.h"
|
|
#include "GBA/GbaCpu.h"
|
|
#include "GBA/GbaDmaController.h"
|
|
#include "GBA/GbaMemoryManager.h"
|
|
#include "GBA/GbaRomPrefetch.h"
|
|
#include "Shared/MessageManager.h"
|
|
#include "Utilities/HexUtilities.h"
|
|
#include "Utilities/BitUtilities.h"
|
|
#include "Utilities/Serializer.h"
|
|
|
|
void GbaDmaController::Init(GbaCpu* cpu, GbaMemoryManager* memoryManager, GbaRomPrefetch* prefetcher)
|
|
{
|
|
_cpu = cpu;
|
|
_memoryManager = memoryManager;
|
|
_prefetcher = prefetcher;
|
|
}
|
|
|
|
GbaDmaControllerState& GbaDmaController::GetState()
|
|
{
|
|
return _state;
|
|
}
|
|
|
|
bool GbaDmaController::IsVideoCaptureDmaEnabled()
|
|
{
|
|
return _state.Ch[3].Enabled && _state.Ch[3].Trigger == GbaDmaTrigger::Special;
|
|
}
|
|
|
|
int8_t GbaDmaController::DebugGetActiveChannel()
|
|
{
|
|
return _dmaActiveChannel;
|
|
}
|
|
|
|
void GbaDmaController::TriggerDmaChannel(GbaDmaTrigger trigger, uint8_t channel, bool forceStop)
|
|
{
|
|
GbaDmaChannel& ch = _state.Ch[channel];
|
|
if(ch.Enabled && ch.Trigger == trigger) {
|
|
if(forceStop) {
|
|
ch.Repeat = false;
|
|
}
|
|
|
|
ch.Pending = true;
|
|
_dmaPending = true;
|
|
|
|
if(_dmaActiveChannel < 0 && !_dmaStartDelay) {
|
|
//CPU runs for 2 more cycles before pausing for DMA
|
|
_dmaStartDelay = 3;
|
|
_memoryManager->SetPendingUpdateFlag();
|
|
}
|
|
}
|
|
}
|
|
|
|
void GbaDmaController::TriggerDma(GbaDmaTrigger trigger)
|
|
{
|
|
for(int i = 0; i < 4; i++) {
|
|
TriggerDmaChannel(trigger, i);
|
|
}
|
|
}
|
|
|
|
void GbaDmaController::RunPendingDma(bool allowStartDma)
|
|
{
|
|
if(_dmaRunning) {
|
|
//Prevent re-entry while DMA is already running
|
|
//This caused unexpected crashes in Sonic Advance 3 because
|
|
//the same DMA was incorrectly running two times in a row.
|
|
return;
|
|
}
|
|
|
|
if(_dmaStartDelay) {
|
|
_dmaStartDelay--;
|
|
if(_dmaStartDelay) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if(!allowStartDma || _memoryManager->IsBusLocked()) {
|
|
//DMA can only start between cpu read/write cycles
|
|
//and can't start if the bus is locked by the cpu (swap instruction)
|
|
//Delay until DMA can start
|
|
_dmaStartDelay++;
|
|
return;
|
|
}
|
|
|
|
_dmaRunning = true;
|
|
//Before starting DMA, an additional idle cycle executes (CPU is blocked during this)
|
|
_memoryManager->ProcessInternalCycle();
|
|
|
|
for(int i = 0; i < 4; i++) {
|
|
if(_state.Ch[i].Pending) {
|
|
RunDma(_state.Ch[i], i);
|
|
}
|
|
}
|
|
|
|
//After stopping DMA, an additional idle cycle executes (CPU is blocked during this)
|
|
_memoryManager->ProcessInternalCycle();
|
|
_dmaRunning = false;
|
|
}
|
|
|
|
void GbaDmaController::RunDma(GbaDmaChannel& ch, uint8_t chIndex)
|
|
{
|
|
uint16_t length = ch.LenLatch ? ch.LenLatch : (chIndex == 3 ? 0xFFFF : 0x3FFF);
|
|
bool wordTransfer = ch.WordTransfer;
|
|
GbaDmaAddrMode srcMode = ch.SrcMode;
|
|
GbaDmaAddrMode destMode = ch.DestMode;
|
|
if(ch.Trigger == GbaDmaTrigger::Special && chIndex != 3) {
|
|
//Audio channel DMA forces these settings (games break otherwise)
|
|
length = 4;
|
|
wordTransfer = true;
|
|
destMode = GbaDmaAddrMode::Fixed;
|
|
}
|
|
|
|
uint8_t offset = wordTransfer ? 4 : 2;
|
|
GbaAccessModeVal mode = (wordTransfer ? GbaAccessMode::Word : GbaAccessMode::HalfWord) | GbaAccessMode::Dma;
|
|
|
|
/*MessageManager::Log("run dma - $" + HexUtilities::ToHex(ch.SrcLatch) + " -> $" + HexUtilities::ToHex(ch.DestLatch) + " - len: " +
|
|
HexUtilities::ToHex(length) + " - " +
|
|
(wordTransfer ? "W" : "H") +
|
|
(ch.SrcMode == GbaDmaAddrMode::Increment ? "I" : (ch.SrcMode == GbaDmaAddrMode::Decrement ? "D" : "F")) +
|
|
(destMode == GbaDmaAddrMode::Increment ? "I" : (destMode == GbaDmaAddrMode::Decrement ? "D" : (destMode == GbaDmaAddrMode::IncrementReload ? "R" : "F")))
|
|
);
|
|
*/
|
|
|
|
//Ignore lower bits for misaligned DMAs (some games break because of this)
|
|
if(wordTransfer) {
|
|
ch.DestLatch &= ~0x03;
|
|
ch.SrcLatch &= ~0x03;
|
|
} else {
|
|
ch.DestLatch &= ~0x01;
|
|
ch.SrcLatch &= ~0x01;
|
|
}
|
|
|
|
ch.Active = true;
|
|
|
|
uint8_t srcBank = ch.SrcLatch >> 24;
|
|
bool isRomSrc = srcBank >= 0x08 && srcBank <= 0x0D;
|
|
uint32_t srcAddr = ch.SrcLatch;
|
|
|
|
_dmaActiveChannel = chIndex;
|
|
|
|
while(length-- > 0) {
|
|
uint32_t value;
|
|
if(srcAddr >= 0x2000000) {
|
|
if(srcAddr & 0x8000000) {
|
|
//DMA accessed ROM, suspend the prefetcher
|
|
_prefetcher->SetSuspendState(true);
|
|
}
|
|
|
|
value = ch.ReadValue = _memoryManager->Read(mode, srcAddr);
|
|
if(isRomSrc) {
|
|
//If a DMA reads from ROM (cart) and writes to ROM (cart), the first ROM write will be sequential
|
|
mode |= GbaAccessMode::Sequential;
|
|
}
|
|
if(!wordTransfer) {
|
|
//Value kept in buffer is mirrored across both half-words when transfering half-words
|
|
//Needed to pass mgba suite tests that perform a half-word transfer before performing
|
|
//a DMA that tries to load data from the boot rom
|
|
ch.ReadValue |= ch.ReadValue << 16;
|
|
}
|
|
} else {
|
|
//Access to boot rom region is not allowed, return the previous value read by DMA
|
|
_memoryManager->ProcessInternalCycle();
|
|
if(wordTransfer) {
|
|
value = ch.ReadValue;
|
|
} else {
|
|
//For half-word transfers, the value written depends on the destination address
|
|
value = ch.ReadValue >> ((ch.DestLatch & 0x02) << 3);
|
|
}
|
|
}
|
|
|
|
if(ch.DestLatch & 0x8000000) {
|
|
//DMA accessed ROM, suspend the prefetcher
|
|
_prefetcher->SetSuspendState(true);
|
|
}
|
|
|
|
_memoryManager->Write(mode, ch.DestLatch, value);
|
|
mode |= GbaAccessMode::Sequential;
|
|
|
|
switch(destMode) {
|
|
case GbaDmaAddrMode::Increment: ch.DestLatch += offset; break;
|
|
case GbaDmaAddrMode::Decrement: ch.DestLatch -= offset; break;
|
|
case GbaDmaAddrMode::Fixed: break;
|
|
case GbaDmaAddrMode::IncrementReload: ch.DestLatch += offset; break;
|
|
}
|
|
|
|
switch(srcMode) {
|
|
case GbaDmaAddrMode::Increment: ch.SrcLatch += offset; break;
|
|
case GbaDmaAddrMode::Decrement: ch.SrcLatch -= offset; break;
|
|
case GbaDmaAddrMode::Fixed: break;
|
|
case GbaDmaAddrMode::IncrementReload: break;
|
|
}
|
|
|
|
if(ch.SrcLatch >= 0x8000000 && ch.SrcLatch < 0xE000000) {
|
|
if(!isRomSrc && ch.Destination >= 0x8000000 && ch.Destination < 0xE000000) {
|
|
//When src moves from non-rom to rom and destination is in rom, use the destination address
|
|
//Passes "burst-into-tears" test (but might be incorrect?)
|
|
srcAddr = ch.Destination;
|
|
isRomSrc = true;
|
|
}
|
|
|
|
if((srcMode == GbaDmaAddrMode::Decrement || srcMode == GbaDmaAddrMode::Fixed) && (srcAddr & 0x1FFFF) == 0) {
|
|
//When on a 0x20000 boundary non-sequential timing is used (passes 128kb-boundary test)
|
|
mode &= ~GbaAccessMode::Sequential;
|
|
}
|
|
|
|
//While the address is in the ROM region, all reads are sequential
|
|
//even if the channel is set to decrement/fixed
|
|
srcAddr += offset;
|
|
} else {
|
|
srcAddr = ch.SrcLatch;
|
|
}
|
|
|
|
if(_dmaPending) {
|
|
//Check if channels with higher priority need to run
|
|
for(int i = 0; i < chIndex; i++) {
|
|
if(_state.Ch[i].Pending) {
|
|
RunDma(_state.Ch[i], i);
|
|
|
|
//Mark next access as non-sequential?
|
|
mode &= ~GbaAccessMode::Sequential;
|
|
}
|
|
}
|
|
_dmaActiveChannel = chIndex;
|
|
}
|
|
}
|
|
|
|
_dmaActiveChannel = -1;
|
|
_prefetcher->SetSuspendState(false);
|
|
|
|
ch.Active = false;
|
|
ch.Pending = false;
|
|
|
|
_dmaPending = false;
|
|
for(int i = 0; i < 4; i++) {
|
|
_dmaPending |= _state.Ch[i].Pending;
|
|
}
|
|
|
|
if(!ch.Repeat || ch.Trigger == GbaDmaTrigger::Immediate) {
|
|
ch.Enabled = false;
|
|
ch.Control &= ~0x8000;
|
|
} else {
|
|
if(destMode == GbaDmaAddrMode::IncrementReload) {
|
|
ch.DestLatch = ch.Destination;
|
|
}
|
|
}
|
|
|
|
//Next access after DMA is never sequential - passes "force-nseq-access" test
|
|
_cpu->ClearSequentialFlag();
|
|
|
|
if(ch.IrqEnabled) {
|
|
_memoryManager->SetIrqSource((GbaIrqSource)((int)GbaIrqSource::DmaChannel0 << chIndex));
|
|
}
|
|
}
|
|
|
|
uint8_t GbaDmaController::ReadRegister(uint32_t addr)
|
|
{
|
|
GbaDmaChannel& ch = _state.Ch[(addr - 0xB0) / 12];
|
|
|
|
switch(addr) {
|
|
case 0xB8: case 0xC4: case 0xD0: case 0xDC:
|
|
case 0xB9: case 0xC5: case 0xD1: case 0xDD:
|
|
return 0;
|
|
|
|
case 0xBA: case 0xC6: case 0xD2: case 0xDE: return BitUtilities::GetBits<0>(ch.Control);
|
|
case 0xBB: case 0xC7: case 0xD3: case 0xDF: return BitUtilities::GetBits<8>(ch.Control);
|
|
|
|
default:
|
|
//MessageManager::Log("Read unknown DMA register: " + HexUtilities::ToHex32(addr));
|
|
return _memoryManager->GetOpenBus(addr);
|
|
}
|
|
}
|
|
|
|
void GbaDmaController::WriteRegister(uint32_t addr, uint8_t value)
|
|
{
|
|
uint8_t chIndex = (addr - 0xB0) / 12;
|
|
GbaDmaChannel& ch = _state.Ch[chIndex];
|
|
|
|
switch(addr) {
|
|
case 0xB0: case 0xBC: case 0xC8: case 0xD4: BitUtilities::SetBits<0>(ch.Source, value); break;
|
|
case 0xB1: case 0xBD: case 0xC9: case 0xD5: BitUtilities::SetBits<8>(ch.Source, value); break;
|
|
case 0xB2: case 0xBE: case 0xCA: case 0xD6: BitUtilities::SetBits<16>(ch.Source, value); break;
|
|
case 0xB3: case 0xBF: case 0xCB: case 0xD7: BitUtilities::SetBits<24>(ch.Source, value & (chIndex == 0 ? 0x07 : 0x0F)); break;
|
|
|
|
case 0xB4: case 0xC0: case 0xCC: case 0xD8: BitUtilities::SetBits<0>(ch.Destination, value); break;
|
|
case 0xB5: case 0xC1: case 0xCD: case 0xD9: BitUtilities::SetBits<8>(ch.Destination, value); break;
|
|
case 0xB6: case 0xC2: case 0xCE: case 0xDA: BitUtilities::SetBits<16>(ch.Destination, value); break;
|
|
case 0xB7: case 0xC3: case 0xCF: case 0xDB: BitUtilities::SetBits<24>(ch.Destination, value & (chIndex == 3 ? 0x0F : 0x07)); break;
|
|
|
|
case 0xB8: case 0xC4: case 0xD0: case 0xDC: BitUtilities::SetBits<0>(ch.Length, value); break;
|
|
case 0xB9: case 0xC5: case 0xD1: case 0xDD: BitUtilities::SetBits<8>(ch.Length, value & (chIndex == 3 ? 0xFF : 0x3F)); break;
|
|
|
|
case 0xBA: case 0xC6: case 0xD2: case 0xDE:
|
|
value &= 0xE0;
|
|
BitUtilities::SetBits<0>(ch.Control, value);
|
|
ch.DestMode = (GbaDmaAddrMode)((value >> 5) & 0x03);
|
|
ch.SrcMode = (GbaDmaAddrMode)(((value >> 7) & 0x01) | ((ch.Control & 0x100) >> 7));
|
|
break;
|
|
|
|
case 0xBB: case 0xC7: case 0xD3: case 0xDF: {
|
|
if(chIndex != 3) {
|
|
//drq mode bit not available on channels 0 to 2
|
|
value &= ~0x08;
|
|
}
|
|
|
|
BitUtilities::SetBits<8>(ch.Control, value);
|
|
ch.SrcMode = (GbaDmaAddrMode)(((ch.Control & 0x80) >> 7) | ((value & 0x01) << 1));
|
|
ch.Repeat = (value & 0x02);
|
|
ch.WordTransfer = (value & 0x04);
|
|
ch.DrqMode = (value & 0x08);
|
|
ch.Trigger = (GbaDmaTrigger)((value >> 4) & 0x03);
|
|
ch.IrqEnabled = (value & 0x40);
|
|
bool enabled = (value & 0x80);
|
|
if(ch.Enabled != enabled && enabled) {
|
|
ch.Enabled = true;
|
|
ch.LenLatch = ch.Length;
|
|
ch.SrcLatch = ch.Source;
|
|
ch.DestLatch = ch.Destination;
|
|
if(ch.Trigger == GbaDmaTrigger::Immediate) {
|
|
TriggerDmaChannel(GbaDmaTrigger::Immediate, chIndex);
|
|
}
|
|
} else {
|
|
ch.Enabled = enabled;
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
MessageManager::Log("Write unknown DMA register: " + HexUtilities::ToHex32(addr) + " = " + HexUtilities::ToHex(value));
|
|
break;
|
|
}
|
|
}
|
|
|
|
void GbaDmaController::Serialize(Serializer& s)
|
|
{
|
|
for(int i = 0; i < 4; i++) {
|
|
SVI(_state.Ch[i].ReadValue);
|
|
|
|
SVI(_state.Ch[i].Destination);
|
|
SVI(_state.Ch[i].Source);
|
|
SVI(_state.Ch[i].Length);
|
|
|
|
SVI(_state.Ch[i].DestLatch);
|
|
SVI(_state.Ch[i].SrcLatch);
|
|
SVI(_state.Ch[i].LenLatch);
|
|
|
|
SVI(_state.Ch[i].Control);
|
|
|
|
SVI(_state.Ch[i].DestMode);
|
|
SVI(_state.Ch[i].SrcMode);
|
|
|
|
SVI(_state.Ch[i].Repeat);
|
|
SVI(_state.Ch[i].WordTransfer);
|
|
SVI(_state.Ch[i].DrqMode);
|
|
|
|
SVI(_state.Ch[i].Trigger);
|
|
SVI(_state.Ch[i].IrqEnabled);
|
|
SVI(_state.Ch[i].Enabled);
|
|
SVI(_state.Ch[i].Active);
|
|
|
|
SVI(_state.Ch[i].Pending);
|
|
}
|
|
|
|
SV(_dmaRunning);
|
|
SV(_dmaPending);
|
|
SV(_dmaActiveChannel);
|
|
SV(_dmaStartDelay);
|
|
}
|