joeylib2/src/port/amiga/hal.c

461 lines
15 KiB
C

// Commodore Amiga HAL for M2 + M2.5.
//
// M2 scope:
// * OpenScreen (Intuition) for a CUSTOMSCREEN at 320x200x4 bitplanes.
// * Chunky 4bpp to 4 separate bitplanes c2p at present time.
// * Partial-rect present covers only the dirty scanlines.
//
// M2.5 scope (per-scanline palette / SCB emulation):
// * Build a user copper list that WAITs for each display scanline
// and MOVEs the 16 color registers with that line's palette.
// * Install it via ViewPort.UCopIns + MakeScreen + RethinkDisplay.
// * Rebuild only when SCB or palette state differs from the last
// presented frame (cached in gCachedScb / gCachedPalette). On
// clean frames (typical game loop, where only pixel bytes change)
// we skip AllocMem + MrgCop + LoadView + WaitTOF entirely.
//
// Deferred:
// * Blitter-assisted c2p for speed on A500.
// * Takeover mode (LoadView(NULL) + OwnBlitter + direct hardware).
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <exec/types.h>
#include <intuition/intuition.h>
#include <intuition/screens.h>
#include <graphics/copper.h>
#include <graphics/gfxbase.h>
#include <graphics/gfxmacros.h>
#include <graphics/displayinfo.h>
#include <graphics/modeid.h>
#include <graphics/rastport.h>
#include <graphics/view.h>
#include <hardware/custom.h>
#include <proto/exec.h>
#include <proto/intuition.h>
#include <proto/graphics.h>
#include "hal.h"
#include "surfaceInternal.h"
extern struct Custom custom;
// ----- Constants -----
#define AMIGA_BITPLANES 4
#define AMIGA_BYTES_PER_ROW 40
// ----- Prototypes -----
static void buildCopperList(const SurfaceT *src);
static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1);
static void dumpCopperList(void);
static void installCopperList(void);
static void uploadFirstBandPalette(const SurfaceT *src);
static void updateCopperIfNeeded(const SurfaceT *src);
// ----- Module state -----
struct Screen *gScreen = NULL; // shared with input.c
static struct BitMap *gBitMap = NULL;
static UBYTE *gPlanes[AMIGA_BITPLANES];
static struct UCopList *gNewUCL = NULL; // built but not yet installed
// Cached SCB + palettes from the last present. halPresent* only needs
// to rebuild/install the copper list when SCB assignments or palette
// RGB values differ from what is already on screen; pure pixel updates
// (which dominate a typical game loop and every frame of the keys
// demo after the initial paint) leave both alone. MrgCop + LoadView +
// WaitTOF is hundreds of milliseconds on a 7 MHz 68000, so skipping
// them on clean frames is a major win.
static uint8_t gCachedScb [SURFACE_HEIGHT];
static uint16_t gCachedPalette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE];
static bool gCacheValid = false;
static bool paletteOrScbChanged(const SurfaceT *src);
// ----- Internal helpers (alphabetical) -----
// Convert a range of chunky scanlines [y0, y1) to Amiga planar.
// Each plane scanline is 40 bytes (1 bit per pixel x 320 pixels).
// For each destination byte, 8 pixels' worth of 4bpp chunky source is
// read and split into one bit per plane.
static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1) {
const uint8_t *srcLine;
UBYTE *p0;
UBYTE *p1;
UBYTE *p2;
UBYTE *p3;
int16_t y;
uint16_t planarByte;
uint16_t px;
uint16_t pixel;
uint8_t srcByte;
uint8_t nibble;
uint8_t bit;
uint8_t b0;
uint8_t b1;
uint8_t b2;
uint8_t b3;
for (y = y0; y < y1; y++) {
srcLine = &src->pixels[y * SURFACE_BYTES_PER_ROW];
p0 = &gPlanes[0][y * AMIGA_BYTES_PER_ROW];
p1 = &gPlanes[1][y * AMIGA_BYTES_PER_ROW];
p2 = &gPlanes[2][y * AMIGA_BYTES_PER_ROW];
p3 = &gPlanes[3][y * AMIGA_BYTES_PER_ROW];
for (planarByte = 0; planarByte < AMIGA_BYTES_PER_ROW; planarByte++) {
b0 = 0;
b1 = 0;
b2 = 0;
b3 = 0;
for (px = 0; px < 8; px++) {
pixel = (uint16_t)(planarByte * 8 + px);
srcByte = srcLine[pixel >> 1];
nibble = (uint8_t)((pixel & 1) ? (srcByte & 0x0F) : (srcByte >> 4));
bit = (uint8_t)(7 - px);
b0 = (uint8_t)(b0 | (((nibble >> 0) & 1) << bit));
b1 = (uint8_t)(b1 | (((nibble >> 1) & 1) << bit));
b2 = (uint8_t)(b2 | (((nibble >> 2) & 1) << bit));
b3 = (uint8_t)(b3 | (((nibble >> 3) & 1) << bit));
}
p0[planarByte] = b0;
p1[planarByte] = b1;
p2[planarByte] = b2;
p3[planarByte] = b3;
}
}
}
// Build a user copper list for per-scanline palette (SCB emulation).
// One WAIT + 16 MOVEs per displayed scanline + one CEND. The list is
// stored in gNewUCL until installCopperList swaps it onto the screen.
// DyOffset tells us where display line 0 sits in hardware coordinates
// so the WAITs line up with the real visible region regardless of
// PAL/NTSC or any overscan the user may have configured.
static void buildCopperList(const SurfaceT *src) {
struct UCopList *ucl;
UWORD line;
UWORD col;
UBYTE palIdx;
UWORD prevPalIdx;
UWORD vpos;
UWORD topBorder;
UWORD bandCount;
ucl = (struct UCopList *)AllocMem(sizeof(struct UCopList),
MEMF_PUBLIC | MEMF_CLEAR);
if (ucl == NULL) {
gNewUCL = NULL;
return;
}
// Worst-case reservation is one band-change per scanline (16 MOVEs
// + 1 WAIT per change), plus the terminal wait. For realistic SCB
// tables the actual count is far smaller, but CINIT only takes a
// single number so we size for the cap.
CINIT(ucl, (SURFACE_HEIGHT * 17) + 1);
// Hardware scanline where display line 0 lives. 0x2C is the
// standard top border for a PAL screen at TopEdge=0; we hardcode
// rather than reading ViewPort.DyOffset because DyOffset is a
// signed +/- adjustment around the standard value, not the
// absolute hardware line.
// User-copper vpos values are DISPLAY-RELATIVE -- graphics.lib
// MrgCop adds the active View's DyOffset to each WAIT before
// emitting, so a vp=0 user WAIT lands at beam line DyOffset,
// which is where Intuition places display line 0. Emitting at
// vpos=line keeps merged vpos under 256 even for the last band
// (175 + 44 = 219 < 256), avoiding MrgCop's destructive wrap-
// handling path that would otherwise disable bitplane DMA at
// the viewport end.
topBorder = 0;
prevPalIdx = 0xFFFF;
bandCount = 0;
for (line = 0; line < SURFACE_HEIGHT; line++) {
palIdx = src->scb[line];
if (palIdx >= SURFACE_PALETTE_COUNT) {
palIdx = 0;
}
if ((UWORD)palIdx == prevPalIdx) {
continue;
}
vpos = (UWORD)(line + topBorder);
CWAIT(ucl, vpos, 0);
for (col = 0; col < SURFACE_COLORS_PER_PALETTE; col++) {
CMOVE(ucl, custom.color[col], src->palette[palIdx][col]);
}
prevPalIdx = (UWORD)palIdx;
bandCount++;
}
(void)bandCount;
CEND(ucl);
gNewUCL = ucl;
}
// Swap the freshly built user copper list onto the screen's ViewPort
// and force a full graphics-library recomputation of the hardware
// copper list. MakeScreen regenerates the viewport copper to include
// our UCopIns; MrgCop merges every viewport's copper into one hardware
// list; LoadView swaps the live copper pointers. Calling the graphics
// primitives directly (rather than only Intuition's RethinkDisplay /
// RemakeDisplay) was observed here to be the step that actually makes
// the user copper list visible -- Intuition's wrappers sometimes
// skipped the merge.
static void installCopperList(void) {
struct View *view;
if (gNewUCL == NULL || gScreen == NULL) {
return;
}
Forbid();
if (gScreen->ViewPort.UCopIns != NULL) {
FreeVPortCopLists(&gScreen->ViewPort);
}
gScreen->ViewPort.UCopIns = gNewUCL;
gNewUCL = NULL;
Permit();
MakeScreen(gScreen);
view = ViewAddress();
Forbid();
MrgCop(view);
LoadView(view);
Permit();
WaitTOF();
}
// Diagnostic: dump the merged hardware copper list (LOFCprList) to a
// text file on the current volume. Written once per halPresent after
// MrgCop, so the host can inspect exactly what the copper is being
// asked to execute. Each line is either MOVE (destination offset +
// data) or WAIT (vp, hp, mask). The dump stops at the first "end of
// copper" marker (0xFFFF + any mask with bit15 clear would be a wait
// past frame end).
static void dumpCopperList(void) {
FILE *fp;
struct View *view;
struct cprlist *cl;
UWORD *p;
WORD i;
WORD count;
UWORD w1;
UWORD w2;
fp = fopen("copper.txt", "w");
if (fp == NULL) {
return;
}
view = ViewAddress();
if (view == NULL) {
fprintf(fp, "view is NULL\n");
fclose(fp);
return;
}
cl = view->LOFCprList;
if (cl == NULL) {
fprintf(fp, "LOFCprList is NULL\n");
fclose(fp);
return;
}
p = cl->start;
count = cl->MaxCount;
fprintf(fp, "LOFCprList.start=0x%08lx MaxCount=%d\n",
(unsigned long)p, (int)count);
fprintf(fp, "vp.DyOffset=%d vp.DxOffset=%d\n",
(int)gScreen->ViewPort.DyOffset,
(int)gScreen->ViewPort.DxOffset);
fprintf(fp, "view.DyOffset=%d view.DxOffset=%d\n",
(int)view->DyOffset, (int)view->DxOffset);
fprintf(fp, "--\n");
if (p == NULL) {
fclose(fp);
return;
}
for (i = 0; i < count; i++) {
w1 = p[i * 2];
w2 = p[i * 2 + 1];
if (w1 == 0xFFFF && w2 == 0xFFFE) {
fprintf(fp, "%4d: END %04x %04x\n", (int)i, w1, w2);
break;
}
if (w1 & 1) {
fprintf(fp, "%4d: %s vp=%3d hp=%3d mask=%04x\n",
(int)i,
(w2 & 0x8000) ? "SKIP" : "WAIT",
(int)(w1 >> 8),
(int)(w1 & 0xFE),
(unsigned)w2);
} else {
fprintf(fp, "%4d: MOVE dst=%03x data=%04x\n",
(int)i,
(unsigned)(w1 & 0x1FE),
(unsigned)w2);
}
}
fclose(fp);
}
// Returns true if the SCB table or palette RGB values differ from the
// last presented frame, or if no frame has been presented yet.
static bool paletteOrScbChanged(const SurfaceT *src) {
if (!gCacheValid) {
return true;
}
if (memcmp(gCachedScb, src->scb, sizeof(gCachedScb)) != 0) {
return true;
}
if (memcmp(gCachedPalette, src->palette, sizeof(gCachedPalette)) != 0) {
return true;
}
return false;
}
// Rebuild and install the user copper list only if the palette/SCB
// state visible to the display differs from what the surface carries
// now. On clean frames we skip the AllocMem + MrgCop + LoadView +
// WaitTOF chain entirely.
static void updateCopperIfNeeded(const SurfaceT *src) {
if (!paletteOrScbChanged(src)) {
return;
}
uploadFirstBandPalette(src);
buildCopperList(src);
installCopperList();
memcpy(gCachedScb, src->scb, sizeof(gCachedScb));
memcpy(gCachedPalette, src->palette, sizeof(gCachedPalette));
gCacheValid = true;
}
// Load the first band's palette into the screen's ColorMap so the
// Intuition-generated frame-start copper writes those values on each
// frame. This acts as a safety net: even if our user copper list does
// not fire (or fires late) for the very first band, the top of the
// display still shows the correct colors because Intuition's own
// COLORxx loads happen before any user copper instruction.
static void uploadFirstBandPalette(const SurfaceT *src) {
UWORD aPalette[SURFACE_COLORS_PER_PALETTE];
UWORD i;
UBYTE palIdx;
palIdx = src->scb[0];
if (palIdx >= SURFACE_PALETTE_COUNT) {
palIdx = 0;
}
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
aPalette[i] = (UWORD)src->palette[palIdx][i];
}
LoadRGB4(&gScreen->ViewPort, aPalette, SURFACE_COLORS_PER_PALETTE);
}
// ----- HAL API (alphabetical) -----
bool halInit(const JoeyConfigT *config) {
uint16_t i;
(void)config;
// SA_DisplayID pins us to OCS PAL low-res so Intuition opens a
// real planar screen rather than an RTG substitute.
gScreen = OpenScreenTags(NULL,
(ULONG)SA_Width, (ULONG)SURFACE_WIDTH,
(ULONG)SA_Height, (ULONG)SURFACE_HEIGHT,
(ULONG)SA_Depth, (ULONG)AMIGA_BITPLANES,
(ULONG)SA_DisplayID, (ULONG)(PAL_MONITOR_ID | LORES_KEY),
(ULONG)SA_DetailPen, (ULONG)0,
(ULONG)SA_BlockPen, (ULONG)1,
(ULONG)SA_Title, (ULONG)"JoeyLib",
(ULONG)SA_Type, (ULONG)CUSTOMSCREEN,
(ULONG)SA_Quiet, (ULONG)TRUE,
TAG_DONE);
if (gScreen == NULL) {
return false;
}
gBitMap = gScreen->RastPort.BitMap;
for (i = 0; i < AMIGA_BITPLANES; i++) {
gPlanes[i] = gBitMap->Planes[i];
if (gPlanes[i] == NULL) {
CloseScreen(gScreen);
gScreen = NULL;
return false;
}
}
return true;
}
const char *halLastError(void) {
return NULL;
}
void halPresent(const SurfaceT *src) {
if (src == NULL || gScreen == NULL) {
return;
}
updateCopperIfNeeded(src);
c2pRange(src, 0, SURFACE_HEIGHT);
}
void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h) {
(void)x;
(void)w;
if (src == NULL || gScreen == NULL) {
return;
}
updateCopperIfNeeded(src);
c2pRange(src, y, y + (int16_t)h);
}
// WaitTOF() blocks the calling task until the next "top of frame"
// VBlank interrupt -- 50 Hz on PAL, 60 Hz on NTSC. graphics.library
// is auto-opened by libnix so no extra plumbing is needed.
void halWaitVBL(void) {
WaitTOF();
}
void halShutdown(void) {
if (gScreen != NULL) {
// CloseScreen should free attached UCopList, but be explicit
// to catch any case where the screen close path skips it.
Forbid();
if (gScreen->ViewPort.UCopIns != NULL) {
FreeVPortCopLists(&gScreen->ViewPort);
}
Permit();
CloseScreen(gScreen);
gScreen = NULL;
gBitMap = NULL;
}
if (gNewUCL != NULL) {
FreeMem(gNewUCL, sizeof(struct UCopList));
gNewUCL = NULL;
}
}