654 lines
29 KiB
C
654 lines
29 KiB
C
// 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 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 <iigs/toolbox.h>
|
|
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<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);
|
|
// 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<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 (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;
|
|
}
|