From b4c016e6c4296c9a74dacdd37511890f2ea6c90b Mon Sep 17 00:00:00 2001 From: TakuikaNinja Date: Mon, 13 Apr 2026 15:00:38 +1200 Subject: [PATCH 1/7] FDS $4023 should not affect read-only registers Passes latest FDS-Mirroring-Test Disk registers behave closer to hardware in FDS-4023-Test --- Core/NES/Mappers/FDS/Fds.cpp | 47 ++++++++++++++++++++++--------- Core/NES/Mappers/FDS/Fds.h | 3 +- Core/NES/Mappers/FDS/FdsAudio.cpp | 1 + 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/Core/NES/Mappers/FDS/Fds.cpp b/Core/NES/Mappers/FDS/Fds.cpp index c5de3cd1e..54418d846 100644 --- a/Core/NES/Mappers/FDS/Fds.cpp +++ b/Core/NES/Mappers/FDS/Fds.cpp @@ -254,6 +254,13 @@ void Fds::ProcessAutoDiskInsert() } } +/**TODO: + - Proper byte transfer flag handling (set every 1792 master cycles, or 149+1/3 CPU cycles, under normal conditions) + - Should allow for accurate handling of level 2->3 load bug in Ai Senshi Nicol + - Verify End of Head handling (may affect Kosodate Gokko and FMC Disk Card Checker) + - DRAM refresh watchdog implementation (must track PRG-RAM vs external access cycles...) + (Ongoing research, please consult TakuikaNinja for further details) +**/ void Fds::ProcessCpuClock() { BaseProcessCpuClock(); @@ -389,6 +396,18 @@ void Fds::UpdateCrc(uint8_t value) } } +void Fds::SetFdsControlReg(uint8_t value) +{ + _motorOn = (value & 0x01) == 0x01; + _resetTransfer = (value & 0x02) == 0x02; + _readMode = (value & 0x04) == 0x04; + SetMirroringType(value & 0x08 ? MirroringType::Horizontal : MirroringType::Vertical); + _crcControl = (value & 0x10) == 0x10; + //TODO $4025 bit 5 is unknown, all known software sets it to 1 + _diskReady = (value & 0x40) == 0x40; + _diskIrqEnabled = (value & 0x80) == 0x80; +} + void Fds::WriteRegister(uint16_t addr, uint8_t value) { if((!_diskRegEnabled && addr >= 0x4024 && addr <= 0x4026) || (!_soundRegEnabled && addr >= 0x4040)) { @@ -417,10 +436,15 @@ void Fds::WriteRegister(uint16_t addr, uint8_t value) case 0x4023: _diskRegEnabled = (value & 0x01) == 0x01; + //TODO Disabling sound registers should pause audio output? _soundRegEnabled = (value & 0x02) == 0x02; if(!_diskRegEnabled) { _irqEnabled = false; + //Disabling disk registers forces $4025 = $06 (bit 3 reflected in $4030 reads) + SetFdsControlReg(0x06); + //TODO Does disabling disk registers force the external connector = 0xFF ? + //_extConWriteReg = 0xFF; _cpu->ClearIrqSource(IRQSource::External); _cpu->ClearIrqSource(IRQSource::FdsDisk); } @@ -435,14 +459,7 @@ void Fds::WriteRegister(uint16_t addr, uint8_t value) break; case 0x4025: - _motorOn = (value & 0x01) == 0x01; - _resetTransfer = (value & 0x02) == 0x02; - _readMode = (value & 0x04) == 0x04; - SetMirroringType(value & 0x08 ? MirroringType::Horizontal : MirroringType::Vertical); - _crcControl = (value & 0x10) == 0x10; - //Bit 6 is not used, always 1 - _diskReady = (value & 0x40) == 0x40; - _diskIrqEnabled = (value & 0x80) == 0x80; + SetFdsControlReg(value); //Writing to $4025 clears IRQ according to FCEUX, puNES & Nintendulator //Fixes issues in some unlicensed games (error $20 at power on) @@ -464,20 +481,24 @@ void Fds::WriteRegister(uint16_t addr, uint8_t value) uint8_t Fds::ReadRegister(uint16_t addr) { uint8_t value = _memoryManager->GetOpenBus(); - if(_soundRegEnabled && addr >= 0x4040) { + if(addr >= 0x4040) { return _audio->ReadRegister(addr); - } else if(_diskRegEnabled && addr <= 0x4033) { + } else if(addr <= 0x4033) { switch(addr) { case 0x4030: - //These 3 pins are open bus + //These 2 pins are open bus value &= 0x24; - + /**TODO: + - DRAM refresh watchdog IRQ status is returned in bit 1, needs implementation + - End of Head is returned in bit 6, needs verification + - Byte transfer flag is returned in bit 7, not bit 1 (required by Tonkachi Editor) + **/ value |= _cpu->HasIrqSource(IRQSource::External) ? 0x01 : 0x00; value |= _transferComplete ? 0x02 : 0x00; value |= GetMirroringType() == MirroringType::Horizontal ? 0x08 : 0; value |= _useQdFormat && _badCrc ? 0x10 : 0x00; //value |= _endOfHead ? 0x40 : 0x00; - //value |= _diskRegEnabled ? 0x80 : 0x00; + //value |= _transferComplete ? 0x80 : 0x00; _transferComplete = false; _cpu->ClearIrqSource(IRQSource::External); diff --git a/Core/NES/Mappers/FDS/Fds.h b/Core/NES/Mappers/FDS/Fds.h index 8d33cffd0..17ce81c51 100644 --- a/Core/NES/Mappers/FDS/Fds.h +++ b/Core/NES/Mappers/FDS/Fds.h @@ -106,6 +106,7 @@ class Fds : public BaseMapper void ProcessCpuClock() override; void UpdateCrc(uint8_t value); + void SetFdsControlReg(uint8_t value); void WriteRegister(uint16_t addr, uint8_t value) override; uint8_t ReadRegister(uint16_t addr) override; @@ -127,4 +128,4 @@ class Fds : public BaseMapper bool IsDiskInserted(); bool IsAutoInsertDiskEnabled(); -}; \ No newline at end of file +}; diff --git a/Core/NES/Mappers/FDS/FdsAudio.cpp b/Core/NES/Mappers/FDS/FdsAudio.cpp index 8a348dba4..33c208e5a 100644 --- a/Core/NES/Mappers/FDS/FdsAudio.cpp +++ b/Core/NES/Mappers/FDS/FdsAudio.cpp @@ -63,6 +63,7 @@ FdsAudio::FdsAudio(NesConsole* console) : BaseExpansionAudio(console) { } +//TODO Update to switch statement and map read-only FDS audio registers at $4090-$4097 uint8_t FdsAudio::ReadRegister(uint16_t addr) { uint8_t value = _console->GetMemoryManager()->GetOpenBus(); From 680b3d7fce3c5f53a2e7ffa7219061802dd7a5fa Mon Sep 17 00:00:00 2001 From: TakuikaNinja Date: Tue, 14 Apr 2026 14:56:11 +1200 Subject: [PATCH 2/7] FDS external connector improvements $4023.D0 = 0 forces output to $7F Always return good battery status in $4033.D7 --- Core/NES/Mappers/FDS/Fds.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Core/NES/Mappers/FDS/Fds.cpp b/Core/NES/Mappers/FDS/Fds.cpp index 54418d846..f34f617e7 100644 --- a/Core/NES/Mappers/FDS/Fds.cpp +++ b/Core/NES/Mappers/FDS/Fds.cpp @@ -443,8 +443,8 @@ void Fds::WriteRegister(uint16_t addr, uint8_t value) _irqEnabled = false; //Disabling disk registers forces $4025 = $06 (bit 3 reflected in $4030 reads) SetFdsControlReg(0x06); - //TODO Does disabling disk registers force the external connector = 0xFF ? - //_extConWriteReg = 0xFF; + //Disabling disk registers forces $4026 (external connector) = 0x7F + _extConWriteReg = 0x7F; _cpu->ClearIrqSource(IRQSource::External); _cpu->ClearIrqSource(IRQSource::FdsDisk); } @@ -542,7 +542,7 @@ uint8_t Fds::ReadRegister(uint16_t addr) case 0x4033: //Always return good battery - return _extConWriteReg; + return _extConWriteReg | 0x80; } } From 5fe66a2b2e0bd0a373b94bb48d108038de1a38ec Mon Sep 17 00:00:00 2001 From: TakuikaNinja Date: Tue, 5 May 2026 16:35:36 +1200 Subject: [PATCH 3/7] FDS: Implement $4090-$4097 Modulation has unfortunately regressed --- Core/NES/Mappers/FDS/Fds.cpp | 41 ++++++-- Core/NES/Mappers/FDS/Fds.h | 2 +- Core/NES/Mappers/FDS/FdsAudio.cpp | 111 +++++++++++++++++---- Core/NES/Mappers/FDS/FdsAudio.h | 6 +- Core/NES/Mappers/FDS/ModChannel.h | 149 ++++++++++++++++++----------- Core/NES/Mappers/NSF/NsfMapper.cpp | 4 +- 6 files changed, 224 insertions(+), 89 deletions(-) diff --git a/Core/NES/Mappers/FDS/Fds.cpp b/Core/NES/Mappers/FDS/Fds.cpp index ae3e4889e..7aece2379 100644 --- a/Core/NES/Mappers/FDS/Fds.cpp +++ b/Core/NES/Mappers/FDS/Fds.cpp @@ -406,11 +406,22 @@ void Fds::SetFdsControlReg(uint8_t value) //TODO $4025 bit 5 is unknown, all known software sets it to 1 _diskReady = (value & 0x40) == 0x40; _diskIrqEnabled = (value & 0x80) == 0x80; + + //Writing to $4025 clears IRQ according to FCEUX, puNES & Nintendulator + //Fixes issues in some unlicensed games (error $20 at power on) + //TODO This probably depends on the bits set? + _cpu->ClearIrqSource(IRQSource::FdsDisk); } void Fds::WriteRegister(uint16_t addr, uint8_t value) { - if((!_diskRegEnabled && addr >= 0x4024 && addr <= 0x4026) || (!_soundRegEnabled && addr >= 0x4040)) { + if(!_diskRegEnabled && addr >= 0x4024 && addr <= 0x4026) { + return; + } + + //Only $4080 and $4088 seem to consistently deny/mangle writes during audio reset + //TODO Determine $4088 (mod table write) behaviour while in audio reset state + if(!_soundRegEnabled && (addr == 0x4080 || addr == 0x4088)) { return; } @@ -436,7 +447,7 @@ void Fds::WriteRegister(uint16_t addr, uint8_t value) case 0x4023: _diskRegEnabled = (value & 0x01) == 0x01; - //TODO Disabling sound registers should pause audio output? + //TODO This is actually an audio reset, rename this variable in Fds.h _soundRegEnabled = (value & 0x02) == 0x02; if(!_diskRegEnabled) { @@ -448,26 +459,34 @@ void Fds::WriteRegister(uint16_t addr, uint8_t value) _cpu->ClearIrqSource(IRQSource::External); _cpu->ClearIrqSource(IRQSource::FdsDisk); } + + //TODO Determine/implement audio reset behaviour, should probably go in FdsAudio + //(Ongoing research, please consult TakuikaNinja for further details) + if (!_soundRegEnabled){ + //Guessed based on instant muting + //(though there seems to be some kind of analogue "resume" window?) + _audio->WriteRegister(0x4080, 0x80); + _audio->WriteRegister(0x4085, 0x00); // based on $4097 state + // need to set wave accumulator to 0... + // mod table init? + } break; case 0x4024: _writeDataReg = value; _transferComplete = false; - //Unsure about clearing irq here: FCEUX/Nintendulator don't do this, puNES does. + //Needed by some Super Magic Card games, which use this IRQ for raster effects _cpu->ClearIrqSource(IRQSource::FdsDisk); break; case 0x4025: SetFdsControlReg(value); - - //Writing to $4025 clears IRQ according to FCEUX, puNES & Nintendulator - //Fixes issues in some unlicensed games (error $20 at power on) - _cpu->ClearIrqSource(IRQSource::FdsDisk); break; case 0x4026: - _extConWriteReg = value; + //External connector only wires 7 bits + _extConWriteReg = value & 0x7F; break; default: @@ -500,9 +519,11 @@ uint8_t Fds::ReadRegister(uint16_t addr) //value |= _endOfHead ? 0x40 : 0x00; //value |= _transferComplete ? 0x80 : 0x00; - _transferComplete = false; _cpu->ClearIrqSource(IRQSource::External); - _cpu->ClearIrqSource(IRQSource::FdsDisk); + + //Byte transfer flag is NOT cleared by this register! + //_transferComplete = false; + //_cpu->ClearIrqSource(IRQSource::FdsDisk); return value; case 0x4031: diff --git a/Core/NES/Mappers/FDS/Fds.h b/Core/NES/Mappers/FDS/Fds.h index e88e4c04b..aa5fec47e 100644 --- a/Core/NES/Mappers/FDS/Fds.h +++ b/Core/NES/Mappers/FDS/Fds.h @@ -85,7 +85,7 @@ class Fds : public BaseMapper uint32_t GetWorkRamPageSize() override { return 0x8000; } uint32_t GetWorkRamSize() override { return 0x8000; } uint16_t RegisterStartAddress() override { return 0x4020; } - uint16_t RegisterEndAddress() override { return 0x4092; } + uint16_t RegisterEndAddress() override { return 0x4097; } bool AllowRegisterRead() override { return true; } bool EnableCpuClockHook() override { return true; } diff --git a/Core/NES/Mappers/FDS/FdsAudio.cpp b/Core/NES/Mappers/FDS/FdsAudio.cpp index 74ea73b87..4311cc362 100644 --- a/Core/NES/Mappers/FDS/FdsAudio.cpp +++ b/Core/NES/Mappers/FDS/FdsAudio.cpp @@ -20,15 +20,17 @@ void FdsAudio::Serialize(Serializer& s) SV(_disableEnvelopes); SV(_haltWaveform); SV(_masterVolume); - SV(_waveOverflowCounter); + SV(_waveAccumulator); + SV(_waveM2Counter); SV(_wavePitch); SV(_wavePosition); SV(_lastOutput); + SV(_lastGain); } void FdsAudio::ClockAudio() { - int frequency = _volume.GetFrequency(); + uint16_t frequency = _volume.GetFrequency(); if(!_haltWaveform && !_disableEnvelopes) { _volume.TickEnvelope(); if(_mod.TickEnvelope()) { @@ -36,28 +38,47 @@ void FdsAudio::ClockAudio() } } - if(_mod.TickModulator()) { + //TODO Check if modulator and wave units are ticked on the same M2 cycle + //(the FDS stuff is probably ticked by the RAM adapter's crystal - we can't really simulate that here) + if(_mod.TickModulator(_haltWaveform)) { //Modulator was ticked, update wave pitch _mod.UpdateOutput(frequency); } - UpdateOutput(); + if(++_waveM2Counter == 16) { + if(_haltWaveform) { + _waveAccumulator = 0; + } else { - if(!_haltWaveform && frequency + _mod.GetOutput() > 0) { - _waveOverflowCounter += frequency + _mod.GetOutput(); - if(_waveOverflowCounter < frequency + _mod.GetOutput()) { - _wavePosition = (_wavePosition + 1) & 0x3F; + if(!_waveWriteEnabled) { + _waveAccumulator += (frequency * _mod.GetOutput()) & 0xFFFFF; + if(_waveAccumulator > 0xFFFFFF) { + _waveAccumulator -= 0x1000000; + } + + _wavePosition = (_waveAccumulator >> 18) & 0x3F; + } } + _waveM2Counter = 0; } + UpdateOutput(); } void FdsAudio::UpdateOutput() { + //The wave unit continues to run, but the output is held at its previous value if(_waveWriteEnabled) { return; } - uint32_t level = std::min((int)_volume.GetGain(), 32) * WaveVolumeTable[_masterVolume]; + //"Changes to the volume envelope only take effect while the wavetable + // pointer (top 6 bits of wave accumulator) is 0." + if(_wavePosition == 0) { + _lastGain = _volume.GetGain(); + } + uint32_t level = std::min(_lastGain, uint8_t(32)) * WaveVolumeTable[_masterVolume]; + //TODO Volume ramp is actually nonlinear, masked by PWM + 2KHz lowpass + //(would likely kill performance for a miniscule audio accuracy improvement) uint8_t outputLevel = (_waveTable[_wavePosition] * level) / 1152; if(_lastOutput != outputLevel) { @@ -70,7 +91,6 @@ FdsAudio::FdsAudio(NesConsole* console) : BaseExpansionAudio(console) { } -//TODO Update to switch statement and map read-only FDS audio registers at $4090-$4097 uint8_t FdsAudio::ReadRegister(uint16_t addr) { uint8_t value = _console->GetMemoryManager()->GetOpenBus(); @@ -82,12 +102,55 @@ uint8_t FdsAudio::ReadRegister(uint16_t addr) //"When writing is disabled ($4089.7), reading anywhere in 4040-407F returns the value at the current wave position" value |= _waveTable[_wavePosition]; } - } else if(addr == 0x4090) { - value &= 0xC0; - value |= _volume.GetGain(); - } else if(addr == 0x4092) { - value &= 0xC0; - value |= _mod.GetGain(); + } else if(addr >= 0x4090) { + switch(addr) { + case 0x4090: + value &= 0xC0; + value |= _volume.GetGain(); + break; + + case 0x4091: + //Wave accumulator + //value &= 0xC0; + value = (_waveAccumulator >> 12) & 0xFF; + break; + + case 0x4092: + value &= 0xC0; + value |= _mod.GetGain(); + break; + + case 0x4093: + //Mod accumulator + value &= 0x80; + value |= (_mod.GetModAccumulator() >> 5) & 0x7F; + break; + + case 0x4094: + //Wave pitch intermediate result + //value &= 0xC0; + value = (_mod.GetOutput() >> 4) & 0xFF; + break; + + case 0x4095: + //Mod counter increment (lower nybble) + //TODO Determine upper nybble + value &= 0xC0; + value |= _mod.GetModIncrement(); + break; + + case 0x4096: + //Wavetable value + //TODO PWM masking + value &= 0xC0; + value |= _waveTable[_wavePosition] & 0x3F; + break; + + case 0x4097: + value &= 0x80; + value |= _mod.GetCounter() & 0x7F; + break; + } } return value; @@ -179,10 +242,9 @@ void FdsAudio::GetMapperStateEntries(vector& entries) entries.push_back(MapperStateEntry("$4086/7.0-11", "Frequency", _mod.GetFrequency(), MapperStateValueType::Number16)); - //todo emulation logic + this based on new info - //entries.push_back(MapperStateEntry("$4087.6", "???", false, MapperStateValueType::Bool)); + entries.push_back(MapperStateEntry("$4087.6", "Force Tick Modulator", _mod.GetForceCarryOut(), MapperStateValueType::Bool)); - entries.push_back(MapperStateEntry("$4087.7", "Disabled", _mod.IsModulationDisabled(), MapperStateValueType::Bool)); + entries.push_back(MapperStateEntry("$4087.7", "Counter Disabled", _mod.IsModCounterDisabled(), MapperStateValueType::Bool)); entries.push_back(MapperStateEntry("", "Gain", _mod.GetGain(), MapperStateValueType::Number8)); entries.push_back(MapperStateEntry("", "Mod Output", std::to_string(_mod.GetOutput()))); @@ -191,4 +253,15 @@ void FdsAudio::GetMapperStateEntries(vector& entries) entries.push_back(MapperStateEntry("$4089.7", "Wave Write Enabled", _waveWriteEnabled, MapperStateValueType::Bool)); entries.push_back(MapperStateEntry("$408A", "Envelope Speed Multiplier", _volume.GetMasterSpeed(), MapperStateValueType::Number8)); + + entries.push_back(MapperStateEntry("$4090-$4097", "Audio Debug")); + entries.push_back(MapperStateEntry("$4090.0-5", "Volume Gain", _volume.GetGain(), MapperStateValueType::Number8)); + entries.push_back(MapperStateEntry("$4091", "Wave Accumulator", ((_waveAccumulator >> 12) & 0xFF), MapperStateValueType::Number8)); + entries.push_back(MapperStateEntry("$4092.0-5", "Mod Gain", _mod.GetGain(), MapperStateValueType::Number8)); + entries.push_back(MapperStateEntry("$4093.0-6", "Mod Accumulator", ((_mod.GetModAccumulator() >> 5) & 0x7F), MapperStateValueType::Number8)); + entries.push_back(MapperStateEntry("$4094", "Mod Counter * Gain", ((_mod.GetOutput() >> 4) & 0xFF), MapperStateValueType::Number8)); + entries.push_back(MapperStateEntry("$4095.0-3", "Mod Counter Increment", _mod.GetModIncrement(), MapperStateValueType::Number8)); + entries.push_back(MapperStateEntry("$4096.0-5", "Wavetable Value", (_waveTable[_wavePosition] & 0x3F), MapperStateValueType::Number8)); + entries.push_back(MapperStateEntry("$4097.0-6", "Mod Counter Value", (modCounter & 0x7F), MapperStateValueType::Number8)); + } diff --git a/Core/NES/Mappers/FDS/FdsAudio.h b/Core/NES/Mappers/FDS/FdsAudio.h index ce570aa5b..bb5126301 100644 --- a/Core/NES/Mappers/FDS/FdsAudio.h +++ b/Core/NES/Mappers/FDS/FdsAudio.h @@ -27,11 +27,13 @@ class FdsAudio : public BaseExpansionAudio uint8_t _masterVolume = 0; //Internal values - uint16_t _waveOverflowCounter = 0; + uint32_t _waveAccumulator = 0; //24-bit accumulator + uint8_t _waveM2Counter = 0; int32_t _wavePitch = 0; uint8_t _wavePosition = 0; uint8_t _lastOutput = 0; + uint8_t _lastGain = 0; protected: void Serialize(Serializer& s) override; @@ -46,4 +48,4 @@ class FdsAudio : public BaseExpansionAudio void WriteRegister(uint16_t addr, uint8_t value); void GetMapperStateEntries(vector& entries); -}; \ No newline at end of file +}; diff --git a/Core/NES/Mappers/FDS/ModChannel.h b/Core/NES/Mappers/FDS/ModChannel.h index a6bbddf55..66c1010d7 100644 --- a/Core/NES/Mappers/FDS/ModChannel.h +++ b/Core/NES/Mappers/FDS/ModChannel.h @@ -5,30 +5,47 @@ class ModChannel : public BaseFdsChannel { private: - const int32_t ModReset = 0xFF; - const int32_t _modLut[8] = { 0, 1, 2, 4, ModReset, -4, -2, -1 }; + const int16_t ModReset = 0xFF; + const int16_t _modLut[8] = { 0, 1, 2, 4, ModReset, -4, -2, -1 }; int8_t _counter = 0; - bool _modulationDisabled = false; + bool _modCounterDisabled = false; + bool _forceCarryOut = false; - uint8_t _modTable[64] = {}; + uint32_t _modAccumulator = 0; //18-bit accumulator + uint8_t _modM2Counter = 0; + uint8_t _modTable[32] = {}; uint8_t _modTablePosition = 0; - uint16_t _overflowCounter = 0; - int32_t _output = 0; + int8_t _output = 0; protected: void Serialize(Serializer& s) override { BaseFdsChannel::Serialize(s); - SVArray(_modTable, 64); + SVArray(_modTable, 32); SV(_counter); - SV(_modulationDisabled); + SV(_modCounterDisabled); + SV(_forceCarryOut); SV(_modTablePosition); - SV(_overflowCounter); + SV(_modAccumulator); + SV(_modM2Counter); SV(_output); } + void IncrementAccumulator(uint32_t value) + { + _modAccumulator += value; + if(_modAccumulator > 0x3FFFF) { + _modAccumulator -= 0x40000; + } + } + + void UpdateModPosition() + { + _modTablePosition = (_modAccumulator >> 13) & 0x1F; + } + public: virtual void WriteReg(uint16_t addr, uint8_t value) override { @@ -42,21 +59,43 @@ class ModChannel : public BaseFdsChannel break; case 0x4087: BaseFdsChannel::WriteReg(addr, value); - _modulationDisabled = (value & 0x80) == 0x80; - if(_modulationDisabled) { - _overflowCounter = 0; + _modCounterDisabled = (value & 0x80) == 0x80; + // "4087.6 forces a carry out from bit 11." + _forceCarryOut = (value & 0x40) == 0x40; + if(_modCounterDisabled) { + // "Bits 0-12 are reset by 4087.7=1. Bits 13-17 have no reset." + _modAccumulator &= 0x3E000; } break; } } + bool TickEnvelope() + { + if(!_envelopeOff && _masterSpeed > 0) { + _timer--; + if(_timer == 0) { + ResetTimer(); + + if(_volumeIncrease && _gain < 32) { + _gain++; + } else if(!_volumeIncrease && _gain > 0) { + _gain--; + } + return true; + } + } + return false; + } + void WriteModTable(uint8_t value) { //"This register has no effect unless the mod unit is disabled via the high bit of $4087." - if(_modulationDisabled) { - _modTable[_modTablePosition & 0x3F] = value & 0x07; - _modTable[(_modTablePosition + 1) & 0x3F] = value & 0x07; - _modTablePosition = (_modTablePosition + 2) & 0x3F; + if(_modCounterDisabled) { + //"Writing $4088 increments the address (bits 13-17) when 4087.7=1." + _modTable[_modTablePosition] = value & 0x07; + IncrementAccumulator(0x2000); + UpdateModPosition(); } } @@ -72,63 +111,47 @@ class ModChannel : public BaseFdsChannel bool IsEnabled() { - return !_modulationDisabled && _frequency > 0; + return _frequency > 0; } - bool TickModulator() + bool TickModulator(bool haltWaveform) { - if(IsEnabled()) { - _overflowCounter += _frequency; - - if(_overflowCounter < _frequency) { - //Overflowed, tick the modulator - int32_t offset = _modLut[_modTable[_modTablePosition]]; + //$4083.7 also stops the mod table accumulator + if(IsEnabled() && !haltWaveform && ++_modM2Counter == 16) { + IncrementAccumulator(_frequency); + + //"On a carry out from bit 11, update the mod counter (increment $4085 with modtable)." + //"4087.6 forces a carry out from bit 11." + if((_modAccumulator & 0xFFF) < _frequency || _forceCarryOut) { + int16_t offset = _modLut[_modTable[_modTablePosition]]; UpdateCounter(offset == ModReset ? 0 : _counter + offset); - - _modTablePosition = (_modTablePosition + 1) & 0x3F; - - return true; + UpdateModPosition(); } + + _modM2Counter = 0; + return true; } return false; } void UpdateOutput(uint16_t volumePitch) { - //Code from NesDev Wiki - + // code from new info by loopy + // https://forums.nesdev.org/viewtopic.php?p=232662#p232662 // pitch = $4082/4083 (12-bit unsigned pitch value) // counter = $4085 (7-bit signed mod counter) // gain = $4084 (6-bit unsigned mod gain) - // 1. multiply counter by gain, lose lowest 4 bits of result but "round" in a strange way int32_t temp = _counter * _gain; - int32_t remainder = temp & 0xF; - temp >>= 4; - if(remainder > 0 && (temp & 0x80) == 0) { - temp += _counter < 0 ? -1 : 2; + if((temp & 0x0f) && !(temp & 0x800)) { + temp += 0x20; } - - // 2. wrap if a certain range is exceeded - if(temp >= 192) { - temp -= 256; - } else if(temp < -64) { - temp += 256; - } - - // 3. multiply result by pitch, then round to nearest while dropping 6 bits - temp = volumePitch * temp; - remainder = temp & 0x3F; - temp >>= 6; - if(remainder >= 32) { - temp += 1; - } - - // final mod result is in temp + temp += 0x400; + temp = (temp >> 4) & 0xff; _output = temp; } - int32_t GetOutput() + int8_t GetOutput() { return _output; } @@ -138,8 +161,24 @@ class ModChannel : public BaseFdsChannel return _counter; } - bool IsModulationDisabled() + uint32_t GetModAccumulator() + { + return _modAccumulator; + } + + bool GetForceCarryOut() + { + return _forceCarryOut; + } + + bool IsModCounterDisabled() + { + return _modCounterDisabled; + } + + uint8_t GetModIncrement() { - return _modulationDisabled; + int16_t offset = _modLut[_modTable[_modTablePosition]]; + return ModReset ? 0x0C : offset & 0x0F; } }; diff --git a/Core/NES/Mappers/NSF/NsfMapper.cpp b/Core/NES/Mappers/NSF/NsfMapper.cpp index 945c2b57d..9e891583d 100644 --- a/Core/NES/Mappers/NSF/NsfMapper.cpp +++ b/Core/NES/Mappers/NSF/NsfMapper.cpp @@ -70,7 +70,7 @@ void NsfMapper::InitMapper(RomData& romData) } if(_nsfHeader.SoundChips & NsfSoundChips::FDS) { - AddRegisterRange(0x4040, 0x4092, MemoryOperation::Any); + AddRegisterRange(0x4040, 0x4097, MemoryOperation::Any); } //Reset/IRQ vector @@ -227,7 +227,7 @@ void NsfMapper::ProcessCpuClock() uint8_t NsfMapper::ReadRegister(uint16_t addr) { - if((_nsfHeader.SoundChips & NsfSoundChips::FDS) && addr >= 0x4040 && addr <= 0x4092) { + if((_nsfHeader.SoundChips & NsfSoundChips::FDS) && addr >= 0x4040 && addr <= 0x4097) { return _fdsAudio->ReadRegister(addr); } else if((_nsfHeader.SoundChips & NsfSoundChips::Namco) && addr >= 0x4800 && addr <= 0x4FFF) { return _namcoAudio->ReadRegister(addr); From 78efd6a13e7ebdea000c85184098d567c06c0c99 Mon Sep 17 00:00:00 2001 From: TakuikaNinja Date: Tue, 5 May 2026 17:55:30 +1200 Subject: [PATCH 4/7] FDS: Modulation tweaks - Fix GetModIncrement() - Optimise mod counter register display - Add disable logic in UpdateCounter() for now fixme: Modulation issues in FDS-Audio-Registers and Bio Miracle Bokutte Upa --- Core/NES/Mappers/FDS/FdsAudio.cpp | 4 ++-- Core/NES/Mappers/FDS/ModChannel.h | 23 +++++++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Core/NES/Mappers/FDS/FdsAudio.cpp b/Core/NES/Mappers/FDS/FdsAudio.cpp index 4311cc362..661d5618b 100644 --- a/Core/NES/Mappers/FDS/FdsAudio.cpp +++ b/Core/NES/Mappers/FDS/FdsAudio.cpp @@ -238,7 +238,7 @@ void FdsAudio::GetMapperStateEntries(vector& entries) entries.push_back(MapperStateEntry("$4084.7", "Envelope Disabled", _mod.IsEnvelopeDisabled(), MapperStateValueType::Bool)); int8_t modCounter = _mod.GetCounter(); - entries.push_back(MapperStateEntry("$4085.0-6", "Counter", std::to_string(modCounter), modCounter < 0 ? (modCounter + 128) : modCounter)); + entries.push_back(MapperStateEntry("$4085.0-6", "Counter", std::to_string(modCounter), (modCounter & 0x7F))); entries.push_back(MapperStateEntry("$4086/7.0-11", "Frequency", _mod.GetFrequency(), MapperStateValueType::Number16)); @@ -262,6 +262,6 @@ void FdsAudio::GetMapperStateEntries(vector& entries) entries.push_back(MapperStateEntry("$4094", "Mod Counter * Gain", ((_mod.GetOutput() >> 4) & 0xFF), MapperStateValueType::Number8)); entries.push_back(MapperStateEntry("$4095.0-3", "Mod Counter Increment", _mod.GetModIncrement(), MapperStateValueType::Number8)); entries.push_back(MapperStateEntry("$4096.0-5", "Wavetable Value", (_waveTable[_wavePosition] & 0x3F), MapperStateValueType::Number8)); - entries.push_back(MapperStateEntry("$4097.0-6", "Mod Counter Value", (modCounter & 0x7F), MapperStateValueType::Number8)); + entries.push_back(MapperStateEntry("$4097.0-6", "Mod Counter Value", std::to_string(modCounter), (modCounter & 0x7F))); } diff --git a/Core/NES/Mappers/FDS/ModChannel.h b/Core/NES/Mappers/FDS/ModChannel.h index 66c1010d7..f8301f96c 100644 --- a/Core/NES/Mappers/FDS/ModChannel.h +++ b/Core/NES/Mappers/FDS/ModChannel.h @@ -101,23 +101,22 @@ class ModChannel : public BaseFdsChannel void UpdateCounter(int8_t value) { - _counter = value; - if(_counter >= 64) { - _counter -= 128; - } else if(_counter < -64) { - _counter += 128; + //"The mod table counter is stopped, that's all. + //The freq mod formula is ALWAYS in effect, 4084/4085 still modify the wave frequency." + if(!_modCounterDisabled) { + _counter = value; + if(_counter >= 64) { + _counter -= 128; + } else if(_counter < -64) { + _counter += 128; + } } } - bool IsEnabled() - { - return _frequency > 0; - } - bool TickModulator(bool haltWaveform) { //$4083.7 also stops the mod table accumulator - if(IsEnabled() && !haltWaveform && ++_modM2Counter == 16) { + if(_frequency > 0 && !haltWaveform && ++_modM2Counter == 16) { IncrementAccumulator(_frequency); //"On a carry out from bit 11, update the mod counter (increment $4085 with modtable)." @@ -179,6 +178,6 @@ class ModChannel : public BaseFdsChannel uint8_t GetModIncrement() { int16_t offset = _modLut[_modTable[_modTablePosition]]; - return ModReset ? 0x0C : offset & 0x0F; + return offset == ModReset ? 0x0C : offset & 0x0F; } }; From bc751c4323fa41979bfcd169207d83345cda520d Mon Sep 17 00:00:00 2001 From: TakuikaNinja Date: Tue, 5 May 2026 20:32:08 +1200 Subject: [PATCH 5/7] Run clang-format on FDS files --- Core/NES/Mappers/FDS/Fds.cpp | 12 ++++++------ Core/NES/Mappers/FDS/FdsAudio.cpp | 16 +++++++--------- Core/NES/Mappers/FDS/FdsAudio.h | 2 +- Core/NES/Mappers/FDS/ModChannel.h | 4 ++-- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Core/NES/Mappers/FDS/Fds.cpp b/Core/NES/Mappers/FDS/Fds.cpp index 7aece2379..60daf4232 100644 --- a/Core/NES/Mappers/FDS/Fds.cpp +++ b/Core/NES/Mappers/FDS/Fds.cpp @@ -256,7 +256,7 @@ void Fds::ProcessAutoDiskInsert() /**TODO: - Proper byte transfer flag handling (set every 1792 master cycles, or 149+1/3 CPU cycles, under normal conditions) - - Should allow for accurate handling of level 2->3 load bug in Ai Senshi Nicol + - Should allow for accurate handling of level 2->3 load bug in Ai Senshi Nicol - Verify End of Head handling (may affect Kosodate Gokko and FMC Disk Card Checker) - DRAM refresh watchdog implementation (must track PRG-RAM vs external access cycles...) (Ongoing research, please consult TakuikaNinja for further details) @@ -406,7 +406,7 @@ void Fds::SetFdsControlReg(uint8_t value) //TODO $4025 bit 5 is unknown, all known software sets it to 1 _diskReady = (value & 0x40) == 0x40; _diskIrqEnabled = (value & 0x80) == 0x80; - + //Writing to $4025 clears IRQ according to FCEUX, puNES & Nintendulator //Fixes issues in some unlicensed games (error $20 at power on) //TODO This probably depends on the bits set? @@ -418,7 +418,7 @@ void Fds::WriteRegister(uint16_t addr, uint8_t value) if(!_diskRegEnabled && addr >= 0x4024 && addr <= 0x4026) { return; } - + //Only $4080 and $4088 seem to consistently deny/mangle writes during audio reset //TODO Determine $4088 (mod table write) behaviour while in audio reset state if(!_soundRegEnabled && (addr == 0x4080 || addr == 0x4088)) { @@ -459,10 +459,10 @@ void Fds::WriteRegister(uint16_t addr, uint8_t value) _cpu->ClearIrqSource(IRQSource::External); _cpu->ClearIrqSource(IRQSource::FdsDisk); } - + //TODO Determine/implement audio reset behaviour, should probably go in FdsAudio //(Ongoing research, please consult TakuikaNinja for further details) - if (!_soundRegEnabled){ + if(!_soundRegEnabled) { //Guessed based on instant muting //(though there seems to be some kind of analogue "resume" window?) _audio->WriteRegister(0x4080, 0x80); @@ -520,7 +520,7 @@ uint8_t Fds::ReadRegister(uint16_t addr) //value |= _transferComplete ? 0x80 : 0x00; _cpu->ClearIrqSource(IRQSource::External); - + //Byte transfer flag is NOT cleared by this register! //_transferComplete = false; //_cpu->ClearIrqSource(IRQSource::FdsDisk); diff --git a/Core/NES/Mappers/FDS/FdsAudio.cpp b/Core/NES/Mappers/FDS/FdsAudio.cpp index 661d5618b..cce59ac5c 100644 --- a/Core/NES/Mappers/FDS/FdsAudio.cpp +++ b/Core/NES/Mappers/FDS/FdsAudio.cpp @@ -49,7 +49,6 @@ void FdsAudio::ClockAudio() if(_haltWaveform) { _waveAccumulator = 0; } else { - if(!_waveWriteEnabled) { _waveAccumulator += (frequency * _mod.GetOutput()) & 0xFFFFF; if(_waveAccumulator > 0xFFFFFF) { @@ -108,44 +107,44 @@ uint8_t FdsAudio::ReadRegister(uint16_t addr) value &= 0xC0; value |= _volume.GetGain(); break; - + case 0x4091: //Wave accumulator //value &= 0xC0; value = (_waveAccumulator >> 12) & 0xFF; break; - + case 0x4092: value &= 0xC0; value |= _mod.GetGain(); break; - + case 0x4093: //Mod accumulator value &= 0x80; value |= (_mod.GetModAccumulator() >> 5) & 0x7F; break; - + case 0x4094: //Wave pitch intermediate result //value &= 0xC0; value = (_mod.GetOutput() >> 4) & 0xFF; break; - + case 0x4095: //Mod counter increment (lower nybble) //TODO Determine upper nybble value &= 0xC0; value |= _mod.GetModIncrement(); break; - + case 0x4096: //Wavetable value //TODO PWM masking value &= 0xC0; value |= _waveTable[_wavePosition] & 0x3F; break; - + case 0x4097: value &= 0x80; value |= _mod.GetCounter() & 0x7F; @@ -263,5 +262,4 @@ void FdsAudio::GetMapperStateEntries(vector& entries) entries.push_back(MapperStateEntry("$4095.0-3", "Mod Counter Increment", _mod.GetModIncrement(), MapperStateValueType::Number8)); entries.push_back(MapperStateEntry("$4096.0-5", "Wavetable Value", (_waveTable[_wavePosition] & 0x3F), MapperStateValueType::Number8)); entries.push_back(MapperStateEntry("$4097.0-6", "Mod Counter Value", std::to_string(modCounter), (modCounter & 0x7F))); - } diff --git a/Core/NES/Mappers/FDS/FdsAudio.h b/Core/NES/Mappers/FDS/FdsAudio.h index bb5126301..38fa456e1 100644 --- a/Core/NES/Mappers/FDS/FdsAudio.h +++ b/Core/NES/Mappers/FDS/FdsAudio.h @@ -27,7 +27,7 @@ class FdsAudio : public BaseExpansionAudio uint8_t _masterVolume = 0; //Internal values - uint32_t _waveAccumulator = 0; //24-bit accumulator + uint32_t _waveAccumulator = 0; //24-bit accumulator uint8_t _waveM2Counter = 0; int32_t _wavePitch = 0; uint8_t _wavePosition = 0; diff --git a/Core/NES/Mappers/FDS/ModChannel.h b/Core/NES/Mappers/FDS/ModChannel.h index f8301f96c..b49662849 100644 --- a/Core/NES/Mappers/FDS/ModChannel.h +++ b/Core/NES/Mappers/FDS/ModChannel.h @@ -12,7 +12,7 @@ class ModChannel : public BaseFdsChannel bool _modCounterDisabled = false; bool _forceCarryOut = false; - uint32_t _modAccumulator = 0; //18-bit accumulator + uint32_t _modAccumulator = 0; //18-bit accumulator uint8_t _modM2Counter = 0; uint8_t _modTable[32] = {}; uint8_t _modTablePosition = 0; @@ -174,7 +174,7 @@ class ModChannel : public BaseFdsChannel { return _modCounterDisabled; } - + uint8_t GetModIncrement() { int16_t offset = _modLut[_modTable[_modTablePosition]]; From 917c301412f48dc9beefa735f4d8de78aec94cb6 Mon Sep 17 00:00:00 2001 From: TakuikaNinja Date: Sun, 10 May 2026 15:27:44 +1200 Subject: [PATCH 6/7] FDS: Move byte transfer flag to $4030 bit 7 Eventually needed by Tonkachi Editor, though it still fails to access disks for editing --- Core/NES/Mappers/FDS/Fds.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Core/NES/Mappers/FDS/Fds.cpp b/Core/NES/Mappers/FDS/Fds.cpp index 60daf4232..21a5726e1 100644 --- a/Core/NES/Mappers/FDS/Fds.cpp +++ b/Core/NES/Mappers/FDS/Fds.cpp @@ -510,14 +510,13 @@ uint8_t Fds::ReadRegister(uint16_t addr) /**TODO: - DRAM refresh watchdog IRQ status is returned in bit 1, needs implementation - End of Head is returned in bit 6, needs verification - - Byte transfer flag is returned in bit 7, not bit 1 (required by Tonkachi Editor) **/ value |= _cpu->HasIrqSource(IRQSource::External) ? 0x01 : 0x00; - value |= _transferComplete ? 0x02 : 0x00; + //value |= _transferComplete ? 0x02 : 0x00; value |= GetMirroringType() == MirroringType::Horizontal ? 0x08 : 0; value |= _useQdFormat && _badCrc ? 0x10 : 0x00; //value |= _endOfHead ? 0x40 : 0x00; - //value |= _transferComplete ? 0x80 : 0x00; + value |= _transferComplete ? 0x80 : 0x00; _cpu->ClearIrqSource(IRQSource::External); From fc19295e7ff549983027acded487d4df07ed1708 Mon Sep 17 00:00:00 2001 From: TakuikaNinja Date: Sun, 10 May 2026 19:01:57 +1200 Subject: [PATCH 7/7] Halt mod accumulator while $4087.D7 = 1 Fixes modulation in FDS-Audio-Registers Also remove redundant TickEnvelope definition --- Core/NES/Mappers/FDS/Fds.cpp | 26 ++++++++++------- Core/NES/Mappers/FDS/ModChannel.h | 48 ++++++++++--------------------- 2 files changed, 30 insertions(+), 44 deletions(-) diff --git a/Core/NES/Mappers/FDS/Fds.cpp b/Core/NES/Mappers/FDS/Fds.cpp index 21a5726e1..e12a75f32 100644 --- a/Core/NES/Mappers/FDS/Fds.cpp +++ b/Core/NES/Mappers/FDS/Fds.cpp @@ -419,9 +419,12 @@ void Fds::WriteRegister(uint16_t addr, uint8_t value) return; } - //Only $4080 and $4088 seem to consistently deny/mangle writes during audio reset - //TODO Determine $4088 (mod table write) behaviour while in audio reset state - if(!_soundRegEnabled && (addr == 0x4080 || addr == 0x4088)) { + /**Only $4080 (volume envelope) seems to consistently deny writes during audio reset + TODO: + - $4085 (mod counter) denies writes too, but there is an unknown delay before being forced to 0 + - Determine $4088 (mod table write) behaviour while in audio reset state + **/ + if(!_soundRegEnabled && (addr == 0x4080 || addr == 0x4085 || addr == 0x4088)) { return; } @@ -460,15 +463,16 @@ void Fds::WriteRegister(uint16_t addr, uint8_t value) _cpu->ClearIrqSource(IRQSource::FdsDisk); } - //TODO Determine/implement audio reset behaviour, should probably go in FdsAudio - //(Ongoing research, please consult TakuikaNinja for further details) + /**TODO Determine/implement audio reset behaviour, should probably go in FdsAudio: + - Proper method of resetting modulation state ($4085 write below doesn't always work) + - Reset wave accumulator to 0 + - Mod table appears to init with (or decay to) all 0s? + - There seems to be some kind of analogue "resume" window? + (Ongoing research, please consult TakuikaNinja for further details) + **/ if(!_soundRegEnabled) { - //Guessed based on instant muting - //(though there seems to be some kind of analogue "resume" window?) - _audio->WriteRegister(0x4080, 0x80); - _audio->WriteRegister(0x4085, 0x00); // based on $4097 state - // need to set wave accumulator to 0... - // mod table init? + _audio->WriteRegister(0x4080, 0x80); //Based on instant muting + _audio->WriteRegister(0x4085, 0x00); //Based on $4097 state } break; diff --git a/Core/NES/Mappers/FDS/ModChannel.h b/Core/NES/Mappers/FDS/ModChannel.h index b49662849..75b0c3e73 100644 --- a/Core/NES/Mappers/FDS/ModChannel.h +++ b/Core/NES/Mappers/FDS/ModChannel.h @@ -5,8 +5,8 @@ class ModChannel : public BaseFdsChannel { private: - const int16_t ModReset = 0xFF; - const int16_t _modLut[8] = { 0, 1, 2, 4, ModReset, -4, -2, -1 }; + const int16_t _modReset = 0xFF; + const int16_t _modLut[8] = { 0, 1, 2, 4, _modReset, -4, -2, -1 }; int8_t _counter = 0; bool _modCounterDisabled = false; @@ -70,24 +70,6 @@ class ModChannel : public BaseFdsChannel } } - bool TickEnvelope() - { - if(!_envelopeOff && _masterSpeed > 0) { - _timer--; - if(_timer == 0) { - ResetTimer(); - - if(_volumeIncrease && _gain < 32) { - _gain++; - } else if(!_volumeIncrease && _gain > 0) { - _gain--; - } - return true; - } - } - return false; - } - void WriteModTable(uint8_t value) { //"This register has no effect unless the mod unit is disabled via the high bit of $4087." @@ -101,15 +83,11 @@ class ModChannel : public BaseFdsChannel void UpdateCounter(int8_t value) { - //"The mod table counter is stopped, that's all. - //The freq mod formula is ALWAYS in effect, 4084/4085 still modify the wave frequency." - if(!_modCounterDisabled) { - _counter = value; - if(_counter >= 64) { - _counter -= 128; - } else if(_counter < -64) { - _counter += 128; - } + _counter = value; + if(_counter >= 64) { + _counter -= 128; + } else if(_counter < -64) { + _counter += 128; } } @@ -117,14 +95,18 @@ class ModChannel : public BaseFdsChannel { //$4083.7 also stops the mod table accumulator if(_frequency > 0 && !haltWaveform && ++_modM2Counter == 16) { - IncrementAccumulator(_frequency); + //"The mod table counter is stopped, that's all. + //The freq mod formula is ALWAYS in effect, 4084/4085 still modify the wave frequency." + if(!_modCounterDisabled) { + IncrementAccumulator(_frequency); + UpdateModPosition(); + } //"On a carry out from bit 11, update the mod counter (increment $4085 with modtable)." //"4087.6 forces a carry out from bit 11." if((_modAccumulator & 0xFFF) < _frequency || _forceCarryOut) { int16_t offset = _modLut[_modTable[_modTablePosition]]; - UpdateCounter(offset == ModReset ? 0 : _counter + offset); - UpdateModPosition(); + UpdateCounter(offset == _modReset ? 0 : _counter + offset); } _modM2Counter = 0; @@ -178,6 +160,6 @@ class ModChannel : public BaseFdsChannel uint8_t GetModIncrement() { int16_t offset = _modLut[_modTable[_modTablePosition]]; - return offset == ModReset ? 0x0C : offset & 0x0F; + return offset == _modReset ? 0x0C : offset & 0x0F; } };