Display subsystem

Managing the display subsystem is probably the least sexy part of working with N64 graphics, but it is necessary to get right if you want your game to display correctly on your screen.

The purpose of the display subsystem is to start, stop and configure the game’s viewport. Surfaces are the data structure used to interact with the display.

Data structures & definitions

surface_t and display_context_t

The only data structure to be used in this area is the surface data type, also known as surface_t. Basically, it is a frame buffer for graphics. Think of it as the area in memory where the screen output is located.

typedef struct surface_s
{
    uint16_t flags;       ///< Flags (including pixel format)
    uint16_t width;       ///< Width in pixels
    uint16_t height;      ///< Height in pixels
    uint16_t stride;      ///< Stride in bytes (length of a row)
    void *buffer;         ///< Frame buffer pointer
} surface_t;

typedef surface_t* display_context_t;

From this code, we can see that display_context_t is just an alias for a pointer to a surface_t structure.

The individual parts of the data type are note really important since they are typically managed by pre-built functions, but they are otherwise quite self-explanatory. All you really need to know is that this contains the frame buffer and its properties.

resolution_t

This is the data type that you pass to show what video resolution you want to use. Generally, the higher resolution looks better, but may cause some frame skips since they are rendered more slowly.

typedef struct {
    /** @brief Screen width (must be between 2 and 800) */
    int32_t width;
    /** @brief Screen height (must be between 1 and 720) */
    int32_t height;
    /** @brief True if interlaced mode enabled */
    bool interlaced;
} resolution_t;

You can of course build your own resolution, but there are some pre-built ones in the Libdragon library:

/** @brief 256x240 mode */
const resolution_t RESOLUTION_256x240 = {256, 240, false};
/** @brief 320x240 mode */
const resolution_t RESOLUTION_320x240 = {320, 240, false};
/** @brief 512x240 mode, high-res progressive */
const resolution_t RESOLUTION_512x240 = {512, 240, false};
/** @brief 640x240 mode, high-res progressive */
const resolution_t RESOLUTION_640x240 = {640, 240, false};
/** @brief 512x480 mode, interlaced */
const resolution_t RESOLUTION_512x480 = {512, 480, true};
/** @brief 640x480 mode, interlaced */
const resolution_t RESOLUTION_640x480 = {640, 480, true};

Display options

These are all just some enums that are used to help populate the display_init() function. They’re not too complicated, so I’ll just list them here in order.

bitdepth_t

typedef enum
{
    /** @brief 16 bits per pixel (5-5-5-1) */
    DEPTH_16_BPP,
    /** @brief 32 bits per pixel (8-8-8-8) */
    DEPTH_32_BPP
} bitdepth_t;

This lets you choose how many bits per pixel (BPP) you want. Fairly self-explanatory. The main difference is that 16-bits gives you only 5 bits per colour (32 values) and one bit for transparency. This is enough for most 2D games, but it suffers when you want to use soft gradients or partial transparency.

gamma_t

typedef enum
{
    /** @brief Uncorrected gamma, should be used by default and with assets built by libdragon tools */
    GAMMA_NONE,
    /** @brief Corrected gamma, should be used on a 32-bit framebuffer
     * only when assets have been produced in linear color space and accurate blending is important */
    GAMMA_CORRECT,
    /** @brief Corrected gamma with hardware dithered output */
    GAMMA_CORRECT_DITHER
} gamma_t;

These values let you choose if you want gamma correction in your display, which is a technique used to adapt screen brightness to human-seeable brightness.

filter_options_t

typedef enum
{
    /** @brief All display filters are disabled */
    FILTERS_DISABLED,
    /** @brief Resize the output image with a bilinear filter. 
     * In general, VI is in charge of resizing the framebuffer to fit the TV resolution 
     * (which is always NTSC 640x480 or PAL 640x512). 
     * This option enables a bilinear interpolation that can be used during this resize. */
    FILTERS_RESAMPLE,
    /** @brief Reconstruct a 32-bit output from dithered 16-bit framebuffer. */
    FILTERS_DEDITHER,
    /** @brief Resize the output image with a bilinear filter (see #FILTERS_RESAMPLE). 
     * Add a video interface anti-aliasing pass with a divot filter. 
     * To be able to see correct anti-aliased output, this display filter must be enabled,
     * along with anti-aliased rendering of surfaces. */
    FILTERS_RESAMPLE_ANTIALIAS,
    /** @brief Resize the output image with a bilinear filter (see #FILTERS_RESAMPLE). 
     * Add a video interface anti-aliasing pass with a divot filter (see #FILTERS_RESAMPLE_ANTIALIAS).
     * Reconstruct a 32-bit output from dithered 16-bit framebuffer. */
    FILTERS_RESAMPLE_ANTIALIAS_DEDITHER
} filter_options_t;

