Compare commits

...

2 Commits

Author SHA1 Message Date
Lucas Scharenbroich 9cc5b2e3af Generalize to allow differrnt play field sizes 2023-07-07 15:57:46 -05:00
Lucas Scharenbroich f6371f914e Add second set of oscillators to output to L and R channels 2023-07-07 15:56:36 -05:00
10 changed files with 649 additions and 191 deletions

View File

@ -18,7 +18,7 @@ DOWN_ARROW equ $0A
; Nametable queue
NT_QUEUE_LEN equ $1000
NT_QUEUE_SIZE equ {2*NT_QUEUE_LEN}
NT_QUEUE_MOD equ {NT_QUEUE_SIZE-1}
NT_QUEUE_MOD equ {NT_QUEUE_SIZE-1}
mx %00
@ -38,8 +38,26 @@ CurrNTQueueEnd equ 40
BGToggle equ 44
LastEnable equ 46
ShowFPS equ 48
HideStatusBar equ 50
;ROMPlayerY equ 52
YOrigin equ 54
OldOneSec equ 100
OldOneSec equ 100
RenderFlags equ 102
NesTop equ 104 ; Clip any sprite that has a NES OAM y-coordinate above this line
NesBottom equ 106 ; Clip any sprite that has a NES OAM y-coordinate below this line
ScreenBase equ 108 ; SHR address of the top-left edge of the play field
ScreenRows equ 110 ; Number of 8-line block we are showing on-screen
ScreenHeight equ 112
ScreenTop equ 114 ; SCB line of the top of the play field
MinYScroll equ 116 ; Smallest YOrigin value: 0 when showing stratus bar, 16 when hiding status bar
MaxYScroll equ 118 ; Largest YOrigin value for GTESetBG0Origin
;VirtTop equ 104 ; The top virtual line that is the top of the active play field (excludes status bar)
;VirtBottom equ 106 ; The bottom virtual line that is bottom of the active play field
;MaxNesY equ 108 ; This is the largest NES y coordinate that will display in the active play field
;MinNesY equ 110 ; This is the smallest NES y coordinate that will display in the active play field
Tmp0 equ 240
Tmp1 equ 242
@ -66,6 +84,8 @@ FTblTmp equ 228
stz OldROMScrollEdge
stz LastAreaType
stz ShowFPS
stz HideStatusBar
stz YOrigin
lda #1
sta BGToggle
@ -73,6 +93,39 @@ FTblTmp equ 228
lda #$0008
sta LastEnable
lda #16
; lda #32
sta NesTop
lda #16
sta MinYScroll
lda #1
sta HideStatusBar
lda #128
sta ScreenHeight
lsr
lsr
lsr
sta ScreenRows
lda #200
sec
sbc ScreenHeight
sta MaxYScroll
lda NesTop
clc
adc ScreenHeight
sec
sbc #8
inc
sta NesBottom
; lda #0
; sta ScreenTop
; The next two direct pages will be used by GTE, so get another 2 pages beyond that for the ROM. We get
; 4K of DP/Stack space by default, so there is plenty to share
@ -143,12 +196,36 @@ FTblTmp equ 228
; pea #184
pea #128
pea #200
pei ScreenHeight
; pea #80
; pea #144
_GTESetScreenMode
pha ; Allocate space for x, y, width, height
pha
pha
pha
_GTEGetScreenInfo
pla ; Discard x
pla
sta ScreenTop
asl
asl
asl
asl
asl
sta ScreenBase
asl
asl
clc
adc ScreenBase
clc
adc #$2000+x_offset
sta ScreenBase
pla ; Discard width and height
pla
ldx #Area1Palette
lda #TmpPalette
jsr NESColorToIIgs
@ -391,6 +468,13 @@ EvtLoop
brl EvtLoop
:not_t
cmp #'x' ; break
bne :not_x
lda #1
sta user_break
brl EvtLoop
:not_x
cmp #'q'
beq Exit
brl EvtLoop
@ -416,6 +500,7 @@ DPSave dw 0
LastAreaType dw 0
frameCount dw 0
show_vbl_cpu dw 0
user_break dw 0
; Toggle an APU control bit
ToggleAPUChannel
@ -497,16 +582,42 @@ RenderFrame
pha
; Get the player's Y coordinate and determine of we need to adjust the camera based on the physical play field size
; sep #$20
; ldal ROMBase+$b5 ; Player_Y_HighPos
; xba
; ldal ROMBase+$ce ; Player_Y_Position
; rep #$20
;
; sec
; sbc TopClip ; If we're hiding the
ldx ROMZeroPg
ldal $0000b5,x ; Player_Y_Page ; 0 = above screen, 1 = on screen, 2 = below
and #$00FF
beq :max_clamp
cmp #2
beq :min_clamp
pea $0000
ldal $0000ce,x ; Player_Y_Position
and #$00FF
; sta ROMPlayerY
; The "full screen" size is 200 lines that cover NES rows 16 through 216. If the
; size of the playfield is less, then we adjust the origin a bit.
;
; Y_Origin = min(200 - ScreenHeight, max(0, ROMPlayerY - 16 - ScreenHeight/2))
sec
sbc NesTop
asl
sec
sbc ScreenHeight
bmi :max_neg
lsr
cmp MinYScroll
bcc :max_clamp
cmp MaxYScroll
bcc :set_y
:min_clamp lda MaxYScroll
bra :set_y
:max_neg
:max_clamp
lda MinYScroll
:set_y
sta YOrigin
pha
_GTESetBG0Origin
lda ppumask
@ -518,11 +629,21 @@ RenderFrame
_GTEEnableBackground
:bghop
pea $FFFF ; NES mode
; pea $FFFF ; NES mode
lda HideStatusBar
beq :full_screen
pea $FFFD
_GTERender
bra :render_done
:full_screen
pea $FFFF
_GTERender
:render_done
; Check the AreaType and see if the palette needs to be changed. We do this after the screen is blitted
; so the palette does not get changed too eary while old pixels are still on the screen.
; so the palette does not get changed too early while old pixels are still on the screen.
ldal ROMBase+$074E
and #$00FF
@ -845,7 +966,8 @@ DrainNTQueue
; issues because many frames can pass before Render gets control again. We need to expose a
; _SetTileImmediate function in the list of function callbacks....
_GTESetTile
_GTESetTileImmediate
; _GTESetTile
; inc :Count
brl :skip
@ -1106,7 +1228,9 @@ CopyStatus
inx
stx Tmp2
_GTESetTile
; _GTESetTile
_GTESetTileImmediate
ply
plx
@ -1193,7 +1317,8 @@ CopyNametable
stx Tmp2
:x_hop
_GTESetTile
; _GTESetTile
_GTESetTileImmediate
ply
plx

