Displaying 3D graphics on PSP

A couple of months ago, I again took out the dusty PSP from the box and decided to port my previously shown engine there . There are no problems with software rendering - everything works. But using GU, everything was not so simple. In this article, I will show by example how you can write a simple three-dimensional application for PSP using GU.



I warn you in advance that there are very few programming manuals for the PSP, and therefore some of my conclusions may turn out to be incorrect. But, to the point.



The main function of the program for PSP, if anyone knows, is made out like this:



#include <pspkernel.h> #include <pspdebug.h> #include <pspdisplay.h> //---------------------------------------------------------------------------------------- PSP_MODULE_INFO("GUTexture", 0, 1, 1); PSP_MAIN_THREAD_ATTR(THREAD_ATTR_USER|THREAD_ATTR_VFPU); void dump_threadstatus(void); bool done=false; int exit_callback(int arg1,int arg2,void *common) { done=true; return(0); } int CallbackThread(SceSize args, void *argp) { int cbid; cbid=sceKernelCreateCallback("Exit Callback",exit_callback,NULL); sceKernelRegisterExitCallback(cbid); sceKernelSleepThreadCB(); return(0); } int SetupCallbacks(void) { int thid = 0; thid=sceKernelCreateThread("update_thread",CallbackThread,0x11,0xFA0,0,0); if(thid>=0) sceKernelStartThread(thid, 0, 0); return(thid); } //---------------------------------------------------------------------------------------- //  //---------------------------------------------------------------------------------------- int main(int argc, char **argv) { pspDebugScreenInit(); //  SetupCallbacks(); //  ………. //   sceKernelExitGame(); return(0); }
      
      





GU initialization is as follows:



First, we request pointers to three buffers β€” screen, offscreen, and depth buffer (Z-buffer). The buffers are aligned to 512 pixels per line (although the PSP line has 480 pixels). You also need to consider the pixel color format. In this example, the GU_PSM_8888 format is used - 8 bits each for R, G, B and Alpha components of the pixel color. For Z-buffer, the GU_PSM_4444 format is used simply because it is 16 bits - the Z-buffer of the PSP 16 bit.



 //  #define SCREEN_WIDTH 480 #define SCREEN_HEIGHT 272 #define SCREEN_LINE_WIDTH 512 void* fbp0=getStaticVramBuffer(SCREEN_LINE_WIDTH, SCREEN_HEIGHT,GU_PSM_8888); void* fbp1=getStaticVramBuffer(SCREEN_LINE_WIDTH, SCREEN_HEIGHT,GU_PSM_8888); void* zbp=getStaticVramBuffer(SCREEN_LINE_WIDTH, SCREEN_HEIGHT,GU_PSM_4444);
      
      





The function of querying pointers to buffers is defined as



 #include <pspge.h> #include <pspgu.h> static unsigned int staticOffset=0; static unsigned int getMemorySize(unsigned int width,unsigned int height,unsigned int psm) { switch (psm) { case GU_PSM_T4: return((width*height)>>1); case GU_PSM_T8: return(width*height); case GU_PSM_5650: case GU_PSM_5551: case GU_PSM_4444: case GU_PSM_T16: return(2*width*height); case GU_PSM_8888: case GU_PSM_T32: return(4*width*height); default: return(0); } } void* getStaticVramBuffer(unsigned int width,unsigned int height,unsigned int psm) { unsigned int memSize=getMemorySize(width,height,psm); void* result=(void*)staticOffset; staticOffset+=memSize; return(result); } void* getStaticVramTexture(unsigned int width,unsigned int height,unsigned int psm) { void* result=getStaticVramBuffer(width,height,psm); return((void*)(((unsigned int)result) + ((unsigned int)sceGeEdramGetAddr()))); }
      
      





These are not my functions - I took them from some program a long time ago and only slightly changed it. The memory is distributed in the video memory area. Textures should also be placed where possible, requesting a pointer via getStaticVramTexture, otherwise the speed will drop dramatically. Of course, no dynamic memory is allocated for such requests, but simply a part of the specified PSP address space under the screen and textures is distributed. As far as I remember, the video memory of the PSP is only 2 megabytes - this is very little for storing multiple textures.



