diff --git a/examples/uber/uber.c b/examples/uber/uber.c index 35f53d9..cef65fd 100644 --- a/examples/uber/uber.c +++ b/examples/uber/uber.c @@ -42,7 +42,14 @@ * just because 4 ops at 16-17 ms happen to fit both windows. The * 16-frame budget extends the windows to 267 ms / 320 ms; quantum * gap shrinks to ~6%. Total run time scales 4x (~80 sec each). */ +/* Overridable so a diagnostic build can shrink the measurement window + * (-DUBER_FRAMES=1) when the run is wanted for its CHK/PROBE rows + * rather than its timings. The golden binary is built without the + * define and stays at 16, so goldened output is unaffected. A -DUBER_ + * PROBE run at 16 does not fit bench-iigs.sh's 1200-second MAME cap. */ +#ifndef UBER_FRAMES #define UBER_FRAMES 16u +#endif // Op calls per clock poll for sub-frame ops. The unrolled batch in @@ -98,6 +105,23 @@ extern uint16_t gJoeyLogRingHead; #define uberMbBeat() (*UBER_MB_HEARTBEAT = (uint16_t)(*UBER_MB_HEARTBEAT + 1u)) #define uberMbTick(_t) (*UBER_MB_TICK = (uint16_t)(_t)) #define uberMbLayout() (*UBER_MB_STAGEPTR = (uint32_t)gStage, *UBER_MB_GSTAGEAT = (uint32_t)&gStage, *UBER_MB_LOGRING = (uint32_t)&gJoeyLogRing[0], *UBER_MB_LOGHEAD = (uint32_t)&gJoeyLogRingHead, *UBER_MB_MAGIC = UBER_MB_MAGIC_VAL) +#elif defined(JOEYLIB_PLATFORM_X68000) +// Same mailbox words at the same addresses, for the same reason: a headless +// probe needs to know WHERE the run is when the serial log has gone quiet. +// $E19DC8 is text plane 0's UNUSED upper half -- the plane spans 128 KB but +// only 512 rows x 128 bytes = 64 KB are displayed -- so these writes cost no +// visible pixel and need no memory of their own. +#define UBER_MB_OP ((volatile uint8_t *)0xE19DC8L) +#define UBER_MB_PHASE ((volatile uint8_t *)0xE19DC9L) +#define UBER_MB_HEARTBEAT ((volatile uint16_t *)0xE19DCAL) +#define UBER_MB_TICK ((volatile uint16_t *)0xE19DCCL) +#define uberMbOp(_i) (*UBER_MB_OP = (uint8_t)(_i)) +#define uberMbPhase(_p) (*UBER_MB_PHASE = (uint8_t)(_p)) +#define uberMbBeat() (*UBER_MB_HEARTBEAT = (uint16_t)(*UBER_MB_HEARTBEAT + 1u)) +#define uberMbTick(_t) (*UBER_MB_TICK = (uint16_t)(_t)) +// No layout word: the RAM log ring it advertises is IIgs-only (src/core/debug.c +// compiles a FILE* logger everywhere else), so there is nothing to point at. +#define uberMbLayout() ((void)0) #else #define uberMbOp(_i) ((void)0) #define uberMbPhase(_p) ((void)0) @@ -719,11 +743,96 @@ static void PROBE_ATTR op_probeFillBandSum(void) { // authoritative image, so all 32,000 displayed bytes at $E1:2000 must // equal it. Any skipped dirty row, mis-based D/SP, wrong entry index, // or short MVN leaves >= 1 differing byte. +// P9 TAIL: the 712 display-state bytes ABOVE the pixel block -- the SCB +// array ($E1:9D00-$9DC7, 200 bytes) and all 16 palettes ($E1:9E00-$9FFF, +// 512 bytes). The pixel compare stops at $9CFF and jlSurfaceHash covers +// the STAGE, not $E1, so until now nothing in the tree ever checked the +// bytes the shifter actually reads to pick a palette per scanline. A +// wrong palette is invisible to the golden gate and to the pixel probe; +// it shows only as wrong colors on a real screen. Reached only via +// `make iigs-verify-shrtail` (this whole block is behind UBER_PROBE, +// which no normal build defines). The "palettes 1-15 wrong on a DRAW" +// note this was written to chase turned out to describe correct data -- +// see src/iigs/hal.c's jlpPresent -- so this is a standing regression +// gate for the region, not a reproduction of a known defect. +// +// $9DC8-$9DFF is deliberately NOT compared: it is scratch, not display +// state, so there is no source of truth for it. The slam parks its +// per-run SP offset at $9DFE and UBER's mailbox owns $9DC8-$9DCF +// (UBER_MB_MAGIC at $9DCE is a uint16, so it ends at $9DCF, and the +// $9DD0-$9DED layout words above it are live during a probe run too). +#define SHR_TAIL_PALETTES 16u +#define SHR_TAIL_COLORS 16u +#define SHR_TAIL_SCB_BYTES 200u +#define SHR_TAIL_NONE 0xFFFFu + +// Compare $E1's SCB block and palette block against the stage, which is +// the authoritative copy. Counts mismatching SCB bytes and mismatching +// palette ENTRIES, and reports the first palette index that differs so a +// failure names the range rather than just the count. +static void PROBE_ATTR probeShrTailCheck(uint16_t *badScb, uint16_t *badPal, uint16_t *firstBadPal) { + const uint8_t *shrScb; + const uint16_t *shrPal; + uint16_t want[SHR_TAIL_COLORS]; + uint16_t line; + uint16_t p; + uint16_t c; + + shrScb = (const uint8_t *)0xE19D00L; + shrPal = (const uint16_t *)0xE19E00L; + *badScb = 0; + *badPal = 0; + *firstBadPal = SHR_TAIL_NONE; + for (line = 0; line < SHR_TAIL_SCB_BYTES; line++) { + if (shrScb[line] != jlScbGet(gStage, line)) { + (*badScb)++; + } + } + for (p = 0; p < SHR_TAIL_PALETTES; p++) { + jlPaletteGet(gStage, (uint8_t)p, want); + for (c = 0; c < SHR_TAIL_COLORS; c++) { + if (shrPal[(p * SHR_TAIL_COLORS) + c] != want[c]) { + (*badPal)++; + if (*firstBadPal == SHR_TAIL_NONE) { + *firstBadPal = p; + } + } + } + } +} + + +// Give every palette a distinct, non-default value so a stale or +// replicated block is detectable: red nibble = palette index, green +// nibble = color index, blue nibble = their sum. Entry 0 is left to +// jlPaletteSet, which forces it to $000 for every palette by contract. +// SCB bands walk all 16 palettes so the SCB block is equally distinct. +static void PROBE_ATTR probeShrTailSeed(void) { + uint16_t colors[SHR_TAIL_COLORS]; + uint16_t p; + uint16_t c; + + for (p = 0; p < SHR_TAIL_PALETTES; p++) { + for (c = 0; c < SHR_TAIL_COLORS; c++) { + colors[c] = (uint16_t)(((p & 0x0Fu) << 8) | ((c & 0x0Fu) << 4) | ((p + c) & 0x0Fu)); + } + jlPaletteSet(gStage, (uint8_t)p, colors); + jlScbSetRange(gStage, (uint16_t)(p * 12u), (uint16_t)((p * 12u) + 11u), (uint8_t)p); + } +} + + static void PROBE_ATTR op_probeShrParity(void) { const uint8_t *stagePx; const uint8_t *shrPx; uint16_t badRows; uint16_t firstBad; + uint16_t badScbUp; + uint16_t badPalUp; + uint16_t firstPalUp; + uint16_t badScbSlam; + uint16_t badPalSlam; + uint16_t firstPalSlam; uint16_t x; uint16_t y; @@ -764,6 +873,32 @@ static void PROBE_ATTR op_probeShrParity(void) { shrPx += 160u; } jlLogF("UBER-PROBE: shrParity %s badRows=%u firstBad=%u\n", (badRows == 0u) ? "OK" : "FAIL", (unsigned int)badRows, (unsigned int)firstBad); + + // Tail phase 1: seed all 16 palettes + the SCB bands, then present. + // jlPaletteSet / jlScbSetRange set the dirty flags, so THIS present + // is the one that uploads them. A mismatch here means the C-side + // upload never wrote the bytes correctly in the first place. + probeShrTailSeed(); + jlStagePresent(); + probeShrTailCheck(&badScbUp, &badPalUp, &firstPalUp); + jlLogF("UBER-PROBE: shrTailUpload %s badScb=%u badPal=%u firstPal=%u\n", ((badScbUp | badPalUp) == 0u) ? "OK" : "FAIL", (unsigned int)badScbUp, (unsigned int)badPalUp, (unsigned int)firstPalUp); + + // Tail phase 2: pixel-only presents. The dirty flags were cleared by + // the upload above and nothing below touches a palette or an SCB, so + // no re-upload happens -- anything that differs now was clobbered + // AFTER being written correctly. The bands are chosen to put wide + // slam runs on the highest rows, where a descending-push overrun + // would land in $9D00+ first, plus a narrow MVN row for that path. + probeMarkRows(190, 200, 0, 79); + jlStagePresent(); + probeMarkRows(199, 200, 0, 79); + jlStagePresent(); + probeMarkRows(196, 200, 70, 79); + jlStagePresent(); + jlFillRect(gStage, 0, 180, 320, 20, 6); + jlStagePresent(); + probeShrTailCheck(&badScbSlam, &badPalSlam, &firstPalSlam); + jlLogF("UBER-PROBE: shrTailSlam %s badScb=%u badPal=%u firstPal=%u\n", ((badScbSlam | badPalSlam) == 0u) ? "OK" : "FAIL", (unsigned int)badScbSlam, (unsigned int)badPalSlam, (unsigned int)firstPalSlam); } #endif diff --git a/include/joey/platform.h b/include/joey/platform.h index ab3083a..5fa84b0 100644 --- a/include/joey/platform.h +++ b/include/joey/platform.h @@ -13,6 +13,7 @@ !defined(JOEYLIB_PLATFORM_AMIGA) && \ !defined(JOEYLIB_PLATFORM_ATARIST) && \ !defined(JOEYLIB_PLATFORM_DOS) && \ + !defined(JOEYLIB_PLATFORM_X68000) && \ !defined(JOEYLIB_PLATFORM_BLANK) #if defined(__DJGPP__) || defined(__MSDOS__) @@ -23,6 +24,8 @@ #define JOEYLIB_PLATFORM_ATARIST #elif defined(__APPLE2GS__) || defined(__GNO__) #define JOEYLIB_PLATFORM_IIGS + #elif defined(__x68k__) || defined(__X68000__) || defined(__HUMAN68K__) + #define JOEYLIB_PLATFORM_X68000 #else #error "JoeyLib: unknown platform; define JOEYLIB_PLATFORM_ explicitly via -D" #endif @@ -39,6 +42,7 @@ defined(JOEYLIB_PLATFORM_AMIGA) + \ defined(JOEYLIB_PLATFORM_ATARIST) + \ defined(JOEYLIB_PLATFORM_DOS) + \ + defined(JOEYLIB_PLATFORM_X68000) + \ defined(JOEYLIB_PLATFORM_BLANK)) != 1 #error "JoeyLib: exactly one JOEYLIB_PLATFORM_* must be defined" #endif @@ -67,6 +71,25 @@ #define JOEYLIB_ENDIAN_LITTLE #define JOEYLIB_NATIVE_CHUNKY #define JOEYLIB_PLATFORM_NAME "MS-DOS" +#elif defined(JOEYLIB_PLATFORM_X68000) + // BRING-UP STATE: chunky stage in main RAM, expanded to GVRAM by jlpPresent. + // This is deliberately the slow-but-correct path -- it makes every one of the + // 772 lines of src/generic work unmodified, so the port renders correctly + // before a single native primitive exists. + // + // The native storage model is NOT yet decided. The two candidates are GVRAM + // (2 bytes/pixel, trivial primitives) and the text plane at $E00000 (four + // 1bpp planes 128 KB apart -- byte-for-byte the Amiga layout with stride 40 + // replaced by 128, so src/amiga adapts by changing one constant). Measured + // under MAME with the wait-state patch: TVRAM costs 1.92 cycles/access + // against GVRAM's 1.03, but needs only 0.25 word-writes per 16-colour pixel + // against GVRAM's 1.0 -- so the text plane still wins ~2x. See + // docs/x68000_mame_crtc_timing.md. Do not switch to JOEYLIB_NATIVE_PLANAR + // until that is settled and measured. + #define JOEYLIB_CPU_68000 + #define JOEYLIB_ENDIAN_BIG + #define JOEYLIB_NATIVE_PLANAR + #define JOEYLIB_PLATFORM_NAME "Sharp X68000" #elif defined(JOEYLIB_PLATFORM_BLANK) // Chunky little-endian is the simplest baseline for a new port: the generic // backend gives you fully-working software rendering out of the box. Adjust @@ -128,6 +151,15 @@ #define JL_HAS_AUDIO_CRITICAL_ENTER #define JL_HAS_AUDIO_CRITICAL_EXIT #define JL_HAS_AUDIO_FRAME_TICK + // SAMPLE side (src/x68000/audioPcm.c): libxmp-lite + the shared 5-slot SFX + // overlay, encoded to MSM6258 ADPCM and played via _iocs_adpcmlot. Separate + // device from the OPM, so chip music and digital audio do not contend. + #define JL_HAS_AUDIO_PLAY_MOD + #define JL_HAS_AUDIO_STOP_MOD + #define JL_HAS_AUDIO_IS_PLAYING_MOD + #define JL_HAS_AUDIO_PLAY_SFX + #define JL_HAS_AUDIO_PLAY_SFX_STREAM + #define JL_HAS_AUDIO_STOP_SFX // lifecycle / present / input (every real port implements) #define JL_HAS_INIT #define JL_HAS_SHUTDOWN @@ -197,6 +229,15 @@ #define JL_HAS_AUDIO_CRITICAL_ENTER #define JL_HAS_AUDIO_CRITICAL_EXIT #define JL_HAS_AUDIO_FRAME_TICK + // SAMPLE side (src/x68000/audioPcm.c): libxmp-lite + the shared 5-slot SFX + // overlay, encoded to MSM6258 ADPCM and played via _iocs_adpcmlot. Separate + // device from the OPM, so chip music and digital audio do not contend. + #define JL_HAS_AUDIO_PLAY_MOD + #define JL_HAS_AUDIO_STOP_MOD + #define JL_HAS_AUDIO_IS_PLAYING_MOD + #define JL_HAS_AUDIO_PLAY_SFX + #define JL_HAS_AUDIO_PLAY_SFX_STREAM + #define JL_HAS_AUDIO_STOP_SFX // lifecycle / present / input (every real port implements) #define JL_HAS_INIT #define JL_HAS_SHUTDOWN @@ -266,6 +307,15 @@ #define JL_HAS_AUDIO_CRITICAL_ENTER #define JL_HAS_AUDIO_CRITICAL_EXIT #define JL_HAS_AUDIO_FRAME_TICK + // SAMPLE side (src/x68000/audioPcm.c): libxmp-lite + the shared 5-slot SFX + // overlay, encoded to MSM6258 ADPCM and played via _iocs_adpcmlot. Separate + // device from the OPM, so chip music and digital audio do not contend. + #define JL_HAS_AUDIO_PLAY_MOD + #define JL_HAS_AUDIO_STOP_MOD + #define JL_HAS_AUDIO_IS_PLAYING_MOD + #define JL_HAS_AUDIO_PLAY_SFX + #define JL_HAS_AUDIO_PLAY_SFX_STREAM + #define JL_HAS_AUDIO_STOP_SFX // lifecycle / present / input (every real port implements) #define JL_HAS_INIT #define JL_HAS_SHUTDOWN @@ -314,6 +364,15 @@ #define JL_HAS_AUDIO_CRITICAL_ENTER #define JL_HAS_AUDIO_CRITICAL_EXIT #define JL_HAS_AUDIO_FRAME_TICK + // SAMPLE side (src/x68000/audioPcm.c): libxmp-lite + the shared 5-slot SFX + // overlay, encoded to MSM6258 ADPCM and played via _iocs_adpcmlot. Separate + // device from the OPM, so chip music and digital audio do not contend. + #define JL_HAS_AUDIO_PLAY_MOD + #define JL_HAS_AUDIO_STOP_MOD + #define JL_HAS_AUDIO_IS_PLAYING_MOD + #define JL_HAS_AUDIO_PLAY_SFX + #define JL_HAS_AUDIO_PLAY_SFX_STREAM + #define JL_HAS_AUDIO_STOP_SFX // lifecycle / present / input (every real port implements) #define JL_HAS_INIT #define JL_HAS_SHUTDOWN @@ -334,6 +393,80 @@ #define JL_HAS_SAVE_DIR_ENSURE #define JL_HAS_SAVE_DELETE #define JL_HAS_DISK_FREE +#elif defined(JOEYLIB_PLATFORM_X68000) + // Bring-up: platform SERVICES only (src/x68000/hal.c), plus the serial + // add-on (src/x68000/serial.c). Everything graphical comes from the portable + // generics, so the port renders correctly before any native primitive + // exists. Native ops arrive with the storage-model decision. + // Planar graphics, adapted from the Amiga (identical surface layout). + #define JL_HAS_SURFACE_CLEAR + #define JL_HAS_DRAW_PIXEL + #define JL_HAS_DRAW_LINE + #define JL_HAS_DRAW_CIRCLE + #define JL_HAS_FILL_CIRCLE + #define JL_HAS_FILL_RECT + #define JL_HAS_TILE_FILL + #define JL_HAS_TILE_COPY + #define JL_HAS_TILE_COPY_MASKED + #define JL_HAS_TILE_PASTE + #define JL_HAS_TILE_SNAP + #define JL_HAS_TILE_PASTE_MONO + #define JL_HAS_SPRITE_DRAW + #define JL_HAS_SPRITE_SAVE + #define JL_HAS_SPRITE_RESTORE + #define JL_HAS_FLOOD_WALK_PLANES + #define JL_HAS_FLOOD_SCAN_ROW_PLANES + #define JL_HAS_SURFACE_COPY_PLANES + #define JL_HAS_SURFACE_COPY_RECT + #define JL_HAS_SAMPLE_PIXEL + #define JL_HAS_SURFACE_HASH + #define JL_HAS_SURFACE_LOAD_FILE + #define JL_HAS_SURFACE_SAVE_FILE + #define JL_HAS_STAGE_ALLOC_PIXELS + #define JL_HAS_SURFACE_ALLOC_PIXELS + #define JL_HAS_SURFACE_ALLOC_PORT_DATA + #define JL_HAS_SURFACE_FREE_PORT_DATA + #define JL_HAS_INIT // _iocs_crtmod display bring-up + #define JL_HAS_SHUTDOWN // restore the previous CRT mode + #define JL_HAS_PRESENT // expand the chunky stage into GVRAM + #define JL_HAS_INPUT_INIT + #define JL_HAS_INPUT_SHUTDOWN + #define JL_HAS_INPUT_POLL // _iocs_bitsns key bitmaps -> gKeyState[] + #define JL_HAS_JOYSTICK_RESET + #define JL_HAS_WAIT_VBL // _iocs_vsync + #define JL_HAS_FRAME_COUNT + #define JL_HAS_FRAME_HZ // ~55 Hz in the 31 kHz modes + // Save add-on: Human68k _dos_dskfre / _dos_mkdir, structurally the ST HAL. + #define JL_HAS_DISK_FREE + #define JL_HAS_SAVE_DELETE + #define JL_HAS_SAVE_DIR_ENSURE + // Serial add-on: IOCS polled RS-232C, structurally the ST HAL. + #define JL_HAS_SERIAL_OPEN + #define JL_HAS_SERIAL_CLOSE + #define JL_HAS_SERIAL_POLL + #define JL_HAS_SERIAL_AVAILABLE + #define JL_HAS_SERIAL_READ + #define JL_HAS_SERIAL_WRITE + #define JL_HAS_SERIAL_FLUSH + // Audio. CHIP side on the YM2151 (OPM): tone voices + noise, which is what + // the portable JYM1 tracker rides, with no CPU mixing. + #define JL_HAS_AUDIO_INIT + #define JL_HAS_AUDIO_SHUTDOWN + #define JL_HAS_AUDIO_TONE + #define JL_HAS_AUDIO_VOICE + #define JL_HAS_AUDIO_NOISE + #define JL_HAS_AUDIO_CRITICAL_ENTER + #define JL_HAS_AUDIO_CRITICAL_EXIT + #define JL_HAS_AUDIO_FRAME_TICK + // SAMPLE side (src/x68000/audioPcm.c): libxmp-lite + the shared 5-slot SFX + // overlay, encoded to MSM6258 ADPCM and played via _iocs_adpcmlot. Separate + // device from the OPM, so chip music and digital audio do not contend. + #define JL_HAS_AUDIO_PLAY_MOD + #define JL_HAS_AUDIO_STOP_MOD + #define JL_HAS_AUDIO_IS_PLAYING_MOD + #define JL_HAS_AUDIO_PLAY_SFX + #define JL_HAS_AUDIO_PLAY_SFX_STREAM + #define JL_HAS_AUDIO_STOP_SFX #elif defined(JOEYLIB_PLATFORM_BLANK) // Copy-to-start template. It overrides ONLY the platform SERVICES (TODO // stubs in src/blank/blank.c). Everything graphical -- draw / tile / sprite diff --git a/make/amiga.mk b/make/amiga.mk index 6fd5f59..a6c59ad 100644 --- a/make/amiga.mk +++ b/make/amiga.mk @@ -138,7 +138,7 @@ ASSETBAKE_TARGET := amiga DATA_DIR := $(BINDIR)/DATA DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx -.PHONY: all amiga clean-amiga +.PHONY: all amiga clean-amiga clean all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(STAXI_ASSET_DSTS) $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @@ -293,6 +293,11 @@ $(DATA_DIR)/test.sfx: $(REPO_DIR)/assets/test.sfx clean-amiga: rm -rf $(BUILD) +# `clean` as an alias for `clean-amiga`. Without it a bare +# `make -f make/amiga.mk clean` FAILS -- and with stderr discarded that +# looks like success while leaving stale objects the next build links. +clean: clean-amiga + # Pull in per-object header-dependency files generated by gcc -MMD/-MP. # Without this, editing a header (e.g. surfaceInternal.h) doesn't rebuild # the .c files that include it, leaving a frankenstein binary where diff --git a/make/atarist.mk b/make/atarist.mk index d2bd0c7..9773f31 100644 --- a/make/atarist.mk +++ b/make/atarist.mk @@ -118,7 +118,7 @@ ASSETBAKE_TARGET := atarist DATA_DIR := $(BINDIR)/DATA DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx -.PHONY: all atarist clean-atarist +.PHONY: all atarist clean-atarist clean all atarist: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(STAXI_ASSET_DSTS) $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @@ -170,63 +170,63 @@ $(LIBXMP_AR): $(LIBXMP_OBJS) @mkdir -p $(dir $@) $(ST_AR) rcs $@ $^ -$(HELLO_BIN): $(HELLO_SRC) $(LIB) +$(HELLO_BIN): $(HELLO_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) -$(PATTERN_BIN): $(PATTERN_SRC) $(LIB) +$(PATTERN_BIN): $(PATTERN_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) -$(DRAW_BIN): $(DRAW_SRC) $(LIB) +$(DRAW_BIN): $(DRAW_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) -$(KEYS_BIN): $(KEYS_SRC) $(LIB) +$(KEYS_BIN): $(KEYS_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) -$(SERIAL_BIN): $(SERIAL_SRC) $(LIB) +$(SERIAL_BIN): $(SERIAL_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) -$(SERTEST_BIN): $(SERTEST_SRC) $(LIB) +$(SERTEST_BIN): $(SERTEST_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) -$(SAVE_BIN): $(SAVE_SRC) $(LIB) +$(SAVE_BIN): $(SAVE_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) -$(JOY_BIN): $(JOY_SRC) $(LIB) +$(JOY_BIN): $(JOY_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) -$(SPRITE_BIN): $(SPRITE_SRC) $(LIB) +$(SPRITE_BIN): $(SPRITE_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) -$(AUDIO_BIN): $(AUDIO_SRC) $(LIB) +$(AUDIO_BIN): $(AUDIO_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) -$(UBER_BIN): $(UBER_SRC) $(LIB) +$(UBER_BIN): $(UBER_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) -$(ADV_BIN): $(ADV_SRC) $(LIB) +$(ADV_BIN): $(ADV_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) -$(ADV2_BIN): $(ADV2_SRC) $(LIB) +$(ADV2_BIN): $(ADV2_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) -$(AGI_BIN): $(AGI_SRCS) $(LIB) +$(AGI_BIN): $(AGI_SRCS) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $(AGI_SRCS) $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) -$(STAXI_BIN): $(STAXI_SRCS) $(LIB) +$(STAXI_BIN): $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) @@ -279,6 +279,11 @@ $(DATA_DIR)/test.sfx: $(REPO_DIR)/assets/test.sfx clean-atarist: rm -rf $(BUILD) +# `clean` as an alias for `clean-atarist`. Without it a bare +# `make -f make/atarist.mk clean` FAILS -- and with stderr discarded that +# looks like success while leaving stale objects the next build links. +clean: clean-atarist + # Pull in per-object header-dependency files generated by gcc -MMD/-MP. # Without this, editing a header (e.g. surfaceInternal.h) doesn't rebuild # the .c files that include it, leaving a frankenstein binary where diff --git a/make/common.mk b/make/common.mk index ecbe9ae..fe45e5a 100644 --- a/make/common.mk +++ b/make/common.mk @@ -23,4 +23,4 @@ GENERIC_C_SRCS := $(filter-out %/spriteEmitStub.c, $(wildcard $(SRC_DIR)/generic # Common include flags. Per-port code can include port.h / surfaceInternal.h # directly because SRC_CORE is in the include path. -COMMON_CFLAGS := -I$(INCLUDE_DIR) -I$(SRC_CORE) -Wall -Wextra -Werror -O2 +COMMON_CFLAGS := -I$(INCLUDE_DIR) -I$(SRC_CORE) -Wall -Wextra -Werror -O2 $(EXTRA_CFLAGS) diff --git a/make/dos.mk b/make/dos.mk index 48276b9..439545c 100644 --- a/make/dos.mk +++ b/make/dos.mk @@ -133,7 +133,7 @@ STAXI_ASSET_DSTS += $(STAXI_SPC_RUN) MKSTLEVEL_BIN := $(REPO_DIR)/build/tools/mkstlevel -.PHONY: all dos clean-dos +.PHONY: all dos clean-dos clean all dos: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(STAXI_ASSET_DSTS) $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @@ -168,77 +168,77 @@ $(LIBXMP_AR): $(LIBXMP_OBJS) @mkdir -p $(dir $@) $(DOS_AR) rcs $@ $^ -$(HELLO_BIN): $(HELLO_SRC) $(LIB) +$(HELLO_BIN): $(HELLO_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(DOS_EMBED_DPMI) $@ -$(PATTERN_BIN): $(PATTERN_SRC) $(LIB) +$(PATTERN_BIN): $(PATTERN_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(DOS_EMBED_DPMI) $@ -$(DRAW_BIN): $(DRAW_SRC) $(LIB) +$(DRAW_BIN): $(DRAW_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(DOS_EMBED_DPMI) $@ -$(KEYS_BIN): $(KEYS_SRC) $(LIB) +$(KEYS_BIN): $(KEYS_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(DOS_EMBED_DPMI) $@ -$(SERIAL_BIN): $(SERIAL_SRC) $(LIB) +$(SERIAL_BIN): $(SERIAL_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(DOS_EMBED_DPMI) $@ -$(SERTEST_BIN): $(SERTEST_SRC) $(LIB) +$(SERTEST_BIN): $(SERTEST_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(DOS_EMBED_DPMI) $@ -$(SAVE_BIN): $(SAVE_SRC) $(LIB) +$(SAVE_BIN): $(SAVE_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(DOS_EMBED_DPMI) $@ -$(JOY_BIN): $(JOY_SRC) $(LIB) +$(JOY_BIN): $(JOY_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(DOS_EMBED_DPMI) $@ -$(SPRITE_BIN): $(SPRITE_SRC) $(LIB) +$(SPRITE_BIN): $(SPRITE_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(DOS_EMBED_DPMI) $@ -$(AUDIO_BIN): $(AUDIO_SRC) $(LIB) +$(AUDIO_BIN): $(AUDIO_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(DOS_EMBED_DPMI) $@ -$(UBER_BIN): $(UBER_SRC) $(LIB) +$(UBER_BIN): $(UBER_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(DOS_EMBED_DPMI) $@ -$(ADV_BIN): $(ADV_SRC) $(LIB) +$(ADV_BIN): $(ADV_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(DOS_EMBED_DPMI) $@ -$(ADV2_BIN): $(ADV2_SRC) $(LIB) +$(ADV2_BIN): $(ADV2_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(DOS_EMBED_DPMI) $@ -$(AGI_BIN): $(AGI_SRCS) $(LIB) +$(AGI_BIN): $(AGI_SRCS) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) $(AGI_SRCS) $(LIB) $(LIBXMP_AR) -o $@ $(DOS_EMBED_DPMI) $@ -$(STAXI_BIN): $(STAXI_SRCS) $(LIB) +$(STAXI_BIN): $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) -o $@ $(DOS_EMBED_DPMI) $@ @@ -303,6 +303,11 @@ $(DATA_DIR)/test.sfx: $(REPO_DIR)/assets/test.sfx clean-dos: rm -rf $(BUILD) +# `clean` as an alias for `clean-dos`. Without it a bare +# `make -f make/dos.mk clean` FAILS -- and with stderr discarded that +# looks like success while leaving stale objects the next build links. +clean: clean-dos + # Nuke regenerable Space Taxi artifacts. Sources under # examples/spacetaxi/assets/ are untouched; everything under # generated/ is recreated by the next build. diff --git a/make/iigs.mk b/make/iigs.mk index ac8984d..481b8ad 100644 --- a/make/iigs.mk +++ b/make/iigs.mk @@ -78,7 +78,7 @@ NTP_BIN := $(BUILD)/audio/ntpplayer.bin NTP_ASM := $(BUILD)/audio/ntpdata.s IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32 -.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-verify iigs-verify-all iigs-verify-save clean-iigs +.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-verify iigs-verify-all iigs-verify-save iigs-verify-shrtail clean-iigs clean # Default: compile-check the library + run the end-to-end smoke test. all iigs: iigs-lib iigs-clang-smoke @@ -129,6 +129,34 @@ iigs-verify: iigs-disk iigs-verify-all: iigs-disk $(REPO_DIR)/scripts/verify-iigs-all.sh +# Gate the 712 display-state bytes ABOVE the pixel block: the SCB array +# ($E1:9D00-$9DC7) and all 16 palettes ($E1:9E00-$9FFF). Nothing else in the +# tree can see them -- the golden hash covers the STAGE, not $E1, and P9's +# pixel compare stops at $9CFF -- so a wrong palette is invisible to every +# other gate and shows only as wrong colors on a real screen. That blind spot +# is why a phantom "PEI-slam corrupts palettes 1-15" note survived five weeks. +# +# UBER's shrTailUpload / shrTailSlam rows live behind #ifdef UBER_PROBE, which +# no normal build defines, so this target builds its own UBER (never the +# goldened one) into a scratch dir, packs a single-app disk, and benches that. +# -DUBER_FRAMES=1 shrinks the timing window: a -DUBER_PROBE run at the golden +# 16 frames does not fit bench-iigs.sh's MAME cap. The goldened UBER binary, +# joey.2mg and joeylog.txt are all left untouched. +IIGS_PROBE_DIR := $(BUILD)/probe + +iigs-verify-shrtail: $(LIB) $(NTP_BIN) + @mkdir -p $(IIGS_PROBE_DIR)/bin + $(IIGS_CLANG_BUILD) -DUBER_PROBE -DUBER_FRAMES=1 $(INCLUDES) -o $(IIGS_PROBE_DIR)/bin/UBER $(UBER_SRC) $(LIB) + BINDIR="$(IIGS_PROBE_DIR)/bin" NTP_BIN="$(NTP_BIN)" JOEY_DISK_EXAMPLES="UBER" \ + $(REPO_DIR)/scripts/make-iigs-disk.sh $(IIGS_PROBE_DIR)/uberprobe.2mg + BENCH_DATA_DISK="$(IIGS_PROBE_DIR)/uberprobe.2mg" BENCH_OUT_LOG="$(IIGS_PROBE_DIR)/probelog.txt" \ + BENCH_MAX_FRAMES="$(or $(PROBE_MAX_FRAMES),45000)" $(REPO_DIR)/scripts/bench-iigs.sh + @grep -E 'shrParity|shrTailUpload|shrTailSlam' $(IIGS_PROBE_DIR)/probelog.txt || \ + { echo "iigs-verify-shrtail: FAIL - no probe rows captured" >&2; exit 1; } + @grep -qE 'shrTail(Upload|Slam) FAIL' $(IIGS_PROBE_DIR)/probelog.txt && \ + { echo "iigs-verify-shrtail: FAIL - \$$E1 SCB/palette block mismatched the stage" >&2; exit 1; } || true + @echo "iigs-verify-shrtail: PASS" + # Runtime gate for the save HAL's GS/OS calls (Create + GetDevNumber/DInfo/ # Volume): boots SAVE under MAME and checks the save-OK lamp + disk-free bar. iigs-verify-save: $(BINDIR)/SAVE $(BINDIR)/DRAW @@ -247,4 +275,9 @@ $(BINDIR)/AUDIO: $(AUDIO_SRC) $(LIB) $(NTP_BIN) $(IIGS_CLANG_BUILD) clean-iigs: rm -rf $(BUILD) +# `clean` as an alias for `clean-iigs`. Without it a bare +# `make -f make/iigs.mk clean` FAILS -- and with stderr discarded that +# looks like success while leaving stale objects the next build links. +clean: clean-iigs + -include $(wildcard $(DEP_DIR)/*.d) diff --git a/make/x68000.mk b/make/x68000.mk new file mode 100644 index 0000000..991a415 --- /dev/null +++ b/make/x68000.mk @@ -0,0 +1,165 @@ +# Sharp X68000 (elf2x68k / m68k-xelf-gcc) build rules. +# +# BRING-UP STATE. This builds libjoey.a and the serial example against the +# generic backend -- no native primitives, no audio, no sprite codegen yet. +# See docs/x68000_port.md for what is and is not real. +# +# The toolchain is elf2x68k (BSD-3, gcc 13.4.0 / binutils / newlib), staged into +# toolchains/x68000 by the tarball. m68k-xelf-gcc emits ELF; the driver's +# -Wl,-elf2x68k pass converts it to a Human68k .X. + +include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk + +# The libxmp-lite rules below define a real target before `all`, which would +# otherwise silently become make's default goal (symptom: a bare `make -f +# make/x68000.mk` reports only "libxmplite.a is up to date" and builds nothing). +.DEFAULT_GOAL := all + +PLATFORM := x68000 +BUILD := $(REPO_DIR)/build/$(PLATFORM) +LIBDIR := $(BUILD)/lib +BINDIR := $(BUILD)/bin + +X68K_ROOT := $(REPO_DIR)/toolchains/x68000/m68k-xelf +X68K_CC := $(X68K_ROOT)/bin/m68k-xelf-gcc +X68K_AR := $(X68K_ROOT)/bin/m68k-xelf-ar + +# The linker is driven by a wrapper script (m68k-elf/bin/ld.x) that shells out +# to `m68k-xelf-ld.bfd` by bare name, so the toolchain's own bin/ MUST be on +# PATH or the link dies with "m68k-xelf-ld.bfd: command not found". Compiling +# and archiving work fine without it, so this only bites at link time. +export PATH := $(X68K_ROOT)/bin:$(PATH) + +CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer \ + -DJOEYLIB_PLATFORM_X68000 \ + -I$(REPO_DIR)/toolchains/audio/libxmp-lite/include \ + -I$(SRC_DIR)/x68000 -I$(SRC_68K) -I$(REPO_DIR)/src/codegen -MMD -MP $(CFLAGS_EXTRA) +LDFLAGS := -lm + +PORT_C_SRCS := $(wildcard $(SRC_DIR)/x68000/*.c) +# Shared 68k planar asm (src/m68k/surface68k.s), already used by Amiga and ST: +# circle outline and 4-plane span fill. Free reuse now the surface layout +# matches. +SHARED_S := $(wildcard $(SRC_68K)/*.s) + +# No src/m68k sources yet: those are the planar sprite emitters and the planar +# surface helpers, and this port is still chunky. They come in with the storage +# model decision. +# common.mk filters spriteEmitStub.c out of GENERIC_C_SRCS because every +# shipping port has a real per-CPU emitter. This port does not yet, so add it +# back: jlSpriteCompile returns false and sprites stay INTERPRETER-ONLY, which +# is correct but slow (see the "sprites must be compiled or they interpret" +# note). Replacing this with a real 68k emitter is part of the storage-model +# work -- src/m68k/spriteEmitPlanar68k.c already exists and, on the text-plane +# path, needs only AMIGA_PLANE_STRIDE changed at three sites. +# Real sprite codegen: the shared 68k PLANAR emitter, unchanged. Its +# AMIGA_PLANE_STRIDE is SURFACE_WIDTH/8 = 40, which is this port's surface +# stride too -- the display's 128-byte stride never reaches the emitter, which +# only ever addresses the surface. So sprites COMPILE here rather than +# interpret; spriteEmitStub.o is gone. +CODEGEN_DIR := $(REPO_DIR)/src/codegen + +LIB_OBJS := \ + $(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS)) \ + $(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_SRCS)) \ + $(patsubst $(SRC_DIR)/x68000/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \ + $(BUILD)/obj/68k/spriteEmitPlanar68k.o \ + $(BUILD)/obj/codegen/spriteCompile.o \ + $(BUILD)/obj/codegen/spriteStage.o + +# libxmp-lite: the Protracker decoder shared with the DOS and ST ports. Same +# build recipe as make/atarist.mk -- LIBXMP_CORE_DISABLE_IT keeps it off math.h, +# which this newlib does not usefully provide for a 68000 target. +LIBXMP_DIR := $(REPO_DIR)/toolchains/audio/libxmp-lite +LIBXMP_SRC := $(filter-out %/win32.c, $(wildcard $(LIBXMP_DIR)/src/*.c) $(wildcard $(LIBXMP_DIR)/src/loaders/*.c)) +LIBXMP_OBJDIR := $(BUILD)/obj/libxmp-lite +LIBXMP_OBJS := $(patsubst $(LIBXMP_DIR)/src/%.c,$(LIBXMP_OBJDIR)/%.o,$(LIBXMP_SRC)) +LIBXMP_AR := $(LIBDIR)/libxmplite.a +LIBXMP_CFLAGS := -DLIBXMP_CORE_PLAYER -DLIBXMP_CORE_DISABLE_IT -DHAVE_FNMATCH=0 \ + -I$(LIBXMP_DIR)/include -I$(LIBXMP_DIR)/include/libxmp-lite \ + -I$(LIBXMP_DIR)/src -Wno-error -w + +$(LIBXMP_OBJDIR)/%.o: $(LIBXMP_DIR)/src/%.c + @mkdir -p $(dir $@) + $(X68K_CC) $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer $(LIBXMP_CFLAGS) -c $< -o $@ + +$(LIBXMP_AR): $(LIBXMP_OBJS) + @mkdir -p $(dir $@) + $(X68K_AR) rcs $@ $(LIBXMP_OBJS) + +LIB := $(LIBDIR)/libjoey.a + +SERIAL_SRC := $(EXAMPLES)/serial/serial.c +UBER_SRC := $(EXAMPLES)/uber/uber.c +AUDIO_SRC := $(EXAMPLES)/audio/audio.c + +.PHONY: all x68000 x68000-lib x68000-examples x68000-verify-serial x68000-verify-golden clean-x68000 clean + +all x68000: x68000-lib x68000-examples + +x68000-lib: $(LIB) $(LIBXMP_AR) + +x68000-examples: $(BINDIR)/SERIAL.X $(BINDIR)/UBER.X $(BINDIR)/AUDIO.X + +$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c + @mkdir -p $(dir $@) + $(X68K_CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/obj/generic/%.o: $(SRC_DIR)/generic/%.c + @mkdir -p $(dir $@) + $(X68K_CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s + @mkdir -p $(dir $@) + $(X68K_CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/obj/68k/%.o: $(SRC_68K)/%.c + @mkdir -p $(dir $@) + $(X68K_CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/obj/codegen/%.o: $(CODEGEN_DIR)/%.c + @mkdir -p $(dir $@) + $(X68K_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@ + +$(BUILD)/obj/port/%.o: $(SRC_DIR)/x68000/%.c + @mkdir -p $(dir $@) + $(X68K_CC) $(CFLAGS) -c $< -o $@ + +$(LIB): $(LIB_OBJS) + @mkdir -p $(dir $@) + $(X68K_AR) rcs $@ $(LIB_OBJS) + +$(BINDIR)/SERIAL.X: $(SERIAL_SRC) $(LIB) $(LIBXMP_AR) + @mkdir -p $(dir $@) + $(X68K_CC) $(CFLAGS) $(SERIAL_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@ + +# UBER is the golden-hash vehicle: it exercises every public op and prints a +# hash per op, which tools/diff-uber-hashes compares against the IIgs reference. +$(BINDIR)/UBER.X: $(UBER_SRC) $(LIB) $(LIBXMP_AR) + @mkdir -p $(dir $@) + $(X68K_CC) $(CFLAGS) $(UBER_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@ + +# AUDIO exercises both audio paths: JYM1 chip music on the OPM and MOD/SFX +# through the ADPCM channel. +$(BINDIR)/AUDIO.X: $(AUDIO_SRC) $(LIB) $(LIBXMP_AR) + @mkdir -p $(dir $@) + $(X68K_CC) $(CFLAGS) $(AUDIO_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@ + +# Both-directions RS-232C gate (~1 min). +x68000-verify-serial: $(BINDIR)/SERIAL.X + $(REPO_DIR)/scripts/verify-x68000-serial.sh + +# Golden-hash gate against the Apple IIgs reference (~70 min: UBER on the +# generic renderer is slow and must reach jlLogFlush before hashes exist). +x68000-verify-golden: $(LIB) $(LIBXMP_AR) + $(REPO_DIR)/scripts/verify-x68000-golden.sh + +clean-x68000: + rm -rf $(BUILD) + +# `clean` as an alias for `clean-x68000`. Without it a bare +# `make -f make/x68000.mk clean` FAILS -- and with stderr discarded that +# looks like success while leaving stale objects the next build links. +clean: clean-x68000 + +-include $(shell find $(BUILD) -name '*.d' 2>/dev/null) diff --git a/patches/mame-0.264-x68k-crtc-timing.patch b/patches/mame-0.264-x68k-crtc-timing.patch new file mode 100644 index 0000000..6ed8789 --- /dev/null +++ b/patches/mame-0.264-x68k-crtc-timing.patch @@ -0,0 +1,276 @@ +--- a/src/mame/sharp/x68k_crtc.h 2026-08-04 16:07:13.182675756 -0500 ++++ b/src/mame/sharp/x68k_crtc.h 2026-08-04 16:32:58.889623301 -0500 +@@ -61,7 +61,7 @@ + private: + // internal helpers + void text_copy(unsigned src, unsigned dest, u8 planes); +- TIMER_CALLBACK_MEMBER(operation_end); ++ void do_fast_clear(); + void refresh_mode(); + TIMER_CALLBACK_MEMBER(hsync); + TIMER_CALLBACK_MEMBER(raster_end); +@@ -84,6 +84,12 @@ + // internal state + u16 m_reg[24]; // registers + u8 m_operation; // operation port (0xe80481) ++ // Graphic high-speed clear (operation port bit 1). Writing 1 only ++ // RESERVES the clear; it starts at the next vertical display start and ++ // runs for one frame (two when interlaced). Reads of bit 1 report ++ // m_clear_frames != 0, so software can wait for start then for end. ++ bool m_clear_standby; // clear reserved, waiting for vertical display start ++ u8 m_clear_frames; // frames remaining in the running clear (0 = idle) + bool m_vblank; // true if in VBlank + bool m_hblank; // true if in HBlank + u16 m_htotal; // Horizontal Total (in characters) +@@ -111,7 +117,6 @@ + emu_timer *m_raster_irq_timer; + emu_timer *m_vblank_irq_timer; + emu_timer *m_raster_end_timer; +- emu_timer *m_operation_end_timer; + }; + + class vinas_device : public x68k_crtc_device +--- a/src/mame/sharp/x68k_crtc.cpp 2026-08-04 16:07:13.181675761 -0500 ++++ b/src/mame/sharp/x68k_crtc.cpp 2026-08-04 16:32:58.889623301 -0500 +@@ -25,6 +25,8 @@ + , m_clock_69m(0) + , m_clock_50m(0) + , m_operation(0) ++ , m_clear_standby(false) ++ , m_clear_frames(0) + , m_vblank(false) + , m_hblank(false) + , m_htotal(0) +@@ -58,7 +60,6 @@ + void x68k_crtc_device::device_start() + { + m_scanline_timer = timer_alloc(FUNC(x68k_crtc_device::hsync), this); +- m_operation_end_timer = timer_alloc(FUNC(x68k_crtc_device::operation_end), this); + m_raster_end_timer = timer_alloc(FUNC(x68k_crtc_device::raster_end), this); + m_raster_irq_timer = timer_alloc(FUNC(x68k_crtc_device::raster_irq), this); + m_vblank_irq_timer = timer_alloc(FUNC(x68k_crtc_device::vblank_irq), this); +@@ -66,6 +67,8 @@ + // save state + save_item(NAME(m_reg)); + save_item(NAME(m_operation)); ++ save_item(NAME(m_clear_standby)); ++ save_item(NAME(m_clear_frames)); + save_item(NAME(m_vblank)); + save_item(NAME(m_hblank)); + save_item(NAME(m_htotal)); +@@ -97,6 +100,10 @@ + m_reg[7] = 552; // Vertical end + m_reg[8] = 27; // Horizontal adjust + ++ // No graphic high-speed clear is reserved or running out of reset. ++ m_clear_standby = false; ++ m_clear_frames = 0; ++ + //m_scanline = screen().vpos();// = m_reg[6]; // Vertical start + + // start VBlank timer +@@ -127,15 +134,58 @@ + } + } + +-TIMER_CALLBACK_MEMBER(x68k_crtc_device::operation_end) +-{ +- if(!(m_operation & param)) ++// Graphic high-speed clear, performed when the operation actually starts ++// (at a vertical display start), not when the register is written. ++// ++// Range is determined by the screen size, the real screen size and PAGE 0's ++// scroll position. When the real screen is 1024x1024 the page select is ++// ignored and every page is cleared. ++void x68k_crtc_device::do_fast_clear() ++{ ++ // this is based on the docs except for the higher color depth modes which isn't ++ // explicitly described this way but is likely based on how the plane scroll works ++ // XXX: not sufficiently tested especially in hires modes ++ // it seems that it only uses the 0 page scroll registers for where to clear see atomrobo ++ uint16_t xscr = xscr_gfx(0) & 0x1ff; ++ uint16_t yscr = yscr_gfx(0) & 0x1ff; ++ uint16_t mask = 0; ++ for (int page = 0; page < 4; page++) + { +- m_operation |= param; +- m_operation_end_timer->adjust(attotime::from_msec(5), param); ++ if (!(m_reg[21] & (1 << page))) ++ mask |= (0xf << (page * 4)); ++ } ++ for (int y = yscr; y < (m_height + yscr); y++) ++ { ++ if (is_1024x1024()) ++ { ++ if (m_width > 256) ++ { ++ for (int x = 0; x < 512; x++) ++ { ++ uint16_t data = m_gvram_read_callback(((y * 512) + x) & 0x3ffff, 0xffff); ++ m_gvram_write_callback(((y * 512) + x) & 0x3ffff, data & mask, 0xffff); ++ } ++ } ++ else ++ { ++ for (int x = 0; x < 256; x++) ++ { ++ uint16_t data = m_gvram_read_callback(((y * 512) + x + xscr) & 0x3ffff, 0xffff); ++ m_gvram_write_callback(((y * 512) + x + xscr) & 0x3ffff, data & mask, 0xffff); ++ data = m_gvram_read_callback(((y * 512) + x + xscr + 256) & 0x3ffff, 0xffff); ++ m_gvram_write_callback(((y * 512) + x + xscr + 256) & 0x3ffff, data & mask, mask); ++ } ++ } ++ } ++ else ++ { ++ for (int x = 0; x < m_width; x++) ++ { ++ uint16_t data = m_gvram_read_callback(((y * 512) + x + xscr) & 0x3ffff, 0xffff); ++ m_gvram_write_callback(((y * 512) + x + xscr) & 0x3ffff, data & mask, 0xffff); ++ } ++ } + } +- else +- m_operation &= ~param; + } + + void x68k_crtc_device::refresh_mode() +@@ -215,9 +265,6 @@ + m_hblank = hstate; + m_hsync_callback(!m_hblank); + +- if (m_operation & 8) +- text_copy((m_reg[22] & 0xff00) >> 8, (m_reg[22] & 0x00ff), (m_reg[21] & 0xf)); +- + int scan = screen().vpos(); + if (hstate == 1) + { +@@ -228,6 +275,18 @@ + } + if (hstate == 0) + { ++ // Text raster copy, done ONCE per horizontal period at the start of ++ // the horizontal front porch (this callback fires at m_hend, the ++ // horizontal display end). Operation port bit 3 is a level-sensitive ++ // switch that the CRTC never clears and that has no busy indication: ++ // while it is set, one raster block is copied every scanline. Running ++ // this at the top of the callback instead copied on BOTH hsync edges, ++ // i.e. twice per scanline -- harmless while R22 is stable, but wrong ++ // for the documented idiom of leaving bit 3 on and pacing R22 updates ++ // against hsync. ++ if (m_operation & 8) ++ text_copy((m_reg[22] & 0xff00) >> 8, (m_reg[22] & 0x00ff), (m_reg[21] & 0xf)); ++ + if (scan == (m_vtotal - 1)) + scan = 0; + else +@@ -278,6 +337,30 @@ + if (val == 0) // V-DISP off + { + m_vblank = 0; ++ // This branch is the VDISP 0->1 edge (it asserts m_vdisp_callback ++ // below), which is where the graphic high-speed clear is serviced. ++ // A running clear counts down one frame; otherwise a reservation ++ // made via the operation port starts here. Only one of the two ++ // happens per frame, so a clear reserved during the frame in which ++ // a previous clear is still running does not start until that one ++ // has finished -- which is what makes a write immediately after ++ // busy drops miss the next frame on hardware. ++ if (m_clear_frames != 0) ++ { ++ m_clear_frames--; ++ } ++ else if (m_clear_standby) ++ { ++ m_clear_standby = false; ++ // One vertical period, and no interlace special case: refresh_mode() ++ // divides the vertical timing by m_vmultiple (0.5 when interlaced), ++ // so an interlaced screen already spans BOTH fields. One VDISP-to- ++ // VDISP here is therefore already the two vertical periods an ++ // interlaced clear occupies on hardware. ++ m_clear_frames = 1; ++ if (m_reg[21] & 0xf) ++ do_fast_clear(); ++ } + vblank_line = m_vend; + if (vblank_line > m_vtotal) + vblank_line = m_vtotal; +@@ -412,54 +495,14 @@ + break; + case 576: // operation register + m_operation = data & ~2; +- if ((data & 0x02) && (m_reg[21] & 0xf)) // high-speed graphic screen clear +- { +- // this is based on the docs except for the higher color depth modes which isn't +- // explicitly described this way but is likely based on how the plane scroll works +- // XXX: not sufficiently tested especially in hires modes +- // it seems that it only uses the 0 page scroll registers for where to clear see atomrobo +- uint16_t xscr = xscr_gfx(0) & 0x1ff; +- uint16_t yscr = yscr_gfx(0) & 0x1ff; +- uint16_t mask = 0; +- for (int page = 0; page < 4; page++) +- { +- if (!(m_reg[21] & (1 << page))) +- mask |= (0xf << (page * 4)); +- } +- for (int y = yscr; y < (m_height + yscr); y++) +- { +- if (is_1024x1024()) +- { +- if (m_width > 256) +- { +- for (int x = 0; x < 512; x++) +- { +- uint16_t data = m_gvram_read_callback(((y * 512) + x) & 0x3ffff, 0xffff); +- m_gvram_write_callback(((y * 512) + x) & 0x3ffff, data & mask, 0xffff); +- } +- } +- else +- { +- for (int x = 0; x < 256; x++) +- { +- uint16_t data = m_gvram_read_callback(((y * 512) + x + xscr) & 0x3ffff, 0xffff); +- m_gvram_write_callback(((y * 512) + x + xscr) & 0x3ffff, data & mask, 0xffff); +- data = m_gvram_read_callback(((y * 512) + x + xscr + 256) & 0x3ffff, 0xffff); +- m_gvram_write_callback(((y * 512) + x + xscr + 256) & 0x3ffff, data & mask, mask); +- } +- } +- } +- else +- { +- for (int x = 0; x < m_width; x++) +- { +- uint16_t data = m_gvram_read_callback(((y * 512) + x + xscr) & 0x3ffff, 0xffff); +- m_gvram_write_callback(((y * 512) + x + xscr) & 0x3ffff, data & mask, 0xffff); +- } +- } +- } +- } +- if (data & 0x02) m_operation_end_timer->adjust(attotime::from_msec(5), 0x02); // time taken to do operation is a complete guess. ++ // Bit 1 only RESERVES the graphic high-speed clear -- it does not ++ // perform it. Hardware latches the request and starts the clear at the ++ // next vertical display start; see vblank_irq(). Writing 0 cannot ++ // cancel or abort a reservation that has already been made, and a ++ // request made while a clear is already running is dropped rather than ++ // queued: the reservation only takes when bit 1 reads back as 0. ++ if ((data & 0x02) && m_clear_frames == 0) ++ m_clear_standby = true; + break; + } + // LOG("%s CRTC: Wrote %04x to CRTC register %i\n",machine().describe_context(), data, offset); +@@ -493,7 +536,14 @@ + } + } + if (offset == 576) // operation port, operation bits are set to 0 when operation is complete +- return m_operation; ++ { ++ // Bit 1 reads as the graphic high-speed clear's busy state: it is set ++ // from the vertical display start at which the clear begins until the ++ // vertical display start one frame later (two when interlaced). The ++ // documented wait sequence is to poll for bit 1 becoming set (start) ++ // and then for it becoming clear again (end). ++ return m_operation | (m_clear_frames != 0 ? 0x02 : 0x00); ++ } + // LOG("CRTC: [%08x] Read from unknown CRTC register %i\n",activecpu_get_pc(),offset); + return 0xffff; + } diff --git a/patches/mame-0.264-x68k-rs232.patch b/patches/mame-0.264-x68k-rs232.patch new file mode 100644 index 0000000..f98dc69 --- /dev/null +++ b/patches/mame-0.264-x68k-rs232.patch @@ -0,0 +1,53 @@ +--- a/src/mame/sharp/x68k.h 2026-08-04 18:08:43.712316105 -0500 ++++ b/src/mame/sharp/x68k.h 2026-08-04 18:06:29.498097848 -0500 +@@ -172,18 +172,10 @@ + uint8_t hdcvector = 0; + uint8_t prnvector = 0; + } m_ioc; +- struct +- { +- int inputtype = 0; // determines which input is to be received +- bool irqactive = false; // true if IRQ is being serviced +- uint8_t irqvector = 0; +- char last_mouse_x = 0; // previous mouse x-axis value +- char last_mouse_y = 0; // previous mouse y-axis value +- int bufferempty = 0; // non-zero if buffer is empty +- } m_mouse; + uint8_t m_ppi_portc = 0; + bool m_dmac_int = false; + bool m_mfp_int = false; ++ bool m_scc_int = false; + bool m_exp_irq2[2]{}; + bool m_exp_irq4[2]{}; + bool m_exp_nmi[2]{}; +@@ -195,7 +187,6 @@ + uint32_t m_gvram_wait = 0; + uint32_t m_tvram_wait = 0; + uint32_t m_vram_wait_accum = 0; // 16.16 carry between accesses +- emu_timer* m_mouse_timer = nullptr; + emu_timer* m_led_timer = nullptr; + unsigned char m_scc_prev = 0; + emu_timer* m_fdc_tc = nullptr; +--- a/src/mame/sharp/x68k.cpp 2026-08-04 18:08:43.712316105 -0500 ++++ b/src/mame/sharp/x68k.cpp 2026-08-04 18:06:57.240157174 -0500 +@@ -1236,6 +1236,20 @@ + mouse.rxd_handler().set(m_scc, FUNC(scc8530_device::rxb_w)); + m_scc->out_rtsb_callback().set(mouse, FUNC(rs232_port_device::write_rts)); + ++ // RS-232C -- SCC channel A. Only channel B (the mouse) was ever wired, so ++ // the machine's actual serial port had no connection at all and could not ++ // be exercised. Exposing it as a standard rs232 slot makes ++ // `-rs232c null_modem` (TCP socket) and `-rs232c pty` available, which is ++ // what an automated both-directions serial test needs. ++ m_scc->out_txda_callback().set("rs232c", FUNC(rs232_port_device::write_txd)); ++ m_scc->out_dtra_callback().set("rs232c", FUNC(rs232_port_device::write_dtr)); ++ m_scc->out_rtsa_callback().set("rs232c", FUNC(rs232_port_device::write_rts)); ++ ++ rs232_port_device &rs232c(RS232_PORT(config, "rs232c", default_rs232_devices, nullptr)); ++ rs232c.rxd_handler().set(m_scc, FUNC(scc8530_device::rxa_w)); ++ rs232c.dcd_handler().set(m_scc, FUNC(scc8530_device::dcda_w)); ++ rs232c.cts_handler().set(m_scc, FUNC(scc8530_device::ctsa_w)); ++ + RP5C15(config, m_rtc, 32.768_kHz_XTAL); + m_rtc->alarm().set(m_mfpdev, FUNC(mc68901_device::i0_w)); + m_rtc->set_year_offset(20); diff --git a/patches/mame-0.264-x68k-waitstates.patch b/patches/mame-0.264-x68k-waitstates.patch new file mode 100644 index 0000000..47d8de5 --- /dev/null +++ b/patches/mame-0.264-x68k-waitstates.patch @@ -0,0 +1,141 @@ +--- a/src/mame/sharp/x68k.h 2026-08-04 16:54:53.872522860 -0500 ++++ b/src/mame/sharp/x68k.h 2026-08-04 16:54:53.876522842 -0500 +@@ -195,6 +195,12 @@ + bool m_exp_nmi[2]{}; + uint8_t m_current_ipl = 0; + int m_led_state = 0; ++ // Extra cycles charged per VRAM access, over and above a main-memory ++ // access, in 16.16 fixed point. Set from measured values in machine_start ++ // for the machines those measurements cover; 0 (unmodelled) otherwise. ++ uint32_t m_gvram_wait = 0; ++ uint32_t m_tvram_wait = 0; ++ uint32_t m_vram_wait_accum = 0; // 16.16 carry between accesses + emu_timer* m_mouse_timer = nullptr; + emu_timer* m_led_timer = nullptr; + unsigned char m_scc_prev = 0; +@@ -265,6 +271,15 @@ + uint16_t exp_r(offs_t offset, uint16_t mem_mask = ~0); + void exp_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0); + ++ // VRAM wait states. The X68000's video RAM is slower than main memory; ++ // MAME charged nothing for it, so VRAM-bound code ran optimistically ++ // fast. These trampolines charge the difference and forward to the CRTC. ++ void charge_vram_wait(uint32_t increment); ++ uint16_t gvram_wait_r(offs_t offset); ++ void gvram_wait_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0); ++ uint16_t tvram_wait_r(offs_t offset); ++ void tvram_wait_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0); ++ + uint16_t spritereg_r(offs_t offset); + void spritereg_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0); + uint16_t spriteram_r(offs_t offset); +--- a/src/mame/sharp/x68k.cpp 2026-08-04 16:54:53.871522864 -0500 ++++ b/src/mame/sharp/x68k.cpp 2026-08-04 16:54:53.876522842 -0500 +@@ -992,12 +992,80 @@ + // Is this an undocumented MB89352 feature, an ASIC register, an original code bug or a bad dump? + } + ++// VRAM wait states. ++// ++// The 68000 in an X68000 does not reach video RAM as fast as it reaches main ++// memory, and MAME modelled no wait at all -- so any VRAM-bound inner loop ran ++// optimistically fast, which is exactly the code a graphics library cares ++// about. The figures used here are Makoto Kamada's real-hardware measurements ++// distributed with XEiJ (credited there to uchopon, tnb and ita), taken by ++// timing tst.w against each region on five 10 MHz X68000 variants: ++// ++// region tst.w time (us) derived wait (cycles) ++// main memory 1.642 .. 1.663 0.123 ++// GVRAM 1.734 .. 1.770 1.145 ++// TVRAM 1.829 .. 1.850 2.046 ++// ++// MAME charges nothing for main memory, so what is added here is the wait ++// RELATIVE to main memory: 1.145 - 0.123 = 1.022 cycles for GVRAM and ++// 2.046 - 0.123 = 1.923 for TVRAM. Cross-check against the raw measurements: ++// GVRAM - main memory averages ~1.03 cycles and TVRAM - main memory ~1.92 ++// across the five machines, which agrees. ++// ++// Main memory's own 0.123 cycles (DRAM refresh) is deliberately NOT modelled: ++// it would mean interposing on the hottest path in the driver to recover ~0.1 ++// cycles per access, and it is a uniform offset that does not change the ++// relative cost of VRAM versus RAM, which is the thing being measured. ++// ++// The wait is fractional, so it accumulates in 16.16 fixed point and whole ++// cycles are charged as they carry out -- the same technique the apple2gs ++// driver uses for its 1 MHz cycles (see slow_cycle() there). ++#define X68K_GVRAM_WAIT 0x000105a2 // 1.022 cycles, 16.16 ++#define X68K_TVRAM_WAIT 0x0001ec4a // 1.923 cycles, 16.16 ++ ++void x68k_state::charge_vram_wait(uint32_t increment) ++{ ++ // side_effects_disabled() covers debugger and Lua reads, which must not ++ // perturb the timing they are measuring. ++ if (increment == 0 || machine().side_effects_disabled()) ++ return; ++ m_vram_wait_accum += increment; ++ int const cycles = m_vram_wait_accum >> 16; ++ m_vram_wait_accum &= 0xffff; ++ if (cycles != 0) ++ m_maincpu->adjust_icount(-cycles); ++} ++ ++uint16_t x68k_state::gvram_wait_r(offs_t offset) ++{ ++ charge_vram_wait(m_gvram_wait); ++ return m_crtc->gvram_r(offset); ++} ++ ++void x68k_state::gvram_wait_w(offs_t offset, uint16_t data, uint16_t mem_mask) ++{ ++ charge_vram_wait(m_gvram_wait); ++ m_crtc->gvram_w(offset, data, mem_mask); ++} ++ ++uint16_t x68k_state::tvram_wait_r(offs_t offset) ++{ ++ charge_vram_wait(m_tvram_wait); ++ return m_crtc->tvram_r(offset); ++} ++ ++void x68k_state::tvram_wait_w(offs_t offset, uint16_t data, uint16_t mem_mask) ++{ ++ charge_vram_wait(m_tvram_wait); ++ m_crtc->tvram_w(offset, data, mem_mask); ++} ++ + void x68k_state::x68k_base_map(address_map &map) + { + map(0x000000, 0xbffffb).rw(FUNC(x68k_state::emptyram_r), FUNC(x68k_state::emptyram_w)); + map(0xbffffc, 0xbfffff).rw(FUNC(x68k_state::rom0_r), FUNC(x68k_state::rom0_w)); +- map(0xc00000, 0xdfffff).rw(m_crtc, FUNC(x68k_crtc_device::gvram_r), FUNC(x68k_crtc_device::gvram_w)); +- map(0xe00000, 0xe7ffff).rw(m_crtc, FUNC(x68k_crtc_device::tvram_r), FUNC(x68k_crtc_device::tvram_w)); ++ map(0xc00000, 0xdfffff).rw(FUNC(x68k_state::gvram_wait_r), FUNC(x68k_state::gvram_wait_w)); ++ map(0xe00000, 0xe7ffff).rw(FUNC(x68k_state::tvram_wait_r), FUNC(x68k_state::tvram_wait_w)); + map(0xe80000, 0xe81fff).rw(m_crtc, FUNC(x68k_crtc_device::crtc_r), FUNC(x68k_crtc_device::crtc_w)); + map(0xe82400, 0xe83fff).rw(FUNC(x68k_state::vid_r), FUNC(x68k_state::vid_w)); + map(0xe84000, 0xe85fff).rw(m_hd63450, FUNC(hd63450_device::read), FUNC(hd63450_device::write)); +@@ -1155,6 +1223,24 @@ + m_spriteram = (uint16_t*)(memregion("user1")->base()); + space.install_ram(0x000000,m_ram->size()-1,m_ram->pointer()); + ++ // VRAM wait states, applied only to the machine the measurements cover. ++ // The published figures are for a 10 MHz 68000 X68000; XEiJ's table also ++ // lists a 25 MHz X68030 (GVRAM 6.377, TVRAM 6.623) but those machines are ++ // MACHINE_NOT_WORKING here and cannot be validated, so they are left ++ // unmodelled rather than given invented numbers. ++ if (m_maincpu->clock() == 10'000'000) ++ { ++ m_gvram_wait = X68K_GVRAM_WAIT; ++ m_tvram_wait = X68K_TVRAM_WAIT; ++ } ++ else ++ { ++ m_gvram_wait = 0; ++ m_tvram_wait = 0; ++ } ++ m_vram_wait_accum = 0; ++ save_item(NAME(m_vram_wait_accum)); ++ + // start mouse timer + m_mouse_timer->adjust(attotime::zero, 0, attotime::from_msec(1)); // a guess for now + m_mouse.inputtype = 0; diff --git a/patches/mame-x68k-upstream-ab6b6fc9e797-serial-mouse.patch b/patches/mame-x68k-upstream-ab6b6fc9e797-serial-mouse.patch new file mode 100644 index 0000000..43f019e --- /dev/null +++ b/patches/mame-x68k-upstream-ab6b6fc9e797-serial-mouse.patch @@ -0,0 +1,511 @@ +From ab6b6fc9e7971b367ec54d565cf10f04e71109ec Mon Sep 17 00:00:00 2001 +From: Patrick Mackinlay +Date: Tue, 14 Jan 2025 13:53:33 +0700 +Subject: [PATCH] x68k: add serial mouse + +--- + src/mame/sharp/x68k.cpp | 169 ++++------------------------------ + src/mame/sharp/x68k.h | 24 +---- + src/mame/sharp/x68k_mouse.cpp | 103 +++++++++++++++++++++ + src/mame/sharp/x68k_mouse.h | 36 ++++++++ + 4 files changed, 158 insertions(+), 174 deletions(-) + create mode 100644 src/mame/sharp/x68k_mouse.cpp + create mode 100644 src/mame/sharp/x68k_mouse.h + +diff --git a/src/mame/sharp/x68k.cpp b/src/mame/sharp/x68k.cpp +index 8e5def0a501a6..c6787eeef393d 100644 +--- a/src/mame/sharp/x68k.cpp ++++ b/src/mame/sharp/x68k.cpp +@@ -119,6 +119,7 @@ + #include "x68k.h" + #include "x68k_hdc.h" + #include "x68k_kbd.h" ++#include "x68k_mouse.h" + + #include "machine/nvram.h" + +@@ -174,127 +175,6 @@ TIMER_CALLBACK_MEMBER(x68k_state::led_callback) + + } + +- +-// mouse input +-// port B of the Z8530 SCC +-// typically read from the SCC data port on receive buffer full interrupt per byte +-int x68k_state::read_mouse() +-{ +- char val = 0; +- char ipt = 0; +- +- if(!(m_scc->get_reg_b(5) & 0x02)) +- return 0xff; +- +- switch(m_mouse.inputtype) +- { +- case 0: +- ipt = m_mouse1->read(); +- break; +- case 1: +- val = m_mouse2->read(); +- ipt = val - m_mouse.last_mouse_x; +- m_mouse.last_mouse_x = val; +- break; +- case 2: +- val = m_mouse3->read(); +- ipt = val - m_mouse.last_mouse_y; +- m_mouse.last_mouse_y = val; +- break; +- } +- m_mouse.inputtype++; +- if(m_mouse.inputtype > 2) +- { +- int i_val = m_scc->get_reg_b(0); +- m_mouse.inputtype = 0; +- m_mouse.bufferempty = 1; +- i_val &= ~0x01; +- m_scc->set_reg_b(0, i_val); +- LOGMASKED(LOG_SYS, "SCC: mouse buffer empty\n"); +- } +- +- return ipt; +-} +- +-/* +- 0xe98001 - Z8530 command port B +- 0xe98003 - Z8530 data port B (mouse input) +- 0xe98005 - Z8530 command port A +- 0xe98007 - Z8530 data port A (RS232) +-*/ +-uint16_t x68k_state::scc_r(offs_t offset) +-{ +- offset %= 4; +- switch(offset) +- { +- case 0: +- return m_scc->reg_r(0); +- case 1: +- return read_mouse(); +- case 2: +- return m_scc->reg_r(1); +- case 3: +- return m_scc->reg_r(3); +- default: +- return 0xff; +- } +-} +- +-void x68k_state::scc_w(offs_t offset, uint16_t data) +-{ +- offset %= 4; +- +- switch(offset) +- { +- case 0: +- m_scc->reg_w(0,(uint8_t)data); +- if((m_scc->get_reg_b(5) & 0x02) != m_scc_prev) +- { +- if(m_scc->get_reg_b(5) & 0x02) // Request to Send +- { +- int val = m_scc->get_reg_b(0); +- m_mouse.bufferempty = 0; +- val |= 0x01; +- m_scc->set_reg_b(0,val); +- } +- } +- break; +- case 1: +- m_scc->reg_w(2,(uint8_t)data); +- break; +- case 2: +- m_scc->reg_w(1,(uint8_t)data); +- break; +- case 3: +- m_scc->reg_w(3,(uint8_t)data); +- break; +- } +- m_scc_prev = m_scc->get_reg_b(5) & 0x02; +-} +- +-TIMER_CALLBACK_MEMBER(x68k_state::scc_ack) +-{ +- if(m_mouse.bufferempty != 0) // nothing to do if the mouse data buffer is empty +- return; +- +-// if((m_ioc.irqstatus & 0xc0) != 0) +-// return; +- +- // hard-code the IRQ vector for now, until the SCC code is more complete +- if((m_scc->get_reg_a(9) & 0x08) || (m_scc->get_reg_b(9) & 0x08)) // SCC reg WR9 is the same for both channels +- { +- if((m_scc->get_reg_b(1) & 0x18) != 0) // if bits 3 and 4 of WR1 are 0, then Rx IRQs are disabled on this channel +- { +- if(m_scc->get_reg_b(5) & 0x02) // RTS signal +- { +- m_mouse.irqactive = true; +- m_mouse.irqvector = 0x54; +- update_ipl(); +- } +- } +- } +-} +- + void x68k_state::set_adpcm() + { + uint32_t rate = adpcm_div[m_adpcm.rate]; +@@ -833,7 +713,7 @@ void x68k_state::update_ipl() + new_ipl = 7; + else if (m_mfp_int) + new_ipl = 6; +- else if (m_mouse.irqactive) ++ else if (m_scc_int) + new_ipl = 5; + else if (m_exp_irq4[0] || m_exp_irq4[1]) + new_ipl = 4; +@@ -946,18 +826,6 @@ uint8_t x68k_state::iack4() + return 0x18; // spurious interrupt + } + +-uint8_t x68k_state::iack5() +-{ +- if (!machine().side_effects_disabled()) +- { +- m_mouse.irqactive = false; +- update_ipl(); +- } +- +- // TODO: use vector from SCC +- return m_mouse.irqvector; +-} +- + void x68k_state::cpu_space_map(address_map &map) + { + map.global_mask(0xffffff); +@@ -965,7 +833,7 @@ void x68k_state::cpu_space_map(address_map &map) + map(0xfffff5, 0xfffff5).r(FUNC(x68k_state::iack2)); + map(0xfffff7, 0xfffff7).r(m_hd63450, FUNC(hd63450_device::iack)); + map(0xfffff9, 0xfffff9).r(FUNC(x68k_state::iack4)); +- map(0xfffffb, 0xfffffb).r(FUNC(x68k_state::iack5)); ++ map(0xfffffb, 0xfffffb).lr8(NAME([this]() { return m_scc->m1_r(); })); + map(0xfffffd, 0xfffffd).r(m_mfpdev, FUNC(mc68901_device::get_vector)); + map(0xffffff, 0xffffff).lr8(NAME([] () { return m68000_base_device::autovector(7); })); + } +@@ -1005,7 +873,7 @@ void x68k_state::x68k_base_map(address_map &map) + map(0xe90000, 0xe91fff).rw(m_ym2151, FUNC(ym2151_device::read), FUNC(ym2151_device::write)).umask16(0x00ff); + map(0xe94000, 0xe94003).m(m_upd72065, FUNC(upd72065_device::map)).umask16(0x00ff); + map(0xe94004, 0xe94007).rw(FUNC(x68k_state::fdc_r), FUNC(x68k_state::fdc_w)); +- map(0xe98000, 0xe99fff).rw(FUNC(x68k_state::scc_r), FUNC(x68k_state::scc_w)); ++ map(0xe98000, 0xe99fff).rw(m_scc, FUNC(scc8530_device::ab_dc_r), FUNC(scc8530_device::ab_dc_w)).umask16(0x00ff); + map(0xe9a000, 0xe9bfff).rw(FUNC(x68k_state::ppi_r), FUNC(x68k_state::ppi_w)); + map(0xe9c000, 0xe9dfff).rw(FUNC(x68k_state::ioc_r), FUNC(x68k_state::ioc_w)); + map(0xe9e000, 0xe9e3ff).rw(FUNC(x68k_state::exp_r), FUNC(x68k_state::exp_w)); // FPU (Optional) +@@ -1068,16 +936,6 @@ static INPUT_PORTS_START( x68000 ) + PORT_CONFNAME( 0x02, 0x02, "Enable fake bus errors") + PORT_CONFSETTING( 0x00, DEF_STR( Off )) + PORT_CONFSETTING( 0x02, DEF_STR( On )) +- +- PORT_START("mouse1") // mouse buttons +- PORT_BIT( 0x00000001, IP_ACTIVE_HIGH, IPT_BUTTON9) PORT_NAME("Left mouse button") PORT_CODE(MOUSECODE_BUTTON1) +- PORT_BIT( 0x00000002, IP_ACTIVE_HIGH, IPT_BUTTON10) PORT_NAME("Right mouse button") PORT_CODE(MOUSECODE_BUTTON2) +- +- PORT_START("mouse2") // X-axis +- PORT_BIT( 0xff, 0x00, IPT_MOUSE_X) PORT_SENSITIVITY(100) PORT_KEYDELTA(0) PORT_PLAYER(1) +- +- PORT_START("mouse3") // Y-axis +- PORT_BIT( 0xff, 0x00, IPT_MOUSE_Y) PORT_SENSITIVITY(100) PORT_KEYDELTA(0) PORT_PLAYER(1) + INPUT_PORTS_END + + void x68k_state::floppy_load_unload(bool load, floppy_image_device *dev) +@@ -1151,10 +1009,6 @@ void x68k_state::machine_start() + m_spriteram = (uint16_t*)(memregion("user1")->base()); + space.install_ram(0x000000,m_ram->size()-1,m_ram->pointer()); + +- // start mouse timer +- m_mouse_timer->adjust(attotime::zero, 0, attotime::from_msec(1)); // a guess for now +- m_mouse.inputtype = 0; +- + // start LED timer + m_led_timer->adjust(attotime::zero, 0, attotime::from_msec(400)); + +@@ -1173,11 +1027,11 @@ void x68k_state::machine_start() + + m_dmac_int = false; + m_mfp_int = false; ++ m_scc_int = false; + m_exp_irq2[0] = m_exp_irq2[1] = false; + m_exp_irq4[0] = m_exp_irq4[1] = false; + m_exp_nmi[0] = m_exp_nmi[1] = false; + m_ioc.irqstatus = 0; +- m_mouse.irqactive = false; + m_current_ipl = 0; + m_adpcm.rate = 0; + m_adpcm.clock = 0; +@@ -1204,7 +1058,6 @@ void x68k_state::driver_start() + // copy last half of BIOS to a user region, to use for initial startup + memcpy(user2,(rom+0xff0000),0x10000); + +- m_mouse_timer = timer_alloc(FUNC(x68ksupr_state::scc_ack), this); + m_led_timer = timer_alloc(FUNC(x68ksupr_state::led_callback), this); + m_fdc_tc = timer_alloc(FUNC(x68ksupr_state::floppy_tc_tick), this); + m_adpcm_timer = timer_alloc(FUNC(x68ksupr_state::adpcm_drq_tick), this); +@@ -1249,6 +1102,11 @@ static void keyboard_devices(device_slot_interface &device) + device.option_add("x68k", X68K_KEYBOARD); + } + ++static void mouse_devices(device_slot_interface &device) ++{ ++ device.option_add("x68k", X68K_MOUSE); ++} ++ + void x68k_state::x68000_base(machine_config &config) + { + config.set_maximum_quantum(attotime::from_hz(60)); +@@ -1281,7 +1139,12 @@ void x68k_state::x68000_base(machine_config &config) + m_hd63450->dma_read<0>().set("upd72065", FUNC(upd72065_device::dma_r)); + m_hd63450->dma_write<0>().set("upd72065", FUNC(upd72065_device::dma_w)); + +- SCC8530(config, m_scc, 40_MHz_XTAL / 8); ++ SCC8530N(config, m_scc, 40_MHz_XTAL / 8); ++ m_scc->out_int_callback().set([this](int state) { m_scc_int = state; update_ipl(); }); ++ ++ rs232_port_device &mouse(RS232_PORT(config, "mouse_port", mouse_devices, "x68k")); ++ mouse.rxd_handler().set(m_scc, FUNC(scc8530_device::rxb_w)); ++ m_scc->out_rtsb_callback().set(mouse, FUNC(rs232_port_device::write_rts)); + + RP5C15(config, m_rtc, 32.768_kHz_XTAL); + m_rtc->alarm().set(m_mfpdev, FUNC(mc68901_device::i0_w)); +diff --git a/src/mame/sharp/x68k.h b/src/mame/sharp/x68k.h +index 2c1dc8552af67..b5c40b213b6bb 100644 +--- a/src/mame/sharp/x68k.h ++++ b/src/mame/sharp/x68k.h +@@ -20,7 +20,7 @@ + #include "cpu/m68000/m68000.h" + #include "cpu/m68000/m68030.h" + #include "imagedev/floppy.h" +-#include "machine/8530scc.h" ++#include "machine/z80scc.h" + #include "machine/hd63450.h" + #include "machine/i8255.h" + #include "machine/mb87030.h" +@@ -64,9 +64,6 @@ class x68k_state : public driver_device + , m_expansion(*this, "exp%u", 1U) + , m_adpcm_out(*this, {"adpcm_outl", "adpcm_outr"}) + , m_options(*this, "options") +- , m_mouse1(*this, "mouse1") +- , m_mouse2(*this, "mouse2") +- , m_mouse3(*this, "mouse3") + , m_eject_drv_out(*this, "eject_drv%u", 0U) + , m_ctrl_drv_out(*this, "ctrl_drv%u", 0U) + , m_access_drv_out(*this, "access_drv%u", 0U) +@@ -100,7 +97,7 @@ class x68k_state : public driver_device + required_device m_pcgpalette; + required_device m_mfpdev; + required_device m_rtc; +- required_device m_scc; ++ required_device m_scc; + required_device m_ym2151; + required_device m_ppi; + required_device m_screen; +@@ -111,9 +108,6 @@ class x68k_state : public driver_device + required_device_array m_adpcm_out; + + required_ioport m_options; +- required_ioport m_mouse1; +- required_ioport m_mouse2; +- required_ioport m_mouse3; + + output_finder<4> m_eject_drv_out; + output_finder<4> m_ctrl_drv_out; +@@ -178,24 +172,15 @@ class x68k_state : public driver_device + uint8_t hdcvector = 0; + uint8_t prnvector = 0; + } m_ioc; +- struct +- { +- int inputtype = 0; // determines which input is to be received +- bool irqactive = false; // true if IRQ is being serviced +- uint8_t irqvector = 0; +- char last_mouse_x = 0; // previous mouse x-axis value +- char last_mouse_y = 0; // previous mouse y-axis value +- int bufferempty = 0; // non-zero if buffer is empty +- } m_mouse; + uint8_t m_ppi_portc = 0; + bool m_dmac_int = false; + bool m_mfp_int = false; ++ bool m_scc_int = false; + bool m_exp_irq2[2]{}; + bool m_exp_irq4[2]{}; + bool m_exp_nmi[2]{}; + uint8_t m_current_ipl = 0; + int m_led_state = 0; +- emu_timer* m_mouse_timer = nullptr; + emu_timer* m_led_timer = nullptr; + unsigned char m_scc_prev = 0; + emu_timer* m_fdc_tc = nullptr; +@@ -233,7 +218,6 @@ class x68k_state : public driver_device + void dma_irq(int state); + void dma_end(offs_t offset, uint8_t data); + +- int read_mouse(); + void set_adpcm(); + + void fm_irq(int state); +@@ -241,8 +225,6 @@ class x68k_state : public driver_device + template void irq4_line(int state); + template void nmi_line(int state); + +- void scc_w(offs_t offset, uint16_t data); +- uint16_t scc_r(offs_t offset); + void fdc_w(offs_t offset, uint16_t data); + uint16_t fdc_r(offs_t offset); + void ioc_w(offs_t offset, uint16_t data); +diff --git a/src/mame/sharp/x68k_mouse.cpp b/src/mame/sharp/x68k_mouse.cpp +new file mode 100644 +index 0000000000000..f30cfc7d25d54 +--- /dev/null ++++ b/src/mame/sharp/x68k_mouse.cpp +@@ -0,0 +1,103 @@ ++// license:BSD-3-Clause ++// copyright-holders:Patrick Mackinlay ++ ++#include "emu.h" ++#include "x68k_mouse.h" ++ ++#include ++ ++//#define VERBOSE (LOG_GENERAL) ++#include "logmacro.h" ++ ++enum status_mask : u8 ++{ ++ STS_RB = 0x01, // right button ++ STS_LB = 0x02, // left button ++ STS_OX = 0x10, // x overflow ++ STS_UX = 0x20, // x underflow ++ STS_OY = 0x40, // y overflow ++ STS_UY = 0x80, // y underflow ++}; ++ ++DEFINE_DEVICE_TYPE(X68K_MOUSE, x68k_mouse_device, "x68k_mouse", "Sharp X68000 Mouse") ++ ++x68k_mouse_device::x68k_mouse_device(machine_config const &mconfig, char const *tag, device_t *owner, u32 clock) ++ : buffered_rs232_device<3>(mconfig, X68K_MOUSE, tag, owner, clock) ++ , m_buttons(*this, "BTN") ++ , m_x_axis(*this, "X") ++ , m_y_axis(*this, "Y") ++{ ++} ++ ++void x68k_mouse_device::device_start() ++{ ++ buffered_rs232_device<3>::device_start(); ++ ++ save_item(NAME(m_b)); ++ save_item(NAME(m_x)); ++ save_item(NAME(m_y)); ++ ++ set_data_frame(1, 8, PARITY_NONE, STOP_BITS_2); ++ set_tra_rate(4'800); ++ ++ transmit_register_reset(); ++ ++ m_b = 0; ++ m_x = 0; ++ m_y = 0; ++} ++ ++s16 read_axis(ioport_port &port, u16 &old_val) ++{ ++ u16 const new_val = port.read(); ++ s16 const delta = new_val - old_val; ++ ++ old_val = new_val; ++ ++ return delta; ++} ++ ++void x68k_mouse_device::input_rts(int state) ++{ ++ if (!state && fifo_empty()) ++ { ++ u8 status = m_buttons->read(); ++ s16 const dx = read_axis(*m_x_axis, m_x); ++ s16 const dy = read_axis(*m_y_axis, m_y); ++ ++ if (dx || dy || m_b != status) ++ { ++ if (dy < -128) ++ status |= STS_UY; ++ if (dy > 127) ++ status |= STS_OY; ++ if (dx < -128) ++ status |= STS_UX; ++ if (dx > 127) ++ status |= STS_OX; ++ ++ transmit_byte(status); ++ transmit_byte(s8(std::clamp(dx, -128, 127))); ++ transmit_byte(s8(std::clamp(dy, -128, 127))); ++ ++ m_b = status & (STS_LB | STS_RB); ++ } ++ } ++} ++ ++INPUT_PORTS_START(x68k) ++ PORT_START("BTN") ++ PORT_BIT(0x0002, IP_ACTIVE_HIGH, IPT_BUTTON1) PORT_CODE(MOUSECODE_BUTTON1) ++ PORT_BIT(0x0001, IP_ACTIVE_HIGH, IPT_BUTTON2) PORT_CODE(MOUSECODE_BUTTON2) ++ ++ PORT_START("X") ++ PORT_BIT(0xfff, 0x000, IPT_MOUSE_X) PORT_SENSITIVITY(100) PORT_KEYDELTA(0) ++ ++ PORT_START("Y") ++ PORT_BIT(0xfff, 0x000, IPT_MOUSE_Y) PORT_SENSITIVITY(100) PORT_KEYDELTA(0) ++INPUT_PORTS_END ++ ++ioport_constructor x68k_mouse_device::device_input_ports() const ++{ ++ return INPUT_PORTS_NAME(x68k); ++} +diff --git a/src/mame/sharp/x68k_mouse.h b/src/mame/sharp/x68k_mouse.h +new file mode 100644 +index 0000000000000..9028c831fe47e +--- /dev/null ++++ b/src/mame/sharp/x68k_mouse.h +@@ -0,0 +1,36 @@ ++// license:BSD-3-Clause ++// copyright-holders:Patrick Mackinlay ++ ++#ifndef MAME_SHARP_X68K_MOUSE_H ++#define MAME_SHARP_X68K_MOUSE_H ++ ++#pragma once ++ ++#include "bus/rs232/rs232.h" ++ ++class x68k_mouse_device : public buffered_rs232_device<3> ++{ ++public: ++ x68k_mouse_device(machine_config const &mconfig, char const *tag, device_t *owner, u32 clock = 0); ++ ++ virtual void input_rts(int state) override; // MSCTRL (active low) ++ ++protected: ++ virtual ioport_constructor device_input_ports() const override ATTR_COLD; ++ virtual void device_start() override ATTR_COLD; ++ ++ virtual void received_byte(u8 byte) override {} ++ ++private: ++ required_ioport m_buttons; ++ required_ioport m_x_axis; ++ required_ioport m_y_axis; ++ ++ u8 m_b; ++ u16 m_x; ++ u16 m_y; ++}; ++ ++DECLARE_DEVICE_TYPE(X68K_MOUSE, x68k_mouse_device) ++ ++#endif // MAME_SHARP_X68K_MOUSE_H diff --git a/patches/x68k-boot-check.lua b/patches/x68k-boot-check.lua new file mode 100644 index 0000000..19172b4 --- /dev/null +++ b/patches/x68k-boot-check.lua @@ -0,0 +1,33 @@ +-- Boot regression: confirm Human68k still reaches a live console after the +-- CRTC clear-timing and VRAM wait-state changes. Counts non-zero bytes in the +-- text plane (TVRAM plane 0 at $E00000) -- a real console lights up hundreds +-- of bytes; a hung or blank machine leaves it near zero. +local cpu = manager.machine.devices[":maincpu"] +local mem = cpu.spaces["program"] +local frame = 0 + +local function tvramNZ() + local n = 0 + for i = 0, 8191 do + if mem:read_u8(0xE00000 + i) ~= 0 then n = n + 1 end + end + return n +end + +emu.register_frame_done(function() + frame = frame + 1 + if frame % 300 == 0 and frame < 1200 then + io.write(string.format("BOOTPROG frame=%d tvramNZ=%d pc=%06X\n", + frame, tvramNZ(), cpu.state["CURPC"].value)) + io.flush() + elseif frame == 1200 then + local g = 0 + for i = 0, 4095 do + if mem:read_u8(0xC00000 + i) ~= 0 then g = g + 1 end + end + io.write(string.format("BOOTCHECK frame=%d tvramNZ=%d gvramNZ=%d pc=%06X\n", + frame, tvramNZ(), g, cpu.state["CURPC"].value)) + io.flush() + manager.machine:exit() + end +end) diff --git a/patches/x68k-crtc-probe.lua b/patches/x68k-crtc-probe.lua new file mode 100644 index 0000000..8d81079 --- /dev/null +++ b/patches/x68k-crtc-probe.lua @@ -0,0 +1,71 @@ +-- CRTC high-speed-clear timing probe. +-- +-- Discriminator 1 (unambiguous): write a pattern into GVRAM, reserve the +-- clear, then read GVRAM back IMMEDIATELY. Stock MAME performs the whole +-- clear synchronously inside the register write, so the pattern is already +-- gone. Real hardware (and the patched build) only RESERVES the clear, so the +-- pattern must still be intact until the next vertical display start. +-- +-- Discriminator 2: the busy bit (operation port $E80480 bit 1) sampled once +-- per frame. Patched: 0 until VDISP, then set for exactly one frame. +local cpu = manager.machine.devices[":maincpu"] +local mem = cpu.spaces["program"] +local frame = 0 + +local OPPORT = 0xE80480 -- CRTC operation port +local R21 = 0xE8002A -- clear page select +local GVRAM = 0xC00000 + +local PAT = { 0x1234, 0x5678, 0x9ABC, 0xDEF0 } +local probeAt = 240 +local done = false +local log = {} + +local function writePattern() + for i = 0, 3 do + mem:write_u16(GVRAM + i * 2, PAT[i + 1]) + end + -- a couple of rows in as well, so we are not only sampling row 0 + mem:write_u16(GVRAM + 512 * 2 * 4, 0x4321) +end + +local function readPattern() + local v = {} + for i = 0, 3 do + v[#v + 1] = string.format("%04X", mem:read_u16(GVRAM + i * 2)) + end + v[#v + 1] = string.format("%04X", mem:read_u16(GVRAM + 512 * 2 * 4)) + return table.concat(v, " ") +end + +local function busy() + return (mem:read_u16(OPPORT) & 0x02) ~= 0 and 1 or 0 +end + +emu.register_frame_done(function() + frame = frame + 1 + + if frame == probeAt then + writePattern() + io.write("CRTCPROBE pattern_written gvram=" .. readPattern() .. "\n") + io.write(string.format("CRTCPROBE busy_before=%d r21_before=%04X\n", + busy(), mem:read_u16(R21))) + mem:write_u16(R21, 0x000F) -- select all four clear pages + mem:write_u16(OPPORT, 0x0002) -- reserve the high-speed clear + -- IMMEDIATELY after the write, in the same emulated instant: + io.write("CRTCPROBE after_write gvram=" .. readPattern() .. + string.format(" busy=%d\n", busy())) + log[#log + 1] = string.format("f+0 busy=%d gv=%s", busy(), readPattern()) + elseif frame > probeAt and frame <= probeAt + 6 then + log[#log + 1] = string.format("f+%d busy=%d gv=%s", + frame - probeAt, busy(), readPattern()) + elseif frame == probeAt + 7 and not done then + done = true + for _, l in ipairs(log) do + io.write("CRTCPROBE " .. l .. "\n") + end + io.write("CRTCPROBE end\n") + io.flush() + manager.machine:exit() + end +end) diff --git a/patches/x68k-wait-probe.lua b/patches/x68k-wait-probe.lua new file mode 100644 index 0000000..a0167a9 --- /dev/null +++ b/patches/x68k-wait-probe.lua @@ -0,0 +1,82 @@ +-- VRAM wait-state measurement. +-- +-- Assembles a tight 68000 loop into RAM and gets it executed by hooking the +-- interrupt vectors -- MAME's m68k ignores writes to the rPC state entry, so +-- the CPU cannot simply be pointed at the code. Whichever interrupt fires +-- first enters the routine, which masks interrupts, runs the loop, writes a +-- sentinel and RTEs. +-- +-- Loop body is 8 unrolled `tst.w .L` + subq.l + bne.s: +-- nominal 68000 cost = 8*18 + 8 + 10 = 162 cycles/iteration at zero wait. +-- Running the identical loop against main memory and against VRAM isolates the +-- per-access wait: the only difference between runs is the address touched. +-- +-- TARGET_ADDR and ITER_COUNT are substituted by the harness. +local cpu = manager.machine.devices[":maincpu"] +local mem = cpu.spaces["program"] +local frame = 0 + +local CODE = 0x010000 +local SENTINEL = 0x011000 +local TARGET = TARGET_ADDR +local ITERS = ITER_COUNT +local UNROLL = 8 +local NOMINAL = 8 * 16 + 8 + 10 -- 146: TST.W (xxx).L = 4 + 12 ea + +local armed, t0 = false, nil + +local function w16(a, v) mem:write_u16(a, v) end + +local function assemble() + local p = CODE + w16(p, 0x007C); p = p + 2 -- ori.w #$0700,sr + w16(p, 0x0700); p = p + 2 + w16(p, 0x203C); p = p + 2 -- move.l #ITERS,d0 + w16(p, (ITERS >> 16) & 0xFFFF); p = p + 2 + w16(p, ITERS & 0xFFFF); p = p + 2 + local loop = p + for _ = 1, UNROLL do + w16(p, 0x4A79); p = p + 2 -- tst.w .L + w16(p, (TARGET >> 16) & 0xFFFF); p = p + 2 + w16(p, TARGET & 0xFFFF); p = p + 2 + end + w16(p, 0x5380); p = p + 2 -- subq.l #1,d0 + w16(p, 0x6600 | ((loop - (p + 2)) & 0xFF)); p = p + 2 -- bne.s loop + w16(p, 0x33FC); p = p + 2 -- move.w #$BEEF,SENTINEL.L + w16(p, 0xBEEF); p = p + 2 + w16(p, (SENTINEL >> 16) & 0xFFFF); p = p + 2 + w16(p, SENTINEL & 0xFFFF); p = p + 2 + w16(p, 0x4E73) -- rte +end + +emu.register_frame_done(function() + frame = frame + 1 + + if frame == 240 then + w16(SENTINEL, 0x0000) + assemble() + if mem:read_u16(CODE) ~= 0x007C then + io.write("WAITPROBE FAIL code did not land in RAM\n") + io.flush(); manager.machine:exit(); return + end + -- Point the autovectors ($60-$7F) and the MFP vector block ($100-$13F) + -- at the routine; whichever interrupt fires first runs it once. + for a = 0x60, 0x7C, 4 do mem:write_u32(a, CODE) end + for a = 0x100, 0x13C, 4 do mem:write_u32(a, CODE) end + t0 = manager.machine.time:as_double() + armed = true + elseif armed and mem:read_u16(SENTINEL) == 0xBEEF then + local dt = manager.machine.time:as_double() - t0 + local cycles = dt * 10000000.0 + local periter = cycles / ITERS + io.write(string.format( + "WAITPROBE target=%06X iters=%d sec=%.6f cyc=%.0f per_iter=%.3f extra_per_access=%.4f\n", + TARGET, ITERS, dt, cycles, periter, (periter - NOMINAL) / UNROLL)) + io.flush() + manager.machine:exit() + elseif frame > 2000 then + io.write("WAITPROBE TIMEOUT (routine never entered)\n") + io.flush() + manager.machine:exit() + end +end) diff --git a/scripts/bench-iigs.sh b/scripts/bench-iigs.sh index 6bda612..11cd040 100755 --- a/scripts/bench-iigs.sh +++ b/scripts/bench-iigs.sh @@ -25,8 +25,12 @@ repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}" sys_disk=$repo/toolchains/emulators/support/gsos-system.po -data_disk=$repo/build/iigs/bin/joey.2mg -out_log=$repo/build/iigs/bin/joeylog.txt +# Overridable so a diagnostic build (e.g. the -DUBER_PROBE binary built by +# `make iigs-verify-shrtail`) can be run off its own single-app disk into its +# own log without disturbing the goldened joey.2mg / joeylog.txt pair. Both +# default to the standard locations, so normal bench runs are unchanged. +data_disk="${BENCH_DATA_DISK:-$repo/build/iigs/bin/joey.2mg}" +out_log="${BENCH_OUT_LOG:-$repo/build/iigs/bin/joeylog.txt}" rompath="${MAME_ROMPATH:-$HOME/.mame/roms}" maxFrames="${BENCH_MAX_FRAMES:-36000}" diff --git a/scripts/verify-iigs.sh b/scripts/verify-iigs.sh index 31f6ae3..7538511 100755 --- a/scripts/verify-iigs.sh +++ b/scripts/verify-iigs.sh @@ -88,9 +88,23 @@ local function report() if not seen[hi] then seen[hi] = true; distinct = distinct + 1 end if not seen[lo] then seen[lo] = true; distinct = distinct + 1 end end + -- Launch witnesses, read from the display state ABOVE the pixel block. + -- scb0: jlScbSet/jlScbSetRange reject any palette index >= 16, so a + -- JoeyLib SCB byte is always 0x00-0x0F, while the Finder leaves 640-mode + -- bytes (bit 7 set, 0x80+). The dirty flags start true, so the very first + -- jlpPresent uploads the SCB -- scb0 < 0x10 therefore proves a JoeyLib + -- app took the screen. slamPark: the PEI slam parks 2*runMax+1 (always + -- ODD, 1..159) at \$E1:9DFE, which the Finder's 0x80 fill never is. + -- palSum: folds all 16 palettes so a wrong/stale palette block is visible + -- at all (the pixel scan stops at \$9CFF and never reads them). local scb0 = mem:read_u8(0xE19D00) - io.write(string.format("VERIFY-IIGS name=$NAME frame=%d distinctNibbles=%d nonZeroBytes=%d checksum=%06X scb0=%02X\n", - frame, distinct, nonzero, sum, scb0)) + local park = mem:read_u8(0xE19DFE) + local palsum = 0 + for i = 0, 511 do + palsum = (palsum + mem:read_u8(0xE19E00 + i)) & 0xFFFFFF + end + io.write(string.format("VERIFY-IIGS name=$NAME frame=%d distinctNibbles=%d nonZeroBytes=%d checksum=%06X scb0=%02X slamPark=%02X palSum=%06X\n", + frame, distinct, nonzero, sum, scb0, park, palsum)) io.flush() end @@ -130,6 +144,18 @@ if [ -z "$line" ]; then echo "$out" | tail -15 >&2 exit 1 fi +# Launch gate FIRST: without it this script grades whatever is on screen, and +# the GS/OS Finder desktop alone lights up ~9-10 distinct nibbles -- i.e. it +# PASSED for an example that never launched (control: `verify-iigs.sh +# nosuchapp` -> "distinctNibbles=9 ... PASS", scb0=80). Every IIgs visual claim +# made through this script was unsound until this check existed, and a memory +# dump taken during such a run reads the Finder's own SCB/palette block rather +# than the app's. +scb0=$(echo "$line" | sed -E 's/.*scb0=([0-9A-Fa-f]+).*/\1/') +if [ $((16#$scb0)) -ge 16 ]; then + echo "verify-iigs: FAIL ($NAME never launched - SHR SCB[0]=0x$scb0 still has bit 7 set, i.e. the Finder's 640-mode fill; a JoeyLib SCB byte is always 0x00-0x0F and the first present uploads it)" >&2 + exit 1 +fi distinct=$(echo "$line" | sed -E 's/.*distinctNibbles=([0-9]+).*/\1/') if [ "$distinct" -ge "$minDistinct" ]; then echo "verify-iigs: PASS ($NAME rendered, distinctNibbles=$distinct >= $minDistinct)" diff --git a/scripts/verify-x68000-golden.sh b/scripts/verify-x68000-golden.sh new file mode 100755 index 0000000..bb86bc2 --- /dev/null +++ b/scripts/verify-x68000-golden.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# verify-x68000-golden.sh - X68000 golden-hash gate. +# +# Builds UBER with a 1-frame measurement window (the timings are meaningless at +# that setting; this run is for the HASHES), stages it on a Human68k image, +# runs it headless under the patched MAME, extracts joeylog.txt back off the +# image with xdftool, and diffs the hashes against the Apple IIgs reference. +# +# Takes roughly 70 minutes: UBER on the generic renderer is slow, and the run +# must reach jlLogFlush at the very end before any hashes exist on disk. +# +# X68K_SCRATCH= bash scripts/verify-x68000-golden.sh +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +SP="${X68K_SCRATCH:?set X68K_SCRATCH to a work dir containing x68mame/}" +MAME="$repo/toolchains/cache/mame-mame0264/x68k" +TEMPLATE="$SP/x68mame/HUMAN302.XDF" +GOLDEN="${X68K_GOLDEN:-$repo/tests/goldens/uber/iigs.txt}" +FRAMES="${X68K_GOLDEN_FRAMES:-300000}" +WALL="${X68K_GOLDEN_WALL:-7200}" + +export PATH="$repo/toolchains/x68000/m68k-xelf/bin:$PATH" +work=$(mktemp -d -t joey-x68gold.XXXXXX) +trap 'rm -rf "$work"' EXIT + +# -s (strip) is REQUIRED: the unstripped binary is ~247 KB and does not fit +# alongside Human68k on a 1232 KB floppy (232 KB free). +m68k-xelf-gcc -s -O2 -m68000 -fomit-frame-pointer \ + -DJOEYLIB_PLATFORM_X68000 -DUBER_FRAMES=1u \ + -I"$repo/include" -I"$repo/src/core" -I"$repo/src/x68000" \ + -I"$repo/toolchains/audio/libxmp-lite/include" \ + "$repo/examples/uber/uber.c" \ + "$repo/build/x68000/lib/libjoey.a" "$repo/build/x68000/lib/libxmplite.a" -lm \ + -o "$work/UBER.X" || exit 1 + +# TWO DISKS. UBER.X is ~240 KB and Human68k needs ~90 KB, which leaves a 1232 KB +# floppy with under a cluster spare -- so joeylog.txt cannot be written and the +# run silently produces nothing after ~50 minutes. The boot disk therefore holds +# only the binary, and a blank second disk on B: takes the log. AUTOEXEC switches +# to B: before launching so the log lands there. +printf 'B:\r\nA:\\UBER.X\r\n\x1a' > "$work/AUTOEXEC.BAT" +cp "$TEMPLATE" "$work/gold.xdf" +# Blank data disk = the template with every file removed (keeps the format). +cp "$TEMPLATE" "$work/data.xdf" +for f in HUMAN.SYS CONFIG.SYS KEY.SYS USKCG.SYS BEEP.SYS STARTUP.ENV COMMAND.X AUTOEXEC.BAT; do + python3 "$repo/tools/xdftool.py" delete "$work/data.xdf" "$f" >/dev/null 2>&1 +done +# Trim the boot disk of anything not needed to boot and run one .X. +for f in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do + python3 "$repo/tools/xdftool.py" delete "$work/gold.xdf" "$f" >/dev/null 2>&1 +done +python3 "$repo/tools/xdftool.py" add "$work/gold.xdf" "$work/UBER.X" UBER.X >/dev/null || exit 1 +python3 "$repo/tools/xdftool.py" add "$work/gold.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null + +# UBER ends on jlWaitForAnyKey, AFTER jlLogFlush -- post keys late so it exits +# cleanly rather than being killed mid-write. +cat > "$work/gold.lua" < ($FRAMES - 60000) and frame % 2000 == 0 then + manager.machine.natkeyboard:post(" ") + end + if frame > $FRAMES then manager.machine:exit() end +end) +LUA + +timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \ + -rompath "$SP/x68mame/roms" -flop1 "$work/gold.xdf" -flop2 "$work/data.xdf" \ + -video none -sound none -nothrottle \ + -autoboot_script "$work/gold.lua" &1 | grep '^GOLD' + +python3 "$repo/tools/xdftool.py" extract "$work/data.xdf" joeylog.txt "$work/x68.txt" || { + echo "verify-x68000-golden: FAIL - no joeylog.txt (run did not reach jlLogFlush)" >&2 + exit 1 +} +tr -d '\r' < "$work/x68.txt" > "$work/x68.clean" +cp "$work/x68.clean" "$repo/build/x68000/joeylog.txt" +"$repo/tools/diff-uber-hashes" "$GOLDEN" "$work/x68.clean" diff --git a/scripts/verify-x68000-serial.sh b/scripts/verify-x68000-serial.sh new file mode 100755 index 0000000..9ba9bc4 --- /dev/null +++ b/scripts/verify-x68000-serial.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# verify-x68000-serial.sh - both-directions RS-232C gate for the X68000 port. +# +# Boots SERIAL.X under the patched MAME with SCC channel A wired to a null_modem +# on a TCP socket, sends a probe string in, and checks the example echoes it +# back. That exercises the full chain: elf2x68k -> .X -> xdftool image -> +# Human68k -> JoeyLib serial HAL -> IOCS -> SCC -> host. +# +# Requires: the patched MAME built at toolchains/cache/mame-mame0264/x68k +# (patches/mame-0.264-x68k-rs232.patch), and a Human68k template +# image to copy. +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +SP="${X68K_SCRATCH:?set X68K_SCRATCH to a work dir containing x68mame/}" +MAME="$repo/toolchains/cache/mame-mame0264/x68k" +TEMPLATE="$SP/x68mame/HUMAN302.XDF" +ROMS="$SP/x68mame/roms" +PORT="${X68K_SERIAL_PORT:-6800}" +PROBE="${X68K_SERIAL_PROBE:-JOEYLIB-X68K}" +MAXWAIT="${X68K_SERIAL_TIMEOUT:-420}" + +for f in "$MAME" "$TEMPLATE"; do + [ -e "$f" ] || { echo "verify-x68000-serial: missing $f" >&2; exit 2; } +done + +work=$(mktemp -d -t joey-x68ser.XXXXXX) +trap 'rm -rf "$work"; kill %1 2>/dev/null' EXIT + +# Autorun SERIAL.X. No ECHO OFF so a load failure is visible in a screenshot. +printf 'A:\\SERIAL.X\r\n\x1a' > "$work/AUTOEXEC.BAT" +cp "$TEMPLATE" "$work/serial.xdf" +python3 "$repo/tools/xdftool.py" add "$work/serial.xdf" \ + "$repo/build/x68000/bin/SERIAL.X" SERIAL.X >/dev/null +python3 "$repo/tools/xdftool.py" add "$work/serial.xdf" \ + "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null + +# Host end of the link. MAME's null_modem socket form CONNECTS, so we listen. +python3 "$repo/scripts/x68kSerialPeer.py" \ + --port "$PORT" --probe "$PROBE" --timeout "$MAXWAIT" \ + > "$work/peer.out" 2>&1 & +peer=$! +sleep 2 + +timeout -s KILL "$MAXWAIT" "$MAME" x68000 \ + -bios ipl10 -rompath "$ROMS" \ + -flop1 "$work/serial.xdf" \ + -rs232c null_modem -bitb "socket.127.0.0.1:$PORT" \ + -video none -sound none -nothrottle /dev/null 2>&1 + +wait "$peer" +rc=$? +cat "$work/peer.out" +exit $rc diff --git a/scripts/x68kSerialPeer.py b/scripts/x68kSerialPeer.py new file mode 100755 index 0000000..b39f74f --- /dev/null +++ b/scripts/x68kSerialPeer.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Host end of the X68000 serial gate. + +MAME's null_modem socket form CONNECTS outward, so this side listens. Once the +guest's SERIAL.X is up it echoes every byte it receives, so sending a probe +string and reading it back proves both directions of the link. + +The guest takes a while to appear: Human68k boots, then COMMAND.X runs +AUTOEXEC.BAT, then SERIAL.X initialises. Rather than guess, this retransmits +the probe periodically until the echo comes back or the deadline passes. +""" + +import argparse +import socket +import sys +import time + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--port", type=int, default=6800) + ap.add_argument("--probe", default="JOEYLIB-X68K") + ap.add_argument("--timeout", type=float, default=420.0) + ap.add_argument("--until", default="", + help="listen-only: keep collecting until this text arrives") + ap.add_argument("--listen-only", action="store_true", + help="never transmit; pass if ANY bytes arrive. Isolates the\n guest TX path (e.g. keyboard -> jlSerialWrite) from echo.") + args = ap.parse_args() + + probe = args.probe.encode("ascii") + deadline = time.time() + args.timeout + + srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + srv.bind(("127.0.0.1", args.port)) + srv.listen(1) + srv.settimeout(max(1.0, args.timeout)) + print(f"X68SER listening on 127.0.0.1:{args.port}") + + try: + conn, peer = srv.accept() + except socket.timeout: + print("X68SER FAIL: MAME never connected") + return 1 + print(f"X68SER connected from {peer}") + conn.settimeout(2.0) + + received = bytearray() + sent_total = 0 + last_send = 0.0 + + while time.time() < deadline: + # Retransmit periodically: the guest is not listening until SERIAL.X + # has booted and opened the port, and we cannot see when that happens. + if not args.listen_only and time.time() - last_send > 5.0: + try: + conn.sendall(probe) + sent_total += len(probe) + last_send = time.time() + except OSError as exc: + print(f"X68SER FAIL: send error {exc}") + return 1 + try: + chunk = conn.recv(256) + except socket.timeout: + continue + except OSError as exc: + print(f"X68SER FAIL: recv error {exc}") + return 1 + if not chunk: + print("X68SER FAIL: link closed by MAME") + return 1 + received += chunk + print(f"X68SER rx {len(chunk)} bytes: {chunk!r}") + if args.listen_only and args.until and args.until.encode() in received: + print(f"X68SER PASS: guest transmitted {len(received)} bytes " + f"unprompted: {bytes(received)!r}") + return 0 + if probe in received: + elapsed = args.timeout - (deadline - time.time()) + print(f"X68SER PASS: probe echoed back after {elapsed:.1f}s " + f"(sent {sent_total} bytes, received {len(received)})") + return 0 + + if args.listen_only and received: + print(f"X68SER COLLECTED {len(received)} bytes: {bytes(received)!r}") + return 0 + print(f"X68SER FAIL: timeout. sent={sent_total} received={len(received)} " + f"bytes: {bytes(received[:64])!r}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/codegen/spriteStage.c b/src/codegen/spriteStage.c index 47c4c66..dfade35 100644 --- a/src/codegen/spriteStage.c +++ b/src/codegen/spriteStage.c @@ -23,7 +23,7 @@ static uint32_t emitDrawForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { #if defined(JOEYLIB_PLATFORM_DOS) return spriteEmitDrawX86(out, cap, sp, shift); -#elif defined(JOEYLIB_PLATFORM_AMIGA) +#elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_X68000) return spriteEmitDrawPlanar68k(out, cap, sp, shift); #elif defined(JOEYLIB_PLATFORM_ATARIST) return spriteEmitDrawInterleaved68k(out, cap, sp, shift); @@ -43,7 +43,7 @@ static uint32_t emitDrawForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *s static uint32_t emitSaveForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { #if defined(JOEYLIB_PLATFORM_DOS) return spriteEmitSaveX86(out, cap, sp, shift); -#elif defined(JOEYLIB_PLATFORM_AMIGA) +#elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_X68000) return spriteEmitSavePlanar68k(out, cap, sp, shift); #elif defined(JOEYLIB_PLATFORM_ATARIST) return spriteEmitSaveInterleaved68k(out, cap, sp, shift); @@ -59,7 +59,7 @@ static uint32_t emitSaveForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *s static uint32_t emitRestoreForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { #if defined(JOEYLIB_PLATFORM_DOS) return spriteEmitRestoreX86(out, cap, sp, shift); -#elif defined(JOEYLIB_PLATFORM_AMIGA) +#elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_X68000) return spriteEmitRestorePlanar68k(out, cap, sp, shift); #elif defined(JOEYLIB_PLATFORM_ATARIST) return spriteEmitRestoreInterleaved68k(out, cap, sp, shift); @@ -101,7 +101,7 @@ static uint32_t spriteStageOne(uint8_t *staging, uint32_t total, const jlSpriteT if (written == SPRITE_EMIT_OVERFLOW || written == 0u || written > cap) { return 0; } -#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) +#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_PLATFORM_X68000) if ((written & 1u) != 0u) { // 68k call targets must stay word-aligned: one odd routine would // misalign every subsequent routine in the slot (address error on diff --git a/src/core/assetLoad.c b/src/core/assetLoad.c index e085451..b4d4ed3 100644 --- a/src/core/assetLoad.c +++ b/src/core/assetLoad.c @@ -99,6 +99,12 @@ typedef char AssetTileSizeCheckT[(sizeof(jlTileT) == TILE_BYTES) ? 1 : -1]; #define EXPECTED_TILE_TARGET 4u #elif defined(JOEYLIB_PLATFORM_BLANK) #define EXPECTED_TILE_TARGET 3u // chunky 4bpp, same as DOS (uses the generic tile ops) +#elif defined(JOEYLIB_PLATFORM_X68000) +// Chunky 4bpp while the port is on the generic backend, so it shares DOS's +// target byte -- the payload is bytes, so the endian difference does not +// matter. This gets its own target the moment the storage model is decided +// (a text-plane port would bake Amiga-shaped tiles and want target 1). +#define EXPECTED_TILE_TARGET 3u #else #error "Unknown platform for asset loader" #endif diff --git a/src/core/debug.c b/src/core/debug.c index a06472d..9027f36 100644 --- a/src/core/debug.c +++ b/src/core/debug.c @@ -28,6 +28,12 @@ static const char *kLogPath = "joeylog.txt"; +#ifdef JOEY_LOG_SERIAL_TEE +// Supplied by the port. Declared ahead of the platform split because BOTH log +// implementations below tee through it. +void jlpLogTee(const char *text); +#endif + #ifdef JOEYLIB_PLATFORM_IIGS // Ring capacity. Head is a free-running total-bytes-written counter @@ -48,6 +54,15 @@ static void ringAppend(const char *text) { uint16_t head; uint16_t idx; +#ifdef JOEY_LOG_SERIAL_TEE + // Opt-in live mirror of the log to the port's serial line. The 8 KB ring + // holds only the last ~100 lines and jlLogFlush needs a writable disk with + // room on it; a run that outlives either loses exactly the output being + // hunted. The tee is unbounded, needs no disk, and arrives in real time, + // which is what makes a wedged run diagnosable at all. OFF by default: + // it is a debugging build option, not a shipping cost. + jlpLogTee(text); +#endif // Explicit read-modify-write of the global (never `++`/`+=` on a // global -- the 65816 inc-abs/DBR trap). head = gJoeyLogRingHead; @@ -165,6 +180,10 @@ void jlLog(const char *msg) { if (msg == NULL) { return; } +#ifdef JOEY_LOG_SERIAL_TEE + jlpLogTee(msg); + jlpLogTee("\n"); +#endif fp = logFile(); if (fp == NULL) { return; @@ -182,6 +201,27 @@ void jlLogF(const char *fmt, ...) { return; } fp = logFile(); +#ifdef JOEY_LOG_SERIAL_TEE + // Format once into a buffer so the same bytes reach the file and the wire. + // Only under the tee: default builds keep the straight vfprintf, which has + // no line-length ceiling. + { + char line[200]; + + va_start(args, fmt); + (void)vsnprintf(line, sizeof(line), fmt, args); + va_end(args); + jlpLogTee(line); + jlpLogTee("\n"); + if (fp == NULL) { + return; + } + fputs(line, fp); + fputc('\n', fp); + fflush(fp); + return; + } +#else if (fp == NULL) { return; } @@ -190,6 +230,7 @@ void jlLogF(const char *fmt, ...) { va_end(args); fputc('\n', fp); fflush(fp); +#endif } diff --git a/src/core/port.h b/src/core/port.h index 6ab8c35..ca7d781 100644 --- a/src/core/port.h +++ b/src/core/port.h @@ -375,6 +375,14 @@ extern uint16_t iigsGetTickWord(void); #define jlpTilePasteMono amigaTilePasteMono #define jlpTileSnap amigaTileSnap #define jlpTileMapPaste amigaTileMapPaste +#elif defined(JOEYLIB_PLATFORM_X68000) + #define jlpTileFill x68kTileFill + #define jlpTileCopy x68kTileCopy + #define jlpTileCopyMasked x68kTileCopyMasked + #define jlpTilePaste x68kTilePaste + #define jlpTilePasteMono x68kTilePasteMono + #define jlpTileSnap x68kTileSnap + #define jlpTileMapPaste x68kTileMapPaste #elif defined(JOEYLIB_PLATFORM_DOS) // DOS overrides only the whole-map walker (dosTile.h, W3); the // single-tile ops stay on the chunky generics. @@ -1048,5 +1056,7 @@ void jlpGenericBigFree(void *p); #include "amigaTile.h" #elif defined(JOEYLIB_PLATFORM_DOS) #include "dosTile.h" +#elif defined(JOEYLIB_PLATFORM_X68000) + #include "x68kTile.h" #endif #endif diff --git a/src/core/sprite.c b/src/core/sprite.c index 8221afc..9737adf 100644 --- a/src/core/sprite.c +++ b/src/core/sprite.c @@ -36,7 +36,14 @@ * routine writes plane bytes directly, so the chunky interpreter * is a no-op and the jlpSpriteDrawPlanes hook would be a redundant * second draw. Same rationale as Amiga. */ -#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) +/* X68000 is pure planar too (s->pixels NULL) and shares the Amiga's compiled + * planar routines via the one spriteDispatch.h arm, so its compiled body writes + * the planes as well. Omitting it here made jlSpriteSaveAndDraw run the + * interpreted jlpSpriteSavePlanes AFTER the compiled draw had already painted + * the sprite -- the backup captured the SPRITE instead of the background, so + * the paired restore repainted it and left residue. Draw-only rows hid this + * because a double draw is idempotent. */ +#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_PLATFORM_X68000) #define COMPILED_SPRITE_WRITES_PLANES 1 #else #define COMPILED_SPRITE_WRITES_PLANES 0 @@ -1229,12 +1236,14 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, // metadata so they can size their buffer for the next // SaveUnder call. RestoreUnder will refuse to operate on a // backup with bytes==NULL. -#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) +#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_PLATFORM_X68000) // Planar metadata widening: a real (buffered) unclipped save on // these ports goes through the compiled 16-px-window path, so a // probe-then-allocate caller must be told the WINDOW size, not // the chunky-rounded size -- otherwise the next buffered save // could overrun the allocation by up to one group per row. + // X68000 takes that same compiled window path (spriteDispatch.h's + // shared planar arm), so it needs the widening for the same reason. if (dx == x && w == (int16_t)widthPx) { uint16_t nGroups = (uint16_t)(SPRITE_GROUPS0(widthPx) + SPRITE_SAVE_CLASS(sp, x)); diff --git a/src/core/spriteDispatch.h b/src/core/spriteDispatch.h index 6ddf1a2..fb7fd4a 100644 --- a/src/core/spriteDispatch.h +++ b/src/core/spriteDispatch.h @@ -22,6 +22,8 @@ #include "amigaPlanar.h" #elif defined(JOEYLIB_PLATFORM_ATARIST) #include "stPlanar.h" +#elif defined(JOEYLIB_PLATFORM_X68000) +#include "x68kPlanar.h" #endif #if defined(JOEYLIB_PLATFORM_IIGS) @@ -358,8 +360,20 @@ static inline __attribute__((always_inline)) void spriteCompiledRestoreMark(jlSp } -#elif defined(JOEYLIB_PLATFORM_AMIGA) +#elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_X68000) +// Amiga AND X68000 planar dispatchers. Both ports emit through +// spriteEmitPlanar68k.c (see src/codegen/spriteStage.c), so both MUST call +// through the 4-plane signature below. X68000 was added to the emitter ladder +// without this arm, so it fell through to the chunky #else and called a +// 4-argument routine with ONE argument: p1/p2/p3 became the caller's saved +// d3/d4/d5, and the emitted code wrote through them. In uber.c's drawShowcase +// d4 held &jlFillRect, so the sprite draw overwrote that function's own +// prologue and the next call through it faulted on a garbage stack slot. +// The two ports' planar layouts are identical -- X68kPlanarT mirrors +// AmigaPlanarT field-for-field and both strides are SURFACE_WIDTH/8 = 40 -- +// so one implementation serves both through these three aliases. +// // Amiga planar dispatchers. spriteEmitPlanar68k.c emits DRAW routines // with a cdecl(p0, p1, p2, p3) signature that write directly to // bitplanes, one pre-shifted variant per x % 8 phase (all 8 compiled @@ -376,19 +390,29 @@ static inline __attribute__((always_inline)) void spriteCompiledRestoreMark(jlSp // pd == NULL gate preserves the "not a planar surface -> no-op" // contract those calls provided. +#if defined(JOEYLIB_PLATFORM_X68000) +#include "x68kPlanar.h" +#define JL_PLANAR_T X68kPlanarT +#define JL_SURFACE_PLANAR(_s) x68kSurfacePlanar(_s) +#define JL_PLANAR_BYTES_PER_ROW X68K_BYTES_PER_ROW +#else #include "amigaPlanar.h" +#define JL_PLANAR_T AmigaPlanarT +#define JL_SURFACE_PLANAR(_s) amigaSurfacePlanar(_s) +#define JL_PLANAR_BYTES_PER_ROW AMIGA_BYTES_PER_ROW +#endif static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) { typedef void (*DrawFn)(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3); - AmigaPlanarT *pd; + JL_PLANAR_T *pd; uint16_t byteOff; DrawFn fn; - pd = amigaSurfacePlanar(dst); + pd = JL_SURFACE_PLANAR(dst); if (pd == NULL) { return; } - byteOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW + ((uint16_t)x >> 3)); + byteOff = (uint16_t)((uint16_t)y * JL_PLANAR_BYTES_PER_ROW + ((uint16_t)x >> 3)); fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset); fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff); } @@ -404,19 +428,19 @@ static inline __attribute__((always_inline)) void spriteCompiledDraw(jlSurfaceT // emitted move.w/move.l are 68000-legal). static inline __attribute__((always_inline)) void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) { typedef void (*CopyFn)(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3, uint8_t *buf); - AmigaPlanarT *pd; + JL_PLANAR_T *pd; int16_t bx; uint16_t nGroups; uint16_t byteOff; CopyFn fn; - pd = amigaSurfacePlanar(src); + pd = JL_SURFACE_PLANAR(src); if (pd == NULL) { return; } bx = (int16_t)(x & ~15); nGroups = (uint16_t)(SPRITE_GROUPS0(sp->widthPx) + shift); - byteOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW + ((uint16_t)bx >> 3)); + byteOff = (uint16_t)((uint16_t)y * JL_PLANAR_BYTES_PER_ROW + ((uint16_t)bx >> 3)); backup->x = bx; backup->y = y; @@ -431,20 +455,24 @@ static inline __attribute__((always_inline)) void spriteCompiledSaveUnder(const static inline __attribute__((always_inline)) void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) { typedef void (*CopyFn)(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3, uint8_t *buf); - AmigaPlanarT *pd; + JL_PLANAR_T *pd; uint16_t byteOff; CopyFn fn; (void)shift; - pd = amigaSurfacePlanar(dst); + pd = JL_SURFACE_PLANAR(dst); if (pd == NULL) { return; } - byteOff = (uint16_t)((uint16_t)backup->y * AMIGA_BYTES_PER_ROW + ((uint16_t)backup->x >> 3)); + byteOff = (uint16_t)((uint16_t)backup->y * JL_PLANAR_BYTES_PER_ROW + ((uint16_t)backup->x >> 3)); fn = (CopyFn)(codegenArenaBase() + backup->sprite->slot->offset + routeOffset); fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff, backup->bytes); } +#undef JL_PLANAR_T +#undef JL_SURFACE_PLANAR +#undef JL_PLANAR_BYTES_PER_ROW + #elif defined(JOEYLIB_PLATFORM_ATARIST) // ST word-interleaved planar runtime dispatch. The JIT routine takes diff --git a/src/core/spriteInternal.h b/src/core/spriteInternal.h index 83bdb6b..9deda1e 100644 --- a/src/core/spriteInternal.h +++ b/src/core/spriteInternal.h @@ -31,7 +31,11 @@ #define JOEY_SPRITE_SHIFT_COUNT 16 #define SPRITE_SHIFT_INDEX(x) ((uint8_t)((x) & 15)) #define SPRITE_DEGRADE_DRAW_MASK 0x0101u // phases 0 and 8 -#elif defined(JOEYLIB_PLATFORM_AMIGA) +#elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_X68000) +// Both emit through spriteEmitPlanar68k.c, whose shift is the x % 8 BIT phase +// within a 1bpp plane byte -- not the chunky x % 2 nibble phase. X68000 took +// the chunky #else before this arm existed, so only 2 of 8 phases were ever +// compiled and every odd x rendered at the wrong bit offset. #define JOEY_SPRITE_SHIFT_COUNT 8 #define SPRITE_SHIFT_INDEX(x) ((uint8_t)((x) & 7)) #define SPRITE_DEGRADE_DRAW_MASK 0x0001u // shift 0 @@ -75,7 +79,7 @@ // backup->bytes, so an odd caller buffer must route BOTH ops to the // byte-safe interpreted paths (pointer parity is stable between save // and restore, so provenance never splits). -#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) +#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_PLATFORM_X68000) #define SPRITE_GROUPS0(_wPx) ((uint16_t)(((_wPx) + 15u) >> 4)) // NOTE (P7-1, 2026-07-11): an algebraically-reduced form of these two // macros (save class = (x&15)+((widthPx+15)&15) >= 16; restore class = diff --git a/src/iigs/hal.c b/src/iigs/hal.c index aa66a19..d98fff6 100644 --- a/src/iigs/hal.c +++ b/src/iigs/hal.c @@ -290,10 +290,34 @@ void jlpPresent(const jlSurfaceT *src) { // 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. + // 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(); } diff --git a/src/x68000/audio.c b/src/x68000/audio.c new file mode 100644 index 0000000..b374c9d --- /dev/null +++ b/src/x68000/audio.c @@ -0,0 +1,242 @@ +// Sharp X68000 audio HAL -- YM2151 (OPM) tone/noise voices. +// +// SCOPE. This implements the CHIP side only: jlAudioVoice / jlAudioTone / +// jlAudioNoise, which is what the portable JYM1 chip-tracker (src/core/music.c) +// is built on. That makes jlMusicPlay work with no CPU mixing at all, the same +// way the Amiga and IIgs get it. +// +// The SAMPLE side (jlAudioPlayMod, the 5 jlAudioPlaySfx slots) is NOT here yet. +// It needs an OKI MSM6258 ADPCM encoder fed by DMAC channel 3 plus a 68000 asm +// mixer -- libxmp-lite's C mixer is not viable on any 68000 (measured at 874 +// cycles per voice per output sample). Those ops fall through to the no-op +// generics, so a program calling them is silent rather than broken. +// +// The OPM and the ADPCM channel are SEPARATE devices, so when the sample side +// does land, chip music and digital audio will run concurrently with zero +// contention -- unlike the ST, where the PWM engine hammers YM register 8 and +// destroys tone voice 0. + +#include + +#include "port.h" + +// The sample side lives in audioPcm.c (libxmp-lite + MSM6258 ADPCM). It is +// driven from this file's Init/Shutdown/FrameTick so there is one owner of the +// audio lifecycle, matching how the other ports are laid out. +bool jlpAudioInitPcm(void); +void jlpAudioShutdownPcm(void); +void jlpAudioPcmFrameTick(void); + + +// ----- YM2151 (OPM) ----- +// +// Two ports: write the register number to $E90001, then the value to $E90003. +// The chip needs a short settle between the address and data writes; the IOCS +// _OPMSET call handles the handshake, so use it rather than banging the ports +// directly and guessing at wait states. +#define OPM_REG_KEYON 0x08u // bits 2-0 channel, bits 6-3 slot mask +#define OPM_REG_NOISE 0x0Fu // bit 7 enable, bits 4-0 NFRQ +#define OPM_REG_RL_FB_CON 0x20u // +channel +#define OPM_REG_KC 0x28u // +channel: key code (octave<<4 | note) +#define OPM_REG_KF 0x30u // +channel: key fraction (bits 7-2) +#define OPM_REG_TL 0x60u // +slot: total level, 0 loudest .. 127 mute +#define OPM_REG_AR 0x80u // +slot: attack rate +#define OPM_REG_D1R 0xA0u // +slot: first decay +#define OPM_REG_D2R 0xC0u // +slot: second decay +#define OPM_REG_RR_D1L 0xE0u // +slot: release rate + first decay level + +#define OPM_SLOTS_ALL 0x78u // all four slots in the key-on mask +#define OPM_TL_MUTE 127u +#define OPM_NOISE_CHANNEL 7u // noise rides channel 7 operator 4 +#define OPM_NOISE_ENABLE 0x80u + +// jlAudioVoice exposes 3 tone voices; they map to OPM channels 0-2, leaving +// channel 7 for noise and channels 3-6 spare. +#define X68K_TONE_CHANNELS 3u + +// Connection 7 makes all four operators carriers, so a single operator's TL +// controls loudness directly and no modulator programming is needed. That is +// the simplest patch that produces a clean, predictable tone. +#define OPM_CONN_ALL_CARRIER 0x07u +#define OPM_PAN_BOTH 0xC0u + +// Slot numbering is channel + 8*operator. Only operator 3 (slot ch+24) is +// driven; the other three are muted, which with connection 7 gives one voice. +#define OPM_SLOT_OP4(_ch) ((uint8_t)((_ch) + 24u)) + +static bool gAudioReady = false; + + +static void opmWrite(uint8_t reg, uint8_t value) { + _iocs_opmset((int)reg, (int)value); +} + + +// Program one channel as a plain always-on tone: instant attack, no decay, +// no release. The tracker gates notes with key-on/key-off, so an envelope +// would fight it. +static void opmInitChannel(uint8_t ch) { + uint8_t slot; + uint8_t op; + + opmWrite((uint8_t)(OPM_REG_RL_FB_CON + ch), + (uint8_t)(OPM_PAN_BOTH | OPM_CONN_ALL_CARRIER)); + for (op = 0u; op < 4u; op++) { + slot = (uint8_t)(ch + (op * 8u)); + opmWrite((uint8_t)(OPM_REG_AR + slot), 31u); // fastest attack + opmWrite((uint8_t)(OPM_REG_D1R + slot), 0u); // no decay + opmWrite((uint8_t)(OPM_REG_D2R + slot), 0u); + opmWrite((uint8_t)(OPM_REG_RR_D1L + slot), 15u); // fast release + opmWrite((uint8_t)(OPM_REG_TL + slot), OPM_TL_MUTE); + } +} + + +// Frequency -> OPM key code. The chip's pitch is (octave, note, fraction); +// notes are NOT contiguous -- only 12 of the 16 codes are used, skipping +// 3, 7, 11 and 15. +// +// NEEDS TUNING AGAINST HARDWARE: the octave-4 anchor below is derived from the +// standard YM2151 relationship at a 3.579545 MHz clock, and the X68000 runs its +// OPM at 4 MHz, so absolute pitch will be sharp by a factor of about 1.117 +// until this is calibrated. The mapping is monotonic and octave-correct, so +// melodies play at the right intervals; they are simply transposed. +static const uint8_t kNoteCode[12] = { + 0u, 1u, 2u, 4u, 5u, 6u, 8u, 9u, 10u, 12u, 13u, 14u +}; + +// Semitone boundaries for octave 4 (C4..B4) in Hz, scaled by 16 to keep the +// comparison in integers without a divide. +static const uint16_t kOctave4Hz16[12] = { + 4186u, 4435u, 4699u, 4978u, 5274u, 5588u, + 5920u, 6272u, 6645u, 7040u, 7459u, 7902u +}; + + +static uint8_t opmKeyCode(uint16_t freqHz) { + uint32_t f; + uint8_t octave; + uint8_t note; + + if (freqHz == 0u) { + return 0u; + } + // Fold into the octave whose C..B range covers freqHz, tracking how many + // doublings that took. kOctave4Hz16 is 10x the real octave-4 pitches, so + // work in the same scaled domain. + f = (uint32_t)freqHz * 10u; + octave = 4u; + while (f < kOctave4Hz16[0] && octave > 0u) { + f *= 2u; + octave--; + } + while (f >= (uint32_t)kOctave4Hz16[11] * 2u && octave < 7u) { + f /= 2u; + octave++; + } + for (note = 0u; note < 11u; note++) { + if (f < kOctave4Hz16[note + 1u]) { + break; + } + } + return (uint8_t)((octave << 4) | kNoteCode[note]); +} + + +// ----- HAL entry points (alphabetical) ----- + +void jlpAudioCriticalEnter(void) { + // No IRQ-driven audio engine yet, so nothing to guard. When the ADPCM + // path lands its DMAC interrupt handler, this masks it. +} + + +void jlpAudioCriticalExit(void) { +} + + +void jlpAudioFrameTick(void) { + // Chip voices sustain in hardware, so the OPM needs nothing per frame. + // The sample path refills one ADPCM block here. + jlpAudioPcmFrameTick(); +} + + +bool jlpAudioInit(void) { + uint8_t ch; + + for (ch = 0u; ch < X68K_TONE_CHANNELS; ch++) { + opmWrite(OPM_REG_KEYON, ch); // all slots off + opmInitChannel(ch); + } + opmInitChannel(OPM_NOISE_CHANNEL); + opmWrite(OPM_REG_KEYON, OPM_NOISE_CHANNEL); + opmWrite(OPM_REG_NOISE, 0u); // noise generator off + gAudioReady = true; + // A PCM failure is not fatal: chip music still works, samples go silent. + (void)jlpAudioInitPcm(); + return true; +} + + +void jlpAudioNoise(uint8_t pitch, uint8_t atten) { + uint8_t slot; + + if (!gAudioReady) { + return; + } + slot = OPM_SLOT_OP4(OPM_NOISE_CHANNEL); + if (atten >= 15u) { + opmWrite(OPM_REG_NOISE, 0u); + opmWrite((uint8_t)(OPM_REG_TL + slot), OPM_TL_MUTE); + opmWrite(OPM_REG_KEYON, OPM_NOISE_CHANNEL); + return; + } + // NFRQ is inverted: 0 is the highest pitch, 31 the lowest. jlAudioNoise + // takes 0..31 with 0 lowest, matching the other ports, so flip it. + opmWrite(OPM_REG_NOISE, (uint8_t)(OPM_NOISE_ENABLE | ((31u - (pitch & 31u))))); + opmWrite((uint8_t)(OPM_REG_TL + slot), (uint8_t)((atten & 15u) * 4u)); + opmWrite(OPM_REG_KEYON, (uint8_t)(OPM_NOISE_CHANNEL | OPM_SLOTS_ALL)); +} + + +void jlpAudioShutdown(void) { + uint8_t ch; + + if (!gAudioReady) { + return; + } + for (ch = 0u; ch < X68K_TONE_CHANNELS; ch++) { + opmWrite(OPM_REG_KEYON, ch); + } + opmWrite(OPM_REG_NOISE, 0u); + opmWrite(OPM_REG_KEYON, OPM_NOISE_CHANNEL); + jlpAudioShutdownPcm(); + gAudioReady = false; +} + + +void jlpAudioTone(uint16_t freqHz) { + jlpAudioVoice(0u, freqHz, (uint8_t)(freqHz == 0u ? 15u : 0u)); +} + + +void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { + uint8_t slot; + + if (!gAudioReady || voice >= X68K_TONE_CHANNELS) { + return; + } + slot = OPM_SLOT_OP4(voice); + if (freqHz == 0u || atten >= 15u) { + opmWrite((uint8_t)(OPM_REG_TL + slot), OPM_TL_MUTE); + opmWrite(OPM_REG_KEYON, voice); // key off, all slots + return; + } + opmWrite((uint8_t)(OPM_REG_KC + voice), opmKeyCode(freqHz)); + opmWrite((uint8_t)(OPM_REG_KF + voice), 0u); + // atten 0..14 -> TL. TL is 0 = loudest, and roughly 0.75 dB per step, so + // 4 per attenuation step gives a usable range without reaching mute. + opmWrite((uint8_t)(OPM_REG_TL + slot), (uint8_t)((atten & 15u) * 4u)); + opmWrite(OPM_REG_KEYON, (uint8_t)(voice | OPM_SLOTS_ALL)); +} diff --git a/src/x68000/audioPcm.c b/src/x68000/audioPcm.c new file mode 100644 index 0000000..cecffa7 --- /dev/null +++ b/src/x68000/audioPcm.c @@ -0,0 +1,317 @@ +// Sharp X68000 sample audio: MOD playback + the 5-slot digital SFX overlay, +// output through the OKI MSM6258 ADPCM channel. +// +// Shares the decoder and the SFX overlay with DOS and the Atari ST: +// libxmp-lite renders the module, src/core/audioSfxMix.c mixes the SFX slots +// on top, and this file converts the result to 4-bit ADPCM and hands it to the +// chip. Only the output stage is X68000-specific. +// +// WHY THE RATE IS LOW. jlpAudioFrameTick does the render + encode on the main +// thread (libxmp-lite is not interrupt-safe -- same constraint, same answer, as +// DOS and ST). libxmp's C mixer costs ~874 cycles per voice per output sample +// on a 68000, so a 4-voice module at 15625 Hz would need ~5.5x the machine. +// MSM6258_RATE picks the chip's lowest rate to keep that affordable; it is the +// honest bring-up trade, and the fix is a 68000 asm mixer (~56 cyc/voice-sample +// measured), which is the same work the 68k HAL rewrite already schedules. +// +// OUTPUT STAGE. _iocs_adpcmlot walks a linked chain of blocks and IOCS drives +// DMAC channel 3, so no manual descriptor programming is needed. The chain is +// circular, giving gapless playback; jlpAudioFrameTick refills the block the +// chip is not currently walking. +// +// NOT YET VERIFIED AUDIBLY. This compiles and the control flow is exercised, +// but no waveform has been captured out of the emulator, so treat the encoder +// output and the block hand-off as unproven. + +#include + +#include +#include + +#include "joey/debug.h" +#include "port.h" +#include "audioSfxMixInternal.h" + + +// MSM6258 rate. NOT the chip's slowest (3906 Hz): libxmp's XMP_MIN_SRATE is +// 4000, so xmp_start_player REJECTS anything below it with XMP_ERROR_INVALID +// (-7) and MOD playback silently never starts. That bug shipped undetected +// until the audio-cost probe reported modPlaying=0 with startRc=-7 -- the +// return value was being discarded, so jlAudioPlayMod looked like it worked. +// +// 7812.5 Hz is the next chip rate clear of that floor, and the measured cost +// makes it affordable: the SFX+encode path benched at ~17 cycles/sample, so +// doubling the rate is ~13% of a 10 MHz 68000 rather than the ~6.6% at 3906. +#define MSM6258_RATE 7812 +// _iocs_adpcmmod / _iocs_adpcmlot mode byte: rate selector low, pan high. +// 0x05 = 7.8 kHz, both channels. +#define ADPCM_MODE 0x05 + +// Catch the 3906 Hz mistake at COMPILE time rather than as silence at runtime. +// xmp_start_player rejects anything under XMP_MIN_SRATE with XMP_ERROR_INVALID, +// and the only symptom is that MOD playback never starts. +typedef char x68kAdpcmRateAboveXmpFloor[(MSM6258_RATE >= XMP_MIN_SRATE) ? 1 : -1]; + +// Two blocks of PCM per chain entry. 1024 PCM samples = 512 ADPCM bytes = +// ~131 ms at 7812 Hz, so a dropped frame cannot starve the chip. +#define PCM_BLOCK 1024 +#define ADPCM_BLOCK (PCM_BLOCK / 2) +#define BLOCK_COUNT 2 + +static xmp_context gXmpCtx = NULL; +static bool gXmpLoaded = false; +static bool gXmpStarted = false; +static int gLoopCount = 0; +static bool gPcmReady = false; +static bool gPlaying = false; + +static AudioSfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS]; +// libxmp renders unsigned 8-bit mono here; audioSfxOverlayMix adds the SFX +// slots in place, then adpcmEncodeBlock converts it. +static uint8_t gMixBuf[PCM_BLOCK]; +static uint8_t gAdpcm[BLOCK_COUNT][ADPCM_BLOCK]; +static struct iocs_chain2 gChain[BLOCK_COUNT]; +static uint8_t gFillBlock = 0; +// Diagnostics: how many times each stage actually ran. Exposed (non-static) so +// a probe can read them without a debugger -- silent no-ops in the refill path +// are exactly the failure this port hit. +uint32_t gX68kPcmRefills = 0; +uint32_t gX68kPcmChainStarts = 0; +uint8_t gX68kPcmReady = 0; +uint8_t gX68kPcmPlaying = 0; +// Last return codes from the libxmp load/start pair, so a failed +// jlAudioPlayMod can be diagnosed without a debugger. +int32_t gX68kModLoadRc = 99; +int32_t gX68kModStartRc = 99; + +// MSM6258 / OKI ADPCM encoder state. The encoder runs the exact decoder on +// every nibble it emits so its predictor stays bit-identical to the chip's -- +// without that the two drift apart and the output degrades into noise. +static int32_t gEncPredictor = 0; +static int16_t gEncStepIndex = 0; + +static const int16_t kStepTable[49] = { + 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, + 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143, + 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, 449, + 494, 544, 598, 658, 724, 796, 876, 963,1060,1166,1282,1411, + 1552 +}; + +static const int8_t kIndexAdjust[8] = { -1, -1, -1, -1, 2, 4, 6, 8 }; + + +// Encode one PCM sample to a 4-bit ADPCM nibble, advancing the predictor +// exactly as the chip's decoder will. +static uint8_t adpcmEncodeSample(int16_t pcm) { + int32_t step; + int32_t diff; + int32_t delta; + uint8_t nibble; + + step = kStepTable[gEncStepIndex]; + diff = (int32_t)pcm - gEncPredictor; + nibble = 0u; + if (diff < 0) { + nibble = 8u; + diff = -diff; + } + // Three-bit magnitude search, MSB first: the standard ladder. + delta = step >> 3; + if (diff >= step) { + nibble |= 4u; + diff -= step; + delta += step; + } + if (diff >= (step >> 1)) { + nibble |= 2u; + diff -= step >> 1; + delta += step >> 1; + } + if (diff >= (step >> 2)) { + nibble |= 1u; + delta += step >> 2; + } + // Mirror the decoder so predictor and step index track the chip. + gEncPredictor += (nibble & 8u) ? -delta : delta; + if (gEncPredictor > 2047) { + gEncPredictor = 2047; + } else if (gEncPredictor < -2048) { + gEncPredictor = -2048; + } + gEncStepIndex = (int16_t)(gEncStepIndex + kIndexAdjust[nibble & 7u]); + if (gEncStepIndex < 0) { + gEncStepIndex = 0; + } else if (gEncStepIndex > 48) { + gEncStepIndex = 48; + } + return nibble; +} + + +// Convert PCM_BLOCK unsigned-8-bit samples into ADPCM_BLOCK bytes, two +// samples per byte, low nibble first (the chip consumes them in that order). +static void adpcmEncodeBlock(const uint8_t *pcm, uint8_t *out) { + uint16_t i; + uint8_t lo; + uint8_t hi; + + for (i = 0u; i < ADPCM_BLOCK; i++) { + // libxmp gives unsigned 8-bit; the encoder wants signed 12-bit-ish. + lo = adpcmEncodeSample((int16_t)(((int16_t)pcm[i * 2u] - 128) * 16)); + hi = adpcmEncodeSample((int16_t)(((int16_t)pcm[(i * 2u) + 1u] - 128) * 16)); + out[i] = (uint8_t)(lo | (hi << 4)); + } +} + + +// Render one block: module (if playing) then the SFX overlay on top. +static void renderBlock(uint8_t *dst) { + if (gXmpStarted) { + xmp_play_buffer(gXmpCtx, (void *)dst, PCM_BLOCK, gLoopCount); + } else { + memset(dst, 128, PCM_BLOCK); // unsigned-8 silence + } + audioSfxOverlayMix(dst, PCM_BLOCK, gSfxSlots, JOEY_AUDIO_SFX_SLOTS); +} + + +static void startChain(void) { + uint8_t b; + + for (b = 0u; b < BLOCK_COUNT; b++) { + renderBlock(gMixBuf); + adpcmEncodeBlock(gMixBuf, gAdpcm[b]); + gChain[b].addr = gAdpcm[b]; + gChain[b].len = ADPCM_BLOCK; + gChain[b].next = &gChain[(b + 1u) % BLOCK_COUNT]; // circular + } + gFillBlock = 0u; + _iocs_adpcmlot(&gChain[0], ADPCM_MODE); + gPlaying = true; + gX68kPcmPlaying = 1u; + gX68kPcmChainStarts++; +} + + +// ----- HAL entry points (alphabetical) ----- + +bool jlpAudioInitPcm(void) { + gXmpCtx = xmp_create_context(); + if (gXmpCtx == NULL) { + return false; + } + memset(gSfxSlots, 0, sizeof(gSfxSlots)); + gPcmReady = true; + gX68kPcmReady = 1u; + return true; +} + + +bool jlpAudioIsPlayingMod(void) { + return gXmpStarted; +} + + +void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { + if (!gPcmReady || data == NULL) { + return; + } + jlpAudioStopMod(); + gX68kModLoadRc = (int32_t)xmp_load_module_from_memory(gXmpCtx, (void *)data, (long)length); + if (gX68kModLoadRc != 0) { + return; + } + gXmpLoaded = true; + gX68kModStartRc = (int32_t)xmp_start_player(gXmpCtx, MSM6258_RATE, + XMP_FORMAT_MONO | XMP_FORMAT_8BIT | XMP_FORMAT_UNSIGNED); + if (gX68kModStartRc != 0) { + // Do not fail silently -- that is exactly how the rate bug survived. + jlLogF("x68k: xmp_start_player failed rc=%d rate=%d\n", + (int)gX68kModStartRc, (int)MSM6258_RATE); + xmp_release_module(gXmpCtx); + gXmpLoaded = false; + return; + } + // XMP_PLAYER_INTERP is left at the default NEAREST: linear interpolation + // more than doubles the mixer cost and the detail it preserves is thrown + // away by the 4-bit re-encode anyway. + gLoopCount = loop ? 0 : 1; + gXmpStarted = true; + if (!gPlaying) { + startChain(); + } +} + + +void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) { + if (!gPcmReady || slot >= JOEY_AUDIO_SFX_SLOTS) { + return; + } + audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MSM6258_RATE); + if (!gPlaying) { + startChain(); + } +} + + +void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) { + if (!gPcmReady || slot >= JOEY_AUDIO_SFX_SLOTS) { + return; + } + audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MSM6258_RATE); + if (!gPlaying) { + startChain(); + } +} + + +void jlpAudioStopMod(void) { + if (!gPcmReady) { + return; + } + if (gXmpStarted) { + xmp_end_player(gXmpCtx); + gXmpStarted = false; + } + if (gXmpLoaded) { + xmp_release_module(gXmpCtx); + gXmpLoaded = false; + } +} + + +void jlpAudioStopSfx(uint8_t slot) { + if (slot < JOEY_AUDIO_SFX_SLOTS) { + memset(&gSfxSlots[slot], 0, sizeof(gSfxSlots[slot])); + } +} + + +// Refill one block per frame. At 3906 Hz a block is ~262 ms and there are two, +// so the chip has roughly half a second of audio queued -- ample margin over a +// 55 Hz frame tick even if several frames are missed. +void jlpAudioPcmFrameTick(void) { + if (!gPcmReady || !gPlaying) { + return; + } + renderBlock(gMixBuf); + adpcmEncodeBlock(gMixBuf, gAdpcm[gFillBlock]); + gFillBlock = (uint8_t)((gFillBlock + 1u) % BLOCK_COUNT); + gX68kPcmRefills++; +} + + +void jlpAudioShutdownPcm(void) { + if (!gPcmReady) { + return; + } + jlpAudioStopMod(); + if (gPlaying) { + _iocs_adpcmmod(0); // stop the chip + gPlaying = false; + } + xmp_free_context(gXmpCtx); + gXmpCtx = NULL; + gPcmReady = false; +} diff --git a/src/x68000/hal.c b/src/x68000/hal.c new file mode 100644 index 0000000..c38760d --- /dev/null +++ b/src/x68000/hal.c @@ -0,0 +1,378 @@ +// Sharp X68000 HAL -- bring-up (Phase B: chunky stage, expand on present). +// +// The stage stays a chunky 4bpp surface in main RAM and jlpPresent expands it +// into GVRAM. That is deliberately the slow-but-correct path: it makes all of +// src/generic work unmodified, so the port renders correctly before any native +// primitive exists. Nothing here claims a JL_HAS_* override yet. +// +// GVRAM lives at $C00000. In 16-colour mode a pixel occupies the low nibble of +// its own 16-bit word (the other three nibbles belong to graphic pages 1-3), so +// one 4bpp source byte becomes two GVRAM words. That is 2 bytes of address +// space per displayed pixel -- the reason this path is a stepping stone rather +// than the destination. See the storage-model note in joey/platform.h. +// +// VERIFIED UNDER EMULATION (MAME 0.264 + the patches in patches/): this path +// renders, and UBER's captured hashes are byte-identical to the Apple IIgs +// golden reference. Input, serial and audio are separately proven -- see +// docs/x68000_port.md for the evidence for each. + +#include +#include + +#include "port.h" +#include + +#include "surfaceInternal.h" +#include "x68kPlanar.h" +#include "inputInternal.h" + + +// ----- Hardware addresses --------------------------------------------------- + +#define X68K_GVRAM ((volatile uint16_t *)0xC00000L) +// GVRAM is addressed as a 512-word-per-line grid regardless of the visible +// width, so the row stride is a constant, not a function of SURFACE_WIDTH. +#define X68K_GVRAM_STRIDE 512u + +// _iocs_crtmod screen mode. 12 = 512x512, 16 colours, 31 kHz. The library's +// 320x200 stage is drawn into the top-left corner; there is no 320x200 mode on +// this machine, so some form of letterboxing is unavoidable. +#define X68K_CRTMOD_512_16 12 + +// _iocs_bitsns key-group numbers that carry the keys jlKeyE cares about. +#define X68K_KEYGROUP_COUNT 15 + +// MC68901 MFP general-purpose I/O. Bit 4 is the CRTC's V-DISP line. +#define X68K_MFP_GPIP ((volatile uint8_t *)0xE88001L) +#define X68K_GPIP_VDISP 0x10u +#define X68K_VBL_SPIN_LIMIT 2000000ul +#define X68K_TEE_SPIN_LIMIT 200000ul + + +// ----- Module state --------------------------------------------------------- + +static int gPrevCrtMode = -1; +static bool gModeSet = false; +static uint16_t gFrameCount = 0; +// Saved USP from the _dos_super(0) switch, or -1 if we were already in +// supervisor mode and must not switch back. +static int gPrevSsp = -1; +// Last sampled V-DISP level, for the frame-counter edge detect. +static uint8_t gLastVdisp = 0; +// True once the V-DISP interrupt handler is live; the poll then stands down. +static bool gVdispInstalled = false; + + +// Defined below with the timing code; declared here because jlpInit installs it. +static void vdispHandler(void); +static void vdispPoll(void); +extern void *const gX68kVdispHandlerRef; + + +// ----- Lifecycle ------------------------------------------------------------ + +bool jlpInit(const jlConfigT *config) { + (void)config; + // Human68k starts .X programs in USER mode, so direct I/O reads (the MFP + // GPIP poll in jlpWaitVBL, and any future register banging) do not see the + // hardware. Same trap the Atari ST port hit, same fix: go supervisor here + // and stay there. _dos_super(0) returns the old USP, or a negative value + // if we were already supervisor -- in which case do not switch back. + gPrevSsp = _dos_super(0); + gPrevCrtMode = _iocs_crtmod(-1); // -1 queries without changing + _iocs_crtmod(X68K_CRTMOD_512_16); + _iocs_g_clr_on(); // clear graphics + enable the plane + // NOTE: installing vdispHandler via _iocs_vdispst HANGS the machine -- + // tested, no serial output at all, so it wedges before main() gets going. + // The handler ABI is not a plain C function: a _VDISPST handler is entered + // from the interrupt and this one returns with RTS from a context IOCS does + // not expect. Getting it right needs an asm thunk (save d0-d1/a0-a1, call, + // restore, correct return), which is worth doing but is not a one-liner. + // Until then the polled fallback below owns the counter. + gVdispInstalled = false; + gModeSet = true; + return true; +} + + +void jlpShutdown(void) { + if (!gModeSet) { + return; + } + if (gPrevCrtMode >= 0) { + _iocs_crtmod(gPrevCrtMode); + } + gModeSet = false; + if (gPrevSsp >= 0) { + (void)_dos_super(gPrevSsp); // back to user mode for Human68k + gPrevSsp = -1; + } +} + + +// ----- Present -------------------------------------------------------------- + +// Copy the surface's four planes into the TEXT PLANE at $E00000. +// +// This is the payoff of native planar storage. The surface rows are already in +// display format -- 40 bytes per row per plane -- so present is a straight copy +// with a stride change (40 -> 128), not a per-pixel expansion. Traffic per full +// frame is 4 planes x 200 rows x 40 bytes = 32,000 bytes, against the 128,000 +// bytes of GVRAM word writes the chunky path moved. Bus traffic is what the +// measured wait states punish, so that 4x cut is the win. +// +// Dirty rows only, and only the marked band within a row. +void jlpPresent(const jlSurfaceT *src) { + X68kPlanarT *pd; + uint16_t y; + uint16_t plane; + uint16_t firstByte; + uint16_t rowBytes; + const uint8_t *s; + uint8_t *d; + + if (src == NULL) { + return; + } + vdispPoll(); // keep the frame counter honest: see the note on vdispPoll + // Push the palette when it has changed. Without this NOTHING ever uploads + // colours and the display keeps whatever Human68k left in the text palette. + // The text plane has ONE 16-entry palette, so SCB per-scanline palette + // selection cannot be honoured -- palette 0 is used, matching the Atari ST. + if (gStagePaletteDirty) { + uint16_t c; + for (c = 0u; c < 16u; c++) { + X68K_TEXT_PALETTE[c] = x68kColorFromRgb12(src->palette[0][c]); + } + gStagePaletteDirty = false; + } + pd = x68kSurfacePlanar(src); + if (pd == NULL) { + return; + } + for (y = 0; y < SURFACE_HEIGHT; y++) { + if (!STAGE_DIRTY_ROW_TOUCHED(y) || STAGE_DIRTY_ROW_CLEAN(y)) { + continue; + } + // Bands are 16-bit stage words = 4 pixels = 2 bytes of a 1bpp plane. + firstByte = (uint16_t)(gStageMinWord[y] >> 1); + rowBytes = (uint16_t)((gStageMaxWord[y] >> 1) - firstByte + 1u); + if ((uint16_t)(firstByte + rowBytes) > X68K_BYTES_PER_ROW) { + rowBytes = (uint16_t)(X68K_BYTES_PER_ROW - firstByte); + } + for (plane = 0u; plane < X68K_BITPLANES; plane++) { + s = pd->planes[plane] + ((uint32_t)y * X68K_BYTES_PER_ROW) + firstByte; + // Centred: shift down X68K_ORIGIN_Y rows and right X68K_ORIGIN_BYTE + // bytes. X is byte-aligned (96/8 = 12) so no bit shifting is needed. + d = (uint8_t *)(X68K_TVRAM_PLANE0 + ((uint32_t)plane * X68K_TVRAM_PLANE_STEP) + + x68kTvramOffset(0u, (uint16_t)(y + X68K_ORIGIN_Y)) + + X68K_ORIGIN_BYTE + firstByte); + memcpy(d, s, rowBytes); + } + } +} + + +// ----- Input ---------------------------------------------------------------- + +// _iocs_bitsns hands back a raw per-group key-DOWN bitmap, so there is no ISR +// to install, no vector to take over and no packet state machine -- unlike the +// ST, which has to replace the TOS ikbdsys vector and decode IKBD packets. +void jlpInputInit(void) { +} + + +void jlpInputShutdown(void) { +} + + +// jlKeyE -> X68000 scancode. Values taken from XEiJ's keyboard table +// (Keyboard.java), which is the authoritative mapping, not guessed. +// 0 means "this key has no equivalent on an X68000 keyboard". +// +// Notes on the keys that do not map one-to-one: +// - The X68000 has a SINGLE shift key (0x70), so KEY_LSHIFT and KEY_RSHIFT +// both read it. A game testing either sees the same physical key. +// - There is no ALT; OPT.1 (0x72) is the closest equivalent and is what +// KEY_LALT reads. OPT.2 (0x73) is left unmapped. +static const uint8_t kScanForKey[KEY_COUNT] = { + [KEY_NONE] = 0x00, + [KEY_A] = 0x1e, [KEY_B] = 0x2e, [KEY_C] = 0x2c, [KEY_D] = 0x20, + [KEY_E] = 0x13, [KEY_F] = 0x21, [KEY_G] = 0x22, [KEY_H] = 0x23, + [KEY_I] = 0x18, [KEY_J] = 0x24, [KEY_K] = 0x25, [KEY_L] = 0x26, + [KEY_M] = 0x30, [KEY_N] = 0x2f, [KEY_O] = 0x19, [KEY_P] = 0x1a, + [KEY_Q] = 0x11, [KEY_R] = 0x14, [KEY_S] = 0x1f, [KEY_T] = 0x15, + [KEY_U] = 0x17, [KEY_V] = 0x2d, [KEY_W] = 0x12, [KEY_X] = 0x2b, + [KEY_Y] = 0x16, [KEY_Z] = 0x2a, + [KEY_0] = 0x0b, [KEY_1] = 0x02, [KEY_2] = 0x03, [KEY_3] = 0x04, + [KEY_4] = 0x05, [KEY_5] = 0x06, [KEY_6] = 0x07, [KEY_7] = 0x08, + [KEY_8] = 0x09, [KEY_9] = 0x0a, + [KEY_SPACE] = 0x35, + [KEY_ESCAPE] = 0x01, + [KEY_RETURN] = 0x1d, + [KEY_TAB] = 0x10, + [KEY_BACKSPACE] = 0x0f, + [KEY_UP] = 0x3c, [KEY_DOWN] = 0x3e, + [KEY_LEFT] = 0x3b, [KEY_RIGHT] = 0x3d, + [KEY_LSHIFT] = 0x70, [KEY_RSHIFT] = 0x70, // one physical SHIFT + [KEY_LCTRL] = 0x71, + [KEY_LALT] = 0x72, // OPT.1 + [KEY_F1] = 0x63, [KEY_F2] = 0x64, [KEY_F3] = 0x65, [KEY_F4] = 0x66, + [KEY_F5] = 0x67, [KEY_F6] = 0x68, [KEY_F7] = 0x69, [KEY_F8] = 0x6a, + [KEY_F9] = 0x6b, [KEY_F10] = 0x6c, +}; + + +// _iocs_bitsns(group) returns a bitmap of the eight keys in that group, bit n +// set meaning "down": group = scancode >> 3, bit = scancode & 7. Every group +// is fetched once per poll rather than per key, so the whole keyboard costs +// X68K_KEYGROUP_COUNT IOCS calls regardless of how many keys are tested. +// +// No ISR, no vector takeover, no packet decoding -- unlike the ST, which has +// to replace the TOS ikbdsys vector and run an IKBD packet state machine. +void jlpInputPoll(void) { + uint8_t groups[X68K_KEYGROUP_COUNT]; + uint16_t group; + uint16_t key; + uint8_t scan; + + for (group = 0; group < X68K_KEYGROUP_COUNT; group++) { + groups[group] = (uint8_t)(_iocs_bitsns((int)group) & 0xFF); + } + for (key = 1; key < KEY_COUNT; key++) { + scan = kScanForKey[key]; + if (scan == 0u) { + continue; + } + group = (uint16_t)(scan >> 3); + if (group < X68K_KEYGROUP_COUNT && + (groups[group] & (uint8_t)(1u << (scan & 7u))) != 0u) { + gKeyState[key] = 1u; + } + } +} + + +void jlpJoystickReset(jlJoystickE js) { + (void)js; + // Digital sticks: nothing to calibrate. +} + + +// ----- Timing --------------------------------------------------------------- + +// There is no plain "wait for vblank" IOCS call -- _iocs_vdispst INSTALLS a +// vertical-display handler rather than blocking -- so poll the MFP's V-DISP +// input directly and edge-detect it. Both spins are bounded so a wedged or +// mis-programmed CRTC degrades to a dropped frame instead of hanging the game. +// +// ASSUMES SUPERVISOR MODE for the $E88001 read. Human68k normally leaves user +// programs in supervisor, unlike TOS on the ST (which needs Super(0L) -- see +// the ST HAL). If that turns out not to hold, this becomes an _iocs_vdispst +// handler bumping gFrameCount instead. +// Vertical-display interrupt handler: the AUTHORITATIVE frame counter. +// +// A polled edge-detector loses ticks, and it loses them exactly when it hurts: +// any frame doing real work between jlFrameCount() calls misses the V-DISP +// transitions that happened meanwhile. Measured: 400 audio refills across ~19 +// frames reported ZERO elapsed frames, because nothing polled in between. +// Animation and music tempo would silently run slow under load. +// +// Registered with _iocs_vdispst, which calls this once per vertical display +// period. Plain C (RTS) is the IOCS convention for a _VDISPST handler; IOCS +// owns the interrupt frame and the RTE. +static void vdispHandler(void) { + gFrameCount++; +} +// Referenced only to keep the handler compiled and honest about its intent +// until the asm thunk exists; see the note in jlpInit. +void *const gX68kVdispHandlerRef = (void *)vdispHandler; + + +// V-DISP edge detect. This is the PRIMARY frame clock (the interrupt route +// hangs -- see jlpInit), so it is called from every per-frame entry point the +// library owns -- jlpPresent, jlpInputPoll, jlpFrameCount, jlpWaitVBL -- not +// just when the app asks the time. A game that presents or polls input once a +// frame therefore keeps an accurate count even while doing heavy work. +// +// The residual limitation is real and worth knowing: a frame that does NONE of +// those for longer than one V-DISP period still loses ticks. An asm thunk for +// _iocs_vdispst removes it for good. +// +// CRITICAL CONTRACT: jlpFrameCount must be monotonic ON ITS OWN, without +// jlpWaitVBL being called. UBER's timing model (and any game that paces by +// polling rather than blocking) sits in a tight loop reading jlFrameCount and +// never calls jlWaitVBL -- so a counter bumped only inside the wait never +// advances and the caller spins forever. That is exactly what happened here: +// UBER ran for 38,000 frames with a live, moving PC and a frozen screen, +// because it was stuck on its first timed op waiting for a tick that could +// not arrive. +static void vdispPoll(void) { + uint8_t now; + + if (gVdispInstalled) { + return; // the interrupt owns the counter + } + now = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u); + if (now != gLastVdisp) { + gLastVdisp = now; + if (now != 0u) { + gFrameCount++; + } + } +} + + +void jlpWaitVBL(void) { + uint16_t start; + uint32_t guard; + + // Block until the counter actually moves, so the wait and the counter can + // never disagree. Bounded, so a wedged CRTC drops a frame rather than + // hanging the game. + start = gFrameCount; + guard = 0ul; + while (gFrameCount == start && guard < X68K_VBL_SPIN_LIMIT) { + vdispPoll(); + guard++; + } +} + + +uint16_t jlpFrameCount(void) { + vdispPoll(); + return gFrameCount; +} + + +#ifdef JOEY_LOG_SERIAL_TEE +// Mirror every log line out RS-232C. Polled and blocking: a diagnostic build +// trades speed for never dropping the line that explains the failure. The +// transmit-ready spin is bounded so an unwired or unopened port degrades to +// slow-but-running instead of wedging the machine being diagnosed. +void jlpLogTee(const char *text) { + uint32_t guard; + + while (*text != '\0') { + guard = 0ul; + while (_iocs_osns232c() == 0 && guard < X68K_TEE_SPIN_LIMIT) { + guard++; + } + if (guard >= X68K_TEE_SPIN_LIMIT) { + return; // port not draining -- give up on this line + } + if (*text == '\n') { + _iocs_out232c((int)'\r'); + } + _iocs_out232c((int)(unsigned char)*text); + text++; + } +} +#endif + + +uint16_t jlpFrameHz(void) { + // 31 kHz modes on this machine run at ~55.5 Hz rather than 60. Reported + // only -- the generic millisElapsed divides by it. + return 55u; +} diff --git a/src/x68000/halPlanar.c b/src/x68000/halPlanar.c new file mode 100644 index 0000000..0a1fb57 --- /dev/null +++ b/src/x68000/halPlanar.c @@ -0,0 +1,1346 @@ +// Sharp X68000 planar graphics HAL. +// +// Adapted from src/amiga/hal.c: a 320px 4bpp surface is 40 bytes per row per +// plane on any planar machine, so the surface layout and every primitive that +// walks it are shared logic. What changed: +// - AmigaPlanarT -> X68kPlanarT (same fields; see x68kPlanar.h) +// - Chip RAM AllocMem -> calloc; nothing but the CPU reads these planes +// - the four blitter helpers -> CPU equivalents below (no blitter here, and +// unlike the ST there is no STE DMA either -- it is pure 68000) +// The display side (scattering to the text plane at stride 128) lives in +// hal.c's jlpPresent, not here. + +#include +#include + +#include "port.h" +#include "surfaceInternal.h" +#include "joey/debug.h" +#include "spriteInternal.h" +#include "x68kPlanar.h" + +// Blitter first/last-word mask "all bits" -- the CPU fill takes the same value. +#define X68K_MASK_ALL 0xFFFFu + +// The Amiga picks between blitter and CPU walk at 16 per-plane words because +// the blitter's fixed setup loses below that. Here BOTH paths are the CPU, and +// the memcpy path wins at essentially any size (it is one call per row instead +// of a per-word store loop), so the threshold is 0: always take it. +#define X68K_COPYRECT_MEMCPY_MIN_WORDS 0u + + + +// ----- C stand-ins for the shared 68k asm ----- +// +// src/m68k/surface68k.s and src/amiga/circle.s export these, but their symbols +// carry the m68k-amigaos/mint leading underscore (_surface68kFillSpan4Planes) +// and elf2x68k is ELF, which has no such prefix. Rather than fork the asm for +// an ABI difference, the port uses C for now; adopting the asm later is a +// symbol-aliasing change, not a rewrite. These are the obvious optimisation +// targets once correctness is banked. + +// Fill one horizontal span across all four planes: a partial left byte, a run +// of whole middle bytes, then a partial right byte. fbN is the fill byte for +// plane N (0x00 or 0xFF), so a colour is applied by writing each plane's bit. +static void surface68kFillSpan4Planes(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3, + uint16_t numMid, uint8_t leftMask, uint8_t rightMask, + uint8_t fb0, uint8_t fb1, uint8_t fb2, uint8_t fb3) { + uint8_t *pl[X68K_BITPLANES]; + uint8_t fb[X68K_BITPLANES]; + uint16_t i; + uint16_t m; + + pl[0] = p0; pl[1] = p1; pl[2] = p2; pl[3] = p3; + fb[0] = fb0; fb[1] = fb1; fb[2] = fb2; fb[3] = fb3; + for (i = 0u; i < X68K_BITPLANES; i++) { + uint8_t *d = pl[i]; + if (leftMask != 0u) { + *d = (uint8_t)((*d & (uint8_t)~leftMask) | (fb[i] & leftMask)); + } + d++; + for (m = 0u; m < numMid; m++) { + *d++ = fb[i]; + } + if (rightMask != 0u) { + *d = (uint8_t)((*d & (uint8_t)~rightMask) | (fb[i] & rightMask)); + } + } +} + + +// Bresenham midpoint circle outline, plotted straight into the planes. Caller +// has verified the whole bbox is on-surface, so there is no per-pixel clip. +static void x68kPlotPlanes(uint8_t *pl[X68K_BITPLANES], uint16_t x, uint16_t y, uint8_t color) { + uint32_t off; + uint8_t bit; + uint16_t i; + + off = ((uint32_t)y * X68K_BYTES_PER_ROW) + (uint32_t)(x >> 3); + bit = (uint8_t)(0x80u >> (x & 7u)); + for (i = 0u; i < X68K_BITPLANES; i++) { + if ((color >> i) & 1u) { + pl[i][off] |= bit; + } else { + pl[i][off] = (uint8_t)(pl[i][off] & (uint8_t)~bit); + } + } +} + + +static void surface68kAmigaCircleOutline(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3, + uint16_t cx, uint16_t cy, uint16_t r, uint8_t color) { + uint8_t *pl[X68K_BITPLANES]; + int16_t x; + int16_t y; + int16_t d; + + pl[0] = p0; pl[1] = p1; pl[2] = p2; pl[3] = p3; + x = 0; + y = (int16_t)r; + d = (int16_t)(1 - (int16_t)r); + while (x <= y) { + x68kPlotPlanes(pl, (uint16_t)(cx + x), (uint16_t)(cy + y), color); + x68kPlotPlanes(pl, (uint16_t)(cx - x), (uint16_t)(cy + y), color); + x68kPlotPlanes(pl, (uint16_t)(cx + x), (uint16_t)(cy - y), color); + x68kPlotPlanes(pl, (uint16_t)(cx - x), (uint16_t)(cy - y), color); + x68kPlotPlanes(pl, (uint16_t)(cx + y), (uint16_t)(cy + x), color); + x68kPlotPlanes(pl, (uint16_t)(cx - y), (uint16_t)(cy + x), color); + x68kPlotPlanes(pl, (uint16_t)(cx + y), (uint16_t)(cy - x), color); + x68kPlotPlanes(pl, (uint16_t)(cx - y), (uint16_t)(cy - x), color); + // The tie d == 0 must take the NO-SHRINK branch. src/core/draw.c:428 + // uses `err <= 0` and src/amiga/circle.s:149 uses tst.w/bgt (shrink + // only when err > 0); `d < 0` shrinks the major axis on the tie and + // plots a different pixel set for 114 of the 199 reachable radii. + if (d <= 0) { + d = (int16_t)(d + (2 * x) + 3); + } else { + d = (int16_t)(d + (2 * (x - y)) + 5); + y--; + } + x++; + } +} + + +// ----- CPU stand-ins for the Amiga blitter helpers ----- +// +// Signatures match the originals exactly so the adapted call sites are +// untouched. widthWords counts 16-bit words; modBytes is added to the pointer +// after each row (0 for a full-stride operation). + +// DEST-only block fill: `height` rows of `widthWords` words set or cleared. +static void x68kCpuFillOne(uint8_t *dst, uint16_t widthWords, uint16_t height, + uint16_t dstModBytes, bool setBits) { + uint16_t row; + uint16_t w; + uint16_t *p; + uint16_t val; + + val = setBits ? 0xFFFFu : 0x0000u; + for (row = 0u; row < height; row++) { + p = (uint16_t *)dst; + for (w = 0u; w < widthWords; w++) { + *p++ = val; + } + dst += (widthWords * 2u) + dstModBytes; + } +} + + +// Masked fill: as above, but the first and last words are merged through +// `fwm`/`lwm` so a rect can start and end mid-word. The Amiga does this with +// the blitter's first/last-word masks; here it is an explicit read-modify-write +// on just those two words, with the interior written whole. +static void x68kCpuMaskedFillOne(uint8_t *dst, uint16_t widthWords, uint16_t height, + uint16_t dstModBytes, uint16_t fwm, uint16_t lwm, + bool setBits) { + uint16_t row; + uint16_t w; + uint16_t *p; + uint16_t val; + uint16_t mask; + + val = setBits ? 0xFFFFu : 0x0000u; + for (row = 0u; row < height; row++) { + p = (uint16_t *)dst; + for (w = 0u; w < widthWords; w++) { + mask = 0xFFFFu; + if (w == 0u) { + mask &= fwm; + } + if (w == (uint16_t)(widthWords - 1u)) { + mask &= lwm; + } + if (mask == 0xFFFFu) { + *p = val; + } else { + *p = (uint16_t)((*p & (uint16_t)~mask) | (val & mask)); + } + p++; + } + dst += (widthWords * 2u) + dstModBytes; + } +} + + +// Copy the same rect out of all four planes. +static void x68kCpuCopyPlanes(uint8_t *dst[X68K_BITPLANES], uint8_t *src[X68K_BITPLANES], + uint16_t offsetBytes, uint16_t widthWords, uint16_t height, + uint16_t modBytes) { + uint16_t i; + uint16_t row; + uint16_t rowBytes; + uint8_t *d; + uint8_t *s; + + rowBytes = (uint16_t)(widthWords * 2u); + for (i = 0u; i < X68K_BITPLANES; i++) { + d = dst[i] + offsetBytes; + s = src[i] + offsetBytes; + for (row = 0u; row < height; row++) { + memcpy(d, s, rowBytes); + d += rowBytes + modBytes; + s += rowBytes + modBytes; + } + } +} + + +void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled) { + X68kPlanarT *pd; + uint8_t colorIndex; + uint16_t i; + bool setBits; + + if (s->portData == NULL) { + jlpGenericSurfaceClear(s, doubled); + return; + } + colorIndex = (uint8_t)(doubled & 0x0Fu); + pd = (X68kPlanarT *)s->portData; + + // Blitter DEST-only block fill of every whole plane to $00 (clear) + // or $FF (set). Each plane is 200 rows x 20 words, contiguous, so + // BLTSIZE = (200 << 6) | 20 and BLTDMOD = 0. The planes are now + // CHIP RAM (see jlpSurfaceAllocPortData) so the blitter can reach + // them. Own the blitter once and fill all four planes; the final + // CPU fill: no blitter to own or wait on, so the planes are complete + // written before any CPU read (a subsequent draw or the present + // copy) touches them. + + for (i = 0; i < X68K_BITPLANES; i++) { + setBits = ((colorIndex >> i) & 1u) != 0u; + x68kCpuFillOne(pd->planes[i], (X68K_BYTES_PER_ROW / 2u), SURFACE_HEIGHT, 0u, setBits); + } + +} + + +// Planar jlFillRect: fills the four off-screen shadow plane buffers via +// the blitter. Caller (cross-platform jlFillRect) has already clipped +// the rect to the surface, so x/y/w/h are on-surface. The shadow planes +// are off-screen, so this is invisible until jlStagePresent. +// +// Layout reminder (see docs/amiga_planar.md): each plane word covers 16 +// horizontal pixels; bit 15 = leftmost pixel of that word. The blitter +// fills whole words and cuts the partial-pixel left/right edges with its +// hardware first/last-word masks (fwm/lwm), so a single DEST(+C) blit per +// plane handles any width -- no per-pixel-edge CPU read-modify-write. +// For each plane the bit value at every rect pixel is constant: +// (colorIndex >> plane) & 1; set bits OR the mask into the plane, clear +// bits AND its complement (see the two masked-fill minterms). +void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) { + X68kPlanarT *pd; + uint16_t wordFirst; + uint16_t wordLast; + uint16_t widthWords; + uint16_t fwm; + uint16_t lwm; + uint16_t dstMod; + uint16_t plane; + uint8_t bitVal; + uint8_t *dst; + + // Callers guarantee s != NULL and positive w/h: the jlFillRect + // wrapper validates + clips before fillRectOnSurface, and the + // fill-circle span loop emits spanWidth = 2x+1 >= 1 with h = 1. + pd = (X68kPlanarT *)s->portData; + if (pd == NULL) { + jlpGenericFillRect(s, x, y, w, h, colorIndex); + return; + } + + // Word-granular rect geometry. wordFirst/wordLast index the 16-pixel + // plane words the rect touches; fwm/lwm are the blitter first/last- + // word masks that cut the partial-pixel edges (pixel 0 is bit 15 of + // its word, so the left mask drops the high bits of the first word + // and the right mask drops the low bits of the last). One blit per + // plane then fills any width, so there is no byte-aligned or + // per-pixel-edge special case to follow. + wordFirst = (uint16_t)((uint16_t)x >> 4); + wordLast = (uint16_t)(((uint16_t)x + w - 1u) >> 4); + widthWords = (uint16_t)(wordLast - wordFirst + 1u); + fwm = (uint16_t)(X68K_MASK_ALL >> ((uint16_t)x & 15u)); + lwm = (uint16_t)(X68K_MASK_ALL << (15u - (((uint16_t)x + w - 1u) & 15u))); + dstMod = (uint16_t)(X68K_BYTES_PER_ROW - (widthWords << 1)); + + /* Full-width fast path: the rect spans every plane word with both + * edge masks all-ones, so each plane row is a whole 20-word store + * with no destination read. jlFillRect 320x200 (full-screen wipes) + * is the dominant case and lands here. DEST-only block fill -- + * (h << 6) | 20 words, BLTDMOD = 0 because consecutive full-width + * rows are contiguous; set bits write $FFFF (minterm $FF), clear + * bits $0000 (minterm $00). One OwnBlitter for all four planes; + * CPU fill completes synchronously, so the planes are ready before + * the present copy or any later CPU draw reads them. */ + if (wordFirst == 0u && wordLast == (uint16_t)((X68K_BYTES_PER_ROW / 2u) - 1u) + && fwm == X68K_MASK_ALL && lwm == X68K_MASK_ALL) { + uint8_t *rowBase; + + for (plane = 0; plane < X68K_BITPLANES; plane++) { + bitVal = (uint8_t)((colorIndex >> plane) & 1u); + rowBase = pd->planes[plane] + (uint16_t)y * X68K_BYTES_PER_ROW; + x68kCpuFillOne(rowBase, (X68K_BYTES_PER_ROW / 2u), (uint16_t)h, 0u, bitVal != 0u); + } + + return; + } + + /* General masked fill: A is the constant $FFFF cut to the rect by + * the fwm/lwm word masks, C reads and D writes the destination so + * bits outside the mask survive. One blit per plane covers any + * width, partial pixel edges included -- the blitter does the edge + * masking in hardware, replacing the old per-pixel-edge CPU walkers. + * UBER jlFillRect 16x16 / 80x80 lands here. (jlFillCircle spans + * left this path in hunt-2 rank 14: the exact-disk walk feeds + * x68kFillSpanInline directly, CPU-side.) widthWords == 1 needs no + * special case: the blitter ANDs both edge masks onto the single + * word. */ + + for (plane = 0; plane < X68K_BITPLANES; plane++) { + bitVal = (uint8_t)((colorIndex >> plane) & 1u); + dst = pd->planes[plane] + + (uint16_t)y * X68K_BYTES_PER_ROW + + (uint16_t)(wordFirst << 1); + x68kCpuMaskedFillOne(dst, widthWords, (uint16_t)h, dstMod, fwm, lwm, bitVal != 0u); + } + +} + + +// Phase 9 sprite save/restore plane-backup hooks. +// Sprite save at (x, y, w, h) records the destination region into the +// caller's backup buffer; restore writes it back. x and w are 2-pixel +// aligned by cross-platform code. +// +// Backup layout matches the CHUNKY-packed format the cross-platform +// contract sizes for ((widthTiles*4)+1)*(heightTiles*8) bytes (see +// include/joey/sprite.h): one byte per pixel-pair, 2 pixels/byte, +// high nibble = even (left) pixel, low nibble = odd (right) pixel, +// (w/2) bytes per row, (w/2)*h bytes total. This is the same format +// the Atari ST port uses and never overruns the contract buffer -- +// unlike a plane-major layout, which would round x down to an 8-pixel +// boundary and width up and then write (4*roundedW/8)*h = up to +// (w/2 + 3)*h bytes for an odd-of-8 x, overrunning the buffer. Each +// pixel's 4bpp color is assembled from / written to the 4 plane bits +// at byte x>>3, bit 0x80 >> (x & 7). + +void jlpSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) { + const X68kPlanarT *pd; + uint16_t pairs; + uint16_t row; + uint16_t pair; + uint8_t *dst; + + pd = (const X68kPlanarT *)s->portData; + if (pd == NULL || dstPlaneBytes == NULL || w == 0u || h == 0u) { + return; + } + + /* Byte-aligned fast path: raw plane-byte copy, no per-pixel c2p. + * When x and w are both 8-px aligned and the rect is fully on + * surface, each sprite tile column maps to whole plane bytes, so we + * save the raw bytes -- ~8x cheaper than the chunky walker below and + * an exact round-trip on restore (the sprite fills its bytes, so + * restoring whole bytes touches no neighbor pixels). The backup row + * holds plane0..plane3, each `wb` bytes, contiguous. The chunky + * walker below stays as the arbitrary-x fallback. */ + if ((x & 7) == 0 && (w & 7) == 0 + && x >= 0 && (x + (int16_t)w) <= SURFACE_WIDTH + && y >= 0 && (y + (int16_t)h) <= SURFACE_HEIGHT) { + uint16_t wb; + uint16_t wb2; + uint16_t wb3; + uint16_t startByte; + uint16_t rowOff; + uint8_t *bdst; + uint16_t col; + + wb = (uint16_t)(w >> 3); + wb2 = (uint16_t)(wb << 1); + wb3 = (uint16_t)(wb2 + wb); + startByte = (uint16_t)((uint16_t)x >> 3); + rowOff = (uint16_t)((uint16_t)y * X68K_BYTES_PER_ROW + startByte); + bdst = dstPlaneBytes; + for (row = 0; row < (uint16_t)h; row++) { + const uint8_t *s0 = pd->planes[0] + rowOff; + const uint8_t *s1 = pd->planes[1] + rowOff; + const uint8_t *s2 = pd->planes[2] + rowOff; + const uint8_t *s3 = pd->planes[3] + rowOff; + for (col = 0; col < wb; col++) { + bdst[col] = s0[col]; + bdst[wb + col] = s1[col]; + bdst[wb2 + col] = s2[col]; + bdst[wb3 + col] = s3[col]; + } + bdst += (uint16_t)(wb << 2); + rowOff += X68K_BYTES_PER_ROW; + } + return; + } + + pairs = (uint16_t)(w >> 1); + for (row = 0; row < (uint16_t)h; row++) { + const uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * X68K_BYTES_PER_ROW; + const uint8_t *p1 = pd->planes[1] + ((uint16_t)y + row) * X68K_BYTES_PER_ROW; + const uint8_t *p2 = pd->planes[2] + ((uint16_t)y + row) * X68K_BYTES_PER_ROW; + const uint8_t *p3 = pd->planes[3] + ((uint16_t)y + row) * X68K_BYTES_PER_ROW; + dst = dstPlaneBytes + (uint16_t)(row * pairs); + for (pair = 0; pair < pairs; pair++) { + uint16_t px = (uint16_t)(x + (int16_t)(pair << 1)); + uint16_t byteOff = (uint16_t)(px >> 3); + uint8_t maskHi = (uint8_t)(0x80u >> (px & 7u)); + uint8_t maskLo = (uint8_t)(0x80u >> ((px + 1u) & 7u)); + uint16_t byteOffLo = (uint16_t)((px + 1u) >> 3); + uint8_t hi = 0u; + uint8_t lo = 0u; + if (p0[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x01u); } + if (p1[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x02u); } + if (p2[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x04u); } + if (p3[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x08u); } + if (p0[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x01u); } + if (p1[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x02u); } + if (p2[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x04u); } + if (p3[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x08u); } + dst[pair] = (uint8_t)((hi << 4) | lo); + } + } +} + + +void jlpSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) { + X68kPlanarT *pd; + uint16_t pairs; + uint16_t row; + uint16_t pair; + const uint8_t *src; + + pd = (X68kPlanarT *)s->portData; + if (pd == NULL || srcPlaneBytes == NULL || w == 0u || h == 0u) { + return; + } + + /* Byte-aligned fast path mirroring jlpSpriteSavePlanes: writes the + * raw plane bytes straight back (whole-byte round-trip), skipping + * the per-pixel chunky->planar masking of the walker below. */ + if ((x & 7) == 0 && (w & 7) == 0 + && x >= 0 && (x + (int16_t)w) <= SURFACE_WIDTH + && y >= 0 && (y + (int16_t)h) <= SURFACE_HEIGHT) { + uint16_t wb; + uint16_t wb2; + uint16_t wb3; + uint16_t startByte; + uint16_t rowOff; + const uint8_t *bsrc; + uint16_t col; + + wb = (uint16_t)(w >> 3); + wb2 = (uint16_t)(wb << 1); + wb3 = (uint16_t)(wb2 + wb); + startByte = (uint16_t)((uint16_t)x >> 3); + rowOff = (uint16_t)((uint16_t)y * X68K_BYTES_PER_ROW + startByte); + bsrc = srcPlaneBytes; + for (row = 0; row < (uint16_t)h; row++) { + uint8_t *d0 = pd->planes[0] + rowOff; + uint8_t *d1 = pd->planes[1] + rowOff; + uint8_t *d2 = pd->planes[2] + rowOff; + uint8_t *d3 = pd->planes[3] + rowOff; + for (col = 0; col < wb; col++) { + d0[col] = bsrc[col]; + d1[col] = bsrc[wb + col]; + d2[col] = bsrc[wb2 + col]; + d3[col] = bsrc[wb3 + col]; + } + bsrc += (uint16_t)(wb << 2); + rowOff += X68K_BYTES_PER_ROW; + } + return; + } + + pairs = (uint16_t)(w >> 1); + for (row = 0; row < (uint16_t)h; row++) { + uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * X68K_BYTES_PER_ROW; + uint8_t *p1 = pd->planes[1] + ((uint16_t)y + row) * X68K_BYTES_PER_ROW; + uint8_t *p2 = pd->planes[2] + ((uint16_t)y + row) * X68K_BYTES_PER_ROW; + uint8_t *p3 = pd->planes[3] + ((uint16_t)y + row) * X68K_BYTES_PER_ROW; + src = srcPlaneBytes + (uint16_t)(row * pairs); + for (pair = 0; pair < pairs; pair++) { + uint8_t b = src[pair]; + uint8_t hi = (uint8_t)(b >> 4); + uint8_t lo = (uint8_t)(b & 0x0Fu); + uint16_t px = (uint16_t)(x + (int16_t)(pair << 1)); + uint16_t byteOff = (uint16_t)(px >> 3); + uint8_t maskHi = (uint8_t)(0x80u >> (px & 7u)); + uint8_t notHi = (uint8_t)(~maskHi); + uint16_t byteOffLo = (uint16_t)((px + 1u) >> 3); + uint8_t maskLo = (uint8_t)(0x80u >> ((px + 1u) & 7u)); + uint8_t notLo = (uint8_t)(~maskLo); + p0[byteOff] = (uint8_t)((p0[byteOff] & notHi) | ((hi & 0x01u) ? maskHi : 0u)); + p1[byteOff] = (uint8_t)((p1[byteOff] & notHi) | ((hi & 0x02u) ? maskHi : 0u)); + p2[byteOff] = (uint8_t)((p2[byteOff] & notHi) | ((hi & 0x04u) ? maskHi : 0u)); + p3[byteOff] = (uint8_t)((p3[byteOff] & notHi) | ((hi & 0x08u) ? maskHi : 0u)); + p0[byteOffLo] = (uint8_t)((p0[byteOffLo] & notLo) | ((lo & 0x01u) ? maskLo : 0u)); + p1[byteOffLo] = (uint8_t)((p1[byteOffLo] & notLo) | ((lo & 0x02u) ? maskLo : 0u)); + p2[byteOffLo] = (uint8_t)((p2[byteOffLo] & notLo) | ((lo & 0x04u) ? maskLo : 0u)); + p3[byteOffLo] = (uint8_t)((p3[byteOffLo] & notLo) | ((lo & 0x08u) ? maskLo : 0u)); + } + } +} + + +// Phase 6 planar dual-write for sprite draw. Walks the sprite's +// chunky tile data with the same clipping the cross-platform code +// applies, calling x68kPlanarSetPixel for every non-transparent +// pixel (nibble != 0). Bypasses the compiled fast path entirely -- +// the chunky compiled draw already ran by the time we get here, so +// we just mirror its pixel set into the planes. +// +// Sprite tile data layout: tileData = wTiles * hTiles tiles, each +// tile = 8 rows x 4 chunky bytes. Tiles laid out row-major. +// For pixel (px, py) within the sprite: +// tileX = px / 8, tileY = py / 8 +// inTileX = px % 8, inTileY = py % 8 +// tileBase = tileData + (tileY * wTiles + tileX) * 32 +// byte = tileBase[inTileY * 4 + inTileX/2] +// nibble = (inTileX & 1) ? byte & 0x0F : byte >> 4 +// +// Save/restore have no equivalent planar dual-write yet (the +// jlSpriteBackupT only has chunky storage); workable approach needs +// a parallel plane-data buffer. Apps that depend on PLANAR_PRESENT +// save/restore semantics will see stale planes after restore. +/* Byte-wide sprite walker (replaces the per-pixel x68kPlanarSetPixel + * loop that was burning ~30+ cycles per pixel). For each 8-pixel + * column chunk of the sprite this routine: + * 1. reads 4 chunky bytes (one row of one 8-pixel tile column) + * 2. converts to 4 plane bytes + a shape mask (1 bit per pixel + * where the chunky nibble is non-transparent) + * 3. shifts the per-plane bytes by (x & 7) so the sprite can land + * at any pixel x, not just 8-px boundaries; the shift is derived + * from the ORIGINAL signed x so a left-edge clip keeps the true + * source-to-dest sub-byte phase + * 3a. each plane-byte write is guarded by a signed [0,40) dest-byte + * range check, which handles left, right, top and bottom clip + * 4. writes the left byte (and the spilled right byte for shifted + * sprites) via AND-mask + OR to all 4 plane buffers + * + * This is the "shared walker" referenced in the codegen-rewrite plan: + * one static routine in lib code handles every sprite cel, so the + * codegen arena no longer needs the ~17-23 KB of unrolled per-shift + * code per cel that the old emitter generated. */ +void jlpSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y) { + X68kPlanarT *pd; + int16_t dy; + int16_t sy; + int16_t w; + int16_t h; + int16_t row; + int16_t byteCol; + int16_t spritePy; + int16_t tileY; + int16_t inTileY; + int16_t startByteX; + int16_t dstByteIdx; + uint16_t wTiles; + uint8_t shift; + uint8_t bit; + uint8_t p0, p1, p2, p3, mask; + uint8_t leftMask, notMask; + uint8_t rshift, rightMask; + uint16_t dstRowBase; + const uint8_t *tile; + const uint8_t *chunky; + + pd = (X68kPlanarT *)s->portData; + if (pd == NULL) { + return; + } + + wTiles = sp->widthTiles; + w = (int16_t)(wTiles * 8u); + h = (int16_t)(sp->heightTiles * 8u); + dy = y; + sy = 0; + + /* Vertical clip only (sy tracks offset INTO sprite); horizontal + * clip is handled per-byte by the dstByteIdx range guards below so + * the source sub-byte phase is preserved on a left-edge clip. */ + if (dy < 0) { sy = (int16_t)(-dy); h = (int16_t)(h - sy); dy = 0; } + if (x >= SURFACE_WIDTH || dy >= SURFACE_HEIGHT || (int16_t)(x + w) <= 0 || h <= 0) { + return; + } + if (dy + h > SURFACE_HEIGHT) { h = (int16_t)(SURFACE_HEIGHT - dy); } + + /* Derive shift / startByteX from the ORIGINAL signed dest x so a + * partial left clip keeps the true source-to-dest sub-byte phase. + * shift in 0..7 (floored modulo), startByteX = floor(x / 8). For + * x = -3: shift = 5, startByteX = -1 -- source column 0 lands + * shifted by 5 into dest bytes -1 (off-surface, skipped) and 0. */ + shift = (uint8_t)(((x % 8) + 8) % 8); + startByteX = (int16_t)((x - (int16_t)shift) / 8); + + for (row = 0; row < h; row++) { + spritePy = (int16_t)(sy + row); + tileY = (int16_t)(spritePy >> 3); + inTileY = (int16_t)(spritePy & 7); + dstRowBase = (uint16_t)((uint16_t)(dy + row) * X68K_BYTES_PER_ROW); + + for (byteCol = 0; byteCol < (int16_t)wTiles; byteCol++) { + tile = sp->tileData + (uint32_t)((tileY * wTiles + byteCol) * 32u); + chunky = tile + inTileY * 4u; + + /* c2p: 8 pixels (4 chunky bytes) -> 4 plane bytes + mask. */ + p0 = 0u; p1 = 0u; p2 = 0u; p3 = 0u; mask = 0u; + for (bit = 0; bit < 8u; bit++) { + uint8_t nibble = (bit & 1u) + ? (uint8_t)(chunky[bit >> 1] & 0x0Fu) + : (uint8_t)(chunky[bit >> 1] >> 4); + uint8_t bm = (uint8_t)(0x80u >> bit); + if (nibble != 0u) { mask |= bm; } + if (nibble & 1u) { p0 |= bm; } + if (nibble & 2u) { p1 |= bm; } + if (nibble & 4u) { p2 |= bm; } + if (nibble & 8u) { p3 |= bm; } + } + if (mask == 0u) { + continue; + } + + /* Signed dest byte index for this source column. Guard each + * write against [0, X68K_BYTES_PER_ROW) in SIGNED math so + * a negative startByteX never wraps to a huge unsigned. */ + dstByteIdx = (int16_t)(startByteX + byteCol); + leftMask = (uint8_t)(mask >> shift); + if (leftMask != 0u && dstByteIdx >= 0 && dstByteIdx < (int16_t)X68K_BYTES_PER_ROW) { + uint16_t dstByteOff = (uint16_t)(dstRowBase + (uint16_t)dstByteIdx); + notMask = (uint8_t)(~leftMask); + pd->planes[0][dstByteOff] = (uint8_t)((pd->planes[0][dstByteOff] & notMask) | (uint8_t)(p0 >> shift)); + pd->planes[1][dstByteOff] = (uint8_t)((pd->planes[1][dstByteOff] & notMask) | (uint8_t)(p1 >> shift)); + pd->planes[2][dstByteOff] = (uint8_t)((pd->planes[2][dstByteOff] & notMask) | (uint8_t)(p2 >> shift)); + pd->planes[3][dstByteOff] = (uint8_t)((pd->planes[3][dstByteOff] & notMask) | (uint8_t)(p3 >> shift)); + } + + if (shift != 0u) { + int16_t rIdx = (int16_t)(dstByteIdx + 1); + rshift = (uint8_t)(8u - shift); + rightMask = (uint8_t)(mask << rshift); + if (rightMask != 0u && rIdx >= 0 && rIdx < (int16_t)X68K_BYTES_PER_ROW) { + uint16_t rOff = (uint16_t)(dstRowBase + (uint16_t)rIdx); + notMask = (uint8_t)(~rightMask); + pd->planes[0][rOff] = (uint8_t)((pd->planes[0][rOff] & notMask) | (uint8_t)(p0 << rshift)); + pd->planes[1][rOff] = (uint8_t)((pd->planes[1][rOff] & notMask) | (uint8_t)(p1 << rshift)); + pd->planes[2][rOff] = (uint8_t)((pd->planes[2][rOff] & notMask) | (uint8_t)(p2 << rshift)); + pd->planes[3][rOff] = (uint8_t)((pd->planes[3][rOff] & notMask) | (uint8_t)(p3 << rshift)); + } + } + } + } +} + + +// Phase 3 planar dual-write for jlSurfaceCopy: 4 plane memcpys after +// the cross-platform chunky pixel memcpy. Both src and dst planes +// are off-screen shadow buffers; the displayed gPlanes[] is updated +// only at jlStagePresent. +void jlpSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) { + X68kPlanarT *dstPd; + X68kPlanarT *srcPd; + + dstPd = (X68kPlanarT *)dst->portData; + srcPd = (X68kPlanarT *)src->portData; + if (dstPd == NULL || srcPd == NULL) { + return; + } + // Straight blitter A->D copy of every whole plane (200 rows x 20 + // words, contiguous). Both source and dest planes are CHIP RAM now + // (jlpSurfaceAllocPortData) so the blitter can reach both. The + // helper copies all four planes synchronously on the CPU and returns + // before DisownBlitter so the dest planes are complete before any + // CPU read. + x68kCpuCopyPlanes(dstPd->planes, srcPd->planes, 0u, + (X68K_BYTES_PER_ROW / 2u), SURFACE_HEIGHT, 0u); +} + + +// Same-position group-snapped rect copy (NATIVE-PERF Phase 4 clean- +// buffer op; Phase 6 W3 blitter upgrade). The core wrapper clipped +// and 16-px-snapped the rect, so x and w are multiples of 16: the +// plane byte offset (x >> 3) is even (68000 word-safe) and each row +// is a whole number of words, so no BLTAFWM/BLTALWM edge masking is +// needed. Both surfaces' planes are CHIP RAM (jlpSurfaceAllocPortData), +// so the copy runs as one modulo-driven A->D blit per plane +// (mod = 40 - rowBytes), replacing 4*h short memcpys whose call +// overhead dominated (a 16x16 window paid 64 memcpy calls to move +// 128 bytes -- ~8-10x the blitter path). Rects below +// X68K_COPYRECT_MEMCPY_MIN_WORDS per-plane words stay on a CPU +// word-store walk instead: the blitter's fixed setup cost loses +// there, and the snap's word alignment makes the walk 68000-safe. +void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + X68kPlanarT *dstPd; + X68kPlanarT *srcPd; + uint16_t *d; + const uint16_t *s; + uint16_t off; + uint16_t rowBytes; + uint16_t widthWords; + uint16_t row; + uint16_t word; + uint8_t plane; + + dstPd = (X68kPlanarT *)dst->portData; + srcPd = (X68kPlanarT *)src->portData; + if (dstPd == NULL || srcPd == NULL) { + jlpGenericSurfaceCopyRect(dst, src, x, y, w, h); + return; + } + off = (uint16_t)(y * X68K_BYTES_PER_ROW + (x >> 3)); + rowBytes = (uint16_t)(w >> 3); + widthWords = (uint16_t)(rowBytes >> 1); + if (true) { // see X68K_COPYRECT_MEMCPY_MIN_WORDS: memcpy always wins here + x68kCpuCopyPlanes(dstPd->planes, srcPd->planes, off, widthWords, h, (uint16_t)(X68K_BYTES_PER_ROW - rowBytes)); + return; + } + for (plane = 0; plane < X68K_BITPLANES; plane++) { + d = (uint16_t *)(dstPd->planes[plane] + off); + s = (const uint16_t *)(srcPd->planes[plane] + off); + for (row = 0; row < h; row++) { + for (word = 0; word < widthWords; word++) { + d[word] = s[word]; + } + d += (X68K_BYTES_PER_ROW / 2u); + s += (X68K_BYTES_PER_ROW / 2u); + } + } +} + + +// Phase 4 planar dual-write helper: set one pixel's bit in each of +// the four shadow planes. Caller (the per-primitive walker below) has +// already validated (x, y) is on-surface. byteOff is computed once +// and reused across all four planes since stride is the same in each. +/* Per-pixel plane RMW with the color-bit classification hoisted by + * the caller. set0..set3 are 0xFF if that plane's color bit is 1 + * (OR-in bitMask), 0 if 0 (AND-out bitMask). Each plane updates with + * `*pn = (*pn & ~bitMask) | (setN & bitMask)` -- branch-free, 4 + * RMWs per pixel. Inlined into shape walkers so the outer loop pays + * no function-call overhead. */ +/* Bit-mask LUT for the per-pixel macro -- replaces a runtime + * `0x80u >> (x & 7)` (8 cyc per bit shifted on 68000) with a single + * byte load. Saves ~25 cyc per pixel inside circle / line walkers. */ +static const uint8_t kAmigaPlanePutPixelBitLut[8] = { + 0x80u, 0x40u, 0x20u, 0x10u, 0x08u, 0x04u, 0x02u, 0x01u +}; + +#define AMIGA_PLANE_PUT_PIXEL(pd_, x_, y_, set0_, set1_, set2_, set3_) do { \ + uint16_t byteOff_ = (uint16_t)((uint16_t)(y_) * X68K_BYTES_PER_ROW + ((uint16_t)(x_) >> 3)); \ + uint8_t bitMask_ = kAmigaPlanePutPixelBitLut[(uint16_t)(x_) & 7u]; \ + uint8_t notMask_ = (uint8_t)~bitMask_; \ + uint8_t *p0_ = &(pd_)->planes[0][byteOff_]; \ + uint8_t *p1_ = &(pd_)->planes[1][byteOff_]; \ + uint8_t *p2_ = &(pd_)->planes[2][byteOff_]; \ + uint8_t *p3_ = &(pd_)->planes[3][byteOff_]; \ + *p0_ = (uint8_t)((*p0_ & notMask_) | ((set0_) & bitMask_)); \ + *p1_ = (uint8_t)((*p1_ & notMask_) | ((set1_) & bitMask_)); \ + *p2_ = (uint8_t)((*p2_ & notMask_) | ((set2_) & bitMask_)); \ + *p3_ = (uint8_t)((*p3_ & notMask_) | ((set3_) & bitMask_)); \ +} while (0) + + +static void x68kPlanarSetPixel(X68kPlanarT *pd, int16_t x, int16_t y, uint8_t color) { + uint16_t byteOff; + uint8_t bitMask; + uint8_t notMask; + uint8_t *p0; + uint8_t *p1; + uint8_t *p2; + uint8_t *p3; + + byteOff = (uint16_t)((uint16_t)y * X68K_BYTES_PER_ROW + ((uint16_t)x >> 3)); + bitMask = (uint8_t)(0x80u >> ((uint16_t)x & 7u)); + notMask = (uint8_t)~bitMask; + + /* Unroll the 4-plane loop. Loop counter + array indexing inside + * the hot per-pixel path is the gating cost on circle outlines: + * UBER jlDrawCircle r=80 calls this ~640 times per call. */ + p0 = &pd->planes[0][byteOff]; + p1 = &pd->planes[1][byteOff]; + p2 = &pd->planes[2][byteOff]; + p3 = &pd->planes[3][byteOff]; + + if (color & 0x01u) { *p0 = (uint8_t)(*p0 | bitMask); } else { *p0 = (uint8_t)(*p0 & notMask); } + if (color & 0x02u) { *p1 = (uint8_t)(*p1 | bitMask); } else { *p1 = (uint8_t)(*p1 & notMask); } + if (color & 0x04u) { *p2 = (uint8_t)(*p2 | bitMask); } else { *p2 = (uint8_t)(*p2 & notMask); } + if (color & 0x08u) { *p3 = (uint8_t)(*p3 | bitMask); } else { *p3 = (uint8_t)(*p3 & notMask); } +} + + +void jlpDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) { + X68kPlanarT *pd; + uint8_t nibLo; + if (s->portData == NULL) { + jlpGenericDrawPixel(s, x, y, colorIndex); + return; + } + nibLo = (uint8_t)(colorIndex & 0x0Fu); + pd = (X68kPlanarT *)s->portData; + x68kPlanarSetPixel(pd, (int16_t)x, (int16_t)y, nibLo); +} + + +// Bresenham's diagonal line, planar-only walk. Same algorithm as +// cross-platform jlDrawLine's fallback so the pixel set matches it +// bit-for-bit. +static void x68kPlanarLine(X68kPlanarT *pd, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color) { + int16_t dx; + int16_t dy; + int16_t sx; + int16_t sy; + int16_t err; + int16_t e2; + uint8_t set0; + uint8_t set1; + uint8_t set2; + uint8_t set3; + + set0 = (color & 0x01u) ? 0xFFu : 0x00u; + set1 = (color & 0x02u) ? 0xFFu : 0x00u; + set2 = (color & 0x04u) ? 0xFFu : 0x00u; + set3 = (color & 0x08u) ? 0xFFu : 0x00u; + + dx = (int16_t)((x1 > x0) ? (x1 - x0) : (x0 - x1)); + dy = (int16_t)(-((y1 > y0) ? (y1 - y0) : (y0 - y1))); + sx = (int16_t)((x0 < x1) ? 1 : -1); + sy = (int16_t)((y0 < y1) ? 1 : -1); + err = (int16_t)(dx + dy); + while (1) { + AMIGA_PLANE_PUT_PIXEL(pd, x0, y0, set0, set1, set2, set3); + if (x0 == x1 && y0 == y1) { + break; + } + e2 = (int16_t)(2 * err); + if (e2 >= dy) { + err = (int16_t)(err + dy); + x0 = (int16_t)(x0 + sx); + } + if (e2 <= dx) { + err = (int16_t)(err + dx); + y0 = (int16_t)(y0 + sy); + } + } +} + + +void jlpDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) { + X68kPlanarT *pd; + if (s->portData == NULL) { + return; + } + pd = (X68kPlanarT *)s->portData; + x68kPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu)); +} + + +void jlpDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) { + X68kPlanarT *pd; + if (s->portData == NULL) { + return; + } + pd = (X68kPlanarT *)s->portData; + surface68kAmigaCircleOutline(pd->planes[0], pd->planes[1], + pd->planes[2], pd->planes[3], + (uint16_t)cx, (uint16_t)cy, r, + (uint8_t)(colorIndex & 0x0Fu)); +} + + +/* Single-row 4-plane span fill via shared68k asm. Caller pre-computes + * the left/right partial-byte masks; fillByte per plane is just + * 0xFF/0x00 based on colorIndex bit. The asm body avoids per-byte + * function-call dispatch and the C compiler's per-iter overhead -- + * critical for jlFillCircle r=40 which pre-asm was paying ~50 ms/call + * for 80 spans. */ +static inline __attribute__((always_inline)) +void x68kFillSpanInline(X68kPlanarT *pd, int16_t spanX, int16_t spanY, + uint16_t spanW, uint8_t colorIndex) { + static const uint8_t kLM[8] = {0xFFu,0x7Fu,0x3Fu,0x1Fu,0x0Fu,0x07u,0x03u,0x01u}; + static const uint8_t kRM[8] = {0x80u,0xC0u,0xE0u,0xF0u,0xF8u,0xFCu,0xFEu,0xFFu}; + + uint16_t byteFirst = (uint16_t)((uint16_t)spanX >> 3); + uint16_t lastBit = (uint16_t)(spanX + spanW - 1); + uint16_t byteLast = (uint16_t)(lastBit >> 3); + uint8_t leftMask = kLM[(uint16_t)spanX & 7u]; + uint8_t rightMask = kRM[lastBit & 7u]; + uint16_t rowOff = (uint16_t)((uint16_t)spanY * X68K_BYTES_PER_ROW + byteFirst); + uint8_t fb0 = ((colorIndex >> 0) & 1u) ? 0xFFu : 0x00u; + uint8_t fb1 = ((colorIndex >> 1) & 1u) ? 0xFFu : 0x00u; + uint8_t fb2 = ((colorIndex >> 2) & 1u) ? 0xFFu : 0x00u; + uint8_t fb3 = ((colorIndex >> 3) & 1u) ? 0xFFu : 0x00u; + uint8_t *p0 = pd->planes[0] + rowOff; + uint8_t *p1 = pd->planes[1] + rowOff; + uint8_t *p2 = pd->planes[2] + rowOff; + uint8_t *p3 = pd->planes[3] + rowOff; + + if (byteFirst == byteLast) { + /* Single-byte case kept in C: the asm path post-increments + * the pointer between leading and trailing RMW, which would + * read the wrong byte if both edges land on the same byte. + * One-byte spans are rare anyway (~1 of 80 in jlFillCircle r=40) + * so the C overhead is fine here. */ + uint8_t mask = (uint8_t)(leftMask & rightMask); + uint8_t notMask = (uint8_t)~mask; + uint8_t *pp[X68K_BITPLANES]; + uint8_t fb[X68K_BITPLANES]; + uint8_t i; + pp[0] = p0; pp[1] = p1; pp[2] = p2; pp[3] = p3; + fb[0] = fb0; fb[1] = fb1; fb[2] = fb2; fb[3] = fb3; + for (i = 0; i < X68K_BITPLANES; i++) { + *pp[i] = (uint8_t)((*pp[i] & notMask) | (fb[i] & mask)); + } + return; + } + + { + uint16_t numMid = (uint16_t)(byteLast - byteFirst - 1u); + surface68kFillSpan4Planes(p0, p1, p2, p3, numMid, leftMask, rightMask, + fb0, fb1, fb2, fb3); + } +} + + +bool jlpFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) { + X68kPlanarT *pd; + uint32_t r2; + uint32_t xx; + uint32_t yy; + int32_t x; + uint16_t y; + + if (s->portData == NULL) { + return false; + } + pd = (X68kPlanarT *)s->portData; + // Exact-disk span walk (perf-hunt-2 rank 14, the recorded "true + // lever"; algorithm copied from the ST stPlanarCircleFill): for + // each y offset track the largest x with x^2 + y^2 <= r^2 + // incrementally, and fill each row exactly ONCE. The old midpoint + // loop emitted 4 octant spans per iteration whose horizontal spans + // overlapped (~14-16% redundant bytes at r=40) and re-dispatched + // x68kFillSpanInline for each. Row coverage is identical to + // draw.c's jlFillCircle and the ST walker, so the golden hashes + // are unchanged by construction. Spans stay unclipped -- draw.c + // routes every off-surface circle through its own clipped loop + // before jlpFillCircle is called (same contract as before). + r2 = (uint32_t)r * (uint32_t)r; + xx = r2; + yy = 0u; + x = (int32_t)r; + for (y = 0u; y <= r; y++) { + uint32_t xxLimit; + int16_t spanX; + uint16_t spanW; + + xxLimit = r2 - yy; + while (xx > xxLimit) { + xx = xx - ((uint32_t)x + (uint32_t)x - 1u); + x--; + } + spanX = (int16_t)(cx - (int16_t)x); + spanW = (uint16_t)((int16_t)x + (int16_t)x + 1); + x68kFillSpanInline(pd, spanX, (int16_t)(cy + (int16_t)y), spanW, colorIndex); + if (y > 0u) { + x68kFillSpanInline(pd, spanX, (int16_t)(cy - (int16_t)y), spanW, colorIndex); + } + yy = yy + ((uint32_t)y + (uint32_t)y + 1u); + } + return true; +} + + +/* Plane-aware nibble at (x, y) given the 4 plane row bases. Reads one + * byte per plane and assembles the 4-bit color from a single bit + * position. Caller is responsible for valid (x, y). */ +static uint8_t x68kNibbleFromPlanes(uint8_t * const planes[X68K_BITPLANES], int16_t x) { + uint16_t byteOff; + uint8_t bitMask; + uint8_t color; + + byteOff = (uint16_t)((uint16_t)x >> 3); + bitMask = (uint8_t)(0x80u >> ((uint16_t)x & 7u)); + color = 0u; + if (planes[0][byteOff] & bitMask) color = (uint8_t)(color | 0x01u); + if (planes[1][byteOff] & bitMask) color = (uint8_t)(color | 0x02u); + if (planes[2][byteOff] & bitMask) color = (uint8_t)(color | 0x04u); + if (planes[3][byteOff] & bitMask) color = (uint8_t)(color | 0x08u); + return color; +} + + +/* Build the 4 plane row pointers for a given y. */ +static void x68kPlaneRowPtrs(const jlSurfaceT *s, int16_t y, uint8_t **out /* [4] */) { + X68kPlanarT *pd; + uint16_t yOff; + uint8_t i; + + pd = (X68kPlanarT *)s->portData; + yOff = (uint16_t)((uint16_t)y * X68K_BYTES_PER_ROW); + for (i = 0; i < X68K_BITPLANES; i++) { + out[i] = pd->planes[i] + yOff; + } +} + + +bool jlpFloodWalkPlanes(const jlSurfaceT *s, int16_t startX, int16_t y, uint8_t matchColor, uint8_t newColor, bool matchEqual, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) { + X68kPlanarT *pd; + uint8_t *rowPlanes[X68K_BITPLANES]; + int16_t leftX; + int16_t rightX; + uint8_t pix; + bool pixMatch; + + pd = (X68kPlanarT *)s->portData; + if (pd == NULL) { + return false; + } + matchColor = (uint8_t)(matchColor & 0x0Fu); + newColor = (uint8_t)(newColor & 0x0Fu); + x68kPlaneRowPtrs(s, y, rowPlanes); + + pix = x68kNibbleFromPlanes(rowPlanes, startX); + pixMatch = (pix == matchColor); + if (matchEqual ? !pixMatch : (pixMatch || pix == newColor)) { + *seedMatched = false; + return true; + } + *seedMatched = true; + + leftX = startX; + while (leftX > 0) { + pix = x68kNibbleFromPlanes(rowPlanes, (int16_t)(leftX - 1)); + pixMatch = (pix == matchColor); + if (matchEqual ? !pixMatch : (pixMatch || pix == newColor)) { + break; + } + leftX--; + } + + rightX = startX; + while (rightX < SURFACE_WIDTH - 1) { + pix = x68kNibbleFromPlanes(rowPlanes, (int16_t)(rightX + 1)); + pixMatch = (pix == matchColor); + if (matchEqual ? !pixMatch : (pixMatch || pix == newColor)) { + break; + } + rightX++; + } + + *leftXOut = leftX; + *rightXOut = rightX; + return true; +} + + +bool jlpFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, int16_t scanY, uint8_t matchColor, uint8_t newColor, bool matchEqual, uint8_t *markBuf) { + X68kPlanarT *pd; + uint8_t *rowPlanes[X68K_BITPLANES]; + int16_t byteCol; + int16_t byteColFirst; + int16_t byteColLast; + int16_t bit; + int16_t x; + int16_t markIdx; + uint8_t p0, p1, p2, p3; + uint8_t bitMask; + uint8_t pix; + bool pixMatch; + + pd = (X68kPlanarT *)s->portData; + if (pd == NULL) { + return false; + } + matchColor = (uint8_t)(matchColor & 0x0Fu); + newColor = (uint8_t)(newColor & 0x0Fu); + x68kPlaneRowPtrs(s, scanY, rowPlanes); + + byteColFirst = (int16_t)(leftX >> 3); + byteColLast = (int16_t)(rightX >> 3); + for (byteCol = byteColFirst; byteCol <= byteColLast; byteCol++) { + p0 = rowPlanes[0][byteCol]; + p1 = rowPlanes[1][byteCol]; + p2 = rowPlanes[2][byteCol]; + p3 = rowPlanes[3][byteCol]; + for (bit = 0; bit < 8; bit++) { + x = (int16_t)((byteCol << 3) + bit); + if (x < leftX || x > rightX) { + continue; + } + bitMask = (uint8_t)(0x80u >> bit); + pix = 0u; + if (p0 & bitMask) pix = (uint8_t)(pix | 0x01u); + if (p1 & bitMask) pix = (uint8_t)(pix | 0x02u); + if (p2 & bitMask) pix = (uint8_t)(pix | 0x04u); + if (p3 & bitMask) pix = (uint8_t)(pix | 0x08u); + pixMatch = (pix == matchColor); + markIdx = (int16_t)(x - leftX); + markBuf[markIdx] = (uint8_t)(matchEqual + ? (pixMatch ? 1 : 0) + : ((!pixMatch && pix != newColor) ? 1 : 0)); + } + } + return true; +} + + +/* ===== Phase 9 reader hooks: pure-planar Amiga implementations ===== + * + * Cross-platform code that USED to read s->pixels (chunky shadow) now + * goes through these. On Amiga the chunky shadow doesn't exist; + * pixels are derived on demand by walking the plane bits. + * + * Per-pixel assembly: for pixel (x, y), read bit (7 - x%8) from plane + * byte at row*40 + x/8 in each of the 4 planes. Color index = sum + * of (bit_p << p). */ + +uint8_t jlpSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) { + X68kPlanarT *pd; + uint16_t byteOff; + uint8_t bitMask; + uint8_t color; + uint16_t i; + + pd = (X68kPlanarT *)s->portData; + if (pd == NULL) { + return 0u; + } + byteOff = (uint16_t)((uint16_t)y * X68K_BYTES_PER_ROW + ((uint16_t)x >> 3)); + bitMask = (uint8_t)(0x80u >> ((uint16_t)x & 7u)); + color = 0u; + for (i = 0; i < X68K_BITPLANES; i++) { + if (pd->planes[i][byteOff] & bitMask) { + color = (uint8_t)(color | (1u << i)); + } + } + return color; +} + + +/* Reverse-c2p: per row, derive 160 chunky bytes from 40 plane bytes + * (per plane, 4 planes). Used by jlpSurfaceHash to fold the planar + * surface into the same byte-stream the chunky ports hash, so cross- + * port hash comparisons stay valid. + * Walks 8 pixels per planar-byte column; per pixel assembles nibble + * from 4 plane bits. Output: 4 chunky bytes per planar-byte column + * (since 8 pixels = 4 chunky bytes at 2px/byte). */ +static void x68kPlanesToChunkyRow(const X68kPlanarT *pd, int16_t y, uint8_t *dstChunkyRow) { + uint16_t col; + uint16_t byteOff; + uint8_t b0, b1, b2, b3; + uint8_t pix; + uint8_t bitMask; + uint16_t p; + + for (col = 0; col < X68K_BYTES_PER_ROW; col++) { + byteOff = (uint16_t)((uint16_t)y * X68K_BYTES_PER_ROW + col); + b0 = pd->planes[0][byteOff]; + b1 = pd->planes[1][byteOff]; + b2 = pd->planes[2][byteOff]; + b3 = pd->planes[3][byteOff]; + /* For each of 8 pixels in this planar byte (bit 7 = leftmost), + * assemble nibble from the 4 plane bits and pack into chunky + * bytes (high nibble = even pixel, low nibble = odd pixel). */ + for (p = 0; p < 8u; p++) { + bitMask = (uint8_t)(0x80u >> p); + pix = 0u; + if (b0 & bitMask) pix = (uint8_t)(pix | 1u); + if (b1 & bitMask) pix = (uint8_t)(pix | 2u); + if (b2 & bitMask) pix = (uint8_t)(pix | 4u); + if (b3 & bitMask) pix = (uint8_t)(pix | 8u); + if ((p & 1u) == 0u) { + dstChunkyRow[col * 4u + (p >> 1)] = (uint8_t)(pix << 4); + } else { + dstChunkyRow[col * 4u + (p >> 1)] = (uint8_t)(dstChunkyRow[col * 4u + (p >> 1)] | pix); + } + } + } +} + + +uint32_t jlpSurfaceHash(const jlSurfaceT *s) { + X68kPlanarT *pd; + uint16_t lo = 0xACE1u, hi = 0x1357u; + uint16_t n, v; + int16_t row; + uint8_t b; + uint8_t chunkyRow[SURFACE_BYTES_PER_ROW]; + const uint16_t *w; + + pd = (X68kPlanarT *)s->portData; + if (pd == NULL) { + return 0u; + } + /* Pixel hash: derive chunky one row at a time, fold byte-by-byte + * via the shared SURFACE_HASH_MIX_BYTE so cross-port hash + * matches. */ + for (row = 0; row < SURFACE_HEIGHT; row++) { + x68kPlanesToChunkyRow(pd, row, chunkyRow); + for (n = 0; n < SURFACE_BYTES_PER_ROW; n++) { + b = chunkyRow[n]; + SURFACE_HASH_MIX_BYTE(lo, hi, b); + } + } + /* SCB: byte sequence, endian-independent. */ + for (n = 0; n < (uint16_t)SURFACE_HEIGHT; n++) { + b = s->scb[n]; + SURFACE_HASH_MIX_BYTE(lo, hi, b); + } + /* Palette: read uint16 values, fold high-then-low for endian- + * independence. */ + w = &s->palette[0][0]; + for (n = 0; n < (uint16_t)SURFACE_PALETTE_ENTRIES; n++) { + v = *w++; + b = (uint8_t)((v >> 8) & 0xFFu); SURFACE_HASH_MIX_BYTE(lo, hi, b); + b = (uint8_t)(v & 0xFFu); SURFACE_HASH_MIX_BYTE(lo, hi, b); + } + return ((uint32_t)hi << 16) | (uint32_t)lo; +} + + +// On-disk format is the Amiga's native plane-major buffer: planes +// 0..3 written sequentially, X68K_PLANE_SIZE bytes each. +bool jlpSurfaceLoadFile(jlSurfaceT *dst, FILE *fp) { + X68kPlanarT *pd; + uint8_t i; + + pd = (X68kPlanarT *)dst->portData; + if (pd == NULL) { + return false; + } + for (i = 0; i < X68K_BITPLANES; i++) { + if (fread(pd->planes[i], 1, X68K_PLANE_SIZE, fp) != X68K_PLANE_SIZE) { + return false; + } + } + return true; +} + + +bool jlpSurfaceSaveFile(const jlSurfaceT *src, FILE *fp) { + X68kPlanarT *pd; + uint8_t i; + + pd = (X68kPlanarT *)src->portData; + if (pd == NULL) { + return false; + } + for (i = 0; i < X68K_BITPLANES; i++) { + if (fwrite(pd->planes[i], 1, X68K_PLANE_SIZE, fp) != X68K_PLANE_SIZE) { + return false; + } + } + return true; +} + + +uint8_t *jlpStageAllocPixels(void) { + /* Phase 9: Amiga has no chunky shadow. The stage pixels pointer + * stays NULL; cross-platform code reads pixels via jlpSamplePixel + * (or the other chunky reader ops) which read from planes. NULL is + * a valid return -- cross-platform stageAlloc treats NULL as + * "port has no chunky storage" and skips the chunky memset. */ + return NULL; +} + + +uint8_t *jlpSurfaceAllocPixels(void) { + /* Same rationale as jlpStageAllocPixels: no chunky on Amiga. */ + return NULL; +} + + +// Allocate the per-surface planar storage: an X68kPlanarT plus 4 +// off-screen chip-RAM plane buffers. The stage gets its OWN shadow +// planes (NOT aliased to gPlanes / Intuition's BitMap) so drawing +// primitives that dual-write to planes don't immediately appear on +// screen -- the display is updated atomically at jlStagePresent time +// (c2p chunky->gPlanes today; memcpy shadow->gPlanes under +// JOEYLIB_PLANAR_PRESENT; pointer-swap or alias in Phase 9). +// Aliasing the stage to gPlanes was tried and reverted because every +// drawing primitive showed AS IT HAPPENED, which broke the "draw +// invisibly, palette flips with content at present" semantic Pattern +// and apps depend on. See project_planar_68k_plan.md Phase 3 notes. +// +// Returns NULL on allocation failure. Cross-platform code stores the +// result in s->portData; primitives access via (X68kPlanarT *) +// s->portData. +void *jlpSurfaceAllocPortData(jlSurfaceT *s, bool isStage) { + X68kPlanarT *pd; + uint16_t i; + + (void)s; + (void)isStage; + pd = (X68kPlanarT *)calloc(1u, sizeof(X68kPlanarT)); + if (pd == NULL) { + return NULL; + } + pd->bytesPerRow = X68K_BYTES_PER_ROW; + pd->bytesPerPlane = X68K_PLANE_SIZE; + + /* Both stage and non-stage get their own zeroed planes. Unlike the + * Amiga these are plain heap: no blitter or DMA reads them, only the + * CPU, so there is no Chip RAM constraint to satisfy. */ + for (i = 0; i < X68K_BITPLANES; i++) { + pd->planes[i] = (uint8_t *)calloc(1u, (size_t)X68K_PLANE_SIZE); + jlLogF("x68k: shadow plane %u on heap (CPU only, addr=$%08lX)", + (unsigned)i, (unsigned long)pd->planes[i]); + if (pd->planes[i] == NULL) { + /* Roll back any planes already allocated. */ + while (i > 0u) { + i--; + free(pd->planes[i]); + } + free(pd); + return NULL; + } + } + pd->ownsPlanes = true; + return pd; +} + + +void jlpSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData) { + X68kPlanarT *pd; + uint16_t i; + + (void)s; + (void)isStage; + if (portData == NULL) { + return; + } + pd = (X68kPlanarT *)portData; + if (pd->ownsPlanes) { + for (i = 0; i < X68K_BITPLANES; i++) { + if (pd->planes[i] != NULL) { + free(pd->planes[i]); + } + } + } + free(pd); +} diff --git a/src/x68000/save.c b/src/x68000/save.c new file mode 100644 index 0000000..ead9a63 --- /dev/null +++ b/src/x68000/save.c @@ -0,0 +1,53 @@ +// Sharp X68000 save-file + disk-space HAL. +// +// Structurally the Atari ST HAL (src/atarist/save.c): SAVES/ is a real +// subdirectory next to the .X, delete is stdio remove(), and free space comes +// from the OS. Human68k's _dos_dskfre is the Dfree equivalent. + +#include + +#include + +#include "joey/file.h" +#include "port.h" + + +#define X68K_DRIVE_CURRENT 0 // _dos_dskfre drive 0 = current drive +#define X68K_MAX_U32 0xFFFFFFFFu + + +uint32_t jlpDiskFree(void) { + struct dos_freeinf info; + uint32_t bytesPerClus; + + // Negative return is an error (bad drive, no media). + if (_dos_dskfre(X68K_DRIVE_CURRENT, &info) < 0) { + return 0u; + } + // free = free clusters, sec = sectors per cluster, byte = bytes per sector. + // Saturate rather than wrap: no 64-bit multiply helper on the 68000, and + // a 1.2 MB floppy cannot overflow this anyway -- the guard is for hard + // disks, where free * secPerClus * bytesPerSec genuinely can. + bytesPerClus = (uint32_t)info.sec * (uint32_t)info.byte; + if (bytesPerClus != 0u && (uint32_t)info.free > X68K_MAX_U32 / bytesPerClus) { + return X68K_MAX_U32; + } + return (uint32_t)info.free * bytesPerClus; +} + + +bool jlpSaveDelete(const char *path) { + return remove(path) == 0; +} + + +bool jlpSaveDirEnsure(const char *dir) { + int rc; + + // "Already exists" is the common case and is success for us, so accept + // _DOSE_EXISTDIR (-20) alongside a clean create. Note the neighbouring + // _DOSE_EXISTFILE is -80 -- easy to transpose, and it means something + // else entirely (a FILE of that name is in the way, which IS a failure). + rc = _dos_mkdir(dir); + return rc >= 0 || rc == _DOSE_EXISTDIR; +} diff --git a/src/x68000/serial.c b/src/x68000/serial.c new file mode 100644 index 0000000..ed772cd --- /dev/null +++ b/src/x68000/serial.c @@ -0,0 +1,116 @@ +// Sharp X68000 serial HAL: RS-232C via the IOCS polled API. +// +// Structurally the Atari ST HAL (src/atarist/serial.c): ride the OS's polled +// interface, hijack no vector, buffer nothing ourselves. The IOCS calls map +// one-for-one onto the ST's XBIOS ones -- +// _iocs_set232c <- Rsconf (baud + framing, one packed word) +// _iocs_isns232c <- Bconstat (input status) +// _iocs_inp232c <- Bconin (read one byte, BLOCKS if empty) +// _iocs_osns232c <- Bcostat (transmitter ready) +// _iocs_out232c <- Bconout (write one byte) +// +// The machine has one RS-232C line (SCC channel A); channel B is the mouse and +// is not reachable here, so jlSerialDeviceE is accepted and ignored exactly as +// on the ST. + +#include + +#include "port.h" + + +// LINE CONFIGURATION IS DELIBERATELY NOT APPLIED YET. +// +// _iocs_set232c takes a single packed mode word, and the packing could not be +// confirmed. What IS confirmed, from XEiJ's RS232CTerminal.java (which reads +// the live mode word out of the RSDRV.SYS work area): +// bit 7 RTS/CTS flow control +// bit 9 XON/XOFF flow control +// and its baud-rate table is +// 0=75 1=150 2=300 3=600 4=1200 5=2400 6=4800 7=9600 8=19200 9=31250 +// 10=38400 11=50000 12=57600 13=76800 14=115200 15=230400 +// i.e. the rate is a FOUR-bit field, so it cannot sit at bits 0-2 with parity +// at bit 3. The positions of the data/stop/parity fields remain unknown. +// +// Writing a guessed mode word is worse than writing none: a wrong framing +// setting still transmits, it just corrupts every byte, which is exactly the +// failure this port hit first time out (every received byte came back as 0x00 +// or 0x80 -- only the MSB surviving). +// +// So jlpSerialOpen inherits whatever RSDRV.SYS configured at boot. The Human68k +// "RS-232C DRIVER for X68000 version 2.02" banner appears during startup and +// leaves the port at its default, which pairs with MAME's null_modem default +// of 9600 8N1. Honour jlSerialConfigT properly once the layout is confirmed +// against hardware documentation. + +// Bounded transmitter-ready spin, so a stalled link (no CTS) reports a partial +// write instead of hanging the frame. Same guard the ST HAL uses. +#define X68K_TX_SPIN_LIMIT 200000ul + + +// ----- HAL entry points (alphabetical) ----- + +uint16_t jlpSerialAvailable(void) { + // _iocs_isns232c reports presence, not a count: non-zero == at least one. + return (_iocs_isns232c() != 0) ? 1u : 0u; +} + + +void jlpSerialClose(void) { + // IOCS path hijacks no vector -- nothing to tear down. +} + + +void jlpSerialFlush(void) { + while (_iocs_isns232c() != 0) { + (void)_iocs_inp232c(); + } +} + + +bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { + (void)device; // one RS-232C line; channel B is the mouse. + if (config == NULL) { + return false; + } + // config is accepted but NOT applied -- see the mode-word note above. The + // port keeps whatever RSDRV.SYS set at boot. + (void)config; + return true; +} + + +void jlpSerialPoll(void) { + // IOCS buffers RX itself; nothing to pump. +} + + +uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) { + uint16_t n; + + n = 0u; + // Gate each read on the status call first: _iocs_inp232c BLOCKS when the + // buffer is empty, which would stall the frame. + while (n < max && _iocs_isns232c() != 0) { + buf[n] = (uint8_t)(_iocs_inp232c() & 0xFF); + n++; + } + return n; +} + + +uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) { + uint16_t n; + uint32_t guard; + + for (n = 0u; n < len; n++) { + guard = 0ul; + while (_iocs_osns232c() == 0) { + guard++; + if (guard > X68K_TX_SPIN_LIMIT) { + return n; + } + } + _iocs_out232c((int)buf[n]); + } + return n; +} diff --git a/src/x68000/x68kPlanar.h b/src/x68000/x68kPlanar.h new file mode 100644 index 0000000..f779467 --- /dev/null +++ b/src/x68000/x68kPlanar.h @@ -0,0 +1,121 @@ +// Sharp X68000 per-surface planar storage. +// +// Step 1 of the storage-model conversion (see docs/x68000_port.md). Nothing +// includes this yet -- the port still ships the chunky-plus-expand path, which +// is hash-verified against the Apple IIgs golden reference. This header lands +// first so the conversion can proceed in verifiable increments instead of one +// unlandable change. +// +// WHY THIS IS ALMOST amigaPlanar.h +// ------------------------------- +// A 320px-wide 4bpp surface is 40 bytes per row per plane on ANY planar +// machine, so the SURFACE layout here is byte-for-byte the Amiga's. That is +// not a coincidence worth exploiting cheaply -- it is why the 2,320 lines of +// planar primitives in src/amiga/hal.c can be adapted rather than rewritten: +// they read the stride out of this struct and never hardcode it +// (grep -c AMIGA_PLANE_STRIDE src/amiga/hal.c == 0). +// +// WHERE THE X68000 DIFFERS +// ------------------------ +// The DISPLAY is not the surface. The text plane is four 1bpp planes of a +// 1024px-wide raster, 128 bytes per row, based 128 KB apart: +// +// plane 0 $E00000 plane 1 $E20000 +// plane 2 $E40000 plane 3 $E60000 +// pixel(x,y) -> plane + y*128 + x/8, bit 7-(x&7), plane 0 = LSB +// +// so jlpPresent scatters the compact 40-byte rows into that 128-byte stride. +// That is the ONLY place the 128 appears, and it is where the win comes from: +// 4 planes x 200 rows x 40 bytes = 32,000 bytes per frame, against 128,000 +// bytes of GVRAM word writes on the chunky path. Bus traffic is what the +// measured wait states punish (TVRAM 1.92 cycles/access, GVRAM 1.03 -- see +// docs/x68000_mame_crtc_timing.md), so a 4x traffic cut beats TVRAM's higher +// per-access cost by roughly 2x overall. +// +// Unlike the Amiga there is no Chip RAM constraint: planes are plain malloc, +// because nothing but the CPU ever reads them. +#ifndef JOEY_X68K_PLANAR_H +#define JOEY_X68K_PLANAR_H + +#include +#include + +#include "surfaceInternal.h" + +#define X68K_BITPLANES 4 +#define X68K_BYTES_PER_ROW (SURFACE_WIDTH / 8) // 40 +#define X68K_PLANE_SIZE (X68K_BYTES_PER_ROW * SURFACE_HEIGHT) + +// Text-plane display geometry. Only jlpPresent uses these. +#define X68K_TVRAM_PLANE0 0xE00000UL +#define X68K_TVRAM_PLANE_STEP 0x020000UL // 128 KB apart +#define X68K_TVRAM_STRIDE 128u // 1024 px / 8 + +// The library surface is 320x200 and the machine has no such mode, so the +// closest is 512x512 (_iocs_crtmod 12) and the window is CENTRED in it rather +// than parked in the corner. X must land on a byte boundary in a 1bpp plane; +// 96/8 = 12 exactly, so no sub-byte shifting is needed. +#define X68K_DISPLAY_W 512u +#define X68K_DISPLAY_H 512u +#define X68K_ORIGIN_X ((X68K_DISPLAY_W - SURFACE_WIDTH) / 2u) // 96 px +#define X68K_ORIGIN_Y ((X68K_DISPLAY_H - SURFACE_HEIGHT) / 2u) // 156 rows +#define X68K_ORIGIN_BYTE (X68K_ORIGIN_X / 8u) // 12 bytes +typedef char x68kOriginXIsByteAligned[((X68K_ORIGIN_X % 8u) == 0u) ? 1 : -1]; + +// Text/PCG palette: 16 entries the text plane indexes. NOT $E82000, which is +// the 256-entry GRAPHIC palette -- the text plane reads the PCG block. +#define X68K_TEXT_PALETTE ((volatile uint16_t *)0xE82200L) + +// Colour word is GGGGG RRRRR BBBBB I -- green and red SWAPPED versus the usual +// RGB555. Writing JoeyLib's $0RGB straight through puts red where green +// belongs, which reads as "greens turn blue, reds turn grey". Verified against +// MAME's own decoder (x68k_v.cpp GGGGGRRRRRBBBBBI): +// i = raw & 1; r = (raw >> 5) & 0x3e | i; +// g = (raw >> 10) & 0x3e | i; b = (raw >> 0) & 0x3e | i; +// so each channel is 6-bit: 5 bits from its field plus the SHARED intensity +// bit. 4-bit input expands to 5 as (v << 1) | (v >> 3) so 0 -> 0 and 15 -> 31. +static inline uint16_t x68kColorFromRgb12(uint16_t rgb12) { + uint16_t r4 = (uint16_t)((rgb12 >> 8) & 0x0Fu); + uint16_t g4 = (uint16_t)((rgb12 >> 4) & 0x0Fu); + uint16_t b4 = (uint16_t)(rgb12 & 0x0Fu); + uint16_t r5 = (uint16_t)((r4 << 1) | (r4 >> 3)); + uint16_t g5 = (uint16_t)((g4 << 1) | (g4 >> 3)); + uint16_t b5 = (uint16_t)((b4 << 1) | (b4 >> 3)); + // Intensity is shared, so it cannot be per-channel: set it for any + // non-black colour, which keeps $000 pure black and lets $FFF reach full + // white (31<<1 | 1 == 63) instead of topping out one step short. + uint16_t i = (uint16_t)(((r4 | g4 | b4) != 0u) ? 1u : 0u); + return (uint16_t)((g5 << 11) | (r5 << 6) | (b5 << 1) | i); +} + +// jlSurfaceT.portData points to this. Mirrors AmigaPlanarT field-for-field so +// the adapted primitives need no structural changes; direct fields rather than +// a union because the consumers are asm / inline C wanting minimal indirection +// in the inner loop. +typedef struct { + uint8_t *planes[X68K_BITPLANES]; + uint16_t bytesPerRow; // = X68K_BYTES_PER_ROW (40) + uint16_t bytesPerPlane; // = X68K_PLANE_SIZE (8000) + bool ownsPlanes; // true = malloc'd (free at destroy) +} X68kPlanarT; + + +// NULL means "surface has no planar storage" -- callers use that as their +// route-to-generic gate, the same contract the Amiga side uses. +static inline X68kPlanarT *x68kSurfacePlanar(const jlSurfaceT *s) { + return (X68kPlanarT *)s->portData; +} + + +// Byte offset of pixel (x,y) within one DISPLAY plane, and the bit within it. +// Split out so the present loop and any future asm agree on one definition. +static inline uint32_t x68kTvramOffset(uint16_t x, uint16_t y) { + return ((uint32_t)y * X68K_TVRAM_STRIDE) + (uint32_t)(x >> 3); +} + + +static inline uint8_t x68kTvramBit(uint16_t x) { + return (uint8_t)(0x80u >> (x & 7u)); +} + +#endif diff --git a/src/x68000/x68kTile.h b/src/x68000/x68kTile.h new file mode 100644 index 0000000..5feff35 --- /dev/null +++ b/src/x68000/x68kTile.h @@ -0,0 +1,352 @@ +// Sharp X68000 planar tile ops -- adapted verbatim from src/amiga/x68kTile.h. +// The surface layout is identical (40 bytes per row per plane), so an 8x8 tile +// is 8 rows x 4 planes x 1 byte on both machines and the .tbk blob format is +// byte-compatible; only the type names differ. +// Amiga tile ops as always-inline functions (NATIVE-PERF Phase 2 +// R4a). Bodies moved verbatim from src/amiga/hal.c so the public +// wrappers in src/core/tile.c inline them, deleting the second full +// cdecl call layer (~130-190 cycles/op on the 68000). The jlpTile* +// dispatch names are macro-aliased to these in port.h, which also +// #includes this header at its tail -- AFTER the jlpGenericTile* +// prototypes the portData == NULL fallbacks call. Do not include this +// header directly; it comes with port.h on the Amiga. +#ifndef JOEY_X68K_TILE_H +#define JOEY_X68K_TILE_H + +#include +#include + +#include "joey/tile.h" +#include "x68kPlanar.h" + +// 8-row unrolled tile access (NATIVE-PERF Phase 2 R8): row offsets +// are compile-time constants row * 40, so each row is one d16(An) +// access with no pointer stepping and no loop control. gcc-amigaos +// -O2 keeps the equivalent row loops rolled as dbf (objdump-verified, +// ~18 cycles/row of pure loop overhead). Only the row dimension +// unrolls -- the 4-plane loop keeps real per-plane setup and stays +// register-light per the m68k-gcc-unroll-backfire lesson. +#define AMIGA_TILE_ROW_OFF(_r) ((uint16_t)((_r) * X68K_BYTES_PER_ROW)) + +// Phase 5 planar dual-write for tile ops, fully planar after Phase 9 +// dropped the chunky shadow. All tiles are 8-pixel aligned (8x8 blocks +// at multiples of 8), so plane writes are byte-aligned -- one plane +// byte per row, 8 rows per tile, no edge masks. Stride between rows +// in a plane is X68K_BYTES_PER_ROW (40). + +static inline __attribute__((always_inline)) void x68kTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) { + X68kPlanarT *pd; + uint16_t plane; + uint8_t fillByte; + uint8_t *p; + + pd = (X68kPlanarT *)s->portData; + if (pd == NULL) { + jlpGenericTileFill(s, bx, by, colorIndex); + return; + } + for (plane = 0; plane < X68K_BITPLANES; plane++) { + fillByte = ((colorIndex >> plane) & 1u) ? 0xFFu : 0x00u; + p = pd->planes[plane] + (uint16_t)by * 8u * X68K_BYTES_PER_ROW + bx; + p[AMIGA_TILE_ROW_OFF(0)] = fillByte; + p[AMIGA_TILE_ROW_OFF(1)] = fillByte; + p[AMIGA_TILE_ROW_OFF(2)] = fillByte; + p[AMIGA_TILE_ROW_OFF(3)] = fillByte; + p[AMIGA_TILE_ROW_OFF(4)] = fillByte; + p[AMIGA_TILE_ROW_OFF(5)] = fillByte; + p[AMIGA_TILE_ROW_OFF(6)] = fillByte; + p[AMIGA_TILE_ROW_OFF(7)] = fillByte; + } +} + + +static inline __attribute__((always_inline)) void x68kTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) { + X68kPlanarT *dstPd; + X68kPlanarT *srcPd; + uint16_t plane; + uint8_t *dp; + const uint8_t *sp; + + dstPd = (X68kPlanarT *)dst->portData; + srcPd = (X68kPlanarT *)src->portData; + if (dstPd == NULL || srcPd == NULL) { + jlpGenericTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy); + return; + } + for (plane = 0; plane < X68K_BITPLANES; plane++) { + dp = dstPd->planes[plane] + (uint16_t)dstBy * 8u * X68K_BYTES_PER_ROW + dstBx; + sp = srcPd->planes[plane] + (uint16_t)srcBy * 8u * X68K_BYTES_PER_ROW + srcBx; + dp[AMIGA_TILE_ROW_OFF(0)] = sp[AMIGA_TILE_ROW_OFF(0)]; + dp[AMIGA_TILE_ROW_OFF(1)] = sp[AMIGA_TILE_ROW_OFF(1)]; + dp[AMIGA_TILE_ROW_OFF(2)] = sp[AMIGA_TILE_ROW_OFF(2)]; + dp[AMIGA_TILE_ROW_OFF(3)] = sp[AMIGA_TILE_ROW_OFF(3)]; + dp[AMIGA_TILE_ROW_OFF(4)] = sp[AMIGA_TILE_ROW_OFF(4)]; + dp[AMIGA_TILE_ROW_OFF(5)] = sp[AMIGA_TILE_ROW_OFF(5)]; + dp[AMIGA_TILE_ROW_OFF(6)] = sp[AMIGA_TILE_ROW_OFF(6)]; + dp[AMIGA_TILE_ROW_OFF(7)] = sp[AMIGA_TILE_ROW_OFF(7)]; + } +} + + +// Pure-planar masked copy. For each row of the 8x8 tile, read 4 src +// plane bytes; compute a per-pixel "non-transparent" mask via XOR +// against the transparent index's per-plane bit pattern (a pixel +// matches transparent IFF all 4 plane bits match transparent's 4 +// bits = OR of 4 XOR'd bytes is 0 in that bit). Then for each plane, +// dst = (dst & ~mask) | (src & mask) -- copy src bits at mask-set +// positions, preserve dst bits elsewhere. +static inline __attribute__((always_inline)) void x68kTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparent) { + X68kPlanarT *dstPd; + X68kPlanarT *srcPd; + uint8_t transBitByte[X68K_BITPLANES]; + uint16_t i; + uint8_t row; + uint16_t srcByteOff; + uint16_t dstByteOff; + uint8_t srcPlaneBytes[X68K_BITPLANES]; + uint8_t maskByte; + + dstPd = (X68kPlanarT *)dst->portData; + srcPd = (X68kPlanarT *)src->portData; + if (dstPd == NULL || srcPd == NULL) { + jlpGenericTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparent); + return; + } + transparent = (uint8_t)(transparent & 0x0Fu); + /* Per-plane "all bits set if transparent's bit at this plane is 1 + * else all 0" -- so XOR gives bit set where pixel differs from + * transparent in that plane. */ + for (i = 0; i < X68K_BITPLANES; i++) { + transBitByte[i] = ((transparent >> i) & 1u) ? 0xFFu : 0x00u; + } + + for (row = 0; row < 8u; row++) { + srcByteOff = (uint16_t)((uint16_t)srcBy * 8u + row) * X68K_BYTES_PER_ROW + srcBx; + dstByteOff = (uint16_t)((uint16_t)dstBy * 8u + row) * X68K_BYTES_PER_ROW + dstBx; + srcPlaneBytes[0] = srcPd->planes[0][srcByteOff]; + srcPlaneBytes[1] = srcPd->planes[1][srcByteOff]; + srcPlaneBytes[2] = srcPd->planes[2][srcByteOff]; + srcPlaneBytes[3] = srcPd->planes[3][srcByteOff]; + /* maskByte: bit set where pixel differs from transparent in + * ANY plane -- i.e., where the pixel is non-transparent. */ + maskByte = (uint8_t)((srcPlaneBytes[0] ^ transBitByte[0]) + | (srcPlaneBytes[1] ^ transBitByte[1]) + | (srcPlaneBytes[2] ^ transBitByte[2]) + | (srcPlaneBytes[3] ^ transBitByte[3])); + for (i = 0; i < X68K_BITPLANES; i++) { + uint8_t existing = dstPd->planes[i][dstByteOff]; + dstPd->planes[i][dstByteOff] = (uint8_t)((existing & (uint8_t)~maskByte) + | (srcPlaneBytes[i] & maskByte)); + } + } +} + + +// Phase 9 plane-to-chunky derivation for jlTileSnap. Reads 8 plane +// bytes (1 byte per row x 8 rows) from each of 4 planes for the +// 8-pixel-aligned tile column at bx, then assembles 32 chunky bytes +// (4 per row x 8 rows, packed 2 px/byte high-then-low nibble) into +// On Amiga, jlTileT.pixels is opaque port-specific storage (cross- +// platform tile.c never reads it directly when s->pixels is NULL). +// We use those 32 bytes as 4 planes x 8 rows, plane-major: +// bytes [0..7] = plane 0, rows 0..7 +// bytes [8..15] = plane 1, rows 0..7 +// bytes [16..23] = plane 2, rows 0..7 +// bytes [24..31] = plane 3, rows 0..7 +// snap/paste then become 32 plain byte loads + stores -- no chunky +// <-> planar conversion at all. The previous c2p-based path paid +// 4 KB LUT lookups + bit shuffling per pixel; this is ~50x cheaper. +#define AMIGA_TILE_PLANE_STRIDE 8 +static inline __attribute__((always_inline)) void x68kTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyOut) { + X68kPlanarT *pd; + uint16_t rowBase; + uint8_t plane; + + pd = (X68kPlanarT *)src->portData; + if (pd == NULL) { + jlpGenericTileSnap(src, bx, by, chunkyOut); + return; + } + rowBase = (uint16_t)((uint16_t)by * 8u) * X68K_BYTES_PER_ROW + bx; + for (plane = 0; plane < X68K_BITPLANES; plane++) { + const uint8_t *p = pd->planes[plane] + rowBase; + uint8_t *q = chunkyOut + plane * AMIGA_TILE_PLANE_STRIDE; + q[0] = p[AMIGA_TILE_ROW_OFF(0)]; + q[1] = p[AMIGA_TILE_ROW_OFF(1)]; + q[2] = p[AMIGA_TILE_ROW_OFF(2)]; + q[3] = p[AMIGA_TILE_ROW_OFF(3)]; + q[4] = p[AMIGA_TILE_ROW_OFF(4)]; + q[5] = p[AMIGA_TILE_ROW_OFF(5)]; + q[6] = p[AMIGA_TILE_ROW_OFF(6)]; + q[7] = p[AMIGA_TILE_ROW_OFF(7)]; + } +} + + +static inline __attribute__((always_inline)) void x68kTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile) { + X68kPlanarT *pd; + uint8_t plane; + uint16_t rowBase; + + pd = (X68kPlanarT *)dst->portData; + if (pd == NULL) { + jlpGenericTilePaste(dst, bx, by, chunkyTile); + return; + } + /* jlTileT.pixels[] holds plane-major planar bytes (see + * jlpTileSnap header for layout). Paste = 32 byte stores + * with no chunky -> planar conversion. */ + rowBase = (uint16_t)((uint16_t)by * 8u) * X68K_BYTES_PER_ROW + bx; + for (plane = 0; plane < X68K_BITPLANES; plane++) { + const uint8_t *q = chunkyTile + plane * AMIGA_TILE_PLANE_STRIDE; + uint8_t *p = pd->planes[plane] + rowBase; + p[AMIGA_TILE_ROW_OFF(0)] = q[0]; + p[AMIGA_TILE_ROW_OFF(1)] = q[1]; + p[AMIGA_TILE_ROW_OFF(2)] = q[2]; + p[AMIGA_TILE_ROW_OFF(3)] = q[3]; + p[AMIGA_TILE_ROW_OFF(4)] = q[4]; + p[AMIGA_TILE_ROW_OFF(5)] = q[5]; + p[AMIGA_TILE_ROW_OFF(6)] = q[6]; + p[AMIGA_TILE_ROW_OFF(7)] = q[7]; + } +} + + +// Whole-map paste engine (NATIVE-PERF W3 item 9). jlTileMapPaste's +// shared whole-map arm calls this ONCE per map: the portData load + +// NULL test, the by*320 row-base derive, and the four plane base +// pointers are hoisted out of the per-cell loop -- the per-cell +// x68kTilePaste route reloads portData, rebuilds the cell offset, +// and re-reads pd->planes[0..3] from the struct every cell (~172 of +// its ~776 cyc/cell, objdump-measured). The four dst pointers stay +// resident in a0-a3 (one byte per column, one row stride per tile +// row), so each cell is 32 move.b off compile-time displacements +// with zero per-plane setup. Plane order, row order, and written +// values are identical to per-cell x68kTilePaste -- pixel output is +// byte-identical. movep is unusable here (plane bytes sit 8000 bytes +// apart across separate AllocMem planes, vs the ST's interleaved +2 +// layout), so C is the right weapon on this port. +#define AMIGA_TILE_MAP_ROW_STRIDE ((uint16_t)(TILE_PIXELS_PER_SIDE * X68K_BYTES_PER_ROW)) + +#define AMIGA_MAP_CELL_PLANE(_dp, _plane) \ + do { \ + (_dp)[AMIGA_TILE_ROW_OFF(0)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 0]; \ + (_dp)[AMIGA_TILE_ROW_OFF(1)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 1]; \ + (_dp)[AMIGA_TILE_ROW_OFF(2)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 2]; \ + (_dp)[AMIGA_TILE_ROW_OFF(3)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 3]; \ + (_dp)[AMIGA_TILE_ROW_OFF(4)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 4]; \ + (_dp)[AMIGA_TILE_ROW_OFF(5)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 5]; \ + (_dp)[AMIGA_TILE_ROW_OFF(6)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 6]; \ + (_dp)[AMIGA_TILE_ROW_OFF(7)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 7]; \ + } while (0) + +static inline __attribute__((always_inline)) void x68kTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uint8_t hTiles, const jlTileT *tiles, const uint8_t *map) { + X68kPlanarT *pd; + const uint8_t *tileBytes; + const uint8_t *src; + uint8_t *p0; + uint8_t *p1; + uint8_t *p2; + uint8_t *p3; + uint16_t rowBase; + uint16_t rewind; + uint8_t idx; + uint8_t ty; + uint8_t tx; + + tileBytes = (const uint8_t *)tiles; + pd = (X68kPlanarT *)dst->portData; + if (pd == NULL) { + for (ty = 0; ty < hTiles; ty++) { + for (tx = 0; tx < wTiles; tx++) { + idx = map[tx]; + if (idx != TILE_MAP_SKIP) { + jlpGenericTilePaste(dst, (uint8_t)(bx + tx), (uint8_t)(by + ty), tileBytes + ((uint16_t)idx << 5)); + } + } + map += wTiles; + } + return; + } + rowBase = (uint16_t)((uint16_t)by * AMIGA_TILE_MAP_ROW_STRIDE + bx); + rewind = (uint16_t)(AMIGA_TILE_MAP_ROW_STRIDE - wTiles); + p0 = pd->planes[0] + rowBase; + p1 = pd->planes[1] + rowBase; + p2 = pd->planes[2] + rowBase; + p3 = pd->planes[3] + rowBase; + for (ty = hTiles; ty != 0u; ty--) { + for (tx = wTiles; tx != 0u; tx--) { + idx = *map++; + if (idx != TILE_MAP_SKIP) { + src = tileBytes + ((uint16_t)idx << 5); + AMIGA_MAP_CELL_PLANE(p0, 0); + AMIGA_MAP_CELL_PLANE(p1, 1); + AMIGA_MAP_CELL_PLANE(p2, 2); + AMIGA_MAP_CELL_PLANE(p3, 3); + } + p0++; + p1++; + p2++; + p3++; + } + p0 += rewind; + p1 += rewind; + p2 += rewind; + p3 += rewind; + } +} +#undef AMIGA_MAP_CELL_PLANE +#undef AMIGA_TILE_MAP_ROW_STRIDE + + +// Planar monochrome paste. monoTile follows the cross-port mono +// contract (include/joey/tile.h + jlpGenericTilePasteMono): 32 chunky +// nibble-pair bytes, row-major, TILE_BYTES_PER_ROW bytes per row, two +// pixels per byte. A pixel renders fgColor when its source nibble is +// nonzero, bgColor when zero; the HIGH nibble is the LEFT pixel. Each +// row's 4 source bytes fold into an 8-bit shape mask (bit 7 = +// leftmost pixel, matching the planar bit order), then each plane k +// writes +// outPlaneK = (shape & maskFgK) | (~shape & maskBgK) +// where maskXK = $FF if (X & (1 << k)) else $00. +static inline __attribute__((always_inline)) void x68kTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) { + X68kPlanarT *pd; + uint8_t shape[TILE_PIXELS_PER_SIDE]; + uint8_t plane; + uint8_t row; + uint8_t col; + uint16_t rowBase; + + pd = (X68kPlanarT *)dst->portData; + if (pd == NULL) { + jlpGenericTilePasteMono(dst, bx, by, monoTile, fgColor, bgColor); + return; + } + // Fold each chunky nibble-pair row into one 1bpp shape byte. + for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) { + uint8_t bits = 0u; + for (col = 0; col < TILE_BYTES_PER_ROW; col++) { + uint8_t srcByte = *monoTile++; + bits = (uint8_t)(bits << 2); + if (srcByte & 0xF0u) { + bits = (uint8_t)(bits | 0x02u); + } + if (srcByte & 0x0Fu) { + bits = (uint8_t)(bits | 0x01u); + } + } + shape[row] = bits; + } + rowBase = (uint16_t)((uint16_t)by * 8u) * X68K_BYTES_PER_ROW + bx; + for (plane = 0; plane < X68K_BITPLANES; plane++) { + uint8_t maskFg = (uint8_t)((fgColor & (1u << plane)) ? 0xFFu : 0x00u); + uint8_t maskBg = (uint8_t)((bgColor & (1u << plane)) ? 0xFFu : 0x00u); + uint8_t *p = pd->planes[plane] + rowBase; + for (row = 0; row < 8u; row++) { + uint8_t s = shape[row]; + p[row * X68K_BYTES_PER_ROW] = (uint8_t)((uint8_t)(s & maskFg) | (uint8_t)((uint8_t)(~s) & maskBg)); + } + } +} + +#endif diff --git a/tools/xdftool.py b/tools/xdftool.py new file mode 100755 index 0000000..99f8bad --- /dev/null +++ b/tools/xdftool.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +"""xdftool - read/write Human68k FAT12 floppy images (.XDF) from Linux. + +Why this exists: mtools cannot handle these images. Human68k 2HD floppies use +1024-byte logical sectors, and mformat mis-sizes the volume while mcopy fails +with rc=1 regardless of configuration. imgtool has no Human68k module. So the +JoeyLib X68000 gate needs its own reader/writer to get a test binary onto a +disk and its log back off again. + +The filesystem itself is ordinary FAT12; only the sector size is unusual, and +every offset in the BPB is already expressed in sectors, so nothing needs +special-casing beyond reading bytesPerSector rather than assuming 512. + +This does NOT create bootable images. Mint one once by running Human68k's own +FORMAT.X/SYS.X inside an emulator (they drive the FDD through IOCS), keep it as +a template, and copy it per run -- the same pattern the IIgs gate uses with +gsos-system.po. + +Usage: + xdftool.py list + xdftool.py add [name] + xdftool.py delete + xdftool.py extract + xdftool.py free +""" + +import os +import struct +import sys + +DIR_ENTRY_SIZE = 32 +ATTR_VOLUME = 0x08 +ATTR_DIR = 0x10 +ATTR_ARCHIVE = 0x20 +FREE_MARKER = 0x00 +DELETED_MARKER = 0xE5 +EOC_MIN = 0xFF8 # >= this in a FAT12 entry means end-of-chain + + +class Xdf: + def __init__(self, path, writable=False): + self.path = path + self.writable = writable + with open(path, "rb") as fp: + self.data = bytearray(fp.read()) + self._parse_bpb() + + def _parse_bpb(self): + (self.bps, self.spc, self.reserved, self.nfats, self.root_entries, + self.total_sectors, self.media, self.spf, self.spt, self.heads, + self.hidden) = struct.unpack("> 4) if (cluster & 1) else (pair & 0x0FFF) + + def fat_set(self, cluster, value): + # Mirror into every FAT copy so the image stays self-consistent. + for fat in range(self.nfats): + base = self.fat_start + fat * self.spf * self.bps + off = base + (cluster * 3) // 2 + pair = self.data[off] | (self.data[off + 1] << 8) + if cluster & 1: + pair = (pair & 0x000F) | ((value & 0x0FFF) << 4) + else: + pair = (pair & 0xF000) | (value & 0x0FFF) + self.data[off] = pair & 0xFF + self.data[off + 1] = (pair >> 8) & 0xFF + + def free_clusters(self): + return [c for c in range(2, self.max_cluster + 1) if self.fat_get(c) == 0] + + def cluster_offset(self, cluster): + return self.data_start + (cluster - 2) * self.cluster_bytes + + # ----- Directory ----- + + def _entries(self): + for i in range(self.root_entries): + off = self.root_start + i * DIR_ENTRY_SIZE + yield i, off, self.data[off:off + DIR_ENTRY_SIZE] + + def listdir(self): + out = [] + for _, _, e in self._entries(): + if e[0] == FREE_MARKER: + break + if e[0] == DELETED_MARKER or (e[11] & (ATTR_VOLUME | ATTR_DIR)): + continue + name = e[0:8].decode("ascii", "replace").rstrip() + ext = e[8:11].decode("ascii", "replace").rstrip() + out.append({ + "name": f"{name}.{ext}" if ext else name, + "cluster": struct.unpack(" 8 or len(ext) > 3: + raise ValueError(f"'{name}' does not fit 8.3") + return stem.ljust(8).encode("ascii") + ext.ljust(3).encode("ascii") + + def find(self, name): + # Human68k PRESERVES filename case in the directory entry (unlike + # MS-DOS, which upcases), so a program writing "joeylog.txt" leaves it + # lowercase on disk. Match case-insensitively or extracting a + # guest-written file fails with a confusing "not in image". + want = self.encode_name(name).upper() + for i, off, e in self._entries(): + if e[0] == FREE_MARKER: + break + if e[0] == DELETED_MARKER: + continue + if bytes(e[0:11]).upper() == want: + return i, off, e + return None, None, None + + # ----- File operations ----- + + def read_file(self, name): + _, _, e = self.find(name) + if e is None: + raise FileNotFoundError(f"{name} not in {self.path}") + size = struct.unpack(" self.max_cluster: + raise ValueError(f"{name}: cluster chain loops") + return bytes(out[:size]) + + def delete(self, name): + _, off, e = self.find(name) + if e is None: + return False + cluster = struct.unpack("9} clus={f['cluster']}") + return 0 + + if cmd == "free": + img = Xdf(image) + n = len(img.free_clusters()) + print(f"{n * img.cluster_bytes} bytes free ({n} clusters)") + return 0 + + if cmd == "add": + if len(argv) < 4: + print("usage: xdftool.py add [name]", file=sys.stderr) + return 2 + host = argv[3] + name = argv[4] if len(argv) > 4 else os.path.basename(host) + img = Xdf(image, writable=True) + with open(host, "rb") as fp: + img.add_file(name, fp.read()) + img.flush() + print(f"added {name} ({os.path.getsize(host)} bytes) to {image}") + return 0 + + if cmd == "delete": + if len(argv) < 4: + print("usage: xdftool.py delete ", file=sys.stderr) + return 2 + img = Xdf(image, writable=True) + if not img.delete(argv[3]): + print(f"{argv[3]} not in {image}", file=sys.stderr) + return 1 + img.flush() + print(f"deleted {argv[3]} from {image}") + return 0 + + if cmd == "extract": + if len(argv) < 5: + print("usage: xdftool.py extract ", file=sys.stderr) + return 2 + img = Xdf(image) + with open(argv[4], "wb") as fp: + fp.write(img.read_file(argv[3])) + print(f"extracted {argv[3]} -> {argv[4]}") + return 0 + + print(f"unknown command '{cmd}'", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + sys.exit(main(sys.argv))