These are some preconfigured filters available in Libdragon. Again, you can use them if you like, but it’s best to leave it at FILTERS_RESAMPLE or FILTERS_DISABLED.

Starting/stopping the display

The most important step in working with a display is to start and stop it.

Initialising the display

void display_init(
	resolution_t res,
	bitdepth_t bit,
	uint32_t num_buffers,
	gamma_t gamma,
	filter_options_t filters
	);

The display_init() function is there to initialise the video system. The parameters are there to specify the proportions of the display we’re using. This is set globally so you can’t initialise it twice.

  • resolution_t res – The resolution of the display. It’s best to use one of the predefined ones found here.
  • bitdepth_t bit – Whether you want to use 16 or 32-bit pixels. See their values here.
  • uint32_t num_buffers – the count of frame buffers you want, between 2 and 32. More buffers take more memory in the form of surface_t structures, but can provide a more stable framerate.
  • gamma_t gamma – Gamma correction mode. See values here.
  • filter_options_t filters – Video filters to use. See values here.

Stopping the display

Now you might wonder “Why would I ever want to stop the display? Can’t I just switch off my N64?” and the answer is “Yes”. You don’t really need to ever use the next function except in one condition.

void display_close();

The only reason why you’d want to close the display if you want to change the parameters of the display_init() function. Say for example, you want to have a high res static title screen but then switch to a lower resolution when you want to run your game at a high frame rate.

One thing to note when doing this is that the buffers in the surface_t are cleared and the screen will turn black for a frame or two until the new buffers are redrawn, so make sure not to do this in the middle of gameplay.

The typical way of doing this is to initialise the display right after closing it with minimal logic in between, something like this:

display_close();
/*
	Optional: Some logic that determines the parameters for display_init()
	*/
display_init(RESOLUTION_320x240, DEPTH_16_BPP, 2, GAMMA_NONE, FILTERS_DISABLED);

Rendering the display

Now that you’ve chosen your display settings, it’s time to do some actual screen rendering.

Locking the display

The first step is to grab a frame buffer. This is one of the memory chunks defined by num_buffers in the display_init() section above. Think of of kind of like a 2D array of pixels which the N64 is preparing to send to your TV.

surface_t* display_lock(void);

Consider the above function to be the ‘start’ of the screen rendering process. What it does is that it grabs the next free frame buffer and returns it. If no free buffer is found, it returns NULL. To avoid writing to a null pointer, in practice it’s best to put it in a loop like this:

display_context_t frame_buffer;
while(!(frame_buffer = display_lock()));

Then you can use the frame_buffer variable in all of your graphics processing functions (explained on another page).

Displaying the display

Once you’re happy with the frame buffer you’ve created, it’s time to push it to the user’s screen. It’s really as simple as using this function:

void display_show(surface_t* surf);

That’s all there is to it. Pass the pointer you got from display_lock in as a parameter and you’re all set.

Advanced

Though the normal way to process a frame is Lock -> Draw -> Send, it is possible to start rendering a second frame before the display_show() function is called. However, it is important to remember that the frame buffers will be shown in order of locking.

This can be useful when doing parallel processing using both the CPU and RDP, or when reusing certain assets between frames.

A basic example

This is the most basic example I could come up with to demonstrate how the display module works. It shows each of the above modules (except for the optional display_close) to show how a simple display cycle works.

#include <libdragon.h>

int main(void) {
	// Initialise the display variable
	static display_context_t disp;
	// Initialise the display
	display_init(RESOLUTION_320x240, DEPTH_16_BPP, 2, GAMMA_NONE, FILTERS_RESAMPLE);

	// Main loop
	while (1) {
		// Wait until we can find a free display buffer and then assign it to disp
		while(!(disp = display_lock()));
		// Draw a big white rectangle onto the buffer
		graphics_draw_box(disp, 20, 20, 280, 200, graphics_make_color(255, 255, 255, 255));
		// Push the display buffer onto the screen
		display_show(disp);
	}
}

Running this will give this output:

Additional reading

There’s also a useful bit of sample code in “test” that goes through a few different display methods.

Search

Subscribe to the mailing list

Follow N64 Squid

  • RSS Feed
  • YouTube

Random posts