Programming GU in PSP is similar to programming for OpenGL with one difference - the execution of commands requires their placement in the display list, and the memory for this list must be allocated in advance and aligned:

static unsigned char __attribute __ ((aligned (16))) DisplayList [262144];

Commands related to coordinate transformations do not require a display list and can be executed anywhere in the program.



You can initialize GU, for example, like this:



 //    PSP #define VIRTUAL_SCREEN_SIZE 2048 //   #define SCREEN_ASPECT 16.0f/9.0f //   #define NEAR_PLANE_Z 5.0f //   #define FAR_PLANE_Z 4096.0f //  #define EYE_ANGLE 60.0f //  GU sceGuInit(); //        -    , .. GU_DIRECT sceGuStart(GU_DIRECT,DisplayList); //   -  ,    ,   (,   ) sceGuDrawBuffer(GU_PSM_8888,fbp0,SCREEN_LINE_WIDTH); //    -  ,   ,   sceGuDispBuffer(SCREEN_WIDTH,SCREEN_HEIGHT,fbp1,SCREEN_LINE_WIDTH); //   -           sceGuDepthBuffer(zbp,SCREEN_LINE_WIDTH); //      4096x4096 ( PSP    ) sceGuOffset(VIRTUAL_SCREEN_SIZE-(SCREEN_WIDTH/2),VIRTUAL_SCREEN_SIZE-(SCREEN_HEIGHT/2));//   //   -  -      sceGuViewport(VIRTUAL_SCREEN_SIZE,VIRTUAL_SCREEN_SIZE,SCREEN_WIDTH,SCREEN_HEIGHT); //      -      (     0  65535 !) sceGuDepthRange(65535,0); //        sceGuScissor(0,0,SCREEN_WIDTH,SCREEN_HEIGHT); sceGuEnable(GU_SCISSOR_TEST); sceGuEnable(GU_CLIP_PLANES); //   sceGumMatrixMode(GU_PROJECTION); sceGumLoadIdentity(); sceGumPerspective(EYE_ANGLE,SCREEN_ASPECT,NEAR_PLANE_Z,FAR_PLANE_Z); //      sceGuShadeModel(GU_SMOOTH); //   sceGuDepthFunc(GU_GEQUAL); sceGuEnable(GU_DEPTH_TEST); sceGuDepthMask(GU_FALSE); //   ,      sceGuFrontFace(GU_CCW); sceGuDisable(GU_CULL_FACE); //  sceGuDisable(GU_BLEND); sceGuBlendFunc(GU_ADD,GU_SRC_ALPHA,GU_ONE_MINUS_SRC_ALPHA,0,0); //   sceGuFinish(); sceGuSync(GU_SYNC_WAIT,GU_SYNC_FINISH); sceGuDisplay(GU_TRUE);
      
      





After you finish working with GU, sceGuTerm () should be called.



After loading a size texture (WidthImage; HeightImage) in any convenient way (Data pointer to texture data - and better to get it in the video memory area), we can display it on the screen.



  //  sceGuStart(GU_DIRECT,DisplayList); //     sceGuClearColor(0); sceGuClearDepth(0); sceGuClear(GU_COLOR_BUFFER_BIT|GU_DEPTH_BUFFER_BIT); //   sceGumMatrixMode(GU_PROJECTION); sceGumLoadIdentity(); sceGumPerspective(EYE_ANGLE,SCREEN_ASPECT,NEAR_PLANE_Z,FAR_PLANE_Z); sceGumUpdateMatrix();. //  sceGumMatrixMode(GU_TEXTURE); sceGumLoadIdentity(); sceGumMatrixMode(GU_VIEW); sceGumLoadIdentity(); sceGumMatrixMode(GU_MODEL); sceGumLoadIdentity(); //    sceGuColor(0xffffffff);//  sceGuEnable(GU_TEXTURE_2D); sceGuTexMode(GU_PSM_8888,0,0,0); sceGuTexImage(0,WidthImage,HeightImage,WidthImage,Data); sceGuTexFunc(GU_TFX_MODULATE,GU_TCC_RGBA); sceGuTexFilter(GU_NEAREST,GU_NEAREST); sceGuTexWrap(GU_REPEAT,GU_REPEAT); sceGuTexScale(1,1); sceGuTexOffset(0,0); //      … sceGuDisable(GU_TEXTURE_2D); //    sceGuFinish(); sceGuSync(GU_SYNC_WAIT,GU_SYNC_FINISH); //  ,     sceDisplayWaitVblankStart(); sceGuSwapBuffers();
      
      





