mirror of
https://github.com/libretro/MesenCE.git
synced 2026-11-10 03:02:05 -10:00
The NES (and AV Famicom) both ignore consecutive reads to 4016/4017 (when the reads on 2 consecutive CPU cycles). This implements that behavior when the corresponding console type is selected. These changes also simplify the DMA code, because a lot of extra code was in the DMA logic to simulate the same result. Moving this logic to the 4016/4017 reads makes the code simpler and allows it to work properly when a regular instruction reads from 4016/4017 twice in a row, too.
613 lines
23 KiB
C++
613 lines
23 KiB
C++
#include "pch.h"
|
|
#include <random>
|
|
#include <assert.h>
|
|
#include "Utilities/Serializer.h"
|
|
#include "Debugger/Debugger.h"
|
|
#include "NES/NesCpu.h"
|
|
#include "NES/NesPpu.h"
|
|
#include "NES/APU/NesApu.h"
|
|
#include "NES/NesMemoryManager.h"
|
|
#include "NES/NesControlManager.h"
|
|
#include "NES/NesConsole.h"
|
|
#include "NES/NesTypes.h"
|
|
#include "Shared/MessageManager.h"
|
|
#include "Shared/EmuSettings.h"
|
|
#include "Shared/Emulator.h"
|
|
#include "Shared/MemoryOperationType.h"
|
|
|
|
NesCpu::NesCpu(NesConsole* console)
|
|
{
|
|
_emu = console->GetEmulator();
|
|
_console = console;
|
|
_memoryManager = _console->GetMemoryManager();
|
|
|
|
// clang-format off
|
|
Func opTable[] = {
|
|
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
|
|
&NesCpu::BRK, &NesCpu::ORA, &NesCpu::HLT, &NesCpu::SLO, &NesCpu::NOP, &NesCpu::ORA, &NesCpu::ASL_Memory, &NesCpu::SLO, &NesCpu::PHP, &NesCpu::ORA, &NesCpu::ASL_Acc, &NesCpu::AAC, &NesCpu::NOP, &NesCpu::ORA, &NesCpu::ASL_Memory, &NesCpu::SLO, //0
|
|
&NesCpu::BPL, &NesCpu::ORA, &NesCpu::HLT, &NesCpu::SLO, &NesCpu::NOP, &NesCpu::ORA, &NesCpu::ASL_Memory, &NesCpu::SLO, &NesCpu::CLC, &NesCpu::ORA, &NesCpu::NOP, &NesCpu::SLO, &NesCpu::NOP, &NesCpu::ORA, &NesCpu::ASL_Memory, &NesCpu::SLO, //1
|
|
&NesCpu::JSR, &NesCpu::AND, &NesCpu::HLT, &NesCpu::RLA, &NesCpu::BIT, &NesCpu::AND, &NesCpu::ROL_Memory, &NesCpu::RLA, &NesCpu::PLP, &NesCpu::AND, &NesCpu::ROL_Acc, &NesCpu::AAC, &NesCpu::BIT, &NesCpu::AND, &NesCpu::ROL_Memory, &NesCpu::RLA, //2
|
|
&NesCpu::BMI, &NesCpu::AND, &NesCpu::HLT, &NesCpu::RLA, &NesCpu::NOP, &NesCpu::AND, &NesCpu::ROL_Memory, &NesCpu::RLA, &NesCpu::SEC, &NesCpu::AND, &NesCpu::NOP, &NesCpu::RLA, &NesCpu::NOP, &NesCpu::AND, &NesCpu::ROL_Memory, &NesCpu::RLA, //3
|
|
&NesCpu::RTI, &NesCpu::EOR, &NesCpu::HLT, &NesCpu::SRE, &NesCpu::NOP, &NesCpu::EOR, &NesCpu::LSR_Memory, &NesCpu::SRE, &NesCpu::PHA, &NesCpu::EOR, &NesCpu::LSR_Acc, &NesCpu::ASR, &NesCpu::JMP_Abs, &NesCpu::EOR, &NesCpu::LSR_Memory, &NesCpu::SRE, //4
|
|
&NesCpu::BVC, &NesCpu::EOR, &NesCpu::HLT, &NesCpu::SRE, &NesCpu::NOP, &NesCpu::EOR, &NesCpu::LSR_Memory, &NesCpu::SRE, &NesCpu::CLI, &NesCpu::EOR, &NesCpu::NOP, &NesCpu::SRE, &NesCpu::NOP, &NesCpu::EOR, &NesCpu::LSR_Memory, &NesCpu::SRE, //5
|
|
&NesCpu::RTS, &NesCpu::ADC, &NesCpu::HLT, &NesCpu::RRA, &NesCpu::NOP, &NesCpu::ADC, &NesCpu::ROR_Memory, &NesCpu::RRA, &NesCpu::PLA, &NesCpu::ADC, &NesCpu::ROR_Acc, &NesCpu::ARR, &NesCpu::JMP_Ind, &NesCpu::ADC, &NesCpu::ROR_Memory, &NesCpu::RRA, //6
|
|
&NesCpu::BVS, &NesCpu::ADC, &NesCpu::HLT, &NesCpu::RRA, &NesCpu::NOP, &NesCpu::ADC, &NesCpu::ROR_Memory, &NesCpu::RRA, &NesCpu::SEI, &NesCpu::ADC, &NesCpu::NOP, &NesCpu::RRA, &NesCpu::NOP, &NesCpu::ADC, &NesCpu::ROR_Memory, &NesCpu::RRA, //7
|
|
&NesCpu::NOP, &NesCpu::STA, &NesCpu::NOP, &NesCpu::SAX, &NesCpu::STY, &NesCpu::STA, &NesCpu::STX, &NesCpu::SAX, &NesCpu::DEY, &NesCpu::NOP, &NesCpu::TXA, &NesCpu::ANE, &NesCpu::STY, &NesCpu::STA, &NesCpu::STX, &NesCpu::SAX, //8
|
|
&NesCpu::BCC, &NesCpu::STA, &NesCpu::HLT, &NesCpu::SHAZ, &NesCpu::STY, &NesCpu::STA, &NesCpu::STX, &NesCpu::SAX, &NesCpu::TYA, &NesCpu::STA, &NesCpu::TXS, &NesCpu::TAS, &NesCpu::SHY, &NesCpu::STA, &NesCpu::SHX, &NesCpu::SHAA,//9
|
|
&NesCpu::LDY, &NesCpu::LDA, &NesCpu::LDX, &NesCpu::LAX, &NesCpu::LDY, &NesCpu::LDA, &NesCpu::LDX, &NesCpu::LAX, &NesCpu::TAY, &NesCpu::LDA, &NesCpu::TAX, &NesCpu::ATX, &NesCpu::LDY, &NesCpu::LDA, &NesCpu::LDX, &NesCpu::LAX, //A
|
|
&NesCpu::BCS, &NesCpu::LDA, &NesCpu::HLT, &NesCpu::LAX, &NesCpu::LDY, &NesCpu::LDA, &NesCpu::LDX, &NesCpu::LAX, &NesCpu::CLV, &NesCpu::LDA, &NesCpu::TSX, &NesCpu::LAS, &NesCpu::LDY, &NesCpu::LDA, &NesCpu::LDX, &NesCpu::LAX, //B
|
|
&NesCpu::CPY, &NesCpu::CPA, &NesCpu::NOP, &NesCpu::DCP, &NesCpu::CPY, &NesCpu::CPA, &NesCpu::DEC, &NesCpu::DCP, &NesCpu::INY, &NesCpu::CPA, &NesCpu::DEX, &NesCpu::AXS, &NesCpu::CPY, &NesCpu::CPA, &NesCpu::DEC, &NesCpu::DCP, //C
|
|
&NesCpu::BNE, &NesCpu::CPA, &NesCpu::HLT, &NesCpu::DCP, &NesCpu::NOP, &NesCpu::CPA, &NesCpu::DEC, &NesCpu::DCP, &NesCpu::CLD, &NesCpu::CPA, &NesCpu::NOP, &NesCpu::DCP, &NesCpu::NOP, &NesCpu::CPA, &NesCpu::DEC, &NesCpu::DCP, //D
|
|
&NesCpu::CPX, &NesCpu::SBC, &NesCpu::NOP, &NesCpu::ISB, &NesCpu::CPX, &NesCpu::SBC, &NesCpu::INC, &NesCpu::ISB, &NesCpu::INX, &NesCpu::SBC, &NesCpu::NOP, &NesCpu::SBC, &NesCpu::CPX, &NesCpu::SBC, &NesCpu::INC, &NesCpu::ISB, //E
|
|
&NesCpu::BEQ, &NesCpu::SBC, &NesCpu::HLT, &NesCpu::ISB, &NesCpu::NOP, &NesCpu::SBC, &NesCpu::INC, &NesCpu::ISB, &NesCpu::SED, &NesCpu::SBC, &NesCpu::NOP, &NesCpu::ISB, &NesCpu::NOP, &NesCpu::SBC, &NesCpu::INC, &NesCpu::ISB //F
|
|
};
|
|
|
|
typedef NesAddrMode M;
|
|
NesAddrMode addrMode[] = {
|
|
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
|
|
M::Imp, M::IndX, M::None, M::IndX, M::Zero, M::Zero, M::Zero, M::Zero, M::Imp, M::Imm, M::Acc, M::Imm, M::Abs, M::Abs, M::Abs, M::Abs, //0
|
|
M::Rel, M::IndY, M::None, M::IndYW, M::ZeroX, M::ZeroX, M::ZeroX, M::ZeroX, M::Imp, M::AbsY, M::Imp, M::AbsYW,M::AbsX, M::AbsX, M::AbsXW,M::AbsXW,//1
|
|
M::Other,M::IndX, M::None, M::IndX, M::Zero, M::Zero, M::Zero, M::Zero, M::Imp, M::Imm, M::Acc, M::Imm, M::Abs, M::Abs, M::Abs, M::Abs, //2
|
|
M::Rel, M::IndY, M::None, M::IndYW, M::ZeroX, M::ZeroX, M::ZeroX, M::ZeroX, M::Imp, M::AbsY, M::Imp, M::AbsYW,M::AbsX, M::AbsX, M::AbsXW,M::AbsXW,//3
|
|
M::Imp, M::IndX, M::None, M::IndX, M::Zero, M::Zero, M::Zero, M::Zero, M::Imp, M::Imm, M::Acc, M::Imm, M::Abs, M::Abs, M::Abs, M::Abs, //4
|
|
M::Rel, M::IndY, M::None, M::IndYW, M::ZeroX, M::ZeroX, M::ZeroX, M::ZeroX, M::Imp, M::AbsY, M::Imp, M::AbsYW,M::AbsX, M::AbsX, M::AbsXW,M::AbsXW,//5
|
|
M::Imp, M::IndX, M::None, M::IndX, M::Zero, M::Zero, M::Zero, M::Zero, M::Imp, M::Imm, M::Acc, M::Imm, M::Ind, M::Abs, M::Abs, M::Abs, //6
|
|
M::Rel, M::IndY, M::None, M::IndYW, M::ZeroX, M::ZeroX, M::ZeroX, M::ZeroX, M::Imp, M::AbsY, M::Imp, M::AbsYW,M::AbsX, M::AbsX, M::AbsXW,M::AbsXW,//7
|
|
M::Imm, M::IndX, M::Imm, M::IndX, M::Zero, M::Zero, M::Zero, M::Zero, M::Imp, M::Imm, M::Imp, M::Imm, M::Abs, M::Abs, M::Abs, M::Abs, //8
|
|
M::Rel, M::IndYW, M::None, M::Other, M::ZeroX, M::ZeroX, M::ZeroY, M::ZeroY, M::Imp, M::AbsYW,M::Imp, M::Other,M::Other,M::AbsXW,M::Other,M::Other,//9
|
|
M::Imm, M::IndX, M::Imm, M::IndX, M::Zero, M::Zero, M::Zero, M::Zero, M::Imp, M::Imm, M::Imp, M::Imm, M::Abs, M::Abs, M::Abs, M::Abs, //A
|
|
M::Rel, M::IndY, M::None, M::IndY, M::ZeroX, M::ZeroX, M::ZeroY, M::ZeroY, M::Imp, M::AbsY, M::Imp, M::AbsY, M::AbsX, M::AbsX, M::AbsY, M::AbsY, //B
|
|
M::Imm, M::IndX, M::Imm, M::IndX, M::Zero, M::Zero, M::Zero, M::Zero, M::Imp, M::Imm, M::Imp, M::Imm, M::Abs, M::Abs, M::Abs, M::Abs, //C
|
|
M::Rel, M::IndY, M::None, M::IndYW, M::ZeroX, M::ZeroX, M::ZeroX, M::ZeroX, M::Imp, M::AbsY, M::Imp, M::AbsYW,M::AbsX, M::AbsX, M::AbsXW,M::AbsXW,//D
|
|
M::Imm, M::IndX, M::Imm, M::IndX, M::Zero, M::Zero, M::Zero, M::Zero, M::Imp, M::Imm, M::Imp, M::Imm, M::Abs, M::Abs, M::Abs, M::Abs, //E
|
|
M::Rel, M::IndY, M::None, M::IndYW, M::ZeroX, M::ZeroX, M::ZeroX, M::ZeroX, M::Imp, M::AbsY, M::Imp, M::AbsYW,M::AbsX, M::AbsX, M::AbsXW,M::AbsXW,//F
|
|
};
|
|
// clang-format on
|
|
|
|
memcpy(_opTable, opTable, sizeof(opTable));
|
|
memcpy(_addrMode, addrMode, sizeof(addrMode));
|
|
|
|
_instAddrMode = NesAddrMode::None;
|
|
_state = {};
|
|
_operand = 0;
|
|
_spriteDmaTransfer = false;
|
|
_spriteDmaOffset = 0;
|
|
_needHalt = false;
|
|
_ppuOffset = 0;
|
|
_startClockCount = 6;
|
|
_endClockCount = 6;
|
|
_masterClock = 0;
|
|
_dmcDmaRunning = false;
|
|
_cpuWrite = false;
|
|
_irqMask = 0;
|
|
_state = {};
|
|
_prevRunIrq = false;
|
|
_runIrq = false;
|
|
}
|
|
|
|
void NesCpu::Reset(bool softReset, ConsoleRegion region)
|
|
{
|
|
_state.NmiFlag = false;
|
|
_state.IrqFlag = 0;
|
|
|
|
_spriteDmaTransfer = false;
|
|
_spriteDmaOffset = 0;
|
|
_needHalt = false;
|
|
_dmcDmaRunning = false;
|
|
_abortDmcDma = false;
|
|
_isDmcDmaRead = false;
|
|
_cpuWrite = false;
|
|
_crashed = false;
|
|
|
|
//Use _memoryManager->Read() directly to prevent clocking the PPU/APU when setting PC at reset
|
|
_state.PC = _memoryManager->Read(NesCpu::ResetVector) | _memoryManager->Read(NesCpu::ResetVector + 1) << 8;
|
|
|
|
if(softReset) {
|
|
SetFlags(PSFlags::Interrupt);
|
|
_state.SP -= 0x03;
|
|
} else {
|
|
//Used by NSF code to disable Frame Counter & DMC interrupts
|
|
_irqMask = 0xFF;
|
|
|
|
_state.A = 0;
|
|
_state.SP = 0xFD;
|
|
_state.X = 0;
|
|
_state.Y = 0;
|
|
_state.PS = PSFlags::Interrupt;
|
|
|
|
_runIrq = false;
|
|
}
|
|
|
|
uint8_t ppuDivider;
|
|
uint8_t cpuDivider;
|
|
switch(region) {
|
|
default:
|
|
case ConsoleRegion::Ntsc:
|
|
ppuDivider = 4;
|
|
cpuDivider = 12;
|
|
break;
|
|
|
|
case ConsoleRegion::Pal:
|
|
ppuDivider = 5;
|
|
cpuDivider = 16;
|
|
break;
|
|
|
|
case ConsoleRegion::Dendy:
|
|
ppuDivider = 5;
|
|
cpuDivider = 15;
|
|
break;
|
|
}
|
|
|
|
_state.CycleCount = (uint64_t)-1;
|
|
_masterClock = 0;
|
|
|
|
uint8_t cpuOffset = 0;
|
|
if(_console->GetNesConfig().RandomizeCpuPpuAlignment) {
|
|
std::random_device rd;
|
|
std::mt19937 mt(rd());
|
|
std::uniform_int_distribution<> distPpu(0, ppuDivider - 1);
|
|
std::uniform_int_distribution<> distCpu(0, cpuDivider - 1);
|
|
_ppuOffset = distPpu(mt);
|
|
cpuOffset += distCpu(mt);
|
|
|
|
string ppuAlignment = " PPU: " + std::to_string(_ppuOffset) + "/" + std::to_string(ppuDivider - 1);
|
|
string cpuAlignment = " CPU: " + std::to_string(cpuOffset) + "/" + std::to_string(cpuDivider - 1);
|
|
MessageManager::Log("CPU/PPU alignment -" + ppuAlignment + cpuAlignment);
|
|
} else {
|
|
_ppuOffset = 1;
|
|
cpuOffset = 0;
|
|
}
|
|
|
|
_masterClock += cpuDivider + cpuOffset;
|
|
|
|
//The CPU takes 8 cycles before it starts executing the ROM's code after a reset/power up
|
|
for(int i = 0; i < 8; i++) {
|
|
StartCpuCycle(true);
|
|
EndCpuCycle(true);
|
|
}
|
|
}
|
|
|
|
void NesCpu::Exec()
|
|
{
|
|
#ifndef DUMMYCPU
|
|
_emu->ProcessInstruction<CpuType::Nes>();
|
|
#endif
|
|
|
|
uint8_t opCode = GetOPCode();
|
|
_instAddrMode = _addrMode[opCode];
|
|
_operand = FetchOperand();
|
|
(this->*_opTable[opCode])();
|
|
|
|
if(_prevRunIrq || _prevNeedNmi) {
|
|
IRQ();
|
|
}
|
|
}
|
|
|
|
void NesCpu::IRQ()
|
|
{
|
|
#ifndef DUMMYCPU
|
|
uint16_t originalPc = PC();
|
|
|
|
if(_console->GetRegion() == ConsoleRegion::Pal) {
|
|
//On PAL, IRQ/NMI sequence also checks for DMA on the first read
|
|
ProcessPendingDma(_state.PC, MemoryOperationType::ExecOpCode);
|
|
}
|
|
#endif
|
|
|
|
DummyPcRead(); //fetch opcode (and discard it - $00 (BRK) is forced into the opcode register instead)
|
|
DummyPcRead(); //read next instruction byte (actually the same as above, since PC increment is suppressed. Also discarded.)
|
|
Push((uint16_t)(PC()));
|
|
|
|
if(_needNmi) {
|
|
_needNmi = false;
|
|
Push((uint8_t)(PS() | PSFlags::Reserved));
|
|
SetFlags(PSFlags::Interrupt);
|
|
|
|
SetPC(MemoryReadWord(NesCpu::NMIVector));
|
|
|
|
#ifndef DUMMYCPU
|
|
_emu->ProcessInterrupt<CpuType::Nes>(originalPc, _state.PC, true);
|
|
#endif
|
|
} else {
|
|
Push((uint8_t)(PS() | PSFlags::Reserved));
|
|
SetFlags(PSFlags::Interrupt);
|
|
|
|
SetPC(MemoryReadWord(NesCpu::IRQVector));
|
|
|
|
#ifndef DUMMYCPU
|
|
_emu->ProcessInterrupt<CpuType::Nes>(originalPc, _state.PC, false);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
void NesCpu::BRK()
|
|
{
|
|
Push((uint16_t)(PC() + 1));
|
|
|
|
uint8_t flags = PS() | PSFlags::Break | PSFlags::Reserved;
|
|
if(_needNmi) {
|
|
_needNmi = false;
|
|
Push((uint8_t)flags);
|
|
SetFlags(PSFlags::Interrupt);
|
|
|
|
SetPC(MemoryReadWord(NesCpu::NMIVector));
|
|
} else {
|
|
Push((uint8_t)flags);
|
|
SetFlags(PSFlags::Interrupt);
|
|
|
|
SetPC(MemoryReadWord(NesCpu::IRQVector));
|
|
}
|
|
|
|
//Ensure we don't start an NMI right after running a BRK instruction (first instruction in IRQ handler must run first - needed for nmi_and_brk test)
|
|
_prevNeedNmi = false;
|
|
}
|
|
|
|
void NesCpu::MemoryWrite(uint16_t addr, uint8_t value, MemoryOperationType operationType)
|
|
{
|
|
#ifdef DUMMYCPU
|
|
LogMemoryOperation(addr, value, operationType);
|
|
#else
|
|
_cpuWrite = true;
|
|
StartCpuCycle(false);
|
|
_memoryManager->Write(addr, value, operationType);
|
|
EndCpuCycle(false);
|
|
_cpuWrite = false;
|
|
#endif
|
|
}
|
|
|
|
uint8_t NesCpu::MemoryRead(uint16_t addr, MemoryOperationType operationType)
|
|
{
|
|
#ifdef DUMMYCPU
|
|
uint8_t value = _memoryManager->DebugRead(addr);
|
|
LogMemoryOperation(addr, value, operationType);
|
|
return value;
|
|
#else
|
|
ProcessPendingDma(addr, operationType);
|
|
|
|
StartCpuCycle(true);
|
|
uint8_t value = _memoryManager->Read(addr, operationType);
|
|
EndCpuCycle(true);
|
|
return value;
|
|
#endif
|
|
}
|
|
|
|
uint16_t NesCpu::FetchOperand()
|
|
{
|
|
switch(_instAddrMode) {
|
|
case NesAddrMode::Acc:
|
|
case NesAddrMode::Imp: DummyPcRead(); return 0;
|
|
case NesAddrMode::Imm:
|
|
case NesAddrMode::Rel: return GetImmediate();
|
|
case NesAddrMode::Zero: return GetZeroAddr();
|
|
case NesAddrMode::ZeroX: return GetZeroXAddr();
|
|
case NesAddrMode::ZeroY: return GetZeroYAddr();
|
|
case NesAddrMode::Ind: return GetIndAddr();
|
|
case NesAddrMode::IndX: return GetIndXAddr();
|
|
case NesAddrMode::IndY: return GetIndYAddr(false);
|
|
case NesAddrMode::IndYW: return GetIndYAddr(true);
|
|
case NesAddrMode::Abs: return GetAbsAddr();
|
|
case NesAddrMode::AbsX: return GetAbsXAddr(false);
|
|
case NesAddrMode::AbsXW: return GetAbsXAddr(true);
|
|
case NesAddrMode::AbsY: return GetAbsYAddr(false);
|
|
case NesAddrMode::AbsYW: return GetAbsYAddr(true);
|
|
case NesAddrMode::Other: return 0; //Do nothing, op is handled specifically
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
void NesCpu::EndCpuCycle(bool forRead)
|
|
{
|
|
_masterClock += forRead ? (_endClockCount + 1) : (_endClockCount - 1);
|
|
_console->GetPpu()->Run(_masterClock - _ppuOffset);
|
|
|
|
//"The internal signal goes high during φ1 of the cycle that follows the one where the edge is detected,
|
|
//and stays high until the NMI has been handled. "
|
|
_prevNeedNmi = _needNmi;
|
|
|
|
//"This edge detector polls the status of the NMI line during φ2 of each CPU cycle (i.e., during the
|
|
//second half of each cycle) and raises an internal signal if the input goes from being high during
|
|
//one cycle to being low during the next"
|
|
if(!_prevNmiFlag && _state.NmiFlag) {
|
|
_needNmi = true;
|
|
}
|
|
_prevNmiFlag = _state.NmiFlag;
|
|
|
|
//"it's really the status of the interrupt lines at the end of the second-to-last cycle that matters."
|
|
//Keep the irq lines values from the previous cycle. The before-to-last cycle's values will be used
|
|
_prevRunIrq = _runIrq;
|
|
_runIrq = ((_state.IrqFlag & _irqMask) > 0 && !CheckFlag(PSFlags::Interrupt));
|
|
}
|
|
|
|
void NesCpu::StartCpuCycle(bool forRead)
|
|
{
|
|
_masterClock += forRead ? (_startClockCount - 1) : (_startClockCount + 1);
|
|
_console->GetPpu()->Run(_masterClock - _ppuOffset);
|
|
_state.CycleCount++;
|
|
_console->ProcessCpuClock();
|
|
}
|
|
|
|
void NesCpu::ProcessPendingDma(uint16_t readAddress, MemoryOperationType opType)
|
|
{
|
|
if(!_needHalt) {
|
|
return;
|
|
}
|
|
|
|
if(_console->GetRegion() == ConsoleRegion::Pal && opType != MemoryOperationType::ExecOpCode) {
|
|
//On PAL, DMA can only start when the CPU attempts to read the opcode for the next instruction
|
|
//This also avoids the bit deletions that can happen because of DMA reads on NTSC
|
|
return;
|
|
}
|
|
|
|
uint16_t prevReadAddress = readAddress;
|
|
bool enableInternalRegReads = (readAddress & 0xFFE0) == 0x4000;
|
|
_needHalt = false;
|
|
|
|
StartCpuCycle(true);
|
|
_memoryManager->Read(readAddress, MemoryOperationType::DmaRead);
|
|
EndCpuCycle(true);
|
|
|
|
if(_abortDmcDma) {
|
|
_dmcDmaRunning = false;
|
|
_abortDmcDma = false;
|
|
|
|
if(!_spriteDmaTransfer) {
|
|
//If DMC DMA was cancelled and OAM DMA isn't about to start,
|
|
//stop processing DMA entirely. Otherwise, OAM DMA needs to run,
|
|
//so the DMA process has to continue.
|
|
_needDummyRead = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
uint16_t spriteDmaCounter = 0;
|
|
uint8_t spriteReadAddr = 0;
|
|
uint8_t readValue = 0;
|
|
|
|
auto processCycle = [this] {
|
|
//Sprite DMA cycles count as halt/dummy cycles for the DMC DMA when both run at the same time
|
|
if(_abortDmcDma) {
|
|
_dmcDmaRunning = false;
|
|
_abortDmcDma = false;
|
|
_needDummyRead = false;
|
|
_needHalt = false;
|
|
} else if(_needHalt) {
|
|
_needHalt = false;
|
|
} else if(_needDummyRead) {
|
|
_needDummyRead = false;
|
|
}
|
|
StartCpuCycle(true);
|
|
};
|
|
|
|
while(_dmcDmaRunning || _spriteDmaTransfer) {
|
|
bool getCycle = (_state.CycleCount & 0x01) == 0;
|
|
if(getCycle) {
|
|
if(_dmcDmaRunning && !_needHalt && !_needDummyRead) {
|
|
//DMC DMA is ready to read a byte (both halt and dummy read cycles were performed before this)
|
|
processCycle();
|
|
_isDmcDmaRead = true; //used by debugger to distinguish between dmc and oam/dummy dma reads
|
|
readValue = ProcessDmaRead(_console->GetApu()->GetDmcReadAddress(), prevReadAddress, enableInternalRegReads);
|
|
_isDmcDmaRead = false;
|
|
EndCpuCycle(true);
|
|
_dmcDmaRunning = false;
|
|
_abortDmcDma = false;
|
|
_console->GetApu()->SetDmcReadBuffer(readValue);
|
|
//On later CPUs, a DMC DMA may start immediately after another DMC DMA. We need to
|
|
//call ProcessPendingDma again to handle the behavior of the halt cycle. This
|
|
//allows the second DMA to clock the joypads again on NES-behavior consoles.
|
|
//Fixes dmc_dma_start_test_v2 case C with sample duplication turned on.
|
|
if(_needHalt) {
|
|
NoInlineProcessPendingDma(readAddress, opType);
|
|
}
|
|
} else if(_spriteDmaTransfer) {
|
|
//DMC DMA is not running, or not ready, run sprite DMA
|
|
processCycle();
|
|
readValue = ProcessDmaRead(_spriteDmaOffset * 0x100 + spriteReadAddr, prevReadAddress, enableInternalRegReads);
|
|
EndCpuCycle(true);
|
|
spriteReadAddr++;
|
|
spriteDmaCounter++;
|
|
} else {
|
|
//DMC DMA is running, but not ready (need halt/dummy read) and sprite DMA isn't runnnig, perform a dummy read
|
|
assert(_needHalt || _needDummyRead);
|
|
processCycle();
|
|
_memoryManager->Read(readAddress, MemoryOperationType::DmaRead);
|
|
EndCpuCycle(true);
|
|
}
|
|
} else {
|
|
if(_spriteDmaTransfer && (spriteDmaCounter & 0x01)) {
|
|
//Sprite DMA write cycle (only do this if a sprite dma read was performed last cycle)
|
|
processCycle();
|
|
_memoryManager->Write(0x2004, readValue, MemoryOperationType::DmaWrite);
|
|
EndCpuCycle(true);
|
|
spriteDmaCounter++;
|
|
if(spriteDmaCounter == 0x200) {
|
|
_spriteDmaTransfer = false;
|
|
}
|
|
} else {
|
|
//Align to read cycle before starting sprite DMA (or align to perform DMC read)
|
|
processCycle();
|
|
_memoryManager->Read(readAddress, MemoryOperationType::DmaRead);
|
|
EndCpuCycle(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
uint8_t NesCpu::ProcessDmaRead(uint16_t addr, uint16_t& prevReadAddress, bool enableInternalRegReads)
|
|
{
|
|
//This is to reproduce a CPU bug that can occur during DMA which can cause the 2A03 to read from
|
|
//its internal registers (4015, 4016, 4017) at the same time as the DMA unit reads a byte from
|
|
//the bus. This bug occurs if the CPU is halted while it's reading a value in the $4000-$401F range.
|
|
//
|
|
//This has a number of side effects:
|
|
// -It can cause a read of $4015 to occur without the program's knowledge, which would clear the frame counter's IRQ flag
|
|
// -It can cause additional bit deletions while reading the input (e.g more than the DMC glitch usually causes)
|
|
// -It can also *prevent* bit deletions from occurring at all in another scenario
|
|
// -It can replace/corrupt the byte that the DMA is reading, causing DMC to play the wrong sample
|
|
|
|
uint8_t val;
|
|
if(!enableInternalRegReads) {
|
|
if(addr >= 0x4015 && addr <= 0x401A) {
|
|
//The readable 2A03 registers ($4015-$4017 and, in test mode, $4018-$401A) can't be seen by DMA in this case.
|
|
val = _memoryManager->GetOpenBus();
|
|
} else {
|
|
val = _memoryManager->Read(addr, MemoryOperationType::DmaRead);
|
|
}
|
|
prevReadAddress = addr;
|
|
return val;
|
|
} else {
|
|
//This glitch causes the CPU to read from the internal APU/Input registers
|
|
//regardless of the address the DMA unit is trying to read
|
|
uint16_t internalAddr = 0x4000 | (addr & 0x1F);
|
|
bool isSameAddress = internalAddr == addr;
|
|
|
|
switch(internalAddr) {
|
|
case 0x4015:
|
|
//Reads to $4015 don't update the value on the external bus
|
|
//Only the CPU's internal bus is updated
|
|
val = _memoryManager->Read<NesCpuBusType::Internal>(internalAddr, MemoryOperationType::DmaRead);
|
|
if(!isSameAddress) {
|
|
//Also trigger a read from the actual address the CPU was supposed to read from (external bus)
|
|
_memoryManager->Read<NesCpuBusType::External>(addr, MemoryOperationType::DmaRead);
|
|
}
|
|
break;
|
|
|
|
case 0x4016:
|
|
case 0x4017:
|
|
val = _memoryManager->Read(internalAddr, MemoryOperationType::DmaRead);
|
|
|
|
if(!isSameAddress) {
|
|
//The DMA unit is reading from a different address, read from it too (external bus)
|
|
uint8_t obMask = ((NesControlManager*)_console->GetControlManager())->GetOpenBusMask(internalAddr - 0x4016);
|
|
uint8_t externalValue = _memoryManager->Read(addr, MemoryOperationType::DmaRead);
|
|
|
|
// Joypads stop driving the bus later than cartridge ROM, so joypad bits win on open bus.
|
|
_memoryManager->SetOpenBus<NesCpuBusType::External>((externalValue & obMask) | (val & ~obMask));
|
|
|
|
//The value seen by the CPU is the bus conflict between the driven joypad bits and the DMA read value.
|
|
//The DMA read may come from an address that is open bus and thus does not drive bits, which should not
|
|
//cause a bus conflict. However, because we read the joypad first before doing the DMA, the joypad read
|
|
//updated open bus, and so any open bus bits in the DMA read will match the joypad read value. So, even
|
|
//if we simulate a bus conflict on these bits, because they are the same, it's the same as taking the
|
|
//joypad's value.
|
|
//For this bus conflict, we keep the external value for all open bus pins on the 4016/4017 port, and we
|
|
//AND all other bits together
|
|
val = (externalValue & obMask) | ((val & ~obMask) & (externalValue & ~obMask));
|
|
}
|
|
break;
|
|
|
|
//TODO if test mode is enabled, handle test mode registers here (and consider making the test mode check earlier in this
|
|
//function check for whether test mode is actually enabled).
|
|
|
|
default:
|
|
val = _memoryManager->Read(addr, MemoryOperationType::DmaRead);
|
|
break;
|
|
}
|
|
|
|
prevReadAddress = internalAddr;
|
|
return val;
|
|
}
|
|
}
|
|
|
|
void NesCpu::RunDMATransfer(uint8_t offsetValue)
|
|
{
|
|
_spriteDmaTransfer = true;
|
|
_spriteDmaOffset = offsetValue;
|
|
_needHalt = true;
|
|
}
|
|
|
|
void NesCpu::StartDmcTransfer()
|
|
{
|
|
_dmcDmaRunning = true;
|
|
_needDummyRead = true;
|
|
_needHalt = true;
|
|
}
|
|
|
|
void NesCpu::StopDmcTransfer()
|
|
{
|
|
if(_dmcDmaRunning) {
|
|
if(_needHalt) {
|
|
//If interrupted before the halt cycle starts, cancel DMA completely
|
|
//This can happen when a write prevents the DMA from starting after being queued
|
|
_dmcDmaRunning = false;
|
|
_needDummyRead = false;
|
|
_needHalt = false;
|
|
} else {
|
|
//Abort DMA if possible (this only appears to be possible if done within the first cycle of DMA)
|
|
_abortDmcDma = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void NesCpu::SetMasterClockDivider(ConsoleRegion region)
|
|
{
|
|
switch(region) {
|
|
default:
|
|
case ConsoleRegion::Ntsc:
|
|
_startClockCount = 6;
|
|
_endClockCount = 6;
|
|
break;
|
|
|
|
case ConsoleRegion::Pal:
|
|
_startClockCount = 8;
|
|
_endClockCount = 8;
|
|
break;
|
|
|
|
case ConsoleRegion::Dendy:
|
|
_startClockCount = 7;
|
|
_endClockCount = 8;
|
|
break;
|
|
}
|
|
}
|
|
|
|
void NesCpu::HLT()
|
|
{
|
|
//Freeze the CPU, implemented by jumping back and re-executing this op infinitely (for performance reasons)
|
|
_state.PC -= 1;
|
|
|
|
//Prevent IRQ/NMI
|
|
_prevRunIrq = false;
|
|
_prevNeedNmi = false;
|
|
|
|
#if !defined(DUMMYCPU)
|
|
if(!_crashed) {
|
|
_crashed = true;
|
|
MessageManager::DisplayMessage("Error", "GameCrash", "Invalid OP code - CPU crashed.");
|
|
_emu->BreakIfDebugging(CpuType::Nes, BreakSource::NesBreakOnCpuCrash);
|
|
|
|
if(!_emu->IsDebugging() && _console->GetRomFormat() == RomFormat::Nsf) {
|
|
//For NSF files, reset cpu if it ever crashes
|
|
_emu->Reset();
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void NesCpu::Serialize(Serializer& s)
|
|
{
|
|
SV(_state.PC);
|
|
SV(_state.SP);
|
|
SV(_state.PS);
|
|
SV(_state.A);
|
|
SV(_state.X);
|
|
SV(_state.Y);
|
|
SV(_state.CycleCount);
|
|
|
|
if(s.GetFormat() != SerializeFormat::Map) {
|
|
//Hide these entries from the Lua API
|
|
SV(_state.NmiFlag);
|
|
SV(_state.IrqFlag);
|
|
SV(_dmcDmaRunning);
|
|
SV(_abortDmcDma);
|
|
SV(_spriteDmaTransfer);
|
|
SV(_needDummyRead);
|
|
SV(_needHalt);
|
|
SV(_startClockCount);
|
|
SV(_endClockCount);
|
|
SV(_ppuOffset);
|
|
SV(_masterClock);
|
|
SV(_prevNeedNmi);
|
|
SV(_prevNmiFlag);
|
|
SV(_needNmi);
|
|
SV(_crashed);
|
|
}
|
|
} |