PS2 Programming Log

PS2 Logo

My working log for learning programming for the Playstation 2. The goal is to have an abstraction/library layer that will be cross platform for the Playstation 1 as well, to leverage my existing psyq_lib stuff, hence the repository name is 'GameLib' currently

The source code is currently here: https://bitbucket.org/williamblair/gamelib

08/22/2020

The ps2sdk is set up, and have a basic Render/Model/Mesh setup. It was easier to set up a virtual machine with Linux to install the SDK, which you can then install via a single script from here: https://github.com/ps2dev/ps2dev. (I really should just re-install Linux on my machine to dual boot!) The code is adapted from the teapot example included with the ps2sdk.

The main loop is currently quite simple:

/*
 * GameLib test program
 */

#include "PS2Includes.h"
#include "PS2Renderer.h"
#include "PS2Model.h"

#include "mesh_data.c"

int main(int argc, char **argv)
{
    PS2Renderer Renderer;

    MeshData TeapotMeshData;
    PS2Model TeapotModel;

    Renderer.Init();
    
    TeapotMeshData.Normals = normals;
    TeapotMeshData.Colors = colours;
    TeapotMeshData.Vertices = vertices;
    TeapotMeshData.Indices = points;
    TeapotMeshData.VertexCount = vertex_count;
    TeapotMeshData.IndexCount = points_count;
    TeapotModel.SetMeshData( TeapotMeshData );

    for (;;)
    {
        TeapotModel.Rotate( 0.0f, 0.01f, 0.0f );
        Renderer.DrawModel( TeapotModel );

        Renderer.Update();
    }

    // Sleep
    SleepThread();

    // End program.
    return 0;

}

Which is shown running in pcsx2 and on a PS2 Slim:

The PS2 is running the ELF file over ethernet via ps2link and ps2client. ps2link is running on the PS2 off of the memory card (thanks to Free McBoot), which in turn is started via uLaunchElf. The IP configuration in order to find the PS2 over TCP from your computer is set in the file IPCONFIG.DAT

On the pc side, ps2client runs by specifying the PS2's IP address, then specifying to run your ELF file on the Emotion Engine (the PS2 main CPU) via execee

Current TODOs include lighting, camera, and controller management. The lighting and camera are currently hard coded in the renderer.

The following links were helpful for getting started:

08/23/2020

Added Camera and Controller classes (for the gamepad), as well as a simple floor mesh/model. The gamepad code I copied from another PS2 C++ class I had made when I was experimenting with PS2SDK years ago: https://github.com/williamblair/PS2Pad. Additionally, the Camera class I adapted from my learnopengl code for FPS camera movement: https://github.com/williamblair/learnopengl/tree/master/07_fps_camera. It felt good to see the fruits of previous labor ripen into use!

The camera movement/rotation calculation isn't optimized and probably could be using VU0 vector math; currently each x/y/z camera point is calculated individually.

Interestingly, moving around the world you can see the geometry freak out in places; I think this is because I need to manually do clipping and back face culling, which I read in Glampert's PS2 page: https://glampert.com/2015/03-23/ps2-homebrew-hardware-and-ps2dev-sdk/

The resulting main code is a little longer now but still not totally complicated:

/*
 * GameLib test program
 */

#include "PS2Includes.h"
#include "PS2Renderer.h"
#include "PS2Model.h"
#include "PS2Camera.h"
#include "PS2Controller.h"

#include "mesh_data.c"

... // Floor class impl is here

static void RotateCamera( Controller& Pad, Camera& Cam )
{
    int RightStickX = Pad.GetRightJoyX();
    int RightStickY = Pad.GetRightJoyY();
    const float Scale = 0.0001f;
    
    if (RightStickX > 150 || RightStickX < 100)
    {
        RightStickX -= 127; // scale from 0..255 to -128, 127
        Cam.Rotate( 0.0f, -Scale * (float)RightStickX, 0.0f );
    }
    
    if (RightStickY > 150 || RightStickY < 100)
    {
        RightStickY -= 127;
        Cam.Rotate( -Scale * (float)RightStickY, 0.0f, 0.0f );
    }
}

static void TranslateCamera( Controller& Pad, Camera& Cam )
{
    int LeftStickX = Pad.GetLeftJoyX();
    int LeftStickY = Pad.GetLeftJoyY();
    const float Scale = 0.01f;
    
    if (LeftStickX > 150 || LeftStickX < 100)
    {
        LeftStickX -= 127; // scale from 0..255 to -128, 127
        Cam.Translate( Scale * (float)LeftStickX, 0.0, 0.0f );
    }
    
    if (LeftStickY > 150 || LeftStickY < 100)
    {
        LeftStickY -= 127;
        // negative is forwards
        Cam.Translate( 0.0f, 0.0f, Scale * (float)LeftStickY );
    }
}

int main(int argc, char **argv)
{
    PS2Renderer Render;
    PS2Camera Cam;
    MeshData TeapotMeshData;
    PS2Model TeapotModel;
    PS2Floor Floor;
    PS2Controller Pad;

    SifInitRpc(0);

    Render.Init();
    Render.AttachCamera( Cam );
    
    TeapotMeshData.Normals = normals;
    TeapotMeshData.Colors = colours;
    TeapotMeshData.Vertices = vertices;
    TeapotMeshData.Indices = points;
    TeapotMeshData.VertexCount = vertex_count;
    TeapotMeshData.IndexCount = points_count;
    TeapotModel.SetMeshData( TeapotMeshData );

    Floor.Init();
    
    Pad.Init(0);

    for (;;)
    {
        Pad.Update();
        RotateCamera( Pad, Cam );
        TranslateCamera( Pad, Cam );
        
        TeapotModel.Rotate( 0.0f, 0.01f, 0.0f );
        Render.DrawModel( TeapotModel );
        
        //Floor.Rotate( 0.01f, 0.0f, 0.0f );
        Render.DrawModel( Floor );

        Render.Update();
    }

    // Sleep
    SleepThread();

    // End program.
    return 0;

}

Here's the result in PCSX2 and via PS2Client:

08/24/2020

