gbdk-2020 | GameBoy Development Kit |
| download: https://git.y1.nz/archives/gbdk.tar.gz | |
| README | Files | Log | Refs | LICENSE |
commit 2b633b83cd44ac565de035a9d581c1c9b86cfd0f parent 10cc14645a3830df30a19f52f58e4446972c6248 Author: Michel Iwaniec <46843052+michel-iwaniec@users.noreply.github.com> Date: Sun, 27 Apr 2025 10:38:06 +0100 NES: Refactor display_on / display_off and deferred isr code (#768) - Move code for deferred isr buffer building to separate function .deferred_isr_run in file deferred_isr.s - Introduce function .deferred_isr_reset to write shadow regs as first (VBL) buffer entry - Add new subroutine .deferred_isr_reset_and_init that runs .deferred_isr_reset and .deferred_isr_run twice - Drain buffer when DISPLAY_OFF is called, by making sure a single NMI execution happens before screen is turned off in vblank - Change DISPLAY_OFF to write only DISPLAY_OFF flag and PPUMASK, but leave shadow_PPU_MASK unchanged - Call .deferred_isr_reset_and_init in DISPLAY_ON when display changes from off to on - Make DISPLAY_ON run run VBL / LCD handlers to reinitialize buffers when display switches from off to on - Change early-out in NMI handler to rely on DISPLAY_OFF flag instead of shadow_PPUMASK - Remove mentioned of manual-draining caveat in docs, as it no longer applies - Add minor size/speed optimization of frame counter increment in NMI, to prevent branch penalties / incorrect system detection Diffstat:
| M | docs/pages/06b_supported_consoles.md | 6 | ------ |
| M | gbdk-lib/libc/targets/mos6502/nes/Makefile | 2 | +- |
| M | gbdk-lib/libc/targets/mos6502/nes/crt0.s | 204 | +++++++++++++++---------------------------------------------------------------- |
| A | gbdk-lib/libc/targets/mos6502/nes/deferred_isr.s | 184 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | gbdk-lib/libc/targets/mos6502/nes/global.s | 3 | +++ |
5 files changed, 226 insertions(+), 173 deletions(-)
diff --git a/docs/pages/06b_supported_consoles.md b/docs/pages/06b_supported_consoles.md @@ -442,12 +442,6 @@ Direct mode also affects how (fake) interrupt handlers are processed. As long as The TIM handler will still be executed as normal. -#### Caveat: Make sure the transfer buffer is emptied before switching to direct mode - -Because the switch to direct mode is instant and doesn't wait for the next invocation of the vblank, it is possible to create situations where there is still remaining data in the transfer buffer that would only get written once the system is switched back to buffered mode. - -To avoid this situation, make sure to always "drain" the buffer by doing a call to vsync when you expect your code to finish. - #### Caveat: Only update the PPU palette during buffered mode The oddity that PPU palette values are accessed through the same mechanism as other PPU memory bytes comes with the side effect that the vblank NMI handler will only write the palette values in buffered mode. diff --git a/gbdk-lib/libc/targets/mos6502/nes/Makefile b/gbdk-lib/libc/targets/mos6502/nes/Makefile @@ -20,7 +20,7 @@ ASSRC = f_ibm_full.s f_ibm_sh.s f_italic.s f_min.s f_spect.s \ delay.s \ rle_decompress.s \ far_ptr.s sdcc_bcall.s mapper.s \ - lcd.s \ + lcd.s deferred_isr.s \ crt0.s CRT0 = crt0.s diff --git a/gbdk-lib/libc/targets/mos6502/nes/crt0.s b/gbdk-lib/libc/targets/mos6502/nes/crt0.s @@ -72,7 +72,7 @@ _attribute_row_dirty:: .ds 1 _attribute_column_dirty:: .ds 1 __oam_valid_display_on:: .ds 1 __SYSTEM:: .ds 1 -__hblank_writes_index: .ds 1 +__hblank_writes_index:: .ds 1 .define __crt0_NMITEMP "___SDCC_m6502_ret4" @@ -87,12 +87,12 @@ _TMA_REG:: .ds 1 .area _BSS __crt0_paletteShadow:: .ds 25 .mode:: .ds 1 -__lcd_isr_PPUCTRL: .ds (2*.MAX_DEFERRED_ISR_CALLS) -__lcd_isr_PPUMASK: .ds (2*.MAX_DEFERRED_ISR_CALLS) -__lcd_isr_scroll_x: .ds (2*.MAX_DEFERRED_ISR_CALLS) -__lcd_isr_scroll_y: .ds (2*.MAX_DEFERRED_ISR_CALLS) -__lcd_isr_delay_num_scanlines: .ds (2*.MAX_DEFERRED_ISR_CALLS) -__lcd_isr_ppuaddr_lo: .ds (2*.MAX_DEFERRED_ISR_CALLS) +__lcd_isr_PPUCTRL:: .ds (2*.MAX_DEFERRED_ISR_CALLS) +__lcd_isr_PPUMASK:: .ds (2*.MAX_DEFERRED_ISR_CALLS) +__lcd_isr_scroll_x:: .ds (2*.MAX_DEFERRED_ISR_CALLS) +__lcd_isr_scroll_y:: .ds (2*.MAX_DEFERRED_ISR_CALLS) +__lcd_isr_delay_num_scanlines:: .ds (2*.MAX_DEFERRED_ISR_CALLS) +__lcd_isr_ppuaddr_lo:: .ds (2*.MAX_DEFERRED_ISR_CALLS) _TAC_REG:: .ds 1 ; Unused, for GB compatibility .area _CODE @@ -234,9 +234,9 @@ __crt0_NMI: pha ; Skip graphics updates if blanked, to allow main code to do VRAM address / scroll updates - lda *_shadow_PPUMASK - and #(PPUMASK_SHOW_BG | PPUMASK_SHOW_SPR) - beq __crt0_NMI_skip + nop + bit *__oam_valid_display_on + bvs __crt0_NMI_skip ; Do Sprite DMA or delay equivalent cycles jsr __crt0_doSpriteDMA ; Update VRAM @@ -326,9 +326,9 @@ __crt0_NMI_skip: clc adc #1 sta *_sys_time - lda *(_sys_time+1) - adc #0 - sta *(_sys_time+1) + bcc 9$ + inc *(_sys_time+1) +9$: pla tay @@ -462,119 +462,19 @@ __crt0_clearVRAM_loop: .wait_vbl_done:: _wait_vbl_done:: _vsync:: - - .define .lcd_scanline_previous "REGTEMP" - .define .lcd_buf_index "REGTEMP+1" - .define .lcd_buf_end "REGTEMP+2" - jsr _flush_shadow_attributes - - ; Save shadow registers that VBL or LCD isr could change - lda *_shadow_PPUMASK - pha - lda *_shadow_PPUCTRL - pha - lda *_bkg_scroll_x - pha - lda *_bkg_scroll_y - pha - - ; Allow VBL isr to modify shadow registers if present - jsr .jmp_to_VBL_isr - - ; Set initial scanline value - lda #0xFF - sta *.lcd_scanline_previous - - lda *__hblank_writes_index - clc - adc #.MAX_DEFERRED_ISR_CALLS - cmp #(2*.MAX_DEFERRED_ISR_CALLS) - bcc 20$ - lda #0 -20$: - sta *.lcd_buf_index - clc - adc #.MAX_DEFERRED_ISR_CALLS - sta *.lcd_buf_end - - ; Write shadow registers as first LCD buffer entry (actually VBL) - ldy *.lcd_buf_index - ldx #.SCREENHEIGHT-1 - jsr .write_shadow_registers_to_buffer - iny - sty *.lcd_buf_index - ; Ensure second entry (actual LCD) starts off with zero (end-of-list) - lda #0 - sta __lcd_isr_delay_num_scanlines,y - ; Skip to end if LCD isr functionality is disabled (0x60 = RTS means LCD isr disabled) - lda .jmp_to_LCD_isr - cmp #0x60 - beq _wait_vbl_done_waitForNextFrame - - lda *.lcd_scanline_previous - - jmp 2$ + ; if display is on, run deferred ISR handlers + bit *__oam_valid_display_on + bvs 1$ + jsr .deferred_isr_run 1$: - pla - sta *.lcd_scanline_previous -2$: - ; We are done if next scanline is <= the previous one - cmp #0xFF - beq 3$ - cmp *__lcd_scanline - bcs _wait_vbl_done_waitForNextFrame -3$: - ; - ldy *.lcd_buf_index - lda *__lcd_scanline - ; We are done if next LCD scanline >= SCREENHEIGHT - cmp #.SCREENHEIGHT - bcs _wait_vbl_done_waitForNextFrame - pha - sec - sbc *.lcd_scanline_previous - sta __lcd_isr_delay_num_scanlines,y - ; Call LCD isr - jsr .jmp_to_LCD_isr - ; Grab previous LCD scanline value from stack and store in X - pla - tax - pha - jsr .write_shadow_registers_to_buffer - - iny - sty *.lcd_buf_index - cpy *.lcd_buf_end - bne 1$ - - ; Clear last-scanline-value from stack - pla - -_wait_vbl_done_waitForNextFrame: - ; Flip __hblank_writes_index for NMI handler - ldy #0 - lda *__hblank_writes_index - bne 21$ - ldy #.MAX_DEFERRED_ISR_CALLS -21$: - sty *__hblank_writes_index - ; Enable OAM DMA in next NMI lda *__oam_valid_display_on ora #OAM_VALID_MASK sta *__oam_valid_display_on - ; Restore shadow registers - pla - sta *_bkg_scroll_y - pla - sta *_bkg_scroll_x - pla - sta *_shadow_PPUCTRL - pla - sta *_shadow_PPUMASK +_wait_vbl_done_waitForNextFrame: lda *_sys_time _wait_vbl_done_waitForNextFrame_loop: cmp *_sys_time @@ -587,65 +487,39 @@ _wait_vbl_done_waitForNextFrame_loop: rts -; -; Writes shadow registers to buffer -; -; Input: -; X: Scanline number -; -.write_shadow_registers_to_buffer: - ; Copy shadow registers - ldy *.lcd_buf_index - lda *_shadow_PPUMASK - sta __lcd_isr_PPUMASK,y - lda *_shadow_PPUCTRL - sta __lcd_isr_PPUCTRL,y - lda *_bkg_scroll_x - sta __lcd_isr_scroll_x,y - lsr - lsr - lsr - sta __lcd_isr_ppuaddr_lo,y - ; Add _bkg_scroll_y+1 to _lcd_scanline to generate final Y-scroll, with 239->0 wrap-around - txa - sec - adc *_bkg_scroll_y - bcc 1$ - sbc #.SCREENHEIGHT -1$: - cmp #.SCREENHEIGHT - bcc 2$ - sbc #.SCREENHEIGHT -2$: - sta __lcd_isr_scroll_y,y - and #0xF8 - asl - asl - ora __lcd_isr_ppuaddr_lo,y - sta __lcd_isr_ppuaddr_lo,y - rts - .display_off:: _display_off:: + ; Skip entirely if display_off is called repeatedly + bit *__oam_valid_display_on + bvs 1$ + ; Reset deferred ISR buffers + jsr .deferred_isr_reset + ; Clear BG and SPR in first __lcd_isr_PPUMASK, to cause + ; NMI code to clear PPUMASK *after* 1 normal execution lda *_shadow_PPUMASK - and #~(PPUMASK_SHOW_BG | PPUMASK_SHOW_SPR) - sta *_shadow_PPUMASK - sta PPUMASK + and #~(PPUMASK_SHOW_BG|PPUMASK_SHOW_SPR) + sta __lcd_isr_PPUMASK + ; Wait for 1 execution of NMI to drain vram transfer buffer + jsr _wait_vbl_done_waitForNextFrame ; Set forced blanking bit lda *__oam_valid_display_on ora #DISPLAY_OFF_MASK sta *__oam_valid_display_on +1$: rts .display_on:: _display_on:: - lda *_shadow_PPUMASK - ora #(PPUMASK_SHOW_BG | PPUMASK_SHOW_SPR) - sta *_shadow_PPUMASK - ; Clear forced blanking bit + ; Skip entirely if display_on is called repeatedly + bit *__oam_valid_display_on + bvc 1$ + ; Reset deferred ISR buffers + jsr .deferred_isr_reset_and_init + ; Set DISPLAY_ON bits lda *__oam_valid_display_on and #~DISPLAY_OFF_MASK sta *__oam_valid_display_on +1$: rts __crt0_RESET: @@ -714,9 +588,7 @@ __crt0_RESET_bankSwitchValue: lda #(PPUCTRL_NMI | PPUCTRL_SPR_CHR) sta *_shadow_PPUCTRL sta PPUCTRL - ; Prepare VBL buffer data (need to start at scanline -1 = SCREENHEIGHT-1 for correct Y scroll) - ldx #.SCREENHEIGHT-1 - jsr .write_shadow_registers_to_buffer + jsr .deferred_isr_reset ; Call main jsr _main ; main finished - loop forever diff --git a/gbdk-lib/libc/targets/mos6502/nes/deferred_isr.s b/gbdk-lib/libc/targets/mos6502/nes/deferred_isr.s @@ -0,0 +1,183 @@ + .include "global.s" + + .area _HOME + + .define .lcd_scanline_previous "REGTEMP" + .define .lcd_buf_index "REGTEMP+1" + .define .lcd_buf_end "REGTEMP+2" + +; +; Writes shadow registers to buffer +; +; Input: +; X: Scanline number +; +.write_shadow_registers_to_buffer:: + ; Copy shadow registers + ldy *.lcd_buf_index + lda *_shadow_PPUMASK + sta __lcd_isr_PPUMASK,y + lda *_shadow_PPUCTRL + sta __lcd_isr_PPUCTRL,y + lda *_bkg_scroll_x + sta __lcd_isr_scroll_x,y + lsr + lsr + lsr + sta __lcd_isr_ppuaddr_lo,y + ; Add _bkg_scroll_y+1 to _lcd_scanline to generate final Y-scroll, with 239->0 wrap-around + txa + sec + adc *_bkg_scroll_y + bcc 1$ + sbc #.SCREENHEIGHT +1$: + cmp #.SCREENHEIGHT + bcc 2$ + sbc #.SCREENHEIGHT +2$: + sta __lcd_isr_scroll_y,y + and #0xF8 + asl + asl + ora __lcd_isr_ppuaddr_lo,y + sta __lcd_isr_ppuaddr_lo,y + rts + +; +; Resets the deferred ISR. +; +; This effectively omits the double-buffering delay and writes the shadow registers to the first buffer. +; It is intended to be used when display is re-enabled after being turned off, to display with reasonable values. +; +; Note that in contrast to .run_deferred_isr_handlers, this will NOT actually run VBL / LCD handlers, so +; can still cause glitches. +; +.deferred_isr_reset:: + ; Prepare VBL buffer data (need to start at scanline -1 = SCREENHEIGHT-1 for correct Y scroll) + lda #0 + sta *__hblank_writes_index + sta *.lcd_buf_index + sta __lcd_isr_delay_num_scanlines+1 + ldx #.SCREENHEIGHT-1 + jmp .write_shadow_registers_to_buffer + +; +; Resets the deferred ISR, then runs VBL and LCD handlers twice to initialize buffers with valid data. +; +.deferred_isr_reset_and_init:: + jsr .deferred_isr_reset + jsr .deferred_isr_run + jmp .deferred_isr_run + +; +; Executes the deferred VBL/LCD handlers. +; +; After each ISR handler has run, PPU shadow registers are written to a buffer +; which is consumed by the vblank NMI handler. +; Double-buffering is used to avoid buffer locking / race conditions. +; +.deferred_isr_run:: + ; Save shadow registers that VBL or LCD isr could change + lda *_shadow_PPUMASK + pha + lda *_shadow_PPUCTRL + pha + lda *_bkg_scroll_x + pha + lda *_bkg_scroll_y + pha + + ; Allow VBL isr to modify shadow registers if present + jsr .jmp_to_VBL_isr + + ; Set initial scanline value + lda #0xFF + sta *.lcd_scanline_previous + + lda *__hblank_writes_index + clc + adc #.MAX_DEFERRED_ISR_CALLS + cmp #(2*.MAX_DEFERRED_ISR_CALLS) + bcc 20$ + lda #0 +20$: + sta *.lcd_buf_index + clc + adc #.MAX_DEFERRED_ISR_CALLS + sta *.lcd_buf_end + + ; Write shadow registers as first LCD buffer entry (actually VBL) + ldy *.lcd_buf_index + ldx #.SCREENHEIGHT-1 + jsr .write_shadow_registers_to_buffer + iny + sty *.lcd_buf_index + + ; Ensure second entry (actual LCD) starts off with zero (end-of-list) + lda #0 + sta __lcd_isr_delay_num_scanlines,y + ; Skip to end if LCD isr functionality is disabled (0x60 = RTS means LCD isr disabled) + lda .jmp_to_LCD_isr + cmp #0x60 + beq .deferred_isr_run_done + + lda *.lcd_scanline_previous + + jmp 2$ +1$: + pla + sta *.lcd_scanline_previous +2$: + ; We are done if next scanline is <= the previous one + cmp #0xFF + beq 3$ + cmp *__lcd_scanline + bcs .deferred_isr_run_done +3$: + ; + ldy *.lcd_buf_index + lda *__lcd_scanline + ; We are done if next LCD scanline >= SCREENHEIGHT + cmp #.SCREENHEIGHT + bcs .deferred_isr_run_done + pha + sec + sbc *.lcd_scanline_previous + sta __lcd_isr_delay_num_scanlines,y + ; Call LCD isr + jsr .jmp_to_LCD_isr + ; Grab previous LCD scanline value from stack and store in X + pla + tax + pha + jsr .write_shadow_registers_to_buffer + + iny + sty *.lcd_buf_index + cpy *.lcd_buf_end + bne 1$ + + ; Clear last-scanline-value from stack + pla + +.deferred_isr_run_done: + ; Flip __hblank_writes_index for NMI handler + ldy #0 + lda *__hblank_writes_index + bne 21$ + ldy #.MAX_DEFERRED_ISR_CALLS +21$: + sty *__hblank_writes_index + + ; Restore shadow registers + pla + sta *_bkg_scroll_y + pla + sta *_bkg_scroll_x + pla + sta *_shadow_PPUCTRL + pla + sta *_shadow_PPUMASK + + rts + diff --git a/gbdk-lib/libc/targets/mos6502/nes/global.s b/gbdk-lib/libc/targets/mos6502/nes/global.s @@ -141,6 +141,9 @@ ;; Symbols defined at link time .globl _shadow_OAM, __vram_transfer_buffer + .globl __lcd_isr_PPUCTRL, __lcd_isr_PPUMASK + .globl __lcd_isr_scroll_x, __lcd_isr_scroll_y, __lcd_isr_ppuaddr_lo + .globl __lcd_isr_delay_num_scanlines ;; Main user routine .globl _main
This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.