Drawing a Texture

From dreamcast.wiki
Jump to navigation Jump to search

The dreamcast does not draw like a modern GPU, it doesn’t have a GPU at all. It has a PVR Core which uses deferred rendering. The way this works is that a small amount of memory inside the Dreamcast VRAM is set aside to hold vertex data. Instead of rasterizing each polygon as they are sent to the dreamcast and immediately drawing it to the destination, the Dreamcast instead caches all polygons upfront. Once all the vertecies for the polygons in the scene are collected, it renders the output in steps, one 32x32 pixel tiles at a time. This has a bunch of really cool added benefits, notably that you do not have to sort polygons for transparancies to work. Overdraw is also generally not a thing, since only the exact amount of pixels needed for the destination are drawn. Every single pixel drawn is depth tested against all polygons around it during rasterization. This means pixel perfect z-buffers!

Types of Polygons

There are 5 types of polygons that are rendered. They are the Opaque Polygons, the Opaque Modifier Polygons, the Translucent polygons, the Translucent modifier polygons, and punch-thru polygons. Opaque Polygons are fully visible, no holes in it. If it is texture mapped, then the texture cannot have an alpha channel in it, it must be only RGB. Translucent Polygons are what we would think of as true-color 32-bit in modern computing. It means colors are represented as ARGB, providing an alpha channel. When used on a texture mapped polygon, the individual alpha channels of pixels in the texture determine the visibility. Instead of per-polygon transparency, the dreamcast does per-pixel! Pixels can be transparent in a range depending on the bitdepth. An offshoot of Translucent Polygons are Punch-Thru polygons. Data-wise, these are basically the same as a translucent polygon, except any value in the alpha channel besides 0 is treated as 100% transparent. That means pixels are either entirely visible, or entirely transparent. It is said to “punch-through” because the transparent pixels look like holes punched through the polygon.

We need to determine how much VRAM space to reserve to hold references to these polygons. Since the output is divided into 32x32 tiles during render, every tile needs space to hold polygon references that keep track of which polygons reside in that tile. Polygon references in a tile are stored in a linked list known as the Object List. Nodes in the linked list can hold a set number of polygon references, and can point to each other for an arbitrary number of polygon references to be stored. These nodes are known as Object Pointer Blocks. We can set the size of each OPB for each polygon type with a KOS structure called pvr_init_params:

static pvr_init_params_t pvr_params = {
   /* OPB (Object Pointer Block) node: */
   {
       PVR_BINSIZE_32, /* Enable Opaque Poly OPB: 32 word (128 byte) length */
       PVR_BINSIZE_0, /* Disable Opaque Modifier Poly OPB  */
       PVR_BINSIZE_32, /* Enable Translucent Poly OPB: 32 word (128 byte) length  */
       PVR_BINSIZE_0, /* Disable Translucent Modifier Poly OPB  */
       PVR_BINSIZE_0  /* Disable Punch-thru Poly OPB*/},
   /* Vertex buffer size 512K */
   512 * 1024,
   /* DMA Enable: Off */
   0,
   /* FSAA Enable: Off*/
   0,
   /* Translucent Autosort Disable: Off */
   0,
   /* OPB Overflow count: Preallocates this many extra OPBs (sets of tile bins), allowing the PVR to use the extra space when there's too much geometry in the first OPB. */
   0
};

The available sizes for allocation for each bin are as follows:

PVR_BINSIZE_0: 0-length, which disables the list
PVR_BINSIZE_8: 8-word (32-byte) length
PVR_BINSIZE_16: 16-word (64-byte) length
PVR_BINSIZE_32: 32-word (128-byte) length

The number of references that can be stored is equal to the binsize. I.e. PVR_BINSIZE_8 can store 8 polygon references in this OPB node. In the above example, we’re allocating 128-bytes to the Opaque Polygon bin, and 128-bytes to the Translucent Polygon bin. With our VRAM memory allocated for our OPBs, it’s time to create some textures. Let’s define some pointers to vram memory in our source code as global variables:

pvr_ptr_t tex_blue;
pvr_ptr_t back_tex;

These pointers will hold the address of our textures in vram, making them our system handles for those textures. Let’s create a function that will let us allocate some memory for these textures in vram, then fill them up:

void textures_init(void) {
   tex_blue = pvr_mem_malloc(128 * 128 * 2);
   png_to_texture("/rd/blue.png", tex_blue, PNG_FULL_ALPHA);
   back_tex = pvr_mem_malloc(512 * 512 * 2);
   png_to_texture("/rd/background.png", back_tex, PNG_NO_ALPHA);
}