Added a Light class. The renderer could use some more work - currently the lights are fixed/unchangeable at runtime. When the light is attached to the renderer, its data is copied into the internal renderer data array. Removing lights would require moving data within the renderer array. Additionaly, the renderer should use its own index for adding to the array instead of relying on the light's auto-assigned number to use as an index, otherwise the first array enter may not be filled but instead could start with an offset, which would break lighting calculation.

The main code is still straightforward, at least:

/*
 * GameLib test program
 */

#include "PS2Includes.h"
#include "PS2Renderer.h"
#include "PS2Model.h"
#include "PS2Camera.h"
#include "PS2Controller.h"

#include "mesh_data.c"

...

int main(int argc, char **argv)
{
    PS2Renderer Render;
    PS2Camera Cam;
    MeshData TeapotMeshData;
    PS2Model TeapotModel;
    PS2Floor Floor;
    PS2Controller Pad;
    PS2Light Lights[4];

    Lights[0].SetDirection(  0.00f,  0.00f,  0.00f );
    Lights[1].SetDirection(  1.00f,  0.00f, -1.00f );
    Lights[2].SetDirection(  0.00f,  1.00f, -1.00f );
    Lights[3].SetDirection( -1.00f, -1.00f, -1.00f );
    Lights[0].SetColor( 0.00f, 0.00f, 0.00f );
    Lights[1].SetColor( 1.00f, 0.00f, 0.00f );
    Lights[2].SetColor( 0.30f, 0.30f, 0.30f );
    Lights[3].SetColor( 0.50f, 0.50f, 0.50f );
    Lights[0].SetType( Light::AMBIENT );
    Lights[1].SetType( Light::DIRECTIONAL );
    Lights[2].SetType( Light::DIRECTIONAL );
    Lights[3].SetType( Light::DIRECTIONAL );

    SifInitRpc(0);

    Render.Init();
    Render.AttachCamera( Cam );
    Render.AttachLight( Lights[0] );
    Render.AttachLight( Lights[1] );
    Render.AttachLight( Lights[2] );
    Render.AttachLight( Lights[3] );
    
    TeapotMeshData.Normals = normals;
    TeapotMeshData.Colors = colours;
    TeapotMeshData.Vertices = vertices;
    TeapotMeshData.Indices = points;
    TeapotMeshData.VertexCount = vertex_count;
    TeapotMeshData.IndexCount = points_count;
    TeapotModel.SetMeshData( TeapotMeshData );

    Floor.Init();
    
    Pad.Init(0);

    for (;;)
    {
        Pad.Update();
        RotateCamera( Pad, Cam );
        TranslateCamera( Pad, Cam );
        
        TeapotModel.Rotate( 0.0f, 0.01f, 0.0f );
        Render.DrawModel( TeapotModel );
        
        //Floor.Rotate( 0.01f, 0.0f, 0.0f );
        Render.DrawModel( Floor );

        Render.Update();
    }

    // Sleep
    SleepThread();

    // End program.
    return 0;

}

11/11/2020

Investigating Glampert's PS2Dev programs (repo here: https://bitbucket.org/glampert/ps2dev-tests/src/master/) to see how they're implemented. Got the cubes application to compile after a few changes (presumably this was needed due to updates/more modern ps2sdk changes)

First, replaced fio* IO functions with c standard library ones (fioOpen() with open(), fioLseek with lseek(), etc.)

Second, removed the virtual CameraBase deconstructor and deconstructor from the thirdperson camera classes, due to the following error: Undefined reference to '__cxa_pure_virtual' with regards to the third person camera deconstructor

Third, removed `-lmf` from the compiler flags as mf wasn't found

After building, the result running in pcsx2 was having controller issues:

So I replaced the game controller pad implementation with my own I had created a while back: https://github.com/williamblair/PS2Pad

Success! The result:

My forked version of Glampert's repo is here: https://bitbucket.org/williamblair/ps2dev-tests/src/master/

11/15/2020

Finished the main setup/system initialization re-writing of Glampert's cube demo. This only required part of the Renderer class to be implemented along with the RenderPacket class.

I had an issue where would never pass 'draw_wait_finish()', however after removing some printf statements it worked. I assume then that the PS2/library are sensitive to timing with regards to hardware status, which I guess makes sense:

dma_wait_fast();
dma_channel_send_chain(DMA_CHANNEL_GIF, currentFramePacket->GetQwordPtr(), currentFramePacket->GetDisplacement(currentFrameQwPtr), 0, 0);

// V-Sync wait
graph_wait_vsync();
draw_wait_finish();

The above worked, while below did not:

printf("end frame dma wait fast\n");
dma_wait_fast();
printf("end frame dma wait channel send chain\n");
dma_channel_send_chain(DMA_CHANNEL_GIF, currentFramePacket->GetQwordPtr(), currentFramePacket->GetDisplacement(currentFrameQwPtr), 0, 0);

// V-Sync wait
printf("end frame wait vsync\n");
graph_wait_vsync();
printf("end frame draw wait finish\n");
draw_wait_finish();

The result isnt very exciting yet, just a brown screen:

The code can be found here: link

11/21/2020

Got a triangle drawn, continuing the glampert code re-implementation. This required addition of basic geometry renderering and the vector/matrix library. Originally I was stuck on thinking there was an error and the triangle wasn't getting drawn; however, it turned out to be that the triangle needed to be rotated 180 degrees about the Y axis due to the original code's camera placement. Without this rotation the triangle was back-facing and thus culled. My current re-write does not cull any triangles, so you can see when it spins you can see both sides

The code to render the triangle looks like this:

renderer.SetViewProjMatrix(gViewMatrix * gProjectionMatrix);

PS2Matrix trans, rotX, rotY;
trans.makeTranslation(0.0f, 0.0f, -3.0f);
rotX.makeRotationX(deg2rad(0.0f));
rotY.makeRotationY(deg2rad(triRotation));
triRotation += 1.0f;
if ( triRotation >= 360.0f )
{
    triRotation -= 360.0f;
}

renderer.SetModelMatrix(rotX * rotY * trans);
renderer.DrawIndexedTriangles(triIndi, 3, triVertices, 3);

11/22/2020

A group of cubes is now drawn instead of a single triangle. The cube data is stored in a Cube object (the vertices, texture coords, colors, and faces/indices). Each cube is drawn 3 times in a different location:

Cube gRedCube;
Cube gWhiteCube;
Cube gGreenCube;
float gCubeRotation = 0.0f;

