NMI: Difference between revisions

From NESdev Wiki
Jump to navigationJump to search
(→‎Race condition: imply the depths of Micronics worst-case programming)
(→‎Race condition: linked to the new article about the solution to the problem described here)
Line 53: Line 53:
Waiting for NMI in this way would miss an NMI that happens while other code is running.
Waiting for NMI in this way would miss an NMI that happens while other code is running.
In some cases, this could cause a sprite 0-triggered scroll split to flicker (or worse).
In some cases, this could cause a sprite 0-triggered scroll split to flicker (or worse).
The next step up involves doing VRAM updates and sprite 0 waiting in a separate [[NMI thread]] that is guaranteed to run every frame.


Gradius counts the approximate time that each object handler takes and deliberately overflowing the calculations to the next frame when it might otherwise come close to missing an NMI or a sprite 0 hit.
Gradius counts the approximate time that each object handler takes and deliberately overflowing the calculations to the next frame when it might otherwise come close to missing an NMI or a sprite 0 hit.
Games developed by [[wikipedia:Micronics|Micronics]] are likely to reduce a game's overall frame rate far below 60 frames per second to match the worst case of lag.
Games developed by [[wikipedia:Micronics|Micronics]] are likely to reduce a game's overall frame rate far below 60 frames per second to match the worst case of lag.

Revision as of 20:33, 28 March 2010

The 2A03 and most other 6502 family CPUs are capable of processing a non-maskable interrupt (NMI). This input is edge-sensitive, meaning that if other circuitry on the board pulls the /NMI pin from high to low voltage, this sets a flip-flop in the CPU. When the CPU checks for interrupts and find that the flip-flop is set, it pushes the processor status register and return address on the stack, reads the NMI handler's address from $FFFA-$FFFB, clears the flip-flop, and jumps to this address.

"Non-maskable" means that no state inside the CPU can prevent the NMI from being processed as an interrupt. However, most boards that use a 6502 CPU's /NMI line allow the CPU to disable the generation of /NMI signals by writing to a memory-mapped I/O device. In the case of the NES, the /NMI line is connected to the NES PPU and used to detect vertical blanking.

Operation

Two 1-bit registers inside the PPU control the generation of NMI signals. Frame timing and accesses to the PPU's PPUCTRL ($2000) and PPUSTATUS ($2002) registers change these registers as follows:

  1. Start of vertical blanking: Set NMI_occurred in PPU to true.
  2. End of vertical blanking, sometime in pre-render scanline: Set NMI_occurred to false.
  3. Read $2002: Return old status of NMI_occurred in bit 7, then set NMI_occurred to false.
  4. Write to $2000: Set NMI_output to bit 7.

The PPU pulls /NMI low if and only if both NMI_occurred and NMI_output are true. By toggling NMI_output ($2000 bit 7) during vertical blank without reading $2002, a program can cause /NMI to be pulled low multiple times, causing multiple NMIs to be generated.

Caveats

Old emulators

Some platforms, such as the Game Boy, keep a flag turned on through the whole vertical blanking interval. Some early emulators such as NESticle were developed under the assumption that $2002.7 worked the same way and thus do not turn off NMI_occurred in line 3. Thus, some defective homebrew programs developed in this era will wait for $2002.7 to become false and expect this to happen at the end of vblank. (The right way to wait for the end of vblank involves triggering a sprite 0 hit and waiting for that flag to become 0.) Some newer homebrew programs have been known to display a diagnostic message if an emulator incorrectly returns true on two consecutive reads of $2002.7 .

Race condition

If 1 and 3 happen simultaneously, $2002.7 is read as false, and NMI_occurred is set to false anyway. This means that the following code that waits for vertical blank by spinning on $2002.7 is likely to miss an occasional frame:

wait_status7:
  bit $2002
  bpl wait_status7
  rts

Code like wait_status7 is fine while your program is waiting for the PPU to warm up. But once the game is running, the most reliable way to wait for a vertical blank is to turn on NMI_output and then wait for the NMI handler to set a variable:

wait_nmi:
  lda retraces
@notYet:
  cmp retraces
  beq @notYet
  rts

nmi_handler:
  inc retraces
  rti

But even this handler is not perfect. If your game code takes significantly longer than 24,000 cycles, such as if you have too many critters moving on the screen, it may take longer than one frame. Waiting for NMI in this way would miss an NMI that happens while other code is running. In some cases, this could cause a sprite 0-triggered scroll split to flicker (or worse). The next step up involves doing VRAM updates and sprite 0 waiting in a separate NMI thread that is guaranteed to run every frame.

Gradius counts the approximate time that each object handler takes and deliberately overflowing the calculations to the next frame when it might otherwise come close to missing an NMI or a sprite 0 hit. Games developed by Micronics are likely to reduce a game's overall frame rate far below 60 frames per second to match the worst case of lag.