PSP Vertices & Data Cache Awareness

Jonathan Janevski <jjanevski@gmail.com> 21 Aug 2008
back

Introducion

This is the first document that I'm publishing to the internet dealing with things of PSP nature. There is a higher reason for writing all of this which transcends learning... it is shear frustration from hours of debugging only to find the error has to do with Sony's bad design of data memory access interface.

In this guide we will cover the proper structure for a vertex, vertex arrays, sceGumDrawArray(), and the PSP's infamous Data Cache. The reader is expected to have a decent working knowledge of the c/c++ language and some basic understand of how to construct and compile a program for the PSP.

If something isn't clear, looks wrong, or if you have any questions, please take the time and send me an email.

Vertex Structure

The PSP is quite a powerful little unit capable of rendering complex and beautiful 3D scenes. But in order to harness this power we first need to understand how to store and manipulate 3D data.

The GE (Graphics Engine) on the PSP has a specific format for how vertex data must be defined.

This is the following order:

  1. weights
  2. texture
  3. color
  4. normal
  5. vertex

You don't have to use every component that is provided. Only the data which you need for your vertices is required to be defined inside of the structure. Let's look at some examples of a Vertex structure.

typedef struct {
unsigned int color;
float x, y, z;
} Vertex;

That was quite painless now wasn't it? In this vertex struct we designated for it to hold values for the vertex position, as well as its color value. This is relatively simple and will only produce some basic results in the way of 3D graphics. Of course we want to harness the full capability of the PSP's hardware and be able to squeeze as much graphics out of this small beast that we can.

On to some more examples...

typedef struct {
float u, v;
unsigned int color;
float x, y, z;
} Vertex;

Here all we have done is added the ability to store texture coordinates. One more quick example.

typedef struct {
float u, v;
unsigned int color;
float nx, ny, nz;
float x, y, z;
} Vertex;

Ah, there we go the mighty Vertex struct in all its glory complete with normals. With this baby we are packing some serious heat and we now have the ability to position our vertices in world space, texture them, and supply the data (normals) needed for lighting and many other things.

There are varying degrees of precision that we can use for each data type in our Vertex struct. As the programmer, we have the ability to mix and match different type-specifiers for varying degrees of precision. On the PSP, we can have types ranging from 8-bits, to 16-bits, to even 32-bit precision. If you don't know, or remember, the different type-specifier, its size and range, don't worry, here is a handy little table that should sort everything out for you.

Type Size Range
unsigned char 8 bits 0 to 255
char 8 bits -128 to 127
unsigned int 16 bits 0 to 65,535
short int 16 bits -32,768 to 32,767
int 16 bits -32,768 to 32,767
unsigned long 32 bits 0 to 4,294,967,295
long 32 bits -2,147,483,648 to 2,147,483,647
float 32 bits 1.17549435 * (10^-38) to 3.40282347 * (10^+38)

Wow, that much freedome is a nice thing to have. Let's take a quick look at an example struct using some different type-specifiers

typedef struct {
float u, v;
unsigned int color;
char nx, ny, nz;
float x, y, z;
} Vertex;

There we use 8-bit signed normals. The flexibility allows the user to create a structure to suite the needs of his/her program. Keep in mind though which types you choose to use for your Vertex struct. We will need to recall these for later. If you don't know why you need to remember your types, don't worry, in a little bit you will.

I think it is high time we learned to put our new Vertex struct into use

Vertex Arrays

With a vertex struct there are a few way to store data and access it for manipulation and rendering, but we will look at, in my opinion, the simplest way. It just so happens to be in our favor that this is also the fastest way for the PSP hardware to decipher raw vertex data. Let's get started.

In order to use our array we must first know how many vertices we will have so we can properly allocate the amount of memory that we will need for our list of vertices. We can do this in c/c++ using malloc (Memory ALLOCation -- malloc) and with sizeof to create space in memory that meet our needs.

Vertex * vertexList;
int numVerts = 3;
vertexList = (Vertex*)malloc(sizeof(Vertex) * numVerts);

That's it; another painless bit of code! What we did here was create a pointer of type Vertex, and then we designated the number of vertices we would have and then allocated the proper amount of memory.

I know that you might be thinking, "Hey, we could just have used a constant instead of declaring and integer variable and we would have saved ourselves some space." Worry not! The reason I presented it this way to you is because when you get into more complex 3D graphics programming you will often want to create dynamic meshes where you don't have a constant vertex number. With this method we have laid the groundwork for some flexibility and usefulness for the programmer.

Just remember that indexing is 0 based so treat this array with respect and do not wander out of bounds or you will receive errors.

vertexList[0].color = GU_COLOR(0.0f, 0.0f, 1.0f, 0.0f);
vertexList[0].x = 0.0f;
vertexList[0].y = 0.0f;
vertexList[0].z = 0.0f;

NOTE: If you plan on texturing your mesh you must set the vertex color to GU_COLOR(1.0f, 1.0f, 1.0f, 1.0f) to have the texture look correct and produce desired results.

Piece of cake!

PSP Draw Array

Here is where the rubber meets the road! With that said, I will cover the following material thoroughly. Most of the info listed here can be found in the unofficial PSP SDK (URL at bottom of page). I will go over some basic use and explain a few of its parameter just because you need to have some knowledge of it before I dive into explaining the data cache.

void sceGuDrawArray(int prim, int vtype, int count, const void * indices, const void * vertices)

Parameters:

Even though the primitive types can found in the PSP SDK, I will be nice and list them here for you as well.

If you have had any experience with OpenGL or PSP GU (Graphics Utility) you should recognize these already. I will however cover a few of the vertex types since it is crucial do pass vertex types that correspond with one's vertex structure.

