RDPQ Texturing with TMEM

While you can blit images directly to the frame buffer using the RDP, it is sometimes more useful to manually manage the texture memory (TMEM) to draw sprites in a way that can be more efficient when used correctly.

Loading textures

The first step is to load your texture into TMEM. To do this, we need to understand the various kinds of texture types. I wrote this guide to explain how all the different colour/image formats work, so have a look at that if you’re unfamiliar with terms like RGBA32, RGBA16, IA16, CI8, I8, IA8, CI4, I4 and IA4.

To actually get the textures loaded into TMEM, we’ll use these two functions: rdpq_tex_upload() and rdpq_tex_upload_sub(). They are identical except that the latter allows you to cut a slice out of a larger image (eg sprite sheets). Let’s have a look at the parameters:

// Returns the number of bytes used in TMEM for this texture
int rdpq_tex_upload_sub(
	// Tile number, ie TILE0 - TILE7. This is the pointer to this texture in TMEM
	// Useful when uploading several different tiles, otherwise just use TILE0
	rdpq_tile_t tile,
	// Surface containing the texture to load
	// Generated by sprite_get_pixels()
	const surface_t *tex,
	// Texture parameters, for default leave as NULL
	const rdpq_texparms_t *parms,
	// Top-left X coordinate of the rectangle to load (_sub only)
	int s0,
	// Top-left Y coordinate of the rectangle to load (_sub only)
	int t0,
	// Bottom-right X coordinate of the rectangle to load (_sub only)
	int s1,
	// Bottom-right Y coordinate of the rectangle to load (_sub only)
	int t1
	);

Remember that the last four parameters are for rdpq_tex_upload_sub() only. The other function will just load the whole texture.

For a basic texture upload, all you need to do is something like this:

// Outside the main loop or when the scene loads
sprite_t* brick_sprite = sprite_load("rom:/rgba16/brick.sprite"); // Get the sprite from the DFS
surface_t brick_surf = sprite_get_pixels(brick_sprite); // Convert sprite to surface (doesn't allocate more memory)

// In the main loop
tmem += rdpq_tex_upload(
	TILE0, // Label the texture at position #0
	&brick_surf, // This is the surface to upload
	NULL // No special parameters
	);

This won’t render anything to the frame buffer. It will need to be used in combination with a rendering function to draw the actual sprite.

Texture parameters

To customise the way the texture is uploaded, we can alter the 3rd parameter in the above function with the following structure:

typedef struct rdpq_texparms_s {
	// Memory location to place the texture at
	int tmem_addr;
	// Palette number where TLUT is stored (CI4 only)
	int palette;

	// This struct configures the settings for S and T dimensions (X and Y)
	struct {
		// How much to translate/offset the texture across
		float translate;
		// Scale modifier (power of 2)
		int scale_log;

		// Number of repetitions before the texture clamps (REPEAT_INFINITE for unlimited repetitions)
		float repeats;
		// Whether to mirror the image or not
		bool mirror;
	} s, t;

} rdpq_texparms_t;

Let’s go through what each one of these parameters does:

int tmem_addr is the position in TMEM where the sprite will be written to. Like in rdpq_tile_t tile, this is normally set to zero unless you want to have multiple textures in TMEM at once. Since the rdpq_tex_upload() functions return the size of a previously loaded texture (in bytes), it’s good to use it to keep track of how much memory we’ve written.

For example, here we have two textures being loaded from a sprite sheet into tmem in two different ways, by keeping track of the memory manually or by using multi upload. If you overflow with the manual method, it will overwrite the TMEM from the beginning but using multi upload will crash the program.

Code snippet
// Within the main loop, once brick_surf has been loaded
// Method #1: Keeping track of TMEM used manually
uint16_t tmem = 0;
tmem += rdpq_tex_upload_sub(
	TILE0,
	&tiles_surf,
	NULL,
	0, 0, 16, 16);
