joeylib2/examples/serloss/serloss.c
2026-09-28 21:03:46 -05:00

408 lines
14 KiB
C

// 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
// 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_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];
#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 bool 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(JOYSTICK_0);
y = jlJoystickY(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
// Bring up audio, build the one-shot's square wave and the stream's sawtooth, and start the
// endless stream.
static bool sfxStart(void) {
uint16_t i;
if (!jlAudioInit()) {
return false;
}
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 (~0.2 s 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);
return true;
}
// Endless sawtooth, whole periods: never returns 0, so the stream runs until the program ends.
static uint32_t sfxStreamFill(void *ctx, int8_t *dst, uint32_t count) {
uint32_t done;
(void)ctx;
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;
config.audioBytes = 0u;
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);
stage = jlStageGet();
mailbox16(MB_FILL_TICKS, benchLoop(stage, false));
mailbox16(MB_PRESENT_TICKS, benchLoop(stage, true));
#ifdef SERLOSS_SFX
if (!sfxStart()) {
MB_BASE[MB_PHASE] = PHASE_FAILED;
for (;;) {
}
}
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(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 (;;) {
}
}