Rewind back a bit to when we started talking about the vertex structure and it's order. For every piece of data in our vertex struct we must pass the correct vertex type to the render call. These flags are ORed together to compose the final vertex format which the hardware can interpret. If you do not understand what I just said, fear not, a bit of code should clear things up

typedef struct {
unsigned int color;
float x, y, z;
} Vertex;

Vertex * vertexList;
int numVerts = 3;
vertexList = (Vertex*)malloc(sizeof(Vertex) * numVerts);

vertexList[0].color = GU_COLOR(0.0f, 0.0f, 1.0f, 0.0f);
vertexList[0].x = 10.0f;
vertexList[0].y = 10.0f;
vertexList[0].z = -1.0f;
vertexList[1].color = GU_COLOR(0.0f, 0.0f, 1.0f, 0.0f);
vertexList[1].x = 20.0f;
vertexList[1].y = 30.0f;
vertexList[2].z = -1.0f;
vertexList[2].color = GU_COLOR(0.0f, 0.0f, 1.0f, 0.0f);
vertexList[2].x = 30.0f;
vertexList[2].y = 10.0f;
vertexList[2].z = -1.0f;

sceGumDrawArray(GU_TRIANGLES, GU_COLOR_8888|GU_VERTEX_32BITF|GU_TRANSFORM_3D, numVerts, 0, vertexList);

See, that's wasn't too bad. All we did was create a vertex struct which holds data for (x, y, z) coordinates and color. We then allocated memory for 3 vertices since we will be building a triangle. Finally we make the render call.

Most of the draw array should be self-explanatory. Notice how we passed structure information in the vertex type parameter that corresponds with our vertex struct. What happens here is each bit flag is ORed together to get the final result. The 0 that follows the numVerts in the draw array just means that we have a null pointer to an indexed list (i.e. we don't have an indexed list). Indexed lists are optional, but if used they can save a lot of space. The drawback of using indexed lists however is that they are slower. So it all boils down to the age old war of Speed vs. Size. As for indexed lists, they are another time, another tutorial.

This next bit that I am about to tell you is quite important. Recall earlier how I told you to remember what type-specifier that you used in your struct. When we defined out Vertex struct we specifically stated that we wanted to use 32-bit vertex positions (float). This is the highest degree of precision that the PSP hardware allows for. So we must make sure to let the PSP hardware know that we want to use 32-bit vertices passing GU_VERTEX_32BITF to the draw array. This applies for all other vertex types as well. So if we were to use declare that our (u, v) (texture coordinates) as floats, then we would have to pass the flag for 32-bit textures to the draw array. GU_TEXTURE_32BITF And, if we chose to use 8-bit normals, we would have to tell the draw array that we did so. GU_NORMAL_8BIT The flag dealing with transforms, GU_TRANSFORM_3D, just specifies that the coordinates are transformed before they are passed to the rasterizer.

There are specific flags for 8-bit, 16-bit, and 32-bit vertex types.

Again, you can find the flag types listed in the PSP SDK.

Data Cache

Now we finally arrived to the real crux of this document.

My main reason for writing all of this was because of the bugs caused by a faulty design with the data cache and the rest of memory on the PSP. What I am about to tell you will save your hours, and possibly even days of searching for bugs only to find out that there is some out-of-date data being used.

Let's continue.

The cache is a small piece of fast memory which keeps copies of pieces of main memory, which is access in a quick and easy manner by the CPU. The PSP's CPU (MIPS r4000 32-bit core 1-333mhz) is in charge of running all your code. Since the CPU is much faster than any of the memory it is connected with, a cache is used to allow for greater memory read/write speeds. The data cache (16kib D-Cache) itself is just a block of memory, 16kib in size, located on the CPU, which allows for fast memory access directly from the CPU. This is a great design which can be seen in use on most computing system. However, problems may arise when data in memory and data in the cache are not in sync.

The GU reads data out of the PSP's system ram, and if something was written to memory, there is a good chance that it may still be sitting in the CPU's cache. Problem!!! Due to this, users can experience anything from streaking triangles, geometric disfiguration, textures missing pixels and being all screwed up, or general funky errors. But, fear not, there is a way in which we can combat this evil.

What users can do is force the cache to write back memory. To enable use of these functions, we must use the proper include.

#include <pspkernel.h>

The function call that will allow us to write back memory in a specified range is:

void sceKernelDcacheWritebackRange(const void * addr, unsigned int size)

Parameters:

Lucky for us we have all the necessary data for this function call. All we need to do is add one more line of code to our previous example and we will be safe.

sceKernelDcacheWritebackRange(vertexList, numVerts * sizeof(Vertex));
sceGumDrawArray(GU_TRIANGLES, GU_COLOR_8888|GU_VERTEX_32BITF|GU_TRANSFORM_3D, numVerts, 0, vertexList);

That's all there is too it. Remember how I said earlier it is a good idea to use a variable for the number of vertices? Here we see a bit of flexibility. If we ever need to change the number of vertices, for instance if we want to draw something other than a triangle, all we need to do is change the vertex count and we don't have to mess with any of these function calls. There is another command that forces a write back of all invalid data from the cache.

sceKernelDcacheWritebackInvalidateAll();

You wouldn't want to use this very often is it could slow down your program, but I would suggest putting this line somewhere in the beginning of your code when you first initialize just to be on the safe side.

Closing Thoughts

I really hope that this guide was helpful. If you have any questions or notice any mistakes please email me.

If you want to learn more about the PSP's hardware, PSP Function Calls, or become a Cache Guru check out the sources listed below. Thanks for reading!

back