Proper way of waiting for vblank without NMI

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems. See the NESdev wiki for more information.

Moderator: Moderators

halkun
Posts: 45
Joined: Mon Jul 18, 2011 10:04 pm

Proper way of waiting for vblank without NMI

Post by halkun »

As an offshoot of my Z-Machine idea, I'm writing terminal routines for the NES. From what I gather, I have to wait until VBLANK, turn off the display, update the nametable, and turn the PPU back on. I've noticed that most are using a NMI to update the screen, however, with text, I have the luxury of updating asynchronously.

They way the terminal works is that when a character needs to be printed, it's pushed to the stack and the output routine is called.

Here is how I will get it to work in theory....
In my output routine I'll loop and check bit 7 of PPU status. Once that goes high, I know I'm in vblank and I have to turn off the background and sprites using PPUMASK (bits 3 and 4)
Then I pop my character off the stack, and use PPUDATA to update the nametable and it will automatically advance to the next position
Then I turn on the background and sprites again and it should be good to go for the next frame, right?
lidnariq
Posts: 11432
Joined: Sun Apr 13, 2008 11:12 am

Re: Proper way of waiting for vblank without NMI

Post by lidnariq »

halkun wrote:I have to wait until VBLANK, turn off the display, update the nametable, and turn the PPU back on.
No. Vertical blanking is good enough. Access to the nametables (and everything else via $2007) is achievable during vertical or forced blanking.

If you don't use forced blanking, you have to be careful to never exceed the amount of timing available to you during vertical blanking.

If you do use forced blanking, you need to be careful about when you re-enable rendering to avoid incorrect scroll.
halkun
Posts: 45
Joined: Mon Jul 18, 2011 10:04 pm

Re: Proper way of waiting for vblank without NMI

Post by halkun »

so I don't need to turn anything off, just wait for vblank and then push right into the nametable?

Code: Select all

.proc nesout
	;check to see if NES is in vblank
	lda PPUSTATUS
	and %1000000
	beq nesout
	;pop the character off the stack
	pla
	;write it to vram
	sta PPUDATA
	rts
.endproc

User avatar
rainwarrior
Posts: 8732
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: Proper way of waiting for vblank without NMI

Post by rainwarrior »

One thing with polling $2002 for vblank is if the write falls directly on the start of vblank it "cancels" it and returns 0, so you end up waiting an extra frame. So it's unfortunately not 100% reliable, though it's perfectly usable as long as it's acceptable to occasionally wait an extra frame.

That's kind of an unavoidable random behaviour, though you can reduce the impact of it if you make the polling loop longer. The more cycles you have in your polling loop, the less chance you have that the one cycle that reads $2002 will fall on the exact start of vblank and do a cancel.
lidnariq
Posts: 11432
Joined: Sun Apr 13, 2008 11:12 am

Re: Proper way of waiting for vblank without NMI

Post by lidnariq »

halkun wrote:so I don't need to turn anything off, just wait for vblank and then push right into the nametable?

Code: Select all

[/quote] Yes, but that code will end up only sending one byte every vblank. You really do want some kind of queuing system.
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Proper way of waiting for vblank without NMI

Post by tokumaru »

halkun wrote:In my output routine I'll loop and check bit 7 of PPU status. Once that goes high, I know I'm in vblank and I have to turn off the background and sprites using PPUMASK (bits 3 and 4)
Then I pop my character off the stack, and use PPUDATA to update the nametable and it will automatically advance to the next position
Then I turn on the background and sprites again and it should be good to go for the next frame, right?
Any reason why you can't flush the buffer this way but in the NMI handler, instead of polling $2002? Like it's been pointed out, reading $2002 in a loop will cause some vblanks to be missed, which will affect music playback, animations, raster effects, and anything else that's timed off of vblank.

A very sensible approach would be to buffer characters whenever they're typed, and in the NMI handler you flush the buffer to VRAM whenever there's anything in it, while also updating the screen position. One thing you'd have to be careful about though is that there are two steps in buffering a character: 1 - writing the character to the buffer; 2 - updating the buffer's length. If the NMI happens to fire between these two steps, you might have problems. If the the length is updated first, and the NMI fires before the character is written to the buffer, a garbage character will be written to the screen. If the character is buffered first, and the NMI fires before the length is updated, two things might happen: if the length reads back as 0, the character won't be output this time but will be on the next vblank, which's fine; however, if there was already data in the buffer, that data will be output while the new character won't, and when the NMI returns, the new length will be set, and that will cause the characters that have already been output to be output again on the next vblank, along with the newest character and whatever may get buffered this frame.

