Video Interface

The Video Interface (or VI) is one of multiple I/O interfaces in the RCP, which configures different parts of the console's video rendering and output. It provides significant flexibility to support NTSC, PAL, and M-PAL, using the same chips and registers. All memory-mapped registers are 32 bits wide and should always be written a full word (32 bits) at a time.

= Video DAC = The Video DAC uses a 7 bit multiplexed data bus that receives the video signal from the RCP. This allows the N64 to output a 21 bit color output even though internally the N64 can store 24 bits. Why this lower bit was not used is not explained in any documentation found at this moment.

The Video DAC clock runs at 4 times the speed of the internal pixel clock so the multiplexing can happen on the VI bus. This 4 clock process outputs the RGB colors and the VSync, Hsync, Clamp and Csync and is reset using a dsync reset signal. The video clock is provided by one of the two MX8330s (IC U7) There are 3 different video clocks that are used in the N64 for the 3 TV standards:

= Video Standards = The video standards send more than just pixel data both on each line and with additional lines at the top and bottom of a frame.

= Hardware = Most of the Video Interface is implemented inside the RCP (Reality CoProcessor), although there is a Video DAC (Digital Analog Converter) on the mainboard, and another encoder IC (ENC-NUS) which which appears to manage some of the signal differences between Composite and S-Video Output.

= Configuration Registers = Memory mapped registers are used to configure the Video Interface. The base address for these registers is, also known as VI_BASE. However, because all memory accesses in the CPU are made using virtual addresses, the following addresses must be offset appropriately. For non-cached reads/writes, add  to the address. As an example, to directly write to the VI_CTRL register, use address.

Table Notation: R = Readable bit W = Writable bit U = Undefined/Unused bit -n = Default value n at power on  = Specifies bits x to y, inclusively

0x0440 0000 - VI_CTRL

 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0


 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || RW-0
 * — || — || — || — || — || — || — || style="font-size: 70%;" | DITHER_FILTER_ENABLE
 * — || — || — || — || — || — || — || style="font-size: 70%;" | DITHER_FILTER_ENABLE


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || U-0 || RW-0 || RW-0
 * colspan="4" | PIXEL_ADVANCE<3:0> || KILL_WE || — || colspan="2" | AA_MODE<1:0>
 * colspan="4" | PIXEL_ADVANCE<3:0> || KILL_WE || — || colspan="2" | AA_MODE<1:0>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * TEST_MODE || SERRATE || style="font-size: 70%;" | VBUS_CLOCK_ENABLE || DIVOT_ENABLE || GAMMA_ENABLE || style="font-size: 70%;" | GAMMA_DITHER_ENABLE || colspan="2" | TYPE<1:0>
 * TEST_MODE || SERRATE || style="font-size: 70%;" | VBUS_CLOCK_ENABLE || DIVOT_ENABLE || GAMMA_ENABLE || style="font-size: 70%;" | GAMMA_DITHER_ENABLE || colspan="2" | TYPE<1:0>

