CPU IRQ handling conflicts regarding MMC3 and MMC5

Discuss emulation of the Nintendo Entertainment System and Famicom.

Moderator: Moderators

Post Reply
User avatar
lukexor
Posts: 22
Joined: Sat Jun 08, 2019 2:53 am
Location: Portland, OR
Contact:

CPU IRQ handling conflicts regarding MMC3 and MMC5

Post by lukexor » Wed Sep 25, 2019 5:42 pm

Hey guys! been awhile since I posted about RustyNES. It's getting a lot better but I'm still struggling with some things that I discovered when implementing MMC5 that I can't quite figure out with regards to IRQs.

I spent way too many hours trying to find out why the nametable swapping wasn't working in Castlevania III when you get to the first vertical room (as referenced here viewtopic.php?f=3&t=17010&hilit=irq).

I actually got it working finally and don't suffer from the status bar jitter (though there is an extra black line when you walk through it so I'm sure I'm off by one scanline somewhere).

However, in order to fix it I had to change my CPU IRQ handling. What seems to happen with Castlevania is that it issues an IRQ on scanline 41 and then another on scanline 42 before the first IRQ has issued an RTI. Previously, my CPU was ignoring that second IRQ because the I flag was still set. That second IRQ is required to load the correct nametable swapping values into memory.

But now, my MMC3 games (notably Star Trek 25th Anniversary) don't work correctly (and they used to) because IRQs that used to be ignored are getting issued and so the screen jitters when it shouldn't.

Now - my question: How is the CPU supposed to handle IRQs when it's in the middle of handling one already? Is there a decay rate?

I've tried to read all the documentation on IRQ being low and have even looked at the source for Nestopia and FCEUX and I'm not able to discern exactly how they handle it.

Here are the relevant bits of code I'm using for IRQ handling. Note it's not cycle accurate, I'm just issuing one instruction at a time, capturing how many cycles that took and then catching up the PPU and mappers with 3x that number. Note also this is Rust code.

Code: Select all

// Called as soon as the mappers set irq_pending
pub fn trigger_irq(&mut self) {
    self.pending_irq = true;
}

// Called by the CPU clock function before fetching the next opcode if pending_irq is true
pub fn irq(&mut self) {
    if self.get_flag(I) == 0 {
        self.push_stackw(self.pc);
        self.set_flag(B, false);
        self.set_flag(U, true);
        self.push_stackb(self.status);
        self.set_flag(I, true);
        self.pc = self.readw(IRQ_ADDR);
        self.cycle_count = self.cycle_count.wrapping_add(7);
        self.pending_irq = false;
    }
}
The one change that makes MMC3 work and breaks MMC5 is this:

Code: Select all

// Called as soon as the mappers set irq_pending
pub fn trigger_irq(&mut self) {
    if self.get_flag(I) == 0 { // Ignores IRQs while I is set
        self.pending_irq = true;
    }
}
I also did some testing inside FCEUX for both games and it seems to finish one IRQ, execute a single instruction, and then handle the next IRQ which is what I've got above for the new changes in the CPU. If the above IS how to handle IRQs, then my MMC3 IRQ timing is off and I'm not sure how to fix it.

This is my clock function for MMC3

Code: Select all

fn clock_irq(&mut self, addr: u16) {
    let next_clock = (addr >> 12) & 1;
    if self.regs.last_clock == 0 && next_clock == 1 {
        // Rising edge
        let counter = self.regs.irq_counter;
        if counter == 0 || self.regs.irq_reload {
            self.regs.irq_counter = self.regs.irq_latch;
            self.regs.irq_reload = false;
        } else {
            self.regs.irq_counter -= 1;
        }

        if self.regs.irq_counter == 0
            && (counter | self.mmc3_alt) == 0x01
            && self.regs.irq_enabled
        {
            self.irq_pending = true;
        }
    }
    self.regs.last_clock = next_clock;
}
It gets called for every VRAM read in the PPU while getting BG and sprite data and it seems to trigger correctly at cycle 260 at irq_latch-1 scanlines

Any clarification would be great!

Edit: When running Blaargs 1-clocking MMC3 tests - The "new" implementation of allowing IRQ to be set to pending while I flag is set fails with "Counter/IRQ/A12 clocking isn't working at all Failed #2".

If I use the old implementation of ignoring IRQs when I is set, it still fails, but at least gets to "Should decrement when A12 is toggled via PPUADDR Failed #3"

nocash
Posts: 1228
Joined: Fri Feb 24, 2012 12:09 pm
Contact:

Re: CPU IRQ handling conflicts regarding MMC3 and MMC5

Post by nocash » Wed Sep 25, 2019 8:35 pm

Your irq pending flag is an external input pin on the cpu chip.
The cpu can read that from that pin - but it can it can't change the pin state.
The irq handler will normally need to acknowledge the irq by writing (or reading) a cartridge register, ie. something that is telling the cartridge that it can release the interrupt signal.
If it doesn't do so then the irq pin would stay on forever, and another irq would execute immediately when the cpu is clearing the I flag. That, of course, needs to be emulated, too: executing any "old" pending interrupts when clearing the I flag.
homepage - patreon - you can think of a bit as a bottle that is either half full or half empty

User avatar
lukexor
Posts: 22
Joined: Sat Jun 08, 2019 2:53 am
Location: Portland, OR
Contact:

Re: CPU IRQ handling conflicts regarding MMC3 and MMC5

Post by lukexor » Wed Sep 25, 2019 11:02 pm

Thank you! This plus a bit more debugging helped clarify I need to re-architect how I do my interrupt handling.

It turns out that one of the key interrupts that was breaking the graphical display was happening after a write to $E000 clearing the IRQ and disabling it but this clear happened AFTER my code had already triggered an IRQ inside the CPU.

A major part of the struggle with this emulator has been Rust's strict limits on mutable state. Getting components like the Mapper and CPU to communicate to each other when they are both owned by the wrapping Console struct has been challenging.

I'm likely going to go with some sort of IRQ bit flag like I see in FCEUX with some better message passing using an Interrupt trait or something. For now, I've just got two interrupt variables - one for the mapper and one for the APU. Not sure how robust it is.

Post Reply