joeylib2/examples/agi/agiPic.c

662 lines
21 KiB
C

// 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 <stddef.h>
#include <stdlib.h>
#include <string.h>
// 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;
}
}