Memory cards
A memory card holds 128 KB, as 15 blocks of 8 KB and a directory. The BIOS keeps a small file system on it: a file has a name of up to 20 characters and a whole number of blocks, and a program reads and writes it through the kernel's file functions, Open, Read, Write, Lseek and Close, with bu00: in front of the name for the card in the first slot and bu10: for the second. A file is read and written in sectors of 128 bytes.
A save file#
A save that other games, and the console's own card screen, can show follows a convention Sony set out (the save file format). Its name starts with the region and the game's product code, BA and a code for an American game, then a name of the game's own. Its first sector is a header: SC, how many frames its icon has, how many blocks the file takes, a title in full-width Shift-JIS, and the icon's 16 colours. The icon's frames come next, 16 by 16 pixels of 4 bits, a sector each, and then the game's data. yaroze.SaveHeader makes the header:
// A SaveHeader is the 128 bytes Sony's convention puts at the start of a
// save file, which the console's card screen and other games read to show
// it: "SC", the number of icon frames, the file's blocks, its title in
// Shift-JIS, and the icon's palette. The icon's frames, 16 by 16 pixels of
// 4 bits each, 128 bytes a frame, follow it, and then the game's data.
type SaveHeader struct {
// Frames is how many frames the icon has, 1 to 3, which the card
// screen shows in turn. Sony asked European games for 1.
Frames uint8
// Blocks is the file's size in blocks.
Blocks uint8
// Title is the name the card screen shows, in Shift-JIS, up to 32
// full-width characters. SetTitle makes one from ASCII.
Title [64]byte
// CLUT is the icon's palette, sixteen colours as a TIM's.
CLUT [16]uint16
}
A Net Yaroze program had no product code, so its saves used BA-NETYAROZE and a name of its own, as the member FAQ suggested.
The card example saves a number with a title and an icon drawn by its gen program and turned into a 4-bit TIM by png2tim, whose pixels and palette are already in the form the header wants:
// writeSave writes the number to the card, with a header and the icon.
func writeSave() bool {
h := yaroze.SaveHeader{Frames: 1, Blocks: 1}
h.SetTitle("PSXSDK CARD EXAMPLE")
clut := unsafe.Slice((*uint16)(unsafe.Pointer(iconImage.Clut)), 16)
copy(h.CLUT[:], clut)
h.Put(save[:yaroze.CardSector])
pixels := unsafe.Slice((*byte)(unsafe.Pointer(iconImage.Pixel)), yaroze.CardSector)
copy(save[yaroze.CardSector:], pixels)
data := save[2*yaroze.CardSector:]
clear(data)
copy(data, mark)
putInt(data[16:], number)
return yaroze.CardWrite(slot, saveName, save[:])
}
Writing and reading#
yaroze.CardWrite deletes the file if it is there, creates it with as many blocks as the data needs, writes it, and reads it back a sector at a time to see that it is there, and CardRead reads one:
// CardWrite creates the file name on the card in slot 0 or 1, as big as
// data needs in whole blocks, replacing one that is there, writes data,
// which must be a whole number of sectors, to it, and reads it back to see
// that it is there. It returns whether it is. A save file's data starts
// with its SaveHeader and icon.
func CardWrite(slot int, name string, data []byte) bool {
if len(data) == 0 || len(data)%CardSector != 0 {
return false
}
blocks := (len(data) + CardBlock - 1) / CardBlock
var path [32]byte
p := CardPath(path[:], slot, name)
Delete(p)
fd := Open(p, O_CREAT|int32(blocks)<<16)
if fd < 0 {
return false
}
Close(fd)
fd = Open(p, O_WRONLY)
if fd < 0 {
return false
}
n := Write(fd, &data[0], int32(len(data)))
Close(fd)
if n != int32(len(data)) {
return false
}
// Read it back a sector at a time, so that a card pulled out half way
// is caught here rather than when the save is loaded.
fd = Open(p, O_RDONLY)
if fd < 0 {
return false
}
ok := true
for i := 0; ok && i < len(data); i += CardSector {
ok = Read(fd, &cardCheck[0], CardSector) == CardSector && [CardSector]byte(data[i:]) == cardCheck
}
Close(fd)
return ok
}
The BIOS does the card's work, a sector at a time, during the vertical blanks, and the call returns when it is done, so a save of a few sectors takes a few frames. A program checks what it loads anyway: a card can be pulled out half way through a save, and a mark at the start of the data, as the example's PSXSDK CARD, says whether what was read is the program's.
Testing for a card#
TestCard says whether a card is in a slot, whether it is new since the last test, and whether it is formatted. It talks to the card over the port the controller shares, and while it does the controller reads as nothing pressed for a frame, so a program tests when it starts and when the player asks to save or load, not every frame. Format writes an empty directory, and a program asks the player before it does.
Sony's checklist (requirements) told magazine-disc programs not to touch memory cards at all, since a save's name should carry a product code. The example is for learning how cards work.
The emulator keeps each slot's card in memory for the run, and the example works with Sony's BIOS and with OpenBIOS alike.