Skip to content
RetrogamingDeep Dive Published Updated 10 min readViews unavailable

Nintendo 64 VI Scanout: VBlank, Scaling, and Display-Stage Filters

Separate N64 RDP rendering from VI scanout: follow framebuffer handoffs, field timing, scaling, anti-alias reconstruction, gamma, divot, and dither filters.

The Nintendo 64 Video Interface (VI) is not just a cable from the RDP framebuffer to a display. It generates television timing, selects and reads a framebuffer, scales the image, applies display-stage processing, and reports the current display position. Those responsibilities make the VI a separate graphics stage with its own state and timing. A game can render correct pixels into RDRAM and still show a cropped, flickering, filtered, or stale image because the VI is reading a different origin, mode, field, or buffer than expected.

The Nintendo 64 Programming Manual describes the split explicitly: the RDP renders to a framebuffer that is not currently being displayed; after RDP completion, software gives that address to the VI Manager; the new display address becomes active at the next vertical blank (VBlank). This is a handoff protocol, not an instantaneous host-side texture swap. Model the rendered image, the next requested framebuffer, the currently scanned framebuffer, and field timing as distinguishable state.

The VI owns signal timing and scanout state

The VI creates output timing for a television system, defines framebuffer size and color depth, transfers framebuffer data toward the video DAC, applies configured filters, and exposes the current display position. The SDK’s VI Manager coordinates updates and can notify the application at VBlank. Mode changes, scaling changes, and framebuffer changes are therefore scheduled state transitions; writing a setting does not mean that the visible signal changes during the same scanline.

The N64 supports television-format families including NTSC, PAL, and MPAL. Nintendo’s manual gives their scan timing as distinct systems: NTSC is nominally 525 lines at 60 Hz, PAL 625 lines at 50 Hz, and MPAL 525 lines at 60 Hz. The output mode also combines resolution, anti-aliasing or point sampling, interlacing, and pixel size. Common low-resolution modes use a 320-by-240 image, while high-resolution modes use 640-by-480; full-screen PAL has a 320-by-288 low-resolution mode. Do not derive the display mode from framebuffer width alone: the VI timing registers and mode flags determine how memory is sampled and turned into a signal.

VI_CURRENT-style position is also not necessarily a simple whole-line count. The programming manual describes osViGetCurrentLine() as returning the current half-line number sampled at each line. In interlaced modes, the field identity matters as well: osViGetCurrentField() reports the field being scanned, while non-interlaced output returns zero. A debugger that logs only “line 120” can miss whether it is in the odd or even field or whether a timing update is still pending.

Buffer handoff is synchronized to VBlank

Double buffering avoids changing memory while it is being scanned. The application renders into a non-displayed image, waits for RDP completion, queues that image’s address through osViSwapBuffer(), and allows the VI Manager to apply it at VBlank. The programming reference distinguishes the requested next framebuffer from the current framebuffer: the next address remains the queued value, while the current address changes when the following VBlank arrives.

That distinction gives an emulator several observable states:

State Meaning Useful evidence
RDP target Address receiving current drawing work RDP task and completion status
VI next origin Address queued for a later display update Most recent swap request
VI current origin Address being scanned now Active VI state after VBlank
Field and line Position within the current output interval VI current-position registers

If the new scene appears one frame late, first ask whether the RDP completed before the swap was queued and whether VBlank occurred after that request. Do not “fix” the symptom by swapping a host texture immediately if the game depends on the VI’s field boundary. A deterministic emulator should advance to the next active transition according to emulated time and expose the same current/next distinction to software.

The VI Manager’s message event is useful for game scheduling, but it does not mean every game updates one complete image at every video frame. Software can update the framebuffer less often, use separate draw and display buffers, or select interlaced output where two fields contribute to a full frame. Keep the RDP completion event, VBlank event, and framebuffer-origin activation separate in logs and save states.

Interlace changes what one “frame” contains

In NTSC interlace, the manual describes a nominal full frame as about 1/30 second, made from alternating odd and even fields displayed at approximately 1/60-second intervals. Low-resolution software often selects non-interlaced output and uses one field, while high-resolution operation uses interlacing. Deflickered high-resolution interlace averages lines and requires data from both fields; Nintendo cautions that this mode needs multiple-buffer processing and can be sensitive to memory-bank contention.

This makes a single “present” call an insufficient model for all modes. The machine can be midway through one field while software has queued a new buffer for a future boundary. A screenshot captured at one field can differ from a screenshot captured at the next even or odd field even when the RDP did not render a new scene. Tests should record both field parity and buffer origin, especially when comparing emulator output to an original interlaced capture.

For deflickered interlace, layout also matters beyond dimensions. Nintendo’s manual warns that display-time access patterns can compete with other traffic to the same RDRAM bank and cause noise. It recommends separating relevant frame buffers and the Z buffer into different banks and avoiding audio-DMA buffers in the same bank as frame buffers for that use case. Treat this as a memory scheduling and placement constraint in hardware software, not as a color-filter bug.

Scaling and sampling are part of display correctness

