317 lines
9.6 KiB
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;
|
|
}
|