// Surface allocation, destruction, persistence, and the library-owned // stage (the back-buffer surface). #include #include #include #include #include "joey/surface.h" #include "port.h" #include "surfaceInternal.h" #define SURFACE_PALETTE_BYTES (SURFACE_PALETTE_ENTRIES * (uint32_t)sizeof(uint16_t)) #define SURFACE_FILE_BYTES (SURFACE_PIXELS_SIZE + SURFACE_HEIGHT + SURFACE_PALETTE_BYTES) // ----- Prototypes ----- // (public API declared in joey/surface.h) // (internal prototypes declared in surfaceInternal.h) // ----- Module state ----- // Non-static: hot paths read it through surfaceInternal.h's extern to // skip the jlStageGet() cross-TU call (PERF-AUDIT #10/#17). Written // only by stageAlloc/stageFree below. jlSurfaceT *gStage = NULL; uint8_t gStageMinWord[SURFACE_HEIGHT]; uint8_t gStageMaxWord[SURFACE_HEIGHT]; // "Stage SCB / palette has changed since last present-side upload." // A cheap flag check at present time lets a port skip re-uploading the // 200-byte SCB and 512-byte palette when neither changed -- ~7 ms / // frame saved on demos that don't churn palette/SCB (i.e., almost all // demos). // // Initially true so the first present uploads. jlScbSet*, jlPaletteSet, // and the bulk stage-replace paths (jlSurfaceCopy / jlSurfaceLoadFile) // re-mark dirty when the stage's data changes; the per-port present // code reads and clears the flags after uploading. bool gStageScbDirty = true; bool gStagePaletteDirty = true; // ----- Internal helpers (alphabetical) ----- // widenRow serves only the non-IIgs surfaceMarkDirtyRows loop; the // IIgs multi-row path is the asm marker and the single-row path is // inlined by the surfaceMarkDirtyRect macro (surfaceInternal.h). #ifndef JOEYLIB_PLATFORM_IIGS static void widenRow(int16_t y, uint8_t minWord, uint8_t maxWord) { if (minWord < gStageMinWord[y]) { gStageMinWord[y] = minWord; } if (maxWord > gStageMaxWord[y]) { gStageMaxWord[y] = maxWord; } } #endif // ----- Public API (alphabetical) ----- jlSurfaceT *jlStageGet(void) { return gStage; } void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src) { if (dst == NULL || src == NULL || dst == src) { return; } jlpSurfaceCopyChunky(dst, src); /* memcpy on chunky ports; no-op on planar */ memcpy(dst->scb, src->scb, sizeof(src->scb)); memcpy(dst->palette, src->palette, sizeof(src->palette)); jlpSurfaceCopyPlanes(dst, src); /* 4 plane memcpys on planar ports; no-op on chunky */ surfaceMarkDirtyAll(dst); // Bulk-replacing the stage's SCB/palette must re-mark them dirty so // the per-port present re-uploads them; surfaceMarkDirtyAll only // widens the pixel bands. Matches the jlScbSet / jlPaletteSet path. if (dst == gStage) { gStageScbDirty = true; gStagePaletteDirty = true; } } jlSurfaceT *jlSurfaceCreate(void) { jlSurfaceT *s; // jlpBigAlloc, NOT the C heap: on IIgs the C heap is a few hundred // bytes of bank-0 leftover that is NEITHER big enough for this // struct NOR reserved from the Memory Manager (Phase 1 IIgs // stabilization; PERF-AUDIT.md #77/#81). Other ports map // jlpBigAlloc to malloc, so behavior is unchanged there. s = (jlSurfaceT *)jlpBigAlloc((uint32_t)sizeof(jlSurfaceT)); if (s == NULL) { coreSetError("out of memory allocating surface"); return NULL; } memset(s, 0, sizeof(jlSurfaceT)); // jlpSurfaceAllocPixels returns NULL on planar ports (Amiga); the // primary storage is the port-allocated planes via portData below. // Classify NULL by the build's storage model: chunky ports need // pixels, planar ports need portData. A NULL in the required slot // is an out-of-memory failure (header contract: return NULL). s->pixels = jlpSurfaceAllocPixels(); s->portData = jlpSurfaceAllocPortData(s, false); #ifdef JOEYLIB_NATIVE_CHUNKY if (s->pixels == NULL) { jlpSurfaceFreePortData(s, false, s->portData); jlpBigFree(s); coreSetError("out of memory allocating surface pixels"); return NULL; } #else if (s->portData == NULL) { jlpSurfaceFreePixels(s->pixels); jlpBigFree(s); coreSetError("out of memory allocating surface planes"); return NULL; } #endif paletteInitDefault(s); return s; } void jlSurfaceDestroy(jlSurfaceT *s) { if (s == NULL) { return; } if (s == gStage) { return; } jlpSurfaceFreePortData(s, false, s->portData); jlpSurfaceFreePixels(s->pixels); jlpBigFree(s); } // Cross-port hash of the surface's logical pixel content (plus SCB and // palette), delegated to the port HAL so each port reads its native // pixel storage (chunky on IIgs/DOS, planes on Amiga/ST) while // producing the same logical-pixel hash. Used by the UBER validation // harness to pixel-compare ports against an IIgs golden reference. uint32_t jlSurfaceHash(const jlSurfaceT *s) { if (s == NULL) { return 0u; } return jlpSurfaceHash(s); } bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path) { FILE *fp; long fileSize; if (dst == NULL || path == NULL) { return false; } fp = fopen(path, "rb"); if (fp == NULL) { return false; } if (fseek(fp, 0L, SEEK_END) != 0) { fclose(fp); return false; } fileSize = ftell(fp); if (fileSize != (long)SURFACE_FILE_BYTES) { fclose(fp); return false; } if (fseek(fp, 0L, SEEK_SET) != 0) { fclose(fp); return false; } if (!jlpSurfaceLoadFile(dst, fp)) { fclose(fp); return false; } if (fread(dst->scb, 1, SURFACE_HEIGHT, fp) != SURFACE_HEIGHT) { fclose(fp); return false; } if (fread(dst->palette, 1, SURFACE_PALETTE_BYTES, fp) != SURFACE_PALETTE_BYTES) { fclose(fp); return false; } fclose(fp); surfaceMarkDirtyAll(dst); /* Loading a file into the stage overwrites its SCB/palette; re-mark * them dirty so the next present re-uploads them. */ if (dst == gStage) { gStageScbDirty = true; gStagePaletteDirty = true; } return true; } bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path) { FILE *fp; if (src == NULL || path == NULL) { return false; } fp = fopen(path, "wb"); if (fp == NULL) { return false; } if (!jlpSurfaceSaveFile(src, fp)) { fclose(fp); return false; } if (fwrite(src->scb, 1, SURFACE_HEIGHT, fp) != SURFACE_HEIGHT) { fclose(fp); return false; } if (fwrite(src->palette, 1, SURFACE_PALETTE_BYTES, fp) != SURFACE_PALETTE_BYTES) { fclose(fp); return false; } fclose(fp); return true; } void surfaceMarkDirtyAll(const jlSurfaceT *s) { if (s != gStage) { return; } // Whole-stage mark is a constant fill of both band arrays; memset // lowers to a tight fill (MVN-seeded on IIgs) and drops the 200-iter // indexed-store loop. One source of truth for "all rows fully dirty". memset(gStageMinWord, 0, SURFACE_HEIGHT); memset(gStageMaxWord, STAGE_DIRTY_FULL_MAX, SURFACE_HEIGHT); } // Multi-row arm of the surfaceMarkDirtyRect macro (surfaceInternal.h): // widen rows y..yEnd-1 to cover [minWord, maxWord]. IIgs builds never // compile this -- the macro routes them straight to the asm marker // iigsMarkDirtyRowsInner instead. #ifndef JOEYLIB_PLATFORM_IIGS void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) { uint16_t row; for (row = y; row < yEnd; row++) { widenRow((int16_t)row, minWord, maxWord); } } #endif // ----- Internal (alphabetical) ----- bool stageAlloc(void) { if (gStage != NULL) { return true; } // jlpBigAlloc for the same reason as jlSurfaceCreate: the IIgs C // heap can neither hold nor protect this struct. gStage = (jlSurfaceT *)jlpBigAlloc((uint32_t)sizeof(jlSurfaceT)); if (gStage == NULL) { return false; } memset(gStage, 0, sizeof(jlSurfaceT)); // jlpStageAllocPixels returns NULL on planar ports (Amiga) where // the chunky shadow doesn't exist; the planes from portData are // the source of truth. On chunky ports NULL pixels means the // back-buffer alloc failed (IIgs pins a fixed address and never // returns NULL, so this is a real OOM only on DOS). gStage->pixels = jlpStageAllocPixels(); if (gStage->pixels != NULL) { memset(gStage->pixels, 0, SURFACE_PIXELS_SIZE); } gStage->portData = jlpSurfaceAllocPortData(gStage, true); #ifdef JOEYLIB_NATIVE_CHUNKY if (gStage->pixels == NULL) { jlpSurfaceFreePortData(gStage, true, gStage->portData); jlpBigFree(gStage); gStage = NULL; return false; } #else if (gStage->portData == NULL) { jlpStageFreePixels(gStage->pixels); jlpBigFree(gStage); gStage = NULL; return false; } #endif stageDirtyClearAll(); paletteInitDefault(gStage); return true; } void stageDirtyClearAll(void) { // Constant fill of both band arrays, same memset form as its // sibling surfaceMarkDirtyAll: lowers to a tight fill (MVN-seeded // on IIgs) instead of a 200-iteration indexed-store loop, and this // runs after every jlStagePresent. memset(gStageMinWord, STAGE_DIRTY_CLEAN_MIN, SURFACE_HEIGHT); memset(gStageMaxWord, STAGE_DIRTY_CLEAN_MAX, SURFACE_HEIGHT); } void stageFree(void) { if (gStage == NULL) { return; } jlpSurfaceFreePortData(gStage, true, gStage->portData); jlpStageFreePixels(gStage->pixels); jlpBigFree(gStage); gStage = NULL; }