How to display a polygon? To draw GU geometry, PSP asks to place all points in the array, a pointer to which you first need to get the sceGuGetMemory command, passing it the size of the requested memory block in bytes. Further along this pointer, you should write down an array of points and ask the PSP to output them, for example, using the sceGumDrawArray command with the necessary parameters. But what is the format of these points? PSP data points are arranged in a certain order and the size of the array describing a single point must be a multiple of 32 bytes: Vertex weight, texture coordinates, point color, normal to point, point coordinate. In that order. In order not to bother with the format, I defined a set of structures and functions for working with them:



 //#pragma pack(1) //[for vertices(1-8)] [weights (0-8)] [texture uv] [color] [normal] [vertex] [/for] #pragma pack(1) //  struct SGuVertex { float X; float Y; float Z; }; //   struct SGuNormal { float Nx; float Ny; float Nz; }; //  struct SGuTexture { float U; float V; }; //  struct SGuColor { unsigned long Color; }; #pragma pack() #pragma pack(32) //  , , ,  struct SGuNVCTPoint { SGuTexture sGuTexture; SGuColor sGuColor; SGuNormal sGuNormal; SGuVertex sGuVertex; }; #pragma pack() void SetVertexCoord(SGuVertex &sGuVertex,float x,float y,float z);//   void SetNormalCoord(SGuNormal &sGuNormal,float nx,float ny,float nz);//   void SetTextureCoord(SGuTexture &sGuTexture,float u,float v);//   void SetColorValue(SGuColor &sGuColor,unsigned long color);//  //---------------------------------------------------------------------------------------------------- //   //---------------------------------------------------------------------------------------------------- void CMain::SetVertexCoord(SGuVertex &sGuVertex,float x,float y,float z) { sGuVertex.X=x; sGuVertex.Y=y; sGuVertex.Z=z; } //---------------------------------------------------------------------------------------------------- //   //---------------------------------------------------------------------------------------------------- void CMain::SetNormalCoord(SGuNormal &sGuNormal,float nx,float ny,float nz) { sGuNormal.Nx=nx; sGuNormal.Ny=ny; sGuNormal.Nz=nz; } //---------------------------------------------------------------------------------------------------- //   //---------------------------------------------------------------------------------------------------- void CMain::SetTextureCoord(SGuTexture &sGuTexture,float u,float v) { sGuTexture.U=u; sGuTexture.V=v; } //---------------------------------------------------------------------------------------------------- //  //---------------------------------------------------------------------------------------------------- void CMain::SetColorValue(SGuColor &sGuColor,unsigned long color) { sGuColor.Color=color; }
      
      





Then you can set the geometry (in this case, the square), for example, as



  //  SGuNVCTPoint sGuNVCTPoint; vector<SGuNVCTPoint> vector_point; SetVertexCoord(sGuNVCTPoint.sGuVertex,-100,100,0); SetTextureCoord(sGuNVCTPoint.sGuTexture,0,0); SetNormalCoord(sGuNVCTPoint.sGuNormal,0,0,1); SetColorValue(sGuNVCTPoint.sGuColor,0xFFFFFFFF); vector_point.push_back(sGuNVCTPoint); SetVertexCoord(sGuNVCTPoint.sGuVertex,100,100,0); SetTextureCoord(sGuNVCTPoint.sGuTexture,1,0); SetNormalCoord(sGuNVCTPoint.sGuNormal,0,0,1); SetColorValue(sGuNVCTPoint.sGuColor,0xFFFFFFFF); vector_point.push_back(sGuNVCTPoint); SetVertexCoord(sGuNVCTPoint.sGuVertex,100,-100,0); SetTextureCoord(sGuNVCTPoint.sGuTexture,1,1); SetNormalCoord(sGuNVCTPoint.sGuNormal,0,0,1); SetColorValue(sGuNVCTPoint.sGuColor,0xFFFFFFFF); vector_point.push_back(sGuNVCTPoint); SetVertexCoord(sGuNVCTPoint.sGuVertex,-100,-100,0); SetTextureCoord(sGuNVCTPoint.sGuTexture,0,1); SetNormalCoord(sGuNVCTPoint.sGuNormal,0,0,1); SetColorValue(sGuNVCTPoint.sGuColor,0xFFFFFFFF); vector_point.push_back(sGuNVCTPoint);
      
      





