Working around 2D primitives

This article is about 2D primitives and provides some code to interact with then. More after the jump.

We’re back to more tricks on how you avoid LibGS like the plague. Let’s start with some background information on sprites.

The main reason why most homebrews are made with LibGS is because it provides a few handy functions to make the PlayStation draw stuff on screen with little to no effort. So what’s the hurdle with direct low level access? There may be quite a few reason for this, the main one being sprites not storing VRAM page indices into the usual structure, while a second guess would be scaling and rotation effects provided by some of the calls. Let’s go in order to fill a few gaps.

DR_TPAGE primitives

[code language=”CPP”]typedef struct tagDrTpage
{
u32 tag;
u32 code;
} DR_TPAGE;[/code]

These primitives function as texture switches in a linked list. In other words, they set up the GPU to move around VRAM pages and can also assign a few effects like blending, dithering, etc. Here’s a list of what the bits inside code do, including some undocumented features like horizontal/vertical flip.

[code language=”CPP”]// DR_MODE bits
#define DMODE_TRANS0 (0<<5) // 50% back, 50% front
#define DMODE_TRANS1 (1<<5) // additive
#define DMODE_TRANS2 (2<<5)
#define DMODE_TRANS3 (3<<5) // subtractive
#define DMODE_4BIT (0<<7)
#define DMODE_8BIT (1<<7)
#define DMODE_15BIT (2<<7)
#define DMODE_DITHER (1<<9)
#define DMODE_OUT_AREA (1<<10) // drawing to display area (0: prohibited, 1: allowed)
#define DMODE_HFLIP (1<<11)
#define DMODE_VFLIP (1<<12)[/code]

The first 5 bits contain the number of VRAM page to use. All the bits above and VRAM index can be ORed together to form any desired effect, then simply register the primitive in a linked list and you’re good to go. For example:

[code language=”CPP”]DR_TPAGE *p = gfxGetPtr();
// populate structure
p->tag = 2<<24;
p->code = 5 | DMODE_4BIT | DMODE_DITHER;
// register in linked list
addPrim(GetOTag(),p);
// update packet
gfxSetPtr(&p[1]);[/code]

This code will set any following sprite primitive to use VRAM page 5 in 16 color mode with a forced dither effect applied.
If you need several changes of VRAM page in a row, there is a trick you can pull that chains a DR_TPAGE primitive and sprite together, which also saves you one useless word in the process. We’ll see later how that works in detail.

Sprites in all sizes

The PlayStation can draw sprites of varying size with a texture cap of 256×256 pixels (i.e. you can still wrap a texture and repeat it on the same sprite). There are three types of sprite primitives: SPRT, SPRT8, and SPRT16. They all act the same, except SPRT8/16 have no width/height attribute (i.e. they are a word smaller), so they will always draw as 8×8 or 16×16 images. This is what a sprite structure looks like:

[code language=”CPP”]typedef struct tagSprt
{
u32 tag;
u8 r0, g0, b0, code;
s16 x, y;
u8 u0, v0;
u16 clut;
u16 w, h; // these don’t exist on SPRT8/16
} SPRT;[/code]

As you can see, sprites have no TPAGE attribute anywhere to be found which can discourage a few to use the structure for drawing anything simple (a few official games actually do this, whoops). So how do we overcome this limitation with a smart solution? We use chained primitives.

Merged sprites

The PlayStation GPU is quite versatile when it comes to primitive handling, as you can chain them together up to 255 words. Let’s see how that works in terms of data representation with two examples of sprites embedding a DR_TPAGE primitive:

[code language=”CPP”]// any size
typedef struct tagFastSprt
{
u32 tag;
u32 mode;
u32 rgbc;
u32 xy;
u32 uvc;
u32 wh;
} FAST_SPRT;

// 8×8 or 16×16
typedef struct tagFastSSprt
{
u32 tag;
u32 mode;
u32 rgbc;
u32 xy;
u32 uvc;
} FAST_SPRT8, FAST_SPRT16;[/code]

Both these structures represent variable and fixed sprites with an extra word called mode, which stores our DR_TPAGE code from the structure above. We can populate them exactly the same, giving sprites the ability to store VRAM attributes per primitive. This trick also saves us one word which would be otherwise wasted on an extra tag from DT_TPAGE (i.e. when you chain primitives only one tag attribute is required, since it only stores an address to the next primitive in list for the lowest 24 bits, while the upper 8 bits are the length of a primitive in words). Let’s see how we can populate a variable merged sprite:

[code language=”CPP”]void Draw_some_sprite(int x, int y, u32 *ot)
{
FAST_SPRT *s = gfxGetPtr();
// set length of chained sprite (5 words, tag doesn’t count)
s->tag = 5<<24;
// set DR_TPAGE code (0xE1), indexing data and properties
s->mode = (0xE1<<24) | 5 | DMODE_DITHER | DMODE_4BIT;
// set SPRT code (0x64) and assign neutral RGB
s->rgbc = (0x64 << 24) | 0x808080;
// populate parameter coordinates in one write
s->xy = (x & 0xffff) | y << 16;
// populate the rest with word writes too
s->uvc = 10 | (12<<8) | (getClut(0,480) << 16);
// sort to linked list and update packet allocator
addPrim(ot,s);
gfxSetPtr(&s[1]);
}
[/code]

This code populates the primitive with just word writes, providing fast RAM access as well. One important note about sprites: they have a few limitations, specifically the U coordinate in UV maps. When you operate them in 4 bit mode, the U value mustn’t be an odd number or it will produce distortion on the console. Just make sure to pad all your textures to even values and you will be good.

In conclusion, what you see happening with these special sprites can be applied to many other primitives as well. For example, you can use a TILE primitive with DR_MODE attributes (similar to DR_TPAGE, just without the VRAM index) or a POLY_G4 with blending effects applied to transparencies. You can even chain more primitives of the same kind to simulate strips, just as long as you keep the total length below 255 words.

Here is some sample code summing up what we got so far. It’s a Visual Studio 2010 project (works fine in 2013/2015) configured as Makefile, so you’re going to need some manual work to make it compile without VS via batches or command line.

Download source