461 lines
15 KiB
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;
|
|
}
|
|
}
|