Sharp X68000 target added.
This commit is contained in:
parent
fe14bb6370
commit
2c54e60824
37 changed files with 5285 additions and 61 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
37
make/dos.mk
37
make/dos.mk
|
|
@ -133,7 +133,7 @@ STAXI_ASSET_DSTS += $(STAXI_SPC_RUN)
|
|||
|
||||
MKSTLEVEL_BIN := $(REPO_DIR)/build/tools/mkstlevel
|
||||
|
||||
.PHONY: all dos clean-dos
|
||||
.PHONY: all dos clean-dos clean
|
||||
all dos: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(STAXI_ASSET_DSTS)
|
||||
|
||||
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
|
||||
|
|
@ -168,77 +168,77 @@ $(LIBXMP_AR): $(LIBXMP_OBJS)
|
|||
@mkdir -p $(dir $@)
|
||||
$(DOS_AR) rcs $@ $^
|
||||
|
||||
$(HELLO_BIN): $(HELLO_SRC) $(LIB)
|
||||
$(HELLO_BIN): $(HELLO_SRC) $(LIB) $(LIBXMP_AR)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(PATTERN_BIN): $(PATTERN_SRC) $(LIB)
|
||||
$(PATTERN_BIN): $(PATTERN_SRC) $(LIB) $(LIBXMP_AR)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(DRAW_BIN): $(DRAW_SRC) $(LIB)
|
||||
$(DRAW_BIN): $(DRAW_SRC) $(LIB) $(LIBXMP_AR)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(KEYS_BIN): $(KEYS_SRC) $(LIB)
|
||||
$(KEYS_BIN): $(KEYS_SRC) $(LIB) $(LIBXMP_AR)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(SERIAL_BIN): $(SERIAL_SRC) $(LIB)
|
||||
$(SERIAL_BIN): $(SERIAL_SRC) $(LIB) $(LIBXMP_AR)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(SERTEST_BIN): $(SERTEST_SRC) $(LIB)
|
||||
$(SERTEST_BIN): $(SERTEST_SRC) $(LIB) $(LIBXMP_AR)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(SAVE_BIN): $(SAVE_SRC) $(LIB)
|
||||
$(SAVE_BIN): $(SAVE_SRC) $(LIB) $(LIBXMP_AR)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(JOY_BIN): $(JOY_SRC) $(LIB)
|
||||
$(JOY_BIN): $(JOY_SRC) $(LIB) $(LIBXMP_AR)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(SPRITE_BIN): $(SPRITE_SRC) $(LIB)
|
||||
$(SPRITE_BIN): $(SPRITE_SRC) $(LIB) $(LIBXMP_AR)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(AUDIO_BIN): $(AUDIO_SRC) $(LIB)
|
||||
$(AUDIO_BIN): $(AUDIO_SRC) $(LIB) $(LIBXMP_AR)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(UBER_BIN): $(UBER_SRC) $(LIB)
|
||||
$(UBER_BIN): $(UBER_SRC) $(LIB) $(LIBXMP_AR)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(ADV_BIN): $(ADV_SRC) $(LIB)
|
||||
$(ADV_BIN): $(ADV_SRC) $(LIB) $(LIBXMP_AR)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(ADV2_BIN): $(ADV2_SRC) $(LIB)
|
||||
$(ADV2_BIN): $(ADV2_SRC) $(LIB) $(LIBXMP_AR)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(AGI_BIN): $(AGI_SRCS) $(LIB)
|
||||
$(AGI_BIN): $(AGI_SRCS) $(LIB) $(LIBXMP_AR)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $(AGI_SRCS) $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(STAXI_BIN): $(STAXI_SRCS) $(LIB)
|
||||
$(STAXI_BIN): $(STAXI_SRCS) $(LIB) $(LIBXMP_AR)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
|
@ -303,6 +303,11 @@ $(DATA_DIR)/test.sfx: $(REPO_DIR)/assets/test.sfx
|
|||
clean-dos:
|
||||
rm -rf $(BUILD)
|
||||
|
||||
# `clean` as an alias for `clean-dos`. Without it a bare
|
||||
# `make -f make/dos.mk clean` FAILS -- and with stderr discarded that
|
||||
# looks like success while leaving stale objects the next build links.
|
||||
clean: clean-dos
|
||||
|
||||
# Nuke regenerable Space Taxi artifacts. Sources under
|
||||
# examples/spacetaxi/assets/ are untouched; everything under
|
||||
# generated/ is recreated by the next build.
|
||||
|
|
|
|||
35
make/iigs.mk
35
make/iigs.mk
|
|
@ -78,7 +78,7 @@ NTP_BIN := $(BUILD)/audio/ntpplayer.bin
|
|||
NTP_ASM := $(BUILD)/audio/ntpdata.s
|
||||
IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32
|
||||
|
||||
.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-verify iigs-verify-all iigs-verify-save clean-iigs
|
||||
.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-verify iigs-verify-all iigs-verify-save iigs-verify-shrtail clean-iigs clean
|
||||
|
||||
# Default: compile-check the library + run the end-to-end smoke test.
|
||||
all iigs: iigs-lib iigs-clang-smoke
|
||||
|
|
@ -129,6 +129,34 @@ iigs-verify: iigs-disk
|
|||
iigs-verify-all: iigs-disk
|
||||
$(REPO_DIR)/scripts/verify-iigs-all.sh
|
||||
|
||||
# Gate the 712 display-state bytes ABOVE the pixel block: the SCB array
|
||||
# ($E1:9D00-$9DC7) and all 16 palettes ($E1:9E00-$9FFF). Nothing else in the
|
||||
# tree can see them -- the golden hash covers the STAGE, not $E1, and P9's
|
||||
# pixel compare stops at $9CFF -- so a wrong palette is invisible to every
|
||||
# other gate and shows only as wrong colors on a real screen. That blind spot
|
||||
# is why a phantom "PEI-slam corrupts palettes 1-15" note survived five weeks.
|
||||
#
|
||||
# UBER's shrTailUpload / shrTailSlam rows live behind #ifdef UBER_PROBE, which
|
||||
# no normal build defines, so this target builds its own UBER (never the
|
||||
# goldened one) into a scratch dir, packs a single-app disk, and benches that.
|
||||
# -DUBER_FRAMES=1 shrinks the timing window: a -DUBER_PROBE run at the golden
|
||||
# 16 frames does not fit bench-iigs.sh's MAME cap. The goldened UBER binary,
|
||||
# joey.2mg and joeylog.txt are all left untouched.
|
||||
IIGS_PROBE_DIR := $(BUILD)/probe
|
||||
|
||||
iigs-verify-shrtail: $(LIB) $(NTP_BIN)
|
||||
@mkdir -p $(IIGS_PROBE_DIR)/bin
|
||||
$(IIGS_CLANG_BUILD) -DUBER_PROBE -DUBER_FRAMES=1 $(INCLUDES) -o $(IIGS_PROBE_DIR)/bin/UBER $(UBER_SRC) $(LIB)
|
||||
BINDIR="$(IIGS_PROBE_DIR)/bin" NTP_BIN="$(NTP_BIN)" JOEY_DISK_EXAMPLES="UBER" \
|
||||
$(REPO_DIR)/scripts/make-iigs-disk.sh $(IIGS_PROBE_DIR)/uberprobe.2mg
|
||||
BENCH_DATA_DISK="$(IIGS_PROBE_DIR)/uberprobe.2mg" BENCH_OUT_LOG="$(IIGS_PROBE_DIR)/probelog.txt" \
|
||||
BENCH_MAX_FRAMES="$(or $(PROBE_MAX_FRAMES),45000)" $(REPO_DIR)/scripts/bench-iigs.sh
|
||||
@grep -E 'shrParity|shrTailUpload|shrTailSlam' $(IIGS_PROBE_DIR)/probelog.txt || \
|
||||
{ echo "iigs-verify-shrtail: FAIL - no probe rows captured" >&2; exit 1; }
|
||||
@grep -qE 'shrTail(Upload|Slam) FAIL' $(IIGS_PROBE_DIR)/probelog.txt && \
|
||||
{ echo "iigs-verify-shrtail: FAIL - \$$E1 SCB/palette block mismatched the stage" >&2; exit 1; } || true
|
||||
@echo "iigs-verify-shrtail: PASS"
|
||||
|
||||
# Runtime gate for the save HAL's GS/OS calls (Create + GetDevNumber/DInfo/
|
||||
# Volume): boots SAVE under MAME and checks the save-OK lamp + disk-free bar.
|
||||
iigs-verify-save: $(BINDIR)/SAVE $(BINDIR)/DRAW
|
||||
|
|
@ -247,4 +275,9 @@ $(BINDIR)/AUDIO: $(AUDIO_SRC) $(LIB) $(NTP_BIN) $(IIGS_CLANG_BUILD)
|
|||
clean-iigs:
|
||||
rm -rf $(BUILD)
|
||||
|
||||
# `clean` as an alias for `clean-iigs`. Without it a bare
|
||||
# `make -f make/iigs.mk clean` FAILS -- and with stderr discarded that
|
||||
# looks like success while leaving stale objects the next build links.
|
||||
clean: clean-iigs
|
||||
|
||||
-include $(wildcard $(DEP_DIR)/*.d)
|
||||
|
|
|
|||
165
make/x68000.mk
Normal file
165
make/x68000.mk
Normal 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)
|
||||
276
patches/mame-0.264-x68k-crtc-timing.patch
Normal file
276
patches/mame-0.264-x68k-crtc-timing.patch
Normal 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;
|
||||
}
|
||||
53
patches/mame-0.264-x68k-rs232.patch
Normal file
53
patches/mame-0.264-x68k-rs232.patch
Normal 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);
|
||||
141
patches/mame-0.264-x68k-waitstates.patch
Normal file
141
patches/mame-0.264-x68k-waitstates.patch
Normal 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;
|
||||
511
patches/mame-x68k-upstream-ab6b6fc9e797-serial-mouse.patch
Normal file
511
patches/mame-x68k-upstream-ab6b6fc9e797-serial-mouse.patch
Normal 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
|
||||
33
patches/x68k-boot-check.lua
Normal file
33
patches/x68k-boot-check.lua
Normal 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)
|
||||
71
patches/x68k-crtc-probe.lua
Normal file
71
patches/x68k-crtc-probe.lua
Normal 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)
|
||||
82
patches/x68k-wait-probe.lua
Normal file
82
patches/x68k-wait-probe.lua
Normal 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)
|
||||
|
|
@ -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}"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
81
scripts/verify-x68000-golden.sh
Executable 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
54
scripts/verify-x68000-serial.sh
Executable 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
94
scripts/x68kSerialPeer.py
Executable 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())
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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
242
src/x68000/audio.c
Normal 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
317
src/x68000/audioPcm.c
Normal 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
378
src/x68000/hal.c
Normal 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
1346
src/x68000/halPlanar.c
Normal file
File diff suppressed because it is too large
Load diff
53
src/x68000/save.c
Normal file
53
src/x68000/save.c
Normal 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
116
src/x68000/serial.c
Normal 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
121
src/x68000/x68kPlanar.h
Normal 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
352
src/x68000/x68kTile.h
Normal 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
270
tools/xdftool.py
Executable 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))
|
||||
Loading…
Add table
Reference in a new issue