Our function begins by calling pvr_mem_malloc(). This function takes a number which represents how many bytes to allocate. We want enough room to hold all the pixels in our textures. Our Blue Texture is 128x128 pixels big, and the Dreamcast uses 16-bit color internally, despite supporting 24-bit color. 16-bits is the same as 2-bytes, which is where the 2 comes from in the above formula. Thus we need 2-bytes for each pixel, and there are 128x128 pixels. We can add the math into the parameters so it’s more readable. Pvr_mem_malloc returns the vram address where it allocated the space, which we save in our tex_blue pvr_ptr.

We do the same with back_tex. This time, our background.png image is 512x512 pixels big. We adjust the parameter to account, and store the pointer in the appropriate pvr_ptr.

We use each pointer to direct our png_to_texture function where to deposite the pixels it loads. png_to_texture opens a png file and returns the raw pixel data in a dreamcast pixel format. It takes a char* string that is the filepath to the png, along with a pvr_ptr that points to an allocated area of VRAM, and a flag. The flag determines how the function treats the png, and how it orders the data. There are 3 enums that an be set as the flag:

PNG_FULL_ALPHA: The png loaded has an alpha channel. It will be turned into ARGB4444.
PNG_NO_ALPHA: The png loaded has no alpha channel. It will be turned into RGB565. 
PNG_MASK_ALPHA: The png loaded has an alpha channel, but will be a punch-thru texture. It will be turned into ARGB1555.

We will need to know the pixel format of the texture in a moment, so take note of which flag you have set. We used FULL_ALPHA for the blue texture, and NO_ALPHA for the background texture.

We need a function to call that will actually Submit vertices to the Dreamcast and command it to draw. We’ll call that function DrawFrame(), although that’s a misnomer. It’s actually just submitting frame data, and the frame is drawn later. Regardless of semantics, let’s create such a function:

void DrawFrame(void)
{
   pvr_wait_ready();
   pvr_scene_begin();
       // Rendering code in here
   pvr_scene_finish();
}

Our DrawFrame function begins with pvr_wait_ready(). This is a function that gets the PVR ready to accept vertices. This means doing some internal house keeping, as well as generally waiting for vblank to start a new frame. That means calling pvr_wait_ready(); ends the previous frame in a loop and waits for a new one to begin. If you call this twice in a loop, then your loop will run at 30 FPS. If you call it 3 times, your loop will run at 20 FPS. And so forth.

After the PVR is ready, we have a pair of calls, pvr_scene_begin() and pvr_scene_end. Everything that goes in between these calls encapsulates a full frame of the scene being drawn. In between these calls, we can further breakdown which polygon bin we are going to be working on:

       pvr_list_begin(PVR_LIST_OP_POLY);
           // Vertex code here for Opaque polygons
       pvr_list_finish();

This is a pair of calls, pvr_list_begin(int) and pvr_list_finish(). pvr_list_begin takes an enum that tells it which list to bind. When a list is bound, all pvr functions will be directed towards that one. pvr_list_finish closes the bin for the scene, once a bin is closed it cannot be reopened until the next frame. In this example, we are opening the bin for opaque polygons, meaning any vertex we submit will go to that bin.

Inside our Opaque Polygon list functions, we can create vertices and submit them. We can also create a command to let the PVR know which texture we want to use when texture mapping the polygon. We need to first create some variables:

   pvr_poly_cxt_t cxt;
   pvr_poly_hdr_t hdr;
   pvr_vertex_t vert;

First, we need to create a variable to hold our polygon context. A context is an instance of the polygon parameter data, encapsulated into an object. This context is where we set which texture to draw with for the next polygon, along with the parameters for the texture. Once we have our polygon context defined, we can compile it into a pvr polygon header. A PVR Polygon Header is an object that represents a command to the PVR to setup the next polygon. It is the same size as an vertex, and thus behaves like one. You submit the polygon header the same way you submit a vertex. In fact, you can store a bunch of vertices into an array, and insert polygon headers into the array, and submit all the polygon data at once, which we’ll get into later. For now, let’s concentrate on submitting one vertex (or header) at a time.

Our actual vertices will be stored in a structure called pvr_vertex_t. The Dreamcast and KOS have a number of predefined vertex types, which have more or less attributes depending on rendering mode. The more features a vertex or render mode has, the more space it consumes. Vertices are either 32-bytes big, or 64-bytes big. A special case is a Sprite object, which is 4 vertices squished together into one 64-byte object, meaning you can define a sprite in one vertex instead of needing 4 vertices. The types of vertices available are: pvr_vertex_t: the generic, default vertex. We are going with the default type. When we submit vertices to the PVR, we will use this variable and fill out the fields inside of it as needed.