The VI’s horizontal and vertical scale settings map framebuffer coordinates to the active output. Low internal resolution scaled to a larger signal is not equivalent to rendering directly at the output resolution. The VI also has display-start and sync ranges that define the visible window. Wrong scale values or start ranges can make an image look vertically compressed, horizontally shifted, stretched, or cropped even when the RDP framebuffer dump is correct.

Keep three sizes in diagnostics: the RDP framebuffer’s allocated pitch, its logical dimensions, and the VI’s output-active dimensions. A framebuffer can have padding between rows, and the VI needs the correct width and origin to find the next row. A host renderer that uses logical width as both allocation stride and visible width may hide errors in one test image, then show diagonal shearing or repeated lines with a different layout.

Point sampling and anti-alias display modes are not merely cosmetic labels. Nintendo describes N64 anti-aliasing as a two-stage operation: the RDP preprocesses and stores coverage-related results, and the VI performs post-processing during scanout. Enabling only the display-stage half does not create a valid anti-aliased result, and enabling only RDP processing leaves the intended display effect incomplete. A faithful emulator should preserve the information needed across the framebuffer boundary rather than applying a generic blur to the final host image.

For a regression test, render a triangle edge crossing a few pixels, dump the color/coverage-related RDP result, then capture the VI output with anti-aliasing enabled and disabled. Hold the RDP commands and framebuffer constant while changing only the VI mode. That isolates display reconstruction from rasterization and prevents a final screenshot comparison from incorrectly attributing a VI difference to the RDP.

Gamma, divot, and dither filters are independent decisions

The VI exposes full-screen processing features including gamma correction, gamma dithering, divot correction, and a dither filter. Nintendo documents gamma correction and gamma dithering as on by default, divot as on by default, and the dither-filter default as dependent on pixel size (on for 16-bit and off for 32-bit). These defaults matter when reproducing a title or comparing images generated with different mode setup paths.

Gamma correction adjusts the display luminance curve. If artwork or an emulator has already applied a similar correction, leaving the VI’s default enabled can make the result look washed out or too bright. The right diagnosis is to record both the source pixel values and the VI gamma state, not to lower every game’s palette or apply an arbitrary host color grade.

Divot correction targets one-pixel holes that can occur where anti-aliased polygon boundaries overlap. The manual recommends leaving it on when anti-aliasing is in use, while also noting that it cannot repair every hole. The dither filter smooths a rough-looking pattern produced by the RDP’s dithering, particularly in 16-bit output. It can cause additional memory accesses and has a performance cost in some cases. It should not be confused with the RDP’s color-dither command: the two settings are at different stages and their combination determines the visible effect.

One easy-to-miss setup detail is that Nintendo documents osViSetMode() as resetting the special VI feature flags to their defaults. A program that toggles gamma or divot and then changes video mode must set its intended special-feature combination again after the mode change. In an emulator, mode activation should update all specified default state together at the documented boundary rather than retaining stale filter bits from an unrelated prior mode.

A reliable validation sequence

Validate the VI with a fixed framebuffer before debugging game geometry. Fill it with a known pattern: horizontal color bars, numbered scanlines, alternating field markers, and a small checkerboard at the image boundary. Then vary one VI parameter at a time and record the output signal or emulator capture. Useful tests include:

  • Queue a new framebuffer origin just before and just after VBlank; verify exactly when it becomes current.
  • Delay RDP completion and confirm that software does not display an unfinished back buffer.
  • Change video mode, scaling, and special-feature flags; confirm that the values activate at the specified boundary and mode defaults are restored as documented.
  • Compare 16-bit and 32-bit framebuffer output with dither-filter defaults explicitly recorded.
  • Compare RDP anti-alias state and VI anti-alias mode as a matrix; do not test either stage in isolation and infer the combined result.
  • Capture both odd and even interlaced fields, including a deflickered mode that draws from multiple fields.
  • Change framebuffer stride and origin independently from logical dimensions to expose row-addressing mistakes.
  • Save and restore while a new framebuffer is queued but before VBlank, then verify next origin, current origin, field, line, and feature flags.

Instrument display-mode bits, region clock, horizontal and vertical sync/start values, logical framebuffer width, origin, scale, current field/half-line, pending swap, active origin, and filter flags. Include RDP completion time in the same trace. When the image is wrong, these fields distinguish a stale buffer, an unsynchronized mode switch, a bad row pitch, a filter mismatch, and an RDP rendering defect.

The display is a pipeline stage, not a screenshot filter

The VI’s output is the result of timed framebuffer reads and configured processing, not a copy of the latest RDP target with a television shader applied afterward. Its field timing, VBlank boundary, scale, current origin, and display filters are machine-visible state. Keep those operations in the emulated device model and let the host renderer present the resulting emulated signal without replacing the underlying contract.

When an N64 image appears soft, shifted, noisy, or a field behind, compare the raw RDP buffer and the VI’s active configuration. This makes the VI a debuggable subsystem: frame handoff can be traced to a specific VBlank, an interlace mismatch to a field, and a filter artifact to a known feature bit. That separation is essential for accurate captures and faithful behavior across software that uses more than the simplest 320-by-240 progressive path.

Related:

Sources:

Comments