gbdk-2020 | GameBoy Development Kit |
| download: https://git.y1.nz/archives/gbdk.tar.gz | |
| README | Files | Log | Refs | LICENSE |
commit ac571c1dab96007faba07ae10ee052f4ab526d5d parent 8df4497c47820261e93c3bd57296b63178791c5f Author: Michel Iwaniec <46843052+michel-iwaniec@users.noreply.github.com> Date: Sun, 30 Mar 2025 23:43:39 +0100 NES: Align coordinates and scanline counting in LCD ISR implementation with GB, add SCX / SCY / LYC defines (#755) NES: Align coordinates and scanline counting in LCD ISR implementation with GB, add SCX / SCY / LYC defines - Change definition of _lcd_scanline to be -1 less than current, aligning with GB LYC register - Change definition of _bkg_scroll_y to match GB's SCY, being relative to current scanline - Refactor do_hblank_writes, delay_fractional and delay_to_scanline subroutines in crt0 - Add "#define LYC_REG" and "#define LY_REG" as aliases of _lcd_scanline - Add #defines for SCX and SCY to alias _bkg_scroll_x / _bkg_scroll_y shadow variables - Change text scroller example to use LYC_REG / SCX / SCY instead of _lcd_scanline / move_bkg, remove redundant #ifdefs - Add subtle shake in y direction to text scroller example, to check that GB / NES coordinates match - Update "From Game Boy to NES" section's descriptions of LCD handlers - Update "Migrating to new GBDK versions" with a short description of bkg_scroll_y changing from absolute to relative Y coordinates for 4.4.0 Diffstat:
| M | docs/pages/06b_supported_consoles.md | 38 | ++++++++++++++++++++------------------ |
| M | docs/pages/09_migrating_new_versions.md | 6 | ++++++ |
| M | gbdk-lib/examples/cross-platform/scroller/src/text_scroller.c | 37 | ++++++++++++------------------------- |
| M | gbdk-lib/include/nes/hardware.h | 12 | ++++++++++++ |
| M | gbdk-lib/libc/targets/mos6502/nes/crt0.s | 99 | ++++++++++++++++++++++++++++++++++++++++++++++--------------------------------- |
5 files changed, 108 insertions(+), 84 deletions(-)
diff --git a/docs/pages/06b_supported_consoles.md b/docs/pages/06b_supported_consoles.md @@ -470,16 +470,31 @@ To simplify the programming interface, gbdk-nes functions like move_bkg / scroll GBDK provides an API for installing Interrupt Service Routines that execute on start of vblank (VBL handler), or on a specific scanline (LCD handler). -But the base NES system has no suitable scanline interrupts that can provide such functionality. So instead, gbdk-nes API allows *fake* handlers to be installed in the goal of keeping code compatible with other platforms. +But the base NES system has no suitable scanline interrupts that can provide the exact equivalent functionality. So instead, gbdk-nes API allows *fake* handlers to be installed in the goal of keeping code compatible with other platforms. -* An installed VBL handler will be called immediately when calling vsync. This handler should only update PPU shadow registers. +* An installed VBL handler will be called immediately when calling vsync. This handler should only update PPU shadow registers. After each invocation, shadow registers are stored into a buffer. * An installed LCD handler for a specific scanline will then be called repeatedly until the value of _lcd_scanline is either set to an earlier scanline or >= 240. After each invocation, shadow registers are stored into a buffer. -* After the vblank NMI handler has finished palette updates, OAM DMA, VRAM updates and scroll updates it will then manually run a delay loop to reach the particular scanlines that the installed LCD handler was pre-called for, and use the contents of the buffer to update registers. +* After the built-in vblank NMI handler has finished palette updates, OAM DMA, VRAM updates it will then use the buffered VBL shadow registers to write the real registers. If LCD handlers are enabled it will then manually run a delay loop to reach the particular scanlines that the installed LCD handler was pre-called for, and use the contents of the buffer to update registers. Because the LCD "ISR" is actually implemented with a delay loop, it will burn a lot of CPU cycles in the frame - the further down the requested scanline is the larger the CPU cycle loss. In practice this makes this faked-LCD-ISR functionality mostly suitable for status bars at the top of the screen screen. Or for simple parallax cutscenes where the CPU has little else to do. -@note The support for VBL and LCD handlers is currently under consideration and subject to change in newer versions of gbdk-nes. +To make porting between user VBL / LCD handlers written in C easier, gbdk-nes also provides aliases for the shadow registers that correspond to the GB hardware registers. + +* @ref SCX_REG is an alias for @ref bkg_scroll_x shadow register +* @ref SCY_REG is an alias for @ref bkg_scroll_y shadow register +* @ref LYC_REG is an alias for @ref _lcd_scanline shadow register + +Because these are shadow registers that are interpreted by the GBDK library to mimick GB behaviour, they won't behave exactly how the GB hardware registers do under all conditions. However, for most practical purposes they allow writing portable VBL / LCD handlers in C. + +@note +The bkg_scroll_y shadow register functions the same as @ref SCY_REG GB, with its value added to @ref _lcd_scanline to determine the final Y scrolling coordinate. However, its range is different due to tilemaps being 32x30 instead 32x32. + +Negative coordinates won't work correctly due to the wrapping from 239 to 0. Instead, they need to be corrected with this wrapping in mind. i.e. a negative coordinate of -1 needs to be converted to 239 before being written to bkg_scroll_y. + +A portable way to do this is to check for a negative offset, and use the screen height define: + + SCY_REG = offset < 0 ? (uint8_t)(DEVICE_SCREEN_BUFFER_HEIGHT*8 + offset) : offset; ### Caveat: Make sure to call vsync on every frame @@ -488,7 +503,7 @@ On the GB, the call to vsync is an optional call that serves two purposes: 1. It provides a consistent frame timing for your game 2. It allows future register writes to be synchronized to the screen -On gbdk-nes the second point is no longer true, because writes need to be made to the shadow registers *before* vsync is called. +On gbdk-nes the second point is no longer true, because writes need to be made to the shadow registers either *before* vsync is called, or in a user VBL isr handler. But the vsync call serves three other very important purposes: @@ -498,19 +513,6 @@ C. It calls flush_shadow_attributes so that updates to background attributes act For these reasons you should always include a call to vsync if you expect to see any graphical updates on the screen. -### Caveat: Do all status bar scroll movement in LCD handlers to mitigate glitches - -The fake LCD ISR system is not bullet-proof. In particular, it has a problem where lag frames can cause the shadow register updates in LCD handlers not to be ready in time for when the timed code in the NMI handler would be called. This will effectively cause all those updates to be missing for one frame, and result in glitched scroll updates. - -There is currently no complete work-around for this problem other than avoiding lag frames altogether. But the glitch can be made less distracting by making sure only the status bar glitches rather than the main background. - -If you are using LCD handlers to achieve a top-screen stationary status bar, it is recommended that you follow the following guidelines to make sure the background itself has consistent scrolling: -* Use move_bkg either in your main loop or in the VBL handler, to set the level scrolling -* Use move_bkg in the first invocation of the LCD handler, to set the (stationary) status bar scroll position -* Use move_bkg in the second invocation of the LCD handler, to reset the background scrolling - -In short: Ensuring that the last called LCD handler sets the scroll back to the original value means the PPU rendering keeps rendering the background from the same scrolling position even when the NMI handling was missed. - ### Implementation of timer handler The nature of the deferred handling for fake VBL and LCD handlers in gbdk-nes means that lag frames will cause these handlers to be called at delayed irregular times. diff --git a/docs/pages/09_migrating_new_versions.md b/docs/pages/09_migrating_new_versions.md @@ -4,6 +4,12 @@ This section contains information that may be useful to know or important when u # GBDK-2020 versions +## Porting to GBDK-2020 4.4.0 + + - NES LCD bkg_scroll_y is now relative to the current scanline + - This change creates higher compatibility with GB's SCY_REG and makes it easier to re-use GB LCD handlers. + - This behaves differently to 4.3.0 and affects LCD handlers that change the y scrolling coordinate mid-frame. + ## Porting to GBDK-2020 4.3.0 - GBDK now requires ~SDCC 4.4.0 or higher with GBDK-2020 patches for the z80 and NES - Changed to new calling convention for @ref printf(), @ref sprintf(), @ref abs() diff --git a/gbdk-lib/examples/cross-platform/scroller/src/text_scroller.c b/gbdk-lib/examples/cross-platform/scroller/src/text_scroller.c @@ -3,44 +3,36 @@ #include <stdint.h> #include <stdio.h> +// Positive 1 in valid y scroll coordinates +#define P1 1 +// Negative 1 in valid y scroll coordinates +#define N1 (uint8_t)(DEVICE_SCREEN_BUFFER_HEIGHT*8 - 1) + +const uint8_t shake_tbl[] = {0, P1, P1, P1, 0, N1, N1, N1}; const uint8_t scanline_offsets_tbl[] = {0, 1, 2, 3, 3, 2, 1, 0, 0, 1, 2, 3, 3, 2, 1, 0}; const uint8_t * scanline_offsets = scanline_offsets_tbl; #define SCROLL_POS 15 -#define SCROLL_POS_PIX_START ((SCROLL_POS + DEVICE_SCREEN_Y_OFFSET) * 8) - 1 +#define SCROLL_POS_PIX_START ((SCROLL_POS + DEVICE_SCREEN_Y_OFFSET) * 8) - 2 #define SCROLL_POS_PIX_END ((SCROLL_POS + DEVICE_SCREEN_Y_OFFSET + 1) * 8) - 1 -extern uint8_t _lcd_scanline; - uint8_t scroller_x = 0; void scanline_isr(void) { -#if defined(NINTENDO_NES) - switch (_lcd_scanline) { - case 0: - move_bkg(0, 0); - _lcd_scanline = SCROLL_POS_PIX_START + 1; - break; - case SCROLL_POS_PIX_START + 1: - move_bkg(scroller_x, SCROLL_POS_PIX_START + 1); - _lcd_scanline = SCROLL_POS_PIX_END + 1; - break; - case SCROLL_POS_PIX_END + 1: - move_bkg(0, SCROLL_POS_PIX_END + 1); - _lcd_scanline = 0; - break; - } -#elif defined(NINTENDO) + +#if defined(NINTENDO) || defined(NINTENDO_NES) switch (LYC_REG) { case 0: SCX_REG = 0; + SCY_REG = 0; LYC_REG = SCROLL_POS_PIX_START; break; case SCROLL_POS_PIX_START: SCX_REG = scroller_x; + SCY_REG = shake_tbl[(scroller_x >> 1) & 7]; LYC_REG = SCROLL_POS_PIX_END; break; case SCROLL_POS_PIX_END: - SCX_REG = LYC_REG = 0; + SCX_REG = SCY_REG = LYC_REG = 0; break; } #elif defined(SEGA) @@ -109,11 +101,6 @@ void main(void) { // put next char set_vram_byte(scroller_vram_addr, *scroller_next_char - 0x20); } -#ifdef NINTENDO_NES - // Normal indirect setting of scroll via shadow registers (written by vblank handler) - move_bkg(0,0); - _lcd_scanline = 0; -#endif vsync(); } } diff --git a/gbdk-lib/include/nes/hardware.h b/gbdk-lib/include/nes/hardware.h @@ -55,9 +55,21 @@ __REG(0x4014) OAMDMA; // Scrolling coordinates (will be written to PPUSCROLL at end-of-vblank by NMI handler) __SHADOW_REG bkg_scroll_x; __SHADOW_REG bkg_scroll_y; +// LCD scanline - a software-driven version of GB's incrasing 'LY' scanline counter +__SHADOW_REG _lcd_scanline; extern volatile UBYTE TIMA_REG; extern volatile UBYTE TMA_REG; extern volatile UBYTE TAC_REG; +// Compatibility defines for GB LY / LYC registers, to allow easier LCD ISR porting +#define SCY_REG bkg_scroll_y /**< Scroll Y */ +#define rSCY SCY_REG +#define SCX_REG bkg_scroll_x /**< Scroll X */ +#define rSCX SCX_REG +#define LY_REG _lcd_scanline /**< LCDC Y-coordinate */ +#define rLY LY_REG +#define LYC_REG _lcd_scanline /**< LY compare */ +#define rLYC LYC_REG + #endif diff --git a/gbdk-lib/libc/targets/mos6502/nes/crt0.s b/gbdk-lib/libc/targets/mos6502/nes/crt0.s @@ -180,29 +180,34 @@ ProcessDrawList: ; .define .acc "___SDCC_m6502_ret4" .delay_to_lcd_scanline:: - jsr .delay_12_cycles jmp 2$ 1$: jsr .delay_28_cycles jsr .delay_28_cycles jsr .delay_12_cycles ; -> 28 + 28 + 12 = 68 cycles + clc 2$: - jsr .delay_fractional ; -> 40.666 NTSC cycles 33.5625 PAL cycles - + jsr .delay_fractional ; -> 35.666 NTSC cycles 28.5625 PAL cycles + lda *0x00 + dex bne 1$ ; -> 5 cycles rts .delay_28_cycles: jsr .delay_12_cycles +.delay_16_cycles: nop +.delay_14_cycles: nop .delay_12_cycles: rts ; -; Takes 40.666 NTSC cycles / 33.5626 PAL cycles +; Takes 35.666 NTSC cycles / 28.5626 PAL cycles +; +; Note: does NOT clear carry - this needs to be handled by caller ; .delay_fractional: lda #144 ; Initialize A with PAL fractional cycle count @@ -215,12 +220,10 @@ ProcessDrawList: nop 3$: ; -> 15 NTSC cycles / 8 PAL cycles ; Add fractional cycles and branch on carry - clc adc *.acc - sta *.acc bcs 4$ 4$: - sta *.acc ; -> 13.666 NTSC cycles / 13.5625 PAL cycles + sta *.acc ; -> 8.666 NTSC cycles / 8.5625 PAL cycles rts ; -> 6 cycles for RTS, 6 cycles for JSR = 12 cycles __crt0_NMI: @@ -441,7 +444,6 @@ _vsync:: .define .lcd_scanline_previous "REGTEMP" .define .lcd_buf_index "REGTEMP+1" .define .lcd_buf_end "REGTEMP+2" - .define .plus_one_flag "REGTEMP+3" jsr _flush_shadow_attributes @@ -461,9 +463,6 @@ _vsync:: ; Set initial scanline value lda #0xFF sta *.lcd_scanline_previous - ; Init +0/+1 bits for simulated Y-increment between calls - lda #0x7F - sta *.plus_one_flag lda *__hblank_writes_index clc @@ -477,16 +476,9 @@ _vsync:: adc #.MAX_DEFERRED_ISR_CALLS sta *.lcd_buf_end - ; Special-case: LCD at scanline 0 should just directly replace first entry - lda *__lcd_scanline - bne 0$ - jsr .jmp_to_LCD_isr - lda #0xFF - sta *.lcd_scanline_previous -0$: - - ; Write shadow registers as first LCD entry (VBL and LCD at scanline 0 are equal) + ; 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 @@ -522,13 +514,12 @@ _vsync:: sec sbc *.lcd_scanline_previous sta __lcd_isr_delay_num_scanlines,y - ; Add number of delayed scanlines+1 to _bkg_scroll_y to simulate PPU increment - ; (but old _bkg_scroll_y needs to be treated as -1 in first simulated-PPU-increment) - asl *.plus_one_flag - adc *_bkg_scroll_y - sta *_bkg_scroll_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 @@ -574,6 +565,12 @@ _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 @@ -587,7 +584,17 @@ _wait_vbl_done_waitForNextFrame_loop: lsr lsr sta __lcd_isr_ppuaddr_lo,y - lda *_bkg_scroll_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 @@ -691,25 +698,44 @@ __crt0_RESET_bankSwitchValue: __crt0_waitForever: jmp __crt0_waitForever +.bndry 0x100 .do_hblank_writes: .define .reg_write_index "__crt0_NMITEMP+1" .define .lda_PPUADDR "__crt0_NMITEMP+2" .define .ldx_PPUMASK "__crt0_NMITEMP+3" - jsr .delay_12_cycles + ; Delay to make hblank at end of scanline 0 + ldx #10 +0$: + dex + bne 0$ + clc nop - + + sty *.reg_write_index + lda #0 sta *.acc 1$: - sty *.reg_write_index ldx __lcd_isr_delay_num_scanlines,y + cpx #1 + beq 3$ ; Skip delay if next scanline + cpx #0 beq 2$ ; Exit if empty buffer (no calls were made within frame) dex - beq 3$ ; Skip delay if next scanline jsr .delay_to_lcd_scanline + jsr .delay_12_cycles + jsr .delay_28_cycles + nop + nop + nop + nop + lda *0x00 3$: - ldy *.reg_write_index + + ; Delay for 35.666 NTSC cycles / 28.5625 PAL cycles + jsr .delay_fractional + ; Pre-write PPUADDR (1st write) and y-scroll sty PPUADDR lda __lcd_isr_scroll_y,y @@ -741,17 +767,8 @@ __crt0_waitForever: stx PPUMASK sty PPUCTRL - ; Delay for 40.666 NTSC cycles / 33.5625 PAL cycles - jsr .delay_fractional + inc *.reg_write_index ldy *.reg_write_index - - ldx #6 -10$: - dex - bne 10$ - nop - - iny jmp 1$ 2$: rts
This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.