From 2efca64bed6289cfdd2b8c4c5750fbd60ac6ae95 Mon Sep 17 00:00:00 2001 From: Sour Date: Sat, 20 Jun 2026 11:20:54 +0900 Subject: [PATCH] SNES: Fixed HDMA trigger DMA syncs on every scanline even after all active HDMA channels are done for the frame Super Bonk triggers an HDMA at the top of the screen on scanline 0 and then the HDMA channel stops. This caused the logic to call SyncStartDma & SyncEndDma on every scanline until vblank, even though no HDMA channel is active for the rest of the frame. The lost CPU time caused Super Bonk's demo to desync even earlier than hardware. --- Core/SNES/DmaControllerTypes.h | 1 - Core/SNES/SnesDmaController.cpp | 53 +++++++++++++++++++-------------- Core/SNES/SnesDmaController.h | 5 +++- UI/Interop/DebugApi.cs | 1 - 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/Core/SNES/DmaControllerTypes.h b/Core/SNES/DmaControllerTypes.h index 278d8d026..4adc98940 100644 --- a/Core/SNES/DmaControllerTypes.h +++ b/Core/SNES/DmaControllerTypes.h @@ -20,7 +20,6 @@ struct DmaChannelConfig uint8_t HdmaBank; uint8_t HdmaLineCounterAndRepeat; bool DoTransfer; - bool HdmaFinished; bool UnusedControlFlag; diff --git a/Core/SNES/SnesDmaController.cpp b/Core/SNES/SnesDmaController.cpp index 9c96636c7..77f88f84b 100644 --- a/Core/SNES/SnesDmaController.cpp +++ b/Core/SNES/SnesDmaController.cpp @@ -2,7 +2,6 @@ #include "SNES/SnesDmaController.h" #include "SNES/DmaControllerTypes.h" #include "SNES/SnesMemoryManager.h" -#include "Shared/MessageManager.h" #include "Utilities/Serializer.h" static constexpr uint8_t _transferByteCount[8] = { 1, 2, 2, 4, 4, 4, 2, 4 }; @@ -43,6 +42,7 @@ void SnesDmaController::Reset() _dmaStartDelay = false; _dmaPending = false; _needToProcess = false; + _stoppedHdmaChannels = 0; for(int i = 0; i < 8; i++) { _state.Channel[i].DmaActive = false; @@ -110,11 +110,11 @@ bool SnesDmaController::InitHdmaChannels() { _hdmaInitPending = false; + //Reset internal flags on every frame, whether or not the channels are enabled for(int i = 0; i < 8; i++) { - //Reset internal flags on every frame, whether or not the channels are enabled - _state.Channel[i].HdmaFinished = false; _state.Channel[i].DoTransfer = false; //not resetting this causes graphical glitches in some games (Aladdin, Super Ghouls and Ghosts) } + _stoppedHdmaChannels = 0; if(!_state.HdmaChannels) { //No channels are enabled, no more processing needs to be done @@ -146,8 +146,9 @@ bool SnesDmaController::InitHdmaChannels() _dmaClockCounter += 8; ch.HdmaTableAddress++; - if(ch.HdmaLineCounterAndRepeat == 0) { - ch.HdmaFinished = true; + bool stopped = ch.HdmaLineCounterAndRepeat == 0; + if(stopped) { + StopHdmaChannel(i); } //3. Load Indirect Address, if necessary. @@ -156,7 +157,7 @@ bool SnesDmaController::InitHdmaChannels() _memoryManager->IncMasterClock4(); _dmaClockCounter += 8; - if(!ch.HdmaFinished) { + if(!stopped) { uint8_t msb = _memoryManager->ReadDma((ch.SrcBank << 16) | ch.HdmaTableAddress++, true); _memoryManager->IncMasterClock4(); _dmaClockCounter += 8; @@ -230,11 +231,26 @@ bool SnesDmaController::HasActiveDmaChannel() return false; } +uint8_t SnesDmaController::GetActiveHdmaChannels() +{ + return _state.HdmaChannels & ~_stoppedHdmaChannels; +} + +bool SnesDmaController::IsHdmaChannelActive(int i) +{ + return GetActiveHdmaChannels() & (1 << i); +} + +void SnesDmaController::StopHdmaChannel(int i) +{ + _stoppedHdmaChannels |= (1 << i); +} + bool SnesDmaController::ProcessHdmaChannels() { _hdmaPending = false; - if(!_state.HdmaChannels) { + if(!GetActiveHdmaChannels()) { UpdateNeedToProcessFlag(); return false; } @@ -251,16 +267,12 @@ bool SnesDmaController::ProcessHdmaChannels() //Run all the DMA transfers for each channel first, before fetching data for the next scanline for(int i = 0; i < 8; i++) { DmaChannelConfig& ch = _state.Channel[i]; - if((_state.HdmaChannels & (1 << i)) == 0) { + if(!IsHdmaChannelActive(i)) { continue; } ch.DmaActive = false; - if(ch.HdmaFinished) { - continue; - } - //1. If DoTransfer is false, skip to step 3. if(ch.DoTransfer) { //2. For the number of bytes (1, 2, or 4) required for this Transfer Mode... @@ -272,7 +284,7 @@ bool SnesDmaController::ProcessHdmaChannels() //Update the channel's state & fetch data for the next scanline for(int i = 0; i < 8; i++) { DmaChannelConfig& ch = _state.Channel[i]; - if((_state.HdmaChannels & (1 << i)) == 0 || ch.HdmaFinished) { + if(!IsHdmaChannelActive(i)) { continue; } @@ -318,7 +330,7 @@ bool SnesDmaController::ProcessHdmaChannels() //"c. If $43xA is zero, terminate this HDMA channel for this frame. The bit in $420c is not cleared, though, so it may be automatically restarted next frame." if(ch.HdmaLineCounterAndRepeat == 0) { - ch.HdmaFinished = true; + StopHdmaChannel(i); } //"d. Set DoTransfer to true." @@ -339,12 +351,8 @@ bool SnesDmaController::ProcessHdmaChannels() bool SnesDmaController::IsLastActiveHdmaChannel(uint8_t channel) { - for(int i = channel + 1; i < 8; i++) { - if((_state.HdmaChannels & (1 << i)) && !_state.Channel[i].HdmaFinished) { - return false; - } - } - return true; + uint8_t mask = (1 << (channel + 1)) - 1; + return (GetActiveHdmaChannels() & ~mask) == 0; } void SnesDmaController::UpdateNeedToProcessFlag() @@ -355,7 +363,7 @@ void SnesDmaController::UpdateNeedToProcessFlag() void SnesDmaController::BeginHdmaTransfer() { - if(_state.HdmaChannels) { + if(GetActiveHdmaChannels()) { _hdmaPending = true; _dmaStartDelay = true; UpdateNeedToProcessFlag(); @@ -804,13 +812,14 @@ void SnesDmaController::Serialize(Serializer& s) SV(_hdmaInitPending); SV(_dmaStartDelay); SV(_needToProcess); + SV(_stoppedHdmaChannels); + for(int i = 0; i < 8; i++) { SVI(_state.Channel[i].Decrement); SVI(_state.Channel[i].DestAddress); SVI(_state.Channel[i].DoTransfer); SVI(_state.Channel[i].FixedTransfer); SVI(_state.Channel[i].HdmaBank); - SVI(_state.Channel[i].HdmaFinished); SVI(_state.Channel[i].HdmaIndirectAddressing); SVI(_state.Channel[i].HdmaLineCounterAndRepeat); SVI(_state.Channel[i].HdmaTableAddress); diff --git a/Core/SNES/SnesDmaController.h b/Core/SNES/SnesDmaController.h index d5c9f846d..685d82042 100644 --- a/Core/SNES/SnesDmaController.h +++ b/Core/SNES/SnesDmaController.h @@ -1,6 +1,5 @@ #pragma once #include "pch.h" -#include "SNES/SnesCpuTypes.h" #include "SNES/DmaControllerTypes.h" #include "Utilities/ISerializable.h" @@ -19,6 +18,7 @@ class SnesDmaController final : public ISerializable bool _dmaStartDelay = false; bool _dmaPending = false; uint32_t _dmaClockCounter = 0; + uint8_t _stoppedHdmaChannels = 0; uint8_t _activeChannel = 0; //Used by debugger's event viewer @@ -38,6 +38,9 @@ class SnesDmaController final : public ISerializable void UpdateNeedToProcessFlag(); bool HasActiveDmaChannel(); + uint8_t GetActiveHdmaChannels(); + bool IsHdmaChannelActive(int i); + void StopHdmaChannel(int i); public: SnesDmaController(SnesMemoryManager* memoryManager); diff --git a/UI/Interop/DebugApi.cs b/UI/Interop/DebugApi.cs index 0c2e68b7e..895d7b81a 100644 --- a/UI/Interop/DebugApi.cs +++ b/UI/Interop/DebugApi.cs @@ -760,7 +760,6 @@ public struct DmaChannelConfig public byte HdmaLineCounterAndRepeat; [MarshalAs(UnmanagedType.I1)] public bool DoTransfer; - [MarshalAs(UnmanagedType.I1)] public bool HdmaFinished; [MarshalAs(UnmanagedType.I1)] public bool UnusedControlFlag; public byte UnusedRegister;