tmem += rdpq_tex_upload_sub(
	TILE1,
	&tiles_surf,
	&(rdpq_texparms_t){
		.tmem_addr = tmem,
	},
	16, 0, 32, 16);

// Method #2: Using the multi upload functions to handle memory locations
rdpq_tex_multi_begin();
rdpq_tex_upload_sub(
	TILE0,
	&tiles_surf,
	NULL,
	0, 0, 16, 16);
rdpq_tex_upload_sub(
	TILE1,
	&tiles_surf,
	NULL,
	16, 0, 32, 16);
rdpq_tex_multi_end();

int palette is the palette ID of the TLUT used.

After those two parameters, there are two structs called s and t which are settings that affect the S(X) and T(Y) axes.

  • float translate moves the texture across along the axis. Can only be a positive number.
  • int scale_log scales that axis according to powers of 2, to achieve an effect similar to mipmapping.
  • bool mirror flips the repeated textures about the axis.
  • float repeats make the texture loop around after it reaches its limit.

Here you can see them in action. Note that any area that isn’t filled by the repeats parameter will just get the nearest neighbour’s colour.

Code snippet
rdpq_tex_multi_begin();
rdpq_tex_upload_sub(
	TILE0,
	&tiles_surf,
	&(rdpq_texparms_t){
		.s.translate = (float)(l%16),
		.t.translate = (float)(l%16),
	},
	0, 0, 16, 16);
rdpq_tex_upload_sub(
	TILE1,
	&tiles_surf,
	&(rdpq_texparms_t){
		.s.scale_log = ((l/16)%4)-2,
		.t.scale_log = ((l/16)%4)-2,
	},
	0, 16, 16, 32);
rdpq_tex_upload_sub(
	TILE2,
	&tiles_surf,
	&(rdpq_texparms_t){
		.s.repeats = REPEAT_INFINITE,
		.t.repeats = REPEAT_INFINITE,
		.s.mirror = l/32%2,
		.t.mirror = l/16%2,
	},
	0, 32, 16, 48);
rdpq_tex_upload_sub(
	TILE3,
	&tiles_surf,
	&(rdpq_texparms_t){
		.s.repeats = 1+l/32%2,
		.t.repeats = 1+l/16%2,
	},
	16, 16, 32, 32);
rdpq_tex_multi_end();
rdpq_set_mode_standard();
rdpq_texture_rectangle(TILE0, 20, 20, 52, 52, 0, 0);
rdpq_texture_rectangle(TILE1, 60, 20, 92, 52, 0, 16);
rdpq_texture_rectangle(TILE2, 100, 20, 132, 52, 0, 32);
rdpq_texture_rectangle(TILE3, 140, 20, 172, 52, 16, 16);

Texture lookup tables (TLUT)

For sprites that are indexed (CI4 and CI8), you need to specify a texture lookup table so that the colour index has something to match up to. You can do this by using something like the following code:

rdpq_set_mode_standard();
rdpq_mode_tlut(TLUT_RGBA16);
rdpq_tex_upload_tlut(
	sprite_get_palette(brick_sprite),
	0,
	16);

Let’s break it down:

  • rdpq_mode_tlut() sets the TLUT mode in the RDPQ. There are 3 options:
    • TLUT_RGBA16 for RGBA colours, the standard parameter to use
    • TLUT_IA16 for greyscale/transparency colours
    • TLUT_NONE for no TLUT, will default to drawing all black.
  • rdpq_tex_upload_tlut() does the actual uploading.
    • Parameter 1 is a pointer to a palette, which can be obtained by the sprite_get_palette() function.
    • Parameter 2 is the colour index, which is a value 0-255 of where the first colour will be written.
    • Parameter 3 is the number of colours that will be loaded (1-256, but only multiples of 8).

Remember that colour index textures are limited to 16 in 4-bit mode and 256 in 8-bit mode.

Uploading from sprite

