Sharp X68000 target added.

This commit is contained in:
Scott Duensing 2026-08-05 21:15:26 -05:00
parent fe14bb6370
commit 2c54e60824
37 changed files with 5285 additions and 61 deletions

View file

@ -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

View file

@ -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_<TARGET> 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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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.

View file

@ -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)

165
make/x68000.mk Normal file
View file

@ -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)

View file

@ -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;
}

View file

@ -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);

View file

@ -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;

View file

@ -0,0 +1,511 @@
From ab6b6fc9e7971b367ec54d565cf10f04e71109ec Mon Sep 17 00:00:00 2001
From: Patrick Mackinlay <pmackinlay@hotmail.com>
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<palette_device> m_pcgpalette;
required_device<mc68901_device> m_mfpdev;
required_device<rp5c15_device> m_rtc;
- required_device<scc8530_legacy_device> m_scc;
+ required_device<scc8530_device> m_scc;
required_device<ym2151_device> m_ym2151;
required_device<i8255_device> m_ppi;
required_device<screen_device> m_screen;
@@ -111,9 +108,6 @@ class x68k_state : public driver_device
required_device_array<filter_volume_device, 2> 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 <int N> void irq4_line(int state);
template <int N> 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 <algorithm>
+
+//#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<s16>(dx, -128, 127)));
+ transmit_byte(s8(std::clamp<s16>(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

View file

@ -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)

View file

@ -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)

View file

@ -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 <target>.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 <TARGET>.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)

View file

@ -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}"

View file

@ -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)"

81
scripts/verify-x68000-golden.sh Executable file
View file

@ -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=<dir with x68mame/> 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" <<LUA
local frame = 0
emu.register_frame_done(function()
frame = frame + 1
if frame % 20000 == 0 then io.write("GOLD f"..frame.."\n"); io.flush() end
if frame > ($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" </dev/null 2>&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"

54
scripts/verify-x68000-serial.sh Executable file
View file

@ -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 >/dev/null 2>&1
wait "$peer"
rc=$?
cat "$work/peer.out"
exit $rc

94
scripts/x68kSerialPeer.py Executable file
View file

@ -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())

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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));

View file

@ -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

View file

@ -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 =

View file

@ -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();
}

242
src/x68000/audio.c Normal file
View file

@ -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 <x68k/iocs.h>
#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));
}

317
src/x68000/audioPcm.c Normal file
View file

@ -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 <string.h>
#include <libxmp-lite/xmp.h>
#include <x68k/iocs.h>
#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;
}

378
src/x68000/hal.c Normal file
View file

@ -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 <x68k/dos.h>
#include <x68k/iocs.h>
#include "port.h"
#include <string.h>
#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;
}

1346
src/x68000/halPlanar.c Normal file

File diff suppressed because it is too large Load diff

53
src/x68000/save.c Normal file
View file

@ -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 <stdio.h>
#include <x68k/dos.h>
#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;
}

116
src/x68000/serial.c Normal file
View file

@ -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 <x68k/iocs.h>
#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;
}

121
src/x68000/x68kPlanar.h Normal file
View file

@ -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 <stdint.h>
#include <stdbool.h>
#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

352
src/x68000/x68kTile.h Normal file
View file

@ -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 <stdint.h>
#include <stdbool.h>
#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

270
tools/xdftool.py Executable file
View file

@ -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 <image>
xdftool.py add <image> <hostfile> [name]
xdftool.py delete <image> <name>
xdftool.py extract <image> <name> <hostfile>
xdftool.py free <image>
"""
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("<HBHBHHBHHHH", self.data[11:30])
if self.bps == 0 or self.spc == 0:
raise ValueError(f"{self.path}: implausible BPB (bytesPerSector={self.bps})")
self.fat_start = self.reserved * self.bps
self.root_start = (self.reserved + self.nfats * self.spf) * self.bps
self.data_start = self.root_start + self.root_entries * DIR_ENTRY_SIZE
self.cluster_bytes = self.spc * self.bps
# Clusters are numbered from 2, and the last valid one is bounded by
# how much space is actually left after the root directory.
self.max_cluster = 1 + (len(self.data) - self.data_start) // self.cluster_bytes
# ----- FAT12 entry access -----
def fat_get(self, cluster):
off = self.fat_start + (cluster * 3) // 2
pair = self.data[off] | (self.data[off + 1] << 8)
return (pair >> 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("<H", e[26:28])[0],
"size": struct.unpack("<I", e[28:32])[0],
"attr": e[11],
})
return out
@staticmethod
def encode_name(name):
name = name.upper()
stem, _, ext = name.partition(".")
if len(stem) > 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("<I", e[28:32])[0]
cluster = struct.unpack("<H", e[26:28])[0]
out = bytearray()
guard = 0
while 2 <= cluster < EOC_MIN and len(out) < size:
off = self.cluster_offset(cluster)
out += self.data[off:off + self.cluster_bytes]
cluster = self.fat_get(cluster)
guard += 1
if guard > 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("<H", e[26:28])[0]
while 2 <= cluster < EOC_MIN:
nxt = self.fat_get(cluster)
self.fat_set(cluster, 0)
cluster = nxt
self.data[off] = DELETED_MARKER
return True
def add_file(self, name, payload):
self.delete(name) # overwrite semantics
need = (len(payload) + self.cluster_bytes - 1) // self.cluster_bytes
free = self.free_clusters()
if len(free) < need:
raise OSError(f"{self.path}: need {need} clusters, {len(free)} free "
f"({len(free) * self.cluster_bytes} bytes)")
chain = free[:need]
for idx, cluster in enumerate(chain):
off = self.cluster_offset(cluster)
chunk = payload[idx * self.cluster_bytes:(idx + 1) * self.cluster_bytes]
self.data[off:off + len(chunk)] = chunk
# Zero the tail of the final cluster so stale bytes never leak.
if len(chunk) < self.cluster_bytes:
self.data[off + len(chunk):off + self.cluster_bytes] = \
bytes(self.cluster_bytes - len(chunk))
self.fat_set(cluster, 0xFFF if idx == need - 1 else chain[idx + 1])
slot = None
for i, off, e in self._entries():
if e[0] in (FREE_MARKER, DELETED_MARKER):
slot = off
break
if slot is None:
raise OSError(f"{self.path}: root directory full ({self.root_entries} entries)")
entry = bytearray(DIR_ENTRY_SIZE)
entry[0:11] = self.encode_name(name)
entry[11] = ATTR_ARCHIVE
# Fixed timestamp: reproducible images matter more than real mtimes,
# since these get byte-compared across gate runs.
struct.pack_into("<H", entry, 22, (12 << 11)) # 12:00:00
struct.pack_into("<H", entry, 24, ((2026 - 1980) << 9) | (1 << 5) | 1)
struct.pack_into("<H", entry, 26, chain[0] if need else 0)
struct.pack_into("<I", entry, 28, len(payload))
self.data[slot:slot + DIR_ENTRY_SIZE] = entry
def flush(self):
if not self.writable:
raise PermissionError("opened read-only")
with open(self.path, "wb") as fp:
fp.write(self.data)
def main(argv):
if len(argv) < 3:
print(__doc__.strip())
return 2
cmd, image = argv[1], argv[2]
if cmd == "list":
img = Xdf(image)
print(f"{image}: {len(img.data)} bytes, {img.bps}-byte sectors, "
f"{img.total_sectors} sectors, {img.spc} sec/cluster")
for f in img.listdir():
print(f" {f['name']:<14} {f['size']:>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 <image> <hostfile> [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 <image> <name>", 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 <image> <name> <hostfile>", 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))