iigs-game-engine/src/Tiles.s

777 lines
27 KiB
ArmAsm

; Basic tile functions
; Copy tileset data from a pointer in memory to the tiledata back
;
; tmp0 = Pointer to tile data
; X = first tile
; Y = last tile
;
; To copy in four tiles starting at tile 5, for example, X = 5 and Y = 9
_LoadTileSet
txa
_Mul128 ; Jump to the target location
tax
tya
_Mul128
sta tmp2 ; This is the terminating byte
ldy #0
:loop lda [tmp0],y
stal tiledata,x
inx
inx
iny
iny
cpx tmp2
bne :loop ; Use BNE so when Y=512 => $0000, we wait for wrap-around
rts
; Low-level function to take a tile descriptor and return the address in the tiledata
; bank. This is not too useful in the fast-path because the fast-path does more
; incremental calculations, but it is handy for other utility functions
;
; A = tile descriptor
;
; The address is the TileID * 128 + (HFLIP * 64)
_GetTileAddr
asl ; Multiply by 2
bit #2*TILE_HFLIP_BIT ; Check if the horizontal flip bit is set
beq :no_flip
inc ; Set the LSB
:no_flip asl ; x4
asl ; x8
asl ; x16
asl ; x32
asl ; x64
asl ; x128
rts
; Ignore the horizontal flip bit
_GetBaseTileAddr
asl ; Multiply by 2
asl ; x4
asl ; x8
asl ; x16
asl ; x32
asl ; x64
asl ; x128
rts
; Helper function to get the address offset into the tile cachce / tile backing store
; X = tile column [0, 40] (41 columns)
; Y = tile row [0, 25] (26 rows)
_GetTileStoreOffset
phx ; preserve the registers
phy
jsr _GetTileStoreOffset0
ply
plx
rts
_GetTileStoreOffset0
tya
asl
tay
txa
asl
clc
adc TileStoreYTable,y
rts
; Initialize the tile storage data structures. This takes care of populating the tile records with the
; appropriate constant values.
InitTiles
:col equ tmp0
:row equ tmp1
:vbuff equ tmp2
:base equ tmp3
; Initialize the Tile Store
ldx #TILE_STORE_SIZE-2
lda #25
sta :row
lda #40
sta :col
lda #$8000
sta :vbuff
:loop
; The first set of values in the Tile Store that are changed during each frame based on the actions
; that are happening
lda #0
sta TileStore+TS_TILE_ID,x ; clear the tile store with the special zero tile
sta TileStore+TS_TILE_ADDR,x
sta TileStore+TS_SPRITE_FLAG,x ; no sprites are set at the beginning
sta TileStore+TS_DIRTY,x ; none of the tiles are dirty
; Set the default tile rendering functions
lda EngineMode
bit #ENGINE_MODE_DYN_TILES+ENGINE_MODE_TWO_LAYER
beq :fast
bit #ENGINE_MODE_TWO_LAYER
beq :dyn
lda #0
ldy #TwoLyrProcs
jsr _SetTileProcs
bra :out
:fast
lda #0 ; Initialize with Tile 0
ldy #FastProcs
jsr _SetTileProcs
bra :out
:dyn lda #0 ; Initialize with Tile 0
ldy #FastProcs
jsr _SetTileProcs
:out
; The next set of values are constants that are simply used as cached parameters to avoid needing to
; calculate any of these values during tile rendering
lda :row ; Set the long address of where this tile
asl ; exists in the code fields
tay
lda #>TileStore ; get middle 16 bits: "00 -->BBHH<-- LL"
and #$FF00 ; merge with code field bank
ora BRowTableHigh,y
sta TileStore+TS_CODE_ADDR_HIGH,x ; High word of the tile address (just the bank)
lda BRowTableLow,y
sta :base
lda :col ; Set the offset values based on the column
asl ; of this tile
asl
sta TileStore+TS_WORD_OFFSET,x ; This is the offset from 0 to 82, used in LDA (dp),y instruction
tay
lda Col2CodeOffset+2,y
clc
adc :base
sta TileStore+TS_CODE_ADDR_LOW,x ; Low word of the tile address in the code field
lda JTableOffset,y
clc
adc :base
sta TileStore+TS_JMP_ADDR,x ; Address of the snippet handler for this tile
dec :col
bpl :hop
dec :row
lda #40
sta :col
:hop
dex
dex
bpl :loop
rts
; Put everything visible on the dirty tile list
_Refresh
:col equ tmp0
:row equ tmp1
:idx equ tmp2
jsr _OriginToTileStore
clc
txa
adc TileStoreLookupYTable,y
sta :idx
lda ScreenTileWidth
sta :col
lda ScreenTileHeight
sta :row
:oloop ldy :idx
:iloop ldx TileStoreLookup,y
jsr _PushDirtyTileX
iny
iny
dec :col
bne :iloop
lda ScreenTileWidth
sta :col
lda :idx
clc
adc #TS_LOOKUP_SPAN*2
sta :idx
dec :row
bne :oloop
rts
; Reset all of the tile proc values in the playfield.
_ResetToDirtyTileProcs
ldx #TILE_STORE_SIZE-2
:loop
lda TileStore+TS_TILE_ID,x
jsr _SetDirtyTileProcs
dex
dex
bpl :loop
rts
_ResetToNormalTileProcs
ldx #TILE_STORE_SIZE-2
:loop
lda TileStore+TS_TILE_ID,x
jsr _SetNormalTileProcs
dex
dex
bpl :loop
rts
; A = tileID
; X = tile store index
_SetDirtyTileProcs
jsr _CalcTileProcIndex
ldy #DirtyProcs
jmp _SetTileProcs
; A = tileID
; X = tile store index
_SetNormalTileProcs
pha ; extra space
pha ; save the tile ID
jsr _CalcTileProcIndex
sta 3,s ; save for later
lda EngineMode
bit #ENGINE_MODE_DYN_TILES+ENGINE_MODE_TWO_LAYER
beq :setTileFast
bit #ENGINE_MODE_TWO_LAYER
beq :setTileDyn
pla ; restore newTileID
bit #TILE_DYN_BIT
beq :pickTwoLyrProc
ldy #TwoLyrDynProcs
brl :pickDynProc
:pickTwoLyrProc ldy #TwoLyrProcs
pla ; pull off the proc index
jmp _SetTileProcs
; Specialized check for when the engine is in "Fast" mode. If is a simple decision tree based on whether
; the tile priority bit is set, and whether this is the special tile 0 or not.
:setTileFast
pla ; Throw away tile ID copy
ldy #FastProcs
pla
jmp _SetTileProcs
; Specialized check for when the engine has enabled dynamic tiles. In this case we are no longer
; guaranteed that the opcodes in a tile are PEA instructions.
:setTileDyn
pla ; get the cached tile ID
bit #TILE_DYN_BIT
beq :pickSlowProc ; If the Dynamic bit is not set, select a tile proc that sets opcodes
ldy #DynProcs ; use this table
:pickDynProc
and #TILE_PRIORITY_BIT
beq :pickZeroDynProc ; If the Priority bit is not set, pick the first entry
pla
lda #1 ; If the Priority bit is set, pick the other one
jmp _SetTileProcs
:pickZeroDynProc pla
lda #0
jmp _SetTileProcs
:pickSlowProc ldy #SlowProcs
pla
jmp _SetTileProcs
; Helper method to calculate the index into the tile proc table given a TileID
; Calculate the base tile proc selector from the tile Id
_CalcTileProcIndex
bit #TILE_PRIORITY_BIT ; 4 if !0, 0 otherwise
beq :low_priority
bit #TILE_ID_MASK ; 2 if !0, 0 otherwise
beq :is_zero_a
bit #TILE_VFLIP_BIT ; 1 if !0, 0 otherwise
beq :no_flip_a
lda #7
rts
:no_flip_a lda #6
rts
:is_zero_a bit #TILE_VFLIP_BIT
beq :no_flip_b
lda #5
rts
:no_flip_b lda #4
rts
:low_priority bit #TILE_ID_MASK ; 2 if !0, 0 otherwise
beq :is_zero_b
bit #TILE_VFLIP_BIT ; 1 if !0, 0 otherwise
beq :no_flip_c
lda #3
rts
:no_flip_c lda #2
rts
:is_zero_b bit #TILE_VFLIP_BIT
beq :no_flip_d
lda #1
rts
:no_flip_d lda #0
rts
; Set a tile value in the tile backing store. Mark dirty if the value changes
;
; A = tile id
; X = tile column [0, 40] (41 columns)
; Y = tile row [0, 25] (26 rows)
;
; Registers are not preserved
oldTileId equ blttmp ; This location is used in _SetTileProcs, too
newTileId equ blttmp+2
procIdx equ blttmp+4
_SetTile
sta newTileId
jsr _GetTileStoreOffset0 ; Get the address of the X,Y tile position
tax
lda TileStore+TS_TILE_ID,x
; cmp newTileId ; Lift this up to the caller
; bne :changed
; rts
:changed sta oldTileId
lda newTileId
sta TileStore+TS_TILE_ID,x ; Value is different, store it.
; If the user bit is set, then skip most of the setup and just fill in the TileProcs with the user callback
; target
bit #TILE_USER_BIT
bne :set_user_tile
jsr _GetTileAddr
sta TileStore+TS_TILE_ADDR,x ; Committed to drawing this tile, so get the address of the tile in the tiledata bank for later
; Set the renderer procs for this tile.
;
; NOTE: Later on, optimize this to just take the Tile ID & TILE_CTRL_MASK and lookup the right proc
; table address from a lookup table....
;
; 1. The dirty render proc is always set the same.
; 2. If BG1 and DYN_TILES are disabled, then the TS_BASE_TILE_DISP is selected from the Fast Renderers, otherwise
; it is selected from the full tile rendering functions.
; 3. The copy process is selected based on the flip bits
;
; When a tile overlaps the sprite, it is the responsibility of the Render function to compose the appropriate
; functionality. Sometimes it is simple, but in cases of the sprites overlapping Dynamic Tiles and other cases
; it can be more involved.
; Calculate the base tile proc selector from the tile Id (need X-register set to tile store index)
lda newTileId
jsr _SetNormalTileProcs
jmp _PushDirtyTileX
:set_user_tile
lda #UserTileDispatch
stal K_TS_BASE_TILE_DISP,x
lda #UserTileDispatch
stal K_TS_SPRITE_TILE_DISP,x
lda #UserTileDispatch
stal K_TS_ONE_SPRITE,x
jmp _PushDirtyTileX
; Trampoline / Dispatch table for user-defined tiles. If the user calls the GTESetCustomTileCallback toolbox routine,
; then this value is patched out. Calling GTESetCustomTileCallback with NULL will disconnect the user's routine.
UserTileDispatch
ldal TileStore+TS_TILE_ID,x ; Replace the tile data address (unset) with the tile id
_UTDPatch jsl UserHook1 ; Call the users code
plb ; Restore the data bank
rts
; Stub to have a valid address for initialization / reset
UserHook1 rtl
; X = Tile Store offset
; Y = Engine Mode Base Table address
; A = Table proc index
;
; see TileProcTables in static/TileStore.s
_SetTileProcs
:tblPtr equ blttmp
; Multiple the proc index by 6 to get the correct table entry offset
asl
sta :tblPtr
asl
adc :tblPtr
sta :tblPtr
; Add this offset to the base table address
tya
adc :tblPtr
sta :tblPtr
; Set the pointer to this bank
phk
phk
pla
and #$00FF
sta :tblPtr+2
; Lookup the tile procedures
ldy #0
lda [:tblPtr],y
stal K_TS_BASE_TILE_DISP,x
ldy #2
lda [:tblPtr],y
stal K_TS_SPRITE_TILE_DISP,x
ldy #4
lda [:tblPtr],y
stal K_TS_ONE_SPRITE,x
rts
; TileProcTables
;
; Tables of tuples used to populate the K_TS_* dispatch arrays for different combinations. This is
; easier to maintain than a bunch of conditional code. Each entry holds three addresses.
;
; First address: Draw a tile directly into the code buffer (no sprites)
; Second address: Draw a tile merged with sprite data from the direct page
; Third address: Specialize routine to draw a tile merged with one sprite
;
; There are unique tuples of routines for all of the different combinations of tile properties
; and engine modes. This is an extensive number of combinations, but it simplifies the development
; and maintainence of the rendering subroutines. Also, the different subroutines can be written
; in any way and can make use of their own subroutines to reduce code size.
;
; Properties:
;
; [MODE] ENGINE_MODE: Fast, Dyn, TwoLayer
; [Z | N] Is Tile 0? : Yes, No
; [A | V] Is VFLIP? : Yes, No
; [Over | Under] Priority? : Yes, No
;
; So eight tuples per engine mode; 24 tuples total. Table name convention
;
; <MODE><Over|Under><Z|N><A|V>
FastProcs
FastOverZA dw ConstTile0Fast,SpriteOver0Fast,OneSpriteFastOver0
FastOverZV dw ConstTile0Fast,SpriteOver0Fast,OneSpriteFastOver0
FastOverNA dw CopyTileAFast,SpriteOverAFast,OneSpriteFastOverA
FastOverNV dw CopyTileVFast,SpriteOverVFast,OneSpriteFastOverV
FastUnderZA dw ConstTile0Fast,SpriteUnder0Fast,SpriteUnder0Fast
FastUnderZV dw ConstTile0Fast,SpriteUnder0Fast,SpriteUnder0Fast
FastUnderNA dw CopyTileAFast,SpriteUnderAFast,OneSpriteFastUnderA
FastUnderNV dw CopyTileVFast,SpriteUnderVFast,OneSpriteFastUnderV
; "Slow" procs. These are duplicates of the "Fast" functions, but also
; set the PEA opcode in all cases.
SlowProcs
SlowOverZA dw ConstTile0Slow,SpriteOver0Slow,OneSpriteSlowOver0
SlowOverZV dw ConstTile0Slow,SpriteOver0Slow,OneSpriteSlowOver0
SlowOverNA dw CopyTileASlow,SpriteOverASlow,OneSpriteSlowOverA
SlowOverNV dw CopyTileVSlow,SpriteOverVSlow,OneSpriteSlowOverV
SlowUnderZA dw ConstTile0Slow,SpriteUnder0Slow,SpriteUnder0Slow
SlowUnderZV dw ConstTile0Slow,SpriteUnder0Slow,SpriteUnder0Slow
SlowUnderNA dw CopyTileASlow,SpriteUnderASlow,OneSpriteSlowUnderA
SlowUnderNV dw CopyTileVSlow,SpriteUnderVSlow,OneSpriteSlowUnderV
; "Dynamic" procs. These are the specialized routines for a dynamic tiles
; that does not need to worry about a second background. Because dynamic
; tiles don't support horizontal or vertical flipping, there are only two
; sets of procedures: one for Over and one for Under.
DynProcs
DynOver dw CopyDynamicTile,DynamicOver,OneSpriteDynamicOver
DynUnder dw CopyDynamicTile,DynamicUnder,OneSpriteDynamicUnder
; "Two Layer" procs. These are the most complex procs. Generally,
; all of these methods are implemented by building up the data
; and mask into the direct page space and then calling a common
; function to create the complex code fragments in the code field.
; There is not a lot of opportuinity to optimize these routines.
;
; To improve the performance when two-layer rendering is enabled,
; the TILE_SOLID_BIT hint bit can be set to indicate that a tile
; has no transparency. This allows one of the faster routines
; to be selected from the other Proc tables
;
; FUTURE: An optimization that can be done is to have the snippets
; code layout fixed based on the EngineFlags and then the Two Layer
; routines should only need to update the DATA and MASK operands in
; the snippet at a fixed location rather than rebuild the ~20 bytes
; of data.
TwoLyrProcs
TwoLyrOverZA dw Tile0TwoLyr,SpriteOver0TwoLyr,OneSpriteOver0TwoLyr
TwoLyrOverZV dw Tile0TwoLyr,SpriteOver0TwoLyr,OneSpriteOver0TwoLyr
TwoLyrOverNA dw CopyTileATwoLyr,SpriteOverATwoLyr,OneSpriteTwoLyrOverA
TwoLyrOverNV dw CopyTileVTwoLyr,SpriteOverVTwoLyr,OneSpriteTwoLyrOverV
TwoLyrUnderZA dw Tile0TwoLyr,SpriteOver0TwoLyr,OneSpriteOver0TwoLyr ; if sprites are over or under the transparent tile, same rendering code
TwoLyrUnderZV dw Tile0TwoLyr,SpriteOver0TwoLyr,OneSpriteOver0TwoLyr
TwoLyrUnderNA dw CopyTileATwoLyr,SpriteUnderATwoLyr,OneSpriteTwoLyrUnderA
TwoLyrUnderNV dw CopyTileVTwoLyr,SpriteUnderVTwoLyr,OneSpriteTwoLyrUnderV
; "Dynamic" procs that can handle the second background.
TwoLyrDynProcs
TwoLyrDynOver dw CopyDynamicTileTwoLyr,DynamicOverTwoLyr,OneSpriteDynamicOverTwoLyr
TwoLyrDynUnder dw CopyDynamicTileTwoLyr,DynamicUnderTwoLyr,OneSpriteDynamicUnderTwoLyr
; "Dirty" procs that are for dirty tile direct rendering. No moving background.
DirtyProcs
DirtyOverZA dw ConstTile0Dirty,SpriteOver0Dirty,OneSpriteDirtyOver0
DirtyOverZV dw ConstTile0Dirty,SpriteOver0Dirty,OneSpriteDirtyOver0
DirtyOverNA dw CopyTileADirty,SpriteOverADirty,OneSpriteDirtyOverA
DirtyOverNV dw CopyTileVDirty,SpriteOverVDirty,OneSpriteDirtyOverV
DirtyUnderZA dw ConstTile0Dirty,SpriteUnder0Dirty,SpriteUnder0Dirty
DirtyUnderZV dw ConstTile0Dirty,SpriteUnder0Dirty,SpriteUnder0Dirty
DirtyUnderNA dw CopyTileADirty,SpriteUnderADirty,OneSpriteDirtyUnderA
DirtyUnderNV dw CopyTileVDirty,SpriteUnderVDirty,OneSpriteDirtyUnderV
; SetBG0XPos
;
; Set the virtual horizontal position of the primary background layer. In addition to
; updating the direct page state locations, this routine needs to preserve the original
; value as well. This is a bit subtle, because if this routine is called multiple times
; with different values, we need to make sure the *original* value is preserved and not
; continuously overwrite it.
;
; We assume that there is a clean code field in this routine
_SetBG0XPos
cmp StartX
beq :out ; Easy, if nothing changed, then nothing changes
ldx StartX ; Load the old value (but don't save it yet)
sta StartX ; Save the new position
lda #DIRTY_BIT_BG0_X
tsb DirtyBits ; Check if the value is already dirty, if so exit
bne :out ; without overwriting the original value
stx OldStartX ; First change, so preserve the value
:out rts
; SetBG0YPos
;
; Set the virtual position of the primary background layer.
_SetBG0YPos
cmp StartY
beq :out ; Easy, if nothing changed, then nothing changes
ldx StartY ; Load the old value (but don't save it yet)
sta StartY ; Save the new position
lda #DIRTY_BIT_BG0_Y
tsb DirtyBits ; Check if the value is already dirty, if so exit
bne :out ; without overwriting the original value
stx OldStartY ; First change, so preserve the value
:out rts
; Macro helper for the bit test tree
; dobit bit_position,dest;next;exit
dobit mac
lsr
bcc next_bit
beq last_bit
tax
lda (SPRITE_VBUFF_PTR+{]1*2}),y ; The carry is always set
; clc
adc _Sprites+TS_VBUFF_BASE+{]1*2} ; [!! INTERLOCK !!] The table is pre-decremented in Sprite2.s
sta sprite_ptr0+{]2*4}
txa
jmp ]3
last_bit lda (SPRITE_VBUFF_PTR+{]1*2}),y
; clc ; pre-adjust these later
adc _Sprites+TS_VBUFF_BASE+{]1*2}
sta sprite_ptr0+{]2*4}
jmp ]4
next_bit
<<<
; Specialization for the first sprite which can optimize its dispatch if its the only one
; dobit bit_position,dest;next;exit
dobit1 mac
lsr
bcc next_bit
beq last_bit
tax
lda (SPRITE_VBUFF_PTR+{]1*2}),y
; clc
adc _Sprites+TS_VBUFF_BASE+{]1*2}
sta sprite_ptr0+{]2*4}
txa
jmp ]3
last_bit lda (SPRITE_VBUFF_PTR+{]1*2}),y
; clc ; pre-adjust these later
adc _Sprites+TS_VBUFF_BASE+{]1*2}
sta sprite_ptr0+{]2*4}
tyx
jmp (K_TS_ONE_SPRITE,x)
next_bit
<<<
; If we find a last bit (4th in this case) and will exit
stpbit mac
lsr
bcc next_bit
lda (SPRITE_VBUFF_PTR+{]1*2}),y
; clc ; pre-adjust these later
adc _Sprites+TS_VBUFF_BASE+{]1*2}
sta sprite_ptr0+{]2*4}
jmp ]3
next_bit
<<<
; Last bit test which *must* be set
endbit mac
lda (SPRITE_VBUFF_PTR+{]1*2}),y
; clc ; pre-adjust these later
adc _Sprites+TS_VBUFF_BASE+{]1*2}
sta sprite_ptr0+{]2*4}
jmp ]3
<<<
endbit1 mac
lda (SPRITE_VBUFF_PTR+{]1*2}),y
; clc ; pre-adjust these later
adc _Sprites+TS_VBUFF_BASE+{]1*2}
sta sprite_ptr0+{]2*4}
tyx
jmp (K_TS_ONE_SPRITE,x)
<<<
; OPTIMIZATION:
;
; bit #$00FF ; Skip the first 8 bits if they are all zeros
; bne norm_entry
; xba
; jmp skip_entry
;
; Placed at the entry point
; This is a complex, but fast subroutine that is called from the core tile rendering code. It
; Takes a bitmap of sprites in the Accumulator and then extracts the VBuff addresses for the
; target TileStore entry and places them in specific direct page locations.
;
; Inputs:
; A = sprite bitmap (assumed to be non-zero)
; Y = tile store index
; D = second work page
; B = vbuff array bank
; Output:
; X =
;
; ]1 address of single sprite process
; ]2 address of two sprite process
; ]3 address of three sprite process
; ]4 address of four sprite process
SpriteBitsToVBuffAddrs mac
dobit1 0;0;b_1_1
dobit1 1;0;b_2_1
dobit1 2;0;b_3_1
dobit1 3;0;b_4_1
dobit1 4;0;b_5_1
dobit1 5;0;b_6_1
dobit1 6;0;b_7_1
dobit1 7;0;b_8_1
dobit1 8;0;b_9_1
dobit1 9;0;b_10_1
dobit1 10;0;b_11_1
dobit1 11;0;b_12_1
dobit1 12;0;b_13_1
dobit1 13;0;b_14_1
dobit1 14;0;b_15_1
endbit1 15;0
b_1_1 dobit 1;1;b_2_2;]2
b_2_1 dobit 2;1;b_3_2;]2
b_3_1 dobit 3;1;b_4_2;]2
b_4_1 dobit 4;1;b_5_2;]2
b_5_1 dobit 5;1;b_6_2;]2
b_6_1 dobit 6;1;b_7_2;]2
b_7_1 dobit 7;1;b_8_2;]2
b_8_1 dobit 8;1;b_9_2;]2
b_9_1 dobit 9;1;b_10_2;]2
b_10_1 dobit 10;1;b_11_2;]2
b_11_1 dobit 11;1;b_12_2;]2
b_12_1 dobit 12;1;b_13_2;]2
b_13_1 dobit 13;1;b_14_2;]2
b_14_1 dobit 14;1;b_15_2;]2
b_15_1 endbit 15;1;]2
b_2_2 dobit 2;2;b_3_3;]3
b_3_2 dobit 3;2;b_4_3;]3
b_4_2 dobit 4;2;b_5_3;]3
b_5_2 dobit 5;2;b_6_3;]3
b_6_2 dobit 6;2;b_7_3;]3
b_7_2 dobit 7;2;b_8_3;]3
b_8_2 dobit 8;2;b_9_3;]3
b_9_2 dobit 9;2;b_10_3;]3
b_10_2 dobit 10;2;b_11_3;]3
b_11_2 dobit 11;2;b_12_3;]3
b_12_2 dobit 12;2;b_13_3;]3
b_13_2 dobit 13;2;b_14_3;]3
b_14_2 dobit 14;2;b_15_3;]3
b_15_2 endbit 15;2;]3
b_3_3 stpbit 3;3;]4
b_4_3 stpbit 4;3;]4
b_5_3 stpbit 5;3;]4
b_6_3 stpbit 6;3;]4
b_7_3 stpbit 7;3;]4
b_8_3 stpbit 8;3;]4
b_9_3 stpbit 9;3;]4
b_10_3 stpbit 10;3;]4
b_11_3 stpbit 11;3;]4
b_12_3 stpbit 12;3;]4
b_13_3 stpbit 13;3;]4
b_14_3 stpbit 14;3;]4
b_15_3 endbit 15;3;]4
<<<
; Store some tables in the K bank that will be used exclusively for jmp (abs,x) dispatch
K_TS_BASE_TILE_DISP ds TILE_STORE_SIZE ; draw the tile without a sprite
;K_TS_COPY_TILE_DATA ds TILE_STORE_SIZE ; copy/merge the tile into temp storage
K_TS_SPRITE_TILE_DISP ds TILE_STORE_SIZE ; select the sprite routine for this tile
K_TS_ONE_SPRITE ds TILE_STORE_SIZE ; specialized sprite routine when only one sprite covers the tile
;K_TS_APPLY_TILE_DATA ds TILE_STORE_SIZE ; move tile from temp storage into code field