Introduction
This is the first in a series of articles focused on programming the Playstation 2. In order to follow along with this tutorial, you will need a PS2 and the PS2 Linux Kit. Information on the Linux kit can be found at www.playstation2-linux.com, and can be purchased at www.playstation.com.
Programming the PS2 is initially very difficult, and at the time that I am writing this there are no comprehensive tutorials geared toward beginners. This article starts with the simplest possible program, and explains in detail what is happening in each step. However, I won't be going into detail on the C programming language or computer graphics, so it's best if you are already familiar with them.
The "Simple" First Program
To begin we will be writing a program that uses the Playstation 2's Graphics Synthesizer (referred to as GS from here on out) to display a single color on the screen.
#include //header file containing GS commands int g_fd_gs; //used to check if GS is opened ps2_gs_gparam *g_gp; //structure that keeps GS parameters ps2_gs_dbuff g_db; //structure for double buffering ps2_gs_finish g_finish; //structure to wait until drawing is done int acquire(); //functions used to acquire and release int release(); //the GS.
Here's the first part of the code. First off, you must include the ps2gs header file in order to program the GS. Next the integer g_fd_gs is created. For this program we will only be using it to verify that the GS is open and ready for us to program. For the curious, it is named g_fd_gs because in the future it is used as a file descriptor.Next a pointer to a ps2_gs_gparam structure is declared. Things like the resolution, video mode, width and height of drawing area, pixel ration, and other general parameters are stored in the ps2_gs_gparam structure. The structure ps2_gs_dbuff is used for double buffering, and the ps2_gs_finish structure contains the data used to make the GS wait until all commands are completed and all pixels are rasterized. Finally, two functions are declared to acquire and release the GS.
int main() { int frame; int r = 0, g = 0, b = 255; //choose your color (0 - 255) g_gp = ps2_gs_get_gparam(); //point *g_gp to the parameters acquire(); //acquire the GS
Okay, here we are in main. You'll see what frame is used for later, so I will point your attention at the r, g, and b integers. These of course, are used to set the color you want the screen to be. Mine is blue.As you hopefully recall, g_gp is a pointer to a structure that holds the parameters to the GS. What we want to do is make that pointer point at the actual parameters in use. That is where the ps2_gs_get_gparam function comes in. It returns a pointer to the parameters in use by the GS. By making our g_gp structure point to the actual parameters, we can later use other functions to alter those parameters. Now lets jump over to the acquire function definition, which is called next.
int acquire() { g_fd_gs = ps2_gs_open(-1); if(g_fd_gs < 0) return PS2_GS_VC_ACQ_FAILURE; ps2_gs_reset(0, PS2_GS_NOINTERLACE, PS2_GS_VESA, PS2_GS_FRAME,PS2_GS_640x480, PS2_GS_60Hz); return PS2_GS_VC_ACQ_SUCCESS; }
The first thing to address here is the ps2_gs_open function. This function opens dev/ps2gs and dev/ps2event. If the number you pass to ps2_gs_open is less then zero, you are telling it to open these to devices on its own. If you pass it a number greater than zero, it takes it as a handle to one of these device drivers. For those not familiar with Linux, the dev folder contains device drivers.If ps2_gs_open returns a number less then zero, an error has occurred. Otherwise, we call ps2_gs_reset, which changes the settings in our ps2_gs_gparam *g_gp. The format for the ps2_gs_reset function is as follows.
ps2_gs_reset(int mode, int inter, int omode, int ffmode, int resolution, int refresh_rate)
mode can be 0 or 1. 0 resets everything, including the parameters in the ps2_gs_gparam structure. A 1 cancels drawing and destroys primitive data in the GS's buffer. If Mode is 1 (meaning you just want to cancel drawing, not reset the parameters) none of the following arguments do anything. In our program, we use a 0 to reset the parameters.inter can be PS2_GS_NOINTERLACE or PS2_GS_INTERLACE. It is used to tell the GS whether to display in interlace or non-interlace mode.
omode can be PS2_GS_VESA, for output to a computer monitor; PS2_GS_DTV, for output to Digital TV; PS2_GS_NTSC, for output to a NTSC television (US and Japan); or PS2_GS_PAL, for output to a PAL television (Europe).
ffmode is used if only if you are in interlace mode. It can be PS2_GS_FIELD, which scans every other line; or PS2_GS_FRAME, which scans every line.
resolution can be PS2_GS_640x480, PS2_GS_800x600, PS2_GS_1024x768, or PS2_GS_1280x1024 for VESA mode; PS2_GS_480P, PS2_GS_1080I, or PS2_GS_720P for DTV mode.
refresh_rate can be 0 for default (75Hz), PS2_GS_60Hz, or PS2_GS_75Hz.
For this example, if you want to output to a computer monitor, leave it as it is above. If you want to output to a TV, change PS2_GS_VESA to PS2_GS_NTSC (or PS2_GS_PAL if your in Europe). Feel free to play around with the other settings (or read up on them on some video related websites), but since all that is being displayed is a colored screen you won't notice much of a difference. The other settings will be explained further when we begin drawing other things to the screen and the settings make a difference. Below the ps2_gs_reset function, you can see that acquire then returns success. Now, lets return to main where acquire left off.
ps2_gs_set_dbuff(&g_db, PS2_GS_PSMCT32, g_gp->width,g_gp->height, PS2_GS_ZGREATER, PS2_GS_PSMZ24,1); ps2_gs_start_display(0);
It's time to set up the double buffer with ps2_gs_set_dbuff. Thankfully, this function is much shorter and has less options then ps2_gs_reset. ps2_gs_set_dbuff(ps2_gs_dbuff *db, int psm, int w, int h, int ztest, int zpsm, int clear)
*db is a pointer to the double buffer structure.psm stands for Pixel Storage Mode. It's basically the same as color depth on a PC. The psm defines how many colors there are, and how they are stored in memory. The list is as follows. PS2_GS_PSMCT32, PS2_GS_PSMCT24, PS2_GS_PSMCT16, PS2_GS_PSM16S.
w, h guess what, these are the width and height of the drawing area. Since the ps2_gs_gparam structure contains these values, you pass g_gp->width and g_gp->height.
ztest is the depth test method. PS2_GS_TEST_ZTST_NEVER doesn't do a depth test, PS2_GS_TEST_ZTST_ALWAYS will always draw a pixel regardless of what it's Z value is, PS2_GS_TEST_ZTST_GEQUAL will draw a pixel if its Z value is greater than or equal to the pixels in the Z buffer, and PS2_GS_TEST_ZTST_GREATER will draw a pixel if its Z value is greater than the pixels in the Z buffer.
zpsm is the storage format for the Z buffer. It defines the size and storage method of the Z buffer. PS2_GS_PSMZ32, PS2_GS_PSMZ24, PS2_GS_PSMZ16, and PS2_GS_PSMZ16S each set the storage format of the z buffer according to their name.
clear can be 0 to not clear, or 1 clear. The zero or one specifies whether or not to clear the frame and z buffer when ps2_gs_swap_dbuff() is called to swap buffers.
Considering that this program only displays a single colored screen, the color and zbuffer formats don't really make a difference. For this program, what we are mainly concerned with is sending the function our ps2_gs_dbuff structure, setting the width and height of the drawing area, and clearing the buffers when the double buffer is swapped. The Z and pixel storage modes, as well as the depth test will become very important once we start drawing primitives.
After resetting all the parameters of the GS with ps2_gs_reset() the display is stopped. A call to the ps2_gs_start_display(0) is made to start it back up again The only two arguments it takes are 1 and 0. 0 starts the display, 1 stops it.
for(frame = 0;frame < 200 ;frame++) { ps2_gs_sync_v(0); ps2_gs_swap_dbuff(&g_db,frame); *(__u64*)&g_db.clear0.rgbaq = PS2_GS_SETREG_RGBAQ(r,g,b,0,0); *(__u64*)&g_db.clear1.rgbaq = PS2_GS_SETREG_RGBAQ(r,g,b,0,0); ps2_gs_put_drawenv((frame & 1) ? &g_db.giftag1 : &g_db.giftag0); ps2_gs_set_finish(&g_finish); ps2_gs_wait_finish(&g_finish); } release(); return 0; }
This is the last block of code, not counting the simple release function. It is the main program loop. After doing all the initialization, this is where the "action" is. Frame is nothing more than our looping variable; you may set it to any number you like. Note that the larger the number, the longer your program will run. In my example, when frame is gets to 200, the loop ends and the program stops. You can leave out the frame < 200 altogether if you want and the program will run forever. Pressing Ctrl + C on the keyboard will stop it.Ps2_gs_sync_v blocks the program until the V-Blank period begins. This is the period between the bottom of one display and the top of the next. During V-Blank any changes to the display are not visible. You want the viewer to see change in the display (otherwise there would be no animation) but you don't want them to see the "act" of changing the display (if you swap buffers while display is visible, it will cause image distortion).
Now that the program is in sync with V-Blank, we can safely swap buffers without any visual defects. The ps2_gs_swap_dbuff function does that for us. It is defined as follows:
ps2_gs_swap_dbuff(ps2_gs_dbuff *db, int id)
dbuff is of course a pointer to the ps2_gs_dbuff structure we created earlier. The function has to know where our double buffer structure is in order to swap buffers.id is the ID of the buffer we want to display. Only its Least Significant Bit (LSB) is effective, because of this we can use a counter to swap buffers:
decimal binary frame = 0 0 1 1 2 10 3 11 4 100 As you can see the LSB changes between zero and one as the counter goes up, effectively switching between the first and second buffer.
The simplest action of the program is the most difficult to understand. The clearing color of the two buffers is set with PS2_GS_SETREG_RGBAQ, which takes our 3 colors, and alpha value, and q which is used for texture mapping. Since we aren't doing any blending or texture mapping, these values are unimportant and left at zero. What is with the *(__u64*)& ? See the explanation below.
The SETREGRGBAQ macro simply takes the colors used and the essentially lines up the data so that it can be stored in the rgbaq register correctly (or memory until sent to the GS). Now, SETREGRGBAQ aligns the bits to fit into 64-bit space, which is correct but structures have no type, so you typecast to a 64-bit area of memory. In this way, the structure will be stored just like a 64-bit variable. Since structures cannot be typecast, you take the address of the structure and make it a pointer to an u64. So this means that the address is no longer the address of the structure, but to a 64-bit variable. Now, we don't want to assign the value from SETRGBAQ to the address, so it is dereferenced, and then assigned.
Next up, the ps2_gs_put_drawenv draws the red background from the double buffer up to the screen. It uses a giftag (&g_db.giftag1 : &g_db.giftag0) to draw the screen. A giftag is a structure that contains information about what is being drawn. It is placed right before the actual data that is being drawn, it is then sent to the GS and the data following it is drawn to the screen. Our double buffer structure sets the data in the giftag automatically, we just send it to the ps2_gs_put_drawenv so it draws the red screen. The LSB of the frame variable is checked to be either true or false (1 or 0). If it is true then the red screen of buffer1 is drawn, if it is false the red screen of buffer 0 is drawn.
Finally, the end of the program. Ps2_gs_set_finish is used to tell the GS that we want to use our ps2_gs_finish structure to have it wait until all pixels are drawn. Then ps2_gs_wait_finish actually causes everything to stop until all the pixels are drawn to the screen, once everything is drawn the program continues. This really doesn't make much of a difference for now, considering all that is being drawn is a red background.
Lastly, the release function is called which simply checks if the GS has been opened, and if so closes it. It then assigns -1 to g_fd_gs and returns PS2_GS_VC_REL_SUCCESS. The actual code can be seen below in the entire code posting.
Minor Enhancements
The program in this example was made to be the simplest possible. In order to do so, a few minor enhancements were cut out. These extra procedures are used in the examples included with the kit, as well as in many example programs floating around the internet. In order for you to better understand more advanced programs, I will include these additions in the entire code posting below. The extra features will appear in bold, along with all the code from above. A short explanation of each will be given after the code posting.
#include int g_fd_gs; ps2_gs_gparam *g_gp; ps2_gs_dbuff g_db; ps2_gs_finish g_finish; int acquire(); int release(); int main() { int frame; int odev; int r = 0, g = 0, b = 255; g_gp = ps2_gs_get_gparam(); acquire(); ps2_gs_vc_graphicsmode(); ps2_gs_set_dbuff(&g_db, PS2_GS_PSMCT32, g_gp->width, g_gp->height, PS2_GS_ZGREATER, PS2_GS_PSMZ24,1); ps2_gs_start_display(0); ps2_gs_vc_enablevcswitch(acquire, release); for(frame = 0;frame < 200 ;frame++) { ps2_gs_vc_lock(); odev = !ps2_gs_sync_v(0); ps2_gs_set_half_offset((frame & 1) ? &g_db.draw1 : &g_db.draw0, odev) ps2_gs_swap_dbuff(&g_db,frame); *(__u64*)&g_db.clear0.rgbaq = PS2_GS_SETREG_RGBAQ(r,g,b,0,0); *(__u64*)&g_db.clear1.rgbaq = PS2_GS_SETREG_RGBAQ(r,g,b,0,0); ps2_gs_put_drawenv((frame & 1) ? &g_db.giftag1 : &g_db.giftag0); ps2_gs_set_finish(&g_finish); ps2_gs_wait_finish(&g_finish); ps2_gs_vc_unlock(); } release(); return 0; } int acquire() { g_fd_gs = ps2_gs_open(-1); if(g_fd_gs < 0) return PS2_GS_VC_ACQ_FAILURE; ps2_gs_reset(0, PS2_GS_NOINTERLACE, PS2_GS_NTSC, PS2_GS_FIELD, PS2_GS_640x480, PS2_GS_60Hz); return PS2_GS_VC_ACQ_SUCCESS; } int release() { if(g_fd_gs >= 0) { ps2_gs_close(); g_fd_gs = -1; } return PS2_GS_VC_REL_SUCCESS; }
The additions to the code do only two things. The first is enabling support for the virtual console, the second is a trick to make the resolution look doubled in interlaced mode. Virtual consoles are, well, virtual consoles. When you start up your ps2 with Linux, you can press Ctrl + Alt + F1 through Ctrl + Alt + F6 to switch in between virtual consoles. If you write your code without the addition of ps2_gs_vc_enablevcswitch the user cannot switch between virtual consoles. Ps2_gs_vc_lock locks the virtual console to the current one while settings are made and the screen is drawn, then ps2_gs_vc_unlock releases it so it can be switched again. Also, virtual consoles can have a text mode and a graphics mode. Since we are doing graphics, ps2_gs_vc_graphicsmode is used to set the virtual console to graphics mode.Odev receives the opposite of the return value from ps2_gs_sync_v. In interlace mode ps2_gs_sync_v returns 0 for an even field or 1 for an odd field. Then the opposite of that is passed to ps2_gs_set_half_offset. If when it gets to set offset it is a 0, nothing happens; if it is a 1, the image is shifted half a pixel. Doing this makes the resolution look to be doubled. The drawing environments from the double buffer are also alternately passed to ps2_gs_set_half_offset so that the may be offset.
Please note that the above additions are not required in order to program ps2 games. Using half offset will probably be the most important later, but using the virtual console simply isn't important when it comes to just PS2 programming.