joeylib2/examples/sermouse/sermouse.c
2026-10-13 14:01:46 -05:00

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