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)
Cycle 1 | Cycle 2 | Cycle 3 | Cycle 4 | |
---|---|---|---|---|
D0 | Red 0 | Green 0 | Blue 0 | !Csync |
D1 | Red 1 | Green 1 | Blue 1 | !Hsync |
D2 | Red 2 | Green 2 | Blue 2 | !Clamp |
D3 | Red 3 | Green 3 | Blue 3 | !Vsync |
D4 | Red 4 | Green 4 | Blue 4 | NA |
D5 | Red 5 | Green 5 | Blue 5 | NA |
D6 | Red 6 | Green 6 | Blue 6 | NA |
DSYNC | HIGH | HIGH | HIGH | Low |
There are 3 different video clocks that are used in the N64 for the 3 TV standards:
-
Mx8330 video clock maths and fselect
TV Signal Type | MX8330 Nominal input clock (by definition) | MX8330 FSEL input | Measured Video Clock | Clock Maths using datasheet |
---|---|---|---|---|
NTSC | 14.32MHz (18 × 227.5 ÷ 286) | HIGH | 48,681,818 Hz | (14.3 * 17) / 5 |
PAL | 17734475 Hz | LOW | 49,656,530 Hz | (17.7 * 14) / 5 |
MPAL | 14.30MHz (18 × 227.25 ÷ 286) | HIGH | 48,628,322 Hz | (14.3 * 17) / 5 |
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 0x0440 0000
, 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 0xA000 0000
to the address. As an example, to directly write to the VI_CTRL register, use address 0xA440 0000
.
Table Notation:
R = Readable bit W = Writable bit U = Undefined/Unused bit -n = Default value n at power on <x:y> = Specifies bits x to y, inclusively
VI_CTRL 0x0440 0000
| ||||||||
---|---|---|---|---|---|---|---|---|
31:24 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 |
— | — | — | — | — | — | — | — | |
23:16 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | RW-0 |
— | — | — | — | — | — | — | DEDITHER_FILTER_ENABLE | |
15:8 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | U-0 | RW-0 | RW-0 |
PIXEL_ADVANCE[3:0] | KILL_WE | — | AA_MODE[1:0] | |||||
7:0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
TEST_MODE | SERRATE | VBUS_CLOCK_ENABLE | DIVOT_ENABLE | GAMMA_ENABLE | GAMMA_DITHER_ENABLE | TYPE[1:0] |
bit 31-17 | Undefined: Initialized to 0
|
bit 16 | DEDITHER_ENABLE: Dedither Enable bit 1 = Dedithering (aka "dither filter") is enabled; normally used for 16-bit framebuffers to try to reconstruct a 32-bit image. Notice that this filter only works correctly when AA_MODE is set to 00 . 0 = Dedithering is disabled (normally used for 32-bit color) |
bit 15-12 | PIXEL_ADVANCE[3:0]: Use 0b0011 for most effective behavior on N64. On the iQue Player a pixel advance of 0b0011 creates video glitches, applications typically use 0b0001 instead.
|
bit 11 | KILL_WE: Diagnostics only, possibly kills VI DMA writes to line buffers making them safe to access via the test registers. |
bit 10 | Undefined: Initialized to 0
|
bit 9-8 | AA_MODE[1:0]: Anti-Alias Mode 11 = AA and resampling disabled, replicate pixels without interpolation 10 = AA disabled, resampling enabled, and operate as if everything is covered 01 = AA enabled, resampling enabled, and only fetches extra lines as needed 00 = AA enabled, resampling enabled, and will always fetch extra lines (required if DEDITHER_ENABLE is 1).
|
bit 7 | TEST_MODE: Diagnostics only, enables usage of the line buffer test registers VI_TEST_ADDR/VI_STAGED_DATA. KILL_WE should also be set to avoid access races between the VI and CPU. |
bit 6 | SERRATE: Required if interlacing, permitted when progressive, often disabled 1 = Enabled 0 = Disabled |
bit 5 | VBUS_CLOCK_ENABLE: Vbus Clock Enable 1 = Enabled 0 = Disabled Warning: Always leave disabled! Setting this bit enables a second driver, which will output on the same pin as another driver, possibly causing physical console damage. |
bit 4 | DIVOT_ENABLE: Fixes minor artifacts left over from anti-aliasing (more details below) 1 = Enabled (usually used if AA is enabled) 0 = Disabled |
bit 3 | GAMMA_ENABLE: Fixes non-linear gamma in TV screens (more details below) 1 = Enabled 0 = Disabled |
bit 2 | GAMMA_DITHER_ENABLE: Adds randomized noise to the video output, in the least significant bits to remove mach banding artifacts 1 = Enabled (usually set unless banding artifacts are desired for extra effect) 0 = Disabled |
bit 1-0 | TYPE[1:0]: Video pixel size, also known as color bit depth 11 = 8/8/8/8 (32 bit color) 10 = 5/5/5/3 (16 bit color, technically 18 bits wide) 01 = reserved 00 = Turned off (no data and no sync, TV screens will either show static or nothing) |
Extra Details:
- DEDITHER_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 only on pixels where coverage is full; on pixels with partial coverage, the standard AA algorithm is performed. NOTE: this filter requires
AA_MODE
to be set to00
, otherwise the image is corrupted by vertical streaks (as seen here).
- 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 only on pixels where coverage is full; on pixels with partial coverage, the standard AA algorithm is performed. NOTE: this filter requires
- 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.
VI_ORIGIN 0x0440 0004
| ||||||||
---|---|---|---|---|---|---|---|---|
31:24 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 |
— | — | — | — | — | — | — | — | |
23:16 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
ORIGIN[23:16] | ||||||||
15:8 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
ORIGIN[15:8] | ||||||||
7:0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
ORIGIN[7:0] |
bit 31-24 | Undefined: Initialized to 0
|
bit 23-0 | ORIGIN[23:0]: RDRAM base address of the video output Frame Buffer. This can be changed as needed to implement double or triple buffering. |
Extra Details:
- ORIGIN must be a multiple of 8 (i.e. ORIGIN[2:0] must be 0). Otherwise the VI output may be noisy, shifted, or weirdly interleaved.
VI_WIDTH 0x0440 0008
| ||||||||
---|---|---|---|---|---|---|---|---|
31:24 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 |
— | — | — | — | — | — | — | — | |
23:16 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 |
— | — | — | — | — | — | — | — | |
15:8 | U-0 | U-0 | U-0 | U-0 | RW-0 | RW-0 | RW-0 | RW-0 |
— | — | — | — | WIDTH[11:8] | ||||
7:0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
WIDTH[7:0] |
bit 31-12 | Undefined: Initialized to 0
|
bit 11-0 | WIDTH[11:0]: This is the width in pixels of the frame buffer if you draw to the frame buffer based on a different width than what is given here the image will drift with each line to the left or right. The common values are 320 and 640, the maximum value is 4095. The same value would also be used on drawing commands for clipping or scissors. This can also be used with High Res interlacing modes to change the odd and even lines of the frame buffer to be drawn to screen by doubling the width of this value and changing the VI_ORIGIN register to the odd or even field being displayed. |
Extra Details:
- WIDTH must be a multiple of 2 (if 32bpp) or 4 (if 16bpp) such that the number of bytes from one scanline to the next is a multiple of 8. The same caveats about VI_ORIGIN apply here, but incorrect display will only happen on some scanlines.
VI_V_INTR 0x0440 000C
| ||||||||
---|---|---|---|---|---|---|---|---|
31:24 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 |
— | — | — | — | — | — | — | — | |
23:16 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 |
— | — | — | — | — | — | — | — | |
15:8 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | RW-1 | RW-1 |
— | — | — | — | — | — | V_INTR[9:8] | ||
7:0 | RW-1 | RW-1 | RW-1 | RW-1 | RW-1 | RW-1 | RW-1 | RW-1 |
V_INTR[7:0] |
bit 31-10 | Undefined: Initialized to 0
|
bit 9-1 | V_INTR[9:0]: When VI_V_CURRENT reaches this half-line number, a VI Interrupt is triggered. Both libultra and libdragon set this to the value 2, which causes an interrupt to be triggered on the second line of vblank. Default value of 0x3FF
|
When VI_CTRL.SERRATE
is 0 (progressive modes), bit 0 of VI_V_INTR
is effectively ignored: the interrupt will trigger when V_INTR[9:1]
matches VI_V_CURRENT[9:1]
, irrespective of the two LSBs.
When VI_CTRL.SERRATE
is 1 (interlaced modes), bit 0 of VI_V_INTR
does have an effect:
- If bit 0 is set to 1, the behavior is the expected one: interrupts happen when
VI_V_INTR[9:1]
matchesVI_V_CURRENT[9:1].
For instance, settingVI_V_INTR=15
can be used to request an interrupt on line 7 of the screen. The interrupt will trigger whenVI_V_CURRENT
is either 14 (line 7, field 0) or 15 (line 7, field 1). - If bit 0 is set to 0, interrupts in odd fields are triggered one line before the actual match. For instance, setting
VI_V_INTR=14
causes an interrupt to generated whenVI_V_CURRENT
is either 14 (line 7, field 0) or 13 (line 6, field 1). This appears to be a hardware bug, probably a side effect on internal timings. An important case to consider for this bug is whenVI_V_INTR
is set 0: in that case, the interrupt will happen on the last line of the previous even field. For instance, in a default PAL configuration with 525 lines,VI_V_INTR=0
will cause an interrupt whenVI_V_CURRENT
is either 0 (line 0, field 0), or 524 (line 262, field 0), though the latter is supposed to be the one to signal the beginning of the odd field. In this case, reading the field number in the interrupt handler would always result in an even field.
When the VI is turned off (VI_CTRL.TYPE
is 0), no VI interrupt is ever generated.
VI_V_CURRENT 0x0440 0010
| ||||||||
---|---|---|---|---|---|---|---|---|
31:24 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 |
— | — | — | — | — | — | — | — | |
23:16 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 |
— | — | — | — | — | — | — | — | |
15:8 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | RW-0 | RW-0 |
— | — | — | — | — | — | V_CURRENT[8:7] | ||
7:0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
V_CURRENT[6:0] | FIELD |
bit 31-10 | Undefined: Initialized to 0
|
bit 9-1 | V_CURRENT: The current half line, sampled once per line. |
bit 0 | V_FIELD: Field number. In interlaced modes, it alternates between 0 or 1 after each frame. In non-interlaced modes, it stays constant (it can be either 0 or 1). |
Writing anything to this register clears the currently triggered VI Interrupt.
When VI_CTRL.SERRATE
is 0 (progressive modes), the V_CURRENT
field counts up from 0 to half of the number of active display lines, rounded up. For instance, in a default PAL configuration of 525 vertical lines (see VI_V_VIDEO
), it will count from 0 to 262, included. FIELD
will normally be 0 in these modes, so the full registers values will be 0x0, 0x2, 0x4, ..., 0x20C. When changing resolutions from interlaced to progressive, sometimes FIELD
stays fixed to 1 instead, in which case the full register values will be 0x1, 0x3, 0x5, ..., 0x20D.
When VI_CTRL.SERRATE
is 1 (interlaced modes), the behavior is similar but the FIELD
value will alternate between 0 and 1 after each run. The even field will be one line longer in case the active vertical display area is odd. So, assuming the same PAL configuration of 525 vertical lines, the full register values will be: 0x0, 0x2, 0x4, ..., 0x20C, 0x1, 0x3, 0x5, ..., 0x20B.
When the VI is turned off (VI_CTRL.TYPE
is 0), this register is fixed to 0, and never changes.
VI_BURST 0x0440 0014
| ||||||||
---|---|---|---|---|---|---|---|---|
31:24 | U-0 | U-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
— | — | BURST_START[9:4] | ||||||
23:16 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
BURST_START[3:0] | VSYNC_WIDTH[3:0] | |||||||
15:8 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
BURST_WIDTH[7:0] | ||||||||
7:0 | RW-1 | RW-1 | RW-0 | RW-1 | RW-0 | RW-0 | RW-0 | RW-1 |
HSYNC_WIDTH[7:0] |
bit 31-30 | Undefined: Initialized to 0
|
bit 29-20 | BURST_START[9:0]: Start of color burst in pixels from hsync |
bit 19-16 | VSYNC_WIDTH[3:0]: One less than the vertical sync duration in half lines |
bit 15-8 | BURST_WIDTH[7:0]: Color burst width in pixels |
bit 7-0 | HSYNC_WIDTH[7:0]: Horizontal sync width in pixels Default value of 0x01
|
Examples:
- NTSC @ any resolution is
0x03E52239
- horizontal sync width in pixels: 57 (decimal)
- color burst width in pixels: 34 (decimal)
- vertical sync width in half lines: 5 (decimal) (and thus 6 half-lines)
- start of color burst in pixels from h-sync: 62 (decimal)
- PAL @ any resolution is
0x0404233A
- horizontal sync width in pixels: 58 (decimal)
- color burst width in pixels: 35 (decimal)
- vertical sync width in half lines: 4 (decimal) (and thus 5 half-lines)
- start of color burst in pixels from h-sync: 64 (decimal)
VI_V_TOTAL[1] 0x0440 0018
| ||||||||
---|---|---|---|---|---|---|---|---|
31:24 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 |
— | — | — | — | — | — | — | — | |
23:16 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 |
— | — | — | — | — | — | — | — | |
15:8 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | RW-0 | RW-0 |
— | — | — | — | — | — | V_TOTAL[9:8] | ||
7:0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
V_TOTAL[7:0] |
bit 31-10 | Undefined: Initialized to 0
|
bit 9-0 | V_TOTAL[9:0]: One less than the total number of visible and non-visible half-lines. This should match either NTSC/MPAL (non-interlaced: 525 , interlaced: 524 ) or PAL (non-interlaced: 625 , interlaced: 624 )
|
VI_H_TOTAL[2] 0x0440 001C
| ||||||||
---|---|---|---|---|---|---|---|---|
31:24 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 |
— | — | — | — | — | — | — | — | |
23:16 | U-0 | U-0 | U-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
— | — | — | LEAP[4:0] | |||||
15:8 | U-0 | U-0 | U-0 | U-0 | RW-1 | RW-1 | RW-1 | RW-1 |
— | — | — | — | H_TOTAL[11:8] | ||||
7:0 | RW-1 | RW-1 | RW-1 | RW-1 | RW-1 | RW-1 | RW-1 | RW-1 |
H_TOTAL[7:0] |
bit 31-21 | Undefined: Initialized to 0
|
bit 20-16 | LEAP[4:0]: 5-bit leap pattern used only for PAL. Should always use standard value of 0x15
|
bit 15-12 | Undefined: Initialized to 0
|
bit 11-0 | H_TOTAL[11:0]: One less than the total length of a scanline in 1/4 pixel units. Should always use standard values: NTSC (3093 ), PAL (3177 ), or MPAL (3090 )Default value of 2047 (0x7FF)
|
Extra Details:
- LEAP chooses whether to use LEAP_A or LEAP_B on each vsync repeating every five vsyncs. The NTSC default (0) means "always use LEAP_A". The PAL default (0x15) means "alternate using LEAP_B, LEAP_A, LEAP_B, LEAP_A, LEAP_B" and repeat.
- Derivation of numbers:
- NTSC has 227.5 chroma periods per scanline. NTSC N64 has 13.6 VI clocks per chroma period. 227.5 × 13.6 = 3094
- MPAL has 227.25 chroma periods per scanline. MPAL N64 has 13.6 VI clocks per chroma period. 227.25 x 13.6 = 3090.6
- PAL (European) has 283.7516 chroma periods per scanline. PAL N64 has 11.2 clocks per chroma period. 283.75 x 11.2 = 3178
- H_TOTAL is also used by the RDRAM Interface for refresh timings. As the default is notably shorter than regular video modes, there will be a noticeable impact to memory bandwidth until H_TOTAL is configured to a valid video mode.
VI_H_TOTAL_LEAP[3] 0x0440 0020
| ||||||||
---|---|---|---|---|---|---|---|---|
31:24 | U-0 | U-0 | U-0 | U-0 | RW-0 | RW-0 | RW-0 | RW-0 |
— | — | — | — | LEAP_A[11:8] | ||||
23:16 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
LEAP_A[7:0] | ||||||||
15:8 | U-0 | U-0 | U-0 | U-0 | RW-0 | RW-0 | RW-0 | RW-0 |
— | — | — | — | LEAP_B[11:8] | ||||
7:0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
LEAP_B[7:0] |
bit 31-28 | Undefined: Initialized to 0
|
bit 27-16 | LEAP_A[11:0]: Special scanline length (H_TOTAL), when LEAP=0 |
bit 15-12 | Undefined: Initialized to 0
|
bit 11-0 | LEAP_B[11:0]: Special scanline length (H_TOTAL), when LEAP=1 |
Extra Details:
- LEAP_n specifies an alternate scanline length (H_TOTAL alternative value) for one scanline during vsync. Values larger than H_TOTAL specify the length of the second scanline of vsync. Values smaller than H_TOTAL specify the length of the first scanline of vsync and have a variety of undesired side effects, such as skipping one hsync entirely or leaving csync erroneously high for one whole scanline. Serration changes these effects subtly.
- Specifically, a counter is started at the start of vsync. When that counter is equal to LEAP_n, the VI starts or restarts the second scanline of vsync without changing the status of the csync bit.
- The default PAL values of LEAP (
0x15
), LEAP_A (3182
), and LEAP_B (3183
) appear to be chosen to add PAL's nominal "one extra chroma period per 625 whole scanlines emitted". Average of 5,6,5,6,5 = 5.4; divide 5.4 by 11.2 VI clocks per chroma period = 1/2 chroma period per field.
VI_H_VIDEO 0x0440 0024
| ||||||||
---|---|---|---|---|---|---|---|---|
31:24 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | RW-0 | RW-0 |
— | — | — | — | — | — | H_START[9:8] | ||
23:16 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
H_START[7:0] | ||||||||
15:8 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | RW-0 | RW-0 |
— | — | — | — | — | — | H_END[9:8] | ||
7:0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
H_END[7:0] |
bit 31-26 | Undefined: Initialized to 0
|
bit 25-16 | H_START[9:0]: Start of the active video image, in screen pixels. Typical values: NTSC (108 ) or PAL (128 )
|
bit 15-10 | Undefined: Initialized to 0
|
bit 9-0 | H_END[9:0]: End of the active video image, in screen pixels from hsync. Typical values: NTSC (748 ) or PAL (768 )
|
Extra Details:
- H_START specifies when VI evaluation starts. The screen remains blanked for several pixels afterwards, while the VI loads values from RAM for filtering, even if AA_MODE is set to "no filtering".
- H_END specifies the first black pixel on the right end of each scanline.
- Setting H_START = H_END = 0 blanks the display output (full black, no picture displayed) but keeps the VI fully active (line counter will increment, interrupts will trigger, etc.). This is different from setting VI_CTRL.TYPE=0, which instead will totally stop VI activity (no output signal).
- Setting VI_H_VIDEO while display is in progress can sometimes cause the VI to hang (normally, a striped picture will be displayed). The registers should only be changed during vertical blank.
VI_V_VIDEO 0x0440 0028
| ||||||||
---|---|---|---|---|---|---|---|---|
31:24 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | RW-0 | RW-0 |
— | — | — | — | — | — | V_START[9:8] | ||
23:16 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
V_START[7:0] | ||||||||
15:8 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | RW-0 | RW-0 |
— | — | — | — | — | — | V_END[9:8] | ||
7:0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
V_END[7:0] |
bit 31-26 | Undefined: Initialized to 0
|
bit 25-16 | V_START[9:0]: Start of the active video image, in screen half-lines. Typical values: NTSC (0x025 ) or PAL (0x05F )
|
bit 15-10 | Undefined: Initialized to 0
|
bit 9-0 | V_END[9:0]: End of the active video image, in screen half-lines from vsync. Typical values: NTSC (0x1FF ) or PAL (0x239 )
|
VI_V_BURST 0x0440 002C
| ||||||||
---|---|---|---|---|---|---|---|---|
31:24 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | RW-0 | RW-0 |
— | — | — | — | — | — | V_BURST_START[9:8] | ||
23:16 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
V_BURST_START[7:0] | ||||||||
15:8 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | RW-0 | RW-0 |
— | — | — | — | — | — | V_BURST_END[9:8] | ||
7:0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
V_BURST_END[7:0] |
bit 31-26 | Undefined: Initialized to 0
|
bit 25-16 | V_BURST_START[9:0]: Start of the color burst enable, in half-lines. Typical values: NTSC (0x00E ) or PAL (0x009 )
|
bit 15-10 | Undefined: Initialized to 0
|
bit 9-0 | V_BURST_END[9:0]: End of the color burst enable, in half-lines. Typical values: NTSC (0x204 ) or PAL (0x26B )
|
VI_X_SCALE 0x0440 0030
| ||||||||
---|---|---|---|---|---|---|---|---|
31:24 | U-0 | U-0 | U-0 | U-0 | RW-0 | RW-0 | RW-0 | RW-0 |
— | — | — | — | X_OFFSET[11:8] | ||||
23:16 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
X_OFFSET[7:0] | ||||||||
15:8 | U-0 | U-0 | U-0 | U-0 | RW-0 | RW-0 | RW-0 | RW-0 |
— | — | — | — | X_SCALE[11:8] | ||||
7:0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
X_SCALE[7:0] |
bit 31-28 | Undefined: Initialized to 0
|
bit 27-16 | X_OFFSET[11:0]: Horizontal subpixel offset (2.10 format) |
bit 15-12 | Undefined: Initialized to 0
|
bit 11-0 | X_SCALE[11:0]: 1/horizontal scale up factor (2.10 format) |
Errata
- If AA_MODE = 11 (resampling disabled), TYPE = 10 (16-bit), X_SCALE is 0x200 or lower, and H_START is less than 128, the VI generates invalid output, consisting of the first 64 pixels from the framebuffer from the current line, then 64 pixels of garbage, and these two repeat for the rest of each scanline
- If X_SCALE is higher than 0x800 (32bpp) or 0xE00 (16bpp), the scaler renders incorrect pixels, with specifics depending on depth. This appears to be due to exceeding the number of VI fetches allocated per scanline.
VI_Y_SCALE 0x0440 0034
| ||||||||
---|---|---|---|---|---|---|---|---|
31:24 | U-0 | U-0 | U-0 | U-0 | RW-0 | RW-0 | RW-0 | RW-0 |
— | — | — | — | Y_OFFSET[11:8] | ||||
23:16 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
Y_OFFSET[7:0] | ||||||||
15:8 | U-0 | U-0 | U-0 | U-0 | RW-0 | RW-0 | RW-0 | RW-0 |
— | — | — | — | Y_SCALE[11:8] | ||||
7:0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
Y_SCALE[7:0] |
bit 31-28 | Undefined: Initialized to 0
|
bit 27-16 | Y_OFFSET[11:0]: Vertical subpixel offset (2.10 format) |
bit 15-12 | Undefined: Initialized to 0
|
bit 11-0 | Y_SCALE[11:0]: 1/vertical scale up factor (2.10 format) |
Erratum
- If Y_SCALE exceeds 0xC00, it instead behaves like a glitchy variation of 3*(0x1000-Y_SCALE)
VI_TEST_ADDR 0x0440 0038
| ||||||||
---|---|---|---|---|---|---|---|---|
31:24 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 |
— | — | — | — | — | — | — | — | |
23:16 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 |
— | — | — | — | — | — | — | — | |
15:8 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 | U-0 |
— | — | — | — | — | — | — | — | |
7:0 | U-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
— | TEST_ADDR[6:0] |
bit 31-7 | Undefined: Initialized to 0
|
bit 6-0 | TEST_ADDR[6:0]: Sets the line buffer word address at which VI_STAGED_DATA will read/write data. |
VI_STAGED_DATA 0x0440 003C
| ||||||||
---|---|---|---|---|---|---|---|---|
31:24 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
STAGED_DATA[31:24] | ||||||||
23:16 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
STAGED_DATA[23:16] | ||||||||
15:8 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
STAGED_DATA[15:8] | ||||||||
7:0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 | RW-0 |
STAGED_DATA[7:0] |
bit 31-0 | STAGED_DATA[31:0]: Reads from this register returns 32 bits of line buffer data at the address specified in VI_TEST_ADDR. Writes to this register emplace 32 bits of data into the line buffer at the address specified in VI_TEST_ADDR. Usage requires TEST_MODE to be set in VI_CTRL. |
Fixed-Point Format
Fixed-point is a method of representing decimal numbers. Unlike floating-point numbers, fixed-point numbers allocate a specific number of bits for the integer (X) and fractional (Y) parts of the number, denoted as "X.Y format" (similar to Q-notation). In this format, a certain number of bits are dedicated to the integer part, while the remaining bits represent the fractional part. For instance, some VI registers employ the 2.10 format, where two bits are used for the integer part and ten bits are allocated for the fractional part, resulting in a total of twelve bits.
Here are examples of decimals represented in 2.10 format:
Failed to parse (syntax error): {\displaystyle \begin{align*} 1\ == &\quad 01\ 0000000000\ \text{(0x400)} \\ 0.25\ == &\quad 00\ 0100000000\ \text{(0x100)} \\ 0.125\ == &\quad 00\ 0010000000\ \text{(0x80)} \\ \end{align*} }
Note that not all decimals can be represented and must be approximated. For example, in 2.10 format the decimal 3.14 is approximated as 3.1416015625, or `11 0010010001`. Here's an example of how to convert from the binary to decimal:
The integer part is given by adding powers of two, starting at zero and going right to left:
Failed to parse (syntax error): {\displaystyle \begin{align*} \text{Binary:} &\quad 11 \\ \text{Value:} &\quad 1 \times 2^1 + 1 \times 2^0 = 3 \\ \end{align*} }
The fractional part is given by adding the inverse of powers of two, staring at one and going left to right:
Failed to parse (syntax error): {\displaystyle \begin{align*} \text{Binary:} &\quad 0010010001 \\ \text{Value:} &\quad \frac{0}{2^1} + \frac{0}{2^2} + \frac{1}{2^3} + \frac{0}{2^4} + \frac{0}{2^5} + \frac{1}{2^6} + \frac{0}{2^7} + \frac{0}{2^8} + \frac{0}{2^9} + \frac{1}{2^{10}} = 0.1416015625 \\ \end{align*} }
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))
Improve visible detail in low resolution
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.
- ↑ This register used to be called VI_V_SYNC, but was changed to better reflect its actual meaning.
- ↑ This register used to be called VI_H_SYNC, but was changed to better reflect its actual meaning.
- ↑ This register used to be called VI_H_SYNC_LEAP, but was changed to better reflect its actual meaning.