Proper way of waiting for vblank without NMI
Moderator: Moderators
Proper way of waiting for vblank without NMI
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?
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?
Re: Proper way of waiting for vblank without NMI
No. Vertical blanking is good enough. Access to the nametables (and everything else via $2007) is achievable during vertical or forced blanking.halkun wrote:I have to wait until VBLANK, turn off the display, update the nametable, and turn the PPU back on.
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.
Re: Proper way of waiting for vblank without NMI
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
- rainwarrior
- Posts: 8732
- Joined: Sun Jan 22, 2012 12:03 pm
- Location: Canada
- Contact:
Re: Proper way of waiting for vblank without NMI
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.
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.
Re: Proper way of waiting for vblank without NMI
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.
Re: Proper way of waiting for vblank without NMI
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.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?
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.
Re: Proper way of waiting for vblank without NMI
Somewhat like so, but I think there are 4 bugs in therehalkun 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
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).
Re: Proper way of waiting for vblank without NMI
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
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?
FECUX doen't seem to want to reset the scroll position
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
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
Re: Proper way of waiting for vblank without NMI
Mesen can use a .dbg file generated using the --dbgfile option.
Re: Proper way of waiting for vblank without NMI
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
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
Re: Proper way of waiting for vblank without NMI
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).
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).
Re: Proper way of waiting for vblank without NMI
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.halkun wrote:So I should set the nametable address every time? does it get corrupted when the PPC renders a frame?
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.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
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.the NMI works in NO$NES, but not in FCEUX. Emulators can be tricky I guess.
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.FECUX doen't seem to want to reset the scroll position
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.
Re: Proper way of waiting for vblank without NMI
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..
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
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
....
"import completed with 0 labels imported"
I feel like I'm not creating the debug file right
- rainwarrior
- Posts: 8732
- Joined: Sun Jan 22, 2012 12:03 pm
- Location: Canada
- Contact:
Re: Proper way of waiting for vblank without NMI
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.
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.
Re: Proper way of waiting for vblank without NMI
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):
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:
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.:
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.
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
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
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
Last edited by koitsu on Thu Feb 07, 2019 12:43 am, edited 1 time in total.