joeylib2/examples/agi/agiView.c

340 lines
12 KiB
C

// AGI v2 VIEW (sprite/animation) decoder.
//
// Parses an in-RAM VIEW resource into a structured loop/cel table
// once at load time, then renders any cel against the priority plane
// for per-pixel actor-vs-picture masking. Re-implements the public
// AGI VIEW byte layout from scratch.
//
// Pixel-doubled blit goes straight to the stage so animation cycles
// don't require an intermediate back-buffer. The 160x168 AGI surface
// maps onto stage columns (2 * agiX, 2 * agiX + 1) at the same destY
// agiPicBlit uses.
#include "agi.h"
#include "joey/draw.h"
#include "surfaceInternal.h"
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
// VIEW header byte offsets (publicly documented v2 layout).
#define AGI_VIEW_HDR_LOOP_COUNT 2u
#define AGI_VIEW_HDR_LOOP_TABLE 5u
// Cel header bits (publicly documented v2 layout).
#define AGI_CEL_TRANSPARENT_MASK 0x0Fu
#define AGI_CEL_MIRROR_FLAG 0x80u
#define AGI_CEL_MIRROR_SRC_SHIFT 4u
#define AGI_CEL_MIRROR_SRC_MASK 0x07u
// Priority bands. Y 0..47 is sky (priority 4); Y 48..167 splits into
// 10 ground bands of 12 pixels each at priorities 5..14. Priority 15
// is "always on top" and reserved for HUD/overlay; an actor's natural
// Y never reaches it.
#define AGI_PRI_SKY_TOP 48
#define AGI_PRI_GROUND_FIRST 5u
#define AGI_PRI_GROUND_BAND_PX 12
#ifdef JOEYLIB_PLATFORM_IIGS
segment "AGIVIEW";
#endif
// ----- Prototypes -----
static bool parseLoop(AgiLoopInfoT *loop, const uint8_t *loopStart, uint16_t maxBytes);
static const AgiCelInfoT *resolveMirror(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx, bool *outMirrored);
// ----- Internal helpers (alphabetical) -----
static bool parseLoop(AgiLoopInfoT *loop, const uint8_t *loopStart, uint16_t maxBytes) {
uint8_t celCount;
uint8_t i;
uint16_t celOffset;
const uint8_t *celStart;
uint8_t flags;
if (maxBytes < 1u) {
return false;
}
celCount = loopStart[0];
if ((uint16_t)celCount > AGI_MAX_CELS_PER_LOOP) {
return false;
}
if (maxBytes < (uint16_t)(1u + (uint16_t)celCount * 2u)) {
return false;
}
loop->celCount = celCount;
for (i = 0u; i < celCount; i++) {
celOffset = (uint16_t)(loopStart[1u + (uint16_t)i * 2u] |
((uint16_t)loopStart[2u + (uint16_t)i * 2u] << 8));
if ((uint16_t)(celOffset + 3u) > maxBytes) {
return false;
}
celStart = loopStart + celOffset;
flags = celStart[2];
loop->cels[i].width = celStart[0];
loop->cels[i].height = celStart[1];
loop->cels[i].transparentColor = (uint8_t)(flags & AGI_CEL_TRANSPARENT_MASK);
loop->cels[i].mirrored = (uint8_t)((flags & AGI_CEL_MIRROR_FLAG) ? 1u : 0u);
loop->cels[i].mirrorSourceLoop = (uint8_t)((flags >> AGI_CEL_MIRROR_SRC_SHIFT) & AGI_CEL_MIRROR_SRC_MASK);
loop->cels[i].rleData = celStart + 3;
}
return true;
}
// If the cel is a mirror, swap to the source loop's same-index cel and
// flag the caller to flip the RLE walk horizontally. Mirror chains
// don't recurse in valid AGI data; we don't follow more than one hop.
//
// Sierra's encoder marks every cel in BOTH halves of a mirror pair
// with the mirror flag, with the source-loop field pointing to the
// lower-numbered loop. The cel is only an actual mirror when the
// source-loop field points somewhere ELSE; when source == this loop,
// this IS the source data and must be drawn unflipped (otherwise both
// loops in a pair render mirrored and e.g. Graham faces left whether
// the player presses left or right).
static const AgiCelInfoT *resolveMirror(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx, bool *outMirrored) {
const AgiCelInfoT *cel;
uint8_t sourceLoop;
if (loopIdx >= view->loopCount) {
return NULL;
}
if (celIdx >= view->loops[loopIdx].celCount) {
return NULL;
}
cel = &view->loops[loopIdx].cels[celIdx];
*outMirrored = false;
if (cel->mirrored) {
sourceLoop = cel->mirrorSourceLoop;
if (sourceLoop != loopIdx && sourceLoop < view->loopCount && celIdx < view->loops[sourceLoop].celCount) {
cel = &view->loops[sourceLoop].cels[celIdx];
*outMirrored = true;
}
}
return cel;
}
// ----- Public API (alphabetical) -----
uint8_t agiActorPriorityForY(int16_t y) {
int16_t capped;
int16_t band;
if (y < AGI_PRI_SKY_TOP) {
return 4u;
}
capped = (y > (int16_t)(AGI_PIC_HEIGHT - 1)) ? (int16_t)(AGI_PIC_HEIGHT - 1) : y;
band = (int16_t)(AGI_PRI_GROUND_FIRST + (capped - AGI_PRI_SKY_TOP) / AGI_PRI_GROUND_BAND_PX);
if (band > 14) {
band = 14;
}
return (uint8_t)band;
}
#ifdef JOEYLIB_PLATFORM_IIGS
segment "AGIDRAW";
#endif
void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx,
int16_t x, int16_t y, uint8_t actorPri,
const uint8_t *picPriority,
jlSurfaceT *stage, int16_t destY) {
const AgiCelInfoT *cel;
const uint8_t *rle;
bool mirrored;
int16_t width;
int16_t height;
uint8_t transparent;
int16_t topY;
int16_t cy;
int16_t cx;
int16_t drawCx;
int16_t agiX;
int16_t agiY;
int16_t stageX;
int16_t stageY;
uint8_t rleByte;
uint8_t color;
uint8_t packed;
uint8_t runLen;
uint8_t r;
uint8_t picPri;
uint8_t *stagePixels;
cel = resolveMirror(view, loopIdx, celIdx, &mirrored);
if (cel == NULL) {
return;
}
width = (int16_t)cel->width;
height = (int16_t)cel->height;
transparent = cel->transparentColor;
rle = cel->rleData;
topY = (int16_t)(y - height + 1);
stagePixels = (stage != NULL) ? stage->pixels : NULL;
// Per-pixel chunky fast path: pixel-doubled output means each
// source pixel maps to exactly one stage byte (both nibbles =
// same color). We can skip jlDrawPixel and write the byte
// direct, marking the cel bbox dirty once at the end. ~80x
// faster on DOSBox @ 386SX-equivalent CPU cycles.
if (stagePixels != NULL) {
int16_t minStageX = (int16_t)(x << 1);
int16_t maxStageX = (int16_t)((x + width) << 1);
int16_t minStageY = (int16_t)(destY + topY);
int16_t maxStageY = (int16_t)(destY + topY + height);
const uint8_t *priRow;
uint8_t *stageRow;
for (cy = 0; cy < height; cy++) {
agiY = (int16_t)(topY + cy);
// Hoist per-row work out of the per-pixel inner loop.
// For rows entirely outside the pic clip we still have to
// walk the RLE so the byte pointer ends in the right place
// for subsequent rows.
if (agiY < 0 || agiY >= (int16_t)AGI_PIC_HEIGHT) {
priRow = NULL;
stageRow = NULL;
} else {
priRow = picPriority + (int32_t)agiY * (int32_t)AGI_PIC_WIDTH;
stageRow = stagePixels +
(int32_t)(destY + agiY) * (int32_t)SURFACE_BYTES_PER_ROW;
}
cx = 0;
for (;;) {
rleByte = *rle++;
if (rleByte == 0u) { break; }
color = (uint8_t)(rleByte >> 4);
runLen = (uint8_t)(rleByte & 0x0Fu);
if (color == transparent) {
cx = (int16_t)(cx + runLen);
if (cx > width) { cx = width; }
continue;
}
if (priRow == NULL) {
cx = (int16_t)(cx + runLen);
if (cx > width) { cx = width; }
continue;
}
packed = (uint8_t)((color << 4) | color);
for (r = 0u; r < runLen; r++) {
if (cx >= width) { break; }
drawCx = mirrored ? (int16_t)(width - 1 - cx) : cx;
agiX = (int16_t)(x + drawCx);
if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH) {
if (priRow[agiX] <= actorPri) {
stageRow[agiX] = packed;
}
}
cx++;
}
}
}
// Dirty-rect tracking at the cel level only: we know the
// bbox a priori (the cel's stage rect), and per-pixel tight
// bounds aren't worth the inner-loop cost on a 386. Clip to
// the stage so the surface marker doesn't reject the whole
// rect for an off-screen edge.
if (minStageX < 0) { minStageX = 0; }
if (minStageY < 0) { minStageY = 0; }
if (maxStageX > (int16_t)SURFACE_WIDTH) { maxStageX = (int16_t)SURFACE_WIDTH; }
if (maxStageY > (int16_t)SURFACE_HEIGHT) { maxStageY = (int16_t)SURFACE_HEIGHT; }
if (maxStageX > minStageX && maxStageY > minStageY) {
surfaceMarkDirtyRect(stage, minStageX, minStageY,
(int16_t)(maxStageX - minStageX),
(int16_t)(maxStageY - minStageY));
}
return;
}
// Planar fallback (s->pixels NULL: Amiga Phase 9). Goes through
// jlDrawPixel so the port's c2p / plane-sync path stays correct.
for (cy = 0; cy < height; cy++) {
cx = 0;
for (;;) {
rleByte = *rle++;
if (rleByte == 0u) { break; }
color = (uint8_t)(rleByte >> 4);
runLen = (uint8_t)(rleByte & 0x0Fu);
for (r = 0u; r < runLen; r++) {
if (cx >= width) { break; }
if (color != transparent) {
drawCx = mirrored ? (int16_t)(width - 1 - cx) : cx;
agiX = (int16_t)(x + drawCx);
agiY = (int16_t)(topY + cy);
if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH && agiY >= 0 && agiY < (int16_t)AGI_PIC_HEIGHT) {
picPri = picPriority[agiY * (int16_t)AGI_PIC_WIDTH + agiX];
if (picPri <= actorPri) {
stageX = (int16_t)(agiX << 1);
stageY = (int16_t)(destY + agiY);
jlDrawPixel(stage, stageX, stageY, color);
jlDrawPixel(stage, (int16_t)(stageX + 1), stageY, color);
}
}
}
cx++;
}
}
}
}
void agiViewFree(AgiViewT *view) {
if (view->raw != NULL) {
free(view->raw);
view->raw = NULL;
}
view->rawLength = 0u;
view->loopCount = 0u;
}
bool agiViewParse(AgiViewT *view, uint8_t *rawBytes, uint16_t length) {
uint8_t loopCount;
uint8_t i;
uint16_t loopOffset;
memset(view, 0, sizeof(*view));
if (length < AGI_VIEW_HDR_LOOP_TABLE) {
free(rawBytes);
return false;
}
loopCount = rawBytes[AGI_VIEW_HDR_LOOP_COUNT];
if ((uint16_t)loopCount > AGI_MAX_LOOPS_PER_VIEW) {
free(rawBytes);
return false;
}
if ((uint16_t)(AGI_VIEW_HDR_LOOP_TABLE + (uint16_t)loopCount * 2u) > length) {
free(rawBytes);
return false;
}
view->raw = rawBytes;
view->rawLength = length;
view->loopCount = loopCount;
for (i = 0u; i < loopCount; i++) {
loopOffset = (uint16_t)(rawBytes[AGI_VIEW_HDR_LOOP_TABLE + (uint16_t)i * 2u] |
((uint16_t)rawBytes[AGI_VIEW_HDR_LOOP_TABLE + 1u + (uint16_t)i * 2u] << 8));
if (loopOffset >= length) {
agiViewFree(view);
return false;
}
if (!parseLoop(&view->loops[i], rawBytes + loopOffset, (uint16_t)(length - loopOffset))) {
agiViewFree(view);
return false;
}
}
return true;
}