You can also upload to TMEM directly from a sprite using sprite_t instead of a surface_t. This may be simpler since it doesn’t require converting the sprite to a surface, the TLUT is automatically loaded in the case of indexed textures, it takes care of mipmapping and optimisation from mksprite persist.

It doesn’t seem to allow for sub-sprite selection when it comes to sprite sheets though. It will crash when you try to upload something that is too big for TMEM.

rdpq_sprite_upload(
	TILE0,
	brick_single_sprite,
	&(rdpq_texparms_t){}
	);

Drawing textures to the screen

Once you have the texture uploaded, it’s time to draw it to the active frame buffer. There are a few ways to do this, so let’s have a look one at a time.

Textured rectangles

To draw a textured rectangle, we need to use the rdpq_texture_rectangle() functions. The first one is fairly simple: it takes the tile number, rectangle coordinates and texture coordinates. Here’s an example from a non-tiled texture:

rdpq_texture_rectangle(
	// Texture tile ID to use
	TILE0,
	// X/Y coordinates of the top-left corner
	20, 20,
	// X/Y coordinates of the bottom-right corner
	36, 36,
	// S/T coordinates of the texture
	0, 0
	);

When using a texture from a tile sheet, it’s important to modify the S/T coordinates to have the same offset as in the texture load.

// Load the texture
rdpq_tex_upload_sub(
	TILE0,
	&brick_surf,
	NULL,
	0, 32,
	16, 48
	);
// Draw the rectangle
rdpq_texture_rectangle(
	TILE2,
	20, 20,
	36, 36,
	0, 32 // <- The offset matches line 6
	);

Note that when making a rectangle bigger than your texture, the empty space will be filled using nearest-neighbour extrapolation, and making it smaller will just crop it. Of course, you can fill in the glitchy space by using the repeats parameter in rdpq_texparms_t when loading the texture into TMEM.

// 32x32 rectangle
rdpq_texture_rectangle(
	TILE0,
	20, 20,
	52, 52,
	0, 0
	);
// 8x8 rectangle
rdpq_texture_rectangle(
	TILE0,
	60, 20,
	68, 28,
	0, 0
	);

Scaled rectangles

Drawing a scaled rectangle is very similar. We can achieve this by using the rdpq_texture_rectangle_scaled() function. Note that the S/T coordinates should match the coordinates of the texture in TMEM or else you will get cropping or extrapolation like in the image above.

// 128x128 rectangle
rdpq_texture_rectangle_scaled(
	// Tile to draw
	TILE0,
	// Top-left X/Y
	20, 20,
	// Bottom-right X/Y
	148, 148,
	// Top-left S/T
	0, 0,
	// Bottom-right S/T
	16, 16
	);

Blitting textures

You can also blit textures to the screen. This however doesn’t require uploading the textures to TMEM manually. You can see more in the advanced blitting guide. It’s pretty much the same as a regular blit except that you’re using a surface_t instead of a sprite_t. Remember that this will overwrite any textures in TMEM, so don’t use one of the above rectangle drawing functions after this one without re-uploading the texture.

rdpq_tex_blit(
	&brick_surf,
	20.0, 20.0,
	NULL
	);

Textured triangles

Texturing triangles works very differently from rectangles. This is mainly because rectangles are used to render 2D graphics and triangles are meant for 3D graphics. To recap, this is how a filled triangle is drawn:

// 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
	);

Drawing a textured triangle is a bit more complicated. Here we’ll structure our vertices in an array, but this time they’ll have five values instead of two: X/Y frame coordinates, S/T texture coordinates and Inverse-W coordinate.

The rdpq_triangle() function is very similar to what we used to make a flat filled triangle, but we use the TRIFMT_TEX triangle format in order to tell the function to read the S/T and Inv-W coordinates correctly.

float v[3][5] = {
	{20.0,	70.0,	0.0,	0.0,	1.0},
	{20.0,	102.0,	0.0,	16.0,	1.0},
	{52.0,	70.0,	16.0,	0.0,	1.0},
};
rdpq_triangle(
	&TRIFMT_TEX,
	v[0],
	v[1],
	v[2]
);

