mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 12:47:58 -10:00
A Doom link tic is 12 one-way single-byte hops (6 pairs, ping-pong). Two parts of the old behaviour made the tic rate depend on the vsync phase between the two devices, collapsing to 5 tics/s at bad phases even on a fast link: JLinkAwaitReply only engaged after we transmitted, so a side that only ANSWERS spun on ASISTAT with no wait, finished its frame in ~2 ms and sat in the frontend while the partner's byte arrived; every such hop then cost a whole video frame. uart.c now counts consecutive ASISTAT reads with nothing to send or receive and, past 24 in a frame, calls JLinkAwaitIdle(), which blocks like the reply wait (same frame budget and timeout) so the byte is seen the moment it arrives. A silent partner backs off exponentially (1..32 frames without idle waits); any received byte resets it. Real transports on the JagLink device only (not loopback, not the voice modem). Simulator, Doom 2P, tics/s averaged over 4 vsync phases, before -> after (emu 2 ms, 5 us poll_receive): 2 ms one-way 27.5 -> 27.5 2.5 ms 19.8 -> 26.0 3 ms 16.3 -> 20.8 4 ms 7.5 -> 19.2 Wi-Fi A (2 ms+3 jitter+2% spikes) 8.9 -> 16.0 Wi-Fi B (3 ms+8 jitter+3% spikes) 5.4 -> 9.0 3 ms with emu 6 ms 10 -> 15 Also: a once-per-5-s INFO log line while a link carries traffic (hops/s, mean hop time, wait per hop, slipped-hop share, timeouts, budget cuts, emulation cost, idle-wait hits) so the numbers can be read from the RetroArch log on iOS; netlink_sim gains --btrace/--log/--loss-pct/--rto-ms/--udp-rto-ms/ --corrupt and the clock patch gains a byte-event hook; netlink_btrace_report.py breaks hop time down by cause and analyses the Doom tic structure. Co-Authored-By: Claude Opus 5.5 <[email protected]>
153 lines
5.4 KiB
C
153 lines
5.4 KiB
C
/* test_jlink.c — unit test for the JagLink byte-transport seam. */
|
|
#include <stdio.h>
|
|
#include <stdint.h>
|
|
#include "src/jerry/jlink.h"
|
|
|
|
/* #552: jlink.c's negotiation state machine calls into uart.c to read the
|
|
config intent and write the negotiated effective divisor. This test
|
|
deliberately does not link uart.c (or the event.c/JERRY-stub chain it
|
|
drags in) -- it tests the transport seam in isolation -- so stub the
|
|
two entry points minimally. Real behavior is exercised end-to-end by
|
|
test/test_jlink_negotiate.c and unit-pinned by test/test_uart_loopback.c. */
|
|
unsigned UARTWireSpeedupIntent(void) { return 0; }
|
|
void UARTSetWireSpeedupEffective(unsigned divisor) { (void)divisor; }
|
|
|
|
static int failures = 0;
|
|
|
|
#define CHECK(cond, msg) \
|
|
do { if (cond) printf("PASS %s\n", msg); \
|
|
else { printf("FAIL %s\n", msg); failures++; } } while (0)
|
|
|
|
static void test_disabled_mode(void)
|
|
{
|
|
uint8_t b = 0xEE;
|
|
JLinkClose();
|
|
CHECK(JLinkMode() == JLINK_MODE_DISABLED, "closed -> mode disabled");
|
|
CHECK(!JLinkConnected(), "disabled -> not connected");
|
|
JLinkSendByte(0x41);
|
|
CHECK(JLinkRxPending() == 0, "disabled send discards");
|
|
CHECK(JLinkRecvByte(&b) == 0 && b == 0xEE, "disabled recv returns none");
|
|
}
|
|
|
|
static void test_loopback_roundtrip(void)
|
|
{
|
|
uint8_t b = 0;
|
|
CHECK(JLinkOpen(JLINK_MODE_LOOPBACK) == 1, "open loopback");
|
|
CHECK(JLinkConnected(), "loopback -> connected");
|
|
JLinkSendByte(0x11);
|
|
JLinkSendByte(0x22);
|
|
JLinkSendByte(0x33);
|
|
CHECK(JLinkRxPending() == 3, "three bytes pending");
|
|
CHECK(JLinkRecvByte(&b) == 1 && b == 0x11, "recv 1st in order");
|
|
CHECK(JLinkRecvByte(&b) == 1 && b == 0x22, "recv 2nd in order");
|
|
CHECK(JLinkRecvByte(&b) == 1 && b == 0x33, "recv 3rd in order");
|
|
CHECK(JLinkRxPending() == 0, "drained");
|
|
JLinkClose();
|
|
}
|
|
|
|
static void test_overflow_drops_newest(void)
|
|
{
|
|
int i;
|
|
uint8_t b = 0;
|
|
JLinkOpen(JLINK_MODE_LOOPBACK);
|
|
for (i = 0; i < 300; i++)
|
|
JLinkSendByte((uint8_t)i);
|
|
CHECK(JLinkRxPending() == 256, "ring capped at 256");
|
|
CHECK(JLinkRecvByte(&b) == 1 && b == 0, "oldest byte survives overflow");
|
|
JLinkClose();
|
|
}
|
|
|
|
static void test_state_roundtrip(void)
|
|
{
|
|
uint8_t buf[1024];
|
|
uint8_t b = 0;
|
|
size_t n;
|
|
JLinkOpen(JLINK_MODE_LOOPBACK);
|
|
JLinkSendByte(0xAB);
|
|
JLinkSendByte(0xCD);
|
|
n = JLinkStateSave(buf);
|
|
CHECK(n > 0, "state save wrote bytes");
|
|
(void)JLinkRecvByte(&b);
|
|
(void)JLinkRecvByte(&b);
|
|
CHECK(JLinkRxPending() == 0, "drained before load");
|
|
CHECK(JLinkStateLoad(buf) == n, "state load consumed same size");
|
|
CHECK(JLinkRxPending() == 2, "load restored 2 pending bytes");
|
|
CHECK(JLinkRecvByte(&b) == 1 && b == 0xAB, "restored byte order");
|
|
JLinkClose();
|
|
}
|
|
|
|
/* jlink.h documents the traffic counters as per-session, reset by
|
|
JLinkClose(). They used to survive a close and accumulate across
|
|
sessions, which made the netlink diagnostics report traffic the current
|
|
session never carried. */
|
|
static void test_counters_reset_on_close(void)
|
|
{
|
|
JLinkOpen(JLINK_MODE_LOOPBACK);
|
|
JLinkSendByte(0x5A);
|
|
JLinkSendByte(0x5B);
|
|
CHECK(JLinkTxTotal() == 2, "tx counted while open");
|
|
JLinkClose();
|
|
CHECK(JLinkTxTotal() == 0, "close resets tx total");
|
|
CHECK(JLinkRxTotal() == 0, "close resets rx total");
|
|
}
|
|
|
|
/* RX is counted where the byte ARRIVES, not where the game drains the ring
|
|
— otherwise a peer's traffic stays invisible in the counters until the
|
|
game happens to read it, and tx/rx are never comparable. */
|
|
static void test_rx_counted_on_arrival(void)
|
|
{
|
|
uint8_t b = 0;
|
|
JLinkOpen(JLINK_MODE_LOOPBACK);
|
|
JLinkSendByte(0x77);
|
|
CHECK(JLinkRxTotal() == 1, "rx counted on arrival, before any read");
|
|
CHECK(JLinkRxPending() == 1, "byte still queued");
|
|
(void)JLinkRecvByte(&b);
|
|
CHECK(JLinkRxTotal() == 1, "draining the ring does not re-count it");
|
|
JLinkClose();
|
|
}
|
|
|
|
/* A dropped byte never entered the ring, so it must not be counted as
|
|
received: rx total has to stay consistent with what a reader can see. */
|
|
static void test_dropped_bytes_not_counted(void)
|
|
{
|
|
int i;
|
|
JLinkOpen(JLINK_MODE_LOOPBACK);
|
|
for (i = 0; i < 300; i++)
|
|
JLinkSendByte((uint8_t)i);
|
|
CHECK(JLinkTxTotal() == 300, "all 300 sends counted as tx");
|
|
CHECK(JLinkRxTotal() == 256, "only the 256 that fit counted as rx");
|
|
JLinkClose();
|
|
}
|
|
|
|
/* The link-health log line (JLinkStatsPoll) must stay silent when there is no
|
|
link and must never fire in the first call of a window or without traffic:
|
|
it runs once per video frame, so a spurious line would be a log flood. */
|
|
static void test_stats_quiet(void)
|
|
{
|
|
char buf[64];
|
|
buf[0] = 'x';
|
|
buf[1] = '\0';
|
|
JLinkClose();
|
|
CHECK(JLinkStatsPoll(buf, sizeof(buf)) == 0, "stats: disabled -> no line");
|
|
JLinkOpen(JLINK_MODE_LOOPBACK);
|
|
CHECK(JLinkStatsPoll(buf, sizeof(buf)) == 0, "stats: first call only starts the window");
|
|
JLinkSendByte(0x42);
|
|
CHECK(JLinkStatsPoll(buf, sizeof(buf)) == 0, "stats: window not elapsed -> no line");
|
|
CHECK(buf[0] == 'x', "stats: buffer untouched when silent");
|
|
JLinkClose();
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
test_disabled_mode();
|
|
test_loopback_roundtrip();
|
|
test_overflow_drops_newest();
|
|
test_state_roundtrip();
|
|
test_counters_reset_on_close();
|
|
test_rx_counted_on_arrival();
|
|
test_dropped_bytes_not_counted();
|
|
test_stats_quiet();
|
|
printf(failures ? "FAILED (%d)\n" : "OK\n", failures);
|
|
return failures ? 1 : 0;
|
|
}
|