Saturday, February 4, 2023

Structured ca65hl Macro Code: Option to Output to Standard Assembly Code.

Clock Test

As a method of troubleshooting, or if there was a need to export code to standard assembly langauge, the customSyntax and ca65hl macros now support a simple method of printing the assembly code that the macros generate to the console. It is possible to coax ca65 to print to a file, but this would require a custom linker config and more macro code, and for the limited use this might see, I plan to leave it as is for now. This feature has been added to https://github.com/Movax12/ca65hl. To understand how to use this feature, please see the following example, which is using code from the previous post.


The code enclosed by the

ca65hl_Listing on
and
ca65hl_Listing off
will be printed to the console as standard assembly code.

; -------------------------------------------------------------------------------------------- ; File: mainGameLoopState ; -------------------------------------------------------------------------------------------- ; this State is one level above rootState ; -------------------------------------------------------------------------------------------- ; required headers: .include "config.h" .include "header/memory.h" .include "header/funcCall.h" .include "header/ca65hl.h" ; -------------------------------------------------------------------------------------------- ; Define State: identifyState "mainGameLoopState" loadState "mainGameLoopState" ; -------------------------------------------------------------------------------------------- ; Constants for State: ; -------------------------------------------------------------------------------------------- ; Headers for State: ; standard library .include "header/string.h" .include "header/nmi.h" .include "header/controller.h" .include "header/condes.h" .include "header/nes_constants.h" .include "header/ppu.h" .include "header/clock.h" ; -------------------------------------------------------------------------------------------- ; Code segment for State: .segment "CODE" ; -------------------------------------------------------------------------------------------- ; global memory settings: resBSS clockRunning .byte ; -------------------------------------------------------------------------------------------- ; Imports: ; -------------------------------------------------------------------------------------------- ; Exports: declarefunc nmiEnd ; -------------------------------------------------------------------------------------------- ; States: declarefunc mainGameLoopState ; -------------------------------------------------------------------------------------------- ; Forward Declarations: .pushseg .segment "RODATA" palette: .byte $01,$36,$05,$16,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F cursorPos: .byte 0, 3, 6, 9 .popseg func mainGameLoopState resBSS timeStr .byte 4 ; 3 byte string plus terminator resBSS cursor .byte ; position as 0 to 3 resBSS i .byte ; loop counter mb cursor = #0 mb clockRunning = #1 call nmi::waitForNMI ; wait for vblank to write palette call ppu::setBackgroundPalette, { &palette } jPrintRenderMode off jPrintSetMaxCol 32 jPrintSetCursor 9, 6 jPrint "Clock Test!" jPrint jPrint jPrint jPrint "00:00:00:00" jPrintSetCursor 1, 14 jPrint "Press START to Pause" jPrint "Press UP/DOWN to change value" jPrint "Press A to reset value" jPrintEnd PPU_RENDER_ON ca65hl_Listing on, "Main Loop" repeat ; draw the time: ; i = 0 -> hours ; i = 1 -> minutes ; i = 2 -> seconds ; i = 3 -> hundredths for ( mb i := #3, !N, dec i ) ; i: 3 to 0 ldx i call str::byteToDecimal, { clock::time[ x ], &timeStr } ldx i : mb a = cursorPos[ x ] + #9 call str::print, { a , #10, &(timeStr + 1) } ; only print the last 2 digits next ; draw cursor jPrintRenderMode on jPrintSetCursor 10, 11 jPrint " " ; create a blank buffer jPrintEnd ldy cursor txa ; x holds vramBufferIndex mb x = a + cursorPos[ y ] mb ppu::vramBuffer[ x ] = #$1F ; arrow symbol ; process input: lda controller::pressedNew : tay ; store a copy of buttons in y to quickly restore a ldx cursor if ( !zero && a & #BUTTON_LEFT ) dex elseif ( tya : a & #BUTTON_RIGHT && x < #3 ) inx elseif ( tya : a & #BUTTON_START ), !Z inc clockRunning elseif ( tya : a & #BUTTON_A ) mb clock::time[ x ] = #0 elseif ( tya : a & #BUTTON_UP && clock::time[ x ] <> #59 ), Z inc clock::time[ x ] elseif ( tya : a & #BUTTON_DOWN && clock::time[ x ] ), !Z dec clock::time[ x ] endif stx cursor call nmi::waitForNMI forever ca65hl_Listing off endfunc ; -------------------------------------------------------------------------------------------- func nmiEnd call controller::readPort, { #0 } call controller::autoRepeatButtons, { #0, #$FF } ; allow all to auto repeat if ( clockRunning & #1 ) call clock::clockTick endif rts endfunc ; --------------------------------------------------------------------------------------------