View File

@ -287,16 +287,24 @@ setup_doc_registers
jsr access_doc_registers
ldx #pulse1_sound_settings
ldx #pulse1_sound_settings_l
jsr copy_register_config
ldx #pulse1_sound_settings_r
jsr copy_register_config
ldx #pulse2_sound_settings
ldx #pulse2_sound_settings_l
jsr copy_register_config
ldx #pulse2_sound_settings_r
jsr copy_register_config
ldx #triangle_sound_settings
ldx #triangle_sound_settings_l
jsr copy_register_config
ldx #triangle_sound_settings_r
jsr copy_register_config
ldx #noise_sound_settings
ldx #noise_sound_settings_l
jsr copy_register_config
ldx #noise_sound_settings_r
jsr copy_register_config
rep #$20
@ -388,7 +396,7 @@ pulse2_oscillator = 2
triangle_oscillator = 4
noise_oscillator = 6
default_freq = 800
pulse1_sound_settings = *
pulse1_sound_settings_l = *
dfb $00+pulse1_oscillator,default_freq ; frequency low register
dfb $20+pulse1_oscillator,default_freq/256 ; frequency high register
dfb $40+pulse1_oscillator,0 ; volume register, volume = 0
@ -396,7 +404,15 @@ pulse1_sound_settings = *
dfb $c0+pulse1_oscillator,0 ; wavetable size register, 256 byte length
dfb $a0+pulse1_oscillator,0 ; mode register, set to free run
pulse2_sound_settings = *
pulse1_sound_settings_r = *
dfb $01+pulse1_oscillator,default_freq ; frequency low register
dfb $21+pulse1_oscillator,default_freq/256 ; frequency high register
dfb $41+pulse1_oscillator,0 ; volume register, volume = 0
dfb $81+pulse1_oscillator,3 ; wavetable pointer register, point to $0300 by default (50% duty cycle)
dfb $c1+pulse1_oscillator,0 ; wavetable size register, 256 byte length
dfb $a1+pulse1_oscillator,$10 ; mode register, set to free run
pulse2_sound_settings_l = *
dfb $00+pulse2_oscillator,default_freq ; frequency low register
dfb $20+pulse2_oscillator,default_freq/256 ; frequency high register
dfb $40+pulse2_oscillator,0 ; volume register, volume = 0
@ -404,7 +420,15 @@ pulse2_sound_settings = *
dfb $c0+pulse2_oscillator,0 ; wavetable size register, 256 byte length
dfb $a0+pulse2_oscillator,0 ; mode register, set to free run
triangle_sound_settings = *
pulse2_sound_settings_r = *
dfb $01+pulse2_oscillator,default_freq ; frequency low register
dfb $21+pulse2_oscillator,default_freq/256 ; frequency high register
dfb $41+pulse2_oscillator,0 ; volume register, volume = 0
dfb $81+pulse2_oscillator,3 ; wavetable pointer register, point to $0300 by default (50% duty cycle)
dfb $c1+pulse2_oscillator,0 ; wavetable size register, 256 byte length
dfb $a1+pulse2_oscillator,$10 ; mode register, set to free run
triangle_sound_settings_l = *
dfb $00+triangle_oscillator,default_freq ; frequency low register
dfb $20+triangle_oscillator,default_freq/256 ; frequency high register
dfb $40+triangle_oscillator,0 ; volume register, volume = 0
@ -412,7 +436,15 @@ triangle_sound_settings = *
dfb $c0+triangle_oscillator,0 ; wavetable size register, 256 byte length
dfb $a0+triangle_oscillator,0 ; mode register, set to free run
noise_sound_settings = *
triangle_sound_settings_r = *
dfb $01+triangle_oscillator,default_freq ; frequency low register
dfb $21+triangle_oscillator,default_freq/256 ; frequency high register
dfb $41+triangle_oscillator,0 ; volume register, volume = 0
dfb $81+triangle_oscillator,5 ; wavetable pointer register, point to $0500
dfb $c1+triangle_oscillator,0 ; wavetable size register, 256 byte length
dfb $a1+triangle_oscillator,$10 ; mode register, set to free run
noise_sound_settings_l = *
dfb $00+noise_oscillator,default_freq ; frequency low register
dfb $20+noise_oscillator,default_freq/256 ; frequency high register
dfb $40+noise_oscillator,128 ; volume register, volume = 0
@ -420,6 +452,15 @@ noise_sound_settings = *
dfb $c0+noise_oscillator,0 ; wavetable size register, 256 byte length
dfb $a0+noise_oscillator,0 ; mode register, set to free run
noise_sound_settings_r = *
dfb $01+noise_oscillator,default_freq ; frequency low register
dfb $21+noise_oscillator,default_freq/256 ; frequency high register
dfb $41+noise_oscillator,128 ; volume register, volume = 0
dfb $81+noise_oscillator,6 ; wavetable pointer register, point to $0600
dfb $c1+noise_oscillator,0 ; wavetable size register, 256 byte length
dfb $a1+noise_oscillator,$10 ; mode register, set to free run
backup_interrupt_ptr ds 4
apu_mode ds 2
@ -703,22 +744,25 @@ update_doc_registers
sta _apu_pulse1_last_period
jsr get_pulse_freq ; return freq in 16-bit accumulator
sep #$30
ldx #$00+pulse1_oscillator
stx sound_address
sta sound_data
jsr set_osc_register_pair
; stx sound_address
; sta sound_data
ldx #$20+pulse1_oscillator
stx sound_address
; stx sound_address
xba
sta sound_data
jsr set_osc_register_pair
; sta sound_data
:freq_end_pulse1 sep #$30 ; redundent, but avoids extra branches
lda #$80+pulse1_oscillator
sta sound_address
ldx #$80+pulse1_oscillator
; sta sound_address
lda APU_PULSE1_REG1 ; Get the cycle duty bits
jsr set_pulse_duty_cycle
lda #$40+pulse1_oscillator
sta sound_address
ldx #$40+pulse1_oscillator
; sta sound_address
lda APU_PULSE1_REG1
bit #PULSE_CONST_VOL_FLAG ; Check the constant volume bit
bne :set_volume_pulse1
@ -727,11 +771,12 @@ update_doc_registers
:mute_pulse1
sep #$30
lda #$40+pulse1_oscillator
sta sound_address
ldx #$40+pulse1_oscillator
; sta sound_address
lda #0
:set_volume_pulse1 jsr set_pulse_volume
; Now do the second square wave
lda APU_PULSE2_MUTE ; If the sweep muted the channel, no output
bne :mute_pulse2
@ -748,21 +793,23 @@ update_doc_registers
jsr get_pulse_freq ; return freq in 16-bic accumulator
sep #$30
ldx #$00+pulse2_oscillator
stx sound_address
sta sound_data
; stx sound_address
; sta sound_data
jsr set_osc_register_pair
ldx #$20+pulse2_oscillator
stx sound_address
; stx sound_address
xba
sta sound_data
; sta sound_data
jsr set_osc_register_pair
:freq_end_pulse2 sep #$30
lda #$80+pulse2_oscillator
sta sound_address
ldx #$80+pulse2_oscillator
; sta sound_address
lda APU_PULSE2_REG1 ; Get the cycle duty bits
jsr set_pulse_duty_cycle
lda #$40+pulse2_oscillator
sta sound_address
ldx #$40+pulse2_oscillator
; sta sound_address
lda APU_PULSE2_REG1
bit #PULSE_CONST_VOL_FLAG ; Check the constant volume bit
bne :set_volume_pulse2
@ -770,8 +817,8 @@ update_doc_registers
bra :set_volume_pulse2
:mute_pulse2
sep #$30
lda #$40+pulse2_oscillator
sta sound_address
ldx #$40+pulse2_oscillator
; sta sound_address
lda #0
:set_volume_pulse2 jsr set_pulse_volume
@ -802,23 +849,25 @@ update_doc_registers
lsr
sep #$30
ldx #$00+triangle_oscillator
stx sound_address
sta sound_data
; stx sound_address
; sta sound_data
jsr set_osc_register_pair
ldx #$20+triangle_oscillator
stx sound_address
; stx sound_address
xba
sta sound_data
; sta sound_data
jsr set_osc_register_pair
:freq_end_triangle sep #$30
lda #$40+triangle_oscillator
sta sound_address
ldx #$40+triangle_oscillator
; sta sound_address
lda #12 ; Triangle is a bit softer than pulse channels
bra :set_volume_triangle
:mute_triangle
sep #$30
lda #$40+triangle_oscillator
sta sound_address
ldx #$40+triangle_oscillator
; sta sound_address
lda #0
:set_volume_triangle jsr set_pulse_volume
@ -828,24 +877,26 @@ update_doc_registers
beq :mute_noise
ldx #$00+noise_oscillator
stx sound_address
; stx sound_address
lda APU_NOISE_CURRENT_PERIOD
sta sound_data
; sta sound_data
jsr set_osc_register_pair
ldx #$20+noise_oscillator
stx sound_address
; stx sound_address
lda APU_NOISE_CURRENT_PERIOD+1
sta sound_data
; sta sound_data
jsr set_osc_register_pair
lda #$40+noise_oscillator
sta sound_address
ldx #$40+noise_oscillator
; sta sound_address
lda APU_NOISE_REG1
bit #NOISE_CONST_VOL_FLAG ; Check the constant volume bit
bne :set_volume_noise
lda APU_NOISE_ENVELOPE
bra :set_volume_noise
:mute_noise
lda #$40+noise_oscillator
sta sound_address
ldx #$40+noise_oscillator
; sta sound_address
lda #0
:set_volume_noise
and #$0F
@ -860,7 +911,8 @@ update_doc_registers
asl
pha
:high_pitch pla
sta sound_data
; sta sound_data
jsr set_osc_register_pair
:not_timer
no_frame
@ -875,17 +927,30 @@ no_frame
clc
rtl
; X = addr
; A = data
set_osc_register_pair
mx %11
stx sound_address
sta sound_data
inc sound_address
sta sound_data
rts
; X = addr
; A = duty cycle select
set_pulse_duty_cycle
mx %11
rol
rol
rol
and #$03
tax
tay
lda duty_cycle_page,x
sta sound_data
rts
lda duty_cycle_page,y
jmp set_osc_register_pair
; sta sound_data
; rts
set_pulse_volume
and #$0F
@ -893,8 +958,9 @@ set_pulse_volume
asl
asl
asl
sta sound_data
rts
jmp set_osc_register_pair
; sta sound_data
; rts
; This is a bit different because we actually calculate a scan rate directly to match the
; rate at which new samples are read form DOC RAM to the period of the noise channel