Here are some things to consider:

  • The Inv-W value is used for perspective correction in 3D space, so it’s best to just leave all of them at 1.0 for a 2D image.
  • S and T coordinates should be equal to the size of the texture in order to get a tight fit. You can always adjust it if you want mirroring/cropping.
  • The texture will auto-stretch so there’s no need to change the S/T values if you change the X/Y coordinates.

And for the sake of completion, here’s the full rectangle drawn with two triangles:

float v[4][5] = {
	{20.0,	70.0,	0.0,	0.0,	1.0},
	{20.0,	102.0,	0.0,	16.0,	1.0},
	{52.0,	70.0,	16.0,	0.0,	1.0},
	{52.0,	102.0,	16.0,	16.0,	1.0},
};
rdpq_triangle(
	&TRIFMT_TEX,
	v[0], v[1], v[2]
);
rdpq_triangle(
	&TRIFMT_TEX,
	v[3], v[1], v[2]
);

Messing around with vectors

Though this is a lot more complicated than using rectanlge textures or even blits, it does provide some flexibility in the way that the sprite is displayed.

We can rotate the double triangle by doing something like this:

You can also make weird animations like this which can’t be done with blitting.

Source for rotating textured square
// At the start of the scene
uint32_t l = 0;
// Source vectors (unchanging)
float v_source[4][5] = {
	{20.0,	70.0,	0.0,	0.0,	1.0},
	{20.0,	102.0,	0.0,	16.0,	1.0},
	{52.0,	70.0,	16.0,	0.0,	1.0},
	{52.0,	102.0,	16.0,	16.0,	1.0},
};
// Temp vectors (source vectors rotated by angle)
float v[4][5];
memcpy(&v, &v_source, 4*5*sizeof(float));
// Angle of rotation
float angle = 0.0;

// Inside the main loop
// Draw the triangles
rdpq_triangle(
	&TRIFMT_TEX,
	v[0], v[1], v[2]
);
rdpq_triangle(
	&TRIFMT_TEX,
	v[3], v[1], v[2]
);

// Angle of rotation (theta)
angle = M_PI*2*(l++/4)/32;
// For each vector, rotate it about point (35,86) by theta radians
for (int i=0; i<4; i++) {
	float x = 36.0;
	float y = 86.0;
	v[i][0] = (v_source[i][0]-x) * cos(angle) - (v_source[i][1]-y) * sin(angle) + x;
	v[i][1] = (v_source[i][1]-y) * cos(angle) + (v_source[i][0]-x) * sin(angle) + y;
}

Conclusion & full example

Managing textures is more complicated than doing plain sprite blitting, but it provides more control which can lead to faster performance or allowing for some unique effects.

Source code

Note that in order for this source code to work, the

#include <libdragon.h>
#include <math.h>
#include <stdlib.h>