The output captured from the console:

; Main Loop DO_WHILE_LOOP_LABEL_0000: lda #$03 sta i FOR_STATEMENT_LABEL_0000: ldx i lda clock::time, x ldx #<timeStr ldy #>timeStr jsr str::byteToDecimal ldx i lda cursorPos, x clc adc #$09 sta ___functionMacro::parameterAddress lda #$0A ldx #<( timeStr + $01) ldy #>( timeStr + $01) jsr str::print dec i bpl FOR_STATEMENT_LABEL_0000 lda #<( jPrintStringData_01 ) ldx #>( jPrintStringData_01 ) ldy #jusPrint::bufferByteCount jsr ppu::vramBufferPutBlock ldy cursor txa clc adc cursorPos, y tax lda #$1F sta ppu::vramBuffer, x lda controller::pressedNew tay ldx cursor beq IF_STATEMENT_0001_ENDIF_LABEL and #BUTTON_LEFT beq IF_STATEMENT_0001_ENDIF_LABEL dex jmp IF_STATEMENT_0001_ELSE_ENDIF_LABEL IF_STATEMENT_0001_ENDIF_LABEL: tya and #BUTTON_RIGHT beq IF_STATEMENT_0001_ELSEIF_LABEL_0000 cpx #$03 bcs IF_STATEMENT_0001_ELSEIF_LABEL_0000 inx bne IF_STATEMENT_0001_ELSE_ENDIF_LABEL IF_STATEMENT_0001_ELSEIF_LABEL_0000: tya and #BUTTON_START beq IF_STATEMENT_0001_ELSEIF_LABEL_0001 inc clockRunning jmp IF_STATEMENT_0001_ELSE_ENDIF_LABEL IF_STATEMENT_0001_ELSEIF_LABEL_0001: tya and #BUTTON_A beq IF_STATEMENT_0001_ELSEIF_LABEL_0002 lda #$00 sta clock::time, x beq IF_STATEMENT_0001_ELSE_ENDIF_LABEL IF_STATEMENT_0001_ELSEIF_LABEL_0002: tya and #BUTTON_UP beq IF_STATEMENT_0001_ELSEIF_LABEL_0003 lda clock::time, x cmp #$3B beq IF_STATEMENT_0001_ELSEIF_LABEL_0003 inc clock::time, x bne IF_STATEMENT_0001_ELSE_ENDIF_LABEL IF_STATEMENT_0001_ELSEIF_LABEL_0003: tya and #BUTTON_DOWN beq IF_STATEMENT_0001_ELSEIF_LABEL_0004 lda clock::time, x beq IF_STATEMENT_0001_ELSEIF_LABEL_0004 dec clock::time, x IF_STATEMENT_0001_ELSE_ENDIF_LABEL: IF_STATEMENT_0001_ELSEIF_LABEL_0004: stx cursor jsr nmi::waitForNMI jmp DO_WHILE_LOOP_LABEL_0000

The output is able to capture all labels generated by the ca65hl macros, as well as all instructions processed by the assembler. It will not capture user labels or data (such as values generated by the .byte statement).

Sunday, January 8, 2023

Clock Library Test

Clock Test

Test program for my clock library. The library code runs a real time clock that adjusts for the NES's ~60.1 frames/second framerate. A clock can be updated once per frame to count about 1/60th of a second, but for a more accurate clock, consider the following:

	60.098811862348404716732985230828 frames/sec
	= 0.01663926405550947066275456212502 seconds/frame
	= 1.663926405550947066275456212502 hundredths seconds/frame
	fractional part = 0.663926405550947066275456212502
	fractional part of 65536 ($10000) = 0.663926405550947066275456212502 * 65536
	= 43,511.080914186866935428298342531
	drop the decimal part: = 43,511

The clock routine is as follows:

FRACTIONAL_CONSTANT = 43511 ; -------------------------------------------------------------------------------------------- ; execute every NMI for a real time clock counter func clockTick mb fractional[ 0 ] = fractional[ 0 ] + #<FRACTIONAL_CONSTANT mb fractional[ 1 ] = fractional[ 1 ] +c #>FRACTIONAL_CONSTANT ; add 1 to hundredths every frame plus any carry: mb hundredths = hundredths +c #1 ; a still holds hundredths if ( a >= #100 ) mb hundredths = a -c #100 ; carry will be set inc seconds if ( lda seconds = #60 ) ldx #0 stx seconds inc minutes if ( lda minutes = #60 ) stx minutes inc hours endif endif endif rts endfunc

The main routine:

; -------------------------------------------------------------------------------------------- ; File: mainGameLoopState ; -------------------------------------------------------------------------------------------- ; this State is one level above rootState ; -------------------------------------------------------------------------------------------- ; required headers: .include "config.h" .include "header/memory.h" .include "header/funcCall.h" .include "header/ca65hl.h" ; -------------------------------------------------------------------------------------------- ; Define State: identifyState "mainGameLoopState" loadState "mainGameLoopState" ; -------------------------------------------------------------------------------------------- ; Constants for State: ; -------------------------------------------------------------------------------------------- ; Headers for State: ; standard library .include "header/string.h" .include "header/nmi.h" .include "header/controller.h" .include "header/condes.h" .include "header/nes_constants.h" .include "header/ppu.h" .include "header/clock.h" ; -------------------------------------------------------------------------------------------- ; Code segment for State: .segment "CODE" ; -------------------------------------------------------------------------------------------- ; global memory settings: resBSS clockRunning .byte ; -------------------------------------------------------------------------------------------- ; Imports: ; -------------------------------------------------------------------------------------------- ; Exports: declarefunc nmiEnd ; -------------------------------------------------------------------------------------------- ; States: declarefunc mainGameLoopState ; -------------------------------------------------------------------------------------------- ; Forward Declarations: .pushseg .segment "RODATA" palette: .byte $01,$36,$05,$16,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F cursorPos: .byte 0, 3, 6, 9 .popseg func mainGameLoopState resBSS timeStr .byte 4 ; 3 byte string plus terminator resBSS cursor .byte ; position as 0 to 3 resBSS i .byte ; loop counter mb cursor = #0 mb clockRunning = #1 call nmi::waitForNMI ; wait for vblank to write palette call ppu::setBackgroundPalette, { &palette } jPrintRenderMode off jPrintSetMaxCol 32 jPrintSetCursor 9, 6 jPrint "Clock Test!" jPrint jPrint jPrint jPrint "00:00:00:00" jPrintSetCursor 1, 14 jPrint "Press START to Pause" jPrint "Press UP/DOWN to change value" jPrint "Press A to reset value" jPrintEnd PPU_RENDER_ON repeat ; draw the time: ; i = 0 -> hours ; i = 1 -> minutes ; i = 2 -> seconds ; i = 3 -> hundredths for ( mb i := #3, !N, dec i ) ; i: 3 to 0 ldx i call str::byteToDecimal, { clock::time[ x ], &timeStr } ldx i : mb a = cursorPos[ x ] + #9 call str::print, { a , #10, &(timeStr + 1) } ; only print the last 2 digits next ; draw cursor jPrintRenderMode on jPrintSetCursor 10, 11 jPrint " " ; create a blank buffer jPrintEnd ldy cursor txa ; x holds vramBufferIndex mb x = a + cursorPos[ y ] mb ppu::vramBuffer[ x ] = #$1F ; arrow symbol ; process input: lda controller::pressedNew : tay ; store a copy of buttons in y to quickly restore a ldx cursor if ( !zero && a & #BUTTON_LEFT ) dex elseif ( tya : a & #BUTTON_RIGHT && x < #3 ) inx elseif ( tya : a & #BUTTON_START ), !Z inc clockRunning elseif ( tya : a & #BUTTON_A ) mb clock::time[ x ] = #0 elseif ( tya : a & #BUTTON_UP && clock::time[ x ] <> #59 ), Z inc clock::time[ x ] elseif ( tya : a & #BUTTON_DOWN && clock::time[ x ] ), !Z dec clock::time[ x ] endif stx cursor call nmi::waitForNMI forever endfunc ; -------------------------------------------------------------------------------------------- func nmiEnd call controller::readPort, { #0 } call controller::autoRepeatButtons, { #0, #$FF } ; allow all to auto repeat if ( clockRunning & #1 ) call clock::clockTick endif rts endfunc ; --------------------------------------------------------------------------------------------

Get the ROM here.

Wednesday, May 27, 2015

How to Add a Function to the Ca65 Assembler


I've been extending ca65 to do a bit more. See my branch here: https://github.com/Movax12/cc65

For my own use I decided I should post some quick information on how to add a custom pseudo function to ca65. Adding a new control command is a bit different, but very similar - I may document that too at a later time.

For now, how to add a function:

You are going to need to edit the following files from the source:

token.h
scanner.c
pseudo.c
expr.c

First, think of a name for you function. I'm going to add the ".ismacro()" function. This function will check if an identifier is a macro or not, and return 1 if true, 0 if false.

First, let's add the token. Open "token.h"