static inline void DrawCubeGroup(const Cube & cube, const float y)
{
	for (u32 z = 0; z < 3; ++z)
	{
		for (u32 x = 0; x < 3; ++x)
		{
			PS2Matrix trans, rotX, rotY;
			trans.makeTranslation((x * 5.0f) - 5.0f, y, (z * -5.0f) - 20.0f);
			rotX.makeRotationX(deg2rad(gCubeRotation));
			rotY.makeRotationY(deg2rad(gCubeRotation));

			renderer.SetModelMatrix(rotX * rotY * trans);
			renderer.DrawIndexedTriangles(cube.indexes, Cube::INDEX_COUNT, cube.vertexes, Cube::VERT_COUNT);
		}
	}
}

Which looks like:

12/13/2020

Added texture support. With the way the renderering is currently set up, there is usually only room for a single texture at a time. Meaning, each time a different texture is used, it must be re-uploaded to vram and take the current texture's place.

Despite this, a texture atlas class is included to determine the available areas in VRam to upload the texture/keep track of the current vram use, however it isn't used (at least for this example). (I had to look up what a texture atlas was, but it's just a different name for a sprite sheet). This texture atlas in turn uses a custom/simple std::vector implementation called Array<>, which is interesting.

Besides the addition of the Texture and TextureAtlas classes, the renderer changes are mostly:

void SetTexture( const Texture& texture )
{
    // no need to change
    if ( &texture == currentTex )
    {
        return;
    }

    currentTex = (Texture*)&texture;
    // assert(currentTex->getPixels() != NULL);
    // assert(currentTex->getTexBuffer().address == uint(vramUserTextureStart));

    flushPipeline();
    //texSwitches++;

    // upload to GS ram
    const u32 width = currentTex->GetWidth();
    const u32 height = currentTex->GetHeight();
    const u32 psm = currentTex->GetPixelFormat();
    u8* pixels = (u8*)currentTex->GetPixels();

    qword_t* q = textureUploadPacket[frameIndex].GetQwordPtr();
    q = draw_texture_transfer( q, 
                               pixels, 
                               width, 
                               height, 
                               psm, 
                               vramUserTextureStart, 
                               width );
    q = draw_texture_flush( q );
    dma_channel_send_chain( DMA_CHANNEL_GIF, 
                            textureUploadPacket[frameIndex].GetQwordPtr(),
                            textureUploadPacket[frameIndex].GetDisplacement(q), 
                            0, 
                            0 );
    dma_wait_fast();
}

...

#define DRAW3D_PROLOGUE() \
BEGIN_DMA_TAG(currentFrameQwPtr); \
if (currentTex != nullptr) \
{ \
	setTextureBufferSampling(); \
} \
u64 * /*restrict*/ packetPtr = (u64 *)(draw_prim_start(currentFrameQwPtr, 0, &primDesc, &primColor))

...

void setTextureBufferSampling()
{
    // assert(currentTex != NULL);
    // assert(currentFrameQwPtr != NULL)
    // assert(currentTex->GetTexBuffer().address == u32(vramUserTextureStart));

    currentFrameQwPtr = draw_texture_sampling( currentFrameQwPtr, 
                                               0, 
                                               &currentTex->GetTexLod() );
    currentFrameQwPtr = draw_texturebuffer( currentFrameQwPtr, 
                                            0, 
                                            &currentTex->GetTexBuffer(), 
                                            &currentTex->GetTexClut() );
}

Then in main.cpp, you add:

// TODO - move this into CPP file or something
u32 Texture::vramUserTextureStart = 0;
...
renderer.SetPrimTextureMapping( true );
...
renderer.SetTexture( gTexture );
...

12/20/2020

Added basic 2D drawing, font drawing, and the ingame console. Now the TextureAtlas previously is used as the texture to store the font in. I think the builtin fonts are actual font files, as opposed to just images. There initially was a bug where I had made some small mistakes in the TextureAtlas code. Also, the 2D drawing seems to cause an error if you don't have enough commands sent or time spent or similar. I confirmed this in the original cubes demo code as well. Having the renderer draw "pre" doesn't work but anything at or above "pres" does

renderer.Begin2D();
{
    // doesn't work
	Vec2f pos = { 5.0f, 5.0f };
	Color4b textCol = { 255, 255, 255, 255 };
	renderer.DrawText(pos, textCol, FontManager::FONT_CONSOLAS_24, "Pre");

	rect.x = 10;
	rect.y = 50;
	rect.width = 100;
	rect.height = 100;
	color.r = 255;
	color.b = 0;
	color.g = 0;
	color.a = 255;
	renderer.DrawRectFilled( rect, color );
}
renderer.End2D();

vs.

renderer.Begin2D();
{
    // works
	Vec2f pos = { 5.0f, 5.0f };
	Color4b textCol = { 255, 255, 255, 255 };
	renderer.DrawText(pos, textCol, FontManager::FONT_CONSOLAS_24, "Pres");

	rect.x = 10;
	rect.y = 50;
	rect.width = 100;
	rect.height = 100;
	color.r = 255;
	color.b = 0;
	color.g = 0;
	color.a = 255;
	renderer.DrawRectFilled( rect, color );
}
renderer.End2D();

There is code to decompress the font and generate a bitmap image from their data. I should look at glampert's website to see if he discusses this. The code directs you via a comment to https://www.silverspaceship.com/inner/imgui/grlib/gr_extra.c, where a similar font decompression function is called grDecompressFont(). That however also doesn't document the expected input format. There is, however, a different font file in the same directory called "font_21", when, when added to the code does indeed work as well.


12/21/2020

Added the first person camera (minus the joystick integration). It's interesting to see the coordinate axis choices. In the current code, the camera looks towards positive z (the forward vector), the cubes are positioned around +20 z (they were positioned around -20 z previously, due to not having a camera and using an identity matrix as the view matrix), and moving the camera right takes a negative x direction (e.g. x -= 10 moves the camera right by 10)

The camera uses a system time counting object - GameTime - to calculate the delta and how much to move, via the clock() function. I'm curious to find out if the PS1 has something similar. I think the clock() function would be kernel/OS dependant, which I don't believe the PSX has.

