399 lines
14 KiB
C
399 lines
14 KiB
C
// 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
|
|
// (scripts/lib/serialStreamPeer.py), 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 <joey/serial.h>, which the
|
|
// umbrella <joey/joey.h> deliberately does NOT pull in.
|
|
|
|
#include <joey/joey.h>
|
|
#include <joey/serial.h>
|
|
|
|
#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[SURFACE_COLORS_PER_PALETTE];
|
|
uint16_t i;
|
|
|
|
for (i = 0u; i < 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, SURFACE_HEIGHT - 1, GREY_PALETTE);
|
|
#if DENSE_SCB_LINES > 0
|
|
{
|
|
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
|
uint16_t i;
|
|
uint16_t line;
|
|
|
|
jlPaletteGet(screen, GREY_PALETTE, colors);
|
|
for (i = 1u; i < 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) > SURFACE_WIDTH) {
|
|
w = (int16_t)(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(MOUSE_BUTTON_LEFT) ? MOUSE_FLAG_LEFT : 0u) |
|
|
(jlMouseDown(MOUSE_BUTTON_RIGHT) ? MOUSE_FLAG_RIGHT : 0u) |
|
|
(jlMouseDown(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. scripts/lib/serialStreamPeer.py 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;
|
|
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(KEY_ESCAPE)) {
|
|
break;
|
|
}
|
|
if (jlKeyPressed(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;
|
|
}
|