VSP is a 4-bit planar image format designed for the graphics hardware of the PC98. It was used in pre-System 3.5 games, and continues to be supported in System 3.x (mostly to support ports of older games).
struct vsp_file {
LE16 x0; // x-coordinate (/8) of the top-left corner
LE16 y0; // y-coordinate of the top-left corner
LE16 x1; // x-coordinate (/8) of the bottom-left corner
LE16 y1; // y-coordinate of the bottom-left corner
BYTE r; // ???
BYTE bank; // default palette bank
BYTE palette[48]; // the palette
BYTE image[]; // the image data
};
The structure of a VSP file is as above. Note that the x-coordinates in the header should be multiplied by 8 to get the real display coordinates.
The image data itself is structured in 8 pixel wide columns, where each column contains the data for 4 planes, one after another. The reason each column is 8 pixels wide is that each bit in every byte represents part of the color index for a single pixel.
The data within a column-plane is compressed using a run-length encoding scheme and a few other tricks. Refer to the commented source code below (taken from xsystem35) to see how it works. The functions vsp_extract
and vsp_get_palette
convert the VSP image data to an 8-bit indexed bitmap.
struct cg_palette {
uint8_t red[16];
uint8_t green[16];
uint8_t blue[16];
};
struct cg {
int width; // width of the CG
int height; // height of the CG
uint8_t *pic; // the pixel data
struct cg_palette pal; // the color palette
};
struct vsp_header {
int x; // display location x
int y; // display location y
int width; // width
int height; // height
int bank; // default palette bank
int palette; // pointer to palette
int pixel; // pointer to pixel data
};
static void vsp_read_header(struct vsp_header *vsp, uint8_t *data)
{
vsp->x = LittleEndian_getW(data, 0);
vsp->y = LittleEndian_getW(data, 2);
vsp->width = LittleEndian_getW(data, 4) - vsp->x;
vsp->height = LittleEndian_getW(data, 6) - vsp->y;
vsp->bank = data[9];
vsp->palette = 0x0a;
vsp->pixel = 0x3a;
}
static void vsp_get_palette(struct cg_palette *pal, uint8_t *b)
{
for (int i = 0; i < 16; i++) {
pal->blue[i] = b[i * 3 + 0] << 4;
pal->red[i] = b[i * 3 + 1] << 4;
pal->green[i] = b[i * 3 + 2] << 4;
}
}
/*
* Extraction buffers. The planar image data is decompressed and read into
* these buffers before being converted to a chunky format.
*/
static uint8_t _bc[4][480];
static uint8_t _bp[4][480];
static uint8_t *bc[4]; // the current buffer
static uint8_t *bp[4]; // the previous buffer
/*
* Convert VSP planar image data to 8-bit indexed bitmap.
*/
static void vsp_extract(struct vsp_header *vsp, uint8_t *pic, uint8_t *b)
{
uint8_t b0, b1, b2, b3, mask = 0;
uint8_t *bt;
int n;
bp[0] = _bp[0]; bc[0] = _bc[0];
bp[1] = _bp[1]; bc[1] = _bc[1];
bp[2] = _bp[2]; bc[2] = _bc[2];
bp[3] = _bp[3]; bc[3] = _bc[3];
// for each column...
// NOTE: Every byte contains 8 pixels worth of data for single plane, so
// each column is actually 8 pixels wide.
for (int x = 0; x < vsp->width; x++) {
// for each plane...
for (int pl = 0; pl < 4; pl++) {
// for each row...
for (int y = 0; y < vsp->height;) {
// read a byte; if it's < 0x08, it's a command byte,
// otherwise it's image data
int c0 = *b++;
// copy byte into buffer
if (c0 >= 0x08) {
*(bc[pl] + y) = c0;
y++;
}
// copy n bytes from previous buffer to current buffer
// (compression for horizontal repetition)
else if (c0 == 0x00) {
n = (*b++) + 1;
memcpy(bc[pl] + y, bp[pl] + y, n);
y += n;
}
// b0 * n (1-byte RLE compression)
else if (c0 == 0x01) {
n = (*b++) + 1;
b0 = *b++;
memset(bc[pl] + y, b0, n);
y += n;
}
// b0,b1 * n (2-byte RLE compression)
else if (c0 == 0x02) {
n = (*b++) + 1;
b0 = *b++;
b1 = *b++;
for (int i = 0; i < n; i++) {
*(bc[pl] + y) = b0;
y++;
*(bc[pl] + y) = b1;
y++;
}
}
// copy n bytes from plane 0 XOR'd by the current mask
else if (c0 == 0x03) {
n = (*b++) + 1;
for (int i = 0; i < n; i++) {
*(bc[pl] + y) = (*(bc[0] + y) ^ mask);
y++;
}
mask = 0;
}
// copy n bytes from plane 1 XOR'd by the current mask
else if (c0 == 0x04) {
n = (*b++) + 1;
for (int i = 0; i < n; i++) {
*(bc[pl] + y) = (*(bc[1] + y) ^ mask);
y++;
}
mask = 0;
}
// copy n bytes from plane 2 XOR'd by the current mask
else if (c0 == 0x05) {
n = (*b++) + 1;
for (int i = 0; i < n; i++) {
*(bc[pl] + y) = (*(bc[2] + y) ^ mask);
y++;
}
mask = 0;
}
// set mask
else if (c0 == 0x06) {
mask = 0xff;
}
// not sure why this exists, padding maybe?
else if (c0 == 0x07) {
*(bc[pl] + y) = *b++ ;
y++;
}
}
}
// planar -> chunky (bitmap) conversion
for (int y = 0; y < vsp->height; y++) {
int loc = (y * vsp->width + x) * 8;
b0 = bc[0][y];
b1 = bc[1][y];
b2 = bc[2][y];
b3 = bc[3][y];
// NOTE: Half of every byte is wasted, since VSP is actually
// a 4-bit format.
pic[loc+0] = ((b0>>7)&1)|((b1>>6)&2)|((b2>>5)&4)|((b3>>4)&8);
pic[loc+1] = ((b0>>6)&1)|((b1>>5)&2)|((b2>>4)&4)|((b3>>3)&8);
pic[loc+2] = ((b0>>5)&1)|((b1>>4)&2)|((b2>>3)&4)|((b3>>2)&8);
pic[loc+3] = ((b0>>4)&1)|((b1>>3)&2)|((b2>>2)&4)|((b3>>1)&8);
pic[loc+4] = ((b0>>3)&1)|((b1>>2)&2)|((b2>>1)&4)|((b3 )&8);
pic[loc+5] = ((b0>>2)&1)|((b1>>1)&2)|((b2 )&4)|((b3<<1)&8);
pic[loc+6] = ((b0>>1)&1)|((b1 )&2)|((b2<<1)&4)|((b3<<2)&8);
pic[loc+7] = ((b0 )&1)|((b1<<1)&2)|((b2<<2)&4)|((b3<<3)&8);
}
// swap current/previous buffers
bt = bp[0]; bp[0] = bc[0]; bc[0] = bt;
bt = bp[1]; bp[1] = bc[1]; bc[1] = bt;
bt = bp[2]; bp[2] = bc[2]; bc[2] = bt;
bt = bp[3]; bp[3] = bc[3]; bc[3] = bt;
}
}
struct cg *vsp_load(uint8_t *data, int *error)
{
struct vsp_header vsp;
struct cg *cg = calloc(1, sizeof(struct cg));
// read header
vsp_read_header(&vsp, data);
// read palette
vsp_get_palette(&cg->pal, data + vsp.palette);
// allocate buffer for pixel data (+10: margin for broken CG)
cg->pic = malloc((vsp.width * 8 + 10) * (vsp.height + 10));
// convert VSP image data to 8-bit indexed bitmap
vsp_extract(&vsp, cg->pic, data + vsp.pixel);
cg->width = vsp.width * 8;
cg->height = vsp.height;
return cg;
}