// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // Serial and mouse together: traffic both ways on the serial port while the // mouse is read and drawn, for a machine whose serial port and mouse share a // chip. The X68000 is one: its Z8530 SCC carries the RS-232C port on channel A // and the mouse on channel B, and the chip has ONE register pointer for both // channels, so a select-then-access register sequence on one side must never // be split by the other. scripts/verify-x68000-serial-mouse.sh runs this under // MAME with a host peer on the line and Lua moving the mouse, and checks that // neither side disturbs the other. // // Serial: the default port at LINE_BAUD (9600 unless built otherwise) 8N1, // both directions at once. // - Transmits an endless stream, byte k = streamByte(k, TX_STREAM_MASK), // STREAM_CHUNK bytes per loop. // - Checks the peer's endless stream in order, byte k = streamByte(k, // RX_STREAM_MASK), counting every byte that is not the one expected. // - Raises DTR, which is already up, DTR_WRITES_PER_LOOP times per loop. A // program raises it once; doing it constantly keeps the port's own // select-then-write register sequence in the path of both channels' // interrupts for the whole run. // SPACE pauses and resumes transmitting. The peer answers byte for byte // (tests/gate/serialStreamPeer.c), so a pause quiets the line both ways. // ESC quits. // // Mouse: a small pointer follows it. // // Built with -DDENSE_SCB_LINES=n, stage lines 0 .. n-1 alternate between two // palettes, a palette change on every one of them. A port that honours the SCB // by reloading its one hardware palette as the beam scans (the X68000's raster // interrupt) then holds that interrupt for the whole run of lines every frame, // and the serial checks show whether receive survives it. The default, 0, is // one palette and no changes. // // The bottom two stage rows are verification scanlines: one nibble per pixel, // high nibble first, behind a sentinel, for a harness that reads the // framebuffer back: // row 198 serial: 3C3C, status, received, bad, sent, short writes, first bad // row 199 mouse: 5A5A, present/left/right/middle, x, y (examples/keys' row) // The serial row is restamped at most every SERIAL_ROW_FRAMES frames, so a // reader that sees the same row on two consecutive frames knows it did not // catch a present half done. The mouse row changes only with the mouse. // // Serial is an OPT-IN add-on: it is reached through , which the // umbrella deliberately does NOT pull in. #include #include #ifndef LINE_BAUD #define LINE_BAUD 9600u #endif #ifndef DENSE_SCB_LINES #define DENSE_SCB_LINES 0u #endif #define LINE_DATA_BITS 8u #define LINE_STOP_BITS 1u #define STREAM_CHUNK 16u // bytes sent per loop, ~17 ms of line at 9600 #define READ_CHUNK 64u #define DTR_WRITES_PER_LOOP 16u #define RX_STREAM_MASK 0x00u // the peer's stream #define TX_STREAM_MASK 0xA5u // ours, so the two can never be mistaken #define BYTE_BITS 8 #define NO_BAD_INDEX 0xFFFFFFFFul #define COUNT_SATURATED 0xFFFFu #define STATUS_OPEN 0x0001u #define STATUS_PAUSED 0x0002u // Verification rows. Every field is a 16-bit word (4 nibbles) or a 32-bit // long (8 nibbles), laid end to end from the sentinel at x = 0. #define NIBBLE_BITS 4 #define NIBBLE_MASK 0x0Fu #define WORD_NIBBLES 4 #define LONG_NIBBLES 8 #define SERIAL_ROW_Y 198 #define SERIAL_ROW_SENTINEL 0x3C3Cu #define SERIAL_STATUS_X WORD_NIBBLES #define SERIAL_RECEIVED_X (SERIAL_STATUS_X + WORD_NIBBLES) #define SERIAL_BAD_X (SERIAL_RECEIVED_X + LONG_NIBBLES) #define SERIAL_SENT_X (SERIAL_BAD_X + WORD_NIBBLES) #define SERIAL_SHORT_X (SERIAL_SENT_X + LONG_NIBBLES) #define SERIAL_FIRST_BAD_X (SERIAL_SHORT_X + WORD_NIBBLES) #define SERIAL_ROW_FRAMES 4u #define MOUSE_ROW_Y 199 #define MOUSE_ROW_SENTINEL 0x5A5Au #define MOUSE_FLAGS_X WORD_NIBBLES #define MOUSE_XPOS_X (MOUSE_FLAGS_X + WORD_NIBBLES) #define MOUSE_YPOS_X (MOUSE_XPOS_X + WORD_NIBBLES) // The mouse flags word, one nibble per predicate, in examples/keys' order. #define MOUSE_FLAG_PRESENT 0x1000u #define MOUSE_FLAG_LEFT 0x0100u #define MOUSE_FLAG_RIGHT 0x0010u #define MOUSE_FLAG_MIDDLE 0x0001u #define MOUSE_FLAGS_NONE 0xFFFFu // never a real flags word: forces the first stamp #define POINTER_SIZE 4 #define COLOR_BACKGROUND 0 #define COLOR_POINTER 15 #define PALETTE_RED_SHIFT 8 #define PALETTE_GREEN_SHIFT 4 #define GREY_PALETTE 0u #define BLUE_PALETTE 1u // the dense SCB's other palette #define BLUE_FLOOR 0x00Fu // added to every colour but black typedef struct StreamStatsT { uint32_t received; // peer bytes read so far = index of the next one expected uint32_t sent; // our bytes accepted by jlSerialWrite uint32_t firstBad; // index of the first unexpected byte, NO_BAD_INDEX if none uint16_t bad; // unexpected bytes, saturating uint16_t shortWrites; // jlSerialWrite calls that took less than asked, saturating } StreamStatsT; typedef struct MouseRowT { int16_t x; int16_t y; uint16_t flags; } MouseRowT; typedef struct PointerT { int16_t x; int16_t y; bool drawn; } PointerT; static void buildPalette(jlSurfaceT *screen); static void buildScb(jlSurfaceT *screen); static void fillPointer(jlSurfaceT *screen, int16_t x, int16_t y, uint8_t color); static uint16_t mouseFlags(void); static void movePointer(jlSurfaceT *screen, PointerT *pointer); static void receiveStream(StreamStatsT *stats); static void sendStream(StreamStatsT *stats); static void stampNibbles(jlSurfaceT *screen, int16_t x, int16_t y, uint32_t value, int16_t nibbles); static void stampSerialRow(jlSurfaceT *screen, const StreamStatsT *stats, uint16_t status); static uint8_t streamByte(uint32_t index, uint8_t mask); static void updateMouseRow(jlSurfaceT *screen, MouseRowT *last); // A grey ramp, so every index the verification rows stamp is visible. static void buildPalette(jlSurfaceT *screen) { uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; uint16_t i; for (i = 0u; i < JL_SURFACE_COLORS_PER_PALETTE; i++) { colors[i] = (uint16_t)((i << PALETTE_RED_SHIFT) | (i << PALETTE_GREEN_SHIFT) | i); } jlPaletteSet(screen, GREY_PALETTE, colors); } // The grey palette everywhere, or DENSE_SCB_LINES lines alternating between // it and the same ramp in blue. static void buildScb(jlSurfaceT *screen) { jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, GREY_PALETTE); #if DENSE_SCB_LINES > 0 { uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; uint16_t i; uint16_t line; jlPaletteGet(screen, GREY_PALETTE, colors); for (i = 1u; i < JL_SURFACE_COLORS_PER_PALETTE; i++) { colors[i] = (uint16_t)(colors[i] | BLUE_FLOOR); } jlPaletteSet(screen, BLUE_PALETTE, colors); for (line = 1u; line < DENSE_SCB_LINES; line += 2u) { jlScbSet(screen, line, BLUE_PALETTE); } } #endif } // The pointer block at (x, y), clipped above the verification rows so it can // never draw over them: a harness samples those between frames. static void fillPointer(jlSurfaceT *screen, int16_t x, int16_t y, uint8_t color) { int16_t w; int16_t h; w = POINTER_SIZE; h = POINTER_SIZE; if ((int16_t)(x + w) > JL_SURFACE_WIDTH) { w = (int16_t)(JL_SURFACE_WIDTH - x); } if ((int16_t)(y + h) > SERIAL_ROW_Y) { h = (int16_t)(SERIAL_ROW_Y - y); } if (w <= 0 || h <= 0) { return; } jlFillRect(screen, x, y, (uint16_t)w, (uint16_t)h, color); } static uint16_t mouseFlags(void) { return (uint16_t)((jlMousePresent() ? MOUSE_FLAG_PRESENT : 0u) | (jlMouseDown(JL_MOUSE_BUTTON_LEFT) ? MOUSE_FLAG_LEFT : 0u) | (jlMouseDown(JL_MOUSE_BUTTON_RIGHT) ? MOUSE_FLAG_RIGHT : 0u) | (jlMouseDown(JL_MOUSE_BUTTON_MIDDLE) ? MOUSE_FLAG_MIDDLE : 0u)); } // Erase the pointer where it was and draw it where the mouse is now. static void movePointer(jlSurfaceT *screen, PointerT *pointer) { int16_t x; int16_t y; x = jlMouseX(); y = jlMouseY(); if (pointer->drawn && x == pointer->x && y == pointer->y) { return; } if (pointer->drawn) { fillPointer(screen, pointer->x, pointer->y, COLOR_BACKGROUND); } fillPointer(screen, x, y, COLOR_POINTER); pointer->x = x; pointer->y = y; pointer->drawn = true; } // Read everything that has arrived and check it against the peer's stream. static void receiveStream(StreamStatsT *stats) { uint8_t buf[READ_CHUNK]; uint16_t got; uint16_t i; jlSerialPoll(); got = jlSerialRead(buf, READ_CHUNK); while (got > 0u) { for (i = 0u; i < got; i++) { if (buf[i] != streamByte(stats->received, RX_STREAM_MASK)) { if (stats->firstBad == NO_BAD_INDEX) { stats->firstBad = stats->received; } if (stats->bad < COUNT_SATURATED) { stats->bad++; } } stats->received++; } got = jlSerialRead(buf, READ_CHUNK); } } // Send the next STREAM_CHUNK bytes of our stream. A short write resumes from // the first byte not taken, so the stream stays gapless either way. static void sendStream(StreamStatsT *stats) { uint8_t chunk[STREAM_CHUNK]; uint16_t sent; uint16_t i; for (i = 0u; i < STREAM_CHUNK; i++) { chunk[i] = streamByte(stats->sent + i, TX_STREAM_MASK); } sent = jlSerialWrite(chunk, STREAM_CHUNK); stats->sent += sent; if (sent < STREAM_CHUNK && stats->shortWrites < COUNT_SATURATED) { stats->shortWrites++; } } // Stamp `value` into `nibbles` consecutive pixels, high nibble first. static void stampNibbles(jlSurfaceT *screen, int16_t x, int16_t y, uint32_t value, int16_t nibbles) { int16_t i; int16_t shift; for (i = 0; i < nibbles; i++) { shift = (int16_t)(NIBBLE_BITS * (nibbles - 1 - i)); jlDrawPixel(screen, (int16_t)(x + i), y, (uint8_t)((value >> shift) & NIBBLE_MASK)); } } static void stampSerialRow(jlSurfaceT *screen, const StreamStatsT *stats, uint16_t status) { stampNibbles(screen, 0, SERIAL_ROW_Y, SERIAL_ROW_SENTINEL, WORD_NIBBLES); stampNibbles(screen, SERIAL_STATUS_X, SERIAL_ROW_Y, status, WORD_NIBBLES); stampNibbles(screen, SERIAL_RECEIVED_X, SERIAL_ROW_Y, stats->received, LONG_NIBBLES); stampNibbles(screen, SERIAL_BAD_X, SERIAL_ROW_Y, stats->bad, WORD_NIBBLES); stampNibbles(screen, SERIAL_SENT_X, SERIAL_ROW_Y, stats->sent, LONG_NIBBLES); stampNibbles(screen, SERIAL_SHORT_X, SERIAL_ROW_Y, stats->shortWrites, WORD_NIBBLES); stampNibbles(screen, SERIAL_FIRST_BAD_X, SERIAL_ROW_Y, stats->firstBad, LONG_NIBBLES); } // Byte `index` of a stream: the index's low byte XOR its next byte, XOR the // stream's mask. A lost or repeated byte throws every byte after it off its // expected value, where a plain counter would miss any gap that is a multiple // of 256. tests/gate/serialStreamPeer.c uses the same formula and masks. static uint8_t streamByte(uint32_t index, uint8_t mask) { return (uint8_t)(index ^ (index >> BYTE_BITS) ^ mask); } // Restamp the mouse row when the reported state has changed. static void updateMouseRow(jlSurfaceT *screen, MouseRowT *last) { int16_t x; int16_t y; uint16_t flags; x = jlMouseX(); y = jlMouseY(); flags = mouseFlags(); if (x == last->x && y == last->y && flags == last->flags) { return; } stampNibbles(screen, 0, MOUSE_ROW_Y, MOUSE_ROW_SENTINEL, WORD_NIBBLES); stampNibbles(screen, MOUSE_FLAGS_X, MOUSE_ROW_Y, flags, WORD_NIBBLES); stampNibbles(screen, MOUSE_XPOS_X, MOUSE_ROW_Y, (uint16_t)x, WORD_NIBBLES); stampNibbles(screen, MOUSE_YPOS_X, MOUSE_ROW_Y, (uint16_t)y, WORD_NIBBLES); last->x = x; last->y = y; last->flags = flags; } int main(void) { jlConfigT config; jlSerialConfigT line; jlSurfaceT *screen; StreamStatsT stats; MouseRowT mouseRow; PointerT pointer; uint16_t status; uint16_t rowFrame; uint16_t i; bool paused; config.codegenBytes = 0u; config.noAudio = true; if (!jlInit(&config)) { return 1; } screen = jlStageGet(); buildPalette(screen); buildScb(screen); jlSurfaceClear(screen, COLOR_BACKGROUND); line.baud = LINE_BAUD; line.dataBits = LINE_DATA_BITS; line.stopBits = LINE_STOP_BITS; line.parity = JL_SERIAL_PARITY_NONE; line.flow = JL_SERIAL_FLOW_NONE; line.unit = 0u; status = jlSerialOpen(JL_SERIAL_DEFAULT, &line) ? STATUS_OPEN : 0u; stats.received = 0ul; stats.sent = 0ul; stats.firstBad = NO_BAD_INDEX; stats.bad = 0u; stats.shortWrites = 0u; mouseRow.x = -1; mouseRow.y = -1; mouseRow.flags = MOUSE_FLAGS_NONE; pointer.x = 0; pointer.y = 0; pointer.drawn = false; paused = false; jlInputPoll(); stampSerialRow(screen, &stats, status); updateMouseRow(screen, &mouseRow); jlStagePresent(); rowFrame = jlFrameCount(); for (;;) { jlInputPoll(); if (jlKeyPressed(JL_KEY_ESCAPE)) { break; } if (jlKeyPressed(JL_KEY_SPACE)) { paused = !paused; } receiveStream(&stats); if (!paused) { sendStream(&stats); } for (i = 0u; i < DTR_WRITES_PER_LOOP; i++) { jlSerialSetDtr(true); } movePointer(screen, &pointer); updateMouseRow(screen, &mouseRow); if ((uint16_t)(jlFrameCount() - rowFrame) >= SERIAL_ROW_FRAMES) { rowFrame = jlFrameCount(); stampSerialRow(screen, &stats, (uint16_t)(status | (paused ? STATUS_PAUSED : 0u))); } jlStagePresent(); } jlSerialClose(); jlShutdown(); return 0; }