// AGI v2 PIC (picture / background) decoder. // // Paints to two private 160x168 chunky 8bpp buffers (visual + priority). // The blit step doubles each AGI source column into two stage columns // so the natural 160x168 internal coordinate system can be used by the // drawing primitives, and the 320-wide stage receives the canonical // AGI look. // // All algorithms here -- line interpolation, corner drawing, scanline // flood fill, pen plotting, opcode dispatch -- are re-implementations // written against the published AGI v2 picture-stream specification. // No third-party source is referenced. #include "agi.h" #include "joey/draw.h" #include "surfaceInternal.h" #include #include #include // PIC opcodes. Anything < 0xF0 is an argument byte; >= 0xF0 ends the // current opcode's argument list. #define OP_FIRST 0xF0u #define OP_SET_PIC_COLOR 0xF0u #define OP_DISABLE_PIC 0xF1u #define OP_SET_PRI_COLOR 0xF2u #define OP_DISABLE_PRI 0xF3u #define OP_Y_CORNER 0xF4u #define OP_X_CORNER 0xF5u #define OP_ABS_LINE 0xF6u #define OP_REL_LINE 0xF7u #define OP_FILL 0xF8u #define OP_SET_PEN 0xF9u #define OP_PLOT_PEN 0xFAu #define OP_END 0xFFu // Pen control byte bits (set via OP_SET_PEN, consumed by OP_PLOT_PEN). #define PEN_PATTERN_BIT 0x20u // Scanline-fill seed stack. Each fill is reentrant-free, so a single // static stack serves both the visual and priority passes. The bound // is a safe over-estimate of typical PIC fill complexity; KQ3 PICs // don't approach it. #define FILL_STACK_MAX 512 typedef struct { uint8_t visualEnabled; uint8_t priorityEnabled; uint8_t visualColor; uint8_t priorityColor; uint8_t penPattern; } PicStateT; // AGI 16-color CGA-derived palette. Entry 6's (R=A, G=5, B=0) keeps the // classic CGA "brown fix" -- pure (R=A, G=A, B=0) would read as a vivid // yellow-green on the host, which is wrong for AGI's intended look. const uint16_t kAgiPalette[16] = { 0x000u, 0x00Au, 0x0A0u, 0x0AAu, 0xA00u, 0xA0Au, 0xA50u, 0xAAAu, 0x555u, 0x55Fu, 0x5F5u, 0x5FFu, 0xF55u, 0xF5Fu, 0xFF5u, 0xFFFu }; // ----- Prototypes ----- static void drawCorner(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos, bool yFirst); static void drawLine(AgiPicT *pic, const PicStateT *state, int16_t x0, int16_t y0, int16_t x1, int16_t y1); static void fillPlane(uint8_t *plane, int16_t x, int16_t y, uint8_t bgValue, uint8_t newValue); static bool isOpcode(uint8_t byte); static void opAbsLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos); static void opFill(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos); static void opPlotPen(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos); static void opRelLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos); static void plot(AgiPicT *pic, const PicStateT *state, int16_t x, int16_t y); #ifdef JOEYLIB_PLATFORM_IIGS segment "AGIPIC"; #endif // ----- Internal helpers (alphabetical) ----- static void drawCorner(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos, bool yFirst) { uint16_t p; int16_t curX; int16_t curY; bool readY; p = *pos; if (p + 1 >= length) { *pos = length; return; } curX = (int16_t)data[p++]; curY = (int16_t)data[p++]; plot(pic, state, curX, curY); readY = yFirst; while (p < length && !isOpcode(data[p])) { if (readY) { int16_t newY = (int16_t)data[p++]; drawLine(pic, state, curX, curY, curX, newY); curY = newY; } else { int16_t newX = (int16_t)data[p++]; drawLine(pic, state, curX, curY, newX, curY); curX = newX; } readY = !readY; } *pos = p; } // Documented AGI line algorithm. Major axis steps in unit increments; // the minor axis is linearly interpolated with round-half-away-from- // zero. All math stays in 16-bit -- AGI coordinates are 8-bit so the // worst product (dy * i = 167 * 167 = 27889) fits in int16, and the // 65816 build avoids ORCA-C's long-arithmetic helpers entirely. static void drawLine(AgiPicT *pic, const PicStateT *state, int16_t x0, int16_t y0, int16_t x1, int16_t y1) { int16_t dx; int16_t dy; int16_t absDx; int16_t absDy; int16_t steps; int16_t i; int16_t half; int16_t px; int16_t py; int16_t sx; int16_t sy; dx = (int16_t)(x1 - x0); dy = (int16_t)(y1 - y0); if (dx == 0 && dy == 0) { plot(pic, state, x0, y0); return; } absDx = (int16_t)((dx >= 0) ? dx : -dx); absDy = (int16_t)((dy >= 0) ? dy : -dy); sx = (int16_t)((dx >= 0) ? 1 : -1); sy = (int16_t)((dy >= 0) ? 1 : -1); if (absDx >= absDy) { steps = absDx; half = (int16_t)(steps / 2); for (i = 0; i <= steps; i++) { px = (int16_t)(x0 + sx * i); py = (int16_t)(y0 + (dy * i + sy * half) / steps); plot(pic, state, px, py); } } else { steps = absDy; half = (int16_t)(steps / 2); for (i = 0; i <= steps; i++) { py = (int16_t)(y0 + sy * i); px = (int16_t)(x0 + (dx * i + sx * half) / steps); plot(pic, state, px, py); } } } // Scanline flood fill on one plane. Replaces every cell of value // `bgValue` 4-connected to (x, y) with `newValue`. The seed stack is // bounded; runs that don't fit are silently dropped, which mirrors // the original interpreter's behavior under pathological PIC inputs. static void fillPlane(uint8_t *plane, int16_t x, int16_t y, uint8_t bgValue, uint8_t newValue) { static int16_t stackX[FILL_STACK_MAX]; static int16_t stackY[FILL_STACK_MAX]; uint16_t sp; int16_t cx; int16_t cy; int16_t left; int16_t right; int16_t scanX; bool aboveOpen; bool belowOpen; if (x < 0 || x >= AGI_PIC_WIDTH || y < 0 || y >= AGI_PIC_HEIGHT) { return; } if (plane[y * AGI_PIC_WIDTH + x] != bgValue) { return; } if (bgValue == newValue) { return; } sp = 0; stackX[sp] = x; stackY[sp] = y; sp++; while (sp > 0u) { sp--; cx = stackX[sp]; cy = stackY[sp]; if (plane[cy * AGI_PIC_WIDTH + cx] != bgValue) { continue; } // Expand the current row to the bg run's full extent. left = cx; while (left > 0 && plane[cy * AGI_PIC_WIDTH + (left - 1)] == bgValue) { left--; } right = cx; while (right < (int16_t)(AGI_PIC_WIDTH - 1) && plane[cy * AGI_PIC_WIDTH + (right + 1)] == bgValue) { right++; } for (scanX = left; scanX <= right; scanX++) { plane[cy * AGI_PIC_WIDTH + scanX] = newValue; } // Push one seed per bg run on the row above. if (cy > 0) { aboveOpen = false; for (scanX = left; scanX <= right; scanX++) { if (plane[(cy - 1) * AGI_PIC_WIDTH + scanX] == bgValue) { if (!aboveOpen) { if (sp < FILL_STACK_MAX) { stackX[sp] = scanX; stackY[sp] = (int16_t)(cy - 1); sp++; } aboveOpen = true; } } else { aboveOpen = false; } } } // And one seed per bg run on the row below. if (cy < (int16_t)(AGI_PIC_HEIGHT - 1)) { belowOpen = false; for (scanX = left; scanX <= right; scanX++) { if (plane[(cy + 1) * AGI_PIC_WIDTH + scanX] == bgValue) { if (!belowOpen) { if (sp < FILL_STACK_MAX) { stackX[sp] = scanX; stackY[sp] = (int16_t)(cy + 1); sp++; } belowOpen = true; } } else { belowOpen = false; } } } } } static bool isOpcode(uint8_t byte) { return byte >= OP_FIRST; } static void opAbsLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos) { uint16_t p; int16_t prevX; int16_t prevY; int16_t curX; int16_t curY; bool have; p = *pos; have = false; prevX = 0; prevY = 0; while (p + 1 < length && !isOpcode(data[p])) { curX = (int16_t)data[p++]; curY = (int16_t)data[p++]; if (!have) { plot(pic, state, curX, curY); have = true; } else { drawLine(pic, state, prevX, prevY, curX, curY); } prevX = curX; prevY = curY; } *pos = p; } static void opFill(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos) { uint16_t p; int16_t x; int16_t y; p = *pos; while (p + 1 < length && !isOpcode(data[p])) { x = (int16_t)data[p++]; y = (int16_t)data[p++]; if (state->visualEnabled) { fillPlane(pic->visual, x, y, AGI_VISUAL_BG, state->visualColor); } if (state->priorityEnabled) { fillPlane(pic->priority, x, y, AGI_PRIORITY_BG, state->priorityColor); } } *pos = p; } // Plot-with-pen. With pen patterning off, each iteration consumes an // X,Y pair and plots a single pixel. With patterning on, each // iteration consumes a texture byte first (we discard it -- proper // patterned brushes are deferred to a later pass). Size and shape // are likewise stubbed to single-pixel; KQ3 PICs use pens sparingly // and dropping the brush footprint just thins occasional grass / sky // dithering. static void opPlotPen(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos) { uint16_t p; int16_t x; int16_t y; p = *pos; while (p < length && !isOpcode(data[p])) { if (state->penPattern) { // Skip the texture-selector byte. p++; if (p >= length || isOpcode(data[p])) { break; } } if (p + 1 >= length) { break; } x = (int16_t)data[p++]; y = (int16_t)data[p++]; plot(pic, state, x, y); } *pos = p; } static void opRelLine(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos) { uint16_t p; int16_t curX; int16_t curY; int16_t newX; int16_t newY; uint8_t disp; int16_t dx; int16_t dy; p = *pos; if (p + 1 >= length) { *pos = p; return; } curX = (int16_t)data[p++]; curY = (int16_t)data[p++]; plot(pic, state, curX, curY); while (p < length && !isOpcode(data[p])) { disp = data[p++]; // High nibble = X disp (bit 7 sign, bits 4-6 magnitude 0..7). // Low nibble = Y disp (bit 3 sign, bits 0-2 magnitude 0..7). dx = (int16_t)((disp >> 4) & 0x07u); if (disp & 0x80u) { dx = (int16_t)-dx; } dy = (int16_t)(disp & 0x07u); if (disp & 0x08u) { dy = (int16_t)-dy; } newX = (int16_t)(curX + dx); newY = (int16_t)(curY + dy); drawLine(pic, state, curX, curY, newX, newY); curX = newX; curY = newY; } *pos = p; } static void plot(AgiPicT *pic, const PicStateT *state, int16_t x, int16_t y) { uint16_t idx; if (x < 0 || x >= (int16_t)AGI_PIC_WIDTH) { return; } if (y < 0 || y >= (int16_t)AGI_PIC_HEIGHT) { return; } idx = (uint16_t)(y * (int16_t)AGI_PIC_WIDTH + x); if (state->visualEnabled) { pic->visual[idx] = state->visualColor; } if (state->priorityEnabled) { pic->priority[idx] = state->priorityColor; } } // ----- Public API (alphabetical) ----- bool agiPicAlloc(AgiPicT *pic) { pic->visual = (uint8_t *)malloc(AGI_PIC_PIXELS); pic->priority = (uint8_t *)malloc(AGI_PIC_PIXELS); if (pic->visual == NULL || pic->priority == NULL) { agiPicFree(pic); return false; } return true; } void agiPicBlit(const AgiPicT *pic, jlSurfaceT *stage, int16_t destY) { int16_t sy; int16_t sx; int16_t dx; uint8_t color; // Fast path: chunky shadow exists -> write packed bytes direct, // mark the whole region dirty once at the end. ~30x faster than // calling jlDrawPixel 53,760 times (each does bounds + 1-pixel // dirty mark). Required for state-2 / state-3 title transitions // to render under a second on period-correct DOSBox CPU cycles. if (stage != NULL && stage->pixels != NULL) { const uint8_t *src = pic->visual; uint8_t *dst; int16_t rowOff; for (sy = 0; sy < (int16_t)AGI_PIC_HEIGHT; sy++) { rowOff = (int16_t)((int16_t)(destY + sy) * (int16_t)SURFACE_BYTES_PER_ROW); dst = &stage->pixels[rowOff]; for (sx = 0; sx < (int16_t)AGI_PIC_WIDTH; sx++) { color = (uint8_t)(src[sy * AGI_PIC_WIDTH + sx] & 0x0Fu); // Pixel-doubled: each source pixel = 1 dst byte // (both nibbles = same color). dst[sx] = (uint8_t)((color << 4) | color); } } surfaceMarkDirtyRect(stage, 0, destY, (int16_t)SURFACE_WIDTH, (int16_t)AGI_PIC_HEIGHT); return; } // Planar fallback (s->pixels NULL: Phase-9 Amiga). Goes through // jlDrawPixel so the port's c2p / plane-sync path stays correct. for (sy = 0; sy < (int16_t)AGI_PIC_HEIGHT; sy++) { for (sx = 0; sx < (int16_t)AGI_PIC_WIDTH; sx++) { color = pic->visual[sy * AGI_PIC_WIDTH + sx]; dx = (int16_t)(sx << 1); jlDrawPixel(stage, dx, (int16_t)(destY + sy), color); jlDrawPixel(stage, (int16_t)(dx + 1), (int16_t)(destY + sy), color); } } } void agiPicClear(AgiPicT *pic) { if (pic->visual != NULL) { memset(pic->visual, AGI_VISUAL_BG, AGI_PIC_PIXELS); } if (pic->priority != NULL) { memset(pic->priority, AGI_PRIORITY_BG, AGI_PIC_PIXELS); } } bool agiPicDecode(AgiPicT *pic, const uint8_t *data, uint16_t length) { PicStateT state; uint16_t pos; uint8_t op; state.visualEnabled = 0u; state.priorityEnabled = 0u; state.visualColor = 0u; state.priorityColor = 0u; state.penPattern = 0u; pos = 0u; while (pos < length) { op = data[pos++]; switch (op) { case OP_SET_PIC_COLOR: if (pos >= length) { return false; } state.visualColor = data[pos++]; state.visualEnabled = 1u; break; case OP_DISABLE_PIC: state.visualEnabled = 0u; break; case OP_SET_PRI_COLOR: if (pos >= length) { return false; } state.priorityColor = data[pos++]; state.priorityEnabled = 1u; break; case OP_DISABLE_PRI: state.priorityEnabled = 0u; break; case OP_Y_CORNER: drawCorner(pic, &state, data, length, &pos, true); break; case OP_X_CORNER: drawCorner(pic, &state, data, length, &pos, false); break; case OP_ABS_LINE: opAbsLine(pic, &state, data, length, &pos); break; case OP_REL_LINE: opRelLine(pic, &state, data, length, &pos); break; case OP_FILL: opFill(pic, &state, data, length, &pos); break; case OP_SET_PEN: if (pos >= length) { return false; } state.penPattern = (uint8_t)((data[pos++] & PEN_PATTERN_BIT) ? 1u : 0u); break; case OP_PLOT_PEN: opPlotPen(pic, &state, data, length, &pos); break; case OP_END: return true; default: // Unknown opcode -- stop cleanly rather than misparsing. return false; } } return true; } // Bake a single VIEW cel into the visual + priority planes (the // implementation of add.to.pic). Pixels outside the picture window // or set to the cel's transparent color are skipped. priColor in // 4..15 overwrites the priority pixels where the visual is opaque; // 0 preserves the existing priority. RLE format matches agiViewDraw // (per-row 0-byte EOR marker after a variable run). void agiPicAddView(AgiPicT *pic, const AgiViewT *view, uint8_t loop, uint8_t cel, int16_t baseX, int16_t baseY, uint8_t priColor, uint8_t margin) { const AgiCelInfoT *info; const AgiCelInfoT *resolved; const uint8_t *rle; uint8_t actualLoop; int16_t width; int16_t height; uint8_t trans; bool mirrored; int16_t topY; int16_t cy; int16_t cx; int16_t drawCx; int16_t agiX; int16_t agiY; uint8_t rleByte; uint8_t color; uint8_t runLen; uint8_t r; (void)margin; if (view == NULL || pic == NULL || pic->visual == NULL) { return; } if (loop >= view->loopCount) { return; } if (cel >= view->loops[loop].celCount) { return; } info = &view->loops[loop].cels[cel]; actualLoop = loop; mirrored = false; if (info->mirrored != 0u) { if (info->mirrorSourceLoop != loop && info->mirrorSourceLoop < view->loopCount && cel < view->loops[info->mirrorSourceLoop].celCount) { actualLoop = info->mirrorSourceLoop; mirrored = true; } } resolved = &view->loops[actualLoop].cels[cel]; width = (int16_t)resolved->width; height = (int16_t)resolved->height; trans = resolved->transparentColor; rle = resolved->rleData; topY = (int16_t)(baseY - height + 1); for (cy = 0; cy < height; cy++) { cx = 0; for (;;) { rleByte = *rle++; if (rleByte == 0u) { // End-of-row marker. break; } color = (uint8_t)(rleByte >> 4); runLen = (uint8_t)(rleByte & 0x0Fu); for (r = 0u; r < runLen; r++) { if (cx >= width) { break; } if (color != trans) { drawCx = mirrored ? (int16_t)(width - 1 - cx) : cx; agiX = (int16_t)(baseX + drawCx); agiY = (int16_t)(topY + cy); if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH && agiY >= 0 && agiY < (int16_t)AGI_PIC_HEIGHT) { int16_t idx = (int16_t)(agiY * (int16_t)AGI_PIC_WIDTH + agiX); bool paint = true; // AGI v2 priority masking: when priColor is a // real priority value (>= AGI_PRIORITY_BG), // skip the pixel if the existing priority is // strictly higher -- the new art is behind // existing higher-priority art. This is how // KQ3's title "III" ends up behind the KQ // logo (III priColor=4 = background, KQ logo // in PIC 45 sits at a higher band). if (priColor >= AGI_PRIORITY_BG && priColor < 16u && pic->priority != NULL) { if (pic->priority[idx] > priColor) { paint = false; } } if (paint) { pic->visual[idx] = color; if (priColor >= AGI_PRIORITY_BG && priColor < 16u && pic->priority != NULL) { pic->priority[idx] = priColor; } } } } cx++; } } } } void agiPicFree(AgiPicT *pic) { if (pic->visual != NULL) { free(pic->visual); pic->visual = NULL; } if (pic->priority != NULL) { free(pic->priority); pic->priority = NULL; } }