Let’s begin by creating our Polygon context. We want to draw the background texture to the entire screen. We can do that like so:

   pvr_poly_cxt_txr(	&cxt, 			/* Destination Context */
				PVR_LIST_OP_POLY, 	/* Flags */
				PVR_TXRFMT_RGB565, 	/* Texture Format */
				512, 				/* Texture Width */
				512, 				/* Texture Height */
				back_tex, 			/* TextureID */
			PVR_FILTER_BILINEAR);		/* Filtering */
   pvr_poly_compile(&hdr, &cxt);
   pvr_prim(&hdr, sizeof(hdr));

We need to create a polygon context for our background texture. We fill in the appropriate fields: this is going to the opaque polygon bin, the texture has no alpha channel so it’s RGB565, the width and height are 512x512, the pointer to the texture in memory is back_tex, and we will be using bilinear filtering. With this context object created, we compile it into our header. PVR_prim() is a function that takes a vertex or header file, and send its data over to the VRAM polygon bin. It takes a second parameter with a size to know how many bytes to send to VRAM. We send our header that tells our PVR that the next polygon renders with the back_tex using bilinear filtering as an opaque polygon.

Now, let’s set some general parameters for our vertices:

   vert.argb = PVR_PACK_COLOR(1.0f, 1.0f, 1.0f, 1.0f);
   vert.oargb = 0;
   vert.flags = PVR_CMD_VERTEX;

Our vertex contains a field for an argb definition, to give the vertex a diffuse color. We set it to pure white, 1.0 (100%) in RGBA. Our vertex also has an offset argb value, this is for specular highlights. We’ll leave it set to 0, unused. We set a command in the flag field: PVR_CMD_VERTEX. This tells the PVR to begin taking in vertices for a polygon strip. When a command is given telling the PVR that it’s the EOL for the strip, it’ll end the polygon and move onto a new one.

With our general parameters set, lets begin making the actual position data in the vertices and uploading them to VRAM:

   vert.x = 0.0f;
   vert.y = 0.0f;
   vert.z = 1.0f;
   vert.u = 0.0f;
   vert.v = 0.0f;
   pvr_prim(&vert, sizeof(vert));

each one of these sections is a single vertex. We are manually changing the XYZ coordinates of the vertex, along with the UV coordinates, before sending it to the PVR. This vertex is at 0,0,1.

   vert.x = 640.0f;
   vert.y = 0.0f;
   vert.z = 1.0f;
   vert.u = 1.0f;
   vert.v = 0.0f;
   pvr_prim(&vert, sizeof(vert));

The next vertex resides at 640,0,1. Our two vertices so far have drawn a 640-pixel straight line from the left side of the screen to the right side, at Y position 0, which is the very top.

   vert.x = 0.0f;
   vert.y = 480.0f;
   vert.z = 1.0f;
   vert.u = 0.0f;
   vert.v = 1.0f;
   pvr_prim(&vert, sizeof(vert));

Our next vertex is at 0,480,1. This has shot back to the left side of the screen, then all the way down to the bottom row of pixels, Y at 480. This has formed a triangle between our previous 3 vertices. However, we have not given the command that this the end of line for the triangle strip, so we can submit another vertex.

   vert.x = 640.0f;
   vert.y = 480.0f;
   vert.z = 1.0f;
   vert.u = 1.0f;
   vert.v = 1.0f;
   vert.flags = PVR_CMD_VERTEX_EOL;
   pvr_prim(&vert, sizeof(vert));

This final vertex is at 640,480,1. It’s at the very bottom right corner of the screen. Given the previous 3 vertices, this forms a second triangle, with the area of both filling the entire 640x480 screen. We pack a command into the flag of this final vertex, “PVR_CMD_VERTEX_EOL.” This tells the PVR that this strip of 4 vertices is a single long polygon that is finished. In the end, we wound up defining a screen-filling square and filled it with our back_tex. Place this call within our Main() loop and run the program, and we should get the background texture filling our screen:

   while(Game.Done == 0)
   {
       //Counter++;
       EvHandler.PollEvent(&Game);
       HandleInput(&Game);
       DrawFrame();    }

We have successfully loaded and displayed a texture. We will come back shortly and continue learning how to use the PVR. However, our program currently has no way of responding, every time we want to make a change we have to turn off the dreamcast and wait for it to boot back up. Let’s give ourselves the ability to communicate with our program using our gamepads.