Rotation is supported around the X axis (Pitch()) or the Y axis (Rotate()). The rotate function works by rotating each of the eye coordinate vectors (up, forward, right) by the input angle via basically 2D rotation matrix calculation. Pitch() is calculated by using a 3D rotation matrix around a generic axis vector, with in this case the axis being the right coordinate vector.

This time I ran on the actual PS2 just for fun:

12/26/2020

Untextured 3D model drawn. The model format is MD2, used Quake. Glampert lists that he obtained the models from here. Adding the textures should be pretty easy as well, just wanted to confirm the non-textured version worked first. Loading and drawing a model looks like this:

// generated via bin2c
#include "models/player_model.h"
#include "models/player_model_texture.h"
Md2Model playerMd2Model;
Md2AnimState playerAnimState; // not animated for now
//Texture playerTexture; // TODO
PS2Matrix playerModelMatrix;
// This pool of temporary vertexes is shared by all MD2 model instances.
// They are used to store the vertexes generated by animation blending.
static DrawVertex * tempMd2Verts = NULL;
static const u32 TEMP_MD2_VERT_COUNT   = 4096; // Default Quake MD2 max was 2048

static inline void initModel()
{
    const float scale = 0.025f;
    if (!playerMd2Model.InitFromMemory(player_model,
                                       size_player_model,
                                       scale))
    {
        printf("Failed to init model from memory\n");
        while (1);
    }
    // TODO
    // loadPermanentTexture(playerTexture, player_model_texture, size_player_model_texture, TEXTURE_FUNCTION_MODULATE);
    
    tempMd2Verts = (DrawVertex*)memalign(16, TEMP_MD2_VERT_COUNT*sizeof(DrawVertex));
    memset(tempMd2Verts, 0, TEMP_MD2_VERT_COUNT*sizeof(DrawVertex));
    
    playerAnimState.Clear();
    
    playerModelMatrix.makeIdentity();
}
static inline void drawModel()
{
    static Color4f colorTint = { 1.0f, 1.0f, 1.0f, 1.0f };
    Md2Model* model = &playerMd2Model;
    const u32 mdlVertCount = model->GetTriangleCount() * 3;
    if (mdlVertCount >= TEMP_MD2_VERT_COUNT)
    {
        printf("ERror = model vert count >= temp vert count\n");
        while (1);
    }
    
    model->AssembleFrame(/*playerAnimState.endFrame*/0,
                        256, 256, // player texture size width, height
                        colorTint,
                        tempMd2Verts);
    renderer.SetModelMatrix( playerModelMatrix );
    renderer.DrawUnindexedTriangles( tempMd2Verts, mdlVertCount );
}

Which gives us the result:

Another step is to try and reproduce the result on the PS1 as well

12/27/2020

Model drawn with textures as well now. This required loading the image (which was stored and compiled in memory as an array of bytes), then setting the renderer texture to said image before drawing:

static void loadPermanentTexture(Texture& texture,
                                 const u8* data,
                                 const u32 sizeBytes,
                                 const u8 textureFunction)
{
    ImageData img;
    if (!loadImageFromMemory(data,
                             sizeBytes,
                             img,
                             true)) // true = force rgba
    {
        printf("Failed to load texture from location %p\n", data);
        while (1);
    }
    // image should be r,g,b,a
    if (img.comps != 4)
    {
        printf("Converted image should be RGBA, but comps != 4\n");
        while (1);
    }

    // Texture isn't mipmapped
    lod_t lod;
    lod.calculation = LOD_USE_K;
    lod.max_level = 0;
    lod.mag_filter = LOD_MAG_LINEAR;
    lod.min_filter = LOD_MIN_LINEAR;
    lod.l = 0;
    lod.k = 0.0f;
    if (!texture.InitFromMemory(img.pixels,
                                TEXTURE_COMPONENTS_RGBA,
                                img.width, img.height,
                                GS_PSM_32,
                                textureFunction,
                                &lod))
    {
        printf("Failed to init image from memory!\n");
        while (1);
    }
    printf("Successfully loaded permanent texture\n");
}
...
Texture playerTexture;
...
static inline void initModel()
{
    const float scale = 0.025f;
    if (!playerMd2Model.InitFromMemory(player_model,
                                       size_player_model,
                                       scale))
    {
        printf("Failed to init model from memory\n");
        while (1);
    }
    loadPermanentTexture(playerTexture,
                         player_model_texture,
                         size_player_model_texture,
                         TEXTURE_FUNCTION_MODULATE);

    tempMd2Verts = (DrawVertex*)memalign(16, TEMP_MD2_VERT_COUNT*sizeof(DrawVertex));
    memset(tempMd2Verts, 0, TEMP_MD2_VERT_COUNT*sizeof(DrawVertex));

    playerAnimState.Clear();

    playerModelMatrix.makeIdentity();
}
static inline void drawModel()
{
    static Color4f colorTint = { 1.0f, 1.0f, 1.0f, 1.0f };
    Md2Model* model = &playerMd2Model;
    const u32 mdlVertCount = model->GetTriangleCount() * 3;
    if (mdlVertCount >= TEMP_MD2_VERT_COUNT)
    {
        printf("ERror = model vert count >= temp vert count\n");
        while (1);
    }

    model->AssembleFrame(/*playerAnimState.endFrame*/0,
                        256, 256, // player texture size width, height
                        colorTint,
                        tempMd2Verts);
    renderer.SetTexture( playerTexture );
    renderer.SetModelMatrix( playerModelMatrix );
    renderer.DrawUnindexedTriangles( tempMd2Verts, mdlVertCount );
}

01/01/2021

Animation with the MD2 model now working. It works by interpolating between the current model keyframe and the next model keyframe based on a floating point value between 0 and 1 (the percentage of the way through the anim). The current and next keyframe are updated based on the GameTime counter and the animation's set FPS (for the below image I set FPS to glampert's default MD2 fps of 7)