View File

@ -110,6 +110,7 @@ PPUMASK_WRITE ENT
; $2002 - PPUSTATUS For "ldx ppustatus"
PPUSTATUS_READ_X ENT
pha ; spacefor result
php
pha
@ -117,31 +118,29 @@ PPUSTATUS_READ_X ENT
stal w_bit ; Reset the address latch used by PPUSCROLL and PPUADDR
ldal ppustatus
tax
sta 3,s
and #$7F ; Clear the VBL flag
stal ppustatus
pla ; Restore the accumulator (return value in X)
plp
phx ; re-read x to set any relevant flags
plx
rtl
PPUSTATUS_READ ENT
pha ; space for return value
php
lda #1
stal w_bit ; Reset the address latch used by PPUSCROLL and PPUADDR
ldal ppustatus
pha
sta 2,s
and #$7F ; Clear the VBL flag
stal ppustatus
pla ; pop the return value
plp
pha ; re-read accumulator to set any relevant flags
pla
rtl
@ -334,8 +333,8 @@ PPUDATA_WRITE ENT
sta nt_queue,y
:full
; lda #1
; jsr setborder
lda #1
jsr setborder
rts
:attrtbl
@ -421,6 +420,30 @@ PPUDATA_WRITE ENT
dex
jmp :enqueue
incborder
php
sep #$20
ldal $E0C034
inc
eorl $E0C034
and #$0F
eorl $E0C034
stal $E0C034
plp
rts
decborder
php
sep #$20
ldal $E0C034
dec
eorl $E0C034
and #$0F
eorl $E0C034
stal $E0C034
plp
rts
setborder
php
sep #$20
@ -588,25 +611,47 @@ PPUDMA_WRITE ENT
plp
rtl
y_offset_rows equ 2
y_height_rows equ 25
y_offset equ {y_offset_rows*8}
y_height equ {y_height_rows*8}
max_nes_y equ {y_height+y_offset-8}
;y_offset_rows equ 2
;y_height_rows equ 25
;y_offset equ {y_offset_rows*8}
;y_height equ {y_height_rows*8}
;max_nes_y equ {y_height+y_offset-8}
x_offset equ 16
; Scan the OAM memory and copy the values of the sprites that need to be drawn. There are two reasons to do this
;
; 1. Freeze the OAM memory at this instanct so that the NES ISR can keep running without changing values
; 2. We have to scan this list twice -- once to build up the shadow list and once to actually render the sprites
; This code has an optimization that it directly scans the NES RAM that would be DMA copied into the PPU
; OAM space. This is ok, because
;
; 1. The OAM DMA occurs in the NES ROM before running any game logic
; 2. This code is running after the prior ISR, so it is loically happening at the beginning of the next NMI
;
; When scanning the OAM values, sprites that are not visible for any number of reasons are skipped and the
; sprite's y-position is adjusted based on the GTE camera view. This allow all of the shadowBitmap and
; shadow lits work to assume an index value of zero is the top of the active play field.
OAM_COPY ds 256
spriteCount ds 0
db 0 ; Pad in case we can to access using 16-bit instructions
mx %00
scanOAMSprites
stz Tmp5
:top_line equ Tmp5
:bot_line equ Tmp6
; In order for the shadow bitmap to be zeroed based on the active playfield, we need to adjust the NES
; sprite y-coordinates by the designated top row of the NES graphics screen, and then add an additional
; adjustment for the position of the GTE rendering window within that vertical space
lda NesTop
clc
adc YOrigin
sta :top_line
lda NesBottom
clc
adc YOrigin
sta :bot_line
sep #$30
@ -616,15 +661,23 @@ scanOAMSprites
:loop
; lda PPU_OAM,x ; Y-coordinate
ldal ROMBase+$200,x
cmp #max_nes_y+1 ; Skip anything that is beyond this line
cmp :bot_line
bcs :skip
cmp #y_offset
cmp :top_line
bcc :skip
sbc :top_line
sta OAM_COPY,y ; Keep the adjusted coordinate
; cmp #max_nes_y+1 ; Skip sprites that are
; bcs :skip
; cmp #y_offset
; bcc :skip
; lda PPU_OAM+1,x ; $FC is an empty tile, don't draw it
ldal ROMBase+$201,x
cmp #$FC
beq :skip
sta OAM_COPY+1,y
; lda PPU_OAM+3,x ; If X-coordinate is off the edge skip it, too.
ldal ROMBase+$203,x
@ -633,8 +686,8 @@ scanOAMSprites
rep #$20
; lda PPU_OAM,x
ldal ROMBase+$200,x
sta OAM_COPY,y
; ldal ROMBase+$200,x
; sta OAM_COPY,y
; lda PPU_OAM+2,x
ldal ROMBase+$202,x
sta OAM_COPY+2,y
@ -834,19 +887,18 @@ buildShadowBitmap
rts
; Set the SCB values equal to the bitmap to visually debug
ldx #0
ldx ScreenTop
ldy #0
:vloop
lda #8
sta Tmp6
lda shadowBitmap+2,y
lda shadowBitmap,y
:iloop
asl
pha
lda #0
bcc :zero
inc
rol
:zero stal $E19D00,x
pla
@ -855,7 +907,7 @@ buildShadowBitmap
bne :iloop
iny
cpy #25
cpy ScreenRows
bcc :vloop
rep #$30
@ -1016,7 +1068,7 @@ exposeShadowList
:exit
ldx :last ; Expose the final part
ldy #y_height
ldy ScreenHeight
lda #0
jsl LngJmp
rts
@ -1030,16 +1082,15 @@ shadowBitmapToList
sep #$30
ldx #y_offset_rows ; Start at the third row (y_offset = 16) walk the bitmap for 25 bytes (200 lines of height)
lda #0
sta shadowListCount ; zero out the shadow list count
ldx #0 ; List is zero-based to the active play field
stz shadowListCount ; zero out the shadow list count
; This loop is called when we are not tracking a sprite range
:zero_loop
ldy shadowBitmap,x
beq :zero_next
lda {mul8-y_offset_rows},x ; This is the scanline we're on (offset by the starting byte)
lda mul8,x ; This is the scanline we're on (offset by the starting byte)
clc
adc offset,y ; This is the first line defined by the bit pattern
sta :top
@ -1047,7 +1098,7 @@ shadowBitmapToList
:zero_next
inx
cpx #y_height_rows+y_offset_rows ; +1 ; End at byte 27
cpx ScreenRows
bcc :zero_loop
bra :exit ; ended while not tracking a sprite, so exit the function
@ -1063,7 +1114,8 @@ shadowBitmapToList
and offsetMask,y
sta shadowBitmap,x
lda {mul8-y_offset_rows},x
; lda {mul8-y_offset_rows},x
lda mul8,x
clc
adc invOffset,y
@ -1081,7 +1133,8 @@ shadowBitmapToList
:one_next
inx
cpx #y_height_rows+y_offset_rows+1
cpx ScreenRows
bcc :one_loop
; If we end while tracking a sprite, add to the list as the last item
@ -1089,7 +1142,7 @@ shadowBitmapToList
ldx shadowListCount
lda :top
sta shadowListTop,x
lda #y_height
lda ScreenHeight
sta shadowListBot,x
inx
stx shadowListCount
@ -1130,6 +1183,8 @@ drawOAMSprites
plb
pha ; Save the phase indicator
pei 124 ; RenderFlags
tdc ; Keep a copy of the second page of GTE direct page space
clc
adc #$0100
@ -1144,6 +1199,8 @@ drawOAMSprites
stx FTblPtr+2
pla
sta RenderFlags
pla
; Check what phase we're in
;
@ -1160,10 +1217,7 @@ drawOAMSprites
; We need to "freeze" the OAM values, otherwise they can change between when we build the rendering pipeline
sei
ldal nmiCount
pha
jsr scanOAMSprites ; Filter out any sprites that don't need to be drawn
pla
cli
jsr buildShadowBitmap ; Run though and quickly create a bitmap of lines with sprites
@ -1202,7 +1256,7 @@ drawSprites
oam_loop
phx ; Save x
lda OAM_COPY,x ; Y-coordinate
lda OAM_COPY,x ; Y-coordinate (zero based to screen)
; inc ; Compensate for PPU delayed scanline
rep #$30
@ -1218,7 +1272,7 @@ oam_loop
clc
adc :tmp
clc
adc #$2000-{y_offset*160}+x_offset
adc ScreenBase
sta :tmp
lda OAM_COPY+3,x

