Frame cycle and NMI
How one frame of the field runs: the game logic in GameLoop_FrameBody, the end-of-frame work
that prepares the next picture, the frame wait, and the NMI handler that uploads it during
vertical blank. The scene mode in bank $C2 and the battle have frame cycles of their own,
summarised at the end.
See also: Engine core, Boot, Architecture.
One field frame
GameLoop_FrameBody ($C0:0097)
input, scene changes, field update, event hooks … game logic (see Engine core)
Field_EndOfFrame ($C0:00BF)
Vblank_Unk59D9 queue the objects whose script timer ran out
Vblank_ReadScanlineCounters run their scripts while the frame has time
JSL EngFD_UnkC2C1 one HDMA table builder (two table sets, alternate frames)
JSL FdVec_FFF7 the $0520 animation records (not analysed)
Vblank_UnkA810 move every object, cull to the view, tick animations
Field_ProcessAnimQueue build queued sprite frames while there is time
BRL Oam_BuildShadow rebuild the OAM shadow for the next frame
Field_WaitFrame ($C0:EC60) handshake with the NMI
BRA GameLoop_FrameBody
The Vblank_ names are historical: none of these routines runs inside the interrupt.
Field_EndOfFrame's header says so plainly: it is "not an interrupt handler and waits for
nothing itself". Two loops in this list stop on the PPU's V counter rather than a fixed count,
so the work spreads over frames when there is a lot of it: the script runner
(Vblank_ReadScanlineCounters) and the frame builder (Field_ProcessAnimQueue).
Field_EndOfFrameShort ($C0:00DE) runs only the two bank-$FD calls, for the fade and idle
loops, and Field_IdleFrame ($C0:00EB) is one frame of upkeep without game logic:
Field_FrameUpdate, Field_EndOfFrameShort, Field_WaitFrame.
The handshake
Field_WaitFrame ($C0:EC60, 23 bytes) counts Field_Unk58 up, increments
Nmi_FramePending ($0152) and spins until it reads 0 again. Only NmiHandler clears it, after
the frame's uploads (the "frame handshake" of ram_engine.inc). The NMI must be on, or
Field_WaitFrame never returns.
NmiHandler
NmiHandler ($C0:EA63, 509 bytes) is entered through the WRAM trampoline at $7E:0500 that
InstallNMI writes. It saves A, X, Y, DP and DB, sets DB=$00, acknowledges the NMI (RDNMI),
turns HDMA off and writes NMITIMEN and BGMODE. Then:
- Lag frame (
Nmi_FramePending= 0: the game has not reached its wait): no uploads at all. -
Otherwise, with DP=
$0100, the first that applies:Field_Unk47nonzero: the first of its bits 0–3 that is set is cleared and its upload runs (Field_UploadUnk1D00,1C00,1F00,57E0), one per frame; then the common uploads.- A map redraw step in
Field_MapRedrawDone: the 2 KB bufferMap_BufC800to one of the map's tilemaps (Map_UploadBuf2K), or to VRAM$7000/$7400; thenHdma_InitChannelsFD, skipping the common uploads. - Otherwise one line of the staff credits if requested (
Credits_UploadLine) and theField_Unk36upload if requested (Field_UploadUnk5800), then the common uploads.
The common uploads are
Pal_UploadCgram(the palette buffer at$7E:2200to CGRAM),Oam_UploadShadow($220bytes from$0700to OAM),VramQ_Flush(queued sprite tile uploads), then, with DP=$1D00, the map edge uploads for each layer and direction marked inMap_BuiltRowYInc…Map_BuiltColXDec(Map_UploadRowYInc1…Map_UploadColXDec3,$C0:8445–$C0:87F0),Hdma_InitChannelsFDandNmi_FramePending= 0. - Every NMI:JSL BankC2_Entry8002(the joypad readerMenu_ReadPad), the window shadowsPpu_W12SelShadow…Ppu_CgwSelShadowtoW12SEL…CGWSEL, the fixed colour,HDMAENfromField_HdmaEnableandINIDISPfromFade_Brightness(forced blank when it is 0). These read the field bytes with absolute addresses, because on a lag frame DP is still the interrupted code's. Then it restores everything and returns withRTI.
The uploads use DMA channel 7. HDMA is set up by Hdma_InitChannelsFD ($FD:C1EE): eight
indirect channels writing the BG tilemap bases, the three layers' scroll registers, the main and
sub screens, two window positions and colour math, from one of two table sets in bank $7F
($7F:0F80 or $7F:1238).
The field IRQ
IrqHandler ($C0:ECCC) is entered through the trampoline at $7E:0504 that InstallIRQ
writes. It does nothing when Field_Unk0F bit 7 is set (who sets it is not traced) or when
TIMEUP says this is not a timer IRQ. Otherwise it turns the V-count IRQ off until the next NMI,
waits for H-blank, forces blank and turns HDMA off, then by Field_Unk53 bit 0, which
EngFD_UnkC2C1 flips each frame, runs Irq_UploadTileAnim or FdVec_FFFD (bank $FD, not
matched). The tile writes therefore happen every other frame.
Irq_UploadTileAnim ($C0:ED15) runs one queued tilemap job per call: either the new metatiles
of a tile animation (the four tilemap words of each map tile, to the VRAM addresses a
Mode*_Handler queued in TileAnim_VramAddrs) or DefaultHandler's single tile step. A step
queued together with an animation waits for the next IRQ.
Before that, while Field_Unk63 is not negative, it calls Irq_DrawChoiceCursor ($C0:F05E).
That routine draws a 2×2-tile picture (tiles $0FC–$0FF) at choice row Field_Unk63 (0–3) of
the tilemap that Field_UploadUnk1C00 sends to VRAM $1C00, and puts Field_Unk2B78's grid
entries back in the other three rows (Irq_ChoiceCursorClearRow0–3). A value of 4 or more clears all four
rows and sets Field_Unk63 idle, so the next IRQs skip it. Its header calls this the choice
cursor as "probable, not traced on screen": Field_Unk63 is the index Sub_1ADF steps with Up
and Down while a message choice is active. What the four tiles show (the hand cursor, probably)
and which layer uses the $1C00 tilemap are open in NEXT.md.
The OAM shadow
Oam_BuildShadow ($C0:B271) rebuilds the OAM shadow before the frame wait: low table at
$0700, high table at $0900, split into three ranges that each object chooses with its
Obj_OamFlags bits 2–3. It visits objects bucket by bucket, last bucket first (Obj_DrawBucket, 64 buckets;
Obj_DrawLink puts an object in the bucket for its screen Y), calls Spr_AppendToOam for each, and parks every entry between
a range's new end and last frame's end off screen (Y = $E0). The NMI copies the whole shadow to
OAM. See How Sprite Init Works.
Other frame cycles
- Scene mode (bank
$C2).C2Scene_InstallInterruptspoints the trampolines atC2Scene_NmiHandler($C2:015E) and the one-byteC2Scene_IrqHandler(nothing in the scene code enables IRQs). Here the NMI does the frame's work itself: whenC2Scene_NmiFlagsbit 0 is set it uploads OAM, the palette (bit 1) and the PPU shadows, flushes the scene's VRAM queue, reads the joypad, then empties the sprite list, runs every task (C2Scene_TaskRunAll) and draws the list into the OAM shadow for the next NMI. The main loop only waits (C2Scene_WaitFrame: spin untilC2Scene_FrameCounterchanges) and dispatches on the scene mode. See Scene engine. - Menu (bank
$C2).Menu_Nmi($C2:840E) runs on its own stack with DP=$0D00; when the menu's threads have a frame ready it runs the upload (Menu_Unk85D6, not matched) and setsMenu_FrameReadyto$FF. The thread schedulerMenu_RunThreadswaits for that, runs each thread that is due until it yields, then clears the flag. Every NMI reads the pad and ticks the play-time clock (Menu_NmiPad).Menu_Irqis a bareRTI. See Menu. - Battle (bank
$C1).BattleSys_PumpFrames($C1:007E) setsBattle_FramePendingand calls a bank-$CD vector until the flag is cleared. Its header reads this as a frame wait, but the clearing code in bank$CFis not traced, so "wait for the frame" is inferred, not proven.