Intro coding and CD-rom usage

Let’s go back to the old style and use this article’s topic to wrap some useful code. Start developing after a leap of faith.

This article is going to take the approach of 90s famous code samples, generally known as cracktros (crack+intro/s). These introductions are nothing more than a boot loader showing a few fancy effects on screen, credits, and usually an annoying tune to haunt your brain cells. We will keep those fancy effects and drop the rest, especially the freaking music, but we’ll also add some way to load data from disk rather than embed everything into the exe like the in previous demos.

Since we already discussed graphics more or less in detail in the previous articles, we should focus more or how the CD-rom unit works on PlayStation. Technical data on hand, the CD-rom works with two speed modes, which roughly translates to 150KB/s for single speed and 300KB/s for double speed. Most games rely entirely on double speed for loading data and streamed playback, but you might also want to use single speed in some cases. We will skip single speed for now as it’s not required for this very demo.

Getting to code, PSY-Q comes with two libraries for handling CD reads, one with the obvious name LibCD while the extended lib is called LibDS (no idea why DS, but whatevs). The main difference between these two libraries is that LibCD provides most basic/low-end access to the hardware. On the other hand, LibDS is an abstraction layer built on top of LibCD, which provides a number of handy functions for parallel request processing. This demo will explain how to operate LibCD, just to keep it simple enough; we don’t need really that much complexity for a simple cracktro, right?

Let’s jump into some actual code with a procedure to initialize the system with LibCD taken into account:

[code language=”CPP”]void InitSys()
// clear environment
// libcd
// some libgpu and libgte stuff
SetGeomOffset(RES_W / 2, RES_H / 2);
// pad protocol
// sound
// more graphics

There is a precise place for LibCD initializers as they tend to reset the SPU volume environment, which is why I left in there some SPU code even if the demo isn’t going to use it at all.

Before we move to some more code about loading data from disk, there is a little something to discuss first about LibCD and its limitations: beware if you are thinking of using the CD table of contents to locate all your files. This is because LibCD has a bug where a directory can’t be fully cached and that causes the function relying on name lookup (i.e. CdSearchFile()) to miserably fail on entries that are too far away to reach. To explain this technically, the libs only cache one sector of directory data, then they stop and ignore whatever is stored into following sectors. On top of this, the functions that look for names on disk can be a bit slow, especially if you use complex paths and try jumping around the disk a lot to grab some files (storing file positions in advance may work, but it’s still quite slow).

How do we solve limitations and performance issues at the root? We create a Virtual File System! Most of the time a VFS consists in a big file with all your data stored inside it, possibly with a header large enough to store all information on how to find your data on disk. There are other cases where the VFS is nothing more than LBA+size values stored somewhere in a hidden location of the disk, which the game knows exactly how to access and caches at boot to have instant access at any time. In either case, performance is preserved and you can address as many files as you need, even pull a few tricks if you can store separate tables to be loaded on demand to save some memory.

We will code the approach with a big file with allocation tables at the top and all user data following. We can try the faster approach, where we read files by calling them as enumerated entries, or with an alternative but slightly slower approach by names using hash tables. For the sake of keeping it simple, let’s see how the former method works. This would be a structure useful for the task:

[code language=”CPP”]typedef struct tagPackHeader
u32 count, // how many files we have in the package
sectors; // number of sectors used by the header

typedef struct tagPackFile
u32 lba, // relative position on disk
size; // size of this entry
} PACK_FILE;[/code]

Now to decide on how addressing and reserved space should work: by implementing these structures, a package could store a number of files, even if typically the best solution it to store it all into a sector (i.e. 2048 bytes) which gives us exactly 255 files for a package. Alternatively we could actually use the sector member of PACK_HEADER to read more sectors from the header and quickly bump up that max count to whatever we want. As for the actual handling of this data, what we want to do for startes is to search the file on disk with CdSearchFile() and keep it’s logical position. From there we cache one sector of the header and check how many other sectors are left, then we repeat the operation until all sectors are cached. With this out of the way, comes another decision: we can leave the allocation data as is or transform lba addresses into absolute values; in the latter case we simply add the package lba to all the entries in the header, otherwise we handle local lba with a reference value from the package. I would suggest the processing approach, since it avoids further remangling of data at run-time and tends to be less bloated.

[code language=”CPP”]#define MAX_FILES 1024 // way more than we’ll ever need

static PACK_FILE Pack_pool[MAX_FILES]; // where we store file allocators
static u8 buffer[2048]; // buffer for byte precise reads

static void Cd_read(u32 lba, u32 size, u32 *dest);

void Cd_init()
u32 base_lba;
CdlFILE f;
int i, si;
// a few useful pointers
const u32 *buf = (u32*)__packable;

// initialize libcd

// search for our big file
while (!CdSearchFile(&f, "\\BIGFILE.PAK;1"));
// MSF to lba
base_lba = CdPosToInt(&f.pos);

// cache first sector of the header
Cd_read(base_lba, 2048, (u32*)buf);
// check if we need more
if (pH->sectors > 1)
Cd_read(base_lba + 1, pH->sectors – 1, (u32*)&((u8*)buf)[2048]);

// copy processed indices to the internal table
for (i = 0, si = pH->count; i < si; i++)
Pack_pool[i].lba = pF[i].lba + base_lba;
Pack_pool[i].size = pF[i].size;

With this function out of the way we can move into the actual loading of sectors and files.

[code language=”CPP”]// read a file from BIGFILE.PAK
u32 Cd_load_file(u32 index, u32 *dest)
u32 lba, size;

lba = Pack_pool[index].lba;
size = Pack_pool[index].size;

Cd_read(lba, size, dest);

return size;

// perform raw reads on disk in blocking mode
void Cd_read(u32 lba, u32 size, u32 *dest)
u32 sectors;
CdlLOC loc;

// obtain sector count
sectors = GetAlign(size, 2048) / 2048;

// convert lba to MSF and seek
CdIntToPos(lba, &loc);
CdControlB(CdlSetloc, (u8*)&loc, NULL);

// byte precise read required
if (size % 2048)
// does it need sector reads first?
if (sectors)
// read sectors into destination
CdRead(sectors, dest, CdlModeSpeed);
CdReadSync(0, NULL);
// seek to last sectors
CdIntToPos(lba + sectors, &loc);
CdControlB(CdlSetloc, (u8*)&loc, NULL);
// read reminder in a separate buffer
CdRead(1, (u32*)buffer, CdlModeSpeed);
CdReadSync(0, NULL);
// copy reminder to destination
memcpy(&((u8*)dest)[sectors * 2048], buffer, size % 2048);
// read is exactly a multiple of sector size
CdRead(sectors, dest, CdlModeSpeed);
CdReadSync(0, NULL);

As you can see, Cd_read() works in blocking mode, which means the console can’t execute any operation in background while it’s loading from disk. Of course it is possible to use non-blocking code to achieve parallelism, but I’ll keep that for a more complex article; all this sample needs is just some basic load code with byte-precise reads (instead padded to sectors).

I think this is all you need to start using the CD-rom in a basic manner. Just in case, I added to the source code a packer that works in dual mode: one to create BIGFILE.PAK with the structure above, the other to merge multiple files into one entry of BIGFILE.PAK. The merger mode is useful if you need to load several small files in a row without unnecessarily killing the CD with a million seek to position requests. As for creating the ISO with all your files, you can use Pixel’s CD-tool or PSx CD-Gen.

Download source