// Apple IIgs HAL: enable SHR, write pixels / SCBs / palettes into the // $E1 bank at the stock addresses the shifter reads from. // // Memory map in bank $E1: // $2000 - $9CFF pixel data (32000 bytes, 160 bytes per scanline) // $9D00 - $9DC7 SCB bytes (200 used) // $9E00 - $9FFF 16 palettes x 16 colors x 2 bytes, $0RGB // // NEWVIDEO register at $00C029 controls SHR enable. Bit 7 turns SHR on. // Long (24-bit) addresses are used so they resolve to bank $E1. // // DIRTY-WALK + PEI-SLAM PRESENT // ----------------------------- // jlpPresent walks the per-row dirty bands maintained by drawing // primitives in src/core/*.c. Fully-dirty rows go through the PEI // slam in src/iigs/peislam.s (~530 cyc/row, ~55% faster than // memcpy/MVN); partial-dirty rows use memcpy, which lowers // to MVN (7 cyc/byte) -- the fastest 65816 way to move bytes into // bank $E1 when the dirty band is too narrow to amortize the slam's // per-call AUXWRITE/RAMRD/shadow toggle. // // peislam.s declares its load segment as DRAWPRIMS so the linker // places it in its own bank, separate from AUDIO's _ROOT (where // audio_full.c + Memory Manager + stdio + NTPstreamsound already // crowd up against the 64 KB-per-bank limit). #include #include #include #include "joey/debug.h" #include "port.h" #include "surfaceInternal.h" // Memory Manager surface for jlpBigAlloc / jlpBigFree (jlAlloc backing). // Same idiom as src/iigs/audio_full.c: Handles/pointers are void*, // the owner ID comes from MMStartUp(), and the attribute bit values are // Apple's Memory Manager constants. Only the bits we use are defined. #include typedef void *Handle; #define _ownerid (MMStartUp()) #define attrAddr 0x0002 #define attrNoSpec 0x0020 #define attrFixed 0x4000 #define attrLocked 0x8000 // Leading bookkeeping slot: jlAlloc/jlFree traffic in plain pointers but // DisposeHandle needs the Handle, so we over-allocate by one Handle and // stash it just before the pointer we return. #define BIG_ALLOC_HDR ((uint32_t)sizeof(Handle)) /* GetTick wrapper in peislam.s: invokes the Misc Toolset GetTick * ($2503) and returns the low 16 bits of the system's tick counter * (firmware VBL ISR-driven). Polling $C019 from C user code missed * transitions for any op over ~1 ms; the system's tick counter is * updated by the actual interrupt handler so it stays accurate * regardless of caller polling rate. Tick rate matches the video * field rate -- 60 Hz on NTSC, 50 Hz on PAL. */ extern uint16_t iigsGetTickWord(void); /* Reads battery RAM hrtz50or60: 0 = NTSC, 1 = PAL. */ extern uint16_t iigsReadHzParam(void); static uint16_t gFrameHz = 60u; // hal.c is the single TU that calls into joeyDraw.asm. The IIgs jlp // macros in core/port.h expand to these asm entry points; cross-platform // draw.c / tile.c / etc. never reference the asm symbols directly. Keeping // the asm externs in a single TU avoids the link-time fragility seen when // each cross-platform TU brought its own asm extern. // 32 KB stack-slam fill via AUXWRITE. ~25 ms full-screen. extern void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord); // 16 fixed-offset word stores along a 160-byte stride. Takes the raw // fill nibble; the asm derives the $NNNN fill word so C never pays the // * 0x1111 software-multiply helper (PERF-AUDIT #41). ~120 cyc per call. extern void iigsTileFillInner(uint8_t *dstRow0, uint16_t nibble); // Tile copy / paste / snap inner loops. All take 4-byte large- // model pointers; bank may differ between dst and src (heap // surface vs stage). Stride contracts: // tileCopyInner / tileCopyMaskedInner: dst 160, src 160 // tilePasteInner / tilePasteMonoInner: dst 160, src 4 // tileSnapInner: dst 4, src 160 // tilePasteMonoInner is the fused mono colorize + paste (PERF-AUDIT #3): // per-byte 2-bit opacity index (static table) into four per-call fg/bg // combo bytes; fg/bg arrive pre-masked to 4 bits (PERF-AUDIT #69). extern void iigsTileCopyInner(uint8_t *dstRow0, const uint8_t *srcRow0); extern void iigsTileCopyMaskedInner(uint8_t *dstRow0, const uint8_t *srcRow0, uint16_t transparent); extern void iigsTilePasteInner(uint8_t *dstRow0, const uint8_t *srcTilePixels); extern void iigsTilePasteMonoInner(uint8_t *dstRow0, const uint8_t *monoTile, uint16_t fgColor, uint16_t bgColor); extern void iigsTileSnapInner(uint8_t *dstTilePixels, const uint8_t *srcRow0); // Single-pixel and Bresenham line plot. jlDrawLine inner takes // pre-clipped endpoints (caller validates against surface bounds); // it does no per-pixel clipping in the loop. extern void iigsDrawPixelInner(uint8_t *pixels, uint16_t x, uint16_t y, uint16_t nibble); extern void iigsDrawLineInner(uint8_t *pixels, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t nibble); // Bresenham midpoint circle outline. Caller has verified the entire // bbox is on-surface so no per-pixel clip. extern void iigsDrawCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble); // Stage-to-SHR pixel upload: per-row dirty-skipped PEI slam (wide // rows) / exact-extent MVN (narrow rows), $01 -> $E1. SCB/palette // upload is NOT here: uploadScbAndPaletteIfNeeded (below) owns it via // dirty-flag-gated C memcpy, and the asm's old scbPtr/palettePtr/ // uploadFlags args plus their dead MVN upload paths were removed // (PERF-AUDIT #52). extern void iigsBlitStageToShr(void); // jlFloodFill walk results: written by iigsFloodWalkAndScansInner, // read back by the jlpFloodWalkAndScans macro (core/port.h). extern uint16_t gFloodSeedMatch; extern uint16_t gFloodLeftX; extern uint16_t gFloodRightX; // Single-call per-popped-seed worker: seed test + walk-left + walk-right // + scan-above + scan-below + push, all sharing cached row addr and // match decoders. Outputs to gFloodSeedMatch / gFloodLeftX / gFloodRightX. extern void iigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y, uint16_t matchColor, uint16_t newColor, uint16_t matchEqual, int16_t *stackX, int16_t *stackY, uint16_t *spInOut, uint16_t maxSp); // One-shot init for the y*160 lookup table (gRowOffsetLut, 400 bytes // in DRAWPRIMS data). Called once from jlpInit. After this returns, // every asm primitive that needs row offset can do `lda >lut,x` instead // of the 7-instruction shift-add. Also builds tpmNibPairIdx (256 // bytes), iigsTilePasteMonoInner's mono-byte -> opacity-pair index // table (PERF-AUDIT #3). extern void iigsInitRowLut(void); // Filled circle, scanline-style. Takes the raw fill nibble; the asm // derives the doubled fill byte and RMW nibble bytes (PERF-AUDIT #41). extern void iigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble); // ----- Hardware addresses (24-bit / long pointers) ----- #define IIGS_NEWVIDEO_REG ((volatile uint8_t *)0x00C029L) #define IIGS_BORDER_REG ((volatile uint8_t *)0x00C034L) #define IIGS_SHADOW_REG ((volatile uint8_t *)0x00C035L) #define IIGS_VBL_STATUS ((volatile uint8_t *)0x00C019L) #define IIGS_SHR_PIXELS ((uint8_t *)0xE12000L) #define IIGS_SHR_SCB ((uint8_t *)0xE19D00L) #define IIGS_SHR_PALETTE ((uint16_t *)0xE19E00L) // The stage lives at $01/2000 -- the same offset as the SHR display // framebuffer at $E1/2000, but in the fast (2.8 MHz) bank. With SHR // shadow inhibited at $C035, writes here are NOT auto-mirrored to // $E1, so drawing is full-speed and isolated from the displayed // frame until the next jlStagePresent. #define IIGS_STAGE_PIXELS ((uint8_t *)0x012000L) #define VBL_BAR_BIT 0x80 // NEWVIDEO bit masks #define NEWVIDEO_SHR_ON 0x80 #define NEWVIDEO_LINEARIZE 0x40 // Bit 0 is documented as reserved-must-be-1 in the IIgs Hardware // Reference for forward compatibility. Real silicon doesn't care, but // GSplus halts on writes that leave it clear (see moremem.c c029 // handler) and bumps its "Code: RED" status. Always include this bit. #define NEWVIDEO_RESERVED_BIT 0x01 // $C035 SHADOW register: bit set = shadow INHIBITED for that range. // Bit 1 = hi-res page 1 ($02000-$03FFF in bank $01) // Bit 2 = hi-res page 2 ($04000-$05FFF in bank $01) // Bit 3 = SHR ($02000-$09FFF in bank $01) // We set 1+2+3 because the SHR pixel range overlaps both hi-res // pages; leaving any of those shadows live would silently mirror // part of the stage to $E1. #define SHADOW_INHIBIT_SHR_MASK 0x0E // $C034 BORDER register: high nibble = beep/IRQ enables (preserve), // low nibble = border color index 0..15. Color 0 is the all-zero // palette entry by IIgs convention; we force the low nibble to 0 // in jlpInit so the visible bezel matches the cleared SHR background. #define BORDER_COLOR_MASK 0xF0 // ----- Module state ----- static uint8_t gPreviousNewVideo = 0; static uint8_t gPreviousBorder = 0; static uint8_t gPreviousShadow = 0; static bool gModeSet = false; // SCB / palette upload skipping is now driven by gStageScbDirty / // gStagePaletteDirty (core/surface.c). The old per-frame memcmp- // against-cached-copy approach was costing ~7 ms / frame. // PEI slam scratch (W2 run-hoisted present). File-scope non-static so // the asm can reference them. All are bank-0 cells the present asm // touches ONLY while AUXWRITE/RAMRD are OFF (bank-0 access with the // switches on is misdirected into the stage pixels). The one datum the // slam loop needs INSIDE a switches-on run -- the per-row SP offset -- // is parked at $E1:9DFE instead: long-addressed, so immune to the // redirects. volatile uint16_t gPeiOrigSp; volatile uint8_t gPeiOrigShadow; volatile uint16_t gPeiCurRow; // scan resume: absolute row volatile uint16_t gPeiChunkRow; // scan resume: in-chunk row volatile uint16_t gPeiMvnCount; // narrow row: MVN byte count - 1 volatile uint16_t gPeiRunFirst; // slam run: first row index volatile uint16_t gPeiRunLen; // slam run: row count (1..40) volatile uint16_t gPeiRunMax; // slam run: rightmost dirty word (low byte valid) volatile uint16_t gPeiRunBytes; // slam run: bytes pushed per row volatile uint16_t gPeiWidthTmp; // scan: band width scratch (low byte valid) // Upload SCB / palette into bank-$E1 SHR memory only when the // matching dirty flag is set. Replaces a per-frame 712-byte memcmp // pair (~7 ms / frame) with a 2-cyc flag check. // gStageScbDirty / gStagePaletteDirty live in core/surface.c; they // start true (forces the very first present to upload), get set true // again whenever jlScbSet* / jlPaletteSet mutate the stage's data, and // get cleared here after upload. static void uploadScbAndPaletteIfNeeded(const jlSurfaceT *src) { if (gStageScbDirty) { memcpy(IIGS_SHR_SCB, src->scb, SURFACE_HEIGHT); gStageScbDirty = false; } if (gStagePaletteDirty) { memcpy(IIGS_SHR_PALETTE, src->palette, sizeof(src->palette)); gStagePaletteDirty = false; } } // ----- HAL API (alphabetical) ----- // Large/persistent allocation via the Memory Manager. The IIgs C heap // is bank-0-only and a single malloc is capped near 32 KB, so buffers // like the AGI view cache (~1.3 MB) and the PIC visual/priority buffers // must be NewHandle'd. We request attrFixed|attrLocked so the block // neither moves nor purges while the caller holds the dereferenced // pointer. The 65816 has no alignment fault, so the BIG_ALLOC_HDR-byte // offset applied to the master pointer is safe for any access pattern. // The returned memory is not zero-filled (matches malloc semantics). void *jlpBigAlloc(uint32_t bytes) { Handle h; uint8_t *base; h = NewHandle((unsigned long)(bytes + BIG_ALLOC_HDR), _ownerid, (unsigned short)(attrFixed | attrLocked), NULL); if (h == NULL) { return NULL; } HLock(h); base = *(uint8_t **)h; if (base == NULL) { DisposeHandle(h); return NULL; } // Stash the Handle in the leading slot so jlpBigFree can recover it. *(Handle *)base = h; return base + BIG_ALLOC_HDR; } void jlpBigFree(void *p) { Handle h; if (p == NULL) { return; } h = *(Handle *)((uint8_t *)p - BIG_ALLOC_HDR); DisposeHandle(h); } static void claimStagePixels(void); bool jlpInit(const jlConfigT *config) { (void)config; claimStagePixels(); gPreviousNewVideo = *IIGS_NEWVIDEO_REG; gPreviousBorder = *IIGS_BORDER_REG; gPreviousShadow = *IIGS_SHADOW_REG; *IIGS_NEWVIDEO_REG = (uint8_t)(NEWVIDEO_SHR_ON | NEWVIDEO_LINEARIZE | NEWVIDEO_RESERVED_BIT); *IIGS_BORDER_REG = (uint8_t)(gPreviousBorder & BORDER_COLOR_MASK); // Inhibit shadowing of the stage region. Without this, every // write to $01/2000-9FFF mirrors to $E1 and the off-screen-buffer // illusion breaks (the user would see drawing in progress). *IIGS_SHADOW_REG = (uint8_t)(gPreviousShadow | SHADOW_INHIBIT_SHR_MASK); // SCB and palette are uploaded by jlpPresent's C-side // uploadScbAndPaletteIfNeeded (the dirty flags start true, so the // first present sets up SCB to 320 mode). C-side memset/memcpy to // bank $E1 is unreliable from jlpInit's calling context, so we // don't try it here. iigsInitRowLut(); gFrameHz = (iigsReadHzParam() == 1u) ? 50u : 60u; gModeSet = true; return true; } void jlpPresent(const jlSurfaceT *src) { if (src == NULL) { return; } // SCB + palette upload via the C-side memcpy to bank $E1, then pixels. // (The asm's old SCB/palette MVN paths never wrote $E1:9E00 reliably and // were dead once this C path landed; PERF-AUDIT #52 removed them along // with the per-present scbPtr/palettePtr/uploadFlags marshalling, so // iigsBlitStageToShr is pixels-only and takes no args.) // KNOWN RESIDUAL: on a DRAW the PEI-slam corrupts $E1:9E20+ (palettes // 1-15; palette 0 and pixels stay correct) -- a peislam.s shadow / soft- // switch spill, independent of upload order. Single-palette apps are // unaffected; multi-palette colors above index 0 need the slam fix. uploadScbAndPaletteIfNeeded(src); iigsBlitStageToShr(); } // GS/OS QuitGS, issued from the pure-constant asm routine in // joeyDraw.s (iigsQuitGS). The first cut built the call stub in a C // BSS array and hit the cross-bank DBR trap on the 10-segment STAXI // link: the byte stores went DB-relative into bank $08 while the // `jsl gQuitStub` resolved to the array's true bank $10 -- the JSL // executed zeros and BRK'd. The asm routine has no relocations; its // parameter block is the fixed scratch word at $E1:9DCC (inside the // survives-present $E1:9DC8-$9DFD window), written here before the // call. QuitGS pCount = 0 = plain quit to the parent (Finder). #define IIGS_QUIT_PARM_LO ((volatile uint8_t *)0xE19DCCL) #define IIGS_QUIT_PARM_HI ((volatile uint8_t *)0xE19DCDL) #define IIGS_QUIT_MAILBOX ((volatile uint8_t *)0xE19DC8L) void jlpShutdown(void) { if (gModeSet) { *IIGS_NEWVIDEO_REG = gPreviousNewVideo; *IIGS_BORDER_REG = gPreviousBorder; *IIGS_SHADOW_REG = gPreviousShadow; gModeSet = false; } // Quit to GS/OS HERE instead of returning: the llvm-mos runtime's // post-main exit path is broken (a php/rtl thunk at segment offset // $0072 mis-balances the stack and the machine COPs to the monitor // at $00:0002 -- reproduced identically for KEYS and STAXI, see // LLVM816-ASKS.md #6). jlShutdown is the last thing every example // calls, so ending the process here is equivalent and safe; on // IIgs jlShutdown therefore does not return. If QuitGS itself // errors, fall through to the old (crashing) path rather than // hang. The mailbox marker lets the MAME harness verify the call // was reached ($AA) or returned ($EE). *IIGS_QUIT_PARM_LO = 0u; // QuitGS pCount = 0 *IIGS_QUIT_PARM_HI = 0u; *IIGS_QUIT_MAILBOX = 0xAAu; iigsQuitGS(); *IIGS_QUIT_MAILBOX = 0xEEu; } // The IIgs draw/tile/flood primitives (surfaceClear, drawLine, drawCircle, // fillCircle, fillRect, tileCopy / tileCopyMasked / tilePaste / tileSnap / // tileFill, floodWalkAndScans) are all dispatched via the jlp macros in // core/port.h (#ifdef JOEYLIB_PLATFORM_IIGS block); they expand to the // iigs*Inner asm entry points above, so no C wrapper functions live here. // Memory Manager reservation for the pinned framebuffer. Without it the // MM considers $01:2000-$9CFF free and can place ANY handle inside the // display -- jlpBigAlloc'd sprite/surface structs landed there and were // painted over by surface clears (PERF-AUDIT.md #85, the root of the // #84 sprite pathology). attrAddr claims the exact address; the range // is in special memory (bank $01) by definition. The claim MUST happen // in jlpInit, BEFORE the first jlpBigAlloc: stageAlloc allocates the // stage struct before it calls jlpStageAllocPixels, so claiming here // would already be too late to keep that struct out of the display. static void *gStagePixelsHandle = NULL; // Bank-0 region the linker uses without telling the Memory Manager: // the C heap ($BC67-$BF00 in the current layout) AND the bank-0 // ALIASES of joeyDraw.s's D-relative pointer-scratch symbols // (dpxlScratch/dlnScratch/dcPixPtr/fcPixPtr, ~$BBB8-$BC66) -- the // [pix],y pattern reads/writes bank 0 at the symbol's OFFSET. If an MM // handle (codegen arena, jlpBigAlloc scratch) lands there, every draw // primitive plots through stomped pointers (PERF-AUDIT.md #88, the // codegen-on showcase hang). Claim generously: $00:B900-$BEFF. #define IIGS_BANK0_CLAIM_BASE ((void *)0x00B900L) #define IIGS_BANK0_CLAIM_BYTES 0x0600ul static void *gBank0ScratchHandle = NULL; static void claimStagePixels(void) { if (gStagePixelsHandle == NULL) { gStagePixelsHandle = NewHandle((unsigned long)SURFACE_PIXELS_SIZE, _ownerid, attrAddr | attrFixed | attrLocked, (void *)IIGS_STAGE_PIXELS); if (gStagePixelsHandle == NULL) { // The range is already owned (another app / a prior claim): // proceed rather than fail init, but leave a breadcrumb for // jlLastError. (No _toolErr shim in this toolchain yet; a // NULL handle is the only failure signal.) coreSetError("stage framebuffer MM reservation failed"); } } if (gBank0ScratchHandle == NULL) { gBank0ScratchHandle = NewHandle(IIGS_BANK0_CLAIM_BYTES, _ownerid, attrAddr | attrFixed | attrLocked, IIGS_BANK0_CLAIM_BASE); if (gBank0ScratchHandle == NULL) { coreSetError("bank-0 heap/scratch MM reservation failed"); } } } uint8_t *jlpStageAllocPixels(void) { return IIGS_STAGE_PIXELS; } void jlpStageFreePixels(uint8_t *pixels) { (void)pixels; // Backing memory is hardware-pinned; release only the MM claims. if (gStagePixelsHandle != NULL) { DisposeHandle(gStagePixelsHandle); gStagePixelsHandle = NULL; } if (gBank0ScratchHandle != NULL) { DisposeHandle(gBank0ScratchHandle); gBank0ScratchHandle = NULL; } } // jlpSurfaceCopyRect override (NATIVE-PERF Phase 7; asm since hunt-3 // rank 1). History of this function IS the llvm-mos lesson arc: the // generic per-row memcpy was a far byte-loop CALL (finding #79); the // Phase-7 replacement inlined a C word copy, which the -S output at // the time showed as lda [dp],y / sta -- but the hunt-3 probe measured // the SHIPPED build at ~5.3 ms per 16x16 erase, and the current -S // shows the compiler re-deriving BOTH 32-bit far pointers from stack // slots on EVERY word (~30+ instructions/word -- dead-lever #12's // pattern). One JSL into iigsCopyRectRows fixes it for good: [dp],y // pair copies at ~21 cyc/word with the row stride advanced in the dp // frame. rowBytes is even (the jlSurfaceCopyRect wrapper widens w to // 16-px groups) -- the asm's contract. DOS deliberately stays on the // generic memcpy (rep-movs wins; an inline word loop regressed -12%). void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint16_t y, uint16_t w, uint16_t h) { uint8_t *d; const uint8_t *s; uint16_t srcLo; uint8_t srcBank; if (dst->pixels == NULL || src->pixels == NULL) { return; } d = dst->pixels + SURFACE_ROW_OFFSET(y) + (x >> 1); s = src->pixels + SURFACE_ROW_OFFSET(y) + (x >> 1); // Byte-alias the src pointer split (no 32-bit shift libcall). { uint32_t sAddr = (uint32_t)s; const uint8_t *sB = (const uint8_t *)&sAddr; srcLo = (uint16_t)(sB[0] | ((uint16_t)sB[1] << 8)); srcBank = sB[2]; } iigsCopyRectRows(d, srcLo, (uint16_t)srcBank, (uint16_t)(w >> 1), h); } // IIgs is chunky-native: portData is unused. The chunky `pixels` // buffer at $01:2000 is the stage's pixel storage and the source for // jlStagePresent's PEI-slam to $E1. // IIgs is chunky-native: the hal*Planes hooks are compiled out by // no-op macros in core/port.h (gated on JOEYLIB_PLATFORM_IIGS), so no // stub functions are defined here. /* Phase 9 chunky reader hooks: IIgs reads from s->pixels just like * the legacy paths did. Same logic as the DOS port. */ // Block until the next VBL boundary by waiting for the firmware tick // (GetTick, incremented by the VBL interrupt handler) to advance. The // old form polled $C019's VBL status bit, which never reads high under // MAME's apple2gs (PERF-AUDIT.md finding #76): the wait-for-scan loop // spun forever, hanging every program that paces with jlWaitVBL. UBER // is the only example that calls it, so no verification gate ever // caught this. Tick granularity IS the VBL boundary, so semantics are // unchanged on real hardware; the caller must not hold SEI (a masked // VBL interrupt freezes the tick, exactly as it froze the old bit). void jlpWaitVBL(void) { uint16_t tick; tick = iigsGetTickWord(); while (iigsGetTickWord() == tick) { /* wait for the VBL interrupt to advance the tick */; } } // (jlpFrameCount is a port.h macro straight to iigsGetTickWord -- the // GetTick asm wrapper in peislam.s -- so no C wrapper lives here.) uint16_t jlpFrameHz(void) { return gFrameHz; }