Getting a NMI in the middle of your bank switching code and switching banks inside the NMI: would that be a problem?
My NMI code always saves the current bank number and restores it before RTI but I just wondered if you were really unlucky with the NMI timing could it screw up the bank switch that was in progress before the interrupt occurred?
When you bankswitch PRG, write the bank number to a variable in RAM first, then write the bank to the mapper. That way you can't go wrong.
Also, interrupts and the like should push the old bank number, then restore it when returning.
The NMI handler can't easily save and restore the state of the MMC1's 5-bit shift register. Ideally, on a mapper with a serial port, the NMI handler shouldn't write to the mapper at all, and it should be located in a fixed bank.
I am using a MMC1 design, with $c000-$ffff fixed and $8000-$bfff switched. I store the bank number at $bfff in each bank, so at any time I can know which bank is active by just reading it directly from $bfff.
Maybe this approach would be helpful to you?
We had some discussions about this in the past. Here's one
thread I could find.
I believe that the general solution that would work for all mappers would be to bankswitch exclusively through a function, and this function would use a flag to indicate that bankswitching is in process, after saving a copy of the command it's supposed to perform. If your interrupt detects it has interrupted a bankswitching operation, it executes the command before returning and skips the rest of the bankswitching function when returning.
tokumaru wrote:
We had some discussions about this in the past. Here's one
thread I could find.
I believe that the general solution that would work for all mappers would be to bankswitch exclusively through a function, and this function would use a flag to indicate that bankswitching is in process, after saving a copy of the command it's supposed to perform. If your interrupt detects it has interrupted a bankswitching operation, it executes the command before returning and skips the rest of the bankswitching function when returning.
So....if your NMI reads the flag, just let it set another flag and RTI and when the bankswitiching is done, tell your program to continue the NMI?
65024U wrote:
So....if your NMI reads the flag, just let it set another flag and RTI and when the bankswitiching is done, tell your program to continue the NMI?
That's not what I suggested, but is another possible approach if you can afford to delay your NMI by several cycles (some programs can't because the NMI is timed for raster effects from the top of the screen).
What I suggested was: Let the NMI interrupt the bankswitching procedure and do whatever it wants, including bankswitches (but not using the same function used by the main thread). Once it's done, check the flag to see if a bankswitching procedure was interrupted, in which case it should be restarted. That would involve hijacking the return address on the stack.
So, use tokumaru's "never disable NMI" approach if your NMI timing is critical. Use the
disable NMI momentarily approach if your NMI can be delayed by 50 cycles or so without problem.
Okay I see. Thanks.
A little to complex for me since I doubt I'll ever do something that close to the cycle count, but good information to poses!
Yeah, if the bankswitch requires multiple writes to registers (which it usually does), definitely save the value like so:
Code:
;main thread
lda #$xx
sta bankswitchreg1_saver ;Very important to write to the virtual register first!
sta $bankswitchreg1
lda #$xx
sta bankswitchreg2_saver
sta $bankswitchreg2
....
;NMI
;blah code
lda bankswitchreg1_saver
sta $bankswitchreg1
lda bankswitchreg2_saver
sta $bankswitchreg2
rti
This solution may not work for all mappers like MMC1 which requires writes to the same register, but I'm pretty sure for things like MMC3 bankswitching this works great. And like I said in the comment above, ALWAYS write to the virtual register FIRST, because you restore the actual register to the value of the virtual register at the end of the interrupt. If you wrote to the actual register first, and there was an interrupt before you wrote to the virtual register, you would restore the actual one to whatever value happens to be in the virtual one; not necessarily the intended value (which could be catastrophic).
Megaman 2 has some infamous bugs that resulted from getting enough objects on the screen to cause lag, then having a bankswitch routine getting interrupted. (Megaman 2 is MMC1)
So at one point, it reads some garbage information on the wrong bank to determine where you go when you reach the edge of the screen. You can see the bug exploited in the TAS.
This is probably also the cause of the bug that takes you from Air Man to Dr. Wily's Stage #2.
Are you sure this is the cause of the bug ? I think it's extremely unlikely a bug like this (MMC1 write interupts another) don't result in a crash. For example, the main thread was banskwtiching some PRG bank, then NMI occurs, and inside the NMI they bankswitch the sound PRG bank to call the sound engine. Unfortunately, this fails, as the 1st write won't be the 1st write, etc... so any other random bank is switched, and the game will crash when calling the sound code.
It's not about it crashing when it calls the sound code, the sound code bankswitch would set the MMC1 reset flag first. It's the code that's running before NMI that had its bankswitch interupted, so it reads invalid data from the wrong bank. And then it bankswitches again soon afterward, so there is little impact from the interrupted bankswitch, so the developers never noticed anything odd.
If you want to see what goes on in a debugger, watch the Rockman 2 TAS. The first exploitation happens near frame 19014.