NMI breaks screen output when I move processing to main [solved]

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
Soph
Posts: 9
Joined: Mon May 30, 2022 1:40 am

NMI breaks screen output when I move processing to main [solved]

Post by Soph »

Good day!

I'm trying to separate my game logic from NMI to main. Right now all my game logic is in NMI, but as I don't want to be limited to ~1500cy I want to set a flag in NMI, do NMI-only stuff there and then check in main if the flag is set for further processing.

Important thing: if I do like this (aka Everything-In-NMI), everything in this configuration works fine and smooth:

Code: Select all

.proc main
    forever:
        jmp forever
        
     rts
.endproc

Code: Select all

.proc handler_nmi
    php
    pha
    txa
    pha
    tya
    pha

    lda #nmi_flag::set ; <--- I set up the flag here
    sta zp_nmi_flag
    
    nmi_oam_map
 
    nmi_loader_jsr ; <--- This is the game logic stuff

    nmi_ppuctrl_set
    nmi_ppumask_set
    nmi_screen_scroll

    pla
    tay
    pla
    tax
    pla
    plp

    rti
.endproc
During animation:
Image

But, as I mentioned, I want to do the game logic in main like adult people do:

Code: Select all

.proc main
    forever:
        jsr _vblank_process
        ; My other stuff that will be run in non-vblank time (nothing here yet)
        jmp forever
.endproc

Code: Select all

.proc _vblank_process
    wait: ; <--- I sync to the nearest VBLANK looping here
        lda zp_nmi_flag
        cmp #nmi_flag::set
        bne wait

    nmi_loader_jsr ; <<---- My game logic is now here, should be run in VBLANK
    
    lda #nmi_flag::unset
    sta zp_nmi_flag
    
     rts
.endproc
During animation:
Image

But when I do stuff like this I have severe graphical glitches during my gameplay, like if I smashed ppu-pointers, or write wrong ppu-addresses.
What have I done wrong? That's literally complete main at the moment, I can't see what can go wrong - I backup the registers, status byte, never use anything that is related to PPU in main but still have a lot of glitches in the second case. My NMI is very short and doesn't oversize the VBLANK time (futhermore the NMI-only case works fine).

Thank you in advance.
Last edited by Soph on Mon May 30, 2022 7:00 am, edited 1 time in total.
Fiskbit
Posts: 891
Joined: Sat Nov 18, 2017 9:15 pm

Re: NMI breaks screen output when I move processing to main

Post by Fiskbit »

Normally, the structure is that your NMI does all of the PPU-related tasks, and the code outside of NMI never touches the PPU except in circumstances when rendering is off, like when loading a new screen. You say you "never use anything that is related to PPU in main", but I'd guess nmi_loader_jsr is writing to the PPU (you have a comment saying you expect it to run during vblank). The problem is that writing data into the PPU requires setting a PPU address via $2006, which uses the same internal PPU registers as the scroll registers you update during NMI with nmi_ppuctrl_set and nmi_screen_scroll. That means that if you do VRAM access after setting the scroll, the internal registers will be in the wrong state when the PPU starts drawing. It looks in your image like the background is simply scrolled to the wrong location. The scroll needs to be the last thing you set when finishing with the PPU in vblank.

In your old NMI handler, where you have nmi_loader_jsr, that's where you should have a function that transfers data to the PPU. Normally, games will have some kind of transfer buffer format where you have a buffer that specifies a 16-bit VRAM address, a number of bytes to write, potentially flags like row vs column, and the data to write (and then more transfers can follow this in the same format until a terminator is reached). Games then queue up transfers by writing a pointer or pointer table index into a variable, which points at the RAM buffer or some static transfer sequence in ROM. When the NMI comes along, it'll safely do it during vblank, and you can even use this for larger transfers if you disable rendering first.

Also, you mention that NMI limits you to 1500 cycles, but this isn't accurate. NMI itself is simply a signal that vblank has begun. vblank is short (about 2400 cycles, with maybe about 2265 left by the time code in the NMI starts running), so you do things that require vblank first, but you can keep running code in NMI after that without issue. There are plenty of games, such as Super Mario Bros and Zelda, which run all of their code in the NMI, disabling NMI at the start of the handler so lag frames won't cause another NMI to run, and enabling it again when the frame is done being handled. Running the game logic outside of NMI is superior, though, because you can handle lag more gracefully (keeping NMI enabled and running the sound engine in NMI so music stays full speed) and because writing to $2000 to turn the NMI back on can cause brief graphical glitches if it happens at the wrong time during rendering.