View File

@ -144,6 +144,12 @@ _GTEEnableSprites MAC
_GTEEnableBackground MAC
UserTool $3100+GTEToolNum
<<<
_GTEDrawTileToScreen MAC
UserTool $3200+GTEToolNum
<<<
_GTESetTileImmediate MAC
UserTool $3300+GTEToolNum
<<<
; EngineMode definitions
; Script definition
@ -189,8 +195,9 @@ vblCallback equ $0004
extSpriteRenderer equ $0005
rawDrawTile equ $0006
extBG0TileUpdate equ $0007
userTileCallback equ $0008
userTileCallback equ $0008 ; Callback for rendering custom tiles into the code field
liteBlitter equ $0009
userTileDirectCallback equ $000A ; Callback for drawing custom tiles directly to the screen buffer
; CopyPicToBG1 flags
COPY_PIC_NORMAL equ $0000 ; Copy into BG1 buffer in "normal mode"

View File

@ -208,8 +208,9 @@ vblCallback equ $0004 ; User routine to be called by VBL int
extSpriteRenderer equ $0005
rawDrawTile equ $0006
extBG0TileUpdate equ $0007
userTileCallback equ $0008
userTileCallback equ $0008 ; Callback for rendering custom tiles into the code field
liteBlitter equ $0009
userTileDirectCallback equ $000A ; Callback for drawing custom tiles directly to the screen buffer
; CopyPicToBG1 flags
COPY_PIC_NORMAL equ $0000 ; Copy into BG1 buffer in "normal mode" treating the buffer as a 164x208 pixmap with stride of 256

