Skip to content
Techniques & Technology

VSync

Synchronising with the display

VSync synchronised game updates with the monitor's vertical refresh, preventing screen tearing and providing consistent timing for game logic.

commodore-64commodore-amiganintendo-entertainment-systematari-2600 graphicstimingtechnique 1977–present

Overview

The CRT electron beam draws the screen line by line, then returns to the top during the vertical blank (VBlank). Updating screen memory while the beam is drawing causes tearing — the upper part of the picture shows the previous frame and the lower part the new one, with a moving seam where they meet. VSync waits for VBlank before updating, so the entire frame's drawing is invisible until it's complete.

The problem

Without synchronisation:
  Beam at line 100 — actively drawing
  Game updates screen memory
  Beam continues drawing the lower half — sees new data
  Result: top half shows old frame, bottom half shows new
  → horizontal tear line at line 100

The solution

Wait for VBlank — the period when the beam is between frames and not reading screen memory:

; C64: rough idiom — wait for raster to enter the upper region
wait_vsync:
    lda $d011                ; bit 7 = high bit of 9-bit raster counter
    bpl wait_vsync           ; loop while bit 7 clear (raster < 256)
    ; raster is now ≥ 256, mostly in vblank on PAL

This is a polling pattern. Most games install a raster IRQ at line 0 and do their VBlank work in the handler — no busy-waiting.

CRT timing

PhaseDescriptionBeam state
Active displayVisible scanlines drawing left to rightReading screen memory
Horizontal blankBeam returns to left edge between scanlinesBrief; ~10 µs
Vertical blankBeam returns to top after last visible lineLonger; non-display

The CPU may safely modify screen memory only during VBlank (or, with care, during borders or behind a status-bar split).

Platform implementations

Commodore 64

; Wait for raster to cross 256 (enter upper border / vblank region)
wait_vblank:
    lda $d011
    bpl wait_vblank          ; loop while raster < 256
    ; bit 7 is now set; safe to update upper-screen related state

For a precise vblank-start hook, install a raster IRQ at line 0:

    sei
    lda #<irq_handler
    sta $0314
    lda #>irq_handler
    sta $0315
    lda #0
    sta $d012                ; trigger at line 0 (vblank start)
    lda $d011
    and #$7f                 ; bit 7 (raster MSB) = 0 → line < 256
    sta $d011
    lda #$01
    sta $d01a                ; enable raster IRQ
    cli

NES

NMI fires at the start of VBlank automatically (if PPUCTRL bit 7 is set). The conventional pattern is to set a flag in the NMI handler and have the main loop wait on it:

nmi_flag:    .byte 0

nmi_handler:
    inc nmi_flag             ; mark frame as ready
    rti

wait_nmi:
    lda nmi_flag
    beq wait_nmi             ; wait until NMI fires
    lda #0
    sta nmi_flag
    rts

VBlank on NES is short — about 2270 CPU cycles (≈1.4 ms). Heavy work has to be deferred to the active frame.

Amiga

The OS-friendly approach is a Level 3 interrupt handler (VBlank) installed via the system. For games that take over the hardware, set up the Copper to fire a COPPERIRQ at the desired raster position, or hook INT3 (vertical blank) directly:

; Set up a vertical-blank interrupt
    move.w  #$4020,$dff09a   ; INTENA: enable INTEN | VERTB
    move.l  #vbl_handler,$6c ; install Level 3 interrupt vector

Atari 2600

The 2600's TIA has no framebuffer — every scanline is generated by the CPU as it runs. A full frame is a precise sequence: 3 scanlines of vertical sync, then 37 lines of vertical blank, then 192 (NTSC) or 228 (PAL) visible lines, then 30 lines of overscan. The kernel runs in lockstep with the beam:

; --- Vertical sync (3 scanlines) ---
    lda  #2
    sta  VSYNC               ; bit 1 set = vsync active
    sta  WSYNC               ; line 1 of sync (CPU halts to end of line)
    sta  WSYNC               ; line 2 of sync
    sta  WSYNC               ; line 3 of sync
    lda  #0
    sta  VSYNC               ; vsync off

; --- Vertical blank (37 scanlines) ---
    lda  #43
    sta  TIM64T              ; timer ticks every 64 cycles → ~37 lines
    lda  #2
    sta  VBLANK              ; blank video output during vblank

    ; ... game logic runs here, in the time before the visible frame ...

vbl_wait:
    lda  INTIM
    bne  vbl_wait            ; wait for timer to hit 0
    sta  WSYNC               ; align to start of next line
    lda  #0
    sta  VBLANK              ; turn video back on for visible frame

; --- 192 visible scanlines: kernel draws each line ---

The 2600 is the extreme case — every game frame is a hand-timed CPU sequence synchronised to the beam.

VBlank duration

Approximate available time for VBlank work:

SystemVBlank lines (approx)TimeNotes
C64 PAL56 lines (raster 256-311 + line 0)~3.6 ms"Vblank" loosely; visible image ends ~line 250
C64 NTSC13 lines (raster 251-262 + line 0)~0.8 msMuch tighter than PAL
Amiga PAL25 vsync + ~24 blanking~3.1 msOCS / ECS, lores
NES NTSC20 vblank + 1 pre-render = 21~1.33 msAbout 2273 CPU cycles
NES PAL70 lines~4.45 msSignificantly more headroom than NTSC
Atari 2600 NTSC3 vsync + 37 vblank + 30 overscan = 70~4.4 msAll non-display time, of which ~37 lines is the conventional vblank

PAL gives roughly 3× more vblank than NTSC on most systems — a reason PAL ports of NTSC games sometimes ran "loose" (didn't fill the extra time) while NTSC versions of PAL games sometimes dropped frames.

What to do during VBlank

TaskPriorityWhy
Pointer / display register updatesHighestMust complete before active display starts
Sprite position writes (NES OAM DMA, Amiga sprite registers)HighestLate writes corrupt mid-frame
Scroll register updatesHighLocked to frame boundary
Palette / colour register updatesHighMid-frame changes are raster-effect territory
Music driver tickMediumOften interrupt-driven independently
Game logic, AILowestCan run during active display

Game loop structure

main_loop:
    jsr  wait_vsync          ; sync to display
    jsr  update_screen       ; safe to modify video registers/memory
    jsr  game_logic          ; can take any time, even spilling into next frame
    jmp  main_loop

If game_logic regularly takes longer than one frame, the game runs at half rate (sync-locked at every other vblank). True frame skipping is a modern technique; classic games typically just budget logic to fit a frame.

Frame rate locking

VSync naturally creates:

  • 50 fps (PAL)
  • 60 fps (NTSC)
  • Consistent timing, identical from boot to boot
  • Predictable game speed — the reason NTSC and PAL versions of the same game often play at different speeds

Tearing vs performance

ChoiceResult
Always VSyncNo tearing, may skip frames if logic is slow
Never VSyncTearing, maximum throughput
Adaptive (modern)VSync when frame finishes in time, fall through if not — irrelevant on retro hardware where every frame is hand-budgeted

See also