iigs-game-engine/src/Render.s

1172 lines
39 KiB
ArmAsm
Raw Normal View History

; Renders a frame of animation
;
; The render function is the point of committment -- most of the APIs that set sprites and
; update coordinates are lazy; they simply save their values and set a dirty flag in the
; DirtyBits word.
;
; This function examines the dirty bits and actually performs the work to update the code field
; and internal data structure to properly render the play field. Then the update pipeline is
; executed.
;
2023-01-02 17:04:26 +00:00
; There are two major rendering modes: a composited mode and a scanline mode. The composited mode
; will render all of the sprites into the playfield tiles, and then perform a single blit to update
; the entire playfield. The scanline mode utilized shadowing and blits the background scanlines
; on sprite lines first, then draws the sprites and finally exposes the updated scanlines.
;
; The composited mode has the advantages of being able to render sprites behind tile data as well
; as avoiding most overdraw. The scanline mode is able to draw sprites correctly even when scanline
; effect are used on the background and has lower overhead, which can make it faster in some cases,
; even with the additional overdraw.
2022-04-29 17:38:04 +00:00
;
; TODO -- actually check the dirty bits and be selective on what gets updated. For example, if
; only the Y position changes, then we should only need to set new values on the
; virtual lines that were brought on screen. If the X position only changes by one
; byte, then we may have to change the CODE_ENTRY values or restore/set new OPCODE
; values, but not both.
; It's important to do _ApplyBG0YPos first because it calculates the value of StartY % 208 which is
; used in all of the other loops
_Render
2022-07-16 20:22:23 +00:00
sta RenderFlags
lda LastRender ; Check to see what kind of rendering was done on the last frame. If
beq :no_change ; it was not this renderer,
jsr _ResetToNormalTileProcs
jsr _Refresh
:no_change
2022-07-05 04:55:32 +00:00
jsr _DoTimers ; Run any pending timer tasks
2022-05-27 00:36:40 +00:00
stz SpriteRemovedFlag ; If we remove a sprite, then we need to flag a rebuild for the next frame
jsr _ApplyBG0YPos ; Set stack addresses for the virtual lines to the physical screen
lda #RENDER_BG1_ROTATION
bit RenderFlags
bne :skip_bg1_y
jsr _ApplyBG1YPos ; Set the y-register values of the blitter
:skip_bg1_y
; _ApplyBG0Xpos need to be split because we have to set the offsets, then draw in any updated tiles, and
; finally patch out the code field. Right now, the BRA operand is getting overwritten by tile data.
jsr _ApplyBG0XPosPre
jsr _ApplyBG1XPosPre
jsr _RenderSprites ; Once the BG0 X and Y positions are committed, update sprite data
2022-06-27 03:08:42 +00:00
jsr _UpdateBG0TileMap ; and the tile maps. These subroutines build up a list of tiles
2022-08-17 00:47:19 +00:00
; jsr _UpdateBG1TileMap ; that need to be updated in the code field
2023-03-03 05:24:03 +00:00
jsr _ApplyTiles ; This function actually draws the new tiles into the code field
jsr _ApplyBG0XPos ; Patch the code field instructions with exit BRA opcode
2023-01-02 17:04:26 +00:00
lda #RENDER_BG1_ROTATION
bit RenderFlags
bne :skip_bg1_x
jsr _ApplyBG1XPos ; Update the direct page value based on the horizontal position
:skip_bg1_x
; The code fields are locked in now and ready to be rendered. See if there is an overlay or any
; other reason to render with shadowing off. Otherwise, just do things quickly.
lda Overlays+OVERLAY_ID
beq :no_ovrly
jsr _ShadowOff
; Shadowing is turned off. Render all of the scan lines that need a second pass. One
; optimization that can be done here is that the lines can be rendered in any order
; since it is not shown on-screen yet.
2023-01-02 17:04:26 +00:00
ldx Overlays+OVERLAY_TOP ; Blit the full virtual buffer to the screen
ldy Overlays+OVERLAY_BOTTOM
iny
jsr _BltRange
; Turn shadowing back on
jsr _ShadowOn
; Now render all of the remaining lines in top-to-bottom (or bottom-to-top) order
jsr _DoOverlay
2023-01-02 17:04:26 +00:00
ldx Overlays+OVERLAY_BOTTOM
inx
cpx ScreenHeight
beq :done
ldy ScreenHeight
jsr _BltRange
bra :done
:no_ovrly
2021-11-15 18:23:38 +00:00
ldx #0 ; Blit the full virtual buffer to the screen
ldy ScreenHeight
jsr _BltRange
:done
ldx #0
ldy ScreenHeight
jsr _BltSCB
2021-11-15 18:23:38 +00:00
lda StartYMod208 ; Restore the fields back to their original state
ldx ScreenHeight
jsr _RestoreBG0Opcodes
lda StartY
sta OldStartY
lda StartX
sta OldStartX
lda BG1StartY
sta OldBG1StartY
lda BG1StartX
sta OldBG1StartX
stz DirtyBits
2022-02-19 02:43:55 +00:00
stz LastRender ; Mark that a full render was just performed
2022-05-27 00:36:40 +00:00
lda SpriteRemovedFlag ; If any sprite was removed, set the rebuild flag
beq :no_removal
lda #DIRTY_BIT_SPRITE_ARRAY
sta DirtyBits
:no_removal
rts
2022-01-20 02:58:57 +00:00
_DoOverlay
2023-01-02 17:04:26 +00:00
lda Overlays+OVERLAY_PROC
stal :disp+1
2023-01-02 17:04:26 +00:00
lda Overlays+OVERLAY_PROC+1
stal :disp+2
lda ScreenY0 ; pass the address of the first line of the overlay
clc
2023-01-02 17:04:26 +00:00
adc Overlays+OVERLAY_TOP
asl
tax
lda ScreenAddr,x
clc
adc ScreenX0
:disp jsl $000000
rts
2022-08-14 12:45:58 +00:00
2022-08-11 19:14:25 +00:00
; Use the per-scanline tables to set the screen. This is really meant to be used without the built-in tilemap
; support and is more of a low-level way to control the background rendering
_RenderScanlines
lda BG1YTable ; Make sure we're in the right mode (0 = scanline mode, $1800 = normal mode)
beq :ytbl_ok
lda #1
jsr _ResetBG1YTable
:ytbl_ok
2022-08-11 19:14:25 +00:00
jsr _ApplyBG0YPos ; Set stack addresses for the virtual lines to the physical screen
jsr _ApplyScanlineBG1YPos ; Set the y-register values of the blitter
2022-08-11 19:14:25 +00:00
; _ApplyBG0Xpos need to be split because we have to set the offsets, then draw in any updated tiles, and
; finally patch out the code field. Right now, the BRA operand is getting overwritten by tile data.
jsr _ApplyBG0XPosPre
2022-08-14 12:45:58 +00:00
jsr _ApplyBG1XPosPre
2022-08-11 19:14:25 +00:00
jsr _ApplyScanlineBG0XPos ; Patch the code field instructions with exit BRA opcode
jsr _ApplyScanlineBG1XPos
2023-03-09 21:29:58 +00:00
2023-04-26 04:39:09 +00:00
jsr _FilterObjectList ; Walk the sorted list and create an array of objects that need to be rendered
2023-03-09 21:29:58 +00:00
2023-04-26 04:39:09 +00:00
jsr _ShadowOff ; Turn off shadowing and draw all the scanlines with sprites on them
jsr _DrawObjShadow ; Draw the background
jsr _DrawDirectSprites ; Draw the sprites directly to the Bank $01 graphics buffer (skipping the render-to-tile step)
2023-03-09 21:29:58 +00:00
2023-04-26 04:39:09 +00:00
jsr _ShadowOn ; Turn shadowing back on
jsr _DrawFinalPass ; Expose the shadowed areas and draw overlays
2022-08-14 12:45:58 +00:00
2023-04-26 04:39:09 +00:00
lda StartYMod208 ; Restore the fields back to their original state
2022-08-11 19:14:25 +00:00
ldx ScreenHeight
jsr _RestoreScanlineBG0Opcodes
lda StartY
sta OldStartY
lda StartX
sta OldStartX
lda BG1StartY
sta OldBG1StartY
lda BG1StartX
sta OldBG1StartX
stz DirtyBits
stz LastRender ; Mark that a full render was just performed
lda SpriteRemovedFlag ; If any sprite was removed, set the rebuild flag
beq :no_removal
lda #DIRTY_BIT_SPRITE_ARRAY
sta DirtyBits
:no_removal
rts
2023-04-26 04:39:09 +00:00
; After the sprites have been filtered, we have a linked list with all of the contiguous sprite regions merged together, so
; when provessing this list we really only have to consider complications from overlays.
;
; Pseudo-code
;
; 0. Set the cursor to the top of the screen
; 1. Load the next segment
; a. If no segments, just draw the full screen
; 2. Draw the background from the cursor to the top of the current segment
; 3. If the current segment is a sprite
; a. Peek at the next segment
; b. If no more segments, then finish
; c. If it's past the bottom, PEI slam the current segment and go to [1]
; d. Must be an overlay
; i. PEI slam up to the overlay top
; ii. Does the sprite extend past the overlay? If yes, split the sprite and insert into the list
; iii. Go to [1]
; 4. If the current segment is an overlay
; a. Peek at the next segment
; b. If no more segments, then finish
; c. If it's past the bottom, draw the overlay and go to [1]
; d. Must be a sprite
; i. Draw the overlay
; ii. Change the sprite segment to start after the overlay
; iii. Go to [1]
_DrawFinalPass2
:cursor equ tmp8
:bottom equ tmp9
stz :cursor
ldy #0
cpy ObjectListCount
bne :enter
ldx #0 ; If there are no object to render, just draw the screen
ldy ScreenHeight
jmp _BltRange
:enter
ldx ObjectList+OL_INDEX,y ; Load the index of the next object record
; Draw the background up to the top line of the next object
phxy
ldy _Sprites+SPRITE_CLIP_TOP,x
ldx :cursor
sty :cursor ; Update the cursor since we have the value
jsr _BltRange
plyx
:_oloop
lda _Sprites+SPRITE_CLIP_BOTTOM,x
sta :bottom
; Load the ID to see what kind of object comes next
lda _Sprites+SPRITE_ID,x ; See if we are processing an overlay or a sprite region
bit #SPRITE_OVERLAY
jne :_overlay
:_sprite
iny
iny
cpy ObjectListCount
jeq :_sprite_end ; If this is the last object, end now on the sprite
ldx ObjectList+OL_INDEX,y ; Load the index of the next item
lda :bottom
cmp _Sprites+SPRITE_CLIP_TOP,x
bcs :_smerge ; If the prior sprite ends before this object, then handle it
phxy
ldy _Sprites+SPRITE_CLIP_TOP,x ; A = :bottom, so load the top of the next object and
sty :bottom ; save it as it is the bottom after the PEISlam
ldx :cursor ; X = :cursor
sta :cursor ; The current :bottom becomes the :cursor after the PEISlam
tay ; Y = :bottom
jsr _PEISlam
ldx :cursor ; This is the previous :bottom value
ldy :bottom ; This is the SPRITE_CLIP_TOP,x value
sty :cursor
jsr _BltRange
plyx
brl :_oloop ; Branch back, it's like starting from from scratch
:_smerge
lda _Sprites+SPRITE_ID,x ; Before we merge, need to know if objects are compatible
bit #SPRITE_OVERLAY
bne :_somerge
lda _Sprites+SPRITE_CLIP_BOTTOM,x ; Can be merged, so pick the largest bottom value and
max :bottom ; continue on as a sprite
sta :bottom
brl :_sprite
:_somerge
phxy
ldx :cursor
ldy _Sprites+SPRITE_CLIP_TOP,x ; PEI Slam to the top of the overlay (:bottom is greater than this value)
sty :cursor
jsr _PEISlam
lda 3,s ; Retrieve the sprite index
tax
jsr _DrawOverlay
plyx
lda _Sprites+SPRITE_CLIP_BOTTOM,x ; This is how far we've drawn. Check to see if we're beyond the current :bottom
sta :cursor
cmp :bottom
jcc :_sprite ; Previous sprite extends past the overlay, continue
; The overlay can cause the cursor to jump ahead an arbitrary distance. We need to continue to scan through the list until
; we find an item that has a bottom greater than the current :cursor
:_so_loop
iny
iny
cpy ObjectListCount
beq :_end
ldx ObjectList+OL_INDEX,y
lda :cursor
cmp _Sprites+SPRITE_CLIP_BOTTOM,x
bcs :_so_loop
cmp _Sprites+SPRITE_CLIP_TOP,x ; Check to see if there is any background that need to be drawn
jcs :_oloop ; If not, go back the see what kind of object it is
phxy
ldy _Sprites+SPRITE_CLIP_TOP,x
ldx :cursor
sty :cursor
jsr _BltRange
plyx
brl :_oloop
; If the last item is a sprite, do a PEI slam from the cursor to the sprite bottom and then blit any remaining
; backround
:_sprite_end
ldx :cursor
ldy :bottom
jsr _PEISlam
ldx :bottom
ldy ScreenHeight
jmp _BltRange
; If there are no more items to process, but we haven't reached the end of the screen, blit the rest of the
; background
:_end
ldx :cursor
ldy ScreenHeight
jmp _BltRange
; An overlay is a bit easier. It just needs to be rendered and then advance to the next object that's not
; covered by it
:_overlay
phxy
jsr _DrawOverlay ; Draw the overlay
plyx
lda :bottom
sta :cursor
brl :_so_loop
_DrawFinalPass
:cursor equ tmp8
stz :cursor ; Current mark in the sweep down the screen
ldy ObjectListHead ; If there are no items, just _BltRange the rest of the screen and return
jmi :finish
:loop
ldx :cursor
lda ObjectList+OL_SPRITE_TOP,y
sta :cursor
jsr :_BltRange3 ; Expose from the cursor to the top and update the cursor
lda ObjectList+OL_SPRITE_ID,y ; See if we are processing an overlay or a sprite region
bit #SPRITE_OVERLAY
jne :obj_is_overlay
:obj_is_sprite
ldx ObjectList+OL_NEXT,y
jmi :sprite_complete
; Look at the next item, if it's below the current sprite range, do a slam and move on to the next item
lda ObjectList+OL_CLIP_BOTTOM,y
cmp ObjectList+OL_CLIP_TOP,x
bcs :sprite_overlap
txy ; Move to the next sprite
ldx :cursor
sta :cursor ; A = bottom, x = :cursor
jsr _PEISlam
bra :loop ; Loop back and do the BltRange up to the top of the next sprite
; Now we know that the next item must be an overlay (because sprite ranges are already combined), so go ahead
; and PEI slam up to the top of the overlay
:sprite_overlap
lda ObjectList+OL_CLIP_TOP,x
ldx :cursor
sta :cursor
jsr _PEISlam
lda ObjectList+OL_CLIP_BOTTOM,x ; If the overlay is fully within the sprite, do extra work.
cmp ObjectList+OL_CLIP_BOTTOM,y ; Otherwise continue knowing we are currently handling an overlay
bcc :split
txy
bra :overlay_next
:split
jsr split
bra :obj_is_sprite
; Do a similar process for the overlays
:obj_is_overlay
ldx ObjectList+OL_NEXT,y
bmi :ovrly_complete
; Look at the next item, if it's below the current overlay range, draw the overlay and move on to the next item
lda ObjectList+OL_CLIP_BOTTOM,y
cmp ObjectList+OL_CLIP_TOP,x
bcs :ovrly_overlap
jsr DrawOverlayY
txy ; Move to the next item
bra :loop ; Loop back and do the BltRange up to the top of the next sprite
; Now we know that the next item must be a sprite. Skip any sprite that are completely covered by the overlay. If
; a sprite is split by the overlay, then reduce the top value
:ovrly_overlap
lda ObjectList+OL_CLIP_TOP,x
ldx :cursor
sta :cursor
jsr PEISlam
lda ObjectList+OL_CLIP_BOTTOM,x ; If the next
cmp ObjectList+OL_CLIP_BOTTOM,y ; Otherwise continue knowing we are currently handling an overlay
bcc :split
txy
bra :overlay_next
; When a sprite is the last item before the end of the screen, jump here
:sprite_complete
ldx :cursor
lda ObjectList+OL_CLIP_BOTTOM,y
sta :cursor
tay
jsr PEISlam
; Jump here when there are no items left to process.
:finish
ldx :cursor
ldy ScreenHeight
jmp _BltRange
lda ObjectList+OL_CLIP_BOTTOM,x ; If the overlay is fully within the sprite, do extra work.
cmp ObjectList+OL_CLIP_BOTTOM,y ; Otherwise continue knowing we are currently handling an overlay
bcc :split
txy
bra :overlay_next
; Run through all of the tiles on the DirtyTile list and render them
_ApplyTiles
ldx DirtyTileCount
phd ; sve the current direct page
tdc
clc
adc #$100 ; move to the next page
tcd
stx DP2_DIRTY_TILE_COUNT ; Cache the dirty tile count
jsr _PopDirtyTilesFast
pld ; Move back to the original direct page
stz DirtyTileCount ; Reset the dirty tile count
rts
2022-02-19 02:43:55 +00:00
; This is a specialized render function that only updates the dirty tiles *and* draws them
2022-01-20 02:58:57 +00:00
; directly onto the SHR graphics buffer. The playfield is not used at all. In some way, this
; ignores almost all of the capabilities of GTE, but it does provide a convenient way to use
; the sprite subsystem + tile attributes for single-screen games which should be able to run
; close to 60 fps.
;
; In this renderer, we assume that there is no scrolling, so no need to update any information about
2022-01-20 02:58:57 +00:00
; the BG0/BG1 positions
_RenderDirty
lda LastRender ; If the full renderer was last called, we assume that
bne :norecalc ; the scroll positions have likely changed, so recalculate
jsr _RecalcTileScreenAddrs ; them to make sure sprites draw at the correct screen address
jsr _ResetToDirtyTileProcs ; Switch the tile procs to the dirty tile rendering functions
; jsr _ClearSpritesFromCodeField ; Restore the tiles to their non-sprite versions
:norecalc
jsr _RenderSprites
jsr _ApplyDirtyTiles
2022-04-29 17:38:04 +00:00
lda #1
sta LastRender
2022-01-20 02:58:57 +00:00
rts
_ApplyDirtyTiles
phd ; save the current direct page
tdc
clc
adc #$100 ; move to the next page
tcd
2022-01-20 02:58:57 +00:00
bra :begin
:loop
; Retrieve the offset of the next dirty Tile Store items in the Y-register
jsr _PopDirtyTile2
; Call the generic dispatch with the Tile Store record pointer at by the Y-register.
jsr _RenderDirtyTile
; Loop again until the list of dirty tiles is empty
:begin ldy DirtyTileCount
2022-01-20 02:58:57 +00:00
bne :loop
pld ; Move back to the original direct page
stz DirtyTileCount ; Reset the dirty tile count
2022-01-20 02:58:57 +00:00
rts
2023-01-02 17:04:26 +00:00
; This rendering mode turns off shadowing and draws all of the relevant background lines and then
; draws sprites on top of the background before turning shadowing on and exposing the lines to the
; screen. Even though entire lines are drawn twice, it's so efficient that it is often faster
; than using all of the logic to draw/erase tiles in the TileBuffer, even though less visible words
; are touched.
;
2023-04-26 04:39:09 +00:00
; This mode is also necessary if per-scanling rendering is used since sprites would not look correct
2023-01-02 17:04:26 +00:00
; if each line had independent offsets.
_RenderWithShadowing
sta RenderFlags
jsr _DoTimers ; Run any pending timer tasks
jsr _ApplyBG0YPos ; Set stack addresses for the virtual lines to the physical screen
jsr _ApplyBG1YPos ; Set the y-register values of the blitter
; _ApplyBG0Xpos need to be split because we have to set the offsets, then draw in any updated tiles, and
; finally patch out the code field. Right now, the BRA operand is getting overwritten by tile data.
jsr _ApplyBG0XPosPre
jsr _ApplyBG1XPosPre
jsr _UpdateBG0TileMap ; and the tile maps. These subroutines build up a list of tiles
; jsr _UpdateBG1TileMap ; that need to be updated in the code field
2023-01-02 17:04:26 +00:00
jsr _ApplyTiles ; This function actually draws the new tiles into the code field
jsr _ApplyBG0XPos ; Patch the code field instructions with exit BRA opcode
jsr _ApplyBG1XPos ; Update the direct page value based on the horizontal position
; At this point, everything in the background has been rendered into the code field. Next, we need
; to create priority lists of scanline ranges.
2023-04-26 04:39:09 +00:00
; jsr _BuildShadowList ; Create the ranges based on the sorted sprite y-values
jsr _ShadowOff ; Turn off shadowing and draw all the scanlines with sprites on them
jsr _DrawShadowList
jsr _DrawDirectSprites ; Draw the sprites directly to the Bank $01 graphics buffer (skipping the render-to-tile step)
jsr _ShadowOn ; Turn shadowing back on
jsr _DrawFinalPass
2023-01-02 17:04:26 +00:00
;
; The objects that need to be reasoned about are
;
; 1. Sprites
; 2. Overlays
; a. Solid High Priority
; b. Solid Low Priority
; c. Masked High Priority
; d. Masked Low Priority
; 3. Background
;
; Notes:
;
; A High Priority overlay is rendered above the sprites
; A Low Priority overlay is rendered below the sprites
; A Solid High Priority overlay obscured everything and if the only thing drawn on the scanline
;
; The order of draw oprations is:
;
; 1. Turn off shadowing
; 2. Draw the background for scanlines with (Sprites OR a Masked Low Priority overlay) AND NOT a Solid Low Priority overlay
; 3. Draw the Solid Low Priority overlays
; 4. Draw the Sprites
; 5. Draw the Masked Low Priority overlays
; 6. Turn on shadowing
; 7. Draw, in top-to-bottom order
; a. Background lines not drawn yet
; b. PEI Slam lines with (Sprites OR a Masked Low Priority Overlay) AND NOT a High Priority overlay
; c. High Priority overlays
;
2023-04-26 04:39:09 +00:00
; The work of this routine is to quickly build a sorted list of scanline ranges that can call the appropriate
2023-01-02 17:04:26 +00:00
; sub-renderer
; jsr BuildShadowSegments
;
; The trick is to create a bit-field mapping for the different actions to define
; lda Overlays
; beq :no_ovrly
;
; jsr _ShadowOff
2023-01-02 17:04:26 +00:00
; Shadowing is turned off. Render all of the scan lines that need a second pass. One
; optimization that can be done here is that the lines can be rendered in any order
; since it is not shown on-screen yet.
; ldx Overlays+OVERLAY_TOP ; Blit the full virtual buffer to the screen
; ldy Overlays+OVERLAY_BOTTOM
; jsr _BltRange
2023-01-02 17:04:26 +00:00
; Turn shadowing back on
; jsr _ShadowOn
2023-01-02 17:04:26 +00:00
; Now render all of the remaining lines in top-to-bottom (or bottom-to-top) order
; ldx #0
; ldy Overlays+OVERLAY_TOP
; beq :skip
; jsr _BltRange
;:skip
; jsr _DoOverlay
2023-01-02 17:04:26 +00:00
; ldx Overlays+OVERLAY_BOTTOM
; cpx ScreenHeight
; beq :done
; ldy ScreenHeight
; jsr _BltRange
; bra :done
2023-01-02 17:04:26 +00:00
;:no_ovrly
2023-01-02 17:04:26 +00:00
; ldx #0 ; Blit the full virtual buffer to the screen
; ldy ScreenHeight
; jsr _BltRange
;:done
; ldx #0
; ldy ScreenHeight
; jsr _BltSCB
2023-01-02 17:04:26 +00:00
lda StartYMod208 ; Restore the fields back to their original state
ldx ScreenHeight
jsr _RestoreBG0Opcodes
lda StartY
sta OldStartY
lda StartX
sta OldStartX
lda BG1StartY
sta OldBG1StartY
lda BG1StartX
sta OldBG1StartX
stz DirtyBits
stz LastRender ; Mark that a full render was just performed
lda SpriteRemovedFlag ; If any sprite was removed, set the rebuild flag
beq :no_removal
lda #DIRTY_BIT_SPRITE_ARRAY
sta DirtyBits
:no_removal
rts
; Look at the overlay list and the sprite list and figure out which scanline ranges need to be
2023-01-02 17:04:26 +00:00
; blitted in what order. We try to build all of the scan line segments lists because that
; saves the work of re-scanning the lists.
;
; The semgent list definitions are:
;
; BLIT_W_SHADOW_OF
BuildShadowSegments
; ldx _SortedHead
; bmi :no_sprite
;:loop
; lda _Sprites+CLIP_TOP,x
; lda _Sprites+SORTED_NEXT,x
; tax
; bpl :loop
;
; lda #0 ; Start at the top of the
rts
2023-04-26 04:39:09 +00:00
; Function go through the object list and draw the background for areas that will need to draw
; additional items on top
_DrawShadowBkgnd
ldx _SortedHead
bmi :empty ; If there is nothing, do nothing
lda _Sprites+SPRITE_ID,x
:empty
rts
; Function to iterate through the object list and build a merged scanline list of areas of the screen that
; need to be drawn with shadowing off.
_BuildShadowList
ldy #0 ; This is the index into the list of shadow segments
ldx _SortedHead
2023-04-26 04:39:09 +00:00
:preloop
bmi :empty ; If the list is empty / skipped, do nothing
lda _Sprites+SPRITE_ID,x
bit #SPRITE_HIDE ; Make sure we don't do extra work for hidden objects
beq :insert
lda _Sprites+SORTED_NEXT,x
tax
bra :preloop
; Start of loop
:advance
iny
iny
:insert
lda _Sprites+SPRITE_CLIP_TOP,x ; Load the sprite's top line
sta _ShadowListTop,y ; Set the top entry of the list to the sprite top
lda _Sprites+SPRITE_CLIP_BOTTOM,x ; Optimistically set the end of the segment to the bottom of this sprite
inc ; Clip values are on the scanline, so add one to make it a proper interval
:replace
sta _ShadowListBottom,y
:skip
lda _Sprites+SORTED_NEXT,x ; If there another sprite in the list?
bmi :no_more_sprites ; If not, we can finish up
tax
2023-04-26 04:39:09 +00:00
lda _Sprites+SPRITE_ID,x
bit #SPRITE_HIDE
bne :skip
lda _ShadowListBottom,y ; If the bottom of the current sprite is _less than_ the top of the next
cmp _Sprites+SPRITE_CLIP_TOP,x ; sprite, then there is a gap and we create a new entry
bcc :advance
lda _Sprites+SPRITE_CLIP_BOTTOM,x ; Get the bottom value of the next sprite.
inc
cmp _ShadowListBottom,y ; If it extends the segment then replace the value, otherwise skip
bcc :skip
bra :replace
:no_more_sprites
iny ; Set the list count to N * 2
iny
:empty
sty _ShadowListCount
rts
; Iterate through the shadow list and call _BltRange on each
_DrawShadowList
ldx #0
bra :start
:loop
phx ; Save the index
lda _ShadowListTop,x
ldy _ShadowListBottom,x
tax
jsr _BltRange
plx
inx
inx
:start
cpx _ShadowListCount
bcc :loop
rts
2023-04-26 04:39:09 +00:00
; Run through the list of sprites that are not OFFSCREEN and not OVERLAYS and draw them directly to the graphics screen. We can use
; compiled sprites here, with limitations.
_DrawDirectSprites
lda RenderFlags
bit #RENDER_SPRITES_SORTED
bne :sorted
; Shift through the sprites
lda SpriteMap
beq :empty
sta tmp15
ldx #0
:iloop
lsr tmp15
bcc :next
2023-04-26 04:39:09 +00:00
phx
jsr _DrawStampToScreen
plx
:next inx
inx
lda tmp15
bne :iloop
rts
:sorted
ldx _SortedHead
bmi :empty
:loop
2023-04-26 04:39:09 +00:00
phx
jsr _DrawStampToScreen
plx
lda _Sprites+SORTED_NEXT,x ; If there another sprite in the list?
tax
bpl :loop
:empty
rts
; Run through the sorted list and perform a final render the jumps between calling _PEISlam for shadowed lines,
; _BltRange for clean backgrounds and Overlays as needed.
;
; The trick here is to merge runs of shared render types.
;
; Loop invariant: X-register is the current object index, Y-register is the next object index
;
; TODO: This does not yet handle the case of a narrow overlay in the middle of a sprite. The second half of the sprite will not be exposed
; by a PEISlam.
;
; e.g. |--- Overlay ---|
; |-------------- Sprite ----------------|
;
; Output Should be |-- PEI --||--- Overlay ---||--- PEI --|
; But currently is |-- PEI --||--- Overlay ---|
2023-04-26 04:39:09 +00:00
;
; The conceptual model of this routine is that it toggles between BltRange and PEISlam modes, but overlays are special and get drawn
; immediately but don't change the mode.
;
; General case to handle is this
;
; 0 1 2 3 4 5 6 7 8 9
; |------ sprite ---------| = A
; |-- overlay ------| = B
; |-- sprite -| = C
; |--- sprite ---| = D
;
; To handle this for each, we need to be able to slice off a piece of a sprite or overlay and insert it into the list for
; handling later. In this case, after the range [0, 1] is exposed for A, it should be dropped and moved like this
;
; 0 1 2 3 4 5 6 7 8 9
; |-- overlay ------| = B
; |-- sprite -| = C
; |--- sprite ---| = D
; |--| = A
;
; We can't alter that actual sorted list of items, so we create a reduced list which allows items to be filtered and
; to keep a simple, single-linked list
isNotHidden mac
bit #SPRITE_OVERLAY
bne ]1
bit #SPRITE_HIDE
beq ]1
2023-04-26 04:39:09 +00:00
EOL equ $FFFF
2023-04-26 04:39:09 +00:00
; New approach here. Walk the sorted, double linked list and copy the IDs into an array. There is
; a parallel structure to use later, but this is the easiest thing to work with
_FilterObjectList
ldy #0
ldx _SortedHead ; Walk the list
bra :entry
:loop
2023-04-26 04:39:09 +00:00
txa
sta ObjectList+OL_INDEX,y
iny
iny
2023-04-26 04:39:09 +00:00
lda _Sprites+SORTED_NEXT,x
tax
2023-04-26 04:39:09 +00:00
:entry
jsr _GetNextItem ; Get the first item from the list
cpx #EOL
bne :loop ; Exit if there are no more items
2023-04-26 04:39:09 +00:00
sty ObjectListCount
rts
2023-04-26 04:39:09 +00:00
_DrawObjShadow
:top equ tmp8
:bottom equ tmp9
2023-04-26 04:39:09 +00:00
ldy #0
cpy ObjectListCount ; Exit if the list of objects is empty
beq :exit
2023-04-26 04:39:09 +00:00
; Initialize with the record
2023-04-26 04:39:09 +00:00
ldx ObjectList+OL_INDEX,y
2023-04-26 04:39:09 +00:00
:loop
lda _Sprites+SPRITE_CLIP_TOP,x ; Get the top scanline
sta :top
lda _Sprites+SPRITE_CLIP_BOTTOM,x
:skip sta :bottom
; Advance to the next record.
iny
iny
cpy ObjectListCount ; Is this the last item
beq :done
; Check to see if the two items overlap
ldx ObjectList+OL_INDEX,y
cmp _Sprites+SPRITE_CLIP_TOP,x ; Compare to the top line of the next item
bcc :no_merge
max _Sprites+SPRITE_CLIP_BOTTOM,x ; Keep the largest of the two bottom values
bra :skip
:no_merge
phx
phy
2023-04-26 04:39:09 +00:00
ldx :top
ldy :bottom
jsr _BltRange
ply
plx
bra :loop
2023-04-26 04:39:09 +00:00
:exit
rts
2023-04-26 04:39:09 +00:00
:done
ldx :top ; X = top line
ldy :bottom ; Y = bottom line
jmp _BltRange ; If so, draw the background and return
2023-04-26 04:39:09 +00:00
;:loop
; Check if the current node and the next node are both sprites and, if they overlap, merge their ranges
; lda _Sprites+SPRITE_ID,x
; ora ObjectList+OL_SPRITE_ID,y
; and #SPRITE_OVERLAY
; bne :no_merge;
; lda ObjectList+OL_CLIP_BOTTOM,y
; cmp _Sprites+SPRITE_CLIP_TOP,x
; bcc :no_merge
; lda _Sprites+SPRITE_CLIP_BOTTOM,x
; max ObjectList+OL_CLIP_BOTTOM,y
; sta ObjectList+OL_CLIP_BOTTOM,y
; bra :skip
;:no_merge
; iny
; iny
; tya
; sta ObjectList+OL_NEXT-2,y ; Store link to this record in the previous node
;:entry
; lda _Sprites+SPRITE_ID,x
; sta ObjectList+OL_SPRITE_ID,y
; lda _Sprites+SPRITE_CLIP_TOP,x
; sta ObjectList+OL_CLIP_TOP,y
; lda _Sprites+SPRITE_CLIP_BOTTOM,x
; sta ObjectList+OL_CLIP_BOTTOM,y
;:skip
; lda _Sprites+SORTED_NEXT,x ; Advance to the next source item
; tax
; jsr _GetNextItem ; Get the first item from the list
; cpx #EOL
; bne :loop ; Exit if there are no valid entries
2023-04-26 04:39:09 +00:00
;:exit
; lda #EOL ; End-of-list marker
; sta ObjectList+OL_NEXT,y
;:empty
; rts
2023-04-26 04:39:09 +00:00
; Walk the object list and call _BltRange for the sprite
_DrawShadowRanges
2023-04-26 04:39:09 +00:00
; Split
;
; Y = current item
; X = next item
;
; Compares the bottom values of X and Y. If the current item extends past the next item, then this splits off the
; bottom ortion of Y and inserts it into the appropriate position in the linked list
split
:prev equ tmp15
lda ObjectList+OL_CLIP_BOTTOM,x ; If the next item is fully within the current one, split
cmp ObjectList+OL_CLIP_BOTTOM,y
bcc :do_split
rts
:do_split
sta ObjectList+OL_CLIP_TOP,y ; Set the top of the current item past the bottom of the next item
:split_lp
lda ObjectList+OL_NEXT,x ; search to find the spot in the linked list that we should
bmi :insert_after ; move the fragment forward to
stx :prev
tax
2023-04-26 04:39:09 +00:00
lda ObjectList+OL_CLIP_TOP,y
cmp ObjectList+OL_CLIP_TOP,x
bcc :insert_before ; If the modified node's top value is <= the node we are inspecting,
beq :insert_before ; then it can be inserted here
bra :split_lp
:insert_before
ldx :prev
lda ObjectList+OL_NEXT,x
; Insert Y node after X node. A = OL_NEXT,x
:insert_after
sta ObjectList+OL_NEXT,y
tya
sta ObjectList+OL_NEXT,x
tyx
rts
2023-04-26 04:39:09 +00:00
; X = top
; A = bottom
; Preserve X, Y
:_BltRange3
phx
phy
tay
jsr _BltRange
ply
plx
rts
2023-04-26 04:39:09 +00:00
_BltRange2
phx
jsr _BltRange
plx
rts
2023-04-26 04:39:09 +00:00
_GetNextItem
cpx #EOL ; early out if we're at the end of the list
bne *+3
rts
2023-04-26 04:39:09 +00:00
lda _Sprites+SPRITE_ID,x ; always return overlays
bit #SPRITE_OVERLAY
beq *+3
rts
2023-04-26 04:39:09 +00:00
bit #SPRITE_HIDE ; skip hidden sprites
bne :next
lda _Sprites+IS_OFF_SCREEN,x ; skip off-screen sprites
bne :next
2023-04-26 04:39:09 +00:00
rts ; found an object to return
:next
lda _Sprites+SORTED_NEXT,x
tax
bra _GetNextItem
2023-04-26 04:39:09 +00:00
DrawOverlayY
phx
2023-04-26 04:39:09 +00:00
phy
txy ; Swap X/Y
plx
2023-04-26 04:39:09 +00:00
phx
jsr _DrawOverlay
2023-04-26 04:39:09 +00:00
ply
plx
rts
2023-04-26 04:39:09 +00:00
DrawOverlayX
phx
2023-04-26 04:39:09 +00:00
phy
jsr _DrawOverlay
ply
plx
2023-04-26 04:39:09 +00:00
rts
2023-04-26 04:39:09 +00:00
; A = top line
; X = sprite record
; Y = bottom line
_DrawOverlay
pha
lda _Sprites+OVERLAY_PROC,x
stal :disp+1
lda _Sprites+OVERLAY_PROC+1,x
stal :disp+2
lda ScreenY0 ; pass the address of the first line of the overlay
clc
adc _Sprites+OVERLAY_TOP,x
asl
tax
2023-04-26 04:39:09 +00:00
lda ScreenAddr,x
clc
adc ScreenX0
plx
:disp jsl $000000
rts
; Helper to set a palette index on a range of SCBs to help show which actions are applied to which lines
DebugSCBs
phx
phy
sep #$30 ; short m/x
pha ; save the SCB value
phx
tya
sec
sbc 1,s
tay ; number of scanlines
pla
clc
adc ScreenY0
tax ; physical line index
pla
:loop
stal SHR_SCB,x
inx
dey
bne :loop
rep #$30
ply
plx
rts