// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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; } }