Super Mario Bros. NMI is pretty straightforward, if I get some details incorrect, please feel free to comment. Throughout the code of Super Mario Bros, relevant data is often in ROM just before the corresponding code block, which I have been marking as data ( with a macro that can disable the segment command - disabled will allow matching against the original game.)
This data block is made of high/low bytes of pointers that point to various data buffers to be copied to the PPU VRAM. Often this will be a VRAM_Buffer of which there are two (I'm unsure as to why there are two, it seems it would work with one) but the VRAM_Buffer_AddrCtrl can also be set to trigger a palette update, or to one the text messages for the end of the castles.
The value VRAM_Buffer_AddrCtrl is the number of the buffer to send to the PPU from $0 to $12 (0 to 18).
01 VRAM_AddrTable_Low:
02
03 .lobytes VRAM_Buffer1, WaterPaletteData, GroundPaletteData
04 .lobytes UndergroundPaletteData, CastlePaletteData, VRAM_Buffer1_Offset
05 .lobytes VRAM_Buffer2, VRAM_Buffer2, BowserPaletteData
06 .lobytes DaySnowPaletteData, NightSnowPaletteData, MushroomPaletteData
07
08 ; Thanks messages:
09 .lobytes MarioThanksMessage, LuigiThanksMessage
10
11 ; World 1 - 7 message:
12 .lobytes MushroomRetainerSaved
13
14 ; World 8 messages:
15 .lobytes PrincessSaved1, PrincessSaved2, WorldSelectMessage1
16 .lobytes WorldSelectMessage2
17
18 VRAM_AddrTable_High:
19
20 .hibytes VRAM_Buffer1, WaterPaletteData, GroundPaletteData
21 .hibytes UndergroundPaletteData, CastlePaletteData, VRAM_Buffer1_Offset
22 .hibytes VRAM_Buffer2, VRAM_Buffer2, BowserPaletteData
23 .hibytes DaySnowPaletteData, NightSnowPaletteData, MushroomPaletteData
24 .hibytes MarioThanksMessage, LuigiThanksMessage, MushroomRetainerSaved
25 .hibytes PrincessSaved1, PrincessSaved2, WorldSelectMessage1
26 .hibytes WorldSelectMessage2
27
28 VRAM_Buffer_Offset:
29 .byte <VRAM_Buffer1_Offset, <VRAM_Buffer2_Offset
The NMI routine:
001 .proc NonMaskableInterrupt
002
003 VRAM_Pointer = temp_byte ; shared memory location $00
004
005 mb Mirror_PPU_CTRL := Mirror_PPU_CTRL & #%01111111 ; disable NMIs in mirror reg, save all other bits
006 mb PPU_CTRL := a & #%01111110 ; alter name table address to be $2800, ($2000), save other bits
007 mb a := Mirror_PPU_MASK & #%11100110 ; disable OAM and background display by default
008
009 if y := DisableScreenFlag == zero ; if not set:
010 mb a := Mirror_PPU_MASK | #%00011110 ; reenable bits and save them
011 endif
012
013 mb Mirror_PPU_MASK := a ; save bits for later but not in register at the moment
014
015 mb PPU_MASK := a & #%11100111 ; disable screen for now
016
017 ldx PPU_STATUS ; reset flip-flop and reset scroll registers to zero
018 lda #$00
019 jsr InitScroll
020 ; reg a still 0
021 sta PPU_SPR_ADDR ; reset spr-ram address register
022 mb SPR_DMA := #$02 ; perform spr-ram DMA access on $0200-$02ff
023
024 ldx VRAM_Buffer_AddrCtrl ; load control for pointer to buffer contents
025
026 mb VRAM_Pointer[ 0 ] := VRAM_AddrTable_Low[ x ] ; set indirect at temp_byte to pointer
027 mb VRAM_Pointer[ 1 ] := VRAM_AddrTable_High[ x ]
028
029 jsr UpdateScreen ; update screen with buffer contents
030 ldy #$00
031
032 if x := VRAM_Buffer_AddrCtrl = #$06 ; check for usage of VRAM_Buffer2
033 iny ; get offset based on usage
034 endif
035
036 mb x := VRAM_Buffer_Offset[ y ]
037 lda #$00 ; clear buffer header at last location
038 sta VRAM_Buffer1_Offset,x
039 sta VRAM_Buffer1,x
040 sta VRAM_Buffer_AddrCtrl ; reinit address control to VRAM_Buffer1
041
042 mb PPU_MASK := Mirror_PPU_MASK ; copy mirror of $2001 to register
043 jsr SoundEngine ; play sound
044 jsr ReadJoypads ; read joypads
045 jsr PauseRoutine ; handle pause
046 jsr UpdateTopScore
047
048 if GamePauseStatus >> 1 == carry clear ; check for pause status
049
050 ; if TimerControl is zero do timers, OR decrement it and do timers if now zero
051
052 if ( a := TimerControl == zero ) || ( dec TimerControl == zero)
053
054 mb x := #$14 ; load end offset for end of frame timers
055
056 ; decrement interval timer control,
057 ; if expired, interval timers will decrement
058 ; along with frame timers
059
060 if dec IntervalTimerControl == negative
061 mb IntervalTimerControl := #$14
062 mb x := #$23
063 endif
064
065 repeat ; check current timer
066 if a := Timers[ x ] == not zero ; if current timer still valid:
067 dec Timers,x ; decrement the current timer
068 endif ; move onto next timer - one less than zero..
069 until dex == negative ; loop will go from $23 or $14 to $0
070 endif
071 inc FrameCounter ; increment frame counter
072 endif
073
074 ldx #$00
075 ldy #$07
076
077 mb temp_byte := PseudoRandomBitReg & #%00000010 ; get first memory location of LSFR bytes, mask out all but d1
078 ; perform exclusive-OR on d1 from first and second bytes
079 mb a := PseudoRandomBitReg[ 1 ] & #%00000010 ^ temp_byte
080
081 clc ; if neither or both are set, carry will be clear
082 if zero clear
083 sec ; if one or the other is set, carry will be set
084 endif
085
086 repeat
087 ror PseudoRandomBitReg,x ; rotate carry into d7, and rotate last bit into carry
088 inx ; increment to next byte
089 until y - 1 == zero
090
091 if a := Sprite0HitDetectFlag == not zero
092
093 repeat
094 mb a := PPU_STATUS & #%01000000 ; wait for sprite 0 flag to clear
095 until zero
096
097 if GamePauseStatus >> 1 == carry clear ; if not in pause, do sprite stuff
098 jsr MoveSpritesOffscreen
099 jsr SpriteShuffler
100 endif
101
102 do
103 mb a := PPU_STATUS & #%01000000 ; do sprite #0 hit detection
104 while zero
105
106 ldy #$14 ; small delay, to wait until we hit horizontal blank time
107 repeat
108 until dey == zero
109
110 endif
111
112 mb PPU_SCROLL := HorizontalScroll ; set scroll registers from variables
113 mb PPU_SCROLL := VerticalScroll
114
115 lda Mirror_PPU_CTRL ; load saved mirror of $2000
116 pha ; keep it safe in the stack
117 sta PPU_CTRL
118
119 if GamePauseStatus >> 1 == carry clear
120 jsr OperModeExecutionTree ; if not in pause mode do one of many, many possible subroutines
121 endif
122
123 lda PPU_STATUS ; reset flip-flop
124
125 pla
126 mb PPU_CTRL := a | #%10000000 ; reactivate NMIs
127 rti ; we are done until the next frame!
128 .endproc
Summary:
- (5 - 11) First, turn off NMI and rendering. Check if DisableScreenFlag is set. If not, in the mirrored register, set values for clipping and turn on sprites and background.
- (17 - 19) Reset the scroll to zero.
- (21, 22) Perform Sprite DMA transfer.
- (26) Load the pointer into the temporary pointer, VRAM_Pointer, based on the value of VRAM_Buffer_AddrCtrl.
- (29) Jump to the VRAM update routine ( UpdateScreen) using the data at the VRAM_Pointer.
- (32 - 40) There are two dynamic buffers. If the buffer in use is VRAM_Buffer2, VRAM_Buffer_AddrCtrl will be equal to $06, so increment register y.
Then use the data at VRAM_Buffer_Offset index by y to find the offset to use to clear either the beginning of VRAM_Buffer1 or VRAM_Buffer2 and reset VRAM_Buffer_AddrCtrl to zero as well.
- (42 - 46) Turn the screen back on if enabled previously. Jump to SoundEngine, ReadJoypads, PauseRoutine, and UpdateTopScore.
- ( 48 - 72 ) Timers. This is a bit to explain but is is not over complex. After checking that we are not in pause, check if TimerControl is clear. If so continue into the timer section.
This code checks for TimerControl at zero. If not it is decremented and checked again. If it is still not zero all timers are left alone. This essentially pauses most of the game action. This is used for things like Mario powering up (mushroom or fireflower), or shrinking. When an animation is complete it sets TimerControl back to zero to allow normal timer countdown.
If TimerControl is zero (not set) then the timer code runs. First IntervalTimerControl is decremented. If it is at zero it is reset to $14 (20), and register x is set to $23 rather than the default $14. This means the loop following decrements all the timers. If IntervalTimerControl is not clear only the first $14 timers count down. This means the last group of timers only count down once every 20 frames.
The FrameCounter is also incremented here if not in pause mode.
- (77 - 89) Do the LFSR algorithm (Linear Feedback Shift Register):
Basically:
c := (PseudoRandomBitReg[0] AND 2) ^ (PseudoRandomBitReg[1] AND 2)
Rotate c into bit 7 of PseudoRandomBitReg[0] and rotate that into PseudoRandomBitReg[1]. This is essentially 16 bit, not sure why there are so many PseudoRandomBitReg slots.
- (91 - 110) Do sprite 0 hit and scroll split (status bar is split from the gameplay area.)
- (112, 113) Sprite zero hit is done, so set scroll.
- (115, 121) Do some stuff with PPU_CTRL and save a copy on the stack (paranoid?) and Jump to the game engine if we are not in pause.
- Turn on NMI again and return to busy loop in reset.
No comments:
Post a Comment