A very simple way to solve this problem would be to zero out the length of the buffer before updating it (so that if the NMI fires it doesn't try to output anything), write the character to the buffer, and then set the new length. Most NES games have to do something along these lines, because an NMI might fire while the various buffers (background, sprites, etc.) are still being filled, and we definitely don't want partial updates to be sent to the PPU, we want to update only when the whole data is ready.
nocash
Posts: 1405
Joined: Fri Feb 24, 2012 12:09 pm
Contact:

Re: Proper way of waiting for vblank without NMI

Post by nocash »

halkun wrote:so I don't need to turn anything off, just wait for vblank and then push right into the nametable?

Code: Select all

.proc nesout
	;check to see if NES is in vblank
	lda PPUSTATUS
	and %1000000
	beq nesout
	;pop the character off the stack
	pla
	;write it to vram
	sta PPUDATA
	rts
.endproc
Somewhat like so, but I think there are 4 bugs in there
1. Your assembler will probably expect "#nn" for immediates to distinguish from "nn" address
2. The immediate used is testing bit6, not bit7
3. Pla and rts both read from stack, but not in the order that you seem to expect
4. You need to set the ppu address before writing ppu data, especially for the 1st byte after begin of vblank

The other issue is that vblank flag in bit gets cleared after reading ppustatus, that's why the function could output only one char per frame.
I would probably queue the outgoing data in a ring buffer, and then forward that data to vram in nmi handler, the advantage is that your main program won't need to pause until vblank.
Best way for ring buffers should be using separate write- and read-pointers. Write pointer getting incremented by the main program upon char output (unless the pointer would become equal, which would that ringbuffer is full). And read pointer getting incremented by the nmi handler when forwarding the chars to vram (until/unless the pointers are same, which would mean that the ringbuffer is empty).
halkun
Posts: 45
Joined: Mon Jul 18, 2011 10:04 pm

Re: Proper way of waiting for vblank without NMI

Post by halkun »

Yea, I'm going to implement the NMI routine. Good catch on my missing # though :)
So I should set the nametable address every time? does it get corrupted when the PPC renders a frame? Also, I can't seem to count to 8 for some reason :P
Oh snap! JSR pushs on the stack too! I spaced that. I'll alter the code so that it's saved to the zero page instead

I'm porting something cool, but may look useless useless at first, but has lots of potential
I don't want to say until I have something that runs, but so far looks promising.

Also, is there a debugger that can take symbols from an ca65 object file? searching the ROM is kind of a pain.

==EDIT==
the NMI works in NO$NES, but not in FCEUX. Emulators can be tricky I guess.
I borrowed code from the SNROM template for PPU. Foes this seem correct?

Code: Select all

.proc initnes
                  ;Stack was initialized before this call 
  sei
  cld            ; disable decimal mode to help generic 6502 debuggers 
  
  ; Acknowledge and disable interrupt sources during bootup
  ldx #0
  stx PPUCTRL    ; disable vblank NMI
  stx PPUMASK    ; disable rendering (and rendering-triggered mapper IRQ)
  lda #$40
  sta $4017      ; disable frame IRQ
  stx $4010      ; disable DPCM IRQ
  bit PPUSTATUS  ; ack vblank NMI
  bit $4015      ; ack DPCM IRQ

  ; Wait for the PPU to warm up (part 1 of 2)
vwait1:
  bit PPUSTATUS
  bpl vwait1

  ; While waiting for the PPU to finish warming up, we have about
  ; 29000 cycles to burn without touching the PPU.  So we have time
  ; to initialize some of RAM to known values.
  ; Ordinarily the "new game" initializes everything that the game
  ; itself needs, so we'll just do zero page and shadow OAM.
  ldy #$00
  lda #$F0
  ldx #$00
clear_zp:
  sty $00,x
  inx
  bne clear_zp
  ; the most basic sound engine possible
  lda #$0F
  sta $4015

  ; Wait for the PPU to warm up (part 2 of 2)
vwait2:
  bit PPUSTATUS
  bpl vwait2

  ; Initalize the screen
  jsr initscreen

  ; Turn screen on
  lda #0
  sta PPUSCROLL
  sta PPUSCROLL
  lda #VBLANK_NMI|BG_1000
  sta PPUCTRL
  lda #BG_ON
  sta PPUMASK

  rts

.endproc


.proc cls
  lda #VBLANK_NMI
  sta PPUCTRL
  lda #$20
  ldx #$00
  stx PPUMASK
  sta PPUADDR
  stx PPUADDR
  ldx #240
:
  sta PPUDATA
  sta PPUDATA
  sta PPUDATA
  sta PPUDATA
  dex
  bne :-
  ldx #64
  lda #0
:
  sta PPUDATA
  dex
  bne :-
  
  lda #$20
  sta PPUADDR
  lda #$62
  sta PPUADDR
  
  rts
.endproc

.proc initscreen
  jsr cls

  ; set monochrome palette
  lda #$3F
  sta PPUADDR
  lda #$00
  sta PPUADDR
  ldx #8
:
  lda #$02    <-- Palette
  sta PPUDATA
  lda #$30
  sta PPUDATA
  sta PPUDATA
  sta PPUDATA
  dex
  bne :-
  rts
.endproc


.proc nmi
	pha                  ;we destroy accumulator so save it first 
	lda #0              ;locking the screen to upper left
	sta PPUSCROLL
	sta PPUSCROLL
	lda #$20
	sta PPUADDR
	lda #$62
	sta PPUADDR
	lda #$40             ;'@'
	sta PPUDATA
	pla
	rti
.endproc

FECUX doen't seem to want to reset the scroll position
Image
tepples
Posts: 22708
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Re: Proper way of waiting for vblank without NMI

Post by tepples »

Mesen can use a .dbg file generated using the --dbgfile option.
halkun
Posts: 45
Joined: Mon Jul 18, 2011 10:04 pm

Re: Proper way of waiting for vblank without NMI

Post by halkun »

I'm trying to make a debug file but it's not allowing me to...
Here is the command I'm using
cl65 -t none -C nes.ini -m map.txt -vm -o rom.nes main.s header.s

When I add a -g it says it doesn't know what to do with rom.nes

==EDIT==
Fixed scrolling problem Now works in FCEUX, N$NES, and Mensen :)
User avatar
koitsu
Posts: 4201
Joined: Sun Sep 19, 2004 9:28 pm
Location: A world gone mad