static void initAnimation(const StdMd2AnimId animId)
{
    u32 start, end, frameRate;

    Md2Model::GetStdAnim( animId, start, end, frameRate );
    playerAnimState.startFrame = start;
    playerAnimState.endFrame = end;
    playerAnimState.nextFrame = start + 1;
    playerAnimState.fps = /*frameRate*/MD2ANIM_DEFAULT_FPS;
}
static inline void drawModel()
{
    ...
    playerAnimState.Update(gTime.currentTimeSeconds, 
                           model->GetKeyframeCount());
    model->AssembleFrameInterpolated(playerAnimState.curFrame,
                                     playerAnimState.nextFrame,
                                     playerAnimState.interp,
                                     256, 256, // texture width, height
                                     colorTint,
                                     tempMd2Verts );
    renderer.SetTexture( playerTexture );
    renderer.DrawUnindexedTriangles( tempMd2Verts, mdlVertCount );
}

There are quite a few default MD2 animations available; so far I've only tried the first two:

// Standard MD2 animations, with predefined frame numbers and FPS.
enum StdMd2AnimId
{
    MD2ANIM_STAND,
    MD2ANIM_RUN,
    MD2ANIM_ATTACK,
    MD2ANIM_PAIN_A,
    MD2ANIM_PAIN_B,
    MD2ANIM_PAIN_C,
    MD2ANIM_JUMP,
    MD2ANIM_FLIP,
    MD2ANIM_SALUTE,
    MD2ANIM_FALL_BACK,
    MD2ANIM_WAVE,
    MD2ANIM_POINT,
    MD2ANIM_CROUCH_STAND,
    MD2ANIM_CROUCH_WALK,
    MD2ANIM_CROUCH_ATTACK,
    MD2ANIM_CROUCH_PAIN,
    MD2ANIM_CROUCH_DEATH,
    MD2ANIM_DEATH_FALL_BACK,
    MD2ANIM_DEATH_FALL_FORWARD,
    MD2ANIM_DEATH_FALL_BACK_SLOW,
    MD2ANIM_BOOM,

    // Number of entries in this enum. Internal use.
    MD2ANIM_COUNT
};

01/27/2021

Using third person camera. Ran into the issue where for some reason the program was crashing when the 2D console wasn't drawn. TODO - add a floor so we have some point of reference so it's not as confusing when we look and move around.

10/31/2021

Starting a cross-platform implementation of the game 2048. Planning on using SDL 1.2 to support PS2 and PS3 with relative ease (knock on wood!). For the base/PC version so far, I just have an image/atlas based font loaded and working, which compiles and runs for PS2 as well (except for the background color on the PS2 for some reason...).

In order to get the PS2 version working, I have the font image being loaded via the romfs PS2SDK port, which embeds a file system into the ROM/ELF itself for the PS2. I've found the current romfs library/genromfs tool built with my version of the PS2SDK requires changing the generated romdisk.c array name from 'romdisk' to 'romdisk_start' based on current version of genromfs I guess.

The font used is proportional_16x16 from opengameart.com

11/20/2021

Joystick initialization using SDL on the PS2 was freezing the system. Fixed by re-compling the PS2 SDL port (link) with the following change to use a different subsystem module:

diff --git a/sdl/src/Makefile b/sdl/src/Makefile
index ad1dbd2..8de569b 100755
--- a/sdl/src/Makefile
+++ b/sdl/src/Makefile
@@ -10,7 +10,8 @@ SDL_USE_HW_SURFACE=1
 HAVE_INPUT_DEVICES=0
 
 # Enable Multitap Support (Uses X* IOP modules)
-ENABLE_MTAP=1
+#ENABLE_MTAP=1
+ENABLE_MTAP=0
 
 # Enable FREESD instead of LIBSD
 USE_FREESD=1

