This page is a mirror of Tepples' nesdev forum mirror (URL TBD).
Last updated on Oct-18-2019 Download

MMC3: What happens if a bank switch is interrupted by NMI?

MMC3: What happens if a bank switch is interrupted by NMI?
by on (#223152)
Hi.

Hypothetical scenario. Let's say I do this a bank switch:

Code:
lda xxx
sta $8000
; NMI triggers here
lda yyy
sta $8001


And the NMI triggers between the 8000/8001. Lets say the NMI also does bank switches for audio and other things. Lets also assume the NMI is being nice and pushing/popping all registers.

What will happen when the code resumes and does that 8001 store?
Will it do anything, will it be ignored?

I read that it is good practice to first store the bank you want to switch to to a variable, so you can restore all banks at the end of NMI to prevent these kind of issues. Does it apply to this exact scenario?

Thanks.

-Mat
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223154)
It depends entirely on what your NMI code does. If your NMI code does any writes to 8000, the write to 8001 from your main code will affect the bank that NMI was messing with rather than what you intended for your code to do.

So what you do is use a Shadow variable for the 8000 register. Write to the shadow variable, then to the actual 8000. Then your NMI can set 8000 back to the value from the shadow variable, thus preventing problems.
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223156)
One possible suggestion might be that you might only ever need to swap one PRG bank outside of NMI/IRQ, so you could just always write to $8000 to re-select the bank before returning to the main thread. (CHR banking in NMI/IRQ only, $C000 banking only in NMI for music maybe.)
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223157)
Yeah from the point of view of MMC3 it doesn't know that an NMI happened, it's just interpreting the writes sequentially.

This situation is fairly easy to deal with when it comes to MMC3. Some other mappers like MMC1 are more annoying (if an interrupt can happen in the middle of the 5-bit write sequence, you have to detect that situation and resume from the beginning).
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223159)
Oh thank you guys. I get it now.

For some reason I always though bank swap always required a pair of writes (8000 + 8001) but that's not the case. The value of the 8000 persists until it is modified, so that's the only value that you need to shadow (and restore at the end of NMI) to prevent that case from being a problem. Easy.

Thanks again.

-Mat
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223164)
As a general tip for situations where the bank switch is a hassle of multiple writes that you absolutely don't want to be interrupted since you cannot buffer it (like in MMC1 where you have five writes to the same register one after another, so each unexpected write to it would totally screw you up):

Use a global variable that you initialize to 0 at the start of the program and then you do this (the code refers to your example, not to MMC1):
Code:
; Bank write preparation.
LDX xxx
LDY yyy

; Flag is set.
INC BankIsBeingWritten

; Bank writes are as close together as possible.
STX $8000
STY $8001

; Flag is reset.
DEC BankIsBeingWriten


Now, the NMI would have three distinct situations to react to:

1. Gameplay logic has finished:
The NMI does all the normal stuff.

2. Gameplay logic is in the middle of execution:
The NMI still does the music update, but not the graphics.

3. The BankIsBeingWritten flag is not set to 0:
The NMI does no writes whatsoever to any NES register. (Except for saving and restoring the A, X and Y register of course.) It immediately leaves again.

This means, in the very, very unlikely case where the NMI starts in the middle of a bank switch, you have a different situation than during a regular lag. In this rare situation, the music will also lag for one frame, but you can be sure that nothing will screw up your bank switch setup.


Also, you might have noticed that I changed your bank writes a bit.
Instead of
LDA xxx, STA $8000, LDA yyy, STA $8001
I used
LDX xxx, LDY yyy, STX $8000, STY $8001.

This way, the moments from the start of the first bank to the end of the last bank write are even shorter and it's more unlikely that NMI hits here.
If the NMI hits somewhere between LDX and LDY, you don't need to care since there haven't been any actual bank writes yet.
So the room for a problematic interruption is exactly between only two commands.
(Might not be important for MMC3, I don't know. But it's a general hint: If the writes cannot be interrupted, put them as close together as possible, so that NMI situation 3 is minimized.)
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223165)
The Curse of Possum Hollow has two shadow registers: $8000 bank and $A000 bank. They are always set through one subroutine that copies them to registers 6 and 7, called mmc3_restore_bank, in this order:

1. Write $06 to $8000
2. Write bank number for $8000 to $8001, changing bank at window $8000
3. Write $07 to $8000
4. Write bank number for $A000 to $8001, changing bank at window $A000

