joeylib2/src/core/surface.c

317 lines
9.6 KiB
C

// Surface allocation, destruction, persistence, and the library-owned
// stage (the back-buffer surface).
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}