// 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 iigsMemUserId(), and the attribute bit values // are Apple's Memory Manager constants. Only the bits we use are defined. // // _ownerid used to expand to MMStartUp() at EVERY call site. MMStartUp // hands out a NEW memory ID each time it is called, so every allocation // burned one of the Memory Manager's application IDs; once the pool ran // dry the next NewHandle went wild (the UBER probe build BRK'd into the // ROM monitor inside jlSurfaceCreate, 2026-09-22). One ID, fetched once. #include typedef void *Handle; #define _ownerid (iigsMemUserId()) #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)) // VBL tick counter in peislam.s: a Misc Toolset heartbeat task that the // firmware VBL interrupt handler bumps every field, read by // iigsGetTickWord as a same-bank word load. It replaced a _GetTick // toolbox call per read (hundreds of cycles, ten-odd reads per Space // Taxi frame). Polling $C019 from C missed transitions for any op over // ~1 ms; the interrupt-driven count stays accurate regardless of the // caller's polling rate. Tick rate matches the video field rate -- 60 Hz // on NTSC, 50 Hz on PAL. iigsVblInstall/iigsVblRemove wrap SetHeartBeat/ // DelHeartBeat for that task and return the Misc Tool Set error (0 = ok). extern uint16_t iigsGetTickWord(void); extern uint16_t iigsVblInstall(void); extern uint16_t iigsVblRemove(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 // Interrupt windows of the two long interrupts-off paths in joeyDraw.s // (see gIigsPresentChunkRows). Loose is what they always did and costs // nothing; tight fits the Z8530 SCC's 3-byte receive FIFO at 9600 baud // (~3.1 ms): 4 present rows ~1.8 ms, 12 clear blocks ~1.4 ms. #define IRQ_PRESENT_ROWS_LOOSE 40u #define IRQ_PRESENT_ROWS_TIGHT 4u #define IRQ_CLEAR_BLOCKS_LOOSE 200u #define IRQ_CLEAR_BLOCKS_TIGHT 12u // Video beam ticks a frame (VERTCNT/HORIZCNT: 65 positions a line), for // the tight joystick read's beam stamps (iigsJoyPass). #define BEAM_TICKS_PER_LINE 65u #define BEAM_LINES_NTSC 262u #define BEAM_LINES_PAL 312u // ----- Module state ----- static uint8_t gPreviousNewVideo = 0; static uint8_t gPreviousBorder = 0; static uint8_t gPreviousShadow = 0; static bool gModeSet = false; // Interrupt-latency bound, read by joeyDraw.s (so not static, and BSS // like every other C global the asm reads DBR-relative; jlpInit sets // the loose values). The present's chunked PEI slam re-enables // interrupts every gIigsPresentChunkRows rows (~0.42 ms a row), the // stage fast clear every gIigsClearChunkBlocks 160-byte blocks // (~0.12 ms a block). A client whose interrupt cannot wait ~17 ms holds // the tight bound while it needs it; the holds are counted, so clients // nest. Tight costs a full-screen present ~5% (83.1 -> 87.2 ms). // gIigsIrqTight tells the C paths that bound themselves (input.c's // joystick read, audio_full.c's NinjaTrackerPlus entries) which way to // go. All three follow gIrqTightHolds, through irqLatencyApply alone. uint16_t gIigsPresentChunkRows; uint16_t gIigsClearChunkBlocks; uint16_t gIigsIrqTight; static uint16_t gIrqTightHolds = 0u; // Beam ticks a frame for the joystick asm's frame wrap (jlpInit). uint16_t gIigsBeamFrameTicks; // 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) // Narrow-row MVN RUN (rows sharing one identical band are set up once). volatile uint16_t gPeiBandMin; // the run's band, low byte valid volatile uint16_t gPeiBandMax; volatile uint16_t gPeiResumeRow; // first row AFTER the run volatile uint16_t gPeiResumeChunk; volatile uint16_t gPeiRunOff; // stage byte offset of the run's first row volatile uint16_t gPeiRunEnd; // offset one row past the run's last volatile uint16_t gPeiRunStride; // 160 - bytes: advance after an MVN volatile uint16_t gPeiRunRows; // rows in the run volatile uint16_t gPeiMulA; // run telemetry: bytes * rows volatile uint16_t gPeiMulB; volatile uint16_t gPeiMulOut; volatile uint16_t gStageGrpTmp; // asm markers: first coarse group index volatile uint16_t gPeiSkipTmp; // scan: rows skipped by a clean group // 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. // Word stores, NOT memcpy: 16-bit stores land at ~8 cycles a word, ~2.8k // cycles for the pair, where libc memcpy's MVN moves a byte at a time at // 7 cycles a byte (~5k). static void copyWords(volatile uint16_t *dst, const uint16_t *src, uint16_t words) { uint16_t i; for (i = 0; i < words; i++) { dst[i] = src[i]; } } static void uploadScbAndPaletteIfNeeded(const jlSurfaceT *src) { if (gStageScbDirty) { copyWords((volatile uint16_t *)IIGS_SHR_SCB, (const uint16_t *)src->scb, SURFACE_HEIGHT / 2u); gStageScbDirty = false; } if (gStagePaletteDirty) { // Only the palettes that changed: a colour-cycling effect touches // one palette a frame, and 32 bytes beats 512 sixteen times over. uint16_t mask = gStagePaletteDirtyMask; uint8_t pal; for (pal = 0; mask != 0u && pal < SURFACE_PALETTE_COUNT; pal++) { if ((mask & 1u) != 0u) { copyWords((volatile uint16_t *)IIGS_SHR_PALETTE + (uint16_t)pal * SURFACE_COLORS_PER_PALETTE, (const uint16_t *)src->palette + (uint16_t)pal * SURFACE_COLORS_PER_PALETTE, SURFACE_COLORS_PER_PALETTE); } mask >>= 1; } gStagePaletteDirty = false; gStagePaletteDirtyMask = 0u; } } // ----- 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); static void irqLatencyApply(bool tight); bool jlpInit(const jlConfigT *config) { uint16_t tick; uint16_t spin = 0u; (void)config; // The heartbeat task is the frame clock; nothing else can wait for a // VBL, so prove it advances before touching anything else (a task // that never runs would spin jlpWaitVBL forever). if (iigsVblInstall() != 0u) { jlLog("iigs: SetHeartBeat failed"); return false; } tick = iigsGetTickWord(); while (iigsGetTickWord() == tick) { spin++; if (spin == 0u) { iigsVblRemove(); jlLog("iigs: VBL heartbeat task never ran"); return false; } } 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(); irqLatencyApply(gIrqTightHolds != 0u); gFrameHz = (iigsReadHzParam() == 1u) ? 50u : 60u; gIigsBeamFrameTicks = (uint16_t)(((gFrameHz == 50u) ? BEAM_LINES_PAL : BEAM_LINES_NTSC) * BEAM_TICKS_PER_LINE); 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. uploadScbAndPaletteIfNeeded(src); iigsBlitStageToShr(); } // Release one hold on the tight interrupt-latency bound; the last one out // puts the loose bound back. void iigsIrqLatencyRelax(void) { if (gIrqTightHolds == 0u) { return; } gIrqTightHolds--; irqLatencyApply(gIrqTightHolds != 0u); } // Take a hold on the tight interrupt-latency bound (see // gIigsPresentChunkRows). void iigsIrqLatencyTighten(void) { gIrqTightHolds++; irqLatencyApply(true); } // The Memory Manager owner ID for every JoeyLib allocation on the IIgs // (jlpBigAlloc, the stage pixels, the audio buffers). MMStartUp creates // a fresh application ID per call, so it is called exactly once here // and the result cached for the life of the program. uint16_t iigsMemUserId(void) { static uint16_t gMemUserId = 0u; static bool gMemUserIdReady = false; if (!gMemUserIdReady) { gMemUserId = (uint16_t)MMStartUp(); gMemUserIdReady = true; } return gMemUserId; } void jlpShutdown(void) { // The task lives in this program's memory: pull it from the heartbeat // queue before GS/OS frees the segments. iigsVblRemove(); if (gModeSet) { *IIGS_NEWVIDEO_REG = gPreviousNewVideo; *IIGS_BORDER_REG = gPreviousBorder; *IIGS_SHADOW_REG = gPreviousShadow; gModeSet = false; } // Returns like every port's: main's return then goes through the // runtime's exit(), which runs the atexit handlers (an open serial // port's close among them) before GS/OS QUIT. This used to QuitGS // itself because that runtime exit path crashed into the monitor // (LLVM816-ASKS #6, fixed 2026-09-28). } // 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 (claudeDocs/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 (claudeDocs/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"); } } } // Point the chunked-SEI asm, and the C paths that bound themselves, at // the tight or the loose interrupt window. static void irqLatencyApply(bool tight) { gIigsPresentChunkRows = tight ? IRQ_PRESENT_ROWS_TIGHT : IRQ_PRESENT_ROWS_LOOSE; gIigsClearChunkBlocks = tight ? IRQ_CLEAR_BLOCKS_TIGHT : IRQ_CLEAR_BLOCKS_LOOSE; gIigsIrqTight = tight ? 1u : 0u; } 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; libc's // memcpy is MVN now, but its per-call setup still loses on rows of a few // words); 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 heartbeat tick // (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 (claudeDocs/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) { // spin until the heartbeat advances } } // (jlpFrameCount is a port.h macro straight to iigsGetTickWord -- the // heartbeat counter read in peislam.s -- so no C wrapper lives here.) uint16_t jlpFrameHz(void) { return gFrameHz; }