Wednesday, December 5, 2012

Daily SMB HL Disassembly Post #1

My overall goal remains to code a game(s) for the NES, but side projects happen I guess. While being sidetracked with implementing various HL stuff for ca65 I have been converting doppelganger's smbdis to ca65/HL. I plan to post some code daily until it is done. I hope to be able to explain everything that a routine is doing, including its subroutines, though this might not always happen. Right now the goal is a byte for byte match of the original game while using HL code macros. This is stage one. Stage two includes more organization ( assembler features and code changes ) and optimization of the game as much as possible while not affecting the logic or gameplay in anyway. While doing this some of the original comments have remained, some have been changed and some have been added as deemed necessary.

 So to begin, I'm going to start at the beginning: reset:

01 .proc reset 02 03 sei 04 cld 05 06 mb PPU_CTRL := #%00010000 07 ldx #$ff 08 txs 09 10 repeat 11 until lda PPU_STATUS == bit7 set 12 13 repeat 14 until lda PPU_STATUS == bit7 set 15 16 ldy #ColdBootOffset ; load default cold boot pointer 17 ldx #$05 ; this is where we check for a warm boot 18 19 repeat ; check each score digit in the top score 20 if TopScoreDisplay[ x ] >= #10 goto coldboot ; to see if we have a valid digit 21 until dex == negative 22 23 if WarmBootValidation = #$a5 ; second checkpoint, check to see if another location has a specific value 24 ldy #WarmBootOffset ; if passed both, load warm boot pointer 25 endif 26 27 coldboot: 28 29 jsr InitializeMemory ; clear memory using pointer in Y, depending on cold/warmboot 30 31 sta SND_DELTA_REG+1 ; reset delta counter load register , a := #0 after InitializeMemory 32 sta OperMode ; reset primary mode of operation 33 34 mb WarmBootValidation := #$a5 ; set warm boot flag with reg a 35 mb PseudoRandomBitReg := a ; set seed for pseudorandom register 36 mb SND_MASTERCTRL_REG := #%00001111 ; enable all sound channels except dmc 37 mb PPU_MASK := #%00000110 ; turn off clipping for OAM and background 38 39 jsr MoveAllSpritesOffscreen 40 jsr InitializeNameTables ; initialize both name tables 41 42 inc DisableScreenFlag ; set flag to disable screen output 43 44 mb a := Mirror_PPU_CTRL | #%10000000 ; enable NMIs 45 46 jsr WritePPU_CTRL ; write to CTRL port and to mirror 47 48 : jmp :- 49 .endproc

Not much to explain here, pretty straightforward. Small loop checks that the score could be valid and checks for an additional good value to keep highscores. SMB does everything in NMI, so reset just loops forever waiting for the next NMI. It also calls four subroutines, first is InitializeMemory:

01 .proc InitializeMemory 02 ; Clear ram, but skip the top of the stack and start with the value in reg y for page 7 03 ; If called by reset: 04 ; If warm boot, start at $07D7, which will leave the following memory alone: 05 ; 06 ; TopScoreDisplay,DisplayDigits,PlayerScoreDisplay, 07 ; ScoreAndCoinDisplay,GameTimerDisplay,WorldSelectEnableFlag 08 ; ContinueWorld,WarmBootValidation 09 ; 10 ; Otherwise y will start at $07fe (clear everything but WarmBootValidation ) 11 ; 12 ; also called by InitializeArea, and InitializeGame 13 ; 14 pointer = $06 15 16 ldx #$07 ; set initial high byte to $0700-$07ff 17 mb a, pointer := #0 ; set initial low byte to start of page 18 19 repeat 20 21 mb pointer[ 1 ] := x 22 repeat 23 if x <> #$01 || y < #$60 ; $0160-$01ff = do not clear (leave stack alone) 24 mb (pointer)[ y ] := a ; #0 25 endif 26 until y := y - 1 = #$FF ; do this all bytes in page have been erased (could be 'until negative') 27 28 until dex == negative ; do this until all pages of memory have been erased 29 rts 30 31 .endproc

It clears the RAM but will skip the top of the stack. As well it starts on page 7 with the value in reg y and counts down form there. Values to be saved are at the end of RAM. Next is MoveAllSpritesOffscreen. This is interesting because it uses the BIT opcode trick to skip a ldy immediate instruction if it is called to remove all sprites.

01 .proc MoveAllSpritesOffscreen 02 03 .export MoveSpritesOffscreen 04 05 ldy #$00 ; this routine moves all sprites off the screen 06 .byte $2c ; BIT instruction opcode - skip over next two bytes trick 07 08 MoveSpritesOffscreen: 09 ldy #$04 ; this routine moves all but sprite 0 10 lda #$f8 ; off the screen 11 12 repeat 13 mb Sprite[ y ]::Y_Position := a ; write 248 into OAM data's Y coordinate 14 until y := y + 4 == zero 15 rts 16 .endproc


Then, InitializeNameTables:

01 .proc InitializeNameTables 02 03 lda PPU_STATUS ; reset flip-flop 04 05 mb a := Mirror_PPU_CTRL | #%00010000 & #%11110000 ; set sprites for first 4k and 06 jsr WritePPU_CTRL ; background for second 4k, clear low half 07 08 lda #$24 ; set vram address to start of name table 1 09 jsr WriteNTAddr 10 lda #$20 ; and then set it to name table 0 11 12 WriteNTAddr: 13 14 sta PPU_ADDRESS 15 lda #$00 16 sta PPU_ADDRESS 17 18 ldx #$04 19 ldy #$c0 20 lda # ' ' ; clear name table with blank tile 21 repeat 22 repeat ; 960 , $3c0h needs to be cleared 23 sta PPU_DATA ; first loop is only $c0 (192) three more loops are 3 * 256 = 960 bytes exactly 24 until dey == zero 25 until dex == zero 26 27 ldy #64 ; now to clear the attribute table (with zero this time) 28 mb a := x ; x is zero 29 sta VRAM_Buffer1_Offset ; init vram buffer 1 offset 30 sta VRAM_Buffer1 ; init vram buffer 1 31 32 repeat 33 sta PPU_DATA 34 until dey == zero 35 36 sta HorizontalScroll ; reset scroll variables 37 sta VerticalScroll 38 jmp InitScroll ; initialize scroll registers to zero and rts 39 .endproc

Note that the blank tile here is defined as # ' ' which is mapped to $24 with the ca65 ".charmap" command. This routine simply loads that blank tile into both nametables and clears the attribute table. The only subroutine left is WritePPU_CTRL:

01 .proc WritePPU_CTRL 02 sta PPU_CTRL ; write contents of A to PPU register 1 03 sta Mirror_PPU_CTRL ; and its mirror 04 rts 05 .endproc

Pretty boring… Well, that's it for now, more code to come.

No comments:

Post a Comment