415 lines
14 KiB
C
415 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
|
|
//
|
|
// Does a serial receiver survive the longest deaf window a JoeyLib program has?
|
|
//
|
|
// The host peer (scripts/verify-iigs-serial-loss.sh) waits for "READY" from
|
|
// this program, then streams SERLOSS_BYTES bytes whose value is the low byte of
|
|
// their index, paced by the emulated line at 9600 baud. Meanwhile every loop
|
|
// iteration dirties the whole stage and presents it -- a full-screen present is
|
|
// the longest stretch a program spends away from its own code -- and then reads
|
|
// whatever the port buffered, calling jlSerialPoll once per frame as the API
|
|
// asks. A receiver that fills its ring from an interrupt loses nothing; one that
|
|
// is drained only when the program gets around to it loses the bytes that
|
|
// arrive while the hardware FIFO is full.
|
|
//
|
|
// Build variants (the harness passes these with -D):
|
|
// SERLOSS_SLOT=n open a Super Serial Card in slot n instead of the modem port
|
|
// SERLOSS_CLEAR dirty the stage with jlSurfaceClear rather than jlFillRect
|
|
// SERLOSS_IDLE no drawing at all: only the interrupts the system itself takes
|
|
// SERLOSS_BAUD=n line rate (default 9600; the harness sets the peer to match)
|
|
// SERLOSS_JOY also poll the input (joystick included) once per frame and
|
|
// report the spread of the calibrated stick readings; the
|
|
// harness holds the paddles still (SERLOSS_PADDLE), so any
|
|
// spread is read jitter
|
|
// SERLOSS_SFX also keep a sound effect streaming and retrigger a one-shot
|
|
// effect once per frame, so NinjaTrackerPlus's entry calls and
|
|
// its DOC refill interrupt run throughout the receive; the
|
|
// stream's refills are counted, the proof audio is running
|
|
// SERLOSS_LOOSE drop the serial add-on's hold on the tight interrupt-latency
|
|
// bound once the port is open: the negative control, which
|
|
// must lose bytes wherever a masked stretch outlasts the FIFO
|
|
//
|
|
// Results go to a mailbox in the unused tail of the SHR SCB page, which the
|
|
// present never writes and the harness reads straight out of emulated memory.
|
|
|
|
#include <joey/joey.h>
|
|
#include <joey/serial.h>
|
|
|
|
#define MB_BASE ((volatile uint8_t *)0x00E19DD0L)
|
|
#define MB_SIG_0 0
|
|
#define MB_SIG_1 1
|
|
#define MB_PHASE 2
|
|
#define MB_RECEIVED 4
|
|
#define MB_IN_SEQUENCE 6
|
|
#define MB_BREAKS 8
|
|
#define MB_PRESENTS 10
|
|
#define MB_TICKS 12
|
|
#define MB_OVERRUNS 14
|
|
#define MB_DROPPED 16
|
|
#define MB_SUM 18
|
|
#define MB_NOW 20
|
|
#define MB_FILL_TICKS 22
|
|
#define MB_PRESENT_TICKS 24
|
|
#define MB_JOY_POLLS 26
|
|
#define MB_JOY_MIN_X 28 // stick readings, biased by MB_JOY_BIAS
|
|
#define MB_JOY_MAX_X 29
|
|
#define MB_JOY_MIN_Y 30
|
|
#define MB_JOY_MAX_Y 31
|
|
#define MB_SFX_SHOTS 32
|
|
#define MB_SFX_FILLS 34
|
|
#define MB_SIG_VALUE_0 0xA5u
|
|
#define MB_SIG_VALUE_1 0x5Au
|
|
#define MB_JOY_BIAS 128
|
|
|
|
#define PHASE_INIT 1u
|
|
#define PHASE_OPEN 2u
|
|
#define PHASE_READY 3u
|
|
#define PHASE_RECEIVING 4u
|
|
#define PHASE_DONE 5u
|
|
#define PHASE_FAILED 0xEEu
|
|
|
|
#ifndef SERLOSS_BAUD
|
|
#define SERLOSS_BAUD 9600u
|
|
#endif
|
|
#define SERLOSS_BYTES 4096u
|
|
#define IDLE_TICKS 120u // done once streaming has been silent for 2 s
|
|
#define START_TICKS 3600u // give up if nothing arrives in 60 s
|
|
#define READ_CHUNK 64u
|
|
#define BENCH_FRAMES 60u // fills (+ presents) timed before the port opens
|
|
#define SCREEN_W 320
|
|
#define SCREEN_H 200
|
|
|
|
// SERLOSS_SFX: a streamed effect on one slot plus a one-shot retriggered once
|
|
// per frame on another. The stream rate sets how often the replayer's DOC
|
|
// interrupt refills it (every 256 samples). The stream is raw (DOC-ready,
|
|
// 0x00-free bytes) and filled by jlMemCopy, as Space Taxi's speech is, so
|
|
// producing it costs the loop next to nothing.
|
|
#define SFX_STREAM_SLOT 4u
|
|
#define SFX_STREAM_RATE 22050u
|
|
#define SFX_SHOT_SLOT 3u
|
|
#define SFX_SHOT_RATE 11025u
|
|
#define SFX_SHOT_BYTES 2048u
|
|
#define SFX_SHOT_PERIOD 32u // one-shot square wave period, in samples
|
|
#define SFX_LEVEL 96 // one-shot square amplitude
|
|
#define SFX_WAVE_BYTES 256u // one period of the stream's sawtooth
|
|
|
|
// Slot I/O select: bit n routes slot n's $Cn00 space and I/O to the card rather
|
|
// than the built-in port (slot 2 defaults to the internal modem port, which
|
|
// shadows a card's 6551 at $C0A8).
|
|
#define SLTROMSEL ((volatile uint8_t *)0x00C02DL)
|
|
|
|
extern uint16_t gIigsRxOverruns;
|
|
extern uint16_t gIigsRxDropped;
|
|
#ifdef SERLOSS_LOOSE
|
|
extern void iigsIrqLatencyRelax(void);
|
|
#endif
|
|
|
|
#ifdef SERLOSS_JOY
|
|
typedef struct {
|
|
uint16_t polls;
|
|
int8_t minX;
|
|
int8_t maxX;
|
|
int8_t minY;
|
|
int8_t maxY;
|
|
} JoySpreadT;
|
|
#endif
|
|
|
|
#ifdef SERLOSS_SFX
|
|
static int8_t gSfxShot[SFX_SHOT_BYTES];
|
|
static uint8_t gSfxWave[SFX_WAVE_BYTES];
|
|
static uint16_t gSfxFills = 0u;
|
|
#endif
|
|
|
|
static uint16_t benchLoop(jlSurfaceT *stage, bool present);
|
|
#ifdef SERLOSS_JOY
|
|
static void joyPoll(JoySpreadT *spread);
|
|
#endif
|
|
static void mailbox16(uint16_t slot, uint16_t value);
|
|
static void publish(uint16_t received, uint16_t inSequence, uint16_t breaks, uint16_t presents, uint16_t ticks, uint16_t sum, uint16_t now);
|
|
#ifdef SERLOSS_SFX
|
|
static void sfxStart(void);
|
|
static uint32_t sfxStreamFill(void *ctx, int8_t *dst, uint32_t count);
|
|
#endif
|
|
|
|
|
|
// Ticks for BENCH_FRAMES full-stage fills, each followed by a present if asked: the difference
|
|
// between the two runs is what a full-screen present costs.
|
|
static uint16_t benchLoop(jlSurfaceT *stage, bool present) {
|
|
uint16_t start;
|
|
uint16_t frame;
|
|
|
|
start = jlFrameCount();
|
|
for (frame = 0u; frame < BENCH_FRAMES; frame++) {
|
|
jlFillRect(stage, 0, 0, SCREEN_W, SCREEN_H, (uint8_t)(frame & 0x0Fu));
|
|
if (present) {
|
|
jlStagePresent();
|
|
}
|
|
}
|
|
return (uint16_t)(jlFrameCount() - start);
|
|
}
|
|
|
|
|
|
#ifdef SERLOSS_JOY
|
|
// One input poll (the joystick read is the long part), folded into the spread and published. The
|
|
// stick was calibrated before the loop, so a still stick reads 0 and anything else is jitter.
|
|
static void joyPoll(JoySpreadT *spread) {
|
|
int8_t x;
|
|
int8_t y;
|
|
|
|
jlInputPoll();
|
|
x = jlJoystickX(JL_JOYSTICK_0);
|
|
y = jlJoystickY(JL_JOYSTICK_0);
|
|
if (spread->polls == 0u || x < spread->minX) {
|
|
spread->minX = x;
|
|
}
|
|
if (spread->polls == 0u || x > spread->maxX) {
|
|
spread->maxX = x;
|
|
}
|
|
if (spread->polls == 0u || y < spread->minY) {
|
|
spread->minY = y;
|
|
}
|
|
if (spread->polls == 0u || y > spread->maxY) {
|
|
spread->maxY = y;
|
|
}
|
|
spread->polls++;
|
|
mailbox16(MB_JOY_POLLS, spread->polls);
|
|
MB_BASE[MB_JOY_MIN_X] = (uint8_t)(spread->minX + MB_JOY_BIAS);
|
|
MB_BASE[MB_JOY_MAX_X] = (uint8_t)(spread->maxX + MB_JOY_BIAS);
|
|
MB_BASE[MB_JOY_MIN_Y] = (uint8_t)(spread->minY + MB_JOY_BIAS);
|
|
MB_BASE[MB_JOY_MAX_Y] = (uint8_t)(spread->maxY + MB_JOY_BIAS);
|
|
}
|
|
#endif
|
|
|
|
|
|
static void mailbox16(uint16_t slot, uint16_t value) {
|
|
MB_BASE[slot] = (uint8_t)(value & 0xFFu);
|
|
MB_BASE[slot + 1u] = (uint8_t)(value >> 8);
|
|
}
|
|
|
|
|
|
__attribute__((noinline)) static void publish(uint16_t received, uint16_t inSequence, uint16_t breaks, uint16_t presents, uint16_t ticks, uint16_t sum, uint16_t now) {
|
|
mailbox16(MB_RECEIVED, received);
|
|
mailbox16(MB_IN_SEQUENCE, inSequence);
|
|
mailbox16(MB_BREAKS, breaks);
|
|
mailbox16(MB_PRESENTS, presents);
|
|
mailbox16(MB_TICKS, ticks);
|
|
mailbox16(MB_OVERRUNS, gIigsRxOverruns);
|
|
mailbox16(MB_DROPPED, gIigsRxDropped);
|
|
mailbox16(MB_SUM, sum);
|
|
mailbox16(MB_NOW, now);
|
|
}
|
|
|
|
|
|
#ifdef SERLOSS_SFX
|
|
// Build the one-shot's square wave and the stream's sawtooth, and start the endless stream
|
|
// (jlInit started audio).
|
|
static void sfxStart(void) {
|
|
uint16_t i;
|
|
|
|
for (i = 0u; i < SFX_SHOT_BYTES; i++) {
|
|
gSfxShot[i] = (int8_t)(((i / (SFX_SHOT_PERIOD / 2u)) & 1u) != 0u ? SFX_LEVEL : -SFX_LEVEL);
|
|
}
|
|
for (i = 0u; i < SFX_WAVE_BYTES; i++) {
|
|
gSfxWave[i] = (uint8_t)(i == 0u ? 1u : i); // 0x00 would stop the DOC
|
|
}
|
|
// The first trigger of a sample converts it for the DOC (~15 ms for this one); do it before
|
|
// the port opens, so the loop's triggers are the cheap repeats and a slow first frame cannot
|
|
// overflow the receive ring.
|
|
jlAudioPlaySfx((uint8_t)SFX_SHOT_SLOT, (const uint8_t *)gSfxShot, SFX_SHOT_BYTES, SFX_SHOT_RATE);
|
|
jlAudioPlaySfxStreamRaw((uint8_t)SFX_STREAM_SLOT, sfxStreamFill, (void *)0, SFX_STREAM_RATE);
|
|
}
|
|
|
|
|
|
// Endless sawtooth, whole periods: never returns 0, so the stream runs until the program ends.
|
|
// Only a running engine asks for refills, so their count proves audio is up.
|
|
static uint32_t sfxStreamFill(void *ctx, int8_t *dst, uint32_t count) {
|
|
uint32_t done;
|
|
|
|
(void)ctx;
|
|
gSfxFills++;
|
|
mailbox16(MB_SFX_FILLS, gSfxFills);
|
|
done = 0u;
|
|
while (count - done >= SFX_WAVE_BYTES) {
|
|
jlMemCopy(dst + done, gSfxWave, SFX_WAVE_BYTES);
|
|
done += SFX_WAVE_BYTES;
|
|
}
|
|
return done;
|
|
}
|
|
#endif
|
|
|
|
|
|
int main(void) {
|
|
static const char ready[] = "READY\r\n";
|
|
jlConfigT config;
|
|
jlSerialConfigT cfg;
|
|
jlSurfaceT *stage;
|
|
uint8_t buf[READ_CHUNK];
|
|
uint16_t received;
|
|
uint16_t inSequence;
|
|
uint16_t breaks;
|
|
uint16_t presents;
|
|
uint16_t sum;
|
|
uint16_t lastTick;
|
|
uint16_t firstTick;
|
|
uint16_t now;
|
|
uint16_t got;
|
|
uint16_t i;
|
|
uint16_t n;
|
|
uint8_t expect;
|
|
uint8_t color;
|
|
bool started;
|
|
#if defined(SERLOSS_JOY) || defined(SERLOSS_SFX)
|
|
uint16_t frameTick;
|
|
#endif
|
|
#ifdef SERLOSS_JOY
|
|
JoySpreadT spread;
|
|
#endif
|
|
#ifdef SERLOSS_SFX
|
|
uint16_t shots;
|
|
#endif
|
|
|
|
config.codegenBytes = 0u;
|
|
#ifdef SERLOSS_SFX
|
|
config.noAudio = false;
|
|
#else
|
|
config.noAudio = true;
|
|
#endif
|
|
if (!jlInit(&config)) {
|
|
MB_BASE[MB_PHASE] = PHASE_FAILED;
|
|
for (;;) {
|
|
}
|
|
}
|
|
// Signed only after jlInit, which rewrites the SCB page this mailbox shares.
|
|
MB_BASE[MB_SIG_0] = MB_SIG_VALUE_0;
|
|
MB_BASE[MB_SIG_1] = MB_SIG_VALUE_1;
|
|
MB_BASE[MB_PHASE] = PHASE_INIT;
|
|
mailbox16(MB_JOY_POLLS, 0u);
|
|
mailbox16(MB_SFX_SHOTS, 0u);
|
|
mailbox16(MB_SFX_FILLS, 0u);
|
|
stage = jlStageGet();
|
|
mailbox16(MB_FILL_TICKS, benchLoop(stage, false));
|
|
mailbox16(MB_PRESENT_TICKS, benchLoop(stage, true));
|
|
#ifdef SERLOSS_SFX
|
|
sfxStart();
|
|
shots = 0u;
|
|
#endif
|
|
|
|
cfg.baud = SERLOSS_BAUD;
|
|
cfg.dataBits = 8u;
|
|
cfg.stopBits = 1u;
|
|
cfg.parity = JL_SERIAL_PARITY_NONE;
|
|
cfg.flow = JL_SERIAL_FLOW_NONE;
|
|
#ifdef SERLOSS_SLOT
|
|
cfg.unit = SERLOSS_SLOT;
|
|
*SLTROMSEL = (uint8_t)(*SLTROMSEL | (uint8_t)(1u << SERLOSS_SLOT));
|
|
started = jlSerialOpen(JL_SERIAL_SLOT, &cfg);
|
|
#else
|
|
cfg.unit = 0u;
|
|
started = jlSerialOpen(JL_SERIAL_MODEM, &cfg);
|
|
#endif
|
|
if (!started) {
|
|
MB_BASE[MB_PHASE] = PHASE_FAILED;
|
|
for (;;) {
|
|
}
|
|
}
|
|
#ifdef SERLOSS_LOOSE
|
|
iigsIrqLatencyRelax();
|
|
#endif
|
|
#ifdef SERLOSS_JOY
|
|
// Calibrate on the first poll, in the latency mode the rest will run in, so a still stick
|
|
// reads 0 from then on.
|
|
jlJoystickReset(JL_JOYSTICK_0, 0u);
|
|
spread.polls = 0u;
|
|
joyPoll(&spread);
|
|
#endif
|
|
MB_BASE[MB_PHASE] = PHASE_OPEN;
|
|
(void)jlSerialWrite((const uint8_t *)ready, (uint16_t)(sizeof(ready) - 1u));
|
|
MB_BASE[MB_PHASE] = PHASE_READY;
|
|
|
|
received = 0u;
|
|
inSequence = 0u;
|
|
breaks = 0u;
|
|
presents = 0u;
|
|
sum = 0u;
|
|
expect = 0u;
|
|
color = 1u;
|
|
started = false;
|
|
firstTick = jlFrameCount();
|
|
lastTick = firstTick;
|
|
now = firstTick;
|
|
#if defined(SERLOSS_JOY) || defined(SERLOSS_SFX)
|
|
frameTick = firstTick;
|
|
#endif
|
|
for (;;) {
|
|
// Published at the top so every exit below is a bare compare-and-branch: llvm816 has
|
|
// been seen hoisting this function's byte splits between a cmp and its branch,
|
|
// clobbering the flags the branch reads (see the report).
|
|
publish(received, inSequence, breaks, presents, (uint16_t)(lastTick - firstTick), sum, now);
|
|
if (received >= SERLOSS_BYTES) {
|
|
break;
|
|
}
|
|
#if defined(SERLOSS_CLEAR)
|
|
jlSurfaceClear(stage, color);
|
|
jlStagePresent();
|
|
#elif !defined(SERLOSS_IDLE)
|
|
jlFillRect(stage, 0, 0, SCREEN_W, SCREEN_H, color);
|
|
jlStagePresent();
|
|
#endif
|
|
presents++;
|
|
color = (uint8_t)((color & 0x0Fu) + 1u);
|
|
#if defined(SERLOSS_JOY) || defined(SERLOSS_SFX)
|
|
// Once per frame, like a game: an idle loop would otherwise run these back to back.
|
|
if (jlFrameCount() != frameTick) {
|
|
frameTick = jlFrameCount();
|
|
#ifdef SERLOSS_JOY
|
|
joyPoll(&spread);
|
|
#endif
|
|
#ifdef SERLOSS_SFX
|
|
jlAudioPlaySfx((uint8_t)SFX_SHOT_SLOT, (const uint8_t *)gSfxShot, SFX_SHOT_BYTES, SFX_SHOT_RATE);
|
|
jlAudioFrameTick();
|
|
shots++;
|
|
mailbox16(MB_SFX_SHOTS, shots);
|
|
#endif
|
|
}
|
|
#endif
|
|
jlSerialPoll();
|
|
got = 0u;
|
|
n = jlSerialRead(buf, READ_CHUNK);
|
|
while (n > 0u) {
|
|
for (i = 0u; i < n; i++) {
|
|
if (buf[i] == expect) {
|
|
inSequence++;
|
|
} else {
|
|
breaks++;
|
|
}
|
|
expect = (uint8_t)(buf[i] + 1u);
|
|
sum = (uint16_t)(sum + buf[i]);
|
|
}
|
|
got = (uint16_t)(got + n);
|
|
n = jlSerialRead(buf, READ_CHUNK);
|
|
}
|
|
received = (uint16_t)(received + got);
|
|
now = jlFrameCount();
|
|
if (got > 0u) {
|
|
if (!started) {
|
|
started = true;
|
|
firstTick = now;
|
|
MB_BASE[MB_PHASE] = PHASE_RECEIVING;
|
|
}
|
|
lastTick = now;
|
|
}
|
|
if (started) {
|
|
if ((uint16_t)(now - lastTick) > IDLE_TICKS) {
|
|
break;
|
|
}
|
|
} else if ((uint16_t)(now - firstTick) > START_TICKS) {
|
|
break;
|
|
}
|
|
}
|
|
publish(received, inSequence, breaks, presents, (uint16_t)(lastTick - firstTick), sum, now);
|
|
MB_BASE[MB_PHASE] = PHASE_DONE;
|
|
for (;;) {
|
|
}
|
|
}
|