// 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 #include #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); }