Hardware graphics (RDPQ)
Note! The content that was previously on this page has been deprecated due to the move of RDPQ from the unstable to trunk branch. In the time that it takes me to update this page with the new functionality, you can still have a look at the legacy page.
The RDPQ (RDP command queue) library interfaces with the N64’s Reality Display Processor (RDP) through the Reality Signal Processor (RSP). The library handles the complex programming of the RDP and supports both 2D and 3D tasks. It is divided into several headers, each addressing different functionalities like low-level command generation, triangle and rectangle drawing, texture handling, and debugging.
To the contrary of the legacy RDP library, the RSPQ allows you to enqueue tasks for the RDP to perform without needing to perform constant syncing after each task. This makes it both easier to develop for and faster to render each frame at runtime.
Let’s go though all the various functionality that RDPQ provides.
Basic setup
This is going to be the simplest ROM I can make to show how the RDPQ works. It’ll display a black square on a white background.
#include <libdragon.h>
int main(void)
{
// Initialise the various systems
display_init(RESOLUTION_640x480, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_DISABLED);
rdpq_init();
// Main loop
while(1) {
// Start a new frame
// Get the frame buffer
surface_t* disp;
while(!(disp = display_try_get()));
// Attach the buffers to the RDP (No z-buffer needed yet)
rdpq_attach_clear(disp, NULL);
// Fill the background with white
rdpq_set_mode_fill(RGBA32(0xFF, 0xFF, 0xFF, 0));
rdpq_fill_rectangle(0, 0, display_get_width(), display_get_height());
// Draw in a black rectangle
rdpq_set_mode_fill(RGBA32(0x00, 0x00, 0x00, 0));
rdpq_fill_rectangle(100, 100, display_get_width()-100, display_get_height()-100);
// Send frame buffer to display (TV)
rdpq_detach_show();
}
}
Breakdown
The program starts by initialising the subsystems. Nothing too special here.
// Initialise the various systems
display_init(RESOLUTION_640x480, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_DISABLED);
rdpq_init();
Within the main loop, the first step is to get a frame buffer. This is a section of memory that contains the pixel data for the next frame and its attributes, we’ll call it disp
. Then we use the display_try_get()
function in a loop to wait until we have a new frame buffer available for us to render.
// Get the frame buffer
surface_t* disp;
while(!(disp = display_try_get()));
This frame buffer then needs to be attached to the RDP. Since we’re not using a z-buffer, we can leave the 2nd parameter as NULL
.
// Attach the buffers to the RDP (No z-buffer needed yet)
rdpq_attach_clear(disp, NULL);
Now we’re ready to draw. Let’s start by setting the colour of the RDP to white. Then draw a rectangle. The parameters for this function are the x/y coordinates for the top-left corner, and then the x/y coordinates of the bottom-right corner. You can also think of it as the coordinates of the left/top/bottom/right sides of the rectangle.
We then do it again for a smaller black rectangle.
// Fill the background with white
rdpq_set_mode_fill(RGBA32(0xFF, 0xFF, 0xFF, 0));
rdpq_fill_rectangle(0, 0, display_get_width(), display_get_height());
// Draw in a black rectangle
rdpq_set_mode_fill(RGBA32(0x00, 0x00, 0x00, 0));
rdpq_fill_rectangle(100, 100, display_get_width()-100, display_get_height()-100);
The final step is to send the generated frame buffer to the N64’s video output.
// Send frame buffer to display (TV)
rdpq_detach_show();
Modes
Programming with the RDP begins with a thorough understanding of the various modes that it has. While there are many more than these, these are the ones that are used most frequently.
rdpq_set_mode_standard()
sets the RDP’s mode to the most basic standard functionality. It can do texturing without shading, dithering, blending, antialiasing etc. This mode can be enhanced with other functions (which will be described in greater detail later).
rdpq_set_mode_fill(color_t color)
is a mode used to fill sections of the screen with a solid, untextured colour. Its parameter is usually defined by the macro RGBA32(0xFF, 0xFF, 0xFF, 0)
which returns a color_t
of the RGBA colour you want.
rdpq_set_mode_copy(bool transparency)
is used for texture blitting. The is the main way of using the RDP for drawing sprites in a 2D environment. The transparency parameter is used to determine whether any transparent pixels should be rendered as a solid colour or as transparent.
rdpq_set_mode_yuv(bool bilinear)
allows the RDP to work with YUV textures. This isn’t really used much in practice. Its single parameter determines whether to use linear interpolation or not.
Basic (filled) shapes
Filled shapes are just shapes that are filled with a solid colour. That means no texture, no gradient, no lighting, nothing. This makes them very fast to draw, but inevitably makes for a very basic visual experience. It’s still good to keep these in mind for creating a simple user interface.
Note that all of these require the rdpq_set_mode_fill()
function in order to work correctly.
Rectangles
These are fairly basic, all you need is rdpq_fill_rectangle(x0, y0, x1, y1), which will draw a rectangle with the top-left and bottom-right corners defined in the parameters. Remember to set the mode to fill with the colour that you want.
To bring back the example from before, in square format:
// Draw in a black rectangle
rdpq_set_mode_fill(RGBA32(0x00, 0x00, 0x00, 0));
rdpq_fill_rectangle(
100, // Top-left pixel X coordinate
100, // Top-left pixel Y coordinate
200, // Bottom-right pixel X coordinate
200 // Bottom-right X coordinate
);
Triangles
Making a filled triangle is a bit more complicated. The function for creating triangles is very flexible to allow for many different use cases, but for the time being we just want to focus on creating a filled triangle.
The basic idea is similar. You need to first set the RDP mode to fill, then draw the triangle using the coordinates of its vertices. For now we’re setting the format to TRIFMT_FILL
, which basically disables all shaders, textures and z-buffering.
Also note that the coordinates in this case are set as arrays of floats. This is because this is a more convenient way to render triangles when there are lots of vertices for say, a 3D model. But that is not needed to just draw a simple 2D triangle.
// Draw in a green rectangle
rdpq_set_mode_fill(RGBA32(0x00, 0xFF, 0x00, 0));
float v1[] = { 200, 100 };
float v2[] = { 300, 200 };
float v3[] = { 200, 200 };
rdpq_triangle(
&TRIFMT_FILL, // Triangle render settings set to fill with a solid colour
v1, // Vertex 1
v2, // Vertex 2
v3 // Vertex 3
);
The same sort of thing can be achieved by putting the floats inline, if that’s the sort of thing you’re into.
rdpq_set_mode_fill(RGBA32(0x00, 0xFF, 0x00, 0));
rdpq_triangle(
&TRIFMT_FILL,
(float[]){200.0, 100.0},
(float[]){300.0, 200.0},
(float[]){200.0, 200.0}
);
Sprites and bitmaps
Sprites are a bit harder to draw to screen since they require not only that you define where to draw something, but also that you convert and import the image files into the Dragon File System (DFS). For the sake of keeping this tutorial simple, we’ll assume that you already have your files uploaded to the DFS.
The process of displaying a basic sprite is fairly simple. All you need to do is load the sprite from the ROM into memory, set the RDP to texture copy mode and then use the RDP to blit it onto the frame buffer.
// Put this at the start of the scene
sprite_t* brick = sprite_load("rom:/brick.sprite");
// Put this in the main loop
rdpq_set_mode_copy(true);
rdpq_sprite_blit(brick, 100, 100, NULL);
There are more ways to customise the way sprtes are displayed, but that will come at a later time. This is good enough to make NES-style graphics.
Using rdpq_sprite_blit(), you can also display larger sprites onto the screen, enough to cover the whole screen. Just keep in mind that it could crash if it takes up too much memory.
Advanced Sprite Blitting
There are many different ways of customising the way that sprites are blitted using Libdragon.
RDPQ Texturing with TMEM
It's possible to have greater control over textures and achieve special 2D effects by manually loading them into TMEM. Let's find out how.
Fonts and text
To draw some basic text to screen, you need to register it and then you can start printing things to the frame buffer. Libdragon has two default fonts to get you started:
FONT_BUILTIN_DEBUG_MONO
for monospace textFONT_BUILTIN_DEBUG_VAR
for variable width text
// Do this on loading your scene
rdpq_font_t *font = dpq_font_load_builtin(FONT_BUILTIN_DEBUG_MONO);
rdpq_text_register_font(
1, // Font ID to set it to
font // Font pointer
);
// Do this in the main loop
rdpq_text_printf(
NULL, // Font settings, this is default
1, // Font ID number
10, // X position
50, // Y position
"This is some text using Monospace font"
);
There are many things you can do to customise your fonts, but for now this is the most basic way to display text on screen.
Advanced text rendering
It is possible to further customise the way that fonts are rendered by using some of the RDPQ's advanced text functions.