And bring it, for example, like this:



  size_t vertex_amount=vector_point.size(); SGuNVCTPoint *sGuNVCTPoint_Ptr=(SGuNVCTPoint*)sceGuGetMemory(vertex_amount*sizeof(SGuNVCTPoint)); if (sGuNVCTPoint_Ptr!=NULL) { for(size_t n=0;n<vertex_amount;n++) sGuNVCTPoint_Ptr[n]=vector_point[n]; sceGumDrawArray(GU_TRIANGLE_FAN,GU_COLOR_8888|GU_VERTEX_32BITF|GU_TRANSFORM_3D|GU_NORMAL_32BITF|GU_TEXTURE_32BITF,vertex_amount,0,sGuNVCTPoint_Ptr); }
      
      





To display I have sceGumDrawArray function, what I paint and what is the format of the point (GU_COLOR_8888 | GU_VERTEX_32BITF | GU_TRANSFORM_3D | GU_NORMAL_32BITF | GU_TEXTURE_32BITF - point consists of color coordinates, normals, texture coordinates and requires multiplying the corresponding coordinates on the matrix before drawing). Drawing is possible only with triangles. But that is not all…



It seems that everything works, but it works only if all the points are before the eyes and visible. It should be at least one point to go into some foggy distance, as GU refuses to draw the entire polygon. As I understand it, GU for PSP requires that with respect to the four clipping planes (left, right, upper and lower (and the front one will turn out automatically)) the point should lie inside this volume, otherwise GU does not agree to display it. Problem. But in games, 3D graphics are present and no such artifacts are observed! Let's see how they solved this problem in PSP Quake 1, since the source code is available for analysis.



What do we see from source analysis? And in essence this:



  //   sceGumMatrixMode(GU_PROJECTION); ScePspFMatrix4 projection_matrix; sceGumStoreMatrix(&projection_matrix); //    sceGumMatrixMode(GU_VIEW); ScePspFMatrix4 view_matrix; sceGumStoreMatrix(&view_matrix); //   sceGumMatrixMode(GU_MODEL); ScePspFMatrix4 model_matrix; sceGumStoreMatrix(&model_matrix); sceGuFinish(); //   view-projection ScePspFMatrix4 projection_view_matrix; MultiplyScePspFMatrix4(view_matrix,projection_matrix,projection_view_matrix); //   view-projection-model ScePspFMatrix4 projection_view_model_matrix; MultiplyScePspFMatrix4(model_matrix,projection_view_matrix,projection_view_model_matrix); //  view-model ScePspFMatrix4 view_model_matrix; MultiplyScePspFMatrix4(model_matrix,view_matrix,view_model_matrix); //      (, , , ) ScePspFVector4 frustum[4];//   : ax+by+cz+d=0 // frustum[0].x=projection_view_model_matrix.x.w+projection_view_model_matrix.xx; frustum[0].y=projection_view_model_matrix.y.w+projection_view_model_matrix.yx; frustum[0].z=projection_view_model_matrix.z.w+projection_view_model_matrix.zx; frustum[0].w=projection_view_model_matrix.w.w+projection_view_model_matrix.wx; NormaliseScePspFVector4(frustum[0]); // frustum[1].x=projection_view_model_matrix.xw-projection_view_model_matrix.xx; frustum[1].y=projection_view_model_matrix.yw-projection_view_model_matrix.yx; frustum[1].z=projection_view_model_matrix.zw-projection_view_model_matrix.zx; frustum[1].w=projection_view_model_matrix.ww-projection_view_model_matrix.wx; NormaliseScePspFVector4(frustum[1]); // frustum[2].x=projection_view_model_matrix.xw-projection_view_model_matrix.xy; frustum[2].y=projection_view_model_matrix.yw-projection_view_model_matrix.yy; frustum[2].z=projection_view_model_matrix.zw-projection_view_model_matrix.zy; frustum[2].w=projection_view_model_matrix.ww-projection_view_model_matrix.wy; NormaliseScePspFVector4(frustum[2]); // frustum[3].x=projection_view_model_matrix.x.w+projection_view_model_matrix.xy; frustum[3].y=projection_view_model_matrix.y.w+projection_view_model_matrix.yy; frustum[3].z=projection_view_model_matrix.z.w+projection_view_model_matrix.zy; frustum[3].w=projection_view_model_matrix.w.w+projection_view_model_matrix.wy; NormaliseScePspFVector4(frustum[3]);
      
      





