Dynamic Background Loading Problems

Are you new to 6502, NES, or even programming in general? Post any of your questions here. Remember - the only dumb question is the question that remains unasked.

Moderator: Moderators

Post Reply
EpicFlan
Posts: 2
Joined: Tue Oct 29, 2024 4:34 pm

Dynamic Background Loading Problems

Post by EpicFlan »

Hey all, I've just been working on my first small project for a couple of weeks, following along with some tutorials for the basics, but have come up against what I imagine to be an issue for a fairly basic thing, and just can't work out what's wrong. Hoping that maybe some experts here could point me in the right direction.

I'm trying to create a simple background looping system which can dynamically load backgrounds into the PPU. I just have a single background at the moment, and have the game scrolling horizontally, loading in each column from the background data one by one as the horizontal scroll reaches it. The dynamic loading of columns works fine, after much work, but I still need to load in the initial background when the game starts. I tried to build a simple procedure which reuses my column loading procedure, simply looping through it 32 times (for 32 columns) before the PPU is then enabled.

When I implement this though, it seems to break everything, even the aforementioned column loading loop contained in my NMI handler. All I can seem to work out based on the FCEUX debugger is that the palette data seemingly gets overwritten, implying that my writes to the PPU are hitting addresses that they're not supposed to. I can't work out why though, as the initial background loading is using mostly the same code as the subsequent writes. Plus it seems to even break the column loop itself during NMI handling. I feel that it must be something simple, like I'm not setting the PPU up properly during the initial load, or misunderstanding what's actually happening under the hood. If anyone could take a look at my code and see if they notice what's wrong about the 'LoadBackground' procedure which would be breaking everything else, it would really help me out, as I've been at this for days now with no progress.

I've attached what I believe to be the relevant pieces of code (not every procedure). I've also left a comment in the code against a particular line in the 'LoadBackground' procedure which I've identified as the culprit that actually breaks everything; if I comment that line the background still doesn't load, but it doesn't break the rest of my game loop.

