mirror of
https://github.com/libretro/MesenCE.git
synced 2026-11-10 11:11:32 -10:00
On late-G and H CPUs, starting a 1-byte sample just before the end of a DMC period can cause the sample to be fetched and played twice. This change fixes 2 issues related to this. First, new research found that this bug is because there is normally at least a 1-APU cycle gap between the end of one DMC DMA and the start of the next DMC DMA on earlier CPUs, but these later CPUs remove that gap, allowing the aborted DMA behavior to occur sooner such that it can't be aborted. But this means it actually affects any sample that is started at the end of a DMC period, not just ones that trigger the extra DMA. Mesen was previously only doing this in the case of 1-byte non-looping samples. This change fixes this issue. This was validated with AccuracyCoin's Implicit DMA Abort test and with dmc_dma_stop_test_v3 test F. Second, when there are two DMC DMA's back to back while reading a joypad register, both of them should cause an extra clock of the joypads when using NES behavior (unless suppressed by the APU register activation bug). However, we were only seeing extra clocks with HVC-001 behavior (which is handled on every DMA cycle always) because Mesen only runs its halt cycle logic for the first of the two DMAs. This change makes ProcessPendingDma call itself in the case where we handle DMC DMA back to back so it can do this extra joypad clock if need be. This second change was validated with dmc_dma_start_test_v2 test C, dmc_dma_implicit_stop_level_test, and a variant of apu_register_activation_test.
339 lines
8.8 KiB
C++
339 lines
8.8 KiB
C++
#include "pch.h"
|
|
|
|
#include "NES/APU/DeltaModulationChannel.h"
|
|
#include "NES/APU/NesApu.h"
|
|
#include "NES/NesCpu.h"
|
|
#include "NES/NesConstants.h"
|
|
#include "NES/NesConsole.h"
|
|
#include "NES/NesMemoryManager.h"
|
|
|
|
DeltaModulationChannel::DeltaModulationChannel(NesConsole* console) : _timer(AudioChannel::DMC, console->GetSoundMixer())
|
|
{
|
|
_console = console;
|
|
}
|
|
|
|
void DeltaModulationChannel::Reset(bool softReset)
|
|
{
|
|
_timer.Reset(softReset);
|
|
|
|
if(!softReset) {
|
|
//At power on, the sample address is set to $C000 and the sample length is set to 1
|
|
//Resetting does not reset their value
|
|
_sampleAddr = 0xC000;
|
|
_sampleLength = 1;
|
|
}
|
|
|
|
_outputLevel = 0;
|
|
_irqEnabled = false;
|
|
_loopFlag = false;
|
|
|
|
_currentAddr = 0;
|
|
_bytesRemaining = 0;
|
|
_readBuffer = 0;
|
|
_bufferEmpty = true;
|
|
|
|
_shiftRegister = 0;
|
|
_bitsRemaining = 8;
|
|
_silenceFlag = true;
|
|
_needToRun = false;
|
|
_transferStartDelay = 0;
|
|
_disableDelay = 0;
|
|
|
|
_lastValue4011 = 0;
|
|
|
|
//Not sure if this is accurate, but it seems to make things better rather than worse (for dpcmletterbox)
|
|
//"On the real thing, I think the power-on value is 428 (or the equivalent at least - it uses a linear feedback shift register), though only the even/oddness should matter for this test."
|
|
_timer.SetPeriod((NesApu::GetApuRegion(_console) == ConsoleRegion::Ntsc ? _dmcPeriodLookupTableNtsc : _dmcPeriodLookupTablePal)[0] - 1);
|
|
|
|
//Make sure the DMC doesn't tick on the first cycle - this is part of what keeps Sprite/DMC DMA tests working while fixing dmc_pitch.
|
|
_timer.SetTimer(_timer.GetPeriod());
|
|
}
|
|
|
|
void DeltaModulationChannel::InitSample()
|
|
{
|
|
_currentAddr = _sampleAddr;
|
|
_bytesRemaining = _sampleLength;
|
|
_needToRun |= _bytesRemaining > 0;
|
|
}
|
|
|
|
void DeltaModulationChannel::StartDmcTransfer()
|
|
{
|
|
if(_bufferEmpty && _bytesRemaining > 0) {
|
|
_console->GetCpu()->StartDmcTransfer();
|
|
}
|
|
}
|
|
|
|
uint16_t DeltaModulationChannel::GetDmcReadAddress()
|
|
{
|
|
return _currentAddr;
|
|
}
|
|
|
|
void DeltaModulationChannel::SetDmcReadBuffer(uint8_t value)
|
|
{
|
|
if(_bytesRemaining > 0) {
|
|
_readBuffer = value;
|
|
_bufferEmpty = false;
|
|
|
|
//"The address is incremented; if it exceeds $FFFF, it is wrapped around to $8000."
|
|
_currentAddr++;
|
|
if(_currentAddr == 0) {
|
|
_currentAddr = 0x8000;
|
|
}
|
|
|
|
_bytesRemaining--;
|
|
|
|
if(_bytesRemaining == 0) {
|
|
if(_loopFlag) {
|
|
//Looped sample should never set IRQ flag
|
|
InitSample();
|
|
} else if(_irqEnabled) {
|
|
_console->GetCpu()->SetIrqSource(IRQSource::DMC);
|
|
}
|
|
}
|
|
}
|
|
|
|
//When DMA ends around the time the bit counter resets, a CPU glitch sometimes causes another DMA to be requested immediately.
|
|
if(_bitsRemaining == 8 && _timer.GetTimer() == _timer.GetPeriod() && _console->GetNesConfig().EnableDmcSampleDuplicationGlitch) {
|
|
//When the DMA ends on the same cycle as the bit counter resets.
|
|
//On earlier CPUs, there is normally a 1 APU cycle gap between the end of one DMC DMA
|
|
//and the start of another. All H CPUs and some G CPUs (those from around 1990 and later)
|
|
//remove this gap requirement.
|
|
_shiftRegister = _readBuffer;
|
|
_silenceFlag = false;
|
|
_bufferEmpty = true;
|
|
|
|
//If the sample was 1 byte, a full DMA is performed on the same address
|
|
//and the same sample byte is played twice in a row by the DMC.
|
|
if(_sampleLength == 1) {
|
|
InitSample();
|
|
}
|
|
StartDmcTransfer();
|
|
} else if(_sampleLength == 1 && !_loopFlag && _bitsRemaining == 1 && _timer.GetTimer() < 2) {
|
|
//When the DMA ends on the APU cycle before the bit counter resets.
|
|
//If this happens right before the bit counter resets,
|
|
//a DMA is triggered and aborted 1 cycle later (causing one halted CPU cycle)
|
|
_shiftRegister = _readBuffer;
|
|
_bufferEmpty = false;
|
|
InitSample();
|
|
_disableDelay = 3;
|
|
}
|
|
}
|
|
|
|
void DeltaModulationChannel::Run(uint32_t targetCycle)
|
|
{
|
|
while(_timer.Run(targetCycle)) {
|
|
if(!_silenceFlag) {
|
|
uint8_t bit;
|
|
if(_console->GetNesConfig().ReverseDpcmBitOrder) {
|
|
bit = _shiftRegister & 0x80;
|
|
_shiftRegister <<= 1;
|
|
} else {
|
|
bit = _shiftRegister & 0x01;
|
|
_shiftRegister >>= 1;
|
|
}
|
|
|
|
if(bit) {
|
|
if(_outputLevel <= 125) {
|
|
_outputLevel += 2;
|
|
}
|
|
} else {
|
|
if(_outputLevel >= 2) {
|
|
_outputLevel -= 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
_bitsRemaining--;
|
|
if(_bitsRemaining == 0) {
|
|
_bitsRemaining = 8;
|
|
if(_bufferEmpty) {
|
|
_silenceFlag = true;
|
|
} else {
|
|
_silenceFlag = false;
|
|
_shiftRegister = _readBuffer;
|
|
_bufferEmpty = true;
|
|
_needToRun = true;
|
|
if(_transferStartDelay == 0) {
|
|
//Don't trigger the DMA if the channel was just enabled by a 4015 write
|
|
//The DMA will be triggered later (see ProcessClock)
|
|
//Based on AccuracyCoin's "Delta Modulation Channel" test result
|
|
StartDmcTransfer();
|
|
}
|
|
}
|
|
}
|
|
|
|
_timer.AddOutput(_outputLevel);
|
|
}
|
|
}
|
|
|
|
bool DeltaModulationChannel::IrqPending(uint32_t cyclesToRun)
|
|
{
|
|
if(_irqEnabled && _bytesRemaining > 0) {
|
|
uint32_t cyclesToEmptyBuffer = (_bitsRemaining + (_bytesRemaining - 1) * 8) * _timer.GetPeriod();
|
|
if(cyclesToRun >= cyclesToEmptyBuffer) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool DeltaModulationChannel::GetStatus()
|
|
{
|
|
return _bytesRemaining > 0;
|
|
}
|
|
|
|
void DeltaModulationChannel::GetMemoryRanges(MemoryRanges& ranges)
|
|
{
|
|
ranges.AddHandler(MemoryOperation::Write, 0x4010, 0x4013);
|
|
}
|
|
|
|
void DeltaModulationChannel::WriteRam(uint16_t addr, uint8_t value)
|
|
{
|
|
_console->GetApu()->Run();
|
|
|
|
switch(addr & 0x03) {
|
|
case 0: //4010
|
|
_irqEnabled = (value & 0x80) == 0x80;
|
|
_loopFlag = (value & 0x40) == 0x40;
|
|
|
|
//"The rate determines for how many CPU cycles happen between changes in the output level during automatic delta-encoded sample playback."
|
|
//Because BaseApuChannel does not decrement when setting _timer, we need to actually set the value to 1 less than the lookup table
|
|
_timer.SetPeriod((NesApu::GetApuRegion(_console) == ConsoleRegion::Ntsc ? _dmcPeriodLookupTableNtsc : _dmcPeriodLookupTablePal)[value & 0x0F] - 1);
|
|
|
|
if(!_irqEnabled) {
|
|
_console->GetCpu()->ClearIrqSource(IRQSource::DMC);
|
|
}
|
|
break;
|
|
|
|
case 1: { //4011
|
|
uint8_t newValue = value & 0x7F;
|
|
uint8_t previousLevel = _outputLevel;
|
|
_outputLevel = newValue;
|
|
|
|
if(_console->GetNesConfig().ReduceDmcPopping && abs(_outputLevel - previousLevel) > 50) {
|
|
//Reduce popping sounds for 4011 writes
|
|
_outputLevel -= (_outputLevel - previousLevel) / 2;
|
|
}
|
|
|
|
//4011 applies new output right away, not on the timer's reload. This fixes bad DMC sound when playing through 4011.
|
|
_timer.AddOutput(_outputLevel);
|
|
|
|
if(_lastValue4011 != value && newValue > 0) {
|
|
_console->SetNextFrameOverclockStatus(true);
|
|
}
|
|
|
|
_lastValue4011 = newValue;
|
|
break;
|
|
}
|
|
|
|
case 2: //4012
|
|
_sampleAddr = 0xC000 | ((uint32_t)value << 6);
|
|
if(value > 0) {
|
|
_console->SetNextFrameOverclockStatus(false);
|
|
}
|
|
break;
|
|
|
|
case 3: //4013
|
|
_sampleLength = (value << 4) | 0x0001;
|
|
if(value > 0) {
|
|
_console->SetNextFrameOverclockStatus(false);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void DeltaModulationChannel::EndFrame()
|
|
{
|
|
_timer.EndFrame();
|
|
}
|
|
|
|
void DeltaModulationChannel::SetEnabled(bool enabled)
|
|
{
|
|
if(!enabled) {
|
|
if(_disableDelay == 0) {
|
|
//Disabling takes effect with a 1 apu cycle delay
|
|
//If a DMA starts during this time, it gets cancelled
|
|
//but this will still cause the CPU to be halted for 1 cycle
|
|
if((_console->GetCpu()->GetCycleCount() & 0x01) == 0) {
|
|
_disableDelay = 2;
|
|
} else {
|
|
_disableDelay = 3;
|
|
}
|
|
}
|
|
_needToRun = true;
|
|
} else if(_bytesRemaining == 0) {
|
|
InitSample();
|
|
|
|
//Delay a number of cycles based on odd/even cycles
|
|
//Allows behavior to match dmc_dma_start_test
|
|
if((_console->GetCpu()->GetCycleCount() & 0x01) == 0) {
|
|
_transferStartDelay = 2;
|
|
} else {
|
|
_transferStartDelay = 3;
|
|
}
|
|
_needToRun = true;
|
|
}
|
|
}
|
|
|
|
void DeltaModulationChannel::ProcessClock()
|
|
{
|
|
if(_disableDelay && --_disableDelay == 0) {
|
|
_disableDelay = 0;
|
|
_bytesRemaining = 0;
|
|
|
|
//Abort any on-going transfer that hasn't fully started
|
|
_console->GetCpu()->StopDmcTransfer();
|
|
}
|
|
|
|
if(_transferStartDelay && --_transferStartDelay == 0) {
|
|
StartDmcTransfer();
|
|
}
|
|
|
|
_needToRun = _disableDelay || _transferStartDelay || _bytesRemaining;
|
|
}
|
|
|
|
bool DeltaModulationChannel::NeedToRun()
|
|
{
|
|
if(_needToRun) {
|
|
ProcessClock();
|
|
}
|
|
return _needToRun;
|
|
}
|
|
|
|
ApuDmcState DeltaModulationChannel::GetState()
|
|
{
|
|
ApuDmcState state;
|
|
state.BytesRemaining = _bytesRemaining;
|
|
state.IrqEnabled = _irqEnabled;
|
|
state.Loop = _loopFlag;
|
|
state.OutputVolume = _timer.GetLastOutput();
|
|
state.Period = _timer.GetPeriod();
|
|
state.Timer = _timer.GetTimer();
|
|
state.SampleRate = (double)NesConstants::GetClockRate(NesApu::GetApuRegion(_console)) / (_timer.GetPeriod() + 1);
|
|
state.SampleAddr = _sampleAddr;
|
|
state.NextSampleAddr = _currentAddr;
|
|
state.SampleLength = _sampleLength;
|
|
return state;
|
|
}
|
|
|
|
void DeltaModulationChannel::Serialize(Serializer& s)
|
|
{
|
|
SV(_sampleAddr);
|
|
SV(_sampleLength);
|
|
SV(_outputLevel);
|
|
SV(_irqEnabled);
|
|
SV(_loopFlag);
|
|
SV(_currentAddr);
|
|
SV(_bytesRemaining);
|
|
SV(_readBuffer);
|
|
SV(_bufferEmpty);
|
|
SV(_shiftRegister);
|
|
SV(_bitsRemaining);
|
|
SV(_silenceFlag);
|
|
SV(_needToRun);
|
|
SV(_timer);
|
|
|
|
SV(_transferStartDelay);
|
|
SV(_disableDelay);
|
|
}
|