That is, in Quake 1, before outputting, they simply transfer all the points inside the volume that limits the view, or throw them away at all (if the whole figure is not visible). How to do it? You just need to read three matrices - GU_PROJECTION, GU_MODEL, GU_VIEW. Multiply them and get the final coordinate transformation matrix. From this matrix, you can pull out all the necessary limiting view of the plane (4 components of the obtained vector define the plane with the equation ax + by + cz + w = ​​0). (a, b, c) is the normal vector, and w = a * x0 + b * y0 + c * z0 - characterizes a certain point (x0, y0, z0) of the plane. We don’t need the coordinates of the point - it’s enough to know w



Clipping is performed as follows (for the four above-mentioned planes in turn in a cycle):



  //  vector<SGuNVCTPoint> vector_clip_point; for(long n=0;n<4;n++) { float nx=frustum[n].x; float ny=frustum[n].y; float nz=frustum[n].z; float w=frustum[n].w; Clip(vector_point,vector_clip_point,nx,ny,nz,w); vector_point=vector_clip_point; }
      
      





But for such a focus, we will need the following functions (written off from Quake 1):



 //---------------------------------------------------------------------------------------------------- //      //---------------------------------------------------------------------------------------------------- void CMain::GetIntersectionPlaneAndLine(const SGuNVCTPoint& A,const SGuNVCTPoint& B,SGuNVCTPoint& new_point,float nx,float ny,float nz,float w) { new_point=A; float ax=A.sGuVertex.X; float ay=A.sGuVertex.Y; float az=A.sGuVertex.Z; float au=A.sGuTexture.U; float av=A.sGuTexture.V; float bx=B.sGuVertex.X; float by=B.sGuVertex.Y; float bz=B.sGuVertex.Z; float bu=B.sGuTexture.U; float bv=B.sGuTexture.V; float dx=bx-ax; float dy=by-ay; float dz=bz-az; float du=bu-au; float dv=bv-av; float top=(nx*ax)+(ny*ay)+(nz*az)+w; float bottom=(nx*dx)+(ny*dy)+(nz*dz); float time=-top/bottom; float vx=ax+time*dx; float vy=ay+time*dy; float vz=az+time*dz; float vu=au+time*du; float vv=av+time*dv; //   SetVertexCoord(new_point.sGuVertex,vx,vy,vz); SetTextureCoord(new_point.sGuTexture,vu,vv); } //---------------------------------------------------------------------------------------------------- //   //---------------------------------------------------------------------------------------------------- void CMain::Clip(const vector<SGuNVCTPoint>& vector_point_input,vector<SGuNVCTPoint>& vector_point_output,float nx,float ny,float nz,float w) { vector_point_output.clear(); long point=vector_point_input.size(); for(long n=0;n<point;n++) { long next_p=n+1; if (next_p>=point) next_p-=point; const SGuNVCTPoint *sGuNVCTPoint_Current_Ptr=&(vector_point_input[n]); float current_vx=sGuNVCTPoint_Current_Ptr->sGuVertex.X; float current_vy=sGuNVCTPoint_Current_Ptr->sGuVertex.Y; float current_vz=sGuNVCTPoint_Current_Ptr->sGuVertex.Z; //     float current_ret=current_vx*nx+current_vy*ny+current_vz*nz+w; const SGuNVCTPoint *sGuNVCTPoint_Next_Ptr=&(vector_point_input[next_p]); float next_vx=sGuNVCTPoint_Next_Ptr->sGuVertex.X; float next_vy=sGuNVCTPoint_Next_Ptr->sGuVertex.Y; float next_vz=sGuNVCTPoint_Next_Ptr->sGuVertex.Z; //     float next_ret=next_vx*nx+next_vy*ny+next_vz*nz+w; if (current_ret>0)//   { if (next_ret>0)//   { vector_point_output.push_back(*sGuNVCTPoint_Next_Ptr); } else { //    SGuNVCTPoint sGuNVCTPoint_New; GetIntersectionPlaneAndLine(*sGuNVCTPoint_Current_Ptr,*sGuNVCTPoint_Next_Ptr,sGuNVCTPoint_New,nx,ny,nz,w); vector_point_output.push_back(sGuNVCTPoint_New); } } else//    { if (next_ret>0)//   { //    SGuNVCTPoint sGuNVCTPoint_New; GetIntersectionPlaneAndLine(*sGuNVCTPoint_Current_Ptr,*sGuNVCTPoint_Next_Ptr,sGuNVCTPoint_New,nx,ny,nz,w); vector_point_output.push_back(sGuNVCTPoint_New); //   vector_point_output.push_back(*sGuNVCTPoint_Next_Ptr); } } } }
      
      