Find the line in the source (around line 55):
typedef enum token_t {

Then find the comment:
 
    /* The next ones are tokens for the pseudo instructions. Keep together! */

Then we'll add the token for our function to the list, between TOK_INTERRUPTOR and TOK_LEFT (The list is in alphabetic order.)
...
TOK_INTERRUPTOR,
TOK_ISMACRO,
TOK_LEFT,
...

Save "token.h"

Now we have to associate that token with a 'dot keyword' to match the user source file to the token.
Open "scanner.c".

Find the array in the source (around line 134):
DotKeywords [] = {


And find a suitable spot to insert our new function:
...
{ ".INTERRUPTOR",   TOK_INTERRUPTOR },
{ ".ISMACRO",       TOK_ISMACRO     },
{ ".LEFT",          TOK_LEFT        },
...

Save "scanner.c".

Open "pseudo.c" and add the new function to the table there:

...
{ ccNone,           DoInterruptor   },
{ ccNone,           DoUnexpected    },      /* .ISMACRO */
{ ccNone,           DoInvalid       },      /* .LEFT */
...

This table is similar to the table in "token.h". This entry must be in the same place you entered it in "token.h". The 'DoUnexpected' means that this will output an error if found on a line by itself. This is a function and should be part of an expression.

Save "pseudo.c"

Now the part where we actually have to tell ca65 what to do with the new function. Open "expr.c"

What is ".ismacro" similar to that exists already in ca65? Something that takes the current token and evaluates it as an identifier.

Looking at existing code, and trying to fit with what is there, I've come up with:

static ExprNode* FuncIsMacro(void)
/* Handle the .ISMACRO builtin function */
{
    Macro* Mac = 0;

    /* Check for a macro or an instruction depending on UbiquitousIdents */

    if (CurTok.Tok == TOK_IDENT) {
        Mac = FindMacro(&CurTok.SVal);
    } 
    else
    {
        Error("Identifier expected." );
    }
    /* Skip it */
    NextTok();
    return GenLiteralExpr(Mac != 0);
}

Add this code to a suitable location in "expr.c".  Now we just need to add a call to the above code to the function:

static ExprNode* Factor (void)

Find somewhere in the switch to add this:

  case TOK_ISMACRO:
       N = Function(FuncIsMacro);      
       break;

Save that file, and build your binary. Done. Your new function should work.

Extra: How to set a new feature to be part of the .feature command.

Open "global.h". Add a new line for your feature:

extern unsigned char IsMacro;            /* Allow .ISMACRO function */

Save. Open "global.c". Add a new line for your feature:

unsigned char IsMacro = 0;   /* Allow .ISMACRO function */

Save.

Now, to disable your new function by default, open "scanner.c" and add a new switch in the function "FindDotKeyword" to 'unmatch' the function:

static token_t FindDotKeyword (void)
/* Find the dot keyword in SVal. Return the corresponding token if found,
** return TOK_NONE if not found.
*/
{
    struct DotKeyword K;
    struct DotKeyword* R;

    /* Initialize K */
    K.Key = SB_GetConstBuf (&CurTok.SVal);
    K.Tok = 0;

    /* If we aren't in ignore case mode, we have to uppercase the keyword */
    if (!IgnoreCase) {
        UpcaseSVal ();
    }

    /* Search for the keyword */
    R = bsearch (&K, DotKeywords, sizeof (DotKeywords) / sizeof (DotKeywords [0]),
                 sizeof (DotKeywords [0]), CmpDotKeyword);
    if (R != 0) {

        /* ADDED:        */

        /* By default, disable any DotKeyword that has been added and not accepted to the main branch. */

        switch (R->Tok) {

            case TOK_ISMACRO:
                /* Disallow .ISMACRO function */
                if (IsMacro == 0) {
                    return TOK_NONE;
                }
                break;

            default: /* Keep GCC quiet */ break;
        }
        /* END ADDED      */
        return R->Tok;
    } else {
        return TOK_NONE;
    }
}


Also alter "feature.h" and "feature.c" to allow turning on the feature. This should be obvious by looking at the existing code.

That's it!

Thursday, September 5, 2013

Getting the most out of CA65 - Part 1

I aim to complete a quality game for the NES using the CA65 macro assembler as one of my primary tools to do so. I seem to enjoy much of my time playing with ideas to extend the assembler itself via macro code. I find its macro language enjoyable to work with and have come up with a few things that may be worth sharing, even if just to show how to utilize the macro language for your own goals.

 Some of these ideas I don't really need at this time, but looking at what I may need from the perspective of a very large NES project, I have tried to implement things I may need in the future.

High Level code constructs

This is slowly evolving as I find things that don't quite work perfectly or something small gets added. See the documentation here: https://www.assembla.com/spaces/ca65hl/wiki
There is no code created by these macros. They work by creating labels, which CA65 can do utilizing the .sprint() and .ident() built-in functions. I may explain how they work at the macro level in more detail later, but as far as the IF-ENDIF structure, once a stack macro is implemented, tracking logical blocks and spitting out labels is not too difficult. The more difficult part was implementing the flexibility of what can be included as a 'boolean expression'.

Organizing code with libraries and header files

This is a simple but very nice way to organize chunks of code that can be reused later. This simple example is NES controller reading. You could code the routine once, and never have to worry about it again. (This example ignores the DPMC conflict.)

First, write the 'library' code. I've standardized on prefixing all filenames with 'lib_', so this is: lib_controller.inc
The .inc indicates that this is to be included somewhere in the project, it could be in a separate module. (Note, in this code func is a custom macro.)

Code:

.ifndef   _LIB_CONTROLLER_
_LIB_CONTROLLER_ = 1

.scope _LIB_CONTROLLER_ ; keep everything in its own scope

.pushseg
.segment "ZEROPAGE"

    pressed:          .res    2    ; new this frame - held down
    pressedLastFrame: .res    2    ; last frame / call
    pressedNew:       .res    2    ; newly pressed since last frame
    releasedNew:      .res    2    ; newly released since last frame

.popseg

exportfunc         readPort
exportfunc         readPort0
exportfunc         readPort1

.export pressed
.export pressedLastFrame
.export pressedNew
.export releasedNew


func readPort0
    ldx #0
    .byte $CD           ; compare absolute, skip ldx #1
endfunc

func readPort1
    ldx #1
endfunc

func readPort
; IN : player number in reg X = 0, or 1 => 1 or 2
; OUT: Y: undefined, A: PadsNewReleased, X: unchanged


    lda pressed,x         ; save last frame's joystick
    sta pressedLastFrame,x

    lda #1
    sta pressed,x         ; set bit0 to activate carry in do-while loop
    sta $4016
    lsr a                    ; a has 0
    sta $4016

    :
        lda $4016,x
        and #3               ; famicom
        cmp #1              ; friendly reads
        rol pressed,x
    bcc :-

    lda pressed,x
    eor pressedLastFrame,x
    tay
    and pressed,x
    sta pressedNew,x
    tya
    eor pressedNew,x
    sta releasedNew,x
    
    rts
    
endfunc

.endscope

.endif

If this has been included somewhere, now you can access this functionality from any module by including the header file: int_iController.h
; Interface for lib_controller.inc

.ifndef _INT_CONTROLLER_H_
_INT_CONTROLLER_H_ = 1

.scope iController
    
    importfunc readPort0
    importfunc readPort1
    importfunc readPort
    
    
    .importZP pressed
    .importZP pressedLastFrame  
    .importZP pressedNew 
    .importZP releasedNew
    
.endscope

.endif

Any module that includes the header file can access the controller reading functionality and the variables via the iController scope:

Somewhere in code:

; include the header:
.include "int_iController.h"
; elsewhere:
lda iController::pressednew
and #BUTTON_A
bne buttonApressed

This has the advantage of quickly reusing code and speeding up assembly of large projects, since the library code will never have to be assembled again if it is not changed.

Some minor improvements

Pass "-I headerFilefolder -I libraryFileFolder" to the command line of CA65 and keep all headers and libraries in a standard location.

I have standardized on a header file format of prefixing with int_ and created a macro to help make things a bit nicer.

Somewhere in code:

uses iController

This will attempt to include "int_iController.h" More to come next time.

Wednesday, March 27, 2013

Updated MetaSprite Code (again)

001 002 .proc evalMetaSprites 003 004 ; metasprite Object in RAM format, one record: 005 ; should be organized as one array of each element 006 ; (example, if there are $10 records, the first $10 bytes are all Ypos 007 008 ; Ypos : byte 009 ; Attribute modifier : byte 010 011 ; vhbcxypp --- Attribute modifier byte 012 ; |||||||| 013 ; ||||||++---- Sprite Colour toggle: EOR with metatile attribs 014 ; |||||+------ Y clipping bit 015 ; ||||+------- X clipping bit 016 ; |||+-------- Clipping or wrap : 0: clip sprites at screen edge, 1: allow wrap 017 ; ||+--------- Background priority bit 018 ; |+---------- Horizontal flipping toggle: EOR metatile attribs 019 ; +----------- Vertical flipping toggle: EOR metatile attribs 020 021 ; Metatile number : byte 022 ; Xpos : byte 023 024 ; -------------------------------------------------------------------------------------------- 025 026 ; metasprite format to define metasprite in ROM data: 027 028 ; xxxxxxxx --- Sprite X coordinate (if required) 029 ; yyyyyyyy --- Sprite Y coordinate (if required) 030 031 ; tttttttt --- Tile number (if required) 032 033 ; vhytxxpp --- Attribute byte (always) 034 ; |||||||| 035 ; ||||||++---- Sprite Colour 036 ; |||||+------ Tile number of next sprite (0: required, 1 : Tile number of this sprite + 1, skip if X control was LF) 037 ; ||||+------- Y position of next sprite (0 : required, 0 : unchanged) 038 ; ||++-------- X position of next sprite (01 : required, 00 : X pos of this sprite + 8, 039 ; || 10 : X pos of the first sprite, Y + 8 (CR/LF), 11 : Escape code for last sprite) 040 ; |+---------- Horizontal flipping 041 ; +----------- Vertical flipping 042 043 ; static 044 declareBSS 045 OAMoffset .byte 046 endBSS 047 048 ; notallOnScreen use zeropage: 049 050 locals 051 sprModifier .byte 052 sprAttrib .byte 053 sprTile .byte 054 sprXpos .byte 055 sprYpos .byte 056 057 sprXbit8 .byte 058 sprYbit8 .byte 059 060 metaspritePointerLo .byte 061 metaspritePointerHi .byte 062 063 sprAdder .byte 064 sprXposStart .byte 065 mSpriteOffset .byte 066 carryYplusHeight .byte 067 carryXplusWidth .byte 068 endlocals 069 070 notallOnScreen = local::sprAdder 071 saveOAMOffset = local::sprTile 072 metaspritePointer = local::metaspritePointerLo 073 074 ; init: 075 ldy #::METASPRITE_COUNT 076 sty local::mSpriteOffset 077 078 079 ; skip reserved sprites 080 if lda M::OAMoffset < #::OAM_ROTATION_START 081 lda #::OAM_ROTATION_START 082 endif 083 084 sta M::OAMoffset 085 sta saveOAMOffset ; use as notallOnScreen: save OAMoffset 086 087 DoNextmetasprite: 088 089 090 ; loop through all entries and skip those with att = $FF 091 ; jump to OAM rotation and leave if no more entries 092 ldx local::mSpriteOffset 093 094 repeat 095 096 if dex == negative 097 jmp clearRemainingOAM 098 endif 099 100 mb a, local::sprModifier := mSpriteMemory[ x ]::att ; load Attrib/meta data which is modifier for all 101 102 until tay : iny != zero ; same as: until mSpriteMemory[ x ]::att <> #$FF 103 104 mb local::sprYbit8 := a >> 2 ; put Y 'bit8' into bit 0 local var (only bit0 matters) 105 mb local::sprXbit8 := a >> 1 ; put X 'bit8' into bit 0 local var (only bit0 matters) 106 107 stx local::mSpriteOffset 108 109 mb local::sprYpos := mSpriteMemory[ x ]::yPos 110 mb local::sprXpos := mSpriteMemory[ x ]::xPos 111 sta local::sprXposStart 112 113 mb a, x := mSpriteMemory[ x ]::mTile ; load metatile number 114 115 ; get metatile pointer 116 mb local::metaspritePointerLo := metaspriteNumbersLo[ x ] 117 mb local::metaspritePointerHi := metaspriteNumbersHi[ x ] 118 119 ; first data from metatile string is Xwidth, Yheight, Tile, and Attribs/command byte 120 121 ldy #0 122 sty notallOnScreen 123 124 mb a := local::sprXpos + (metaspritePointer)[ y ] ; add width ( is stored 8 less than actual ) 125 tax ; x has right side of object X coord 126 rol local::carryXplusWidth 127 128 iny 129 mb a := local::sprYpos + (metaspritePointer)[ y ] ; add height ( is stored 8 less than actual ) 130 tay ; y has bottom of object Y coord 131 rol local::carryYplusHeight 132 133 ; ----------- check if all onscreen 134 135 if local::sprModifier & #010000 goto AllOnScreen 136 137 tya ; y has bottom of object Y coord 138 cmp #238 139 rol 140 ora local::sprXbit8 141 ora local::sprYbit8 142 ora local::carryXplusWidth 143 ora local::carryYplusHeight 144 and #1 145 146 if sta notallOnScreen == zero goto AllOnscreen 147 148 ; ------ end check all onscreen 149 150 ; ----------- check if all offscreen 151 152 ; check if all offscreen for X 153 lda local::carryXplusWidth 154 eor #1 155 and local::sprXbit8 156 and #1 157 bne DoNextmetasprite 158 159 ; check all offscreen for Y 160 lda local::carryYplusHeight 161 eor #1 162 and local::sprYbit8 163 and #1 164 bne DoNextmetasprite 165 166 ; check offscreen special case, bottom of screen area: 167 lda local::sprYpos 168 cmp #239 169 rol 170 eor #1 ; b0 = 0 if y > 238 171 ora local::sprYbit8 ; b0 still = 0 if sprYbit8 = 0 172 and #1 173 beq DoNextmetasprite 174 175 ; ------------ end check offscreen 176 177 AllOnscreen: 178 179 bit local::sprModifier ; copy b7 and b6 into N, V 180 181 if V set ; H flipping: 182 mb local::sprXpos := x ; x has right side of object X coord 183 mb local::sprXposStart := x 184 185 if ror local::carryXplusWidth == carry set 186 mb local::sprModifier := local::sprModifier ^ # ( 1 << OFFSCREEN_XBIT ) 187 endif 188 endif 189 190 iny 191 192 ; --------- end width 193 ; --------- process height: 194 195 ror local::carryYplusHeight 196 if lda local::sprModifier == N set ; V flipping: 197 mb local::sprYpos := y ; ; y holds Ypos + height..add height ( is stored 8 less than actual ) 198 if carry set 199 mb a := a ^ #( 1 << OFFSCREEN_YBIT ) 200 endif 201 endif 202 203 sta local::sprModifier 204 205 mb local::sprYbit8 := a >> 2 ; put Y 'bit8' into bit 0 local var (only bit0 matters) 206 mb local::sprXbit8 := a >> 1 ; put X 'bit8' into bit 0 local var (only bit0 matters) 207 208 ; --------------------------- 209 210 ldx saveOAMOffset ; X now has OAMoffset 211 212 ldy #2 ; start at tile entry 213 214 mb local::sprTile := (metaspritePointer)[ y ] 215 iny 216 217 ; if all sprites are onscreen, jump to sped up routine: 218 219 if notallOnScreen == zero 220 jmp DrawFast 221 endif 222 223 DoNextTile: 224 225 mb local::sprAttrib := (metaspritePointer)[ y ] ; read attrib and flags 226 iny 227 228 ; if both offscreen bits are clear OR No clipping mode is on 229 230 if ( local::sprYpos < #239 && ( local::sprXbit8 | local::sprYbit8 >> 1 == carry clear )) 231 232 mb OAM_SHADOW[ x + 2] := local::sprAttrib & #%11000011 ^ local::sprModifier 233 mb OAM_SHADOW[ x + 3] := local::sprXpos 234 mb OAM_SHADOW[ x + 0] := local::sprYpos 235 mb OAM_SHADOW[ x + 1] := local::sprTile 236 237 if x := x + 4 == zero 238 ldx #::OAM_ROTATION_START 239 endif 240 241 endif 242 243 sec ; sec carry to end making reg A hold #8 244 mb a := local::sprAttrib & #$F0 245 rol 246 asl 247 asl ; test bit 3, 2 as C, N 248 249 ; if X MODIFIER value is 00 or 01: 250 if C clear 251 asl ; A will have #8 252 if C set ; X MODIFIER IS 01 253 mb a := (metaspritePointer)[ y ] 254 iny 255 clc 256 endif 257 258 ; HERE. either way, carry clear 259 260 if { flagSet local::sprModifier, 6 } ; if bit6 (h mirror) 261 ; negate adder 262 mb a := a ^ #$FF 263 sec 264 endif 265 266 sta local::sprAdder 267 268 mb local::sprXpos := a +c local::sprXpos 269 270 ror 271 if a ^ local::sprAdder == N set ; if carry and adder sign are different: 272 inc local::sprXbit8 ; toggle offscreen bit 273 endif 274 275 else 276 if N clear ; X MODIFIER IS 10 277 278 ; this idea takes the same # of cycles as the clearer code: 279 ; ------------------------------------------------------- 280 ; asl ; reg A holds #8 281 ; and local::sprModifier 282 ; cmp #1 ; only bit8 could be set, copy it into C 283 ; rol local::sprXbit8 284 285 mb local::sprXbit8 := local::sprModifier >> 3 286 mb local::sprXpos := local::sprXposStart 287 288 lda #8 289 jmp Add8toYPos 290 endif 291 ; else: X MODIFIER IS 11: 292 stx saveOAMOffset ; save OAM offset here 293 ;--------------- 294 jmp DoNextmetasprite ; if = 0 then bail and do next RAm based metasprite 295 296 endif 297 298 ; -----------END X pos 299 300 ; ----------- Y pos 301 302 ; load Ypos? Skip all this if not 0 303 if ! local::sprAttrib & #001000 304 305 mb a := (metaspritePointer)[ y ] 306 iny 307 308 Add8toYPos: ; jump here if CR/LF code 309 310 clc 311 if flagSet local::sprModifier ; if bit7 (v mirror) 312 mb a := a ^ #$FF 313 sec 314 endif 315 316 sta local::sprAdder 317 mb local::sprYpos := a +c local::sprYpos 318 319 ror 320 if a ^ local::sprAdder == N set 321 inc local::sprYbit8 322 endif 323 324 endif 325 ; -----------END Y pos 326 327 ; -----------check tile: 328 329 ; BIT4: check if 0: load tile, else use next tile 330 331 inc local::sprTile ; inc by default 332 333 if ! local::sprAttrib & #000100 334 mb local::sprTile := (metaspritePointer)[ y ] 335 iny 336 endif 337 338 ; -------end tile processing 339 340 jmp DoNextTile 341 342 ; super fast, no clipping version: 343 344 DrawFast: 345 346 mb local::sprAttrib := (metaspritePointer)[ y ] ; read attrib and flags 347 iny 348 349 mb OAM_SHADOW[ x + 2] := local::sprAttrib & #%11000011 ^ local::sprModifier 350 mb OAM_SHADOW[ x + 3] := local::sprXpos 351 mb OAM_SHADOW[ x + 0] := local::sprYpos 352 mb OAM_SHADOW[ x + 1] := local::sprTile 353 354 if x := x + 4 == zero 355 ldx #::OAM_ROTATION_START 356 endif 357 358 sec ; sec carry to end making reg A hold #8 359 mb a := local::sprAttrib & #$F0 360 rol 361 asl 362 asl ; test bit 3, 2 as C, N 363 364 ; if X MODIFIER value is 00 or 01: 365 if C clear 366 asl ; A will have #8 367 if C set ; X MODIFIER IS 01 368 mb a := (metaspritePointer)[ y ] 369 iny 370 clc 371 endif 372 373 ; HERE. either way, carry clear 374 375 if { flagSet local::sprModifier, 6 } ; if bit6 (h mirror) 376 ; negate adder 377 mb a := a ^ #$FF 378 sec 379 endif 380 381 mb local::sprXpos := a +c local::sprXpos 382 383 else 384 if N clear ; X MODIFIER IS 10 385 mb local::sprXpos := local::sprXposStart 386 387 lda #8 388 jmp Add8toYPosFast 389 endif 390 ; else: X MODIFIER IS 11: 391 stx saveOAMOffset ; save OAM offset here 392 ;--------------- 393 jmp DoNextmetasprite ; if = 0 then bail and do next RAm based metasprite 394 395 endif 396 397 ; -----------END X pos 398 399 ; ----------- Y pos 400 401 ; load Ypos? Skip all this if not 0 402 if ! local::sprAttrib & #001000 403 404 mb a := (metaspritePointer)[ y ] 405 iny 406 407 Add8toYPosFast: ; jump here if CR/LF code 408 409 clc 410 if flagSet local::sprModifier ; if bit7 (v mirror) 411 mb a := a ^ #$FF 412 sec 413 endif 414 415 mb local::sprYpos := a +c local::sprYpos 416 endif 417 ; -----------END Y pos 418 419 ; -----------check tile: 420 421 ; BIT4: check if 0: load tile, else use next tile 422 423 inc local::sprTile ; inc by default 424 425 if ! local::sprAttrib & #000100 426 mb local::sprTile := (metaspritePointer)[ y ] 427 iny 428 endif 429 430 ; -------end tile processing 431 432 jmp DrawFast 433 434 ; -------------------------------------------------------------------------------------------- 435 436 clearRemainingOAM: 437 OAMnext = saveOAMOffset ; alias 438 439 ; -------------------------------------------------------------------------------------------- 440 441 ldx OAMnext ; x holds start of next blank spot 442 443 ; take away blank spaces from total slots if we wrapped 444 if OAMnext <= M::OAMoffset 445 mb a := OAMnext - #::OAM_ROTATION_START 446 endif 447 448 mb a := a - M::OAMoffset ; bytes used 449 mb a := a >> 2 ; slots used 450 sta OAMnext ; use as notallOnScreen, since this value is in reg X 451 452 mb y := #(64 - ( ::OAM_ROTATION_START / 4 ) ) - OAMnext ; total avaliable slots MINUS slots used 453 lda #$FF 454 455 cpy #00 456 while not zero do 457 458 sta OAM_SHADOW, x 459 mb x := x + 4 460 if zero 461 ldx #::OAM_ROTATION_START 462 endif 463 dey 464 endwhile 465 466 mb M::OAMoffset := M::OAMoffset + #::OAM_ROTATION_AMOUNT 467 468 rts 469 470 .endproc