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.

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.

Sunday, October 28, 2012

HL macros technical

A more technical explanation of the HL macros I created, so others will feel more comfortable using them, as well as for myself in the event I forget how they work.

I'll be focusing mainly on the if macro due to the fact it was first, and the other macros are very similar.

To begin, somewhere in code the ca65 runs into the macro 'if'. From beginning to end, this is what the macro logic does:

1. Pass the entire parameter token list to the macro. There is only one parameter, commas will cause problems unless the parameter is enclosed in {}. (Example: Passing an indexed mnemonic)

2. Check for goto label on the end of the parameter. If there is a 'goto', strip it and the label from the parameter string and call the main if_macro with the label as the last macro parameter.
(The syntax goto can be changed to user preferred syntax - look for the line:
 ".define _USER_GOTO_DEFINITION goto")

3. In this macro, check if the first character is a bracket. If so, find the matching closing bracket. If the closing bracket is on the outside, strip the outside brackets.

4. Scan from left to right for the 1st matching OR symbol (user defined, default: || ). If found, split the parameters into everything to the left of the OR and everything right of the OR. Everything to the left is recursively expanded with recursive and leftsideOR set, everything to the right is recursively expanded with recursive (or possibly lastrecursive if no more OR/AND found) set. Setting leftsideOR signals that expansion that there are more branch(es) to follow and the logic to combine them is OR.

Setting recursive signals to the macro expansion that it should not end the branches (there is more to come)  and that it should not increment the current if block counter. If a goto label was set it is always passed in the recursive expansions.

5. If no more OR, test for AND. This works similarly to the OR, but sets leftsideAND, and results in logic that combines the following branch instruction with AND logic.

The macro code has no ability to properly handle more than one logical test to the left of an AND or OR due to how this works. It may be possible to make it work, but it would result in a much more complex macro and not nearly as efficient 6502 code in my opinion. As well OR logic must be first to make things easier for the macro and the 6502.

6. Scan for any more ANDs or ORs on the right of the last AND/OR. If none found, the last recursive call sets lastrecursive, which is only to signal to the macro to not increment the if counter. Everything else happens as if it was a regular simple if statement.

During any of this, regular brackets are honored and will only be scanned inside as described in step 3. This allows for placing an OR after an AND, ie if cond1 && (cond2 || cond3) will work properly, as well as recognized symbols to be passed to custom macro 'functions'.

After this step, it would be similar to coding all these if macros, line by line with the different parameters set to combine the logic. The tokens in between the OR and AND (the conditions to 'evaluate') symbols are passed to another macro expansion that checks for a valid flag to be tested and turned into a branch instruction. If it does not find a flag in the form of foo bar, where foo is the flag (C, Z, N, V, G) and bar is the default (set, clear) it will assume that it should be including the code from a macro, or some inline instructions:

It will attempt to create code from anything up to a + symbol. If this is an instruction or macro, it will be output, scan for another instruction or macro and output it until an equality ( == ) is found, or, if no equality symbol, a compatible macro sets the flags with set_flag_test 'command'.

Code explanation

I've been testing this a lot with Super Mario Brothers, and rewriting some of the disassembly for testing and to hopefully make it more readable, which results in some good examples:


;Original:

  lda OperMode           ;are we in victory mode?
  cmp #VictoryModeValue  ;if so, go ahead
  beq ChkPauseTimer
  cmp #GameModeValue     ;are we in game mode?
  bne ExitPause          ;if not, leave
  lda OperMode_Task      ;are we running game engine?
  cmp #$03
  bne ExitPause          ;if not, leave
  ChkPauseTimer:
   ;code block
ExitPause:

;HL if macro:

if (comp OperMode = #VictoryModeValue) || ((comp a = #GameModeValue)  && (comp OperMode_Task = #$03))                                                               
; code block
endif

This does produce the same code, so a closer look of how:

The first test is a macro that is called and expanded resulting in the assembly:

lda OperMode
cmp #VictoryModeValue
set_flag_test Z set

After that the Z set test is interpreted to the correct branch instruction due to the leftsideOR setting in the recursive macro call. A branch to the beginning of the code to be executed is created.

beq IF_CODE_BLOCK_START_LABEL_0001

So if this test is true, no other branches would need to be checked.
The next comparison macro expanded:


cmp #GameModeValue
set_flag_test Z set
This time, however, leftsideAND is set and because this test is now required to be true to continue, the flag is negated and a bne instruction to endif  is generated.


bne _END_IF_0001

If the test passed then there is one more, and this (because it is last) is treated like a regular if without any recursive restrictions except that the if counter is not incremented. (This also results in the creation of the  IF_CODE_BLOCK_START_LABEL_ ) This test is also required to pass to enter the code block, so the opposite condition is branched on:

; the macro is expanded:
lda OperMode_Task
cmp #$03
set_flag_test Z set

; the branch is negated:
bne _END_IF_0001


The complete code output would look like this:
  lda OperMode
  cmp #VictoryModeValue
  beq IF_CODE_BLOCK_START_LABEL_0001
  cmp #GameModeValue
  bne _END_IF_0001
  lda OperMode_Task     
  cmp #$03
  bne _END_IF_0001
  IF_CODE_BLOCK_START_LABEL_0001:

   ;code block

_END_IF_0001:
Of course, you never see this code, but this is what the macro is creating. I feel some low-level coders are afraid that higher levels constructs take away their control and perhaps produce poor code, so I also hope to show that this is not a true concern if you understand how the macro code is working, and how to avoid those pitfalls and make coding easier. The other looping macros are very similar.

If goto is used, the macro will behave very differently and not use any counters, and invert the test. Normally the test will jump to the label (_ENDIF_IF_) if it fails, but with goto the code will jump to the user defined label if it passes.

The macro will also create efficient long jumps (jmp) if set_long_branch + is set.

 
set_long_branch +

; If start not pressed, and start and A not pressed then:  
  if ( comp a <> #Start_Button )  && ( comp  a <> #( A_Button+Start_Button))  
 
  set_long_branch -

This results in:
  cmp #Start_Button
  beq _IF_ENDIF_JMP_
  cmp #A_Button+Start_Button
  bne IF_CODE_BLOCK_START_LABEL_0001

_IF_ENDIF_JMP_:    
  jmp _END_IF_0001             
IF_CODE_BLOCK_START_LABEL_0001:
Which is the same as Super Mario Brothers code:

  cmp #Start_Button
  beq StartGame
  cmp #A_Button+Start_Button  ;check to see if A + start pressed
  bne ChkSelect               ;if not, check select
StartGame:    
  jmp ChkContinue             ;if start or A + start, execute
ChkSelect:    
Update: As well I'd like to demonstrate what is possible with the inline code as well: 
Using the previous example:
if (comp OperMode = #VictoryModeValue) || ((comp a = #GameModeValue)  && (comp OperMode_Task = #$03))     
This can also be written as:
if (lda OperMode + cmp #VictoryModeValue == equal) || ((cmp #GameModeValue == equal)  && (lda OperMode_Task + cmp #$03 ==  equal))     
If you don't feel like writing a macro or you want more control over what is happening in a specific case.

For now that's it, I don't know if I am going to write up too many more macros like this, possibly a switch statement if I find I need it, or possibly an elseif to use with the if-endif.

Monday, October 22, 2012

ca65 HL Macros updated again

Another, but big update. Just posting some quick info for now, perhaps better documentation to come.

Quick summary of features:

Basic flag test syntax:

if (flag condition) 
   ;code
endif

Flag refers to 6502 CPU status flags, Z, C, N , V as well as an added flag I called G for greater or less or equal tests. the text inside the brackets above can be any of the flags plus 'set' or 'clear' (required). This is the simplest form of the macro. Example: C set would result in true if the carry flag was set after an instruction:

ror a
if C set
; do code
endif
You can add readability by creating or using the .define macros in the source file, so you could use 'zero' for example, and ca65 will substitute Z set.

Macro 'Function' Calling

The next feature I added was the ability for the expression evaluation to try and call a macro if it didn't recognize the flag to be tested. You can name any macro as a condition to be tested as long as the macro ends with 'set_flag_test foo', where foo is the flag to test:

.macro padpressed buttons
   lda _pads_new_pressed
   and #(buttons)
   set_flag_test Z clear
.endmacro

;elsewhere:

if padpressed BUTTON_A
; do stuff
endif

AND and OR Evaluation

Now there is also and and or support. There are some rules due to the way the macro code and 6502 work: - All ORs must be before ANDs in the same bracket level - There cannot be more than one test on the left side of an AND or OR in a bracket. For example:  

If (zero and carry) or minus 

The test on the left of the OR will not assemble. - You can place multiple test in brackets on the right of an AND or OR:     

If minus or (zero and carry) 

This is okay.

The negate symbol (! or not by default) currently will only work for one test, not a set of brackets. All tests are short circuited for the best possible code. An example from Super Mario Brothers:

if (comp OperMode = #VictoryModeValue) || ((comp a = #GameModeValue)  && (comp OperMode_Task = #$03))                                                               
; code block
endif

;original

  lda OperMode           ;are we in victory mode?
  cmp #VictoryModeValue  ;if so, go ahead
  beq ChkPauseTimer
  cmp #GameModeValue     ;are we in game mode?
  bne ExitPause          ;if not, leave
  lda OperMode_Task      ;are we running game engine?
  cmp #$03
  bne ExitPause          ;if not, leave
  ChkPauseTimer:
   ;code block
ExitPause:

The code in the if structure assembles to the exact same machine code as the block below it. Just for the sake of understanding what is required of the syntax, please note that this would work as well:

if comp OperMode = #VictoryModeValue || comp a = #GameModeValue  && comp OperMode_Task = #$03                                                               

The brackets do not matter in this case, but they can be useful in the case of having an and before an or:


if comp a = #GameModeValue  && (comp OperMode_Task = #$03 || comp OperMode_Task = #$04)
As well , if you needed to pass an and or or symbol to a macro you could do so with brackets. Brackets are only processed by the macro code if necessary. It will look inside a bracket pair if that is all there is (outside brackets over the entire expression) or if brackets are the next thing after an and or or. But if the brackets are part of an argument to a macro they will be left alone. Example:

 if scomp #(<-5) < xcoord
Long Jumps

There will come a time when a long branch is needed. This can be turned on with set_long_branch:

set_long_branch + ; turn on long branching
set_long_branch - ; turn off long branching
set_long_branch +,- ; turn on long branching turn off warning messages
All branching code will use a jmp opcode when long branching is turned on. If the macro is able to branch without a long jump (sometimes not 100% accurate at this point) it will output an error message unless messages are turned off as shown above.

Other Features

Besides the standard IF-ENDIF shown above, there are a number of other supported syntaxes and features:

if-else-endif

Use a else with the if-endif block.

if - goto label

Use an if with a conditional expression ending with a goto label to jump to that label, where label is a valid label in your code, rather than creating an if-endif block.

do-while

A do while code block. Code between the do and the while will be executed until the expression after the while is false.

repeat - until

This is the same as the do-while block but the conditions are reversed - repeat until the expression is true.
Note: The keywords do and repeat are exactly the same.

while-do-endwhile

The while keyword starts this code block. To differentiate between this and do-while, the while must end in do.. example:

  while not buttonpressed BUTTON_A do
    jsr read_pad
  endwhile

Inline code:

Sometimes it may be clearer to read code if it is included as part of the conditional code. For example:

  do
    lda Timers,x
    if not zero
      dec Timers,x
    endif
    dex
  until negative
This isn't too hard too read, but consider:

  do
    lda Timers,x
    if not zero
      dec Timers,x
    endif
  until dex == negative
This, to me, is even more readable. This also allows for some flexible coding solutions:

; orignal example:

  lda TimerControl
  beq DecTimers    
  dec TimerControl
  bne NoDecTimers
  DecTimers:
  ;code block
  NoDecTimers:

  
This is pretty difficult to change into an if structure, but:

  if (lda TimerControl == zero ) || (dec TimerControl  == zero)
  ;code block
  endif
..does the exact same thing. The use of the == indicates the end of code and the start of what flag is going to be used to determine the branch. Assembly commands can be separated by + and you can mix in macros as well, or end the list of commands with a macro if it sets set_flag_test.

 For now, a block of example code from SMB:

  lda GamePauseStatus
  lsr
  if carry clear
    if (lda TimerControl == zero ) || (dec TimerControl  == zero)
      ldx #$14
      dec IntervalTimerControl

      if negative
        lda #$14
        sta IntervalTimerControl
        ldx #$23
      endif

      do
        lda Timers,x
        if not zero
          dec Timers,x
        endif
      until dex == negative

    endif
    inc FrameCounter
  endif

continued...

ca65 reads commas as macro parameter seperators. An example of where curly braces are needed:


  do
    if {lda Timers,x == not zero}
      dec Timers,x
    endif
  until dex == negative

;original code:

DecTimersLoop: 
  lda Timers,x              ;check current timer
  beq SkipExpTimer          ;if current timer expired, branch
  dec Timers,x              ;otherwise decrement the current timer
SkipExpTimer:  
  dex                       ;move onto next timer
  bpl DecTimersLoop  



Code here: http://pastebin.com/azMMvh4r updated 1 time since this post was first created.

Sunday, October 21, 2012

ca65 Tokens revisited.

Quick note on ca65 tokens. The source file with the enumeration I posted here is pretty helpful, but I noticed that ca65 will actually recognize C style syntax boolean and/or ( && and || ) as a single token, which isn't obvious at all from that source listing. I checked the source and in scanner.c you can see what string data is actually matched to what. Note: &, &&, |, || are different tokens, and && is actually the exact same thing as .and, likewise || is the same as .or (they match the exact same internal token value). As well I found it interesting that '==' is not a token - it will be scanned as two equal tokens, so macro code must be exended to match only '=='.

Friday, October 12, 2012

CA65 Highlevel Macro Updated

I've added a cleaner method for adding functions to the IF/WHILE statements for better readability.
The idea is for a user of the macros to write a simple macro that results in some flag being set (in the CPU) and then set that flag so the conditional check knows what code to generate. You can achieve some nice things with it, and as an example and for my own use I've written both a signed and unsigned comparison function.

First, how to make a function - a simple example:
     

.macro reg_is_negative reg
  .if .xmatch(reg,a)
    pha
    pla
 .elseif .xmatch(reg,x)
    txa
 .elseif .xmatch(reg,y)
    tya
 .else
   .error "no register"
 .endif
  set_flag_test N set
.endmacro
You can do anything you want like a normal macro, just add set_flag_test followed by the flag you want to test. You can still use all the .defines as well form the first post on this: http://mynesdev.blogspot.ca/2012/09/ca65-highlevel-macros.html So in this example you could:
if reg_is_negative a
;code
endif
The if macro "knows" to check for the status of the N flag, where N set means TRUE.

Macros comp and scomp

 Both of these macros accept the same values and can evaluate anything on either side of one comparison operator except for a comparison of two registers (they can only evaluate one comparison, no and or or). If using a greater or less compare with a low byte or high byte operator, you must enclose the operator in brackets: Use would be like this:
if scomp a >= #(<-29)
; code
endif
There is not much more to it, operators supported are the standard <, >, <=, >=, =, <>. You can also use not at the beginning as so:
if not scomp a >= #(<-29)
; code
endif
Another example..
if comp my_var1 >= #29
; code
endif
Full example code here: https://app.dumptruck.goldenfrog.com/p/b9UI5-8_ck

Monday, October 1, 2012

Assignment macro!

Quick macro for CA65 easy to read assignment, using variable labels or actual values. wb or ww for writebyte or writeword:

; assign immediate to a variable:

wb my_var := #23

; copy variable value to another:

wb my_var := my_other_var

; write to memory:

wb $2007 := #20

; write word:

ww score := #$1234

; copy word sized variable:

ww highscore := score

; assign value of register example ( a x or y)

wb p1score := #0
wb p2score := a
wb highscore := a


Code here: (updated..x1)


.macro findequal exp
.if .xmatch(.mid(_equalpos_,1,exp),:=)
.exitmacro
.else
_equalpos_ .set _equalpos_ + 1
.if (_equalpos_ >= .tcount(exp))
.exitmacro
.endif
findequal exp
.endif
.endmacro

.macro wb exp
_equalpos_ .set 1 ; first check at token 1
findequal exp
.if (_equalpos_ >= .tcount(exp))
.error "No assignment in poke macro"
.exitmacro
.endif

.if .xmatch(.mid((_equalpos_+1), .tcount({exp}) - _equalpos_  - 1 , {exp}),x) ; assign reg x
stx .mid(0,_equalpos_,{exp})
.elseif .xmatch(.mid((_equalpos_+1), .tcount({exp}) - _equalpos_  - 1 , {exp}),y) ; assign reg y
sty .mid(0,_equalpos_,{exp})
.elseif .xmatch(.mid((_equalpos_+1), .tcount({exp}) - _equalpos_  - 1 , {exp}),a) ; assign reg a
sta .mid(0,_equalpos_,{exp})
.else
lda  .mid((_equalpos_+1), .tcount({exp}) - _equalpos_  - 1 , {exp})
sta .mid(0,_equalpos_,{exp})
.endif


.endmacro

.macro ww exp
_equalpos_ .set 1 ; first check at token 1
findequal exp
.if (_equalpos_ >= .tcount(exp))
.error "No assignment in poke macro"
.exitmacro
.endif

.if  .xmatch(.mid((_equalpos_+1),1, {exp}), #) ; immeidate mode

lda  #<(.mid((_equalpos_+2), .tcount({exp}) - _equalpos_  - 2 , {exp}))
sta .mid(0,_equalpos_,{exp})
.if (>(.mid((_equalpos_+2), .tcount({exp}) - _equalpos_  - 2 , {exp}))) <> (<(.mid((_equalpos_+2), .tcount({exp}) - _equalpos_  - 2 , {exp}))) ; high byte not equal to low byte
lda  #>(.mid((_equalpos_+2), .tcount({exp}) - _equalpos_  - 2 , {exp}))
.endif
sta .mid(0,_equalpos_,{exp}) + 1

.else

lda  .mid((_equalpos_+1), .tcount({exp}) - _equalpos_  - 1 , {exp})
sta .mid(0,_equalpos_,{exp})
lda  .mid((_equalpos_+1), .tcount({exp}) - _equalpos_  - 1 , {exp})+1
sta .mid(0,_equalpos_,{exp}) + 1

.endif


.endmacro