joeylib2/examples/agi/agiView.c

402 lines
14 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
#endif
// ----- File-local types -----
// Per-draw invariants for one cel blit, grouped so they live in memory
// rather than competing for the w65816's tiny register file. Passing a
// pointer to this keeps the hot blit helpers under the backend's
// register-allocation ceiling.
typedef struct {
const uint8_t *picPriority;
int16_t x;
int16_t width;
uint8_t actorPri;
uint8_t transparent;
bool mirrored;
} AgiCelBlitT;
// ----- Prototypes -----
static void blitPlanar(const AgiCelBlitT *blit, const uint8_t *rle, int16_t height, int16_t topY, jlSurfaceT *stage, int16_t destY);
static const uint8_t *blitRowChunky(const AgiCelBlitT *blit, const uint8_t *rle, const uint8_t *priRow, uint8_t *stageRow);
static void markCelDirty(jlSurfaceT *stage, int16_t x, int16_t width, int16_t destY, int16_t topY, int16_t height);
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) -----
// Planar fallback row walker (stage->pixels == NULL). Routes every
// opaque, visible pixel through jlDrawPixel so the port's c2p /
// plane-sync path stays correct. Split out of agiViewDraw so its
// locals don't pile onto the chunky path's register budget.
static void blitPlanar(const AgiCelBlitT *blit, const uint8_t *rle, int16_t height, int16_t topY, jlSurfaceT *stage, int16_t destY) {
int16_t width;
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 runLen;
uint8_t r;
uint8_t picPri;
width = blit->width;
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 != blit->transparent) {
drawCx = blit->mirrored ? (int16_t)(width - 1 - cx) : cx;
agiX = (int16_t)(blit->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 = blit->picPriority[agiY * (int16_t)AGI_PIC_WIDTH + agiX];
if (picPri <= blit->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++;
}
}
}
}
// Chunky fast-path walker for one source row. Consumes the cel's RLE
// stream from `rle`, writing pixel-doubled bytes into `stageRow` where
// the picture priority allows it, and returns the advanced RLE pointer
// so the caller can hand it to the next row. A NULL priRow / stageRow
// means the row is clipped out vertically: the RLE is still walked (so
// the pointer lands correctly for later rows) but nothing is drawn.
static const uint8_t *blitRowChunky(const AgiCelBlitT *blit, const uint8_t *rle, const uint8_t *priRow, uint8_t *stageRow) {
int16_t width;
int16_t cx;
int16_t drawCx;
int16_t agiX;
uint8_t rleByte;
uint8_t color;
uint8_t runLen;
uint8_t packed;
uint8_t r;
width = blit->width;
cx = 0;
for (;;) {
rleByte = *rle++;
if (rleByte == 0u) { break; }
color = (uint8_t)(rleByte >> 4);
runLen = (uint8_t)(rleByte & 0x0Fu);
// Transparent runs and vertically-clipped rows both just skip
// ahead by runLen without touching the stage.
if (color == blit->transparent || 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 = blit->mirrored ? (int16_t)(width - 1 - cx) : cx;
agiX = (int16_t)(blit->x + drawCx);
if (agiX >= 0 && agiX < (int16_t)AGI_PIC_WIDTH) {
if (priRow[agiX] <= blit->actorPri) {
stageRow[agiX] = packed;
}
}
cx++;
}
}
return rle;
}
// Mark the cel's stage bounding box dirty once after a chunky blit.
// Computes the pixel-doubled bbox from the source rect, clips it to the
// stage so the surface marker doesn't reject the whole rect for an
// off-screen edge, and records it if any of it is on-screen.
static void markCelDirty(jlSurfaceT *stage, int16_t x, int16_t width, int16_t destY, int16_t topY, int16_t height) {
int16_t minStageX;
int16_t maxStageX;
int16_t minStageY;
int16_t maxStageY;
minStageX = (int16_t)(x << 1);
maxStageX = (int16_t)((x + width) << 1);
minStageY = (int16_t)(destY + topY);
maxStageY = (int16_t)(destY + topY + height);
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));
}
}
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
#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;
AgiCelBlitT blit;
const uint8_t *rle;
bool mirrored;
int16_t height;
int16_t topY;
uint8_t *stagePixels;
cel = resolveMirror(view, loopIdx, celIdx, &mirrored);
if (cel == NULL) {
return;
}
height = (int16_t)cel->height;
blit.picPriority = picPriority;
blit.x = x;
blit.width = (int16_t)cel->width;
blit.actorPri = actorPri;
blit.transparent = cel->transparentColor;
blit.mirrored = mirrored;
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 cy;
int16_t agiY;
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;
}
rle = blitRowChunky(&blit, rle, priRow, stageRow);
}
markCelDirty(stage, x, blit.width, destY, topY, height);
return;
}
// Planar fallback (s->pixels NULL: Amiga Phase 9). Goes through
// jlDrawPixel so the port's c2p / plane-sync path stays correct.
blitPlanar(&blit, rle, height, topY, stage, destY);
}
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;
}