Re: Proper way of waiting for vblank without NMI

Post by koitsu »

You didn't provide the actual command you used, just that you "added -g somewhere". Argument order matters, i.e. cl65 ... -g -o rom.nes ... is not the same thing as cl65 ... -o -g rom.nes .... I have no idea what you actually did so I can't tell you what's likely wrong. :-) --debug-info is also the same as -g if you prefer long/descriptive arguments.

Takeaway: Never ever append or insert flags/switches into a command-line if you aren't sure where they should go. Always refer to the usage documentation for what argument syntax is on any command-line program you use (on Windows, *IX systems, anything); -h or --help (a lot of things) or -? (some unique *IX programs) or /? (DOS/Windows) often display usage, but it varies per program; sometimes running a program with no arguments will show usage, but other programs might just sit there doing nothing (attempting to read from standard input).

In this case, this is the usage documentation, and this too which goes over nuances of argument order in cl65 / how the argument parser works (for the latter doc: read, don't skim).
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Proper way of waiting for vblank without NMI

Post by tokumaru »

halkun wrote:So I should set the nametable address every time? does it get corrupted when the PPC renders a frame?
The PPU's address register is used internally for rendering, so yeah, you need to set the output address every time, as well as reset the scroll before rendering begins, every frame.
Oh snap! JSR pushs on the stack too! I spaced that. I'll alter the code so that it's saved to the zero page instead
You can buffer things on the stack for later consumption, but since the CPU is always using the stack when calling subroutines or interrupt handlers, this normally requires you to manipulate the stack pointer, something that must be done with caution. Placing your buffers elsewhere is a better call if you don't absolutely need the speed/convenience of PLA/STA.
the NMI works in NO$NES, but not in FCEUX. Emulators can be tricky I guess.
Emulators are not tricky for common stuff, but they can be when you're trying more advanced stuff that relies on perfect timing. If you're trying to use the console in a "standard" way, with no funny tricks, it should work all across the board without any trouble. If that's not the case, there's probably something wrong with the way you're structuring your code.
FECUX doen't seem to want to reset the scroll position
Setting the scroll must be the last thing you do before trying starts. Like I said before, the PPU address registers is used by the programmer to update VRAM, but also by the PPU itself in order to render the picture.

In your code, you're setting the scroll (which prepares the PPU address register for rendering), and then clobbering it by using $2006 and $2007 to write to VRAM.
halkun
Posts: 45
Joined: Mon Jul 18, 2011 10:04 pm

Re: Proper way of waiting for vblank without NMI

Post by halkun »

Yup: I fixed it up after I realized the PPU messes with it's own registers

I got cl65 dump a debug file, but sadly, Mesen doesn't like it very much

here's my final command line
cl65 -t none -C nes.ini -m map.txt -vm -o rom.nes -g main.s header.s -d -Ln rom.dbg
it generated a file and it's contents looks like this..

Code: Select all

al 000300 .__BSS_LOAD__
al 000300 .__BSS_RUN__
al 000000 .__BSS_SIZE__
al 00E8D3 .nesin
al 00E8D2 .nesout
al 00E8AF .initscreen
al 00E877 .cls
al 00E85C .vwait2
al 00E852 .clear_zp
al 00E847 .vwait1
al 00E82F .initnes
....
When I load it in mesen I get an error
"import completed with 0 labels imported"
I feel like I'm not creating the debug file right
User avatar
rainwarrior
Posts: 8732
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: Proper way of waiting for vblank without NMI

Post by rainwarrior »

The option to generate the DBG file is --dbgfile. You've used -Ln which is not equivalent. (It's a VICE label file, which has a similar purpose, but it's not what Mesen uses.)

Also you should not use -d, I think that's for debugging the compiler itself, not your program, and probably slows down the process or adds unnecessary output. Only -g is important for what you want.

What -g does is generate more debug symbols to go in the DBG file (and label file). Without -g, by default it doesn't keep track of much that it doesn't have to and these files will be fairly empty.
User avatar
koitsu
Posts: 4201
Joined: Sun Sep 19, 2004 9:28 pm
Location: A world gone mad

Re: Proper way of waiting for vblank without NMI

Post by koitsu »

Yeah, it's sort of what I suspected, re: argument order is probably an issue. I mirror rainwarrior's sentiments. So, documentation references once more:

cl65 (compiler) flags: https://www.cc65.org/doc/cl65-2.html
cl65 (compiler) argument order: https://www.cc65.org/doc/cl65-3.html
ld65 (linker) flags: https://www.cc65.org/doc/ld65-2.html#ss2.1

And tips/hints:

1. You don't want -Ln {filename}, that generates a VICE label file, which is not what Mesen understands,
2. You don't want -d, which is "debug mode" for the compiler itself; not a common-used flag unless I think you're running into issues with cl65 behaviour itself,
3. --dbgfile {filename} is a linker flag, thus only available to ld65. You aren't calling ld65 natively, which means you need to use the -Wl {options} or --ld-args {options} flag of cl65 to pass arguments to ld65 (which its calling behind the scenes) (see below),
4. Strongly recommend putting your source filenames at the end of your command-line and stop appending arguments out of (I strongly suspect) laziness. See the "argument order" doc above for why.

In summary, your command should really be this (I have not tested this, I'm going purely off of documentation):

Code: Select all

cl65 -t none -C nes.ini -m map.txt -vm -o rom.nes -g -Wl --dbgfile rom.dbg main.s header.s
If this doesn't work and ends up passing all the remaining arguments/files to the linker (I'm doubting this will happen; just be aware if you need to give more options to the linker, you separate them with commas), then #4 above is invalid and you should try this instead:

Code: Select all

cl65 -t none -C nes.ini -m map.txt -vm -o rom.nes -g main.s header.s -Wl --dbgfile rom.dbg
Also note that the l of -Wl is a lowercase-ELL, not a one.

Alternately, you can try using cl65 on each source file separately with -c and then calling ld65 yourself to link together all the objects, e.g.:

Code: Select all

cl65 -t none -g -c -o main.o main.s
cl65 -t none -g -c -o header.o header.s
ld65 -t none -C nes.ini -m map.txt -vm -Wl --dbgfile rom.dbg -o rom.nes main.o header.o
The latter is very much how most C software on *IX systems with GNU make work, using a Makefile to call the compiler repeatedly on several source files (example from BSD make, not GNU make), but make is a whole other complicated topic (and requires other software/binaries) that I don't want to get into.
Last edited by koitsu on Thu Feb 07, 2019 12:43 am, edited 1 time in total.
Post Reply