View File

@ -160,13 +160,96 @@ ExtFuncBlock
; Special NES renderer that externalizes the sprite rendering in order to exceed the internal limit of 16 sprites
_RenderNES
; jsr _ApplyBG0YPos
; jsr _ApplyBG0XPosPre
sta RenderFlags
bit #$0002 ; If this bit is off, don't render the top. If the top *is* rendered, assume full-screen
bne *+5
jmp _RenderNES2
jsr _ApplyBG0YPosPreLite
jsr _ApplyBG0YPosLite
jsr _ApplyBG0XPosPre
; Callback to update the tilestore with any new tiles
jsr _RenderNESTileCallback
jsr _ApplyTiles ; This function actually draws the new tiles into the code field
; Render the status bar area, which is fixed
stz tmp1 ; virt_line_x2
lda #16*2
sta tmp2 ; lines_left_x2
lda #0 ; Xmod164
jsr _ApplyBG0XPosAltLite
lda tmp4 ; :exit_offset
stal nesTopOffset
; Next render the remaining lines, which should have their screen locations adjusted for the
; vertical offset
lda #16*2
clc
adc StartYMod208
sta tmp1 ; virt_line_x2
lda ScreenHeight
sec
sbc #16
asl
sta tmp2 ; lines_left_x2
lda StartXMod164 ; Xmod164
jsr _ApplyBG0XPosAltLite
lda tmp4
stal nesBottomOffset
jsr _RenderNESSpritesCallback
stz tmp1 ; :virt_line_x2
lda #16*2
sta tmp2 ; :lines_left_x2
ldal nesTopOffset
sta tmp4 ; :exit_offset
jsr _RestoreBG0OpcodesAltLite
lda #16*2
sta tmp1 ; :virt_line_x2
lda ScreenHeight
sec
sbc #16
asl
sta tmp2 ; lines_left_x2
ldal nesBottomOffset
sta tmp4 ; :exit_offset
jsr _RestoreBG0OpcodesAltLite
bra :final_step
:no_status_restore
stz tmp1
lda ScreenHeight
sta tmp2
ldal nesBottomOffset
sta tmp4 ; :exit_offset
jsr _RestoreBG0OpcodesAltLite
:final_step
jmp _RenderNESExit
_RenderNES2
jsr _ApplyBG0YPosPreLite
jsr _ApplyBG0YPosLite
jsr _ApplyBG0XPosPre
jsr _RenderNESTileCallback
jsr _ApplyTiles ; This function actually draws the new tiles into the code field
jsr _ApplyBG0XPosLite ; Do the full screen
jsr _RenderNESSpritesCallback
lda StartYMod208 ; Restore the fields back to their original state
ldx ScreenHeight
jsr _RestoreBG0OpcodesLite
jmp _RenderNESExit
; Callback to update the tilestore with any new tiles
_RenderNESTileCallback
lda ExtUpdateBG0Tiles
ora ExtUpdateBG0Tiles+2
beq :no_tile
@ -176,31 +259,7 @@ _RenderNES
lda ExtUpdateBG0Tiles+1
stal :patch0+2
:patch0 jsl $000000
:no_tile
jsr _ApplyTiles ; This function actually draws the new tiles into the code field
stz tmp1 ; virt_line_x2
lda #16*2
sta tmp2 ; lines_left_x2
lda #0 ; Xmod164
; jsr _ApplyBG0XPosAlt
jsr _ApplyBG0XPosAltLite
lda tmp4 ; :exit_offset
stal nesTopOffset
lda #16*2
sta tmp1 ; virt_line_x2
lda ScreenHeight
sec
sbc #16
asl
sta tmp2 ; lines_left_x2
lda StartXMod164 ; Xmod164
; jsr _ApplyBG0XPosAlt
jsr _ApplyBG0XPosAltLite
lda tmp4
stal nesBottomOffset
:no_tile rts
; This is a tricky part. The NES does not keep sprites sorted, so we need an alternative way to figure out
; which lines to shadow and which ones not to. Our compromise is to build a bitmap of lines that the sprite
@ -208,7 +267,7 @@ _RenderNES
;
; This is handled by the callback in two phases. We pass pointers to the internal function the callback needs
; access to. If there is no function defined, do nothing
_RenderNESSpritesCallback
lda GTEControlBits
bit #CTRL_SPRITE_DISABLE
bne :no_render
@ -243,32 +302,9 @@ _RenderNES
ldx #^ExtFuncBlock
ldy #ExtFuncBlock
:patch2 jsl $000000
:no_render rts
:no_render
stz tmp1 ; :virt_line_x2
lda #16*2
sta tmp2 ; :lines_left_x2
ldal nesTopOffset
sta tmp4 ; :exit_offset
; jsr _RestoreBG0OpcodesAlt
jsr _RestoreBG0OpcodesAltLite
lda #16*2
sta tmp1 ; :virt_line_x2
lda ScreenHeight
sec
sbc #16
asl
sta tmp2 ; lines_left_x2
ldal nesBottomOffset
sta tmp4 ; :exit_offset
; jsr _RestoreBG0OpcodesAlt
jsr _RestoreBG0OpcodesAltLite
; lda StartYMod208 ; Restore the fields back to their original state
; ldx ScreenHeight
; jsr _RestoreBG0Opcodes
_RenderNESExit
lda StartY
sta OldStartY
lda StartX
@ -637,6 +673,7 @@ _RenderLite
sta RenderFlags
jsr _DoTimers ; Run any pending timer tasks
jsr _ApplyBG0YPosPreLite
jsr _ApplyBG0YPosLite ; Set stack addresses for the virtual lines to the physical screen
jsr _ApplyBG0XPosPre ; Lock in certain rendering variables (not lite/non-lite specific)

