Quantcast
Channel: nesdev.org
Viewing all articles
Browse latest Browse all 746

Thread-safe MMC1 bankswap without checks

$
0
0
So on the Discord, I've seen it stated here and there that the MMC1 serial interface inherently results in it being more difficult to perform bankswitches that can be safely interrupted and returned to without corrupting the mapper state. One suggestion I saw was to perform a check after each of the serial writes to ensure that an interrupt did not occur, and if one did, then reset the mapper and try again. However, I believe to have come up with a thread-safe bankswitching function that requires zero extra checks and is nearly as fast as what you would write if your interrupt handlers don't do any bankswitching:

Code:

;;; Arguments:;;;     A: New bank valueset_prg_bank:    STA cur_prg_bank    INC * ; reset mapper    STA $E000    LSR A    STA $E000    LSR A    STA $E000    LSR A    STA $E000    LSR A    STA $E000    RTS
And then, in any interrupt handler that does PRG bankswitching:

Code:

interrupt_handler:    <save registers>    LDA cur_prg_bank    PHA        <do interrupt stuff>        ;;; cleanup    PLA    JSR set_prg_bank    INC * ; reset mapper    <restore registers>    RTI
The most crucial thing here is that we guarantee after an interrupt returns:
  1. The currently loaded prg bank is cur_prg_bank as set before the interrupt
  2. The mapper is reset.
Let's consider two cases:
  • If the interrupt occurs immediately after the mapper reset instruction, then the interrupt handler returns in a reset state, and the bankswitch function proceeds to do 5 serial writes like normal. This will just set the current PRG bank to cur_prg_bank again, which is fine.
  • If the interrupt occurs after any of the serial writes, then the handler returns with cur_prg_bank swapped in, the mapper reset, and then the bankswitch function continues to do <=4 serial writes. Since 5 writes aren't performed, these serial writes do nothing, and the function returns with cur_prg_bank swapped in from the interrupt handler. The "leftover writes" are fine because when another bankswitch happens, the first thing it does is reset the mapper again.
The reason there needs to be the extra mapper reset after the bankswitch in the interrupt handler is to account for the case of nested interrupts. For example, if the main game logic is doing a bankswitch, gets interrupted by IRQ, and the IRQ cleanup bankswitch is interrupted by NMI, then there's a chance the IRQ handler finishes its bankswitch with "leftover writes" still on the table. And if it returns into the middle of the game logic's bankswitch without resetting, and even more serial writes are performed, we may end up with >=5 "leftover writes" and the wrong bank ends up being loaded. So, we manually reset the mapper before returning from the interrupt to ensure that this is not possible.

The only caveat with this approach as far as I can tell is that the resetting makes this only work if the game is written to keep the fixed bank in the 2nd window.

I thought this would be helpful to anyone writing a MMC1 game, and I am also very curious to hear if anybody sees issues or improvements to this technique.

Cheers!

[edit: added pha/pla for cur_prg_bank in interrupt handler]

Statistics: Posted by foobles — Fri Oct 11, 2024 9:49 pm — Replies 4 — Views 208



Viewing all articles
Browse latest Browse all 746

Trending Articles