The NMI and IRQ handlers call this subroutine before they finish. So even if a call to mmc3_restore_bank happens during a call to mmc3_restore_bank, the following three sequences are possible. All produce the correct result:

1. Write $06 to $8000
2. Write bank number for $8000 to $8001, changing bank at window $8000
3. Write $07 to $8000
4. Write bank number for $A000 to $8001, changing bank at window $A000
[Return]
2. Write bank number for $8000 to $8001, changing bank at window $A000 (wrong bank but will be corrected immediately)
3. Write $07 to $8000
4. Write bank number for $A000 to $8001, changing bank at window $A000

1. Write $06 to $8000
2. Write bank number for $8000 to $8001, changing bank at window $8000
3. Write $07 to $8000
4. Write bank number for $A000 to $8001, changing bank at window $A000
[Return]
3. Write $07 to $8000
4. Write bank number for $A000 to $8001, changing bank at window $A000

1. Write $06 to $8000
2. Write bank number for $8000 to $8001, changing bank at window $8000
3. Write $07 to $8000
4. Write bank number for $A000 to $8001, changing bank at window $A000
[Return]
4. Write bank number for $A000 to $8001, changing bank at window $A000

The only times registers 0-5 get written are in an interrupt (NMI or IRQ) and during bulk CHR RAM loading (during which IRQ is off and most NMI processing is skipped).

To make reentrancy easier, the NMI handler does not call the audio driver. Instead, the main loop checks a few times to see how far the audio has fallen behind and repeatedly calls the audio driver until it's up to date.
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223168)
I'm curious about scenarios where it makes sense to bank switch outside of NMI, but also outside of timed code (ie. on a specific scanline, NMI guaranteed to not happen).
I'm guessing it would be related to a special case requiring you to read data from another bank? but I can't imagine a scenario where I really need to do that while rendering is turned on.
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223175)
tepples wrote:
The Curse of Possum Hollow has two shadow registers: $8000 bank and $A000 bank. They are always set through one subroutine that copies them to registers 6 and 7, called mmc3_restore_bank, in this order:


Oh thanks. I didn't consider the scanline IRQ could also interfere.

Wouldnt simply shadowing the 8000 reg work?

Would something like that work? (Not shown: irq/nmi push/pop all registers and stuff). I cant help but feel like there is a flaw. Its too simple.

Code:
main:
   lda ...
   sta shadow8000
   sta 8000
   ; ===> IRQ OR NMI FIRES HERE!
   lda ...
   sta 8001

irq:
   lda shadow8000
   sta shadow8000_copy
   
   lda ...
   sta shadow8000
   sta 8000
   ; ===> NMI FIRES HERE!
   lda ...
   sta 8001
   
   lda shadow8000_copy
   sta shadow8000
   sta 8000
   
   rti

nmi:

   lda ...
   sta 8000
   lda ...
   sta 8001
   
   lda shadow8000
   sta 8000
   
   rti


-Mat
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223176)
bleubleu wrote:
Oh thanks. I didn't consider the scanline IRQ could also interfere.

NMI (or another IRQ) could technically interrupt your IRQ handler, but how often do you really have an IRQ at the bottom of the screen? So it depends.

If you want to make it really foolproof, push shadow8000 on stack and restore as you'd CPU registers. Then you're only limited by the stack size.
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223214)
What I like about the FME-7 over the other mappers (like MMC3) is that its simple protocol for its functions lends to very simple and fool-proof code when dealing with its registers:
Code:
;; calling with A: mapper register value, X: mapper register address
    stx mapper_reg  ; shadow register
    stx $8000
    sta $A000

Moreover, you can as easily remember the PRG banks, should you need to change/restore them in NMI:
Code:
switch_prgbank:
    sta mapper_prgbank_save - 8, x
    ; fallthrough to this function (optimized tail-call)
mapper_write:
    stx mapper_reg
    stx $8000
    sta $A000


Here, mapper_prgbank_save is an array big enough for the 4 PRG bank registers, and we assume that this function is called only when switching PRG banks (where X=8..B) outside of NMI. The NMI just have to restore the PRG banks and write mapper_reg to $8000 at its end.
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223217)
Just thought of another way to detect a botched PRG bankswitch that doesn't require flags: it's common practice to store the number of the bank at a fixed location in each bank, and if that's the case, you can just compare the desired bank number against that location after the switch is supposedly done, and repeat the process if there's no match.

