From 9709bc6ed72de10b17a01cdddd7c5cd29559ea70 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Sat, 15 Nov 2025 10:25:30 +0000 Subject: [PATCH 01/11] APU: On write to 4017, run events immediatly --- src/Nes/APU/BusInterface/FrameCounter.hs | 4 ++++ src/Nes/APU/Tick.hs | 6 +----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Nes/APU/BusInterface/FrameCounter.hs b/src/Nes/APU/BusInterface/FrameCounter.hs index 638aaa0..47a71a5 100644 --- a/src/Nes/APU/BusInterface/FrameCounter.hs +++ b/src/Nes/APU/BusInterface/FrameCounter.hs @@ -19,6 +19,10 @@ write4017 byte = do modifyAPUState $ modifyFrameCounter $ \fc -> fc{sequenceMode = seqMode, inhibitInterrupt = inhibit, delayedWriteSideEffectCycle = Just delay} + when (seqMode == FiveStep) $ do + runQuarterFrameEvent + runHalfFrameEvent + -- If the mode flag is set, then both "quarter frame" and "half frame" signals are also generated when inhibit $ do setFrameInterruptFlag False diff --git a/src/Nes/APU/Tick.hs b/src/Nes/APU/Tick.hs index 7730d79..7fde30c 100644 --- a/src/Nes/APU/Tick.hs +++ b/src/Nes/APU/Tick.hs @@ -96,14 +96,10 @@ tickDelayedWriteBuffer = do fc <- withAPUState frameCounter case delayedWriteSideEffectCycle fc of Nothing -> return () - Just 0 -> do - seqMode <- withAPUState $ sequenceMode . frameCounter + Just 0 -> modifyAPUState $ modifyFrameCounter $ const fc{delayedWriteSideEffectCycle = Nothing, FC.sequenceStep = 0, cycles = 0} - when (seqMode == FiveStep) $ do - runQuarterFrameEvent - runHalfFrameEvent Just n -> modifyAPUState $ modifyFrameCounter $ From 51a721628268d65dc4693d986b9ddb4d297dd96f Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Sat, 15 Nov 2025 10:25:44 +0000 Subject: [PATCH 02/11] APU: status register: fix length counter bits --- src/Nes/APU/BusInterface/Status.hs | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Nes/APU/BusInterface/Status.hs b/src/Nes/APU/BusInterface/Status.hs index 48aa36a..f171a16 100644 --- a/src/Nes/APU/BusInterface/Status.hs +++ b/src/Nes/APU/BusInterface/Status.hs @@ -49,17 +49,20 @@ read4015 = do dmcInterruptBit <- withSideEffect $ getFlag DMCDMA when frameInterruptBit $ do setFrameInterruptFlag False - return $ - setBit' dmcInterruptBit 7 $ - setBit' frameInterruptBit 6 $ - setBit' dmcBit 4 $ - setBit' noiseBit 3 $ - setBit' triangleBit 2 $ - setBit' pulse2Bit 1 $ - setBit' - pulse1Bit - 0 - 0 + let res = + setBit' dmcInterruptBit 7 $ + setBit' frameInterruptBit 6 $ + setBit' dmcBit 4 $ + setBit' noiseBit 3 $ + setBit' triangleBit 2 $ + setBit' pulse2Bit 1 $ + setBit' + pulse1Bit + 0 + 0 + return res where setBit' b i a = if b then a `setBit` i else a `clearBit` i - lengthCounterBit st = let lc = getLengthCounter st in isEnabled lc + lengthCounterBit st = + let lc = getLengthCounter st + in isEnabled lc && not (isSilencedByLengthCounter st) From 9b397d35519107ddeb75ffff7e7e4e077169ab3b Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Sat, 15 Nov 2025 20:19:27 +0000 Subject: [PATCH 03/11] APU: DMC: rename interrupt flag field --- src/Nes/APU/BusInterface/DMC.hs | 2 +- src/Nes/APU/State/DMC.hs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Nes/APU/BusInterface/DMC.hs b/src/Nes/APU/BusInterface/DMC.hs index 921a5a4..b046b60 100644 --- a/src/Nes/APU/BusInterface/DMC.hs +++ b/src/Nes/APU/BusInterface/DMC.hs @@ -15,7 +15,7 @@ write4010 byte = do rate = getPeriodValue rateIdx modifyAPUState $ modifyDMC $ \dmc -> dmc - { irqEnabledFlag = irq + { interruptFlag = irq , loopFlag = loop , period = rate } diff --git a/src/Nes/APU/State/DMC.hs b/src/Nes/APU/State/DMC.hs index 39c4219..933fd5d 100644 --- a/src/Nes/APU/State/DMC.hs +++ b/src/Nes/APU/State/DMC.hs @@ -23,7 +23,7 @@ import Nes.FlagRegister import Nes.Memory data DMC = MkDMC - { irqEnabledFlag :: {-# UNPACK #-} !Bool + { interruptFlag :: {-# UNPACK #-} !Bool , loopFlag :: {-# UNPACK #-} !Bool , period :: {-# UNPACK #-} !Int , timer :: {-# UNPACK #-} !Int @@ -44,7 +44,7 @@ data DMC = MkDMC newDMC :: DMC newDMC = MkDMC{..} where - irqEnabledFlag = False + interruptFlag = False loopFlag = False period = 0 timer = 0 @@ -127,7 +127,7 @@ loadSampleBuffer byte dmc = , shouldClock = newRemainingLength > 0 } shouldRestartSample = newRemainingLength == 0 && loopFlag dmc - shouldIRQ = newRemainingLength == 0 && irqEnabledFlag dmc + shouldIRQ = newRemainingLength == 0 && interruptFlag dmc in if shouldRestartSample then (restartSample dmc1, mempty) From 0f8bab4a608e5010749bd08e325ced4cac9138dd Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Sat, 15 Nov 2025 20:19:45 +0000 Subject: [PATCH 04/11] APU: Fix bit 7 of status register --- src/Nes/APU/BusInterface/Status.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Nes/APU/BusInterface/Status.hs b/src/Nes/APU/BusInterface/Status.hs index f171a16..163eefa 100644 --- a/src/Nes/APU/BusInterface/Status.hs +++ b/src/Nes/APU/BusInterface/Status.hs @@ -46,7 +46,7 @@ read4015 = do pulse2Bit <- withAPUState $ lengthCounterBit . pulse2 dmcBit <- withAPUState $ \st -> sampleBytesRemaining (dmc st) > 0 frameInterruptBit <- withSideEffect $ getFlag IRQ - dmcInterruptBit <- withSideEffect $ getFlag DMCDMA + dmcInterruptBit <- withAPUState $ interruptFlag . dmc when frameInterruptBit $ do setFrameInterruptFlag False let res = From 4fa4e334c7d13ff76fa4fdba3cfedc7d41a524d9 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Sat, 15 Nov 2025 20:20:24 +0000 Subject: [PATCH 05/11] CPU: Handle side effect: Call APU function for DMC DMA --- src/Nes/APU/Tick.hs | 2 +- src/Nes/CPU/Monad.hs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Nes/APU/Tick.hs b/src/Nes/APU/Tick.hs index 7fde30c..e946fd0 100644 --- a/src/Nes/APU/Tick.hs +++ b/src/Nes/APU/Tick.hs @@ -31,7 +31,7 @@ import Nes.Bus.SideEffect import Nes.FlagRegister import Prelude hiding (cycle) --- $use +-- $semantic -- The APU being a part of the CPU, they both tick at the same time. However, some ticks are updated every other CPU cycles. -- Here the 'tick' function should be called every CPU tick, and pass as parameter whether the tick is on an even CPU cycle or not. -- Same goes for 'tickMany'. diff --git a/src/Nes/CPU/Monad.hs b/src/Nes/CPU/Monad.hs index 455c840..13a386f 100644 --- a/src/Nes/CPU/Monad.hs +++ b/src/Nes/CPU/Monad.hs @@ -40,9 +40,9 @@ module Nes.CPU.Monad ( import Control.Monad import Control.Monad.IO.Class import Data.Bits (Bits (shiftR), testBit) -import Nes.APU.Monad (modifyAPUState) -import Nes.APU.State (APUState (dmc), modifyDMC) -import Nes.APU.State.DMC (DMC (sampleBuffer, sampleBufferAddr)) +import Nes.APU.Monad (modifyAPUState, modifyAPUStateWithSideEffect) +import Nes.APU.State (APUState (dmc), modifyDMC, modifyDMC') +import Nes.APU.State.DMC (DMC (sampleBuffer, sampleBufferAddr), loadSampleBuffer) import Nes.Bus (Bus (..)) import Nes.Bus.Constants import Nes.Bus.Monad (BusM, runBusM) @@ -221,5 +221,5 @@ handleSideEffect = do when hasDMCDMA $ withBus $ do sampleByteAddr <- BusM.withBus $ sampleBufferAddr . dmc . apuState sample <- Nes.Memory.readByte sampleByteAddr () - BusM.withAPU $ modifyAPUState $ modifyDMC $ \d -> d{sampleBuffer = Just sample} + BusM.withAPU $ modifyAPUStateWithSideEffect $ modifyDMC' $ loadSampleBuffer sample BusM.modifyBus $ \b -> b{cpuSideEffect = clearFlag DMCDMA (cpuSideEffect b)} From f75a3b9b2ced2239140a24de16a4c19ac74cb8a9 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Sat, 15 Nov 2025 20:21:53 +0000 Subject: [PATCH 06/11] APU: DMC: Fix period table --- src/Nes/APU/State/DMC.hs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Nes/APU/State/DMC.hs b/src/Nes/APU/State/DMC.hs index 933fd5d..42399ea 100644 --- a/src/Nes/APU/State/DMC.hs +++ b/src/Nes/APU/State/DMC.hs @@ -61,8 +61,11 @@ newDMC = MkDMC{..} shouldClock = False sleepingCycles = 0 +periodTable :: [Int] +periodTable = [428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54] + getPeriodValue :: Int -> Int -getPeriodValue idx = fromMaybe 428 ([428, 380, 340, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54] !? idx) +getPeriodValue idx = fromMaybe 428 (periodTable !? idx) -- | When a sample is (re)started, the current address is set to the sample address, and bytes remaining is set to the sample length. restartSample :: DMC -> DMC From d9372ae09907bae08b0f9c71e2d228371c10fe1c Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Mon, 17 Nov 2025 11:58:11 +0000 Subject: [PATCH 07/11] APU: Clear IRQ flag on get cycle --- src/Nes/APU/BusInterface/Status.hs | 7 +++++-- src/Nes/APU/Tick.hs | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Nes/APU/BusInterface/Status.hs b/src/Nes/APU/BusInterface/Status.hs index 163eefa..fe662c0 100644 --- a/src/Nes/APU/BusInterface/Status.hs +++ b/src/Nes/APU/BusInterface/Status.hs @@ -5,6 +5,7 @@ import Data.Bits import Nes.APU.Monad import Nes.APU.State import Nes.APU.State.DMC +import Nes.APU.State.FrameCounter (FrameCounter (frameInterruptFlag)) import Nes.APU.State.LengthCounter import Nes.APU.Tick (setFrameInterruptFlag) import Nes.Bus.SideEffect @@ -45,8 +46,10 @@ read4015 = do pulse1Bit <- withAPUState $ lengthCounterBit . pulse1 pulse2Bit <- withAPUState $ lengthCounterBit . pulse2 dmcBit <- withAPUState $ \st -> sampleBytesRemaining (dmc st) > 0 - frameInterruptBit <- withSideEffect $ getFlag IRQ - dmcInterruptBit <- withAPUState $ interruptFlag . dmc + frameInterruptBit <- withAPUState $ frameInterruptFlag . frameCounter + dmcInterruptBit <- withSideEffect $ getFlag DMCDMA + -- TODO Clearing flag should be done on every GET cycle + -- https://github.com/100thCoin/AccuracyCoin/blob/a7bf0cfaee7dee9e7bfbd0e30435b85cb539139e/AccuracyCoin.asm#L9003 when frameInterruptBit $ do setFrameInterruptFlag False let res = diff --git a/src/Nes/APU/Tick.hs b/src/Nes/APU/Tick.hs index e946fd0..360363d 100644 --- a/src/Nes/APU/Tick.hs +++ b/src/Nes/APU/Tick.hs @@ -59,7 +59,6 @@ tickOnce isAPUCycle = do modifyPulse1 tickPulse . modifyPulse2 tickPulse tickFrameCounter - -- Mixing sample <- withAPUState getMixerOutput modifyFilterChain $ consume sample @@ -119,6 +118,8 @@ tickFrameCounterFourStep = do inhibitFrameInterrupt <- withAPUState $ inhibitInterrupt . frameCounter when (step < 4) runQuarterFrameEvent when (step == 1 || step == 3) runHalfFrameEvent + when (step == 4) $ -- Flag should be cleared when going from put to get + setFrameInterruptFlag False when (step == 3 && not inhibitFrameInterrupt) $ setFrameInterruptFlag True From f8d04f71e193ecd2b5b3b14ed9484fbfe9ee7143 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Mon, 17 Nov 2025 19:26:06 +0000 Subject: [PATCH 08/11] tmp --- funes.cabal | 1 + src/Nes/Bus.hs | 7 +++++ src/Nes/CPU/Instructions/Interrupt.hs | 6 +--- src/Nes/CPU/Interpreter.hs | 23 +++++++-------- src/Nes/CPU/Interrupt.hs | 31 +++++++++++++++++++++ src/Nes/CPU/Monad.hs | 40 +++++++++++++-------------- src/Nes/Interrupt.hs | 29 +++++++++++++++---- 7 files changed, 94 insertions(+), 43 deletions(-) create mode 100644 src/Nes/CPU/Interrupt.hs diff --git a/funes.cabal b/funes.cabal index 514ad7a..df09ce2 100644 --- a/funes.cabal +++ b/funes.cabal @@ -69,6 +69,7 @@ library Nes.CPU.Instructions.Transfer Nes.CPU.Instructions.Unofficial Nes.CPU.Interpreter + Nes.CPU.Interrupt Nes.CPU.Monad Nes.CPU.State Nes.FlagRegister diff --git a/src/Nes/Bus.hs b/src/Nes/Bus.hs index 95321aa..433bf49 100644 --- a/src/Nes/Bus.hs +++ b/src/Nes/Bus.hs @@ -9,12 +9,14 @@ module Nes.Bus ( Bus (..), newBus, modifyPPUState, + modifyCPUInterrupt, ) where import Nes.APU.State (APUState, newAPUState) import Nes.Bus.SideEffect (CPUSideEffect) import Nes.Controller import Nes.Internal +import Nes.Interrupt import Nes.Memory import Nes.Memory.Unsafe () import Nes.PPU.Constants @@ -46,6 +48,7 @@ data Bus = Bus -- ^ Last read/written byte , apuState :: !APUState , cpuSideEffect :: {-# UNPACK #-} !CPUSideEffect + , cpuInterrupt :: {-# UNPACK #-} !InterruptStatus } newBus :: Rom -> (Bus -> IO Bus) -> (Float -> IO ()) -> (Double -> Int -> IO (Double, Int)) -> IO Bus @@ -68,6 +71,10 @@ newBus rom_ onNewFrame_ pushSample_ tickCallback_ = do 0 (newAPUState pushSample_) mempty + (MkIE []) modifyPPUState :: (PPUState -> PPUState) -> Bus -> Bus modifyPPUState f bus = bus{ppuState = f $ ppuState bus} + +modifyCPUInterrupt :: (InterruptStatus -> InterruptStatus) -> Bus -> Bus +modifyCPUInterrupt f b = b{cpuInterrupt = f (cpuInterrupt b)} diff --git a/src/Nes/CPU/Instructions/Interrupt.hs b/src/Nes/CPU/Instructions/Interrupt.hs index eb238dd..ea650db 100644 --- a/src/Nes/CPU/Instructions/Interrupt.hs +++ b/src/Nes/CPU/Instructions/Interrupt.hs @@ -1,13 +1,9 @@ module Nes.CPU.Instructions.Interrupt (brk) where -import Control.Monad import Nes.CPU.Monad -import Nes.CPU.State (CPUState (status), StatusRegisterFlag (InterruptDisable)) -import Nes.FlagRegister (getFlag) import Nes.Interrupt brk :: CPU r () brk = do incrementPC - interruptDisable <- withCPUState $ getFlag InterruptDisable . status - unless interruptDisable $ interrupt BRK + pushInterrupt BRK diff --git a/src/Nes/CPU/Interpreter.hs b/src/Nes/CPU/Interpreter.hs index ec481d9..d89d3af 100644 --- a/src/Nes/CPU/Interpreter.hs +++ b/src/Nes/CPU/Interpreter.hs @@ -11,6 +11,7 @@ import Data.Map import Nes.Bus (Bus (..)) import Nes.Bus.Monad (withPPU) import Nes.CPU.Instructions.Map +import Nes.CPU.Interrupt (handleInterrupt) import Nes.CPU.Monad import Nes.CPU.State import Nes.Interrupt @@ -45,21 +46,21 @@ interpretWithCallback callback = do modifyPPUState $ \st -> st{nmiInterrupt = False} return f ) - when hasNmiInterrupt $ interrupt NMI + when hasNmiInterrupt $ pushInterrupt NMI callback oldCycleCount <- getCycles opCode <- readAtPC incrementPC - do - forceMultiByte <- go opCode - newCycleCount <- getCycles - -- Each opcode should take at least 2 ticks - -- We cannot just check that addressing is none, - -- because some opcode w/o addressing take more that 1 cycle - -- e.g. php - -- This does not apply to unofficial KIL/JAM opcodes - when (forceMultiByte && newCycleCount - 1 == oldCycleCount) tickOnce - interpretWithCallback callback + forceMultiByte <- go opCode + newCycleCount <- getCycles + -- Each opcode should take at least 2 ticks + -- We cannot just check that addressing is none, + -- because some opcode w/o addressing take more that 1 cycle + -- e.g. php + -- This does not apply to unofficial KIL/JAM opcodes + when (forceMultiByte && newCycleCount - 1 == oldCycleCount) tickOnce + handleInterrupt + interpretWithCallback callback where {-# INLINE go #-} go opcode = case Data.Map.lookup opcode opcodeMap of diff --git a/src/Nes/CPU/Interrupt.hs b/src/Nes/CPU/Interrupt.hs new file mode 100644 index 0000000..380c2e0 --- /dev/null +++ b/src/Nes/CPU/Interrupt.hs @@ -0,0 +1,31 @@ +module Nes.CPU.Interrupt (handleInterrupt) where + +import Data.Bits +import Nes.CPU.Monad +import Nes.CPU.State +import Nes.FlagRegister +import Nes.Interrupt +import Nes.Memory + +handleInterrupt :: CPU r () +handleInterrupt = do + maskInterrupt <- withCPUState $ getFlag InterruptDisable . status + pendingInterrupt <- popInterrupt + case pendingInterrupt of + Nothing -> pure () + Just signal + | maskInterrupt && signal /= NMI -> pure () + | otherwise -> handleInterruptSignal signal + +handleInterruptSignal :: Interrupt -> CPU r () +handleInterruptSignal signal = do + pushAddrStack =<< getPC + let mask = getFlagMask signal + flag <- + withCPUState $ + setFlag' BreakCommand (testBit mask 4) + . setFlag' BreakCommand2 (testBit mask 5) + . status + pushByteStack $ unSR flag + modifyCPUState $ modifyStatusRegister $ setFlag InterruptDisable + setPC =<< readAddr (getVectorAddr signal) () diff --git a/src/Nes/CPU/Monad.hs b/src/Nes/CPU/Monad.hs index 13a386f..419f656 100644 --- a/src/Nes/CPU/Monad.hs +++ b/src/Nes/CPU/Monad.hs @@ -30,8 +30,9 @@ module Nes.CPU.Monad ( pushAddrStack, pushByteStack, - -- * Interruption - interrupt, + -- * Interrupt + pushInterrupt, + popInterrupt, -- * Unsafe unsafeWithBus, @@ -39,13 +40,14 @@ module Nes.CPU.Monad ( import Control.Monad import Control.Monad.IO.Class -import Data.Bits (Bits (shiftR), testBit) -import Nes.APU.Monad (modifyAPUState, modifyAPUStateWithSideEffect) -import Nes.APU.State (APUState (dmc), modifyDMC, modifyDMC') -import Nes.APU.State.DMC (DMC (sampleBuffer, sampleBufferAddr), loadSampleBuffer) -import Nes.Bus (Bus (..)) +import Data.Bits (Bits (shiftR)) +import Data.Maybe (listToMaybe) +import Nes.APU.Monad (modifyAPUStateWithSideEffect) +import Nes.APU.State (APUState (dmc), modifyDMC') +import Nes.APU.State.DMC (DMC (sampleBufferAddr), loadSampleBuffer) +import Nes.Bus (Bus (..), modifyCPUInterrupt) import Nes.Bus.Constants -import Nes.Bus.Monad (BusM, runBusM) +import Nes.Bus.Monad (BusM, modifyBus, runBusM) import qualified Nes.Bus.Monad as BusM import Nes.Bus.SideEffect import Nes.CPU.State @@ -170,19 +172,14 @@ reset = do pc <- readAddr 0xfffc () modifyCPUState (const $ newCPUState{programCounter = pc}) -interrupt :: Interrupt -> CPU r () -interrupt signal = do - pushAddrStack =<< getPC - let mask = getFlagMask signal - flag <- - withCPUState $ - setFlag' BreakCommand (testBit mask 4) - . setFlag' BreakCommand2 (testBit mask 5) - . status - pushByteStack $ unSR flag - modifyCPUState $ modifyStatusRegister $ setFlag InterruptDisable - withBus $ BusM.tick (getCPUCycles signal) - setPC =<< readAddr (getVectorAddr signal) () +pushInterrupt :: Interrupt -> CPU r () +pushInterrupt i = withBus (modifyBus $ modifyCPUInterrupt $ modifyPendingInterrupt (++ [i])) + +popInterrupt :: CPU r (Maybe Interrupt) +popInterrupt = withBus $ do + pendingHead <- BusM.withBus $ take 1 . pendingInterrupts . cpuInterrupt + modifyBus $ modifyCPUInterrupt $ modifyPendingInterrupt $ drop 1 + return $ listToMaybe pendingHead instance MemoryInterface () (CPU r) where {-# INLINE readByte #-} @@ -215,6 +212,7 @@ tick = withBus . BusM.tick tickOnce :: CPU r () tickOnce = Nes.CPU.Monad.tick 1 +-- TODO Delete me handleSideEffect :: CPU r () handleSideEffect = do hasDMCDMA <- withBusState $ getFlag DMCDMA . cpuSideEffect diff --git a/src/Nes/Interrupt.hs b/src/Nes/Interrupt.hs index d400661..d94ee38 100644 --- a/src/Nes/Interrupt.hs +++ b/src/Nes/Interrupt.hs @@ -1,20 +1,37 @@ -module Nes.Interrupt (Interrupt (..), getVectorAddr, getFlagMask, getCPUCycles) where +module Nes.Interrupt ( + -- * Interrupt Enum + Interrupt (..), + IRQSource (..), + getVectorAddr, + getFlagMask, + + -- * Status + InterruptStatus (..), + modifyPendingInterrupt, +) where import Nes.Memory -data Interrupt = NMI | BRK deriving (Eq, Show) +data Interrupt = NMI | BRK | IRQ IRQSource deriving (Eq, Show) + +data IRQSource = DMA | FrameCounter deriving (Eq, Show) getVectorAddr :: Interrupt -> Addr getVectorAddr = \case NMI -> 0xfffa BRK -> 0xfffe + IRQ _ -> 0xfffe getFlagMask :: Interrupt -> Byte getFlagMask = \case NMI -> 0b00100000 + IRQ _ -> 0b00100000 BRK -> 0b00110000 -getCPUCycles :: Interrupt -> Int -getCPUCycles = \case - NMI -> 2 - BRK -> 1 +data InterruptStatus = MkIE + { pendingInterrupts :: {-# UNPACK #-} ![Interrupt] + -- ^ Will be true if the CPU is executing the interrupt handler + } + +modifyPendingInterrupt :: ([Interrupt] -> [Interrupt]) -> InterruptStatus -> InterruptStatus +modifyPendingInterrupt f s = s{pendingInterrupts = f $ pendingInterrupts s} From f4050a5214e1613f6c0c19441fa12b130c150cc8 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 18 Nov 2025 14:45:06 +0000 Subject: [PATCH 09/11] Refactor Interrupts --- src/Nes/APU/BusInterface/Status.hs | 5 +-- src/Nes/APU/Monad.hs | 54 +++++++++++++-------------- src/Nes/APU/State.hs | 6 +-- src/Nes/APU/State/DMC.hs | 33 +++++++++------- src/Nes/APU/Tick.hs | 7 ++-- src/Nes/Bus.hs | 10 +++-- src/Nes/Bus/Monad.hs | 12 +++--- src/Nes/Bus/SideEffect.hs | 2 + src/Nes/CPU/Instructions/Interrupt.hs | 2 +- src/Nes/CPU/Instructions/Jump.hs | 12 +----- src/Nes/CPU/Instructions/Stack.hs | 22 +---------- src/Nes/CPU/Interpreter.hs | 2 +- src/Nes/CPU/Interrupt.hs | 13 ++----- src/Nes/CPU/Monad.hs | 43 +++++++++++++++------ src/Nes/CPU/State.hs | 4 +- src/Nes/Interrupt.hs | 31 ++++++++++----- test/nestest/Spec.hs | 24 ++++++++++-- 17 files changed, 152 insertions(+), 130 deletions(-) diff --git a/src/Nes/APU/BusInterface/Status.hs b/src/Nes/APU/BusInterface/Status.hs index fe662c0..9cc0b9b 100644 --- a/src/Nes/APU/BusInterface/Status.hs +++ b/src/Nes/APU/BusInterface/Status.hs @@ -8,8 +8,7 @@ import Nes.APU.State.DMC import Nes.APU.State.FrameCounter (FrameCounter (frameInterruptFlag)) import Nes.APU.State.LengthCounter import Nes.APU.Tick (setFrameInterruptFlag) -import Nes.Bus.SideEffect -import Nes.FlagRegister (getFlag) +import Nes.Interrupt import Nes.Memory {-# INLINE write4015 #-} @@ -47,7 +46,7 @@ read4015 = do pulse2Bit <- withAPUState $ lengthCounterBit . pulse2 dmcBit <- withAPUState $ \st -> sampleBytesRemaining (dmc st) > 0 frameInterruptBit <- withAPUState $ frameInterruptFlag . frameCounter - dmcInterruptBit <- withSideEffect $ getFlag DMCDMA + dmcInterruptBit <- withInterruptStatus $ elem (IRQ DMC) . pendingInterrupts -- TODO Clearing flag should be done on every GET cycle -- https://github.com/100thCoin/AccuracyCoin/blob/a7bf0cfaee7dee9e7bfbd0e30435b85cb539139e/AccuracyCoin.asm#L9003 when frameInterruptBit $ do diff --git a/src/Nes/APU/Monad.hs b/src/Nes/APU/Monad.hs index d870f8d..4d18305 100644 --- a/src/Nes/APU/Monad.hs +++ b/src/Nes/APU/Monad.hs @@ -2,70 +2,70 @@ module Nes.APU.Monad ( APU (..), runAPU, modifyAPUState, - modifyAPUStateWithSideEffect, + modifyAPUStateWithInterrupt, withAPUState, modifyFilterChain, - setSideEffect, - withSideEffect, + modifyInterruptStatus, + withInterruptStatus, ) where import Control.Monad.IO.Class import Nes.APU.State import Nes.APU.State.Filter.Chain (FilterChain) -import Nes.Bus.SideEffect +import Nes.Interrupt newtype APU r a = MkAPU - { unAPU :: APUState -> CPUSideEffect -> (APUState -> CPUSideEffect -> a -> IO r) -> IO r + { unAPU :: APUState -> InterruptStatus -> (APUState -> InterruptStatus -> a -> IO r) -> IO r } deriving (Functor) instance Applicative (APU r) where {-# INLINE pure #-} - pure a = MkAPU $ \(!st) (!cpuEff) cont -> cont st cpuEff a + pure a = MkAPU $ \(!st) (!interr) cont -> cont st interr a {-# INLINE liftA2 #-} - liftA2 f (MkAPU a) (MkAPU b) = MkAPU $ \(!st) (!cpuEff) cont -> - a st cpuEff $ \(!st') (!cpuEff') !a' -> b st' cpuEff' $ \(!st'') (!cpuEff'') !b' -> cont st'' cpuEff'' (f a' b') + liftA2 f (MkAPU a) (MkAPU b) = MkAPU $ \(!st) (!interr) cont -> + a st interr $ \(!st') (!interr') !a' -> b st' interr' $ \(!st'') (!interr'') !b' -> cont st'' interr'' (f a' b') instance Monad (APU r) where {-# INLINE (>>=) #-} - (MkAPU a) >>= next = MkAPU $ \st cpuEff cont -> - a st cpuEff $ \(!st') (!cpuEff') (!a') -> unAPU (next a') st' cpuEff' cont + (MkAPU a) >>= next = MkAPU $ \st interr cont -> + a st interr $ \(!st') (!interr') (!a') -> unAPU (next a') st' interr' cont instance MonadIO (APU r) where {-# INLINE liftIO #-} - liftIO io = MkAPU $ \st cpuEff cont -> io >>= cont st cpuEff + liftIO io = MkAPU $ \st interr cont -> io >>= cont st interr instance MonadFail (APU r) where {-# INLINE fail #-} fail = liftIO . fail {-# INLINE runAPU #-} -runAPU :: APUState -> APU (a, APUState, CPUSideEffect) a -> IO (a, APUState, CPUSideEffect) -runAPU !st f = unAPU f st mempty $ \(!st') (!cpuEff) a -> return (a, st', cpuEff) +runAPU :: APUState -> InterruptStatus -> APU (a, APUState, InterruptStatus) a -> IO (a, APUState, InterruptStatus) +runAPU !st !s f = unAPU f st s $ \(!st') (!interr) a -> return (a, st', interr) {-# INLINE modifyAPUState #-} modifyAPUState :: (APUState -> APUState) -> APU r () -modifyAPUState f = MkAPU $ \(!st) (!cpuEff) cont -> cont (f st) cpuEff () +modifyAPUState f = MkAPU $ \(!st) (!interr) cont -> cont (f st) interr () -{-# INLINE modifyAPUStateWithSideEffect #-} -modifyAPUStateWithSideEffect :: (APUState -> (APUState, CPUSideEffect)) -> APU r () -modifyAPUStateWithSideEffect f = MkAPU $ \(!st) !cpuEff cont -> - let (st', sideEff) = f st in cont st' (cpuEff <> sideEff) () +{-# INLINE modifyAPUStateWithInterrupt #-} +modifyAPUStateWithInterrupt :: (APUState -> InterruptStatus -> (APUState, InterruptStatus)) -> APU r () +modifyAPUStateWithInterrupt f = MkAPU $ \(!st) !interr cont -> + let (st', interr') = f st interr in cont st' interr' () {-# INLINE withAPUState #-} withAPUState :: (APUState -> a) -> APU r a -withAPUState f = MkAPU $ \(!st) !cpuEff cont -> cont st cpuEff (f st) +withAPUState f = MkAPU $ \(!st) !interr cont -> cont st interr (f st) {-# INLINE modifyFilterChain #-} modifyFilterChain :: (FilterChain -> FilterChain) -> APU r () -modifyFilterChain f = MkAPU $ \(!st) !cpuEff cont -> - cont st{filterChain = f $ filterChain st} cpuEff () +modifyFilterChain f = MkAPU $ \(!st) !interr cont -> + cont st{filterChain = f $ filterChain st} interr () -{-# INLINE setSideEffect #-} -setSideEffect :: (CPUSideEffect -> CPUSideEffect) -> APU r () -setSideEffect f = MkAPU $ \(!st) !cpuEff cont -> cont st (f cpuEff) () +{-# INLINE modifyInterruptStatus #-} +modifyInterruptStatus :: (InterruptStatus -> InterruptStatus) -> APU r () +modifyInterruptStatus f = MkAPU $ \(!st) !interrupt cont -> cont st (f interrupt) () -{-# INLINE withSideEffect #-} -withSideEffect :: (CPUSideEffect -> a) -> APU r a -withSideEffect f = MkAPU $ \(!st) !cpuEff cont -> cont st cpuEff (f cpuEff) +{-# INLINE withInterruptStatus #-} +withInterruptStatus :: (InterruptStatus -> a) -> APU r a +withInterruptStatus f = MkAPU $ \(!st) !interrupt cont -> cont st interrupt (f interrupt) diff --git a/src/Nes/APU/State.hs b/src/Nes/APU/State.hs index 4f605ff..1d74f24 100644 --- a/src/Nes/APU/State.hs +++ b/src/Nes/APU/State.hs @@ -22,7 +22,7 @@ import Nes.APU.State.FrameCounter import Nes.APU.State.Noise import Nes.APU.State.Pulse import Nes.APU.State.Triangle -import Nes.Bus.SideEffect (CPUSideEffect) +import Nes.Interrupt (InterruptStatus) import Prelude hiding (cycle) data APUState = MkAPUState @@ -77,8 +77,8 @@ modifyDMC :: (DMC -> DMC) -> APUState -> APUState modifyDMC f st = let dmc' = f $ dmc st in st{dmc = dmc'} {-# INLINE modifyDMC' #-} -modifyDMC' :: (DMC -> (DMC, CPUSideEffect)) -> APUState -> (APUState, CPUSideEffect) -modifyDMC' f st = let (dmc', sideEff) = f $ dmc st in (st{dmc = dmc'}, sideEff) +modifyDMC' :: (DMC -> InterruptStatus -> (DMC, InterruptStatus)) -> APUState -> InterruptStatus -> (APUState, InterruptStatus) +modifyDMC' f st s = let (dmc', s') = f (dmc st) s in (st{dmc = dmc'}, s') {-# INLINE modifyFrameCounter #-} modifyFrameCounter :: (FrameCounter -> FrameCounter) -> APUState -> APUState diff --git a/src/Nes/APU/State/DMC.hs b/src/Nes/APU/State/DMC.hs index 42399ea..01d6a86 100644 --- a/src/Nes/APU/State/DMC.hs +++ b/src/Nes/APU/State/DMC.hs @@ -18,8 +18,7 @@ import Data.Array import Data.Bits import Data.List ((!?)) import Data.Maybe (fromMaybe, isNothing) -import Nes.Bus.SideEffect -import Nes.FlagRegister +import Nes.Interrupt import Nes.Memory data DMC = MkDMC @@ -80,9 +79,9 @@ restartSample dmc = getDMCOutput :: DMC -> Int getDMCOutput dmc = if silentFlag dmc then 0 else outputLevel dmc -tickDMC :: DMC -> (DMC, CPUSideEffect) -tickDMC dmc = - (if clocks then tickOutputUnit else (,mempty)) +tickDMC :: DMC -> InterruptStatus -> (DMC, InterruptStatus) +tickDMC dmc s = + (if clocks then (`tickOutputUnit` s) else (,s)) dmc { timer = newTimer , outputLevel = newOutputLevel @@ -100,25 +99,31 @@ tickDMC dmc = in if (0, 127) `inRange` tmpOutLevel then tmpOutLevel else outputLevel dmc else outputLevel dmc -tickOutputUnit :: DMC -> (DMC, CPUSideEffect) -tickOutputUnit dmc = if isEndOfOutputCycle then onOutputCycleEnd dmc1 else (dmc1, mempty) +tickOutputUnit :: DMC -> InterruptStatus -> (DMC, InterruptStatus) +tickOutputUnit dmc s = + if isEndOfOutputCycle + then onOutputCycleEnd dmc1 s + else (dmc1, s) where newRemainingBits = max 0 (remainingBits dmc - 1) isEndOfOutputCycle = newRemainingBits == 0 dmc1 = dmc{remainingBits = newRemainingBits} -onOutputCycleEnd :: DMC -> (DMC, CPUSideEffect) -onOutputCycleEnd dmc = (dmc1, sideEffect) +onOutputCycleEnd :: DMC -> InterruptStatus -> (DMC, InterruptStatus) +onOutputCycleEnd dmc interr = (dmc1, interr') where dmc0 = dmc{remainingBits = 8} dmc1 = case sampleBuffer dmc0 of Nothing -> dmc0{silentFlag = True} Just b -> dmc0{shiftRegister = b, sampleBuffer = Nothing} - sideEffect = setFlag' DMCDMA (isNothing (sampleBuffer dmc1) && sampleBytesRemaining dmc1 > 0) mempty + interr' = + if isNothing (sampleBuffer dmc1) && sampleBytesRemaining dmc1 > 0 + then pushInterrupt (IRQ DMC) interr + else interr -- | Loads the byte into the sample buffer and shift the sample buffer-related values -loadSampleBuffer :: Byte -> DMC -> (DMC, CPUSideEffect) -loadSampleBuffer byte dmc = +loadSampleBuffer :: Byte -> DMC -> InterruptStatus -> (DMC, InterruptStatus) +loadSampleBuffer byte dmc s = let newSampleBufferAddr = let addr = sampleBufferAddr dmc + 1 in if addr >= 0xffff then addr - 0x8000 else addr newRemainingLength = max 0 (sampleBytesRemaining dmc - 1) @@ -133,5 +138,5 @@ loadSampleBuffer byte dmc = shouldIRQ = newRemainingLength == 0 && interruptFlag dmc in if shouldRestartSample - then (restartSample dmc1, mempty) - else (dmc1, setFlag' IRQ shouldIRQ mempty) + then (restartSample dmc1, s) + else (dmc1, if shouldIRQ then pushInterrupt (IRQ DMC) s else s) diff --git a/src/Nes/APU/Tick.hs b/src/Nes/APU/Tick.hs index 360363d..8f916b5 100644 --- a/src/Nes/APU/Tick.hs +++ b/src/Nes/APU/Tick.hs @@ -27,8 +27,7 @@ import Nes.APU.State.LengthCounter import Nes.APU.State.Noise import Nes.APU.State.Pulse import Nes.APU.State.Triangle -import Nes.Bus.SideEffect -import Nes.FlagRegister +import Nes.Interrupt import Prelude hiding (cycle) -- $semantic @@ -50,7 +49,7 @@ tickOnce :: IsAPUCycle -> APU r () tickOnce isAPUCycle = do -- Ticks tickDelayedWriteBuffer - modifyAPUStateWithSideEffect $ modifyDMC' tickDMC + modifyAPUStateWithInterrupt $ modifyDMC' tickDMC modifyAPUState $ modifyTriangle tickTriangle . modifyNoise tickNoise @@ -150,7 +149,7 @@ runHalfFrameEvent = modifyAPUState $ \st -> {-# INLINE setFrameInterruptFlag #-} setFrameInterruptFlag :: Bool -> APU r () setFrameInterruptFlag b = do - setSideEffect $ setFlag IRQ + modifyInterruptStatus $ pushInterrupt (IRQ FrameCounter) modifyAPUState $ modifyFrameCounter $ \fc -> fc{frameInterruptFlag = b} diff --git a/src/Nes/Bus.hs b/src/Nes/Bus.hs index 433bf49..38dff16 100644 --- a/src/Nes/Bus.hs +++ b/src/Nes/Bus.hs @@ -9,7 +9,8 @@ module Nes.Bus ( Bus (..), newBus, modifyPPUState, - modifyCPUInterrupt, + modifyInterruptStatus, + modifyInterruptStatus', ) where import Nes.APU.State (APUState, newAPUState) @@ -76,5 +77,8 @@ newBus rom_ onNewFrame_ pushSample_ tickCallback_ = do modifyPPUState :: (PPUState -> PPUState) -> Bus -> Bus modifyPPUState f bus = bus{ppuState = f $ ppuState bus} -modifyCPUInterrupt :: (InterruptStatus -> InterruptStatus) -> Bus -> Bus -modifyCPUInterrupt f b = b{cpuInterrupt = f (cpuInterrupt b)} +modifyInterruptStatus :: (InterruptStatus -> InterruptStatus) -> Bus -> Bus +modifyInterruptStatus f b = b{cpuInterrupt = f (cpuInterrupt b)} + +modifyInterruptStatus' :: (InterruptStatus -> (a, InterruptStatus)) -> Bus -> (a, Bus) +modifyInterruptStatus' f b = let (res, interr) = f (cpuInterrupt b) in (res, b{cpuInterrupt = interr}) diff --git a/src/Nes/Bus/Monad.hs b/src/Nes/Bus/Monad.hs index ad55d31..d77c12d 100644 --- a/src/Nes/Bus/Monad.hs +++ b/src/Nes/Bus/Monad.hs @@ -16,9 +16,9 @@ import Nes.APU.State import qualified Nes.APU.Tick as APU import Nes.Bus import Nes.Bus.Constants -import Nes.Bus.SideEffect (CPUSideEffect) import Nes.Controller import Nes.FlagRegister (clearFlag) +import Nes.Interrupt (InterruptStatus) import Nes.Memory import Nes.PPU.Constants (oamDataSize) import Nes.PPU.Monad hiding (modifyPPUState, tick) @@ -68,10 +68,10 @@ withPPU f = MkBusM $ \bus cont -> do cont (bus{ppuState = ppuSt}) res {-# INLINE withAPU #-} -withAPU :: APU (a, APUState, CPUSideEffect) a -> BusM r a +withAPU :: APU (a, APUState, InterruptStatus) a -> BusM r a withAPU f = MkBusM $ \bus cont -> do - (!res, !apuSt, !cpuEff) <- runAPU (apuState bus) f - cont (bus{apuState = apuSt, cpuSideEffect = cpuEff}) res + (!res, !apuSt, !interr) <- runAPU (apuState bus) (cpuInterrupt bus) f + cont (bus{apuState = apuSt, cpuInterrupt = interr}) res {-# INLINE withController #-} withController :: ControllerM (a, Controller) a -> BusM r a @@ -91,7 +91,7 @@ tick n = MkBusM $ \bus cont -> do isNewFrame <- PPUM.tick (n * 3) after <- withPPUState nmiInterrupt return (isNewFrame, before, after) - ((), !apuSt, !cpuEff) <- runAPU (apuState bus) $ APU.tick (odd (Nes.Bus.cycles bus)) n + ((), !apuSt, !interr) <- runAPU (apuState bus) (cpuInterrupt bus) $ APU.tick (odd (Nes.Bus.cycles bus)) n let bus' = bus { unsleptCycles = newUnsleptCycles @@ -99,7 +99,7 @@ tick n = MkBusM $ \bus cont -> do , apuState = apuSt , cycles = fromIntegral n + cycles bus , lastSleepTime = newLastSleepTime - , cpuSideEffect = cpuEff + , cpuInterrupt = interr } if not nmiBefore && nmiAfter then onNewFrame bus' bus' >>= flip cont () diff --git a/src/Nes/Bus/SideEffect.hs b/src/Nes/Bus/SideEffect.hs index d737768..57888e5 100644 --- a/src/Nes/Bus/SideEffect.hs +++ b/src/Nes/Bus/SideEffect.hs @@ -4,6 +4,8 @@ import Data.Bits ((.|.)) import Nes.FlagRegister import Nes.Memory +-- TODO Delete me + newtype CPUSideEffect = MkSE {unSE :: Byte} data CPUSideEffectFlag = IRQ | DMCDMA deriving (Eq, Show, Enum) diff --git a/src/Nes/CPU/Instructions/Interrupt.hs b/src/Nes/CPU/Instructions/Interrupt.hs index ea650db..211bf4a 100644 --- a/src/Nes/CPU/Instructions/Interrupt.hs +++ b/src/Nes/CPU/Instructions/Interrupt.hs @@ -6,4 +6,4 @@ import Nes.Interrupt brk :: CPU r () brk = do incrementPC - pushInterrupt BRK + modifyInterruptStatus $ pushInterrupt $ IRQ BRK diff --git a/src/Nes/CPU/Instructions/Jump.hs b/src/Nes/CPU/Instructions/Jump.hs index 6380fb8..b33aee1 100644 --- a/src/Nes/CPU/Instructions/Jump.hs +++ b/src/Nes/CPU/Instructions/Jump.hs @@ -11,8 +11,6 @@ module Nes.CPU.Instructions.Jump ( import Data.Bits import Nes.CPU.Instructions.Addressing import Nes.CPU.Monad -import Nes.CPU.State -import Nes.FlagRegister import Nes.Memory -- | Sets the program counter to the address specified by the operand @@ -62,14 +60,6 @@ rts = do -- https://www.nesdev.org/obelisk-6502-guide/reference.html#RTI rti :: CPU r () rti = do - -- Note: When one opcode does multiple stack pops, the max - newStatus <- popStackByte - modifyCPUState (\st -> st{status = MkSR newStatus}) - modifyCPUState $ - modifyStatusRegister $ - clearFlag BreakCommand - . setFlag BreakCommand2 + popStatusRegister setPC =<< popStackAddr tick 2 - --- Note: Source for both: https://github.com/bugzmanov/nes_ebook/blob/785b9ed8b803d9f4bd51274f4d0c68c14a1b3a8b/code/ch3.3/src/cpu.rs#L703 diff --git a/src/Nes/CPU/Instructions/Stack.hs b/src/Nes/CPU/Instructions/Stack.hs index 0c6a09a..7739acf 100644 --- a/src/Nes/CPU/Instructions/Stack.hs +++ b/src/Nes/CPU/Instructions/Stack.hs @@ -3,7 +3,6 @@ module Nes.CPU.Instructions.Stack (pha, php, pla, plp) where import Nes.CPU.Instructions.After import Nes.CPU.Monad import Nes.CPU.State -import Nes.FlagRegister -- | Pushes a copy of the accumulator on to the stack. -- @@ -17,15 +16,7 @@ pha = tickOnce >> withCPUState (getRegister A) >>= pushByteStack -- -- Source: https://github.com/bugzmanov/nes_ebook/blob/785b9ed8b803d9f4bd51274f4d0c68c14a1b3a8b/code/ch3.3/src/cpu.rs#L486 php :: CPU r () -php = do - st <- - withCPUState - ( setFlag BreakCommand2 - . setFlag BreakCommand - . status - ) - pushByteStack $ unSR st - tickOnce +php = pushStatusRegister True >> tickOnce -- | Pulls an 8 bit value from the stack and into the accumulator. -- @@ -40,14 +31,5 @@ pla = do -- | Pulls an 8 bit value from the stack and into the accumulator. -- -- https://www.nesdev.org/obelisk-6502-guide/reference.html#PLP --- --- Source: https://github.com/bugzmanov/nes_ebook/blob/785b9ed8b803d9f4bd51274f4d0c68c14a1b3a8b/code/ch3.3/src/cpu.rs#L478 plp :: CPU r () -plp = do - value <- popStackByte - tick 2 - modifyCPUState $ \st -> st{status = MkSR value} - modifyCPUState $ - modifyStatusRegister $ - clearFlag BreakCommand - . setFlag BreakCommand2 +plp = popStatusRegister >> tick 2 diff --git a/src/Nes/CPU/Interpreter.hs b/src/Nes/CPU/Interpreter.hs index d89d3af..590176a 100644 --- a/src/Nes/CPU/Interpreter.hs +++ b/src/Nes/CPU/Interpreter.hs @@ -46,7 +46,7 @@ interpretWithCallback callback = do modifyPPUState $ \st -> st{nmiInterrupt = False} return f ) - when hasNmiInterrupt $ pushInterrupt NMI + when hasNmiInterrupt $ modifyInterruptStatus $ pushInterrupt NMI callback oldCycleCount <- getCycles opCode <- readAtPC diff --git a/src/Nes/CPU/Interrupt.hs b/src/Nes/CPU/Interrupt.hs index 380c2e0..1128680 100644 --- a/src/Nes/CPU/Interrupt.hs +++ b/src/Nes/CPU/Interrupt.hs @@ -1,10 +1,9 @@ module Nes.CPU.Interrupt (handleInterrupt) where -import Data.Bits import Nes.CPU.Monad import Nes.CPU.State import Nes.FlagRegister -import Nes.Interrupt +import Nes.Interrupt (IRQSource (..), Interrupt (..), getVectorAddr, pushesBFlag) import Nes.Memory handleInterrupt :: CPU r () @@ -14,18 +13,12 @@ handleInterrupt = do case pendingInterrupt of Nothing -> pure () Just signal - | maskInterrupt && signal /= NMI -> pure () + | maskInterrupt && signal /= NMI && signal /= IRQ BRK -> pure () | otherwise -> handleInterruptSignal signal handleInterruptSignal :: Interrupt -> CPU r () handleInterruptSignal signal = do pushAddrStack =<< getPC - let mask = getFlagMask signal - flag <- - withCPUState $ - setFlag' BreakCommand (testBit mask 4) - . setFlag' BreakCommand2 (testBit mask 5) - . status - pushByteStack $ unSR flag + pushStatusRegister (pushesBFlag signal) modifyCPUState $ modifyStatusRegister $ setFlag InterruptDisable setPC =<< readAddr (getVectorAddr signal) () diff --git a/src/Nes/CPU/Monad.hs b/src/Nes/CPU/Monad.hs index 419f656..45ade8b 100644 --- a/src/Nes/CPU/Monad.hs +++ b/src/Nes/CPU/Monad.hs @@ -30,8 +30,12 @@ module Nes.CPU.Monad ( pushAddrStack, pushByteStack, + -- * Status register + popStatusRegister, + pushStatusRegister, + -- * Interrupt - pushInterrupt, + modifyInterruptStatus, popInterrupt, -- * Unsafe @@ -41,18 +45,19 @@ module Nes.CPU.Monad ( import Control.Monad import Control.Monad.IO.Class import Data.Bits (Bits (shiftR)) -import Data.Maybe (listToMaybe) -import Nes.APU.Monad (modifyAPUStateWithSideEffect) +import Nes.APU.Monad (modifyAPUStateWithInterrupt) import Nes.APU.State (APUState (dmc), modifyDMC') import Nes.APU.State.DMC (DMC (sampleBufferAddr), loadSampleBuffer) -import Nes.Bus (Bus (..), modifyCPUInterrupt) +import Nes.Bus (Bus (..)) +import qualified Nes.Bus import Nes.Bus.Constants import Nes.Bus.Monad (BusM, modifyBus, runBusM) import qualified Nes.Bus.Monad as BusM import Nes.Bus.SideEffect import Nes.CPU.State import Nes.FlagRegister -import Nes.Interrupt +import Nes.Interrupt (Interrupt, InterruptStatus) +import qualified Nes.Interrupt as I import Nes.Memory -- | Note: we use IO because it is likely to read/write from/to memory, which is not pure @@ -138,6 +143,21 @@ pushByteStack byte = do writeByte byte (stackAddr + byteToAddr regS) () modifyCPUState $ setRegister S (regS - 1) +-- | If the argument is True, the pushed value will have the B Flag set +pushStatusRegister :: Bool -> CPU r () +pushStatusRegister b = do + s <- withCPUState status + let value = unSR $ setFlag Unusued $ setFlag' BFlag b s + pushByteStack value + +-- | Pops value on the stack, clear BFlag and sets the results value as status register +popStatusRegister :: CPU r () +popStatusRegister = do + value <- fromByte <$> popStackByte + -- TODO Breaks Nestest + let s = clearFlag Unusued $ clearFlag BFlag value + modifyCPUState $ modifyStatusRegister $ const s + {-# INLINE pushAddrStack #-} pushAddrStack :: Addr -> CPU r () pushAddrStack addr = do @@ -172,14 +192,13 @@ reset = do pc <- readAddr 0xfffc () modifyCPUState (const $ newCPUState{programCounter = pc}) -pushInterrupt :: Interrupt -> CPU r () -pushInterrupt i = withBus (modifyBus $ modifyCPUInterrupt $ modifyPendingInterrupt (++ [i])) +modifyInterruptStatus :: (InterruptStatus -> InterruptStatus) -> CPU r () +modifyInterruptStatus = withBus . modifyBus . Nes.Bus.modifyInterruptStatus popInterrupt :: CPU r (Maybe Interrupt) -popInterrupt = withBus $ do - pendingHead <- BusM.withBus $ take 1 . pendingInterrupts . cpuInterrupt - modifyBus $ modifyCPUInterrupt $ modifyPendingInterrupt $ drop 1 - return $ listToMaybe pendingHead +popInterrupt = MkCPU $ \st bus cont -> do + let (res, bus') = Nes.Bus.modifyInterruptStatus' I.popInterrupt bus + cont st bus' res instance MemoryInterface () (CPU r) where {-# INLINE readByte #-} @@ -219,5 +238,5 @@ handleSideEffect = do when hasDMCDMA $ withBus $ do sampleByteAddr <- BusM.withBus $ sampleBufferAddr . dmc . apuState sample <- Nes.Memory.readByte sampleByteAddr () - BusM.withAPU $ modifyAPUStateWithSideEffect $ modifyDMC' $ loadSampleBuffer sample + BusM.withAPU $ modifyAPUStateWithInterrupt $ modifyDMC' $ loadSampleBuffer sample BusM.modifyBus $ \b -> b{cpuSideEffect = clearFlag DMCDMA (cpuSideEffect b)} diff --git a/src/Nes/CPU/State.hs b/src/Nes/CPU/State.hs index ee5a621..e9a6b2b 100644 --- a/src/Nes/CPU/State.hs +++ b/src/Nes/CPU/State.hs @@ -78,8 +78,8 @@ data StatusRegisterFlag | Zero | InterruptDisable | DecimalMode - | BreakCommand - | BreakCommand2 + | BFlag + | Unusued | Overflow | Negative deriving (Eq, Show, Enum) diff --git a/src/Nes/Interrupt.hs b/src/Nes/Interrupt.hs index d94ee38..0851c57 100644 --- a/src/Nes/Interrupt.hs +++ b/src/Nes/Interrupt.hs @@ -3,30 +3,32 @@ module Nes.Interrupt ( Interrupt (..), IRQSource (..), getVectorAddr, - getFlagMask, + pushesBFlag, -- * Status InterruptStatus (..), modifyPendingInterrupt, + popInterrupt, + pushInterrupt, ) where +import Data.Maybe (listToMaybe) import Nes.Memory -data Interrupt = NMI | BRK | IRQ IRQSource deriving (Eq, Show) +data Interrupt = NMI | IRQ IRQSource deriving (Eq, Show) -data IRQSource = DMA | FrameCounter deriving (Eq, Show) +data IRQSource = BRK | DMC | FrameCounter deriving (Eq, Show) getVectorAddr :: Interrupt -> Addr getVectorAddr = \case NMI -> 0xfffa - BRK -> 0xfffe IRQ _ -> 0xfffe -getFlagMask :: Interrupt -> Byte -getFlagMask = \case - NMI -> 0b00100000 - IRQ _ -> 0b00100000 - BRK -> 0b00110000 +pushesBFlag :: Interrupt -> Bool +pushesBFlag = \case + NMI -> False + IRQ BRK -> True + IRQ _ -> False data InterruptStatus = MkIE { pendingInterrupts :: {-# UNPACK #-} ![Interrupt] @@ -35,3 +37,14 @@ data InterruptStatus = MkIE modifyPendingInterrupt :: ([Interrupt] -> [Interrupt]) -> InterruptStatus -> InterruptStatus modifyPendingInterrupt f s = s{pendingInterrupts = f $ pendingInterrupts s} + +pushInterrupt :: Interrupt -> InterruptStatus -> InterruptStatus +pushInterrupt i = modifyPendingInterrupt (++ [i]) + +popInterrupt :: InterruptStatus -> (Maybe Interrupt, InterruptStatus) +popInterrupt st = + let + pendingHead = take 1 $ pendingInterrupts st + st1 = modifyPendingInterrupt (drop 1) st + in + (listToMaybe pendingHead, st1) diff --git a/test/nestest/Spec.hs b/test/nestest/Spec.hs index 821fba2..035f2e6 100644 --- a/test/nestest/Spec.hs +++ b/test/nestest/Spec.hs @@ -10,6 +10,7 @@ import Data.ByteString (ByteString) import qualified Data.ByteString as BS import Data.ByteString.Char8 (pack) import qualified Data.ByteString.Char8 as BSC +import Data.Char (isAlphaNum) import Data.IORef (IORef, modifyIORef, newIORef, readIORef) import Data.Int import qualified Data.Map as Map @@ -38,12 +39,12 @@ spec = it "Trace should match logfile" $ do rom <- do eitherRom <- fromFile "test/assets/rom.nes" either fail return eitherRom - bus <- newBus rom pure (\_ -> pure ()) (\a b -> return (a, b)) + bus <- newBus rom pure (\_ -> pure ()) (curry return) traceRef <- newIORef (T [] 0) let st = newCPUState{programCounter = 0xc000} -- TODO why is the tick count set to 7 ? Reset? _ <- try @IOException $ runProgram' st (bus{cycles = 7}) (trace traceRef) - actualTrace <- beforeUnhandledOpcode . toRawTrace <$> readIORef traceRef + actualTrace <- fmap fixStackValue . beforeUnhandledOpcode . toRawTrace <$> readIORef traceRef -- BS.writeFile "actual.log" $ BSC.unlines actualTrace length actualTrace `shouldBe` length expectedTrace forM_ [0 .. length expectedTrace - 1] $ \i -> do @@ -176,7 +177,7 @@ beforeUnhandledOpcode = loadExpectedRawTrace :: IO RawTrace loadExpectedRawTrace = do fileContent <- BS.readFile "test/assets/rom_trace.log" - let rawTrace = BSC.lines fileContent + let rawTrace = fixStackValue <$> BSC.lines fileContent -- TODO When project is finished, we shouldn't have to do the following filters return $ withoutPPUCycles <$> beforeUnhandledOpcode rawTrace where @@ -185,6 +186,21 @@ loadExpectedRawTrace = do afterPPU = snd . BS.breakSubstring " CYC:" $ bs in BS.concat [beforePPU, afterPPU] +-- | In nestest, the B flag and bit 5 are preserved when popped from the stack +-- In the Accuracy coin tests, that's not the case. Thus, we modify nestest's output +-- so that P = P & 0b11001111 +fixStackValue :: ByteString -> ByteString +fixStackValue bs = + let + (prefix, bs') = BS.breakSubstring " P:" bs + stringP = drop 3 $ BSC.unpack bs' + p = read ("0x" ++ takeWhile isAlphaNum stringP) :: Int + fixedP = p .&. 0b11001111 + in + if p == fixedP + then bs + else BS.concat [prefix, " P:", BSC.pack $ printf "%02X" fixedP, BS.drop 5 bs'] + withoutTick :: CPU r a -> CPU r a withoutTick (MkCPU f) = MkCPU $ \st bus cont -> do - f st bus{cycleCallback = \a b -> pure (a, b)} $ \st' _ -> cont st' bus + f st bus{cycleCallback = curry pure} $ \st' _ -> cont st' bus From 3f864575e789fcd26dea83ce0e4f4b2e7d1f36b6 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 18 Nov 2025 15:02:28 +0000 Subject: [PATCH 10/11] Restore DMC DMA --- src/Nes/CPU/Interrupt.hs | 11 +++++++++++ src/Nes/CPU/Monad.hs | 23 +++-------------------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/Nes/CPU/Interrupt.hs b/src/Nes/CPU/Interrupt.hs index 1128680..e60a6e4 100644 --- a/src/Nes/CPU/Interrupt.hs +++ b/src/Nes/CPU/Interrupt.hs @@ -1,5 +1,11 @@ module Nes.CPU.Interrupt (handleInterrupt) where +import Control.Monad +import Nes.APU.Monad (modifyAPUStateWithInterrupt) +import Nes.APU.State (APUState (dmc), modifyDMC') +import Nes.APU.State.DMC (DMC (sampleBufferAddr), loadSampleBuffer) +import Nes.Bus (Bus (apuState)) +import qualified Nes.Bus.Monad as BusM import Nes.CPU.Monad import Nes.CPU.State import Nes.FlagRegister @@ -22,3 +28,8 @@ handleInterruptSignal signal = do pushStatusRegister (pushesBFlag signal) modifyCPUState $ modifyStatusRegister $ setFlag InterruptDisable setPC =<< readAddr (getVectorAddr signal) () + -- Ugly, shouldn't be here + when (signal == IRQ DMC) $ withBus $ do + sampleByteAddr <- BusM.withBus $ sampleBufferAddr . dmc . apuState + sample <- Nes.Memory.readByte sampleByteAddr () + BusM.withAPU $ modifyAPUStateWithInterrupt $ modifyDMC' $ loadSampleBuffer sample diff --git a/src/Nes/CPU/Monad.hs b/src/Nes/CPU/Monad.hs index 45ade8b..b80859e 100644 --- a/src/Nes/CPU/Monad.hs +++ b/src/Nes/CPU/Monad.hs @@ -42,12 +42,8 @@ module Nes.CPU.Monad ( unsafeWithBus, ) where -import Control.Monad import Control.Monad.IO.Class import Data.Bits (Bits (shiftR)) -import Nes.APU.Monad (modifyAPUStateWithInterrupt) -import Nes.APU.State (APUState (dmc), modifyDMC') -import Nes.APU.State.DMC (DMC (sampleBufferAddr), loadSampleBuffer) import Nes.Bus (Bus (..)) import qualified Nes.Bus import Nes.Bus.Constants @@ -168,12 +164,9 @@ pushAddrStack addr = do {-# INLINE withBus #-} withBus :: BusM (a, Bus) a -> CPU r a -withBus f = do - res <- MkCPU $ \st bus cont -> do - (res, bus') <- runBusM bus f - cont st bus' res - handleSideEffect - return res +withBus f = MkCPU $ \st bus cont -> do + (res, bus') <- runBusM bus f + cont st bus' res -- | Unsafe action that provides access to Bus -- @@ -230,13 +223,3 @@ tick = withBus . BusM.tick {-# INLINE tickOnce #-} tickOnce :: CPU r () tickOnce = Nes.CPU.Monad.tick 1 - --- TODO Delete me -handleSideEffect :: CPU r () -handleSideEffect = do - hasDMCDMA <- withBusState $ getFlag DMCDMA . cpuSideEffect - when hasDMCDMA $ withBus $ do - sampleByteAddr <- BusM.withBus $ sampleBufferAddr . dmc . apuState - sample <- Nes.Memory.readByte sampleByteAddr () - BusM.withAPU $ modifyAPUStateWithInterrupt $ modifyDMC' $ loadSampleBuffer sample - BusM.modifyBus $ \b -> b{cpuSideEffect = clearFlag DMCDMA (cpuSideEffect b)} From 3df21160688620ced505d110ccd32ce3d4803422 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Tue, 18 Nov 2025 17:29:03 +0000 Subject: [PATCH 11/11] Make interrupt status more sensible --- src/Nes/APU/BusInterface/Status.hs | 2 +- src/Nes/APU/State/DMC.hs | 4 +-- src/Nes/APU/Tick.hs | 2 +- src/Nes/Bus.hs | 2 +- src/Nes/CPU/Instructions/Interrupt.hs | 2 +- src/Nes/CPU/Interpreter.hs | 2 +- src/Nes/CPU/Interrupt.hs | 49 +++++++++++++++++++-------- src/Nes/CPU/Monad.hs | 9 +---- src/Nes/Interrupt.hs | 46 +++---------------------- test/nestest/Spec.hs | 2 +- 10 files changed, 48 insertions(+), 72 deletions(-) diff --git a/src/Nes/APU/BusInterface/Status.hs b/src/Nes/APU/BusInterface/Status.hs index 9cc0b9b..e9d1de4 100644 --- a/src/Nes/APU/BusInterface/Status.hs +++ b/src/Nes/APU/BusInterface/Status.hs @@ -46,7 +46,7 @@ read4015 = do pulse2Bit <- withAPUState $ lengthCounterBit . pulse2 dmcBit <- withAPUState $ \st -> sampleBytesRemaining (dmc st) > 0 frameInterruptBit <- withAPUState $ frameInterruptFlag . frameCounter - dmcInterruptBit <- withInterruptStatus $ elem (IRQ DMC) . pendingInterrupts + dmcInterruptBit <- withInterruptStatus $ (== Just DMC) . irq -- TODO Clearing flag should be done on every GET cycle -- https://github.com/100thCoin/AccuracyCoin/blob/a7bf0cfaee7dee9e7bfbd0e30435b85cb539139e/AccuracyCoin.asm#L9003 when frameInterruptBit $ do diff --git a/src/Nes/APU/State/DMC.hs b/src/Nes/APU/State/DMC.hs index 01d6a86..30ee964 100644 --- a/src/Nes/APU/State/DMC.hs +++ b/src/Nes/APU/State/DMC.hs @@ -118,7 +118,7 @@ onOutputCycleEnd dmc interr = (dmc1, interr') Just b -> dmc0{shiftRegister = b, sampleBuffer = Nothing} interr' = if isNothing (sampleBuffer dmc1) && sampleBytesRemaining dmc1 > 0 - then pushInterrupt (IRQ DMC) interr + then interr{irq = Just DMC} else interr -- | Loads the byte into the sample buffer and shift the sample buffer-related values @@ -139,4 +139,4 @@ loadSampleBuffer byte dmc s = in if shouldRestartSample then (restartSample dmc1, s) - else (dmc1, if shouldIRQ then pushInterrupt (IRQ DMC) s else s) + else (dmc1, if shouldIRQ then s{irq = Just DMC} else s) diff --git a/src/Nes/APU/Tick.hs b/src/Nes/APU/Tick.hs index 8f916b5..a0e37d8 100644 --- a/src/Nes/APU/Tick.hs +++ b/src/Nes/APU/Tick.hs @@ -149,7 +149,7 @@ runHalfFrameEvent = modifyAPUState $ \st -> {-# INLINE setFrameInterruptFlag #-} setFrameInterruptFlag :: Bool -> APU r () setFrameInterruptFlag b = do - modifyInterruptStatus $ pushInterrupt (IRQ FrameCounter) + modifyInterruptStatus $ \s -> s{irq = Just FrameCounter} modifyAPUState $ modifyFrameCounter $ \fc -> fc{frameInterruptFlag = b} diff --git a/src/Nes/Bus.hs b/src/Nes/Bus.hs index 38dff16..c32918e 100644 --- a/src/Nes/Bus.hs +++ b/src/Nes/Bus.hs @@ -72,7 +72,7 @@ newBus rom_ onNewFrame_ pushSample_ tickCallback_ = do 0 (newAPUState pushSample_) mempty - (MkIE []) + (MkIS Nothing False) modifyPPUState :: (PPUState -> PPUState) -> Bus -> Bus modifyPPUState f bus = bus{ppuState = f $ ppuState bus} diff --git a/src/Nes/CPU/Instructions/Interrupt.hs b/src/Nes/CPU/Instructions/Interrupt.hs index 211bf4a..91337b5 100644 --- a/src/Nes/CPU/Instructions/Interrupt.hs +++ b/src/Nes/CPU/Instructions/Interrupt.hs @@ -6,4 +6,4 @@ import Nes.Interrupt brk :: CPU r () brk = do incrementPC - modifyInterruptStatus $ pushInterrupt $ IRQ BRK + modifyInterruptStatus $ \s -> s{irq = Just BRK} diff --git a/src/Nes/CPU/Interpreter.hs b/src/Nes/CPU/Interpreter.hs index 590176a..fcae078 100644 --- a/src/Nes/CPU/Interpreter.hs +++ b/src/Nes/CPU/Interpreter.hs @@ -46,7 +46,7 @@ interpretWithCallback callback = do modifyPPUState $ \st -> st{nmiInterrupt = False} return f ) - when hasNmiInterrupt $ modifyInterruptStatus $ pushInterrupt NMI + when hasNmiInterrupt $ modifyInterruptStatus $ \s -> s{nmi = True} callback oldCycleCount <- getCycles opCode <- readAtPC diff --git a/src/Nes/CPU/Interrupt.hs b/src/Nes/CPU/Interrupt.hs index e60a6e4..5c0aec6 100644 --- a/src/Nes/CPU/Interrupt.hs +++ b/src/Nes/CPU/Interrupt.hs @@ -4,32 +4,53 @@ import Control.Monad import Nes.APU.Monad (modifyAPUStateWithInterrupt) import Nes.APU.State (APUState (dmc), modifyDMC') import Nes.APU.State.DMC (DMC (sampleBufferAddr), loadSampleBuffer) -import Nes.Bus (Bus (apuState)) +import Nes.Bus (Bus (apuState, cpuInterrupt)) import qualified Nes.Bus.Monad as BusM import Nes.CPU.Monad import Nes.CPU.State import Nes.FlagRegister -import Nes.Interrupt (IRQSource (..), Interrupt (..), getVectorAddr, pushesBFlag) +import Nes.Interrupt import Nes.Memory +data Signal = NMI | IRQ IRQSource deriving (Eq) + +{-# INLINE signalFromInterrupt #-} +signalFromInterrupt :: InterruptStatus -> Maybe Signal +signalFromInterrupt s + | nmi s = Just NMI + | otherwise = IRQ <$> irq s + +signalShouldPushBFlag :: Signal -> Bool +signalShouldPushBFlag = \case + NMI -> False + IRQ BRK -> True + IRQ _ -> False + +signalVectorAddr :: Signal -> Addr +signalVectorAddr = \case + NMI -> 0xfffa + IRQ _ -> 0xfffe + handleInterrupt :: CPU r () handleInterrupt = do maskInterrupt <- withCPUState $ getFlag InterruptDisable . status - pendingInterrupt <- popInterrupt - case pendingInterrupt of + pendingSignal <- signalFromInterrupt <$> withBusState cpuInterrupt + case pendingSignal of Nothing -> pure () Just signal | maskInterrupt && signal /= NMI && signal /= IRQ BRK -> pure () - | otherwise -> handleInterruptSignal signal - -handleInterruptSignal :: Interrupt -> CPU r () -handleInterruptSignal signal = do - pushAddrStack =<< getPC - pushStatusRegister (pushesBFlag signal) - modifyCPUState $ modifyStatusRegister $ setFlag InterruptDisable - setPC =<< readAddr (getVectorAddr signal) () - -- Ugly, shouldn't be here - when (signal == IRQ DMC) $ withBus $ do + | otherwise -> do + pushAddrStack =<< getPC + pushStatusRegister (signalShouldPushBFlag signal) + modifyCPUState $ modifyStatusRegister $ setFlag InterruptDisable + setPC =<< readAddr (signalVectorAddr signal) () + -- TODO Ugly, shouldn't be here + when (pendingSignal == Just (IRQ DMC)) $ withBus $ do sampleByteAddr <- BusM.withBus $ sampleBufferAddr . dmc . apuState sample <- Nes.Memory.readByte sampleByteAddr () BusM.withAPU $ modifyAPUStateWithInterrupt $ modifyDMC' $ loadSampleBuffer sample + -- Cleanup state + case pendingSignal of + Nothing -> return () + Just NMI -> modifyInterruptStatus $ \s -> s{nmi = False} + Just (IRQ _) -> modifyInterruptStatus $ \s -> s{irq = Nothing} diff --git a/src/Nes/CPU/Monad.hs b/src/Nes/CPU/Monad.hs index b80859e..2d48dd1 100644 --- a/src/Nes/CPU/Monad.hs +++ b/src/Nes/CPU/Monad.hs @@ -36,7 +36,6 @@ module Nes.CPU.Monad ( -- * Interrupt modifyInterruptStatus, - popInterrupt, -- * Unsafe unsafeWithBus, @@ -52,8 +51,7 @@ import qualified Nes.Bus.Monad as BusM import Nes.Bus.SideEffect import Nes.CPU.State import Nes.FlagRegister -import Nes.Interrupt (Interrupt, InterruptStatus) -import qualified Nes.Interrupt as I +import Nes.Interrupt import Nes.Memory -- | Note: we use IO because it is likely to read/write from/to memory, which is not pure @@ -188,11 +186,6 @@ reset = do modifyInterruptStatus :: (InterruptStatus -> InterruptStatus) -> CPU r () modifyInterruptStatus = withBus . modifyBus . Nes.Bus.modifyInterruptStatus -popInterrupt :: CPU r (Maybe Interrupt) -popInterrupt = MkCPU $ \st bus cont -> do - let (res, bus') = Nes.Bus.modifyInterruptStatus' I.popInterrupt bus - cont st bus' res - instance MemoryInterface () (CPU r) where {-# INLINE readByte #-} readByte n () = do diff --git a/src/Nes/Interrupt.hs b/src/Nes/Interrupt.hs index 0851c57..48d9540 100644 --- a/src/Nes/Interrupt.hs +++ b/src/Nes/Interrupt.hs @@ -1,50 +1,12 @@ module Nes.Interrupt ( -- * Interrupt Enum - Interrupt (..), - IRQSource (..), - getVectorAddr, - pushesBFlag, - - -- * Status InterruptStatus (..), - modifyPendingInterrupt, - popInterrupt, - pushInterrupt, + IRQSource (..), ) where -import Data.Maybe (listToMaybe) -import Nes.Memory - -data Interrupt = NMI | IRQ IRQSource deriving (Eq, Show) - data IRQSource = BRK | DMC | FrameCounter deriving (Eq, Show) -getVectorAddr :: Interrupt -> Addr -getVectorAddr = \case - NMI -> 0xfffa - IRQ _ -> 0xfffe - -pushesBFlag :: Interrupt -> Bool -pushesBFlag = \case - NMI -> False - IRQ BRK -> True - IRQ _ -> False - -data InterruptStatus = MkIE - { pendingInterrupts :: {-# UNPACK #-} ![Interrupt] - -- ^ Will be true if the CPU is executing the interrupt handler +data InterruptStatus = MkIS + { irq :: {-# UNPACK #-} !(Maybe IRQSource) + , nmi :: {-# UNPACK #-} !Bool } - -modifyPendingInterrupt :: ([Interrupt] -> [Interrupt]) -> InterruptStatus -> InterruptStatus -modifyPendingInterrupt f s = s{pendingInterrupts = f $ pendingInterrupts s} - -pushInterrupt :: Interrupt -> InterruptStatus -> InterruptStatus -pushInterrupt i = modifyPendingInterrupt (++ [i]) - -popInterrupt :: InterruptStatus -> (Maybe Interrupt, InterruptStatus) -popInterrupt st = - let - pendingHead = take 1 $ pendingInterrupts st - st1 = modifyPendingInterrupt (drop 1) st - in - (listToMaybe pendingHead, st1) diff --git a/test/nestest/Spec.hs b/test/nestest/Spec.hs index 035f2e6..a92bcec 100644 --- a/test/nestest/Spec.hs +++ b/test/nestest/Spec.hs @@ -186,7 +186,7 @@ loadExpectedRawTrace = do afterPPU = snd . BS.breakSubstring " CYC:" $ bs in BS.concat [beforePPU, afterPPU] --- | In nestest, the B flag and bit 5 are preserved when popped from the stack +-- In nestest, the B flag and bit 5 are preserved when popped from the stack -- In the Accuracy coin tests, that's not the case. Thus, we modify nestest's output -- so that P = P & 0b11001111 fixStackValue :: ByteString -> ByteString