// 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 <stddef.h>
#include <stdlib.h>
#include <string.h>

#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 <iigs/toolbox.h>
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<Op>
// 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);
// Serial soft-ring pump (serial.c) -- called from every long-running HAL path.
extern void iigsSerialPump(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.)
    // The KNOWN RESIDUAL that used to be documented here -- "the PEI-slam
    // corrupts $E1:9E20+ (palettes 1-15) on a DRAW" -- described correct
    // data, not a defect. $9E20 is not a corruption boundary: a palette
    // row is 16 entries x 2 bytes = 32 = $20, so $9E00+$20 = $9E20 is
    // exactly the palette-0 / palette-1 boundary. DRAW sets ONLY palette 0
    // (draw.c's jlPaletteSet(screen, 0, ...) + jlScbSetRange(..., 0)), so
    // palettes 1-15 legitimately hold paletteInitDefault's EGA table. The
    // "32 bytes intact / 480 bytes wrong" split is the app-wrote-row-0 /
    // library-defaulted-rows-1-15 split, and it is the only boundary in
    // the system that lands at $9E20 without a coincidence.
    // The note also refuted itself twice: it blamed the slam yet recorded
    // the symptom as "independent of upload order" (a slam clobber would
    // be repaired by uploading AFTER it), and it said "single-palette apps
    // are unaffected" while citing DRAW, which IS single-palette -- every
    // SCB is 0, so the shifter never fetches palettes 1-15 there and the
    // "corruption" could not have been on screen. It was read out of a
    // memory dump against a wrong expectation.
    // Re-verified under MAME 2026-08-04: a DRAW run reads back palette 0
    // == DRAW's own table and palettes 1-15 == the EGA default, byte-exact,
    // 200 SCB bytes correct; PATTERN (8 authored palettes + 8 SCB bands)
    // reads back all 8 byte-exact. Do not re-hunt a slam bug here.
    // Nothing else can see this region -- the golden hash covers the STAGE,
    // not $E1, and P9's pixel compare stops at $9CFF -- so it has its own
    // OPT-IN gate: `make iigs-verify-shrtail` builds UBER with -DUBER_PROBE
    // (no normal build defines it) and runs the shrTailUpload / shrTailSlam
    // rows, which seed all 16 palettes, present, then do pixel-only presents
    // over rows 190-199 (where a descending-push overrun would land first)
    // and re-compare $E1 against the stage.
    iigsSerialPump();
    uploadScbAndPaletteIfNeeded(src);
    iigsBlitStageToShr();
    iigsSerialPump();
}


// 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<Op> 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
// link-time BSS (which holds the bank-0 ALIASES of joeyDraw.s's D-relative
// pointer-scratch symbols -- the [pix],y pattern reads/writes bank 0 at the
// symbol's OFFSET) plus the link-time C heap window. If an MM handle
// (codegen arena, jlpBigAlloc scratch, a GS/OS file buffer) lands there,
// every draw primitive plots through stomped pointers (PERF-AUDIT.md #88,
// the codegen-on showcase hang).
// The span is COMPUTED from the link symbols, not hard-coded: the old
// $B900-$BEFF constant was the DEMO layout's top slice, and the bigger
// RetroNet client linked its scratch at ~$84CC -- entirely unprotected,
// with the claim failing against whatever GS/OS had placed at $B900.
extern char __bss_start[];
extern char __heap_end[];

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) {
        unsigned long base = (unsigned long)(void *)__bss_start;
        unsigned long end  = (unsigned long)(void *)__heap_end;

        if (end > base) {
            gBank0ScratchHandle = NewHandle(end - base,
                                            _ownerid,
                                            attrAddr | attrFixed | attrLocked,
                                            (void *)base);
        }
        if (gBank0ScratchHandle == NULL) {
            coreSetError("bank-0 bss/heap 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) {
        // The VBL spin is the client loop's longest deaf window (~16ms); drain the 3-byte SCC FIFO
        // into the serial soft ring while waiting or a 9600-baud downlink loses mid-frame bytes
        // (the X68000 lesson, same Z8530).
        iigsSerialPump();
    }
}


// (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;
}