And only after performing such a cut-off, finally, the output of three-dimensional graphics on the PSP with the help of GU will work correctly. You can create a game! :)







By the way, you can also use the PSP vector processor for the scalar product of vectors. For example, here's a function that determines whether clipping is required at all (torn out in pieces from the same Quake 1 for the PSP):



 //  vector<SGuNVCTPoint> vector_clip_point; //   PSP __asm__ volatile ( "ulv.q C700, %0\n" //    "ulv.q C710, %1\n" //    "ulv.q C720, %2\n" //    "ulv.q C730, %3\n" //    :: "m"(FrustumPlane[0]),"m"(FrustumPlane[1]),"m"(FrustumPlane[2]),"m"(FrustumPlane[3]) ); //   long vertex=vector_point.size(); bool clipping=false; for(long n=0;n<vertex;n++) { ScePspFVector4 current_vertex; current_vertex.x=vector_point[n].sGuVertex.X; current_vertex.y=vector_point[n].sGuVertex.Y; current_vertex.z=vector_point[n].sGuVertex.Z; current_vertex.w=1; float ret1,ret2,ret3,ret4; __asm__ volatile ( "ulv.q C610, %4\n" //      "vone.s S613\n" //       "vdot.q S620, C700, C610\n" // s620 =    "vdot.q S621, C710, C610\n" // s621 =    "vdot.q S622, C720, C610\n" // s622 =    "vdot.q S623, C730, C610\n" // s623 =    "mfv %0, S620\n" // out1 = s620 "mfv %1, S621\n" // out2 = s621 "mfv %2, S622\n" // out3 = s622 "mfv %3, S623\n" // out4 = s623 : "=r"(ret1), "=r"(ret2), "=r"(ret3), "=r"(ret4) : "m"(current_vertex) ); if (ret1<0 || ret2<0 || ret3<0 || ret4<0)//  { clipping=true; break; } }
      
      





Everything is simple - they placed the vector of the planes and the coordinates of the point in the registers and asked the VFPU to perform the scalar product.



β†’ Link to the simplest application that displays the texture



β†’ Link to PSP engine using GU



PS I know, there are programming professionals for PSP. Maybe they will tell why the PSP's GU is so structured and how to work with it correctly.



All Articles