iigs-game-engine/src/Sprite.s

1246 lines
39 KiB
ArmAsm

; Functions for sprite handling. Mostly maintains the sprite list and provides
; utility functions to calculate sprite/tile intersections
;
; Initialize the sprite data and mask banks (all data = $0000, all masks = $FFFF)
InitSprites
ldx #$FFFE
lda #0
:loop1 stal spritedata,x
dex
dex
cpx #$FFFE
bne :loop1
ldx #$FFFE
lda #$FFFF
:loop2 stal spritemask,x
dex
dex
cpx #$FFFE
bne :loop2
; Initialize the VBuff offset values for the different cases. These are locations in
; the TileStoreLookup table, which has different dimensions than the underlying TileStore
; array
LAST_ROW equ {2*TS_LOOKUP_SPAN*{TILE_STORE_HEIGHT-1}}
NEXT_TO_LAST_ROW equ {2*TS_LOOKUP_SPAN*{TILE_STORE_HEIGHT-2}}
LAST_COL equ {{TILE_STORE_WIDTH-1}*2}
NEXT_TO_LAST_COL equ {{TILE_STORE_WIDTH-2}*2}
lda #0 ; Normal row, Normal column
ldx #0
jsr _SetVBuffValues
lda #8
ldx #LAST_COL ; Normal row, Last column
jsr _SetVBuffValues
lda #16
ldx #NEXT_TO_LAST_COL ; Normal row, Next-to-Last column
jsr _SetVBuffValues
lda #24 ; Last row, normal column
ldx #LAST_ROW
jsr _SetVBuffValues
lda #32
ldx #LAST_ROW+LAST_COL ; Last row, Last column
jsr _SetVBuffValues
lda #40
ldx #LAST_ROW+NEXT_TO_LAST_COL ; Last row, Next-to-Last column
jsr _SetVBuffValues
lda #48 ; Next-to-Last row, normal column
ldx #NEXT_TO_LAST_ROW
jsr _SetVBuffValues
lda #56
ldx #NEXT_TO_LAST_ROW+LAST_COL ; Next-to-Last row, Last column
jsr _SetVBuffValues
lda #64
ldx #NEXT_TO_LAST_ROW+NEXT_TO_LAST_COL ; Next-to-Last row, Next-to-Last column
jsr _SetVBuffValues
; Initialize the Page 2 pointers
ldx #$100
lda #^spritemask
sta sprite_ptr0+2,x
sta sprite_ptr1+2,x
sta sprite_ptr2+2,x
sta sprite_ptr3+2,x
; Precalculate some bank values
jsr _CacheSpriteBanks
rts
; Call with X-register set to TileStore tile and A set to the VBuff slot offset
_SetVBuffValues
COL_BYTES equ 4 ; VBUFF_TILE_COL_BYTES
ROW_BYTES equ 384 ; VBUFF_TILE_ROW_BYTES
clc
adc #VBuffArray
sec
sbc TileStoreLookup,x
sta tmp0
ldy TileStoreLookup,x
lda #{0*COL_BYTES}+{0*ROW_BYTES}
sta (tmp0),y
ldy TileStoreLookup+2,x
lda #{1*COL_BYTES}+{0*ROW_BYTES}
sta (tmp0),y
ldy TileStoreLookup+4,x
lda #{2*COL_BYTES}+{0*ROW_BYTES}
sta (tmp0),y
ldy TileStoreLookup+{1*{TS_LOOKUP_SPAN*2}},x
lda #{0*COL_BYTES}+{1*ROW_BYTES}
sta (tmp0),y
ldy TileStoreLookup+{1*{TS_LOOKUP_SPAN*2}}+2,x
lda #{1*COL_BYTES}+{1*ROW_BYTES}
sta (tmp0),y
ldy TileStoreLookup+{1*{TS_LOOKUP_SPAN*2}}+4,x
lda #{2*COL_BYTES}+{1*ROW_BYTES}
sta (tmp0),y
ldy TileStoreLookup+{2*{TS_LOOKUP_SPAN*2}},x
lda #{0*COL_BYTES}+{2*ROW_BYTES}
sta (tmp0),y
ldy TileStoreLookup+{2*{TS_LOOKUP_SPAN*2}}+2,x
lda #{1*COL_BYTES}+{2*ROW_BYTES}
sta (tmp0),y
ldy TileStoreLookup+{2*{TS_LOOKUP_SPAN*2}}+4,x
lda #{2*COL_BYTES}+{2*ROW_BYTES}
sta (tmp0),y
rts
; _RenderSprites
;
; The function is responsible for updating all of the rendering information based on any changes
; that occured to the sprites on this frame. Sprite handling is one of the most expensive and
; complicated pieces of the rendering pipeline, so these functions are aggressively simplified and
; optimized.
;
; The sprite rendering pipeline is:
;
; 0. Check if any new sprites have been added by testing the DIRTY_BIT_SPRITE_ARRAY. If so, then
; the activeSpriteList (a 32-byte array on the direct page) is rebuilt from the SpriteBits bitmap
; word.
;
; Next, the activeSpriteList is scanned for changes to specific sprites. If the screen has been
; scrolled, then every sprite is considered to have the SPRITE_STATUS_MOVED flag set.
;
; 1. If a sprite is marked as (SPRITE_STATUS_MOVED or SPRITE_STATUS_UPDATED or SPRITE_STATUS_ADDED) and not SPRITE_STATUS_REMOVED
; A. Calculate the TS_COVERAGE_SIZE, TS_LOOKUP_INDEX, and TS_VBUFF_BASE for the sprite
; B. For each tile the sprite overlaps with:
; i. Set its bit in the TileStore's TS_SPRITE_FLAG
; ii. Add the tile to the DirtyTile list
; iii. Set the VBUFF address for the sprite block
; C. If the sprite is not marked as SPRITE_STATUS_ADDED
; i. For each old tile the sprite overlaps with
; a. If it is not marked in the DirtyTile list
; * Clear its bit from the TileStore's TS_SPRITE_FLAG
; * Add the tile to the DirtyTile list
;
; 2. If a sprite is marked as SPRITE_STATUS_REMOVED, then
; A. Clear its bit from the SpriteBits bitmap
; B. For each tile the sprite overlaps with:
; i. Clear its bit from the TileStore's TS_SPRITE_FLAG
; ii. Add the tile to the DirtyTile list
; C. Clear the SPRITE_STATUS flags (work complete)
;
; 3. For each tile on the Dirty Tile list
; A. Place the sprite VBUFF addresses in TS_VBUFF_ADDR_0 through TS_VBUFF_ADDR_3 and set TS_VBUFF_ADDR_COUNT
;
; It is important that this work is done *prior* to any tile map updates so that we can interate over the
; DirtyTile list and *know* that it only contains tiles that are impacted by sprite changes.
_RenderSprites
; Check to see if any sprites have been added or removed. If so, then we regenerate the active
; sprite list. Since adding and removing sprites is rare, this is a worthwhile tradeoff, because
; there are several places where we want to iterate over the all of the sprites, and having a list
; and not have to constantly load and test the SPRITE_STATUS just to skip unused slots can help
; streamline the code.
lda #DIRTY_BIT_SPRITE_ARRAY
trb DirtyBits ; clears the flag, if it was set
beq :no_rebuild
jsr RebuildSpriteArray
:no_rebuild
; First step is to look at the StartX and StartY values. If the screen has scrolled, then it has
; the same effect as moving all of the sprites.
;
; OPTIMIZATION NOTE: Should check that the sprite actually changes position. If the screen scrolls
; by +X, but the sprite moves by -X (so it's relative position is unchanged), then
; it does NOT need to be marked as dirty.
stz ForceSpriteFlag
lda StartX
cmp OldStartX
bne :force_update
lda StartY
cmp OldStartY
beq :no_change
:force_update
lda #SPRITE_STATUS_MOVED
sta ForceSpriteFlag
:no_change
; Dispatch to the update process for sprites. By pre-building the list, we know exactly
; how many sprite to process and they are in a contiguous array. So we don't have to keep
; track of an iteration variable
ldx ActiveSpriteCount
jmp (phase1,x)
; Implement the logic for updating sprite and tile rendering information. Each iteration of the
; ActiveSpriteCount will call this routine with the Y-register set to the sprite index
_DoPhase1
lda _Sprites+SPRITE_STATUS,y
ora ForceSpriteFlag
; First step, if a sprite is being removed, then we just have to clear its old tile information
; and mark the tiles it overlapped as dirty.
bit #SPRITE_STATUS_REMOVED
beq :no_clear
lda _SpriteBits,y ; Clear from the sprite bitmap
sta SpriteRemovedFlag ; Stick a non-zero value here
trb SpriteMap
lda #SPRITE_STATUS_EMPTY ; Mark as empty so no error if we try to Add a sprite here again
sta _Sprites+SPRITE_STATUS,y
tyx
jsr _DeleteSprite ; Remove sprite from linked list
txy ; Restore y-register
:hidden
jmp _ClearSpriteFromTileStore ; Clear the tile flags, add to the dirty tile list and done
:no_clear
; If the sprite is hidden, then treat it like it's offscreen and clear it from the tile store. Because
; the hidden flag must be changed via the UpdateSprite function, if it's toggled, the SPRITE_STATUS_UPDATED
; flag will be set, too.
bit #SPRITE_STATUS_HIDDEN
bne :hidden
; If the sprite is marked as ADDED, then it does not need to have its old tile locations cleared
bit #SPRITE_STATUS_ADDED
bne :added
; If the sprite was not ADDED and also not MOVED, then there is no reason to erase the old tiles
; because they will be overwritten anyway.
bit #SPRITE_STATUS_MOVED
bne :moved
; Finally, see if it was updated. If not, return early
bit #SPRITE_STATUS_UPDATED
bne :updated
rts
:moved
phy
jsr _ClearSpriteFromTileStore
ply
; Anything else (MOVED, UPDATED, ADDED) will need to have the VBUFF information updated and the
; current tiles marked for update
:added
:updated
jsr _CalcDirtySprite ; This function preserves Y
lda #$FFFF!{SPRITE_STATUS_ADDED.SPRITE_STATUS_UPDATED.SPRITE_STATUS_MOVED}
and _Sprites+SPRITE_STATUS,y
sta _Sprites+SPRITE_STATUS,y ; Clear the dirty bits (ADDED, UPDATED, MOVED)
jmp _MarkDirtySpriteTiles
; Dispatch table. It's unintersting, so it's tucked out of the way
phase1 dw :phase1_0
dw :phase1_1,:phase1_2,:phase1_3,:phase1_4
dw :phase1_5,:phase1_6,:phase1_7,:phase1_8
dw :phase1_9,:phase1_10,:phase1_11,:phase1_12
dw :phase1_13,:phase1_14,:phase1_15,:phase1_16
:phase1_16 ldy activeSpriteList+30
jsr _DoPhase1
:phase1_15 ldy activeSpriteList+28
jsr _DoPhase1
:phase1_14 ldy activeSpriteList+26
jsr _DoPhase1
:phase1_13 ldy activeSpriteList+24
jsr _DoPhase1
:phase1_12 ldy activeSpriteList+22
jsr _DoPhase1
:phase1_11 ldy activeSpriteList+20
jsr _DoPhase1
:phase1_10 ldy activeSpriteList+18
jsr _DoPhase1
:phase1_9 ldy activeSpriteList+16
jsr _DoPhase1
:phase1_8 ldy activeSpriteList+14
jsr _DoPhase1
:phase1_7 ldy activeSpriteList+12
jsr _DoPhase1
:phase1_6 ldy activeSpriteList+10
jsr _DoPhase1
:phase1_5 ldy activeSpriteList+8
jsr _DoPhase1
:phase1_4 ldy activeSpriteList+6
jsr _DoPhase1
:phase1_3 ldy activeSpriteList+4
jsr _DoPhase1
:phase1_2 ldy activeSpriteList+2
jsr _DoPhase1
:phase1_1 ldy activeSpriteList
jmp _DoPhase1
:phase1_0 rts
; Utility function to calculate the difference in tile positions between a sprite's current
; position and it's previous position. This gets interesting because the number of tiles
; that a sprite covers can change based on the relative alignemen of the sprite with the
; background.
;
; Ideally, we would be able to quickly calculate exactly which new background tiles a sprite
; intersects with and which ones it has left to minimize the number of TileStore entries
; that need to be updated.
;
; In the short-term, we just do an equality test which lets us know if the sprite is
; covering the exact same tiles.
; Render a sprite stamp into the sprite buffer. Stamps exist independent of the sprites
; and sprite reference a specific stamp. This is necessary because it's common for a
; sprite to change its graphic as its animating, but it is too costly to have to set up
; the stamp every time. So this allows users to create stamps in advance and then
; assign them to the sprites as needed.
;
; Note that the user has full freedom to create a stamp at any VBUFF address, however,
; without leaving a buffer around each stamp, graphical corruption will occur. It is
; recommended that the defines for VBUFF_SPRITE_START, VBUFF_TILE_ROW_BYTES and
; VBUFF_TILE_COL_BYTES to calculate tile-aligned corner locations to lay out the
; sprite stamps in VBUFF memory.
;
; Input:
; A = sprite descriptor
; Y = vbuff address
;
; The Sprite[VBUFF_ADDR] property must be set to the vbuff address passed into this function
; to bind the sprite stamp to the sprite record.
_CreateSpriteStamp
pha ; Save the descriptor
jsr _GetBaseTileAddr ; Get the address of the tile data
tax ; Tile data address
pla ; Pop the sprite ID
jmp _DrawSpriteStamp ; Render the sprite data and create a stamp
; Add a new sprite to the rendering pipeline
;
; The tile id in the range 0 - 511. The top 7 bits are used as sprite control bits
;
; Bit 9 : Horizontal flip.
; Bit 10 : Vertical flip.
; Bits 11 - 12 : Sprite Size Selector
; 00 - 8x8 (1x1 tile)
; 01 - 8x16 (1x2 tiles)
; 10 - 16x8 (2x1 tiles)
; 11 - 16x16 (2x2 tiles)
; Bit 13 : Show/Hide sprite during rendering
; Bit 14 : Mark sprite as a compile sprite. SPRITE_DISP is treated as a compilation token.
; Bit 15 : Reserved. Must be zero.
; TBD: Bit 15 : Low Sprite priority. Draws behind high priority tiles.
;
; When a sprite has a size > 8x8, the horizontal tiles are taken from the next tile index and
; the vertical tiles are taken from tileId + 32. This is why tile sheets should be saved
; with a width of 256 pixels.
;
; A = Sprite ID / Flags
; Y = High Byte = x-pos, Low Byte = y-pos
; X = Sprite Slot (0 - 15)
_AddSprite
pha
txa
and #$000F
asl
tax
pla
sta _Sprites+SPRITE_ID,x ; Keep a copy of the full descriptor
lda #SPRITE_STATUS_ADDED ; Used to initialize the SPRITE_STATUS
sta _Sprites+SPRITE_STATUS,x
lda #$FFFF
sta _Sprites+VBUFF_ADDR,x ; Clear the VBUFF address, just to initialize it
phy
tya
and #$00FF
sta _Sprites+SPRITE_Y,x ; Y coordinate
pla
xba
and #$00FF
sta _Sprites+SPRITE_X,x ; X coordinate
; Mark the dirty bit to indicate that the active sprite list needs to be rebuilt in the next
; render call
lda #DIRTY_BIT_SPRITE_ARRAY
tsb DirtyBits
lda _SpriteBits,x ; Get the bit flag for this sprite slot
tsb SpriteMap ; Mark it in the sprite map bit field
jsr _PrecalcSpriteSize ; Cache sprite property values
jsr _PrecalcSpriteBounds
jsr _InsertSprite ; Insert it into the sorted list
jmp _Validate
; _SortSprite
;
; Given a sprite's index, i, update the sprite permutation array such that p[j] = i where
; the sprite is the j.th sprite ordered by the SPRITE_CLIP_TOP value. It is important to
; note that the sorted sprite order does not impact rendering order (that is determined by
; the sprite index position), but is only used to calculate region of the screen to update
; and, in the future, may be useful for isometric perspectives where sorting order *is*
; determined by y-position
;
; X = current sprite index
;
; The sorting strategy is to
;
; a) check if the current slot's y-pos is greater than the next item. If yes, then search forward
; b) check if the current slot's y-pos is less than the prev item. If yes, then search in reverse
; c) sprite is in the correct location
;
; The heuristic in play here is that, usually sprites will only move one position in the sorted order
; between frames, if at all.
_SortSprite
lda _Sprites+SPRITE_CLIP_TOP,x
ldy _Sprites+SORTED_PREV,x
bmi :chk_fwd
cmp _Sprites+SPRITE_CLIP_TOP,y
bcc :scan_bkwd ; The current node needs to move to an lower position
:chk_fwd
ldy _Sprites+SORTED_NEXT,x ; If there is nothing ahead of the current node, we're done
bmi :early_out
cmp _Sprites+SPRITE_CLIP_TOP,y ; If the current node is <= the next node, we're done
bcc :early_out
bne :scan_fwd
:early_out
rts
; Look to move the sprite into a later position
:scan_fwd
lda _Sprites+SORTED_NEXT,y ; Need to step forward; if we're at the end, then insert here
bmi :insert_end
tay
lda _Sprites+SPRITE_CLIP_TOP,y ; Check against the next node. If it's less that current, keep going
cmp _Sprites+SPRITE_CLIP_TOP,x
bcc :scan_fwd
; Put X before Y
;
; Change
; a <=> x <=> b
; c <=> y <=> d
;
; Into
; a <=> b and c <=> x <=> y <=> d
:insert_before
jsr _ReleaseNode
tya
sta _Sprites+SORTED_NEXT,x ; Link X to Y
lda _Sprites+SORTED_PREV,y
sta _Sprites+SORTED_PREV,x ; Link X to C
txa ; Link Y to X
sta _Sprites+SORTED_PREV,y
ldy _Sprites+SORTED_PREV,x ; Link C to X
sta _Sprites+SORTED_NEXT,y
rts
; Move X to the end of the list. Y point to the last element
;
; ; Change
; a <=> x <=> b
; y -> nil
;
; Into
; a <=> b and y <=> x -> nil
:insert_end
jsr _ReleaseNode
lda #$FFFF
sta _Sprites+SORTED_NEXT,x
tya
sta _Sprites+SORTED_PREV,x
txa
sta _Sprites+SORTED_NEXT,y
rts
; Look to move the sprite into an earlier position
:scan_bkwd
lda _Sprites+SORTED_PREV,y ; Need to step backward; if we're at the beginning, then insert here
bmi :insert_front
tay
lda _Sprites+SPRITE_CLIP_TOP,x ; Check against the next node. If it's less that current, keep going
cmp _Sprites+SPRITE_CLIP_TOP,y
bcc :scan_bkwd
; Put X after Y
;
; Change
; a <=> x <=> b
; c <=> y <=> d
;
; Into
; a <=> b and c <=> y <=> x <=> d
:insert_after
jsr _ReleaseNode
tya
sta _Sprites+SORTED_PREV,x ; c <=> y <-- x --- d
lda _Sprites+SORTED_NEXT,y ; c <=> y <-- x --> d
sta _Sprites+SORTED_NEXT,x
txa
ldx _Sprites+SORTED_NEXT,y
sta _Sprites+SORTED_NEXT,y ; c <=> y <=> x --> d
sta _Sprites+SORTED_PREV,x ; c <=> y <=> x <=> d
rts
; Move X to the front of the list. Y points to the first element
;
; ; Change
; a <=> x <=> b
; head -> y
;
; Into
; a <=> b and head -> x <=> y
:insert_front
jsr _ReleaseNode
stx _SortedHead
txa
sta _Sprites+SORTED_PREV,y
lda #$FFFF
sta _Sprites+SORTED_PREV,x
tya
sta _Sprites+SORTED_NEXT,x
:done
rts
; Take the node pointed at X and remove it from the doubly-linked list.
_ReleaseNode
phy
jsr _DeleteSprite
ply
rts
; Add a new sprite into the sorted double-linked list
_InsertSprite
lda _SortedHead ; If the list is empty, just insert the sprite index
bmi :empty
tay ; Check the first item
lda _Sprites+SPRITE_CLIP_TOP,x
cmp _Sprites+SPRITE_CLIP_TOP,y
bcc :insert_head
:next
lda _Sprites+SORTED_NEXT,y
bmi :insert_tail
tay
lda _Sprites+SPRITE_CLIP_TOP,x
cmp _Sprites+SPRITE_CLIP_TOP,y
bcs :next
lda _Sprites+SORTED_PREV,y
sta _Sprites+SORTED_PREV,x ; [p] <-- [c] [n]
tya
sta _Sprites+SORTED_NEXT,x ; [p] <-- [c] --> [n]
txa
ldx _Sprites+SORTED_PREV,y ; get ref to the [p]revious node
sta _Sprites+SORTED_PREV,y ; [p] <-- [c] <=> [n]
sta _Sprites+SORTED_NEXT,x ; [p] <=> [c] <=> [n]
rts
:insert_head
stx _SortedHead
lda #$FFFF
sta _Sprites+SORTED_PREV,x
tya
sta _Sprites+SORTED_NEXT,x
txa
sta _Sprites+SORTED_PREV,y
rts
:insert_tail
txa
sta _Sprites+SORTED_NEXT,y
tya
sta _Sprites+SORTED_PREV,x
lda #$FFFF
sta _Sprites+SORTED_NEXT,x
rts
:empty
sta _Sprites+SORTED_NEXT,x
sta _Sprites+SORTED_PREV,x
stx _SortedHead
rts
; Remove a sprite from the double-linked list
_DeleteSprite
ldy _Sprites+SORTED_NEXT,x
bmi :remove_tail
cpx _SortedHead
beq :remove_head
lda _Sprites+SORTED_PREV,x
sta _Sprites+SORTED_PREV,y
tay
lda _Sprites+SORTED_NEXT,x
sta _Sprites+SORTED_NEXT,y
rts
:remove_head
sty _SortedHead
lda #$FFFF
sta _Sprites+SORTED_PREV,y
rts
:remove_tail
ldy _Sprites+SORTED_PREV,x
bmi :make_empty
lda #$FFFF
sta _Sprites+SORTED_NEXT,y
rts
:make_empty
lda #$FFFF
sta _SortedHead
rts
; Validate the integrity of the linked list
_Validate
:prev equ tmp0
:curr equ tmp1
ldy #$FFFF
ldx _SortedHead
bmi :done
:loop
sty :prev
stx :curr
lda _Sprites+SORTED_PREV,x
cmp :prev
beq *+4
brk $08
cpy #$FFFF
beq :skip
lda _Sprites+SORTED_NEXT,y
cmp :curr
beq *+4
brk $06
lda _Sprites+SPRITE_CLIP_TOP,x
cmp _Sprites+SPRITE_CLIP_TOP,y
bcs *+4
brk $0A
:skip
txy
lda _Sprites+SORTED_NEXT,x
tax
bpl :loop
:done
rts
; Macro to make the unrolled loop more concise
;
; 1. Load the tile store address from a fixed offset
; 2. Clears the sprite bit from the TS_SPRITE_FLAG location
; 3. Checks if the tile is dirty and marks it
; 4. If the tile was dirty, save the tile store address to be added to the DirtyTiles list later
TSClearSprite mac
ldy TileStoreLookup+{]1},x
lda TileStore+TS_SPRITE_FLAG,y
and tmp0
sta TileStore+TS_SPRITE_FLAG,y
lda TileStore+TS_DIRTY,y
bne next
inc
sta TileStore+TS_DIRTY,y
tya
ldy DirtyTileCount
sta DirtyTiles,y
iny
iny
sty DirtyTileCount
next
<<<
; Alternate implementation that uses the TS_COVERAGE_SIZE and TS_LOOKUP_INDEX properties to
; load the old values directly from the TileStoreLookup table, rather than caching them.
; This is more efficient, because the work in MarkDirtySprite is independent of the
; sprite size and, by inlining the _PushDirtyTile logic, we can save a fair amount of overhead
_ClearSpriteFromTileStore
lda _SpriteBitsNot,y ; Cache this value in a direct page location
sta tmp0
ldx _Sprites+TS_COVERAGE_SIZE,y
jmp (csfts_tbl,x)
csfts_tbl dw csfts_1x1,csfts_1x2,csfts_1x3,csfts_out
dw csfts_2x1,csfts_2x2,csfts_2x3,csfts_out
dw csfts_3x1,csfts_3x2,csfts_3x3,csfts_out
dw csfts_out,csfts_out,csfts_out,csfts_out
csfts_out rts
csfts_3x3 ldx _Sprites+TS_LOOKUP_INDEX,y
TSClearSprite 0
TSClearSprite 2
TSClearSprite 4
TSClearSprite 1*{TS_LOOKUP_SPAN*2}+0
TSClearSprite 1*{TS_LOOKUP_SPAN*2}+2
TSClearSprite 1*{TS_LOOKUP_SPAN*2}+4
TSClearSprite 2*{TS_LOOKUP_SPAN*2}+0
TSClearSprite 2*{TS_LOOKUP_SPAN*2}+2
TSClearSprite 2*{TS_LOOKUP_SPAN*2}+4
rts
csfts_3x2 ldx _Sprites+TS_LOOKUP_INDEX,y
TSClearSprite 0
TSClearSprite 2
TSClearSprite 1*{TS_LOOKUP_SPAN*2}+0
TSClearSprite 1*{TS_LOOKUP_SPAN*2}+2
TSClearSprite 2*{TS_LOOKUP_SPAN*2}+0
TSClearSprite 2*{TS_LOOKUP_SPAN*2}+2
rts
csfts_3x1 ldx _Sprites+TS_LOOKUP_INDEX,y
TSClearSprite 0
TSClearSprite 1*{TS_LOOKUP_SPAN*2}+0
TSClearSprite 2*{TS_LOOKUP_SPAN*2}+0
rts
csfts_2x3 ldx _Sprites+TS_LOOKUP_INDEX,y
TSClearSprite 0
TSClearSprite 2
TSClearSprite 4
TSClearSprite 1*{TS_LOOKUP_SPAN*2}+0
TSClearSprite 1*{TS_LOOKUP_SPAN*2}+2
TSClearSprite 1*{TS_LOOKUP_SPAN*2}+4
rts
csfts_2x2 ldx _Sprites+TS_LOOKUP_INDEX,y
TSClearSprite 0
TSClearSprite 2
TSClearSprite 1*{TS_LOOKUP_SPAN*2}+0
TSClearSprite 1*{TS_LOOKUP_SPAN*2}+2
rts
csfts_2x1 ldx _Sprites+TS_LOOKUP_INDEX,y
TSClearSprite 0
TSClearSprite 1*{TS_LOOKUP_SPAN*2}+0
rts
csfts_1x3 ldx _Sprites+TS_LOOKUP_INDEX,y
TSClearSprite 0
TSClearSprite 2
TSClearSprite 4
rts
csfts_1x2 ldx _Sprites+TS_LOOKUP_INDEX,y
TSClearSprite 0
TSClearSprite 2
rts
csfts_1x1 ldx _Sprites+TS_LOOKUP_INDEX,y
TSClearSprite 0
rts
; Use the blttmp space to build the active sprite list. Since the sprite tiles are not drawn until later,
; it's OK to use that scratch space here. And it's just the right size, 32 bytes
RebuildSpriteArray
lda SpriteMap ; Get the bit field
; Unrolled loop to get the sprite index values that correspond to the set bit positions
pea $FFFF ; end-of-list marker
]step equ 0
lup 4
lsr
bcc :skip_1
pea ]step
:skip_1 lsr
bcc :skip_2
pea ]step+2
:skip_2 lsr
bcc :skip_3
pea ]step+4
:skip_3 lsr
bcc :skip_4
pea ]step+6
:skip_4 beq :end_1
]step equ ]step+8
--^
:end_1
; Now pop the values off of the stack until reaching the sentinel value. This could be unrolled, but
; it is only done once per frame.
ldx #0
:loop
pla
bmi :out
sta activeSpriteList,x
inx
inx
bra :loop
:out
stx ActiveSpriteCount
rts
; _GetTileAt
;
; Given a relative playfield coordinate [0, ScreenWidth), [0, ScreenHeight) return the
; X = horizontal point [0, ScreenTileWidth]
; Y = vertical point [0, ScreenTileHeight]
;
; Return
; C = 1, out of range
; C = 0, X = column, Y = row
_GetTileAt
cpx ScreenWidth
bcc *+3
rts
cpy ScreenHeight
bcc *+3
rts
tya ; carry is clear here
adc StartYMod208 ; This is the code field line that is at the top of the screen
cmp #208
bcc *+5
sbc #208
lsr
lsr
lsr
tay ; This is the code field row for this point
clc
txa
adc StartXMod164
cmp #164
bcc *+5
sbc #164
lsr
lsr
tax ; Could call _CopyBG0Tile with these arguments
clc
rts
; Small initialization routine to cache the banks for the sprite data and mask and tile/sprite stuff
_CacheSpriteBanks
lda #>spritemask
and #$FF00
ora #^spritedata
sta SpriteBanks
lda #$0100
ora #^TileStore
sta TileStoreBankAndBank01
lda #>tiledata
and #$FF00
ora #^TileStore
sta TileStoreBankAndTileDataBank
xba
ldx #$100
sta DP2_TILEDATA_AND_TILESTORE_BANKS,x ; put a reversed copy in the second direct page
and #$FF00
ora #$0001
sta DP2_BANK01_AND_TILESTORE_BANKS,x ; put a value with bank 01 and the tile store
lda #>spritedata
and #$FF00
ora #^tiledata
sta DP2_TILEDATA_AND_SPRITEDATA_BANKS,x
lda #>spritedata
and #$FF00
ora #^TileStore
xba
ldx #$100
sta DP2_SPRITEDATA_AND_TILESTORE_BANKS,x ; put a reversed copy in the second direct page
lda #>TileStore
and #$FF00
ora #^TileStore
sta TileStoreBankDoubled
rts
; Precalculate some cached values for a sprite. These are *only* to make other parts of code,
; specifically the draw/erase routines more efficient.
;
; X = sprite index
_PrecalcSpriteVBuff
lda _Sprites+SPRITE_ID,x ; Compiled sprites use the SPRITE_DISP as a fixed address to compiled code
bit #SPRITE_COMPILED
bne :compiled
xba
and #$0006
tay
lda _Sprites+VBUFF_ADDR,x
clc
adc _stamp_step,y
sta _Sprites+SPRITE_DISP,x ; Interpreted as an address in the VBUFF bank
rts
:compiled
xba
and #$0006 ; Pick the address from the table of 4 values. Can use this value directly
clc ; as an index
adc _Sprites+VBUFF_ADDR,x
tay
lda [CompileBank0],y
sta _Sprites+SPRITE_DISP,x ; Interpreted as an address in the CompileBank
rts
; Compile the four stamps and keep a reference to the addresses. We take the current CompileBankTop address and allocate 8 bytes
; of memory. Then compile each stamp and save the compilation address in the header area. Finally, the DISP_ADDR is set
; to that value and the SPRITE_COMPILED bit is set in the SPRITE_ID word.
;
; A = sprite Id
; X = vbuff base address
_CompileStampSet
:height equ tmp8
:width equ tmp9
:base equ tmp10
:output equ tmp11
:addrs equ tmp12 ; 4 words (tmp12, tmp13, tmp14 and tmp15)
; Save the base address
stx :base
; Initialize the height and width based on the sprite flags
ldy #8
sty :height
ldx #4
stx :width
bit #$1000 ; wide flag
beq :skinny
ldx #8
stx :width
:skinny
bit #$0800 ; tall flag
beq :short
ldy #16
sty :height
:short
lda CompileBankTop
sta :output ; Save the current address as the return value
clc
adc #8
sta CompileBankTop ; Allocate space for the 4 addresses return by _CompileStamp
; ldy :height ; X and Y are already set for the first call
; ldx :width
lda :base
jsr _CompileStamp ; Compile into the bank
sta :addrs ; Save the address temporarily
ldy :height
ldx :width
clc
lda :base
adc _stamp_step+2
jsr _CompileStamp
sta :addrs+2
ldy :height
ldx :width
clc
lda :base
adc _stamp_step+4
jsr _CompileStamp
sta :addrs+4
ldy :height
ldx :width
clc
lda :base
adc _stamp_step+6
jsr _CompileStamp
sta :addrs+6
; Now the sprite stamps are all compiled. Set the bank to the compilation bank and fill in the header
phb
ldy :output
pei CompileBank
plb
lda :addrs
sta: 0,y
lda :addrs+2
sta: 2,y
lda :addrs+4
sta: 4,y
lda :addrs+6
sta: 6,y
plb
plb
tya ; Put the output value into the accumulator
clc ; No error
rts
_PrecalcSpriteSize
; Set the sprite's width and height
lda #4
sta _Sprites+SPRITE_WIDTH,x
lda #8
sta _Sprites+SPRITE_HEIGHT,x
lda _Sprites+SPRITE_ID,x
bit #$1000 ; width select
beq :width_4
lda #8
sta _Sprites+SPRITE_WIDTH,x
:width_4
lda _Sprites+SPRITE_ID,x
bit #$0800 ; height select
beq :height_8
lda #16
sta _Sprites+SPRITE_HEIGHT,x
:height_8
rts
; Clip the sprite's bounding box to the play field size and also set a flag if the sprite
; is fully off-screen or not
_PrecalcSpriteBounds
lda _Sprites+SPRITE_X,x
bpl :pos_x
lda #0
:pos_x cmp ScreenWidth
bcs :offscreen ; sprite is off-screen, exit early
sta _Sprites+SPRITE_CLIP_LEFT,x
lda _Sprites+SPRITE_Y,x
bpl :pos_y
lda #0
:pos_y cmp ScreenHeight
bcs :offscreen ; sprite is off-screen, exit early
sta _Sprites+SPRITE_CLIP_TOP,x
lda _Sprites+SPRITE_X,x
clc
adc _Sprites+SPRITE_WIDTH,x
dec
bmi :offscreen
cmp ScreenWidth
bcc :ok_x
lda ScreenWidth
dec
:ok_x sta _Sprites+SPRITE_CLIP_RIGHT,x
lda _Sprites+SPRITE_Y,x
clc
adc _Sprites+SPRITE_HEIGHT,x
dec
bmi :offscreen
cmp ScreenHeight
bcc :ok_y
lda ScreenHeight
dec
:ok_y sta _Sprites+SPRITE_CLIP_BOTTOM,x
stz _Sprites+IS_OFF_SCREEN,x ; passed all of the off-screen tests
; Calculate the clipped width and height
lda _Sprites+SPRITE_CLIP_RIGHT,x
sec
sbc _Sprites+SPRITE_CLIP_LEFT,x
inc
sta _Sprites+SPRITE_CLIP_WIDTH,x
lda _Sprites+SPRITE_CLIP_BOTTOM,x
sec
sbc _Sprites+SPRITE_CLIP_TOP,x
inc
sta _Sprites+SPRITE_CLIP_HEIGHT,x
rts
:offscreen
lda #1
sta _Sprites+IS_OFF_SCREEN,x
rts
; Remove a sprite from the list. Just mark its STATUS as FREE and it will be
; picked up in the next AddSprite.
;
; A = Sprite ID
_RemoveSprite
cmp #MAX_SPRITES
bcc :ok
rts
:ok
asl
tax
lda _Sprites+SPRITE_STATUS,x
ora #SPRITE_STATUS_REMOVED
sta _Sprites+SPRITE_STATUS,x
rts ; The _DeleteSprite call is made in _DoPhase1 during the next render
; Update the sprite's flags. We do not allow the size of a sprite to be changed. That requires
; the sprite to be removed and re-added.
;
; A = Sprite slot
; X = New Sprite Flags
; Y = New Sprite Stamp Address | New Compiled Sprite Token
_UpdateSprite
cmp #MAX_SPRITES
bcc :ok
rts
:ok
phx ; Save X to swap into A
asl
tax
pla
; Do some work to see if only the H or V bits have changed. If so, merge them into the
; SPRITE_ID
eor _Sprites+SPRITE_ID,x ; If either bit has changed, this will be non-zero
and #SPRITE_VFLIP+SPRITE_HFLIP+SPRITE_HIDE
bne :sprite_flag_change
tya
cmp _Sprites+VBUFF_ADDR,x ; Did the stamp change?
bne :sprite_stamp_change
rts ; Nothing changed, so just return
:sprite_flag_change
eor _Sprites+SPRITE_ID,x ; put the new bits into the value. ---HV--- ^ SPRITE_ID & 00011000 ^ SPRITE_ID = SSSHVSSS
sta _Sprites+SPRITE_ID,x ; Keep a copy of the full descriptor
bit #SPRITE_HIDE ; Sync the SPRITE_HIDE bit into the SPRITE_STATUS_HIDDEN flag
beq :not_hidden
lda #SPRITE_STATUS_HIDDEN
ora _Sprites+SPRITE_STATUS,x
sta _Sprites+SPRITE_STATUS,x
bra :sync_complete
:not_hidden lda #$FFFF!SPRITE_STATUS_HIDDEN
and _Sprites+SPRITE_STATUS,x
sta _Sprites+SPRITE_STATUS,x
:sync_complete
tya
:sprite_stamp_change
sta _Sprites+VBUFF_ADDR,x ; Just save this to stay in sync
lda _Sprites+SPRITE_STATUS,x ; Mark this sprite as updated
ora #SPRITE_STATUS_UPDATED
sta _Sprites+SPRITE_STATUS,x
jmp _PrecalcSpriteVBuff ; Cache stuff and return
; Move a sprite to a new location. If the tile ID of the sprite needs to be changed, then
; a full remove/add cycle needs to happen
;
; A = sprite slot
; X = x position
; Y = y position
_MoveSprite
cmp #MAX_SPRITES
bcc :ok
rts
:ok
phx ; Save X to swap into A
asl
tax
pla
cmp _Sprites+SPRITE_X,x
bne :changed1
tya
cmp _Sprites+SPRITE_Y,x
bne :changed2
rts
:changed1
sta _Sprites+SPRITE_X,x ; Update the X coordinate
tya
:changed2
sta _Sprites+SPRITE_Y,x ; Update the Y coordinate
lda _Sprites+SPRITE_STATUS,x
ora #SPRITE_STATUS_MOVED
sta _Sprites+SPRITE_STATUS,x
jsr _PrecalcSpriteBounds ; Can be specialized to only update (x,y) values
jsr _SortSprite ; Update the sprite's sorted position
jmp _Validate