Also, I needed to initialize the SDL timing subsystem (which wasn't needed on PC) (SDL_INIT_TIMER):

if ( SDL_Init( SDL_INIT_VIDEO|SDL_INIT_JOYSTICK|SDL_INIT_TIMER ) < 0 ) { 
    //LOG_ERROR( "Failed to int SDL" );
    printf( "Failed to init SDL\n" );
    return false;
}

After that, it's working the same as the PC version so far :)

11/21/2021

Got sound working, although it has a dependency on loading audsrv.irx from the host over ps2link currently. I tried the SDL mixer port but it wasn't working at all in pcsx2, and while the sample program worked on real hardware, In my code the audio got stuck stuttering on startup. The current ps2 audio implementation uses adpcm (.adp) audio following the audsrv example in samples/rpc/audsrv. TODO is possibly look at the .wav loading sample instead.

Also added more to the start menu screen, game over screen, and user score. Next TODO is to save/load high scores.

11/22/2021

Setup the project to create a .iso image and got a simple text file read from CD-ROM working, as well as loading the audio srv .irx module from a buffer in EE ram. Loading the audsrv.irx from EE RAM wasn't working before because there was a function call I was missing to patch the IOP allowing this. Figured that out thanks to this link.

To read from CD-ROM, the cdfs.irx IOP module needed to be loaded:

int irxRet = 0;
int ret = SifExecModuleBuffer(
    cdfs_irx,
    size_cdfs_irx,
    0,
    nullptr,
    &irxRet
);  

Where cdfs_irx and size_cdfs_irx are the contents and size of the cdfs.irx file converted into a byte array via bin2c. Loading this in the same place as the audsrv.irx however was causing the audio to not work, so for now it's placed in main under a test function. Loading a file is then done via

FILE* fp = fopen("cdfs:test.txt", "r");
...

With the 'cdfs:' prefix.

Creating a .iso image for the ps2 is pretty similar to how it's done for ps1. Create SYSTEM.CNF, which looks like

BOOT2 = cdrom0:\MAIN.ELF;1
VER = 1.0
VMODE = NTSC

and place it in a folder along with MAIN.ELF (or whatever executable), then you can run mkisofs:

mkisofs -o mygame.iso cdrom

Where cdrom is the folder containing the .elf and SYSTEM.CNF. Got the system.cnf info from here.

11/29/2021

PS2 Save Data to memory card working

Saved the high score unsigned integer in the unknown3 field in the mcIcon struct: https://ps2dev.github.io/ps2sdk/libmc_8h.html#structmc_icon, altough it looks like I could create a separate file no problemo. The files are stored in mc0:TW48, so it assumes use of memory card 0. TODO I could add option to format the memory card if it's unformatted, and give error messages to user graphically.

Made the following changes to the memory card sample in ps2sdk/samples/rpc/memorycard

diff --git a/ee/rpc/memorycard/samples/mc_example.c b/ee/rpc/memorycard/samples/mc_example.c
index 7450cd13..7f9e7d3d 100644
--- a/ee/rpc/memorycard/samples/mc_example.c
+++ b/ee/rpc/memorycard/samples/mc_example.c
@@ -14,7 +14,10 @@
 #include <kernel.h>
 #include <sifrpc.h>
 #include <loadfile.h>
-#include <fileio.h>
+//#include <fileio.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sjis.h>
 #include <malloc.h>
 #include <libmc.h>
 #include <stdio.h>
@@ -33,7 +36,8 @@ void LoadModules(void);
 int CreateSave(void);
 
 #define ARRAY_ENTRIES  64
-static mcTable mcDir[ARRAY_ENTRIES] __attribute__((aligned(64)));
+//static mcTable mcDir[ARRAY_ENTRIES] __attribute__((aligned(64)));
+static sceMcTblGetDir mcDir[ARRAY_ENTRIES] __attribute__((aligned(64)));
 static int mc_Type, mc_Free, mc_Format;
 
 int main() {
@@ -105,10 +109,10 @@ int main() {
 
        for(i=0; i < ret; i++)
        {
-               if(mcDir[i].attrFile & MC_ATTR_SUBDIR)
-                       printf("[DIR] %s\n", mcDir[i].name);
+               if(mcDir[i].AttrFile & MC_ATTR_SUBDIR)
+                       printf("[DIR] %s\n", mcDir[i].EntryName);
                else
-                       printf("%s - %d bytes\n", mcDir[i].name, mcDir[i].fileSizeByte);
+                       printf("%s - %d bytes\n", mcDir[i].EntryName, mcDir[i].FileSizeByte);
        }
 
        // Check if existing save is present
@@ -132,10 +136,10 @@ int main() {
 
                for(i=0; i < ret; i++)
                {
-                       if(mcDir[i].attrFile & MC_ATTR_SUBDIR)
-                               printf("[DIR] %s\n", mcDir[i].name);
+                       if(mcDir[i].AttrFile & MC_ATTR_SUBDIR)
+                               printf("[DIR] %s\n", mcDir[i].EntryName);
                        else
-                               printf("%s - %d bytes\n", mcDir[i].name, mcDir[i].fileSizeByte);
+                               printf("%s - %d bytes\n", mcDir[i].EntryName, mcDir[i].FileSizeByte);
                }
        }

Also, I used modules SIO2MAN, MCMAN, and MCSERV instead of XSIO2MAN, XMCMAN, and XMCSERV like in the sample. I tried the X... versions and they seemed to not initialize properly.

01/29/2022

Thinking about trying to port the code from Beginning OpenGL Programming. I got the Md2 model working (previously the Md2 model code was from G. Lampert's repository). I replaced the C++ STL code with C code as the ps2sdk doesn't support STL.

I also wrote a bunch of Math helpers similar to Sanjay Madhav's Game Programming in C++. I wrote custom Mat4 and Vec4 classes which essentially wrap the ps2sdk VECTOR and MATRIX classes:

// Compatible with VECTOR via *((VECTOR*)Vec4::v)
struct Vec4
{
    union
    {
        struct
        {
            float x;
            float y;
            float z;
            float w;
        };
        float v[4];
    };

    Vec4() :
        x(0.0f),
        y(0.0f),
        z(0.0f),
        w(1.0f)
    {}
    
    Vec4(float x, float y, float z, float w) :
        x(x),
        y(y),
        z(z),
        w(w)
    {}

    Vec4(const Vec4& other) {
        vector_copy(VECTORCAST(*this), VECTORCAST(other));
    }

    Vec4& operator=(const Vec4& other) {
        vector_copy(VECTORCAST(*this), VECTORCAST(other));
        return *this;
    }
    
    Vec4& operator+=(const Vec4& other) {
        x += other.x;
        y += other.y;
        z += other.z;
        w += other.w;
        return *this;
    }
    Vec4& operator-=(const Vec4& other) {
        x -= other.x;
        y -= other.y;
        z -= other.z;
        w -= other.w;
        return *this;
    }
} __attribute__((aligned(16)));

inline Vec4 operator*(const Vec4& lhs, const Vec4& rhs) {
    Vec4 result;
    vector_multiply(VECTORCAST(result), VECTORCAST(lhs), VECTORCAST(rhs));
    return result;
}
...
// Compatible with MATRIX via Mat4::v1d
struct Mat4
{
    union
    {
        struct
        {
            float row0[4];
            float row1[4];
            float row2[4];
            float row3[4];
        };
        float v1d[16];
        float v2d[4][4];
    };

    Mat4() { matrix_unit(MATRIXCAST(*this)); }
    Mat4(const Mat4& other) {
        matrix_copy(MATRIXCAST(*this), MATRIXCAST(other));
    }
    Mat4& operator=(const Mat4& other) {
        matrix_copy(MATRIXCAST(*this), MATRIXCAST(other));
        return *this;
    }
} __attribute__((aligned(16)));

inline Mat4 operator*(const Mat4& lhs, const Mat4& rhs) {
    Mat4 result;
    matrix_multiply(
        MATRIXCAST(result), MATRIXCAST(lhs), MATRIXCAST(rhs));
    return result;
}

Although noting that the Mat4 struct names (row0...) should be changed to col0... as the values are stored column-major not row-major.

I improved the book's Md2 model code as well by generating the list of animations from the file contents, instead of hard coding which frames go with which animation:

void Md2Model::genAnimations()
{
    animationsSize = countNumAnims();
    animations = (Animation*)malloc(animationsSize*sizeof(Animation));
    char curAnimName[16];
    memset((void*)curAnimName, 0, sizeof(curAnimName));
    size_t animIndex = 0;
    size_t c = 0;
    animations[0].startFrame = 0;
    animations[0].endFrame = 0;
    while (!(keyFrames[0].name[c] >= '0' && keyFrames[0].name[c] <= '9'))
    {
        animations[0].name[c] = keyFrames[0].name[c];
        curAnimName[c] = keyFrames[0].name[c];
        ++c;
    }
    animations[0].name[c] = '\0';
    curAnimName[c] = '\0';
    animations[0].loop = true;
    c = 0;
    for (size_t i = 1; i < keyFramesSize; ++i)
    {
        char frameAnimName[16];
        while (!(keyFrames[i].name[c] >= '0' && keyFrames[i].name[c] <= '9'))
        {
            frameAnimName[c] = keyFrames[i].name[c];
            ++c;
        }
        frameAnimName[c] = '\0';
        c = 0;
        if (strcmp(curAnimName, frameAnimName) != 0) {
            strcpy(curAnimName, frameAnimName);
            animations[animIndex].endFrame = i - 1;
            animIndex++;
            animations[animIndex].startFrame = i;
            animations[animIndex].loop = true;
            strcpy(animations[animIndex].name, curAnimName);
        }
    }
    animations[animationsSize-1].endFrame = keyFramesSize - 1;

    for (size_t i = 0; i < animationsSize; ++i)
    {
        printf("Anim name, start, stop: %s, %ld, %ld\n",
            animations[i].name, 
            animations[i].startFrame, animations[i].endFrame
        );
    }
}

Which assumes the keyframes names are ordered such as "walk1, walk2, walk3, idle001, idle002, ...". I tested out a different Md2 model than what the book provides, which I got from https://www.md2.sitters-electronics.nl/models.htm.

Porting the TARGA image parsing was pretty straightforward, just again converting the C++ STL functions/variables with C. The tricky part was figuring out how to use the texture data with PS2SDK. I started with the textured cube example included with PS2SDK, but that only uses a single texture. There seem's to be only enough room left in VRAM for a single texture to be loaded at a time, so switching between textures during drawing requires re-uploading it to VRAM, which I figured out based on looking at G. Lampert's repository. It might be possible to store more textures using 8-bit CLUT indexed instead of RGB24. To re-upload a texture during the draw loop, you need to end and upload the current DMA chain and wait for the texture to be uploaded before continuing. Also, textures are currently limited to 256x256px.

For file IO, instead of compiling everything into the executable, I used the cdfs IRX plugin. This worked in PCSX2, but I tried installing the ISO on my actual PS2 harddrive via HDLGAmeInstaller and it didn't seem to work - I just got a white screen during boot. So for testing on the real system, I used ps2link and loaded everything via the TCP connection (via filename "host:path/to/file." Also, I was getting weird triangle clipping and freakout to the center of the screen when testing on PCSX2, which doesn't happen on the actual hardware. During attempt to debug this on PCSX2 I don't draw the floor tiles which they are above a certain radius away from the model, which is why in below you see floor tiles popping in and out.

The final current result looks like so:

And the code can be found at: https://bitbucket.org/williamblair/gskittest/src/master/

01/31/2022

Redid the above gskittest code to use VU1 instead of VU0/EE in attempt to fix the triangle clipping issue from above. Based the code off of the ps2sdk VU1 sample: https://github.com/ps2dev/ps2sdk/tree/master/ee/draw/samples/vu1 .

The main issue I ran into was rendering the Md2 model due to its much higher triangle count compared to a cube. One of the vu1 packet functions turns out to have a maximum face count of 256: (packet2_vif.h)

/** 
 * Close UNPACK manually. 
 * In reality, get back to pointer "vif_code_opened_at" and 
 * fix num value with qwords counted from last packet2_vif_open_unpack().
 * @param packet2 Pointer to packet.
 * @param unpack_num Amount of data written to the VU Mem (qwords) or MicroMem (dwords). 
 * 256 is max value! 
 */
static inline void packet2_vif_close_unpack_manual(packet2_t *packet2, u32 unpack_num)
{
    assert(packet2->vif_code_opened_at != NULL);               // There is open UNPACK/DIRECT.
    assert(((u32)packet2->next & 0x3) == 0);                   // Make sure we're u32 aligned
    assert((packet2->vif_code_opened_at->cmd & 0x60) == 0x60); // It was UNPACK
    assert(unpack_num <= 256);
    packet2->vif_code_opened_at->num = (unpack_num == 256) ? 0 : unpack_num;
    packet2->vif_code_opened_at = (vif_code_t *)NULL;
}

So I ended up looping through the faces in chunks of 255 or less (since 255 is divisible by 3, so triangles are kept together):

void Renderer::DrawVertexBuffer(Mat4& modelMat, Mat4& viewMat, PS2::VertexBuffer& vb)
{
    texbuffer_t* t_texbuff = &texbuff;

    if (vb.mIndicesCount > MAX_FACES_COUNT ||
        vb.mVertexCount > MAX_FACES_COUNT)
    {
        printf("Invalid vertex buffer num indices or vertices %d, %d\n",
            vb.mIndicesCount, vb.mVertexCount);
    }

    VECTOR* vertices = (VECTOR*)vb.mVertices;
    VECTOR* sts = (VECTOR*)vb.mTexCoords;
    int faces_count = 0;
    if (vb.mIndicesCount > 0)
    {
        int* faces = vb.mIndices;
        faces_count = vb.mIndicesCount;
        int cur_face = 0;
        int faces_remain = faces_count;
        while (faces_remain > 0)
        {
            int num_faces = (faces_remain > 255 ? 255 : faces_remain);
            if (num_faces % 3 != 0) {
                printf("ERROR - num faces not mult of 3\n");
                return;
            }
            for (int i = 0; i < num_faces; ++i)
            {
                c_verts[i][0] = vertices[faces[cur_face]][0];
                c_verts[i][1] = -vertices[faces[cur_face]][1]; // negative because everything's upside down currently...
                c_verts[i][2] = vertices[faces[cur_face]][2];
                c_verts[i][3] = vertices[faces[cur_face]][3];

                c_sts[i][0] = sts[faces[cur_face]][0];
                c_sts[i][1] = sts[faces[cur_face]][1];
                c_sts[i][2] = sts[faces[cur_face]][2];
                c_sts[i][3] = sts[faces[cur_face]][3];

                ++cur_face;
            }
            // see ps2sdk/ee/include/packet2.h
            // 0 = do NOT clear memory
            packet2_reset(zbyszek_packet, 0);
            calculate_cube(t_texbuff, num_faces);
            draw_cube(
                MATRIXCAST(viewMat),
                MATRIXCAST(modelMat),
                t_texbuff,
                num_faces
            );
            
            faces_remain -= num_faces;
        }
    }

The calculate_cube and draw_cube functions don't actually do anything cube related; they're names leftover from the original sample code. TODO - rename them

Now we can draw the model and full floor tiles without clipping issues. And, the code seems to run faster/get higher FPS! Tested on both PCSX2 and hardware. On PCSX2, a couple of the model triangles aren't quite right, but seems to be fine on hardware.



02/01/2022

Got audio working in the above example. Was a bit more of a hassle than I thought. Basically did the same thing as for 2048 (using audsrv), but ran into a few snags:

  1. Needed to recompile the audsrv.irx file (using bin2c), using the one in /usr/local/ps2dev/ps2sdk/iop/irx. I originally copy and pasted the one from 2048 but it wasn't working (I was using a slightly different ps2sdk version)
  2. The audsrv.irx module load doesn't seem to work from cdfs; it needs to be loaded from host: or EE memory
  3. I had to lower the WAV file project rate to 8000Hz in Audacity before converting to adp. I saved the WAV file as 16 bit
    1. Convert via `adpenc <wav file> <adp result file>`
  4. I had to decrease the sound file length. Although the original file was less than 2MB (the size of SPU ram). The original length file worked in PCSX2 but not on real hardware.

Also, I didn't post the code for above yesterday: https://bitbucket.org/williamblair/ps2vu1test/src/master/

02/02/2022

Added the heightmap which loads a .raw image to set the height of each vertex. Only change I really had to make (besides replacing STL vector/ec. with C functions) was changing the texture coordinates to be within 0..1, as I didn't easily see how to make textures wrap:

float s = ((float)x / (float)mWidth) * 8.0f;
float t = ((float)z / (float)mWidth) * 8.0f;
while (s >= 1.0f) { s -= 1.0f; }
while (t >= 1.0f) { t -= 1.0f; }
mTexCoords[i].x = s;
mTexCoords[i].y = t;
mTexCoords[i].z = 1.0f; // q
mTexCoords[i].w = 0.0f;

You can see distance clipping in action as well in the result:

02/23/2022

Got a 2d texture drawing on the screen. Don't seem to need to disable the z-buffer (although I haven't tried drawing textures on top of each other). I estimated normalizing the draw location from 0..640/512 to NDC by trying values until they seemed to work fine. Although I'm sure there's a way to properly calculate it instead. The drawing uses the same 3D pipeline but with the mode/view/projection matrices all set to identity, with the vertex coordinates set manually based on the NDC calculation:

void DrawTexture(Texture& tex, int x, int y, int w, int h)
{
    MATRIX modelMat; // identity matrices
    MATRIX viewMat;
    MATRIX projMat;

    matrix_unit(modelMat);
    matrix_unit(viewMat);
    matrix_unit(projMat);
    
    SetTexture(tex);
    
    // disable z-buffering
    // TODO - not create packet each time
    /*packet2_t* zbufPacket = packet2_create(35, P2_TYPE_NORMAL, P2_MODE_NORMAL, 0);
    packet2_update(zbufPacket, draw_disable_tests(zbufPacket->next, 0, &z));
    packet2_update(zbufPacket, draw_finish(zbufPacket->next));
    dma_wait_fast();
    dma_channel_send_packet2(zbufPacket, DMA_CHANNEL_GIF, 1);
    draw_wait_finish();*/

    float xNorm = (float)x / 640.0f;
    float yNorm = ((float)y / 512.0f);
    float wNorm = (float)w / 640.0f;
    float hNorm = (float)h / 512.0f;
    float leftX = -0.156f + ((float)xNorm)*(0.156f*2.0f); // estimated numbers...
    float bottomY = -0.125f + ((float)yNorm)*(0.125f*2.0f);
    float rightX = leftX + (((float)wNorm)*(0.156f*2.0f));
    float topY = bottomY + (((float)hNorm)*(0.125f*2.0f));
    
    // put vertex data in zbyszek_packet
    // TODO - not create/set vectors each time
    int num_faces = 6;
    *((Vec4*)c_verts[0]) = Vec4( leftX, bottomY, 0.0f, 1.0f ); // bottom left
    *((Vec4*)c_verts[1]) = Vec4(  rightX, bottomY, 0.0f, 1.0f ); // bottom right
    *((Vec4*)c_verts[2]) = Vec4(  rightX,  topY, 0.0f, 1.0f ); // top right
    *((Vec4*)c_verts[3]) = Vec4( leftX, bottomY, 0.0f, 1.0f ); // bottom left
    *((Vec4*)c_verts[4]) = Vec4(  rightX,  topY, 0.0f, 1.0f ); // top right
    *((Vec4*)c_verts[5]) = Vec4( leftX,  topY, 0.0f, 1.0f ); // top left
    
    *((Vec4*)c_sts[0]) = Vec4( 0.0f, 0.0f, 1.0f, 0.0f ); // bottom left
    *((Vec4*)c_sts[1]) = Vec4( 1.0f, 0.0f, 1.0f, 0.0f ); // bottom right
    *((Vec4*)c_sts[2]) = Vec4( 1.0f, 1.0f, 1.0f, 0.0f ); // top right
    *((Vec4*)c_sts[3]) = Vec4( 0.0f, 0.0f, 1.0f, 0.0f ); // bottom left
    *((Vec4*)c_sts[4]) = Vec4( 1.0f, 1.0f, 1.0f, 0.0f ); // top right
    *((Vec4*)c_sts[5]) = Vec4( 0.0f, 1.0f, 1.0f, 0.0f ); // top left
    packet2_reset(zbyszek_packet, 0);
    calculate_cube(&texbuff, num_faces);
    
    // draw via VIF packet
    draw_cube(projMat, viewMat, modelMat, &texbuff, num_faces);
    
    // reenable zbuffering
    /*packet2_reset(zbufPacket, 0);
    packet2_update(zbufPacket, draw_enable_tests(zbufPacket->next, 0, &z));
    packet2_update(zbufPacket, draw_finish(zbufPacket->next));
    dma_wait_fast();
    dma_channel_send_packet2(zbufPacket, DMA_CHANNEL_GIF, 1);
    draw_wait_finish();
    packet2_free(zbufPacket);*/
}

Also, for some reason had to render the texture before the terrain, otherwise the texture would be clipped along with the terrain:

Final result drawing with x,y,w,h=50,50,100,100:

<-- Back to Home