ANESE/roms/tests/ppu/nmi_sync
2017-10-21 17:33:02 -07:00
..
demo_ntsc.nes add ppu docs, add more test roms 2017-10-21 17:33:02 -07:00
demo_ntsc.s add ppu docs, add more test roms 2017-10-21 17:33:02 -07:00
demo_pal.nes add ppu docs, add more test roms 2017-10-21 17:33:02 -07:00
demo_pal.s add ppu docs, add more test roms 2017-10-21 17:33:02 -07:00
nmi_sync.s add ppu docs, add more test roms 2017-10-21 17:33:02 -07:00
readme.txt add ppu docs, add more test roms 2017-10-21 17:33:02 -07:00
unrom.cfg add ppu docs, add more test roms 2017-10-21 17:33:02 -07:00

NES Precise NMI Synchronization
-------------------------------
This library allows synchronizing exactly to the PPU from within a
normal NMI handler. It allows PPU writes from within an NMI handler of
the same precision that is otherwise only possible using completely
cycle-timed code. It supports NTSC and PAL.

The code is written for the ca65 assembler. Other assemblers will
require minor changes.

For more about how the technique works, see
http://wiki.nesdev.com/w/index.php/Consistent_frame_synchronization .


Demos
-----
NTSC and PAL demos are included. These show minimal use of this library
to manually draw a line using timed writes. They manually draw a line by
setting bit 0 of $2001 to enable monochrome mode. The time of the write
determines the position on screen, so any synchronization problems will
cause the line's left side to move. Reference lines are shown above and
below the manually-drawn one, showing the correct left edge position.

On the NTSC version, the left pixel of the middle line will be darker,
since it's flashing:
	
	********************
	                 ***
	-*******************
	                 ***
	********************

On the PAL version, the left pixel or two will be darker, since it's
flashing. The left edge's general position will change randomly each
time you press reset. The upper line shows the farthest left it can ever
be after reset, and the lower line shows the farthest right it can be.
It may appear as one of the following:

	********************
	                 ***
	-*******************
	                 ***
	  ******************


	********************
	                 ***
	 -******************
	                 ***
	  ******************


	********************
	                 ***
	  -*****************
	                 ***
	  ******************


Usage
-----
To use this library:

* Include "nmi_sync.s".

* Call init_nmi_sync[_pal] before synchronization is needed, then wait
in a loop that calls wait_nmi, does anything that is necessary between
NMIs, then loops back.

* Inside NMI, call begin_nmi_sync, do sprite DMA, delay appropriately,
then call end_nmi_sync. If running on NTSC NES, sprite and/or background
rendering MUST be enabled before calling end_nmi_sync. It can be
disabled again after it returns.

* On frames where synchronization isn't needed, but will be needed a few
frames later, call track_nmi_sync. If it won't be needed for a long
time, nothing needs to be done, and init_nmi_sync can be called again
later to re-synchronize and start over.

After end_nmi_sync returns, the next instruction will be synchronized to
2286 (NTSC)/7471 (PAL) cycles after the frame began.

If the NMI handler's timing is off by even one cycle, synchronization
will fail sometimes. To verify timing, write an odd value to $2001 after
synchronization. The point where monochrome mode begins on the scanline
should be very stable. If it ever jiggles, then something is wrong in
your code.

The following demonstrates:

	.include "nmi_sync.s"
	
	reset:
		...
		jsr init_nmi_sync/init_nmi_sync_pal
		
	loop:
		jsr wait_nmi
		...anything done outside of NMI...
		jmp loop
	
	nmi:
		...save registers...
		jsr begin_nmi_sync  ; count as 6 cycles
		...
		
		; Instructions between nmi: and STA $4014 must take an even
		; number of cycles. STA $4014 must be done as a part of
		; synchronization.
		sta $4014           ; count as 4 cycles
		...
		
		; Instructions between nmi: and here must take
		; 1715 (NTSC)/6900 (PAL) cycles.
		
		delay 1715 - ...    ; NTSC
		delay 6900 - ...    ; PAL
		
		; On NTSC, sprite and/or background rendering MUST be
		; enabled at this point, or else synchronization will
		; be lost.
		
		jsr end_nmi_sync
		
		; Sprite and background rendering can be disabled again
		; at this point, if it's not needed.
		
		; Next instruction is now synchronized to exactly
		; 2286 (NTSC)/7471 (PAL) cycles after cycle that
		; frame began in.
		...
		
		...restore registers...
		rti