View File

@ -352,6 +352,75 @@ _CalcTileProcIndex
:no_flip_d lda #0
rts
; Set a tile value in the backing store and immediately render into the code field
;
; A = tile id
; X = tile column [0, 40] (41 columns)
; Y = tile row [0, 25] (26 rows)
;
; Registers are not preserved
_SetTileImmediate
sta newTileId
jsr _GetTileStoreOffset0 ; Get the address of the X,Y tile position
tax
lda TileStore+TS_TILE_ID,x
cmp newTileId
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
bra :render_tile
:set_user_tile
and #$01FF
jsr _GetTileAddr ; The user tile callback needs access to this info, too, but
sta TileStore+TS_TILE_ADDR,x ; we just give the base tile address (hflip bit is ignored)
lda #UserTileDispatch
stal K_TS_BASE_TILE_DISP,x
stal K_TS_SPRITE_TILE_DISP,x
stal K_TS_ONE_SPRITE,x
:render_tile
phd ; save the current direct page
tdc
clc
adc #$100 ; move to the next page
tcd
jsr _RenderTile
pld
rts
; Set a tile value in the tile backing store. Mark dirty if the value changes
;
; A = tile id
@ -447,6 +516,27 @@ _UTDPatch jsl UserHook1 ; Call the users code
:done
rts
; Provides a direct callback without using the TileStore information
; A = user data
; Y = screen address
; X = tile address
;
; Bank will be set to the tiledata bank, so lda $0000,x will load the first word of the tile's data
UserTileDirectDispatch
phd
pha
tdc
clc
adc #$100
tcd
pla
pei DP2_TILEDATA_AND_TILESTORE_BANKS ; set the bank to the tiledata bank
plb
_UTD2Patch jsl UserHook1 ; Call the user's code
plb ; Restore the curent bank
pld
rts
; Stub to have a valid address for initialization / reset
UserHook1 rtl