Extra Details:
 * DEDITHER_FILTER_ENABLE
 * When enabled, the VI will run a de-dithering algorithm, trying to reverse the effects of dithering on each pixel to produce an higher resolution color information on the analog output. This is useful when the framebuffer is 16-bit and has been dithered while drawing. To do so, VI looks at the 8 neighbors around each pixel and perform an error correction; the algorithm used works best with images that have been dithered using the "Magic Square" dithering algorithm (that the RDP can be configured to do). The VI does de-dedithering onky on pixels where coverage is full; on pixels with partial coverage, the standard AA algorithm is performed.
 * DIVOT_ENABLE
 * When enabled, this feature fixes artifacts that the anti-aliasing algorithm leaves behind. The median color of three neighboring pixels, from any pixels on or next to silhouette edges, is selected to be displayed in place of the center pixel. Effectively removing any one pixel divots that can be seen in some fractal-based terrains. The anti-aliasing function encounters issues when multiple fragments occur on a single pixel. Since this filter is only used on edges, and not the surface of an object, texture details will not be affected. Be aware that bad quality effects can occur when the decal line rendering mode is used in conjunction with this filter, as the rendering mode generates edges that the filter can detect.
 * GAMMA_ENABLE
 * This feature is used to correct non-linear gamma found in TV screens (although this may have changed in modern TV's). To do this, the feature square roots the linear color space that the rendering pipeline uses. TV screens will raise these color values to the power of 2.2 to 2.4, which leaves a residual gamma behind of around 1.1 to 1.2. This residual value is actually preferred as a gamma slightly above 1.0 will generate more color accurate images when the TV is in darker than normal rooms. When using MPEG or JPG images, the gamma correction is included in the image data, so this feature should be turned off accordingly.

0x0440 0004 - VI_ORIGIN

 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | ORIGIN<23:16>
 * colspan="8" | ORIGIN<23:16>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | ORIGIN<15:8>
 * colspan="8" | ORIGIN<15:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | ORIGIN<7:0>
 * colspan="8" | ORIGIN<7:0>

0x0440 0008 - VI_WIDTH

 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0


 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0


 * U-0 || U-0 || U-0 || U-0 || RW-0 || RW-0 || RW-0 || RW-0
 * — || — || — || — || colspan="4" | WIDTH<11:8>
 * — || — || — || — || colspan="4" | WIDTH<11:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | WIDTH<7:0>
 * colspan="8" | WIDTH<7:0>

0x0440 000C - VI_V_INTR

 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0


 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0


 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || RW-1 || RW-1
 * — || — || — || — || — || — || colspan="2" | V_INTR<9:8>
 * — || — || — || — || — || — || colspan="2" | V_INTR<9:8>


 * RW-1 || RW-1 || RW-1 || RW-1 || RW-1 || RW-1 || RW-1 || RW-1
 * colspan="8" | V_INTR<7:0>
 * colspan="8" | V_INTR<7:0>

0x0440 0010 - VI_V_CURRENT

 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0


 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0


 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || RW-0 || RW-0
 * — || — || — || — || — || — || style="font-size: 88%;" colspan="2" | V_CURRENT<9:8>
 * — || — || — || — || — || — || style="font-size: 88%;" colspan="2" | V_CURRENT<9:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | V_CURRENT<7:0>
 * colspan="8" | V_CURRENT<7:0>

0x0440 0014 - VI_BURST

 * U-0 || U-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * — || — || colspan="6" | BURST_START<9:4>
 * — || — || colspan="6" | BURST_START<9:4>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="4" | BURST_START<3:0> || colspan="4" | VSYNC_WIDTH<3:0>
 * colspan="4" | BURST_START<3:0> || colspan="4" | VSYNC_WIDTH<3:0>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | BURST_WIDTH<7:0>
 * colspan="8" | BURST_WIDTH<7:0>


 * RW-1 || RW-1 || RW-0 || RW-1 || RW-0 || RW-0 || RW-0 || RW-1
 * colspan="8" | HSYNC_WIDTH<7:0>
 * colspan="8" | HSYNC_WIDTH<7:0>

Examples:
 * NTSC @ any resolution is
 * horizontal sync width in pixels: 57 (decimal)
 * color burst width in pixels: 34 (decimal)
 * vertical sync width in half lines: 5 (decimal)
 * start of color burst in pixels from h-sync: 62 (decimal)
 * PAL @ any resolution is
 * horizontal sync width in pixels: 58 (decimal)
 * color burst width in pixels: 35 (decimal)
 * vertical sync width in half lines: 4 (decimal)
 * start of color burst in pixels from h-sync: 64 (decimal)

0x0440 0018 - VI_V_SYNC

 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0


 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0


 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || RW-0 || RW-0
 * — || — || — || — || — || — || colspan="2" | V_SYNC<9:8>
 * — || — || — || — || — || — || colspan="2" | V_SYNC<9:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | V_SYNC<7:0>
 * colspan="8" | V_SYNC<7:0>

0x0440 001C - VI_H_SYNC

 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0


 * U-0 || U-0 || U-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * — || — || — || colspan="5" | LEAP<4:0>
 * — || — || — || colspan="5" | LEAP<4:0>


 * U-0 || U-0 || U-0 || U-0 || RW-1 || RW-1 || RW-1 || RW-1
 * — || — || — || — || colspan="4" | H_SYNC<11:8>
 * — || — || — || — || colspan="4" | H_SYNC<11:8>


 * RW-1 || RW-1 || RW-1 || RW-1 || RW-1 || RW-1 || RW-1 || RW-1
 * colspan="8" | H_SYNC<7:0>
 * colspan="8" | H_SYNC<7:0>

0x0440 0020 - VI_H_SYNC_LEAP

 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || RW-0 || RW-0
 * — || — || — || — || — || — || colspan="2" | LEAP_A<9:8>
 * — || — || — || — || — || — || colspan="2" | LEAP_A<9:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | LEAP_A<7:0>
 * colspan="8" | LEAP_A<7:0>


 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || RW-0 || RW-0
 * — || — || — || — || — || — || colspan="2" | LEAP_B<9:8>
 * — || — || — || — || — || — || colspan="2" | LEAP_B<9:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | LEAP_B<7:0>
 * colspan="8" | LEAP_B<7:0>

0x0440 0024 - VI_H_VIDEO

 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || RW-0 || RW-0
 * — || — || — || — || — || — || colspan="2" | H_START<9:8>
 * — || — || — || — || — || — || colspan="2" | H_START<9:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | H_START<7:0>
 * colspan="8" | H_START<7:0>


 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || RW-0 || RW-0
 * — || — || — || — || — || — || colspan="2" | H_END<9:8>
 * — || — || — || — || — || — || colspan="2" | H_END<9:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | H_END<7:0>
 * colspan="8" | H_END<7:0>

0x0440 0028 - VI_V_VIDEO

 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || RW-0 || RW-0
 * — || — || — || — || — || — || colspan="2" | V_START<9:8>
 * — || — || — || — || — || — || colspan="2" | V_START<9:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | V_START<7:0>
 * colspan="8" | V_START<7:0>


 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || RW-0 || RW-0
 * — || — || — || — || — || — || colspan="2" | V_END<9:8>
 * — || — || — || — || — || — || colspan="2" | V_END<9:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | V_END<7:0>
 * colspan="8" | V_END<7:0>

0x0440 002C - VI_V_BURST

 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || RW-0 || RW-0
 * — || — || — || — || — || — || style="font-size: 90%;" colspan="2" | V_BURST_START<9:8>
 * — || — || — || — || — || — || style="font-size: 90%;" colspan="2" | V_BURST_START<9:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | V_BURST_START<7:0>
 * colspan="8" | V_BURST_START<7:0>


 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || RW-0 || RW-0
 * — || — || — || — || — || — || style="font-size: 90%;" colspan="2" | V_BURST_END<9:8>
 * — || — || — || — || — || — || style="font-size: 90%;" colspan="2" | V_BURST_END<9:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | V_BURST_END<7:0>
 * colspan="8" | V_BURST_END<7:0>

0x0440 0030 - VI_X_SCALE

 * U-0 || U-0 || U-0 || U-0 || RW-0 || RW-0 || RW-0 || RW-0
 * — || — || — || — || colspan="4" | X_OFFSET<11:8>
 * — || — || — || — || colspan="4" | X_OFFSET<11:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | X_OFFSET<7:0>
 * colspan="8" | X_OFFSET<7:0>


 * U-0 || U-0 || U-0 || U-0 || RW-0 || RW-0 || RW-0 || RW-0
 * — || — || — || — || colspan="4" | X_SCALE<11:8>
 * — || — || — || — || colspan="4" | X_SCALE<11:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | X_SCALE<7:0>
 * colspan="8" | X_SCALE<7:0>

0x0440 0034 - VI_Y_SCALE

 * U-0 || U-0 || U-0 || U-0 || RW-0 || RW-0 || RW-0 || RW-0
 * — || — || — || — || colspan="4" | Y_OFFSET<11:8>
 * — || — || — || — || colspan="4" | Y_OFFSET<11:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | Y_OFFSET<7:0>
 * colspan="8" | Y_OFFSET<7:0>


 * U-0 || U-0 || U-0 || U-0 || RW-0 || RW-0 || RW-0 || RW-0
 * — || — || — || — || colspan="4" | Y_SCALE<11:8>
 * — || — || — || — || colspan="4" | Y_SCALE<11:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | Y_SCALE<7:0>
 * colspan="8" | Y_SCALE<7:0>

0x0440 0038 - VI_TEST_ADDR

 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0


 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0


 * U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0 || U-0


 * U-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * — || colspan="7" | TEST_ADDR<6:0>
 * — || colspan="7" | TEST_ADDR<6:0>

0x0440 003C - VI_STAGED_DATA

 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | STAGED_DATA<31:24>
 * colspan="8" | STAGED_DATA<31:24>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | STAGED_DATA<23:16>
 * colspan="8" | STAGED_DATA<23:16>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | STAGED_DATA<15:8>
 * colspan="8" | STAGED_DATA<15:8>


 * RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0 || RW-0
 * colspan="8" | STAGED_DATA<7:0>
 * colspan="8" | STAGED_DATA<7:0>

= How to use this information =

Interlace Mode
The NTSC (and PAL) standard support interlace mode which is commonly associated with high resolution, but it can be used for more than that.


 * High Resolution Mode
 * Improve the visible detail of the image
 * 60 frames per second in low resolution

High Resolution Mode
High resolution mode supports up to 480 lines (NTSC) while low resolution is 240 lines (NTSC). The Image doesn't magically grow or shrink because first the even lines are drawn on the screen, then it goes back to the top and draws the odd lines. If your game only draws the even lines then on a larger display you may have the image scanlines with smaller black lines visible between them.

In order to implement this feature it requires the VI_V_START_REG to be modified on every VI Interrupt, so that it outputs even lines then odd lines as needed.

NTSC Alternates between: 0x002301fd and 0x002501ff

PAL Alternates between: 0x005f0239 and 0x005d0237

Once this is explained I believe it will be fixed soon, so this is explained as an example of what the difference can be. The libdragon homebrew library doesn't actually support High Resolution Mode, because it doesn't implement this register value change. To be fair this is very easy to overlook, it works fine in every emulator and would at least look OK on a console. The difference is that emulators present the framebuffer memory as a single block of data. While the VI Interface and Video DAC see the 1 framebuffer as even lines top to bottom, then odd lines top to bottom.

Learnt from Factor 5 games(Mazamars312):

The VI_DRAM_ADDR_REG address is set to the odd or even line of the framebuffer and the VI_H_WIDTH_REG value is doubled to help skip to the next Odd or Even field line for the VI core to process.

Once the odd or even field has been displayed the VI_DRAM_ADDR_REG is updated to the other field's address. Also the VI_Y_SCALE_REG.Subpixel is changed between fields with the values 12'h0100 and 12'h0200 to help the scaling and AA calculations (Need to find out which one is Odd and Even based as this could be game based)

The real width and height values are calculated by the following calculations

Width: C programming(float) ((VI_H_START_REG.END - VI_H_START_REG.START) * (VI_X_SCALE_REG.ScaleUp / 1024))

Height: C programming(float) (((VI_H_START_REG.END - VI_H_START_REG.START) >> 1) * (VI_X_SCALE_REG.ScaleUp / 1024))

60 Frames per second in Low Resolution mode
This is the easiest mode to use if your frame processing time is very low, because you simply swap the frame buffer 60 times per second inside the VI Interrupt, no other register changes are needed.

Letter Boxing
This is a fairly common effect that is nice for cut scenes or to indicate overworld vs a level.

VI_V_START_REG

Pillar Boxing
This feature is almost the default now since the N64 is intended for a 4:3 screen but is commonly played on 16:9 ratio screens.

VI_H_START_REG

Reduce both Height and Width
Reducing the display size by just a few pixels also reduces the size of the world view that the player has, while usually improving performance. Especially if the purpose of this is to improve performance I recommend doing it in increments of 8 pixels, for example either 4 or 8 pixels off each side and my increasing the size of the player status bars at either the top or bottom of the screen can also reduce the number of objects to draw on the screen.

Use the same techniques mentioned above for Letter Boxing and Pillar Boxing.

Advanced version of this is to reduce either the height or width and to increase the scaling so it still fits the screen but stretches the image out to fill the screen.