llvm-mos / llvm-mos-sdk

SDK for developing with the llvm-mos compiler

Home Page:https://www.llvm-mos.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Audit NES shadow registers for non-reentrancy

mysterymath opened this issue · comments

In cases where a memory location is shadowing a MMIO register, a NMI interrupt may occur between the set of the MMIO register and the set of the shadow, and that interrupt may make an incorrect determination of the MMIO state by reading the shadow register. We should analyze the SDK so far for such issues, see if there's a straightforward way to address them, and if not, explicitly call out in which circumstances it's guaranteed accurate to call those accessors.

Here's some loose thoughts:

The typical shadowing routine for a MMIO register on a one-register mapper (UNROM, CNROM, etc.) would be (written with ASM this time):

tax
sta _BANK_SHADOW
sta __rom_poke_table, x

In this situation, there's only one point during which an NMI can fire where the shadowed register no longer matches the real register: the write between _BANK_SHADOW and rom_poke_table.

The easy way to resolve that is to always write the shadow to the ROM at the beginning of NMI:

lda _BANK_SHADOW
tax
sta __rom_poke_table, x

This has the nice property of unlocking two new functionalities:

  • the ability to write to the ROM without writing to _BANK_SHADOW, creating a bank change that will automatically undo itself at NMI, but invalidating getters,
  • the ability to write to _BANK_SHADOW without writing to the ROM, creating a delayed bank change that will only apply at NMI.

Combined with re-applying _BANK_SHADOW at the beginning of NMI, this allows for "change CHR bank until vertical blank" handlers, as well as "change CHR bank at vertical blank" handlers - both very useful constructs! To borrow terms from the MMC1 bank implementation, I'm going to refer to the former as a "split".

Re-applying _BANK_SHADOW at the beginning of NMI also means that getters and setters will work correctly inside an NMI. The only case where they won't work correctly is "splits" - this should be appropriately documented; however, CHR banks don't tend to be paged in/out the same way a PRG bank would across calls, which makes me believe this won't be much of a problem in real-world use.

This will work fine enough for those discrete mappers whose register corresponds to only one value (CNROM, UNROM). For UNROM-512, though, one needs to rethink it: we don't want a situation in which, say, writing _BANK_SHADOW to the register on set_prg_bank also changes the CHR bank.

More broadly this issue also should include IRQ handlers; I'm not sure what the current scope of behaviors are, but this case is considerably easier to handle, since interrupt state can be pushed/popped around critical sections and interrupts disabled to keep shadows and the MMIO registers observably identical.

More broadly, from discussion in #198, if we can't identify a need for CHR shadowing, and the cost of keeping it consistent against NMI and IRQ is too high, it may be a good idea to either withdraw such functionality or mark it as possibly stale in the case of interrupts.

I'm might be being too persnickety about IRQs; we can wrap various routines in PHP; SEI; ... ;PLP if we want things to always work, but that imposes a 9 cycle and 3 byte cost to essentially every banking function that doesn't use the even more expensive retry approach I picked for MMC1 and MMC3. I'd leave it open to someone more experienced to the Nesdev community whether this is worth it.

I think bank changing is sufficiently uncommon for this 9 cycle/3 byte cost to be acceptable. Worst-case, set/swap/split_chr_bank_unsafe could be added which pushes the interrupt wrapping and other potential safety mechanisms on the end user.

However, I'm not experienced at NES development myself, either.

I think it might be worth re-opening this issue (or creating a new one), as while #198 fixes the much more pressing NMI part of the issue, it does nothing about IRQs.

Closing this one for now; I had become sufficiently far removed from the issue that I had forgotten how special handling of interrupts in C was in llvm-mos. None of the SDK banking functions are annotated with the relevant interrupt attributes, so none of them are safe to call from an IRQ or NMI. Even if they were, they'd still need the AXY calling convention to be practical. So, there may actually be too much logic in these functions to make them reentrant, at least in how they're legal to be called today (a descendant call of _start).