Play-/Source/ee/Dmac_Channel.cpp
2022-09-10 12:37:16 -04:00

585 lines
13 KiB
C++

#include <string.h>
#include <assert.h>
#include "string_format.h"
#include "../states/RegisterStateFile.h"
#include "../Log.h"
#include "Dmac_Channel.h"
#include "DMAC.h"
#define LOG_NAME ("ee_dmac")
#define STATE_REGS_XML_FORMAT ("dmac/channel_%d.xml")
#define STATE_REGS_CHCR ("CHCR")
#define STATE_REGS_MADR ("MADR")
#define STATE_REGS_QWC ("QWC")
#define STATE_REGS_TADR ("TADR")
#define STATE_REGS_SCCTRL ("SCCTRL")
#define STATE_REGS_ASR0 ("ASR0")
#define STATE_REGS_ASR1 ("ASR1")
#define DMATAG_IRQ 0x8000
#define DMATAG_ID 0x7000
#define DMATAG_ADDR_MASK 0xFFFFFFF0
using namespace Dmac;
CChannel::CChannel(CDMAC& dmac, unsigned int nNumber, const DmaReceiveHandler& pReceive)
: m_dmac(dmac)
, m_number(nNumber)
, m_receive(pReceive)
{
}
void CChannel::Reset()
{
memset(&m_CHCR, 0, sizeof(CHCR));
m_nMADR = 0;
m_nQWC = 0;
m_nTADR = 0;
m_nSCCTRL = 0;
m_nASR[0] = 0;
m_nASR[1] = 0;
}
void CChannel::SaveState(Framework::CZipArchiveWriter& archive)
{
auto path = string_format(STATE_REGS_XML_FORMAT, m_number);
CRegisterStateFile* registerFile = new CRegisterStateFile(path.c_str());
registerFile->SetRegister32(STATE_REGS_CHCR, m_CHCR);
registerFile->SetRegister32(STATE_REGS_MADR, m_nMADR);
registerFile->SetRegister32(STATE_REGS_QWC, m_nQWC);
registerFile->SetRegister32(STATE_REGS_TADR, m_nTADR);
registerFile->SetRegister32(STATE_REGS_SCCTRL, m_nSCCTRL);
registerFile->SetRegister32(STATE_REGS_ASR0, m_nASR[0]);
registerFile->SetRegister32(STATE_REGS_ASR1, m_nASR[1]);
archive.InsertFile(registerFile);
}
void CChannel::LoadState(Framework::CZipArchiveReader& archive)
{
auto path = string_format(STATE_REGS_XML_FORMAT, m_number);
CRegisterStateFile registerFile(*archive.BeginReadFile(path.c_str()));
m_CHCR <<= registerFile.GetRegister32(STATE_REGS_CHCR);
m_nMADR = registerFile.GetRegister32(STATE_REGS_MADR);
m_nQWC = registerFile.GetRegister32(STATE_REGS_QWC);
m_nTADR = registerFile.GetRegister32(STATE_REGS_TADR);
m_nSCCTRL = registerFile.GetRegister32(STATE_REGS_SCCTRL);
m_nASR[0] = registerFile.GetRegister32(STATE_REGS_ASR0);
m_nASR[1] = registerFile.GetRegister32(STATE_REGS_ASR1);
}
uint32 CChannel::ReadCHCR()
{
return m_CHCR;
}
void CChannel::WriteCHCR(uint32 nValue)
{
bool nSuspend = false;
//We need to check if the purpose of this write is to suspend the current transfer
if(m_dmac.m_D_ENABLE & CDMAC::ENABLE_CPND)
{
nSuspend = (m_CHCR.nSTR != 0) && ((nValue & CDMAC::CHCR_STR) == 0);
}
if(m_CHCR.nSTR == 1)
{
m_CHCR.nSTR = ((nValue & CDMAC::CHCR_STR) != 0) ? 1 : 0;
}
else
{
m_CHCR = *(CHCR*)&nValue;
}
if(m_CHCR.nSTR != 0)
{
if(m_nQWC == 0)
{
m_nSCCTRL |= SCCTRL_INITXFER;
}
if(m_number == CDMAC::CHANNEL_ID_VIF1)
{
m_nSCCTRL |= SCCTRL_PAUSE;
}
m_nSCCTRL &= ~SCCTRL_RETTOP;
Execute();
}
}
void CChannel::Execute()
{
if(m_CHCR.nSTR != 0)
{
//Don't start the DMA transfer now, wait till the next iteration
//Needed for Wild Arms 4
if(m_nSCCTRL & SCCTRL_PAUSE)
{
m_nSCCTRL &= ~SCCTRL_PAUSE;
return;
}
if(m_dmac.m_D_ENABLE & CDMAC::ENABLE_CPND)
{
//TODO: Need to check cases where this is done on channels other than 4
assert(m_number == CDMAC::CHANNEL_ID_TO_IPU);
return;
}
switch(m_CHCR.nMOD)
{
case 0x00:
ExecuteNormal();
break;
case 0x02:
assert((m_number == CDMAC::CHANNEL_ID_FROM_SPR) || (m_number == CDMAC::CHANNEL_ID_TO_SPR));
if((m_dmac.m_D_SQWC.sqwc == 0) || (m_dmac.m_D_SQWC.tqwc == 0))
{
//If SQWC or TQWC is 0, execute normally
ExecuteNormal();
}
else
{
ExecuteInterleave();
}
break;
case 0x01:
case 0x03: //FFXII uses 3 here, assuming source chain mode
if(m_number == CDMAC::CHANNEL_ID_FROM_SPR)
{
ExecuteDestinationChain();
}
else
{
ExecuteSourceChain();
}
break;
default:
assert(0);
break;
}
}
}
void CChannel::ExecuteNormal()
{
bool isMfifo = false;
switch(m_dmac.m_D_CTRL.mfd)
{
case 0:
isMfifo = false;
break;
case 1:
//Invalid
assert(false);
break;
case 2:
case 3:
isMfifo = (m_number == CDMAC::CHANNEL_ID_FROM_SPR);
break;
}
// When starting a normal transfer, always at least one
// QW is send, regardless of the value of the QWC register.
// In case of a QWC of 0, this causes the QWC register to
// underflow down the line, which is intentional (actually
// happens on real hardware).
// Games that do exhibit a normal transfer with 0 QWC
// (Enter the Matrix, Star Wars Racer Revenge) mitigate
// this by setting the QWC register each frame.
uint32 qwc = m_nQWC;
if(qwc == 0)
{
qwc = 1;
}
if(isMfifo)
{
m_nMADR &= m_dmac.m_D_RBSR;
m_nMADR |= m_dmac.m_D_RBOR;
//Adjust QWC if we're in MFIFO mode
uint32 ringBufferAddr = m_nMADR - m_dmac.m_D_RBOR;
uint32 ringBufferSize = m_dmac.m_D_RBSR + 0x10;
assert(ringBufferAddr < ringBufferSize);
qwc = std::min<int32>(qwc, (ringBufferSize - ringBufferAddr) / 0x10);
}
uint32 nRecv = m_receive(m_nMADR, qwc, m_CHCR.nDIR, false);
m_nMADR += nRecv * 0x10;
m_nQWC -= nRecv;
m_nQWC &= CDMAC::QWC_WRITE_MASK;
if(m_nQWC == 0)
{
ClearSTR();
}
if(isMfifo)
{
//Loop MADR if needed
uint32 ringBufferSize = m_dmac.m_D_RBSR + 0x10;
if(m_nMADR == (m_dmac.m_D_RBOR + ringBufferSize))
{
m_nMADR = m_dmac.m_D_RBOR;
}
}
}
void CChannel::ExecuteInterleave()
{
assert((m_nQWC % m_dmac.m_D_SQWC.tqwc) == 0);
while(1)
{
//Transfer
{
uint32 qwc = m_dmac.m_D_SQWC.tqwc;
uint32 recv = m_receive(m_nMADR, qwc, CHCR_DIR_FROM, false);
assert(recv == qwc);
m_nMADR += recv * 0x10;
m_nQWC -= recv;
}
//Skip
m_nMADR += m_dmac.m_D_SQWC.sqwc * 0x10;
if(m_nQWC == 0)
{
ClearSTR();
break;
}
}
}
void CChannel::ExecuteSourceChain()
{
bool isMfifo = false;
switch(m_dmac.m_D_CTRL.mfd)
{
case 0:
isMfifo = false;
break;
case 1:
//Invalid
assert(false);
break;
case 2:
isMfifo = (m_number == CDMAC::CHANNEL_ID_VIF1);
break;
case 3:
isMfifo = (m_number == CDMAC::CHANNEL_ID_GIF);
break;
}
bool isStallDrainChannel = false;
switch(m_dmac.m_D_CTRL.std)
{
case CDMAC::D_CTRL_STD_NONE:
isStallDrainChannel = false;
break;
case CDMAC::D_CTRL_STD_VIF1:
isStallDrainChannel = (m_number == CDMAC::CHANNEL_ID_VIF1);
break;
case CDMAC::D_CTRL_STD_GIF:
isStallDrainChannel = (m_number == CDMAC::CHANNEL_ID_GIF);
break;
default:
assert(false);
break;
}
//Pause transfer if channel is stalled
if(isStallDrainChannel && ((m_CHCR.nTAG & DMATAG_ID) == (DMATAG_SRC_REFS << 12)) && (m_nMADR >= m_dmac.m_D_STADR))
{
return;
}
//Execute current
if(m_nQWC != 0)
{
ExecuteSourceChainTransfer(isMfifo);
if(m_nQWC != 0)
{
//Transfer isn't finished, suspend for now
return;
}
}
while(m_CHCR.nSTR == 1)
{
//Check if MFIFO is enabled with this channel
if(isMfifo)
{
//Hold transfer if not ready
if(m_nTADR == m_dmac.m_D8.m_nMADR)
{
break;
}
}
//Check if device received DMAtag
if(m_CHCR.nReserved0)
{
assert(m_CHCR.nTTE);
m_CHCR.nReserved0 = 0;
if(m_receive(m_nTADR, 1, CHCR_DIR_FROM, true) != 1)
{
//Device didn't receive DmaTag, break for now
m_CHCR.nReserved0 = 1;
break;
}
}
else
{
//Check if we've finished our DMA transfer
if(m_nQWC == 0)
{
if(m_nSCCTRL & SCCTRL_INITXFER)
{
//Clear this bit
m_nSCCTRL &= ~SCCTRL_INITXFER;
}
else
{
if(CDMAC::IsEndSrcTagId(m_CHCR.nTAG))
{
ClearSTR();
continue;
}
if((m_CHCR.nTIE != 0) && ((m_CHCR.nTAG & DMATAG_IRQ) != 0))
{
ClearSTR();
continue;
}
//Transfer must also end when we encounter a RET tag with ASR == 0
if(m_nSCCTRL & SCCTRL_RETTOP)
{
ClearSTR();
m_nSCCTRL &= ~SCCTRL_RETTOP;
continue;
}
}
}
else
{
//Suspend transfer
break;
}
if(m_CHCR.nTTE == 1)
{
m_CHCR.nReserved0 = 0;
if(m_receive(m_nTADR, 1, CHCR_DIR_FROM, true) != 1)
{
//Device didn't receive DmaTag, break for now
m_CHCR.nReserved0 = 1;
break;
}
}
}
//Half-Life does this...
if(m_nTADR == 0)
{
ClearSTR();
continue;
}
uint64 nTag = m_dmac.FetchDMATag(m_nTADR);
//Save higher 16 bits of tag into CHCR
m_CHCR.nTAG = static_cast<uint16>(nTag >> 16);
uint8 nID = static_cast<uint8>((nTag >> 28) & 0x07);
switch(nID)
{
case DMATAG_SRC_REFE:
//REFE - Data to transfer is pointer in memory address, transfer is done
m_nMADR = (uint32)((nTag >> 32) & DMATAG_ADDR_MASK);
m_nQWC = (uint32)((nTag >> 0) & 0x0000FFFF);
m_nTADR = m_nTADR + 0x10;
break;
case DMATAG_SRC_CNT:
//CNT - Data to transfer is after the tag, next tag is after the data
m_nMADR = m_nTADR + 0x10;
m_nQWC = (uint32)(nTag & 0xFFFF);
m_nTADR = (m_nQWC * 0x10) + m_nMADR;
break;
case DMATAG_SRC_NEXT:
//NEXT - Transfers data after tag, next tag is at position in ADDR field
m_nMADR = m_nTADR + 0x10;
m_nQWC = (uint32)((nTag >> 0) & 0x0000FFFF);
m_nTADR = (uint32)((nTag >> 32) & DMATAG_ADDR_MASK);
break;
case DMATAG_SRC_REF:
case DMATAG_SRC_REFS:
//REF/REFS - Data to transfer is pointed in memory address, next tag is after this tag
m_nMADR = (uint32)((nTag >> 32) & DMATAG_ADDR_MASK);
m_nQWC = (uint32)((nTag >> 0) & 0x0000FFFF);
m_nTADR = m_nTADR + 0x10;
break;
case DMATAG_SRC_CALL:
//CALL - Transfers QWC after the tag, saves next address in ASR, TADR = ADDR
assert(m_CHCR.nASP < 2);
m_nMADR = m_nTADR + 0x10;
m_nQWC = (uint32)(nTag & 0xFFFF);
m_nASR[m_CHCR.nASP] = m_nMADR + (m_nQWC * 0x10);
m_nTADR = (uint32)((nTag >> 32) & DMATAG_ADDR_MASK);
m_CHCR.nASP++;
break;
case DMATAG_SRC_RET:
//RET - Transfers QWC after the tag, pops TADR from ASR
m_nMADR = m_nTADR + 0x10;
m_nQWC = (uint32)(nTag & 0xFFFF);
if(m_CHCR.nASP > 0)
{
m_CHCR.nASP--;
m_nTADR = m_nASR[m_CHCR.nASP];
}
else
{
m_nSCCTRL |= SCCTRL_RETTOP;
}
break;
case DMATAG_SRC_END:
//END - Data to transfer is after the tag, transfer is finished
m_nMADR = m_nTADR + 0x10;
m_nQWC = (uint32)(nTag & 0xFFFF);
break;
default:
m_nQWC = 0;
assert(0);
break;
}
assert((m_nMADR & 0xF) == 0);
assert((m_nTADR & 0xF) == 0);
//Pause transfer if channel is stalled
if(isStallDrainChannel && (nID == DMATAG_SRC_REFS) && (m_nMADR >= m_dmac.m_D_STADR))
{
continue;
}
ExecuteSourceChainTransfer(isMfifo);
}
}
void CChannel::ExecuteDestinationChain()
{
assert(m_number == CDMAC::CHANNEL_ID_FROM_SPR);
while(m_CHCR.nSTR == 1)
{
//QWC is 0, fetch a new tag
//Some games don't reset the TAG value in CHCR and start a transfer that looks like a normal transfer
//I guess this is because TAG is still the end tag and the DMAC only fetches a new tag if it's started with QWC = 0
//- Hitman 2: Silent Assassin
//- Ridge Racer 5
if(m_nQWC == 0)
{
auto tag = make_convertible<DMAtag>(m_dmac.FetchDMATag(m_dmac.m_D8_SADR | 0x80000000));
m_dmac.m_D8_SADR += 0x10;
assert(tag.irq == 0);
assert(tag.pce == 0);
assert((tag.addr & 0x0F) == 0);
switch(tag.id)
{
case DMATAG_DST_CNTS:
//Stall register update not supported yet
assert(m_dmac.m_D_CTRL.sts != CDMAC::D_CTRL_STS_FROM_SPR);
[[fallthrough]];
case DMATAG_DST_CNT:
case DMATAG_DST_END:
m_nMADR = tag.addr;
m_nQWC = tag.qwc;
break;
default:
assert(false);
break;
}
m_CHCR.nTAG = static_cast<uint16>(tag >> 16);
}
uint32 recv = m_receive(m_nMADR, m_nQWC, m_CHCR.nDIR, false);
assert(recv == m_nQWC);
m_nMADR += recv * 0x10;
m_nQWC -= recv;
if(CDMAC::IsEndDstTagId(m_CHCR.nTAG))
{
ClearSTR();
}
}
}
void CChannel::SetReceiveHandler(const DmaReceiveHandler& handler)
{
m_receive = handler;
}
void CChannel::ExecuteSourceChainTransfer(bool isMfifo)
{
uint32 nID = m_CHCR.nTAG >> 12;
uint32 qwc = m_nQWC;
if((nID == DMATAG_SRC_CNT) && isMfifo)
{
//Adjust QWC in MFIFO mode
uint32 ringBufferAddr = m_nMADR - m_dmac.m_D_RBOR;
uint32 ringBufferSize = m_dmac.m_D_RBSR + 0x10;
assert(ringBufferAddr <= ringBufferSize);
qwc = std::min<int32>(m_nQWC, (ringBufferSize - ringBufferAddr) / 0x10);
}
if(qwc != 0)
{
uint32 nRecv = m_receive(m_nMADR, qwc, CHCR_DIR_FROM, false);
m_nMADR += nRecv * 0x10;
m_nQWC -= nRecv;
}
if(isMfifo)
{
if(nID == DMATAG_SRC_CNT)
{
//Loop MADR if needed
uint32 ringBufferSize = m_dmac.m_D_RBSR + 0x10;
if(m_nMADR == (m_dmac.m_D_RBOR + ringBufferSize))
{
m_nMADR = m_dmac.m_D_RBOR;
}
}
//Mask TADR because it's in the ring buffer zone
m_nTADR -= m_dmac.m_D_RBOR;
m_nTADR &= m_dmac.m_D_RBSR;
m_nTADR += m_dmac.m_D_RBOR;
//Check MFIFO empty
if(m_nTADR == m_dmac.m_D8.m_nMADR)
{
m_dmac.m_D_STAT |= CDMAC::D_STAT_MEIS;
}
}
}
void CChannel::ClearSTR()
{
m_CHCR.nSTR = ~m_CHCR.nSTR;
//Set interrupt
m_dmac.m_D_STAT |= (1 << m_number);
m_dmac.UpdateCpCond();
}