340 lines
12 KiB
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;
|
|
}
|