Some other thoughts: You'll want to avoid OAM DMA and PPU transfers on lag frames, because the buffers may be in an incomplete state. You also don't need to PHP and PLP in an interrupt handler; interrupts automatically push P and RTI automatically pulls it.
Soph
Posts: 9
Joined: Mon May 30, 2022 1:40 am

Re: NMI breaks screen output when I move processing to main

Post by Soph »

You say you "never use anything that is related to PPU in main", but I'd guess nmi_loader_jsr is writing to the PPU (you have a comment saying you expect it to run during vblank)
You're absolutely correct and I now understood my mistake. I'm very grateful to you for your answer and for your support.
That means that if you do VRAM access after setting the scroll, the internal registers will be in the wrong state when the PPU starts drawing. It looks in your image like the background is simply scrolled to the wrong location. The scroll needs to be the last thing you set when finishing with the PPU in vblank.
That's very vital information. Thank you.
Normally, games will have some kind of transfer buffer format where you have a buffer that specifies a 16-bit VRAM address, a number of bytes to write, potentially flags like row vs column, and the data to write
I have read about that but thought that this technique is only used for sprites/tiles rewriting. Now it's crystal clear for me, thank you again.
about 2400 cycles, with maybe about 2265 left by the time code in the NMI starts running
Yes, I took this number from memory and was ~800 cycles wrong
You also don't need to PHP and PLP in an interrupt handler; interrupts automatically push P and RTI automatically pulls it.
Thank you. I never found any information about that at nesdev.org but probably I was careless.
Fiskbit
Posts: 891
Joined: Sat Nov 18, 2017 9:15 pm

Re: NMI breaks screen output when I move processing to main

Post by Fiskbit »

Glad you got it working!
Soph wrote: Mon May 30, 2022 6:59 am
about 2400 cycles, with maybe about 2265 left by the time code in the NMI starts running
Yes, I took this number from memory and was ~800 cycles wrong
My point here is less about the exact timing, but more that you don't actually gain time by moving your code out of NMI (you had mentioned earlier that you didn't want "to be limited to ~1500cy "). The vblank timing restrictions are the same whether you're running inside or outside of NMI, and you have the same ~30,000 cycle budget before the next vblank in which to do whatever you want, regardless of whether your code is in the NMI.
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: NMI breaks screen output when I move processing to main [solved]

Post by tokumaru »

He's probably talking about the program structure used in the Nerdy Nights tutorials, where the "game logic" runs in the NMI handler *before* the PPU updates. It causes problems to a lot of newbies once they try to do anything more complex than moving a handful of sprites, because the time spent on the game logic + PPU updates exceeds the vblank period.

Putting the game logic in the main thread is indeed one way to solve this problem, but it's not the only one. You can stick to using the NMI handler for everything, but you have to do the PPU updates first, and then the game logic - but don't forget to account for reentrant NMIs. What I don't recommend is putting everything in the main thread (with the NMI handler acting as a barebones frame counter), because that setup cannot handle lag frames properly.
Soph
Posts: 9
Joined: Mon May 30, 2022 1:40 am

Re: NMI breaks screen output when I move processing to main [solved]

Post by Soph »

Nerdy Nights tutorials
I have no idea who or what's Nerdy Nights. I have used famicom.party to kickstart and continue with ca65.org and nesdev.org manuals. I have ~10 years experience in C and asm so I only need some tech specs and caveats (like what have been mentioned before about "scroll should be the last one").

I'm not going to put everything in main, I only want to expand the calculation time, because with the methods of level storage I use (like 2x2 compressed metablocks) I need a lot of cycles to "unzip" it.
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: NMI breaks screen output when I move processing to main [solved]

Post by tokumaru »

Soph wrote: Tue May 31, 2022 3:04 amI have no idea who or what's Nerdy Nights. I have used famicom.party to kickstart and continue with ca65.org and nesdev.org manuals.
And I have no idea who or what famicom.party is, but since you said you didn't want to be limited to ~1500 CPU cycles, I assumed you were using the same naive program structure that a very popular series of NES programming tutorials called "Nerdy Nights" uses, which puts the entire program in the NMI handler, game logic first.
I'm not going to put everything in main
I know, I was just pointing out that there are other possible solutions, but I don't recommend this one in particular.
Post Reply