// The picture buffers and the screen objects drawn into them. // // Like Sierra's interpreter, JoeyAGI keeps one 160x168 picture buffer pair // -- a colour and a priority byte per AGI pixel -- that holds the picture // with the visible objects blitted into it. An object's blit saves the // bytes under its cel, then draws the cel's pixels whose priority is at // least the buffer's, writing the object's priority with the colour, so a // lower object drawn later stays hidden behind it. Erasing puts the saved // bytes back, so blits stack: the buffer is always erased in the reverse // order it was drawn. Objects that are not updated (stop.update) are drawn // first, sorted by depth, and the updated ones on top of them. // // Changed areas are copied to the stage, each AGI pixel two screen // pixels wide, below the status line (configure.screen's picture row). // display() text written over the picture lives only on the stage, so an // object moving across it wipes it, as in the original. #include "agi.h" #include #include #include "joey/core.h" #include "joey/draw.h" #include "joey/input.h" #include "joey/present.h" #include "joey/sprite.h" #include "joey/tile.h" #include "surfaceInternal.h" // Room for the save-unders of every drawn object: two bytes per pixel. #define SAVE_ARENA_BYTES 65535u // Areas waiting to be copied to the stage; more are merged into one. #define DIRTY_MAX 24u #define NIBBLE_SHIFT 4u // A cel run byte: the colour in the high nibble, the length in the low. #define RLE_COUNT_MASK 0x0Fu // add.to.pic's margin box: margins 0..3 are control values; 4 and above // mean no box. #define MARGIN_NONE 4u // shake.screen, measured on Sierra's interpreter: each beat shows the // whole screen moved 8 pixels right and 4 down (the uncovered edges // black) for 4 frames, then in place for 4; a shake is 4 beats per unit // counted in 16 bits, so shake.screen(0) is 65536 beats. #define SHAKE_DX 8 #define SHAKE_DY 4 #define SHAKE_PHASE_FRAMES 4u #define SHAKE_BEATS_PER_UNIT 4u #define MS_PER_SECOND 1000u // A chunky surface's byte holds two pixels. #define CHUNKY_PX_PER_BYTE (SURFACE_WIDTH / SURFACE_BYTES_PER_ROW) typedef struct { int16_t x; int16_t y; int16_t w; int16_t h; } GfxRectT; // ----- Prototypes ----- static void blitObject(uint8_t n); static void buildShifted(jlSurfaceT *shifted, const jlSurfaceT *normal); static void drawCel(const AgiCelT *cel, bool mirror, int16_t left, int16_t top, uint8_t priority, bool keepPriority); static void drawList(bool updating); static uint8_t effectivePriority(int16_t x, int16_t y); static void markDirty(int16_t x, int16_t y, int16_t w, int16_t h); static void pushRect(const GfxRectT *r); static void pushRuns(jlSurfaceT *stage, int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t screenY); static void restoreRect(const uint8_t *save, int16_t x, int16_t y, uint8_t w, uint8_t h); static void saveRect(uint8_t *save, int16_t x, int16_t y, uint8_t w, uint8_t h); static void shakePhase(const jlSurfaceT *show, uint32_t phaseMs); static int16_t sortKey(const AgiObjectT *o); static void unblitTop(void); // ----- Module state ----- static uint8_t *gVis; static uint8_t *gPri; static uint8_t *gSaveArena; static uint16_t gSaveTop; // Objects in the order they were blitted; the first gStaticBlits of them // are the not-updated batch. static uint8_t gBlitStack[AGI_MAX_OBJECTS]; static uint8_t gBlitCount; static uint8_t gStaticBlits; static GfxRectT gDirty[DIRTY_MAX]; static uint8_t gDirtyCount; static bool gPicShown; // ----- Internal helpers (alphabetical) ----- static void blitObject(uint8_t n) { AgiObjectT *o; const AgiCelT *cel; bool mirror; int16_t left; int16_t top; uint16_t need; o = &gAgi->objects[n]; cel = agiViewCel(o->view, o->loop, o->cel, &mirror); if (cel == NULL || cel->width == 0u || cel->height == 0u) { return; } left = o->x; top = (int16_t)(o->y - (int16_t)cel->height + 1); // Clip the save rectangle to the buffer. o->blitX = left; o->blitY = (top < 0) ? 0 : top; o->blitW = cel->width; if (left + (int16_t)cel->width > AGI_WIDTH) { o->blitW = (uint8_t)(AGI_WIDTH - left); } o->blitH = (uint8_t)(o->y - o->blitY + 1); if (left < 0 || left >= AGI_WIDTH || o->y < 0 || o->y > AGI_MAX_Y || o->blitW == 0u) { return; } need = (uint16_t)((uint16_t)o->blitW * o->blitH * 2u); if ((uint32_t)gSaveTop + need > SAVE_ARENA_BYTES) { return; } o->saveUnder = &gSaveArena[gSaveTop]; saveRect(o->saveUnder, left, o->blitY, o->blitW, o->blitH); gSaveTop = (uint16_t)(gSaveTop + need); o->blitted = true; gBlitStack[gBlitCount] = n; gBlitCount++; if ((o->flags & OBJ_FIX_PRIORITY) == 0u) { o->priority = agiPriorityForY(o->y); } drawCel(cel, mirror, left, top, o->priority, false); markDirty(o->blitX, o->blitY, o->blitW, o->blitH); } // The screen as a shake beat shows it: moved SHAKE_DX right and SHAKE_DY // down over black. Chunky surfaces move their bytes; planar ones draw the // screen as one big sprite (colour 0 is transparent, and the surface is // black beneath). static void buildShifted(jlSurfaceT *shifted, const jlSurfaceT *normal) { jlSpriteT *sprite; uint16_t y; jlSurfaceCopy(shifted, normal); jlSurfaceClear(shifted, AGI_COLOR_BLACK); if (shifted->pixels != NULL && normal->pixels != NULL) { for (y = SHAKE_DY; y < SURFACE_HEIGHT; y++) { memcpy(&shifted->pixels[y * SURFACE_BYTES_PER_ROW + SHAKE_DX / CHUNKY_PX_PER_BYTE], &normal->pixels[(uint16_t)(y - SHAKE_DY) * SURFACE_BYTES_PER_ROW], SURFACE_BYTES_PER_ROW - SHAKE_DX / CHUNKY_PX_PER_BYTE); } return; } sprite = jlSpriteCreateFromSurface(normal, 0, 0, TILE_BLOCKS_PER_ROW, TILE_BLOCKS_PER_COL); if (sprite != NULL) { jlSpriteDraw(shifted, sprite, SHAKE_DX, SHAKE_DY); jlSpriteDestroy(sprite); } } // Draw a cel's opaque pixels into the buffers where its priority is at // least the buffer's (control lines take the priority below them). // keepPriority leaves the priority buffer alone (add.to.pic writes it). static void drawCel(const AgiCelT *cel, bool mirror, int16_t left, int16_t top, uint8_t priority, bool keepPriority) { const uint8_t *rle; uint8_t row; int16_t col; int16_t y; int16_t x; uint8_t color; uint8_t run; uint16_t idx; rle = cel->rle; for (row = 0u; row < cel->height; row++) { y = (int16_t)(top + row); col = 0; while (*rle != 0u) { color = (uint8_t)(*rle >> NIBBLE_SHIFT); run = (uint8_t)(*rle & RLE_COUNT_MASK); rle++; if (color == cel->transparent || y < 0 || y > AGI_MAX_Y) { col = (int16_t)(col + run); continue; } while (run > 0u) { x = mirror ? (int16_t)(left + (int16_t)cel->width - 1 - col) : (int16_t)(left + col); if (x >= 0 && x < AGI_WIDTH && col < (int16_t)cel->width) { idx = (uint16_t)((uint16_t)y * AGI_WIDTH + (uint16_t)x); if (priority >= effectivePriority(x, y)) { gVis[idx] = color; if (!keepPriority) { gPri[idx] = priority; } } } col++; run--; } } rle++; } } // Blit one batch, sorted by depth: the not-updated objects or the updated // ones. static void drawList(bool updating) { uint8_t order[AGI_MAX_OBJECTS]; uint8_t count; uint8_t i; uint8_t j; uint16_t want; want = updating ? (uint16_t)(OBJ_ANIMATED | OBJ_DRAWN | OBJ_UPDATE) : (uint16_t)(OBJ_ANIMATED | OBJ_DRAWN); count = 0u; for (i = 0u; i < gAgi->objectCount; i++) { const AgiObjectT *o = &gAgi->objects[i]; if ((o->flags & (OBJ_ANIMATED | OBJ_DRAWN | OBJ_UPDATE)) != want || o->blitted || o->view == NULL) { continue; } // Insertion sort: by depth, then by object number. j = count; while (j > 0u && sortKey(&gAgi->objects[order[j - 1u]]) > sortKey(o)) { order[j] = order[j - 1u]; j--; } order[j] = i; count++; } for (i = 0u; i < count; i++) { blitObject(order[i]); } } // The priority a pixel has for drawing: control lines (0..3) take the // priority of the first non-control pixel below them. static uint8_t effectivePriority(int16_t x, int16_t y) { uint8_t p; for (;;) { p = gPri[(uint16_t)y * AGI_WIDTH + (uint16_t)x]; if (p > AGI_CTL_WATER) { return p; } y++; if (y > AGI_MAX_Y) { return AGI_PRIORITY_MAX; } } } static void markDirty(int16_t x, int16_t y, int16_t w, int16_t h) { GfxRectT *r; int16_t right; int16_t bottom; if (w <= 0 || h <= 0) { return; } if (gDirtyCount < DIRTY_MAX) { r = &gDirty[gDirtyCount]; gDirtyCount++; r->x = x; r->y = y; r->w = w; r->h = h; return; } // Out of slots: grow the last one to cover this one too. r = &gDirty[DIRTY_MAX - 1u]; right = (int16_t)(r->x + r->w); bottom = (int16_t)(r->y + r->h); if (x + w > right) { right = (int16_t)(x + w); } if (y + h > bottom) { bottom = (int16_t)(y + h); } if (x < r->x) { r->x = x; } if (y < r->y) { r->y = y; } r->w = (int16_t)(right - r->x); r->h = (int16_t)(bottom - r->y); } // Copy a buffer rectangle to the stage, each AGI pixel two screen pixels: // straight into a chunky stage's bytes, or as runs on a planar one. static void pushRect(const GfxRectT *r) { jlSurfaceT *stage; int16_t x0; int16_t y0; int16_t x1; int16_t y1; int16_t x; int16_t y; int16_t screenY; uint8_t c; x0 = (r->x < 0) ? 0 : r->x; y0 = (r->y < 0) ? 0 : r->y; x1 = (int16_t)(r->x + r->w); y1 = (int16_t)(r->y + r->h); if (x1 > AGI_WIDTH) { x1 = AGI_WIDTH; } if (y1 > AGI_HEIGHT) { y1 = AGI_HEIGHT; } if (x0 >= x1 || y0 >= y1) { return; } stage = jlStageGet(); screenY = (int16_t)((int16_t)gAgi->picRow * AGI_CELL_PX); if (stage->pixels == NULL) { pushRuns(stage, x0, y0, x1, y1, screenY); return; } for (y = y0; y < y1; y++) { const uint8_t *src = &gVis[(uint16_t)y * AGI_WIDTH]; uint8_t *dst = &stage->pixels[(uint16_t)(screenY + y) * SURFACE_BYTES_PER_ROW]; for (x = x0; x < x1; x++) { c = src[x]; dst[x] = (uint8_t)((c << NIBBLE_SHIFT) | c); } } surfaceMarkDirtyRect(stage, (int16_t)(x0 * AGI_SCREEN_PX_PER_AGI), (int16_t)(screenY + y0), (int16_t)((x1 - x0) * AGI_SCREEN_PX_PER_AGI), (int16_t)(y1 - y0)); } // Planar stages (Amiga, ST, X68000) have no bytes to write: each row goes // out as runs of one colour through jlFillRect, which the ports do fast. static void pushRuns(jlSurfaceT *stage, int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t screenY) { const uint8_t *src; int16_t x; int16_t y; int16_t run; uint8_t c; for (y = y0; y < y1; y++) { src = &gVis[(uint16_t)y * AGI_WIDTH]; x = x0; while (x < x1) { c = src[x]; run = 1; while (x + run < x1 && src[x + run] == c) { run++; } jlFillRect(stage, (int16_t)(x * AGI_SCREEN_PX_PER_AGI), (int16_t)(screenY + y), (uint16_t)(run * AGI_SCREEN_PX_PER_AGI), 1u, c); x = (int16_t)(x + run); } } } // Put back a rectangle of both buffers that saveRect kept. static void restoreRect(const uint8_t *save, int16_t x, int16_t y, uint8_t w, uint8_t h) { uint8_t row; uint16_t dst; for (row = 0u; row < h; row++) { dst = (uint16_t)((uint16_t)(y + row) * AGI_WIDTH + (uint16_t)x); memcpy(&gVis[dst], save, w); save += w; memcpy(&gPri[dst], save, w); save += w; } } // Keep a rectangle of both buffers, a row of colours then a row of // priorities at a time (2 * w * h bytes). static void saveRect(uint8_t *save, int16_t x, int16_t y, uint8_t w, uint8_t h) { uint8_t row; uint16_t src; for (row = 0u; row < h; row++) { src = (uint16_t)((uint16_t)(y + row) * AGI_WIDTH + (uint16_t)x); memcpy(save, &gVis[src], w); save += w; memcpy(save, &gPri[src], w); save += w; } } // One phase of a shake beat: `show` on the screen for phaseMs (NULL, for // want of memory, is only the wait). static void shakePhase(const jlSurfaceT *show, uint32_t phaseMs) { uint32_t start; if (show != NULL) { jlSurfaceCopy(jlStageGet(), show); } start = jlMillisElapsed(); do { agiIdle(); } while (jlMillisElapsed() - start < phaseMs); } // Objects sort by their baseline, or by the top of their band when their // priority is fixed. static int16_t sortKey(const AgiObjectT *o) { if ((o->flags & OBJ_FIX_PRIORITY) != 0u) { return agiYForPriority(o->priority); } return o->y; } static void unblitTop(void) { AgiObjectT *o; gBlitCount--; o = &gAgi->objects[gBlitStack[gBlitCount]]; restoreRect(o->saveUnder, o->blitX, o->blitY, o->blitW, o->blitH); gSaveTop = (uint16_t)(o->saveUnder - gSaveArena); o->blitted = false; markDirty(o->blitX, o->blitY, o->blitW, o->blitH); } // ----- Public API (alphabetical) ----- // add.to.pic: draw a cel into the picture itself with a fixed priority // (0 means the priority of its baseline) and, for margins 0..3, a box of // that control value along its base. void gfxAddToPic(uint8_t viewId, uint8_t loop, uint8_t cel, int16_t x, int16_t y, uint8_t priority, uint8_t margin) { const AgiViewT *view; const AgiCelT *c; bool mirror; int16_t top; int16_t boxTop; int16_t i; view = agiViewGet(viewId); c = agiViewCel(view, loop, cel, &mirror); if (c == NULL) { return; } if (priority < AGI_PRIORITY_MIN) { priority = agiPriorityForY(y); } gfxEraseAll(); top = (int16_t)(y - (int16_t)c->height + 1); drawCel(c, mirror, x, top, priority, false); // The margin box (measured): an outline of the control value, as wide // as the cel, from its baseline up to the top of the baseline's band. if (margin < MARGIN_NONE) { boxTop = agiYForPriority(agiPriorityForY(y)); for (i = 0; i < (int16_t)c->width; i++) { int16_t yy; if (x + i < 0 || x + i > AGI_MAX_X) { continue; } for (yy = boxTop; yy <= y; yy++) { if (yy == y || yy == boxTop || i == 0 || i == (int16_t)c->width - 1) { gPri[(uint16_t)yy * AGI_WIDTH + (uint16_t)(x + i)] = margin; } } } } markDirty(x, top, c->width, c->height); gfxRedrawAll(); } // The priority or control value at an AGI pixel, for collision tests. uint8_t gfxControlAt(int16_t x, int16_t y) { if (x < 0 || x > AGI_MAX_X || y < 0 || y > AGI_MAX_Y) { return AGI_PRIORITY_MAX; } return gPri[(uint16_t)y * AGI_WIDTH + (uint16_t)x]; } // draw.pic (clear first) and overlay.pic: draw a picture into the buffers // under the objects. The screen keeps the old picture until show.pic. void gfxDrawPic(uint8_t picId, bool clearFirst) { uint8_t *data; uint16_t length; data = agiResLoad(&gAgi->game, AGI_RES_PIC, picId, &length); if (data == NULL) { return; } gfxEraseAll(); if (clearFirst) { memset(gVis, AGI_PIC_BG_COLOR, AGI_PIXELS); memset(gPri, AGI_PIC_BG_PRIORITY, AGI_PIXELS); } (void)agiPicDecode(gVis, gPri, data, length); jlFree(data); gDirtyCount = 0u; gPicShown = false; drawList(false); gStaticBlits = gBlitCount; drawList(true); gDirtyCount = 0u; } // Draw the objects that are drawn but not yet blitted (after a change), // keeping the not-updated ones under the updated ones. void gfxDrawStatic(void) { drawList(false); gStaticBlits = gBlitCount; drawList(true); } void gfxEraseAll(void) { while (gBlitCount > 0u) { unblitTop(); } gStaticBlits = 0u; } void gfxEraseUpdating(void) { while (gBlitCount > gStaticBlits) { unblitTop(); } } void gfxFree(void) { jlFree(gVis); jlFree(gPri); jlFree(gSaveArena); gVis = NULL; gPri = NULL; gSaveArena = NULL; } bool gfxInit(void) { gVis = (uint8_t *)jlAlloc(AGI_PIXELS); gPri = (uint8_t *)jlAlloc(AGI_PIXELS); gSaveArena = (uint8_t *)jlAlloc(SAVE_ARENA_BYTES); if (gVis == NULL || gPri == NULL || gSaveArena == NULL) { gfxFree(); return false; } memset(gVis, AGI_COLOR_BLACK, AGI_PIXELS); memset(gPri, AGI_PIC_BG_PRIORITY, AGI_PIXELS); gSaveTop = 0u; gBlitCount = 0u; gStaticBlits = 0u; gDirtyCount = 0u; gPicShown = false; return true; } // Copy every area that changed since the last call to the stage. void gfxPresentDirty(void) { uint8_t i; if (gPicShown && !gAgi->textMode) { for (i = 0u; i < gDirtyCount; i++) { pushRect(&gDirty[i]); } } gDirtyCount = 0u; } // After a change to which objects are drawn or updated: take them all off // and put them back in the right order. void gfxRedrawAll(void) { gfxEraseAll(); gfxDrawStatic(); gfxPresentDirty(); } void gfxRedrawUpdating(void) { drawList(true); } // Copy the whole picture area to the stage (after text mode or a window). void gfxRestorePicture(void) { GfxRectT r; if (!gPicShown || gAgi->textMode) { return; } r.x = 0; r.y = 0; r.w = AGI_WIDTH; r.h = AGI_HEIGHT; pushRect(&r); } // shake.screen: the whole screen jumps and comes back, beat after beat, // as long as the game waits (keys, sound and the clock carry on). Each // phase lasts SHAKE_PHASE_FRAMES of this display's frames, counted from // when it was put up, so a machine that takes longer than that to change // the whole screen (the IIgs) shows each phase for as long as that takes. // Without the memory for the two screens it is just as long a pause. void gfxShakeScreen(uint8_t times) { jlSurfaceT *normal; jlSurfaceT *shifted; uint16_t beats; uint32_t phaseMs; normal = jlSurfaceCreate(); shifted = jlSurfaceCreate(); if (normal == NULL || shifted == NULL) { jlSurfaceDestroy(shifted); jlSurfaceDestroy(normal); normal = NULL; shifted = NULL; } else { jlSurfaceCopy(normal, jlStageGet()); buildShifted(shifted, normal); } phaseMs = (uint32_t)SHAKE_PHASE_FRAMES * MS_PER_SECOND / jlFrameHz(); beats = (uint16_t)((uint16_t)times * SHAKE_BEATS_PER_UNIT); do { shakePhase(shifted, phaseMs); shakePhase(normal, phaseMs); beats--; } while (beats != 0u); jlSurfaceDestroy(shifted); jlSurfaceDestroy(normal); } // show.obj: a view's first cel at the bottom middle of the picture, on // the stage only: drawn into the buffers over everything, copied out and // the buffers put back (the caller restores the screen after). void gfxShowObjectCel(uint8_t viewId) { const AgiCelT *cel; bool mirror; GfxRectT r; uint8_t *save; cel = agiViewCel(agiViewGet(viewId), 0u, 0u, &mirror); if (cel == NULL || cel->width > AGI_WIDTH || cel->height > AGI_HEIGHT) { return; } r.x = (int16_t)((AGI_MAX_X - (int16_t)cel->width) / 2); r.y = (int16_t)(AGI_HEIGHT - (int16_t)cel->height); r.w = cel->width; r.h = cel->height; if ((uint32_t)gSaveTop + 2u * (uint32_t)r.w * (uint32_t)r.h > SAVE_ARENA_BYTES) { return; } save = &gSaveArena[gSaveTop]; saveRect(save, r.x, r.y, cel->width, cel->height); drawCel(cel, mirror, r.x, r.y, AGI_PRIORITY_MAX, true); pushRect(&r); restoreRect(save, r.x, r.y, cel->width, cel->height); } void gfxShowPic(void) { gPicShown = true; gDirtyCount = 0u; gfxRestorePicture(); } // show.pri.screen: the priority buffer as colours until a key. void gfxShowPriority(void) { uint8_t *saved; saved = gVis; gVis = gPri; gfxRestorePicture(); gVis = saved; (void)inputWaitKey(); gfxRestorePicture(); }