View File

@ -105,6 +105,9 @@ _CallTable
adrl _TSEnableSprites-1
adrl _TSEnableBackground-1
adrl _TSDrawTileToScreen-1
adrl _TSSetTileImmediate-1
_CTEnd
_GTEAddSprite MAC
UserTool $1000+GTEToolNum
@ -271,19 +274,36 @@ _TSReadControl
_TSExit #0;#0
; SetTile(xTile, yTile, tileId)
_TSSetTile
tileId equ FirstParam
yTile equ FirstParam+2
xTile equ FirstParam+4
; SetTileImmediate(xTile, yTile, tileId)
_TSSetTileImmediate
:tileId equ FirstParam
:yTile equ FirstParam+2
:xTile equ FirstParam+4
_TSEntry
lda xTile,s ; Valid range [0, 40] (41 columns)
lda :xTile,s ; Valid range [0, 40] (41 columns)
tax
lda yTile,s ; Valid range [0, 25] (26 rows)
lda :yTile,s ; Valid range [0, 25] (26 rows)
tay
lda tileId,s
lda :tileId,s
jsr _SetTileImmediate
_TSExit #0;#6
; SetTile(xTile, yTile, tileId)
_TSSetTile
:tileId equ FirstParam
:yTile equ FirstParam+2
:xTile equ FirstParam+4
_TSEntry
lda :xTile,s ; Valid range [0, 40] (41 columns)
tax
lda :yTile,s ; Valid range [0, 25] (26 rows)
tay
lda :tileId,s
jsr _SetTile
_TSExit #0;#6
@ -312,6 +332,8 @@ _TSRender
beq :nes
cmp #$FFFE ; Experimental gte-lite mode
beq :lite
cmp #$FFFD
beq :nes
bit #RENDER_WITH_SHADOWING
beq :no_shadowing
@ -1054,7 +1076,26 @@ _TSSetAddress
stal _UTDPatch+2
brl :out
:next_6
:next_6 cmp #userTileDirectCallback
bne :next_7
lda :ptr,s
ora :ptr+2,s
beq :utd2_restore
lda :ptr,s
stal _UTD2Patch+1 ; long addressing because we're patching code in the K bank
lda :ptr+1,s
stal _UTD2Patch+2
brl :out
:utd2_restore
lda #UserHook1
stal _UTD2Patch+1
lda #>UserHook1
stal _UTD2Patch+2
brl :out
:next_7
:out
_TSExit #0;#6
@ -1110,6 +1151,32 @@ _TSEnableBackground
:done
_TSExit #0;#2
; Draw a tile directly to the graphics screen. Provides an convenient way to immediately draw
; tile content on the the graphics buffer without having to go through a Render function.
;
; DrawTileToScreen(screenAddr, tileId, tileFlags)
_TSDrawTileToScreen
:tileId equ FirstParam+4 ; fetches the address of the internal tile data buffer
:screenAddr equ FirstParam+2 ; screen address on the SHR
:userData equ FirstParam ; used in place of :tileId for user callback function
_TSEntry
lda :tileId,s
bit #TILE_USER_BIT
bne :doUserTile
bra :done
:doUserTile
jsr _GetTileAddr
tax
lda :screenAddr,s
tay
lda :userData,s
jsr UserTileDirectDispatch
:done
_TSExit #0;#6
; Insert the GTE code
put Math.s