NTSC Timing
-----------
Given the following NMI handler

	nmi:
		...
		jsr end_nmi_sync
		delay N cycles
		lda #$01
		sta $2001   ; writes 2286+N+5 cycles into frame

The $2001 write will be 2286+N+5 cycles after frame began. To have the
$2001 write at a particular pixel, calculate N with

	pixel = y * 341 + x
	N = (pixel + 290) / 3

For example, to write at y=121 x=80, N should be 13877.

The pixel position can be calculated from N with

	pixel = N * 3 - 290
	y = pixel / 341
	x = pixel - (y * 341)

where y=0 x=0 is the top-left pixel. For example, if the delay is 13877,
then the $2001 write will occur at y=121 x=80.

After init_nmi_sync is called, the first, third, fifth, etc. frames have
the above timing. On the second, fourth, sixth, etc. frames, the write
is one pixel LATER (x=81 in the example). This one-pixel jitter is an
unavoidable hardware limitation.

The above applies to enabling monochrome mode by setting bit 0 of $2001;
other registers take effect at slightly different times. For some
registers, the pixel written can vary slightly after pressing reset.
It's best to use the above as a guide, reduce delay until glitches occur
due to it occurring too early, increase delay until glitches occur as
well, then choose a delay in the middle of those two extremes.
 

PAL Timing
----------
Given the following NMI handler

	nmi:
		...
		jsr end_nmi_sync
		delay N cycles
		lda #$01
		sta $2001   ; writes 7471+N+5 cycles into frame

The $2001 write will be 7471+N+5 cycles after frame began. To have the
$2001 write at a particular pixel, calculate N with

	pixel = y * 341 + x
	N = (pixel * 5 + 1444) / 16

For example, to write at y=121 x=82, N should be 13009.

The pixel position can be calculated from N with

	pixel = (N * 16 - 1444 + extra) / 5
	y = pixel / 341
	x = pixel - (y * 341)

where y=0 x=0 is the top-left pixel, and extra is an additional delay
that depends on whether it's an even or odd frame, and also a random
offset selected when reset is pressed. For example, if the delay is
13009, then the $2001 write will occur no earlier than y=121 x=81.

After init_nmi_sync is called, the first, third, fifth, etc. frames have
the above timing. On the second, fourth, sixth, etc. frames, extra is 8
greater, causing the write to be one or two pixels LATER (x=83 or 84 in
the example). This jitter is an unavoidable hardware limitation.

After pressing reset, extra is set to a random value from 0 to 7,
causing writes to be one or two pixels later. This doesn't change until
reset is pressed. This is also unavoidable.

The above applies to enabling monochrome mode by setting bit 0 of $2001;
other registers take effect at slightly different times. For some
registers, the pixel written can vary slightly after pressing reset.
It's best to use the above as a guide, reduce delay until glitches occur
due to it occurring too early, increase delay until glitches occur as
well, then choose a delay in the middle of those two extremes.


Limitations
-----------
* DMC samples can't be played, since they introduce too much timing
variation. A normal NMI performs just as well/poorly in this case.

* If NMI occurs while executing an instruction that takes more than
three cycles, synchronization will be upset for that frame. Note that a
taken branch counts as more than three cycles, due to an obscure detail.
To avoid this, call wait_nmi each frame, or sit in a loop of
instructions two/three-cycle instructions. I have found some workarounds
that allow NMI to occur during almost any instruction, but they require
some extra helper sprites and use of the sprite overflow flag; contact
me for details.

* Every frame from that point, NMI must either call begin/end_nmi_sync,
or track_nmi_sync, or else synchronization will be lost and
init_nmi_sync will need to be called again.

* The NMI handler must not read $2002 until after end_nmi_sync has been
called.

* After synchronizing, NMI and rendering must be enabled by the next
frame, and left enabled (rendering can be disabled on PAL since it
doesn't affect PPU timing). If rendering isn't desired, it can be
enabled just before calling end_nmi_sync, then disabled afterwards.

* Sprite DMA must be done on frames needing synchronization, even if no
sprites are being used.

* Even when perfectly synchronized, frames don't always begin exactly on
a cycle. On NTSC, a given cycle will toggle between two adjacent pixels.
On PAL, it will toggle between the calculated pixel and one or two
pixels after. These effects are hardware limitations; this library
synchronizes as precisely as is possible in software.


Thanks
------
* Bregalad for his initial questions that inspired the idea, and for
trying an early version.


-- 
Shay Green <gblargg@gmail.com>