// 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 #include #include // 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; }