View File

@ -1,38 +1,39 @@
; Subroutines that deal with the vertical scrolling and rendering. The primary function
; of these routines are to adjust tables and patch in new values into the code field
; when the virtual Y-position of the play field changes.
_ApplyBG0YPosLite
:rtbl_idx_x2 equ tmp0
:virt_line_x2 equ tmp1
:lines_left_x2 equ tmp2
:draw_count_x2 equ tmp3
:stk_save equ tmp4
:line_count equ tmp5
; First task is to fill in the STK_ADDR values by copying them from the RTable array. We
; copy from RTable[i] into BlitField[StartY+i].
stz :rtbl_idx_x2 ; Start copying from the first entry in the table
_ApplyBG0YPosPreLite
lda StartY ; This is the base line of the virtual screen
jsr Mod208
sta StartYMod208
rts
asl
sta :virt_line_x2 ; Keep track of it
_ApplyBG0YPosLite
; copy a range of address from the table into the destination bank. If we restrict ourselves to
; rectangular playfields, this can be optimized to just subtracting a constant value. See the
; Templates::SetScreenAddrs subroutine.
:virt_line_x2 equ tmp1
:lines_left_x2 equ tmp2
; First task is to fill in the STK_ADDR values by copying them from the RTable array. We
; copy from RTable[i] into BlitField[StartY+i].
lda ScreenHeight
asl
sta :lines_left_x2
lda StartYMod208
asl
sta :virt_line_x2 ; Keep track of it
lda #0
_ApplyBG0YPosAltLite
:rtbl_idx_x2 equ tmp0
:virt_line_x2 equ tmp1
:lines_left_x2 equ tmp2
:draw_count_x2 equ tmp3
; Check to see if we need to split the update into two parts, e.g. do we wrap around the end
; of the code field?
sta :rtbl_idx_x2
ldx :lines_left_x2
lda #208*2
@ -46,6 +47,11 @@ _ApplyBG0YPosLite
stz :virt_line_x2 ; virtual line is at the top (by construction)
lda :rtbl_idx_x2
clc
adc :draw_count_x2
sta :rtbl_idx_x2
lda :lines_left_x2
sec
sbc :draw_count_x2 ; this many left to draw. Fall through to finish up
@ -62,6 +68,8 @@ _ApplyBG0YPosLite
tay
iny ; Fill in the first byte (_ENTRY_1 = 0)
ldx :rtbl_idx_x2 ; Load the stack address from here
sep #$20 ; Set the data bank to the code field
lda BTableHigh
pha

View File

@ -1,3 +1,6 @@
; External entrypoint to render a tile directly into the code field
RenderTile
; If there are no sprites, then we copy the tile data into the code field as fast as possible.
; If there are sprites, then additional work is required
_RenderTile