int main(void)
{
	// Initialise the various systems
	display_init(RESOLUTION_320x240, DEPTH_32_BPP, 3, GAMMA_NONE, FILTERS_DISABLED);
	rdpq_init();
	dfs_init(DFS_DEFAULT_LOCATION);

	// Load sprites
	sprite_t* brick_sprite = sprite_load("rom:/CI4/brick-sheet.sprite");
	sprite_t* brick_single_sprite = sprite_load("rom:/CI4/brick.sprite");

	// Convert to surface
	surface_t brick_surf = sprite_get_pixels(brick_sprite);

	// Initialise the vectors for the triangle, and the angle of rotation
	uint32_t l = 0;
	float v_source[4][5] = {
		{20.0,	70.0,	0.0,	0.0,	1.0},
		{20.0,	102.0,	0.0,	16.0,	1.0},
		{52.0,	70.0,	16.0,	0.0,	1.0},
		{52.0,	102.0,	16.0,	16.0,	1.0},
	};
	float v[4][5];
	memcpy(&v, &v_source, 4*5*sizeof(float));
	float angle = 0.0;

	// Main loop
	while(1) {
		// Start a new frame
		// Get the frame buffer
		surface_t* disp;
		while(!(disp = display_try_get()));

		// Attach the buffer to the RDP (No z-buffer needed yet)
		rdpq_attach_clear(disp, NULL);

		// Reset the frame buffer with a plain white background
		rdpq_set_mode_fill(RGBA32(255, 255, 255, 0));
		rdpq_fill_rectangle(0, 0, display_get_width(), display_get_height());

		// Set the rendering modes
		rdpq_set_mode_standard();
		rdpq_mode_tlut(TLUT_RGBA16);

		// Upload the TLUT for the sprite
		rdpq_tex_upload_tlut(
			sprite_get_palette(brick_sprite),
			0,
			8);

		// Load a bunch of textures with a variety of settings
		rdpq_tex_multi_begin();
		rdpq_tex_upload_sub(
			TILE0,
			&brick_surf,
			&(rdpq_texparms_t){
				.s.translate = (float)((l/16%4)*5),
				.t.translate = (float)((l/16%4)*5),
			},
			0, 0, 16, 16);
		rdpq_tex_upload_sub(
			TILE1,
			&brick_surf,
			&(rdpq_texparms_t){
				.s.scale_log = ((l/16)%4)-2,
				.t.scale_log = ((l/16)%4)-2,
			},
			0, 16, 16, 32);
		rdpq_tex_upload_sub(
			TILE2,
			&brick_surf,
			&(rdpq_texparms_t){
				.s.repeats = REPEAT_INFINITE,
				.t.repeats = REPEAT_INFINITE,
				.s.mirror = l/32%2,
				.t.mirror = l/16%2,
			},
			0, 32, 16, 48);
		rdpq_tex_upload_sub(
			TILE3,
			&brick_surf,
			&(rdpq_texparms_t){
				.s.repeats = 1+l/32%2,
				.t.repeats = 1+l/16%2,
			},
			16, 16, 32, 32);
		rdpq_tex_multi_end();

		// Draw a bunch of rectangles to show those settings (and one scaled)
		rdpq_texture_rectangle(TILE0, 20, 20, 52, 52, 0, 0);
		rdpq_texture_rectangle(TILE1, 60, 20, 92, 52, 0, 16);
		rdpq_texture_rectangle_scaled(TILE2, 100, 20, 132, 52, 0, 32, 24, 56);
		rdpq_texture_rectangle(TILE3, 140, 20, 172, 52, 16, 16);

		//Just to show that we can use blitting to display larger textures
		rdpq_tex_blit(&brick_surf, 20.0, 130.0, NULL);

		// This is just to show that we can draw textures by uploading from sprite_t instead of a surface_t
		rdpq_sprite_upload(TILE0, brick_single_sprite, NULL);
		rdpq_texture_rectangle(TILE0, 20, 110, 36, 126, 0, 0);

		// Angle of rotation (theta)
		angle = M_PI*2*(l/4)/32;
		// For each vector, rotate it about point (35,86) by theta radians
		for (int i=0; i<4; i++) {
			float x = 36;
			float y = 86;
			v[i][0] = (v_source[i][0]-x) * cos(angle) - (v_source[i][1]-y) * sin(angle) + x;
			v[i][1] = (v_source[i][1]-y) * cos(angle) + (v_source[i][0]-x) * sin(angle) + y;
		}

		// Draw the spinning square made of triangles using the texture uploaded with rdpq_sprite_upload()
		rdpq_triangle(
			&TRIFMT_TEX,
			v[0], v[1], v[2]
		);
		rdpq_triangle(
			&TRIFMT_TEX,
			v[3], v[1], v[2]
		);

		// Send frame buffer to display (TV)
		rdpq_detach_show();

		// Increase the frame counter
		l++;
	}
}

Search

Subscribe to the mailing list

Follow N64 Squid

  • RSS Feed
  • YouTube

Random posts