Friday, December 7, 2012

Daily SMB HL Disassembly Post #2

So daily is going to be maybe more like weekly or bi-weekly, not sure. But anyway:

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:

  1. (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.

  2. (17 - 19) Reset the scroll to zero.

  3. (21, 22) Perform Sprite DMA transfer.

  4. (26) Load the pointer into the temporary pointer, VRAM_Pointer, based on the value of VRAM_Buffer_AddrCtrl.

  5. (29) Jump to the VRAM update routine ( UpdateScreen) using the data at the VRAM_Pointer.

  6. (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.

  7. (42 - 46) Turn the screen back on if enabled previously. Jump to SoundEngine, ReadJoypads, PauseRoutine, and UpdateTopScore.

  8. ( 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.

  9. (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.

  10. (91 - 110) Do sprite 0 hit and scroll split (status bar is split from the gameplay area.)

  11. (112, 113) Sprite zero hit is done, so set scroll.

  12. (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.

  13. Turn on NMI again and return to busy loop in reset.

No comments:

Post a Comment