(I've also attached the full project, if needed)

Code: Select all

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Procedure to Load Background Data into PPU Nametable
;;      - Loop through and draw 32 columns of tiles
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.proc LoadBackground
    lda #<BackgroundData
    sta SourceAddr                  ; lo-byte of source address to background data lo-byte
    lda #>BackgroundData
    sta SourceAddr+1                ; hi-byte of source address to background data hi-byte
    
    bit PPU_STATUS
    lda #$20
    sta NewColAddr+1                ; hi-byte of PPU Address to $20 (first nametable)
    lda #$00
    sta NewColAddr                  ; lo-byte of PPU Address to $00

LoopBackgroundColumn:
    jsr DrawNewColumn                   ; draw a new column
    ;inc NewColAddr                      ; update PPU Address lo-byte to next column
                                            ; <- CURRENTLY BREAKS GAME PALETTE AND SUBSEQUENT COLUMN LOADING WHEN ENABLED
    lda Column
    cmp #0                              ; if current column is 0 (wrapped around from 32)
    bne LoopBackgroundColumn                ; Else: continue looping

    rts
.endproc

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Reset Handler
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Reset:
    INIT_NES

InitVariables:
    lda #0
    sta Frame
    sta Clock60

    ldx #0
    lda SpriteData,x        ; get Sprite Data at index 0 (y-position)
    sta YPos+1              ; store Sprite Data y-position in YPos pixels

    ldx #3
    lda SpriteData,x        ; get Sprite Data at index 3 (x-position)
    sta XPos+1              ; store Sprite Data x-position in XPos pixels

    lda #0
    sta XScroll+1
    sta XScroll
    sta Row
    sta Column

    lda #1
    sta NewColumn           ; setup to draw new column immediately

Main:
    jsr LoadPalette         ; load Palette Data
    jsr LoadSprites         ; load Sprites

    lda #%00010100          ; disable NMI, Set Background to second Pattern Table
    sta PPU_CTRL
    jsr LoadBackground      ; load initial Background Data

EnablePPURendering:
    bit PPU_STATUS
    lda #%10010100          ; Enable NMI, Set Background to second Pattern Table
    sta PPU_CTRL
    lda #0
    sta PPU_SCROLL
    sta PPU_SCROLL
    lda #%00011110
    sta PPU_MASK            ; Set PPU_MASK bits to show background

LoopForever:
    jmp LoopForever

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; NMI Handler
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
NMI:
    lda #$00                ; disable rendering
    sta PPU_MASK

NewColumnCheck:
    lda NewColumn
    cmp #1                  ; if new column flag set
    bne FinishColumn
        jsr DrawNewColumn   ; If it is, proceed to draw a new column of tiles

FinishColumn:
    jsr ScrollBackground
    jsr RefreshRendering
    jsr CheckColumnAddresses

    rti

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Routine to check if a new column of tiles needs to be drawn
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.proc CheckColumnAddresses
NewColumnCheck:
    lda XScroll+1
    and #%00000111              ; Check if the scroll a multiple of 8
    bne NoNewColumn                 ; Else: we still don't need to draw a new column
        lda XScroll
        cmp #0                      ; Check if scroll fraction is 0
        bne NoNewColumn                 ; Else: we still don't need to draw a new column
            jsr SetColumnAddresses
            jmp FinishColumn
NoNewColumn:
    lda #0
    sta NewColumn
FinishColumn:
    rts
.endproc

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Routine to set a new column of tiles
;;      - Calculate lo-byte & hi-byte of PPU destination address
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.proc SetColumnAddresses
    lda #1
    sta NewColumn

    lda XScroll+1
    lsr
    lsr
    lsr                             ; divide by 8 to get column (pixel position 8 -> column 1)
    sta NewColAddr                  ; lo-byte of destination address

    lda CurNam
    eor #1                          ; invert current nametable to 0 or 1, into accumulator
    asl
    asl                             ; multiply by 4, get either $00 -> $00 or $01 -> $04
    clc
    adc #$20                        ; add to $20 to get either $20 or $24
    sta NewColAddr+1                ; hi-byte of destination address ($20XX or $24XX)

    lda #<BackgroundData
    sta SourceAddr                  ; lo-byte of source address to background data lo-byte

    lda #>BackgroundData
    sta SourceAddr+1                ; hi-byte of source address to background data hi-byte

    rts
.endproc

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Routine to draw a new column of tiles off-screen as we scroll horizontally
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.proc DrawNewColumn
    PPU_SETADDR_VALUE NewColAddr

    ldx #$04
    ldy Column
ColumnLoop:
    PPU_SETDATA_OFFSET (SourceAddr),y

    inc Row                         ; increment current row
    lda #30
    cmp Row                         ; is current row last row
    beq FinishColumn                    ; Then: finish loop

    tya
    clc
    adc #32                         ; add 32 to y-offset, to next row
    tay
    bcc ColumnLoop                  ; if y not exceed 256 and set Carry, continue loop
NextBatch:
    inc SourceAddr+1                ; increment hi-byte of source, to next batch
    ldy Column                      ; reset y offset
    jmp ColumnLoop                  ; restart draw loop

FinishColumn:
    lda Column
    clc
    adc #1                          ; increment current column
    and #%00011111                  ; drop left-most bit to clamp value to 32, wrap around to 0
    sta Column

    rts
.endproc
Attachments
loadbackground.zip
(31.23 KiB) Downloaded 17 times
Last edited by EpicFlan on Wed Oct 30, 2024 5:11 am, edited 2 times in total.
User avatar
TakuikaNinja
Posts: 140
Joined: Mon Jan 09, 2023 6:42 pm
Location: New Zealand
Contact:

Re: Dynamic Background Loading Problems

Post by TakuikaNinja »

I haven't looked at your code in detail yet, but you need to ensure that bulk transfers like this cannot be interfered with by interrupt handlers (NMI, IRQ). Writing a full screen's worth of tiles can easily take longer than a single PPU frame, hence the need to account for them.
The interrupt handlers must save the registers (and any shared scratch ram variables) at the start, and restore them before returning.

I believe most sensible games do bulk transfers in the main loop without setting an "NMI ready" flag, so that the NMI handler can detect and skip PPU accesses which could interfere with them (i.e. treated the same as a lag frame). So the general form is something like this:
  1. Buffer the PPUMASK write to disable rendering during the next NMI/vblank. Also buffer any palette writes here to prevent them from being visible.
  2. Set the NMI ready flag and wait for the NMI to return.
  3. Do the bulk transfer without setting the NMI ready flag. The NMI handler must be able to skip PPU handling when the flag is not set.
  4. Buffer the PPUMASK write to enable rendering during the next NMI/vblank once it's ready to be displayed.
  5. Resume normal game logic.
Fiskbit
Site Admin
Posts: 1050
Joined: Sat Nov 18, 2017 9:15 pm

Re: Dynamic Background Loading Problems

Post by Fiskbit »

I've noticed these two issues:
- You're modifying SourceAddr as part of drawing a column, but you assume it's unmodified in the outer loop and try just incrementing to the next address. This causes you to load the wrong data. You need to reinitialize it each time and then add the column index.
- You only initialize Row once. Every time you finish a row, it's left at $1E, and so you do $100 iterations on the next pass instead of $1E iterations. This causes you to clobber VRAM.
EpicFlan
Posts: 2
Joined: Tue Oct 29, 2024 4:34 pm

Re: Dynamic Background Loading Problems

Post by EpicFlan »

TakuikaNinja wrote: Tue Oct 29, 2024 6:03 pm I haven't looked at your code in detail yet, but you need to ensure that bulk transfers like this cannot be interfered with by interrupt handlers (NMI, IRQ). Writing a full screen's worth of tiles can easily take longer than a single PPU frame, hence the need to account for them.
The interrupt handlers must save the registers (and any shared scratch ram variables) at the start, and restore them before returning.

I believe most sensible games do bulk transfers in the main loop without setting an "NMI ready" flag, so that the NMI handler can detect and skip PPU accesses which could interfere with them (i.e. treated the same as a lag frame). So the general form is something like this:
  1. Buffer the PPUMASK write to disable rendering during the next NMI/vblank. Also buffer any palette writes here to prevent them from being visible.
  2. Set the NMI ready flag and wait for the NMI to return.
  3. Do the bulk transfer without setting the NMI ready flag. The NMI handler must be able to skip PPU handling when the flag is not set.
  4. Buffer the PPUMASK write to enable rendering during the next NMI/vblank once it's ready to be displayed.
  5. Resume normal game logic.
Thanks Ninja, this is useful advice. I have attempted to handle this in my code thus far, but maybe I'm not accounting for everything regarding this. My code currently doesn't set the NMI ready flag until all initial Palette and Background Tile loading is done, at which point the PPU_CTRL and PPU_MASK registers are appropriately set. I am also setting the PPU_MASK to 0 when the NMI starts to prevent rendering, and then resetting the PPU_CTRL and PPU_MASK back to normal after any PPU_DATA is set. I'm not turning off the NMI flag in PPU_CTRL when NMI starts though; I added that in, along with prodding the PUU_STATUS just before, and will see if that also adds a bit more stability to the loop.
Fiskbit wrote: Tue Oct 29, 2024 6:56 pm I've noticed these two issues:
- You're modifying SourceAddr as part of drawing a column, but you assume it's unmodified in the outer loop and try just incrementing to the next address. This causes you to load the wrong data. You need to reinitialize it each time and then add the column index.
- You only initialize Row once. Every time you finish a row, it's left at $1E, and so you do $100 iterations on the next pass instead of $1E iterations. This causes you to clobber VRAM.
This is really helpful Fiskbit.

That makes sense, good spot. This was handled in a part of the code I left out (didn't think it was relevant, I've added it in to the original post now). There's a SetColumnAddresses procedure which runs each NMI, and setting the SourceAddress pointer was part of this logic, and since only 1 column was drawn during NMI this only needed to be set once. Of course during initialization, when a new column was looped to, the SourceAddress hi-byte remained at what it had last been incremented to. I've moved the reset logic for that hi-byte into the initialization loop now; the lo-byte can remain outside as that's tied to the Y-Register which gets reset each loop anyway.

And about the row, again, good spot. The SetColumnAddresses procedure was also handling this, resetting the row to 0 if there's no new column to be added. So of course it was being reset during NMI, but then this wasn't being accounted for during initial loading. I've moved this row reset step into the DrawNewColumn procedure now.

Changing these two things (along with some off-by-one issues which appeared) has fixed the issue, so the dynamic loading now appears to work flawlessly with the background initialization!
Post Reply