joeyagi/agiPic.c
2026-10-06 16:59:41 -05:00

537 lines
17 KiB
C

// AGI v2 PICTURE decoding (AGI specification chapter 7).
//
// A picture is a byte stream of drawing actions, 0xF0..0xFA, each followed
// by argument bytes below 0xF0, ending at 0xFF. It draws into the visual
// buffer and the priority buffer (160x168, a byte per AGI pixel); each
// action draws to whichever of the two is enabled.
#include "agi.h"
#include <stddef.h>
#include <string.h>
#define PIC_FIRST_ACTION 0xF0u
#define PIC_SET_COLOR 0xF0u
#define PIC_NO_COLOR 0xF1u
#define PIC_SET_PRIORITY 0xF2u
#define PIC_NO_PRIORITY 0xF3u
#define PIC_Y_CORNER 0xF4u
#define PIC_X_CORNER 0xF5u
#define PIC_ABS_LINE 0xF6u
#define PIC_REL_LINE 0xF7u
#define PIC_FILL 0xF8u
#define PIC_SET_PEN 0xF9u
#define PIC_PLOT_PEN 0xFAu
#define PIC_END 0xFFu
// Relative line displacement byte: bit 7 the x sign, bits 4-6 x, bit 3
// the y sign, bits 0-2 y.
#define REL_X_SIGN 0x80u
#define REL_X_SHIFT 4u
#define REL_Y_SIGN 0x08u
#define REL_MAG_MASK 0x07u
// Pen byte: bit 5 splatter, bit 4 rectangle, bits 0-2 the size.
#define PEN_SPLATTER 0x20u
#define PEN_RECTANGLE 0x10u
#define PEN_SIZE_MASK 0x07u
#define PEN_SIZES 8u
#define PEN_MAX_ROWS 15u
#define PEN_ROW_MSB 0x80u
#define PEN_ROW_FULL 0xFFu
#define TEXTURE_BITS_LAST 254u
#define TEXTURE_CODE_SHIFT 1u
#define BIT_BYTE_SHIFT 3u
#define BIT_INDEX_MASK 7u
// Flood fill: a scanline fill with an explicit stack of row spans.
#define FILL_STACK_MAX 1024u
typedef struct {
uint8_t *visual;
uint8_t *priority;
bool visualOn;
bool priorityOn;
uint8_t color;
uint8_t priorityColor;
uint8_t pen;
} PicDrawT;
// ----- Prototypes -----
static void drawLine(const PicDrawT *d, int16_t x0, int16_t y0, int16_t x1, int16_t y1);
static void fill(const PicDrawT *d, int16_t x, int16_t y);
static bool fillable(const PicDrawT *d, uint16_t idx);
static uint16_t opAbsLine(const PicDrawT *d, const uint8_t *data, uint16_t length, uint16_t pos);
static uint16_t opCorner(const PicDrawT *d, const uint8_t *data, uint16_t length, uint16_t pos, bool yFirst);
static uint16_t opPlotPen(const PicDrawT *d, const uint8_t *data, uint16_t length, uint16_t pos);
static uint16_t opRelLine(const PicDrawT *d, const uint8_t *data, uint16_t length, uint16_t pos);
static void plot(const PicDrawT *d, int16_t x, int16_t y);
static void plotPen(const PicDrawT *d, int16_t x, int16_t y, uint8_t texture);
// ----- Tables -----
// The circle pens of each size, a row per byte with the leftmost pixel in
// bit 7: size n is n+1 wide and 2n+1 tall (specification 7.2).
static const uint8_t kCircleRows[PEN_SIZES][PEN_MAX_ROWS] = {
{ 0x80 },
{ 0xC0, 0xC0, 0xC0 },
{ 0x40, 0xE0, 0xE0, 0xE0, 0x40 },
{ 0x60, 0x60, 0xF0, 0xF0, 0xF0, 0x60, 0x60 },
{ 0x20, 0x70, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0x70, 0x20 },
{ 0x30, 0x78, 0x78, 0x78, 0xFC, 0xFC, 0xFC, 0x78, 0x78, 0x78, 0x30 },
{ 0x38, 0x7C, 0x7C, 0x7C, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0x7C, 0x7C, 0x7C, 0x38 },
{ 0x18, 0x3C, 0x7E, 0x7E, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x7E, 0x7E, 0x3C, 0x18 }
};
// Splatter pens: 256 bits of texture and each texture number's starting
// bit within them (specification 7.2).
static const uint8_t kTextureBits[] = {
0x20, 0x94, 0x02, 0x24, 0x90, 0x82, 0xa4, 0xa2,
0x82, 0x09, 0x0a, 0x22, 0x12, 0x10, 0x42, 0x14,
0x91, 0x4a, 0x91, 0x11, 0x08, 0x12, 0x25, 0x10,
0x22, 0xa8, 0x14, 0x24, 0x00, 0x50, 0x24, 0x04
};
static const uint8_t kTextureStart[] = {
0x00, 0x18, 0x30, 0xc4, 0xdc, 0x65, 0xeb, 0x48,
0x60, 0xbd, 0x89, 0x04, 0x0a, 0xf4, 0x7d, 0x6d,
0x85, 0xb0, 0x8e, 0x95, 0x1f, 0x22, 0x0d, 0xdf,
0x2a, 0x78, 0xd5, 0x73, 0x1c, 0xb4, 0x40, 0xa1,
0xb9, 0x3c, 0xca, 0x58, 0x92, 0x34, 0xcc, 0xce,
0xd7, 0x42, 0x90, 0x0f, 0x8b, 0x7f, 0x32, 0xed,
0x5c, 0x9d, 0xc8, 0x99, 0xad, 0x4e, 0x56, 0xa6,
0xf7, 0x68, 0xb7, 0x25, 0x82, 0x37, 0x3a, 0x51,
0x69, 0x26, 0x38, 0x52, 0x9e, 0x9a, 0x4f, 0xa7,
0x43, 0x10, 0x80, 0xee, 0x3d, 0x59, 0x35, 0xcf,
0x79, 0x74, 0xb5, 0xa2, 0xb1, 0x96, 0x23, 0xe0,
0xbe, 0x05, 0xf5, 0x6e, 0x19, 0xc5, 0x66, 0x49,
0xf0, 0xd1, 0x54, 0xa9, 0x70, 0x4b, 0xa4, 0xe2,
0xe6, 0xe5, 0xab, 0xe4, 0xd2, 0xaa, 0x4c, 0xe3,
0x06, 0x6f, 0xc6, 0x4a, 0x75, 0xa3, 0x97, 0xe1
};
// ----- Module state -----
// fill's seed stack, in byte coordinates (the picture is 160x168) to keep
// it small in the IIgs's bank 0.
static uint8_t gFillX[FILL_STACK_MAX];
static uint8_t gFillY[FILL_STACK_MAX];
// ----- Internal helpers (alphabetical) -----
// A line steps a pixel at a time along its longer side; the shorter side
// moves when the error it carries (starting at half the longer side's
// length, growing by the shorter side's each step) reaches the longer
// side's length -- the exact fraction rounded to nearest, an exact half
// rounding away from the start, so the last of the longer side + 1 points
// is the end point. Sierra's interpreter carries the error in a byte
// (AGI_QUIRK_LINE_BYTE_ERROR): when it passes 255 the step is lost, and a
// line long on both sides ends short of its end point (measured on 2.435:
// (0,0) to (159,167) stalls two rows in ten and ends at (134,167)).
static void drawLine(const PicDrawT *d, int16_t x0, int16_t y0, int16_t x1, int16_t y1) {
int16_t adx;
int16_t ady;
int16_t sx;
int16_t sy;
int16_t major;
int16_t minorLen;
int16_t majorX;
int16_t majorY;
int16_t minorX;
int16_t minorY;
int16_t i;
uint16_t err;
bool byteError;
adx = (int16_t)((x1 < x0) ? x0 - x1 : x1 - x0);
ady = (int16_t)((y1 < y0) ? y0 - y1 : y1 - y0);
sx = (int16_t)((x1 < x0) ? -1 : 1);
sy = (int16_t)((y1 < y0) ? -1 : 1);
if (adx > ady) {
major = adx;
minorLen = ady;
majorX = sx;
majorY = 0;
} else {
major = ady;
minorLen = adx;
majorX = 0;
majorY = sy;
}
minorX = (int16_t)(sx - majorX);
minorY = (int16_t)(sy - majorY);
byteError = agiQuirk(AGI_QUIRK_LINE_BYTE_ERROR);
err = (uint16_t)(major / 2);
for (i = 0; i <= major; i++) {
plot(d, x0, y0);
x0 = (int16_t)(x0 + majorX);
y0 = (int16_t)(y0 + majorY);
err = (uint16_t)(err + (uint16_t)minorLen);
if (byteError) {
err = (uint8_t)err;
}
if (err >= (uint16_t)major) {
err = (uint16_t)(err - (uint16_t)major);
x0 = (int16_t)(x0 + minorX);
y0 = (int16_t)(y0 + minorY);
}
}
}
// Flood fill from (x, y) over the pixels fillable() accepts, painting
// whichever buffers are enabled.
static void fill(const PicDrawT *d, int16_t x, int16_t y) {
uint16_t sp;
int16_t left;
int16_t right;
int16_t scan;
int16_t ny;
uint16_t row;
bool open;
if (x < 0 || x > AGI_MAX_X || y < 0 || y > AGI_MAX_Y) {
return;
}
// Filling with the colour being searched for would never end.
if (d->visualOn) {
if (d->color == AGI_PIC_BG_COLOR) {
return;
}
} else if (!d->priorityOn || d->priorityColor == AGI_PIC_BG_PRIORITY) {
return;
}
gFillX[0] = (uint8_t)x;
gFillY[0] = (uint8_t)y;
sp = 1u;
while (sp > 0u) {
sp--;
x = gFillX[sp];
y = gFillY[sp];
row = (uint16_t)y * AGI_WIDTH;
if (!fillable(d, (uint16_t)(row + (uint16_t)x))) {
continue;
}
left = x;
while (left > 0 && fillable(d, (uint16_t)(row + (uint16_t)(left - 1)))) {
left--;
}
right = x;
while (right < AGI_MAX_X && fillable(d, (uint16_t)(row + (uint16_t)(right + 1)))) {
right++;
}
for (scan = left; scan <= right; scan++) {
if (d->visualOn) {
d->visual[row + (uint16_t)scan] = d->color;
}
if (d->priorityOn) {
d->priority[row + (uint16_t)scan] = d->priorityColor;
}
}
for (ny = (int16_t)(y - 1); ny <= (int16_t)(y + 1); ny = (int16_t)(ny + 2)) {
if (ny < 0 || ny > AGI_MAX_Y) {
continue;
}
open = false;
for (scan = left; scan <= right; scan++) {
if (fillable(d, (uint16_t)((uint16_t)ny * AGI_WIDTH + (uint16_t)scan))) {
if (!open && sp < FILL_STACK_MAX) {
gFillX[sp] = (uint8_t)scan;
gFillY[sp] = (uint8_t)ny;
sp++;
}
open = true;
} else {
open = false;
}
}
}
}
}
// A fill spreads over white in the visual buffer when that is being
// drawn, otherwise over priority 4 in the priority buffer.
static bool fillable(const PicDrawT *d, uint16_t idx) {
if (d->visualOn) {
return d->visual[idx] == AGI_PIC_BG_COLOR;
}
return d->priority[idx] == AGI_PIC_BG_PRIORITY;
}
// The absolute line action: a start point, then a line to each point.
static uint16_t opAbsLine(const PicDrawT *d, const uint8_t *data, uint16_t length, uint16_t pos) {
int16_t x;
int16_t y;
if (pos + 1u >= length || data[pos] >= PIC_FIRST_ACTION || data[pos + 1u] >= PIC_FIRST_ACTION) {
return pos;
}
x = data[pos];
y = data[pos + 1u];
pos = (uint16_t)(pos + 2u);
plot(d, x, y);
while (pos + 1u < length && data[pos] < PIC_FIRST_ACTION && data[pos + 1u] < PIC_FIRST_ACTION) {
drawLine(d, x, y, data[pos], data[pos + 1u]);
x = data[pos];
y = data[pos + 1u];
pos = (uint16_t)(pos + 2u);
}
return pos;
}
// The corner actions: a start point, then alternately a new y and a new x
// (or x first), a line drawn to each.
static uint16_t opCorner(const PicDrawT *d, const uint8_t *data, uint16_t length, uint16_t pos, bool yFirst) {
int16_t x;
int16_t y;
bool changeY;
if (pos + 1u >= length || data[pos] >= PIC_FIRST_ACTION || data[pos + 1u] >= PIC_FIRST_ACTION) {
return pos;
}
x = data[pos];
y = data[pos + 1u];
pos = (uint16_t)(pos + 2u);
changeY = yFirst;
plot(d, x, y);
while (pos < length && data[pos] < PIC_FIRST_ACTION) {
if (changeY) {
drawLine(d, x, y, x, data[pos]);
y = data[pos];
} else {
drawLine(d, x, y, data[pos], y);
x = data[pos];
}
pos++;
changeY = !changeY;
}
return pos;
}
static uint16_t opPlotPen(const PicDrawT *d, const uint8_t *data, uint16_t length, uint16_t pos) {
uint8_t texture;
texture = 0u;
for (;;) {
if ((d->pen & PEN_SPLATTER) != 0u) {
if (pos >= length || data[pos] >= PIC_FIRST_ACTION) {
break;
}
texture = data[pos];
pos++;
}
if (pos + 1u >= length || data[pos] >= PIC_FIRST_ACTION || data[pos + 1u] >= PIC_FIRST_ACTION) {
break;
}
plotPen(d, data[pos], data[pos + 1u], texture);
pos = (uint16_t)(pos + 2u);
}
return pos;
}
// The relative line action: a start point, then a line per byte to a
// point at most 7 pixels away on each axis.
static uint16_t opRelLine(const PicDrawT *d, const uint8_t *data, uint16_t length, uint16_t pos) {
int16_t x;
int16_t y;
int16_t nx;
int16_t ny;
uint8_t b;
if (pos + 1u >= length || data[pos] >= PIC_FIRST_ACTION || data[pos + 1u] >= PIC_FIRST_ACTION) {
return pos;
}
x = data[pos];
y = data[pos + 1u];
pos = (uint16_t)(pos + 2u);
plot(d, x, y);
while (pos < length && data[pos] < PIC_FIRST_ACTION) {
b = data[pos++];
nx = (int16_t)((b >> REL_X_SHIFT) & REL_MAG_MASK);
ny = (int16_t)(b & REL_MAG_MASK);
if ((b & REL_X_SIGN) != 0u) {
nx = (int16_t)-nx;
}
if ((b & REL_Y_SIGN) != 0u) {
ny = (int16_t)-ny;
}
nx = (int16_t)(x + nx);
ny = (int16_t)(y + ny);
drawLine(d, x, y, nx, ny);
x = nx;
y = ny;
}
return pos;
}
static void plot(const PicDrawT *d, int16_t x, int16_t y) {
uint16_t idx;
if (x < 0 || x > AGI_MAX_X || y < 0 || y > AGI_MAX_Y) {
return;
}
idx = (uint16_t)((uint16_t)y * AGI_WIDTH + (uint16_t)x);
if (d->visualOn) {
d->visual[idx] = d->color;
}
if (d->priorityOn) {
d->priority[idx] = d->priorityColor;
}
}
// Plot the current pen centred on (x, y): a rectangle or circle size+1
// wide and 2*size+1 tall, solid or masked by a texture's bits.
static void plotPen(const PicDrawT *d, int16_t x, int16_t y, uint8_t texture) {
uint8_t size;
uint8_t rows;
uint8_t r;
uint8_t c;
uint8_t bits;
uint8_t bitPos;
int16_t left;
int16_t top;
bool circle;
bool splatter;
size = (uint8_t)(d->pen & PEN_SIZE_MASK);
rows = (uint8_t)(size * 2u + 1u);
circle = (d->pen & PEN_RECTANGLE) == 0u;
splatter = (d->pen & PEN_SPLATTER) != 0u;
left = (int16_t)(x - (int16_t)((size + 1u) / 2u));
top = (int16_t)(y - (int16_t)size);
bitPos = kTextureStart[(texture >> TEXTURE_CODE_SHIFT) % sizeof(kTextureStart)];
for (r = 0u; r < rows; r++) {
bits = circle ? kCircleRows[size][r] : (uint8_t)(PEN_ROW_FULL << (PEN_SIZES - 1u - size));
for (c = 0u; c <= size; c++) {
if ((bits & (uint8_t)(PEN_ROW_MSB >> c)) == 0u) {
continue;
}
if (splatter) {
bool on;
on = (kTextureBits[bitPos >> BIT_BYTE_SHIFT] & (uint8_t)(PEN_ROW_MSB >> (bitPos & BIT_INDEX_MASK))) != 0u;
// The texture wraps at bit 254, not 255 (specification 7.2).
bitPos = (uint8_t)((bitPos >= TEXTURE_BITS_LAST) ? 0u : bitPos + 1u);
if (!on) {
continue;
}
}
plot(d, (int16_t)(left + (int16_t)c), (int16_t)(top + (int16_t)r));
}
}
}
// ----- Public API (alphabetical) -----
// Draw a picture over the buffers' current contents (draw.pic clears them
// first, overlay.pic does not). Returns false on a malformed stream, with
// whatever was drawn so far left in place.
bool agiPicDecode(uint8_t *visual, uint8_t *priority, const uint8_t *data, uint16_t length) {
PicDrawT d;
uint16_t pos;
uint8_t op;
d.visual = visual;
d.priority = priority;
d.visualOn = false;
d.priorityOn = false;
d.color = 0u;
d.priorityColor = 0u;
d.pen = 0u;
pos = 0u;
while (pos < length) {
op = data[pos++];
switch (op) {
case PIC_SET_COLOR:
if (pos >= length) {
return false;
}
d.color = (uint8_t)(data[pos++] & AGI_COLOR_MASK);
d.visualOn = true;
break;
case PIC_NO_COLOR:
d.visualOn = false;
break;
case PIC_SET_PRIORITY:
if (pos >= length) {
return false;
}
d.priorityColor = (uint8_t)(data[pos++] & AGI_COLOR_MASK);
d.priorityOn = true;
break;
case PIC_NO_PRIORITY:
d.priorityOn = false;
break;
case PIC_Y_CORNER:
case PIC_X_CORNER:
pos = opCorner(&d, data, length, pos, op == PIC_Y_CORNER);
break;
case PIC_ABS_LINE:
pos = opAbsLine(&d, data, length, pos);
break;
case PIC_REL_LINE:
pos = opRelLine(&d, data, length, pos);
break;
case PIC_FILL:
while (pos + 1u < length && data[pos] < PIC_FIRST_ACTION && data[pos + 1u] < PIC_FIRST_ACTION) {
fill(&d, data[pos], data[pos + 1u]);
pos = (uint16_t)(pos + 2u);
}
break;
case PIC_SET_PEN:
if (pos >= length) {
return false;
}
d.pen = data[pos++];
break;
case PIC_PLOT_PEN:
pos = opPlotPen(&d, data, length, pos);
break;
case PIC_END:
return true;
default:
// Unknown action bytes are skipped with their arguments.
while (pos < length && data[pos] < PIC_FIRST_ACTION) {
pos++;
}
break;
}
}
return true;
}
uint8_t agiPriorityForY(int16_t y) {
if (y < AGI_PRI_BAND_TOP) {
return AGI_PRIORITY_MIN;
}
if (y > AGI_MAX_Y) {
y = AGI_MAX_Y;
}
return (uint8_t)(AGI_PRI_FIRST_BAND + (uint8_t)((y - AGI_PRI_BAND_TOP) / AGI_PRI_BAND_HEIGHT));
}
// The lowest y that has a priority: where an object with that fixed
// priority sorts among the others when they are drawn.
int16_t agiYForPriority(uint8_t priority) {
if (priority <= AGI_PRIORITY_MIN) {
return 0;
}
return (int16_t)(AGI_PRI_BAND_TOP + (int16_t)(priority - AGI_PRI_FIRST_BAND) * AGI_PRI_BAND_HEIGHT);
}