There are better solutions for the MMC3, as has already been explained, but maybe this could work well for another mapper.
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223224)
tokumaru wrote:
Just thought of another way to detect a botched PRG bankswitch that doesn't require flags: it's common practice to store the number of the bank at a fixed location in each bank, and if that's the case, you can just compare the desired bank number against that location after the switch is supposedly done, and repeat the process if there's no match.

But how would you do this, for example, in MMC1?

MMC1 requires five writes to the same register. How would you detect whether the NMI hit in the middle of these writes by simply comparing the bank?
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223226)
It's not a technique for MMC1, and probably not MMC3 either.

It's useful for e.g. UxROM or AxROM that have more atomic state, if you need to bankswitch in an IRQ or NMI you can inspect the bank number to know what to put back afterward.

Edit: tokumaru was suggesting a diffferent technique than I thought: verify and retry. See below.
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223228)
No, no, I actually meant for any mapper with non-atomic switching. Something like this:

Code:
  lda #04 ;try to select bank 4
SelectBank:
  ;[MAPPER-SPECIFIC CODE TO SELECT BANK GOES HERE]
  cmp BankNumber ;see if the desired bank number matches what the ROM says is mapped
  bne SelectBank ;keep trying until you get a match...

MMC1 would require trashing the accumulator though, so you'd probably have to copy the bank number to X or Y for the comparison.

Nothing groundbreaking or particularly innovative, it's just that sometimes we already have identifiers in every bank (like in the example rainwarrior mentioned), and also the bank number in a register, so this comparison can be cheaper than using flags and/or shadow registers.
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223230)
Ah, verify and retry. I see. That makes sense.
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223233)
Sumez wrote:
I'm curious about scenarios where it makes sense to bank switch outside of NMI, but also outside of timed code (ie. on a specific scanline, NMI guaranteed to not happen).
I'm guessing it would be related to a special case requiring you to read data from another bank? but I can't imagine a scenario where I really need to do that while rendering is turned on.

Reading data or code along normal execution. Say common code is bank 0, enemy 0-3 code is bank 1, enemy 4-7 is bank 2, etc. If you have enemy 2 and enemy 6 visible, then you naturally need to switch there to execute their logic.
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223236)
I admit I only skimmed this, so apologies if this method is already here.

I write the intent of my bank switch to shadow RAM before I bank switch. At the end of my NMI, it reads the shadow RAM and does the intent unconditionally. I do this for MMC3, but I think it'd work for most mappers.

My understanding of MMC1 might be shaky having not used it, but I think this might even work for it. Suppose an NMI interrupts after say... the third write.

The NMI does whatever it will do, and then resets the shift register, loads the shadow RAM and does the five writes to swap in that bank that the code it interrupted was trying to swap in. It returns. The code does two more writes. Nothing happens because only the fifth write matters.

But nothing happening doesn't matter because the right bank is already in thanks to the NMI. This does mean you have to reset the shift register for every bank swap, but that's not that bad. Let me know if that's wrong!

For mappers where only one write swaps, you end up swapping in the bank that's already there (because the NMI already did it) in the worst case, but that also doesn't matter.
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223242)
So, how do you reset the MMC1 register, so that it definitely jumps back to zero writes, no matter how many writes have already happened?
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223243)
By writing a set high bit to $8000, right?
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223244)
Right. I just had a look at my code again: I indeed have that in the beginning of the program flow.
It resets the whole mapper, including the general bank setup (which one is the fixed bank, which one the variable) and the mirroring.

Since there's probably no way to reset just the writes to $E000, I did the thing with the flag:

After the start, before any write to $8000 for the mirroring or $E000 for the bank, a variable in incremented and later decremented again.

If NMI hits and it detects that this variable is not 0, it immediately exits, even without updating the music. (Since music updates would require another bank switch.)

In my setup, I never try to correct incomplete MMC1 writes because I make sure that there can never be any incomplete writes to begin with.
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223245)
If it really resets the mirroring a wiki update may be in order.

It says it rewrites Control OR $0C. That does force control to "fix last bank at $C000 and switch 16 KB bank at $8000", but that's how I'd personally have it set up anyway. If the wiki is right (and I'm understanding the wiki correctly), the mirroring doesn't change and neither does CHR ROM bank mode.

So you're correct that my method would not work if you preferred a different PRG mode (or switched at run time). Otherwise it might be fine. But code is king, so I'll just write a test ROM when I've got a minute.
Re: MMC3: What happens if a bank switch is interrupted by NM
by on (#223246)
Kasumi wrote:
If it really resets the mirroring a wiki update may be in order.

I'm not quite sure. I just suspected it, I didn't do actual tests.