mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
virtualjaguar_netlink_speed now offers disabled/auto only. auto has the two emulator instances agree out-of-band at link-up on the one non-stock divisor (UART_WIRE_SPEEDUP_MAX=4); either side falls back to stock the instant the peer rejects, doesn't understand the request, or never answers. This removes the "both sides must match by hand" coordination burden the old 2x/4x value list had, and structurally prevents the asymmetric-application case #498 flagged as unproven -- a side only ever accelerates once its peer has independently confirmed the same. Negotiation channel: piggybacks on jlink_discover.c's existing LAN discovery UDP socket (port 42170), using a distinct magic ("VJNG" vs the beacon's "VJAG") and length (12 vs 40 bytes) so JLinkDiscDecode() on a peer that doesn't implement this silently drops the packet exactly like a corrupt/hostile one always has -- nothing negotiation-related ever reaches the emulated UART ring. The raw TCP/netpacket byte pipe and the netpacket backend were both rejected: an old peer forwards everything it receives straight into the UART RX ring, so any negotiation bytes sent there corrupt real game state rather than failing safely. Derive-not-exchange (the guide's preferred approach) did not hold up: asiClk is 0 after reset and only becomes the game's value once the game writes it, so the two sides can legitimately disagree at link-up purely from boot skew -- there is no shared state to derive a divisor from at the moment negotiation needs one. It collapses further than expected instead: since the option now offers only disabled/auto, there is exactly one non-stock divisor to begin with (a compile-time constant), so the wire only ever needs to carry one bit ("are you also in auto"), symmetry is automatic, and the existing UART_WIRE_SPEEDUP_MAX clamp is the cap -- no magnitude is ever exchanged. Protocol (src/jerry/jlink.c, JLinkNegTick/JLinkNegOnRaw): the TCP client sends a hello to the server's discovery port every ~500ms (~8 tries, ~4s ceiling) until it gets one back or gives up; the server is purely reactive, learning the client's address for free from the incoming datagram and replying with its own packet. Each packet carries a random per-session id, and a receiver ignores any incoming packet carrying its OWN id back -- required because two cores on one machine legitimately share the discovery port via SO_REUSEPORT (documented dev/test topology), and a first cut of test/test_jlink_negotiate.c measured a unicast hello/ack reliably hairpinning back to its own sender on that topology rather than crossing over. That measurement is now a documented KNOWN LIMITATION on JLinkNegEligible(): same-host auto negotiation usually will not confirm (safe -- falls back to stock, same as "peer never answers" -- but real for anyone dev-testing two cores on localhost). Real play across two different machines is unaffected. Scope is deliberately conservative: only a single TCP peer negotiates (a CatNet multi-drop hub stays stock), and netpacket/loopback modes stay stock too (no peer address to reach for netpacket; JLinkDiscActive() is never true for loopback). Savestate (THE SHARP EDGE): uartWireSpeedup is split into Intent (config-derived "auto" request, stays out of the blob like NTSC/PAL) and Effective (the negotiated result, which changes UARTFrameUsec()'s UART TX/RX event-queue scheduling the instant a peer confirms it, so it IS serialized). Effective is a new trailing chunk in the existing v13 blob (STATE_VERSION_TEAMTAP, extended in place per this cycle's one-bump policy -- NOT bumped to 14), strictly after the Team Tap chunk so every v12 and pre-#552 v13 blob still loads. jlink.c's JLinkNegTick() reconciles Effective every frame against the live connection, so a state saved mid-negotiation and reloaded with the peer gone reverts to stock within one frame rather than silently keeping the accelerated rate. Tests: test/test_jlink_negotiate.c (new) drives the real jlink.c/uart.c negotiation state machine over real sockets against a hand-crafted fake peer, proving both the positive case (confirms and applies UART_WIRE_SPEEDUP_MAX) and the safety case (a TCP-connected peer that never answers on the discovery port never gets accelerated). Extended test_jlink_discover.c with negotiate-packet codec tests, a never-collides-with-a-beacon cross-protocol test, and a real-socket test proving JLinkDiscPoll() forwards unrecognized datagrams to the new raw handler without disturbing beacon handling. Extended test_uart_loopback.c with the Intent/Effective split and a savestate round-trip test (including that a just-loaded negotiated value is overridden by the current session's intent=off, and that the loaded value actually reschedules UARTFrameUsec(), not just the getter). test_jlink.c/test_jlink_tcp.c needed minimal UART stub additions since jlink.c now calls into uart.c's Intent/Effective API. Also updates: test/tools/netlink_latency.c's --speed flag now drives a new jlink.c test-only VJ_FORCE_WIRE_SPEEDUP escape hatch (bypasses negotiation entirely, for exercising UARTFrameUsec()'s divisor mechanism under real event-driven timing without depending on same-host negotiation succeeding); netlink_wire_speed_test.sh drops the now- unreachable "one side accelerated" mismatch scenario and instead proves off/off vs forced-N/N beats it; uv_modem_game_test.sh's doc comment updated to match (its default run is unaffected, both sides already default to "disabled"). docs/netlink-design.md, docs/netlink-user- guide.md, docs/voice-modem.md updated for the new value list and the negotiation design/limitations. Makefile: added the test/test_jlink_negotiate build rule and wired it into `make test` (no new compile-affecting switch, so BUILD_AXES is unaffected). Verification status: full `make TEST_EXPORTS=1 test` was still running in the background when this commit was made (long private-ROM suite) -- committing now for durability rather than waiting, per instruction. All newly-added/modified unit and integration tests (test_uart_loopback, test_jlink_discover, test_jlink_negotiate, test_jlink, test_jlink_tcp) were run individually and passed, plus a manual run of netlink_wire_speed_test.sh confirming the escape-hatch mechanism still shows a real ~3x exchange-rate improvement (17.5 -> 50.8/sec). The full suite's private-ROM/audio/CD sections had not been observed to completion at commit time; will report the actual exit code and any failures separately once it finishes. Co-Authored-By: Claude Opus 5 <[email protected]>
506 lines
20 KiB
C
506 lines
20 KiB
C
/* test_uart_loopback.c — unit test for JERRY UART emulation over the
|
|
loopback transport, driving the real event queue. */
|
|
#include <stdio.h>
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <math.h>
|
|
#include "src/core/event.h"
|
|
#include "src/core/settings.h"
|
|
#include "src/jerry/jerry.h"
|
|
#include "src/jerry/uart.h"
|
|
#include "src/jerry/jlink.h"
|
|
|
|
/* ---- stubs required by event.c's savestate callback registry ---- */
|
|
void HalflineCallback(void) {}
|
|
void TOMPITCallback(void) {}
|
|
void JERRYPIT1Callback(void) {}
|
|
void JERRYPIT2Callback(void) {}
|
|
void JERRYI2SCallback(void) {}
|
|
void DSPSampleCallback(void) {}
|
|
void GPUCPUINTCallback(void) {}
|
|
|
|
/* ---- stubs required by uart.c ---- */
|
|
struct VJSettings vjs;
|
|
static int stubIrqMask = 0;
|
|
static int stubPending = 0;
|
|
static int stubIpl = -1;
|
|
bool JERRYIRQEnabled(int irq) { return (stubIrqMask & irq) != 0; }
|
|
void JERRYSetPendingIRQ(int irq) { stubPending |= irq; }
|
|
void m68k_set_irq(unsigned int level) { stubIpl = (int)level; }
|
|
/* TOM INT1 enable byte: JERRY's interrupt output reaches the 68K only
|
|
through bit 4 (C_JERENA, IRQ_DSP). Stubbed rather than pulling in
|
|
tom.c, which this test deliberately does not link. */
|
|
#define STUB_IRQ_DSP 4
|
|
static int stubTomInt1 = 0;
|
|
int TOMIRQEnabled(int irq) { return stubTomInt1 & (1 << irq); }
|
|
|
|
#define ASIDATA 0xF10030u
|
|
#define ASISTAT 0xF10032u
|
|
#define ASICTRL 0xF10032u
|
|
#define ASICLK 0xF10034u
|
|
|
|
#define ST_RBF 0x0080
|
|
#define ST_TBE 0x0100
|
|
#define ST_OE 0x0800
|
|
#define ST_SERIN 0x2000
|
|
#define ST_ERROR 0x8000
|
|
#define CT_TINTEN 0x0010
|
|
#define CT_RINTEN 0x0020
|
|
#define CT_CLRERR 0x0040
|
|
|
|
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 fresh(void)
|
|
{
|
|
InitializeEventList();
|
|
UARTReset();
|
|
/* #552: both halves off unless a test sets them -- UARTReset() only
|
|
drops Effective (mirrors production JERRYInit()), Intent is
|
|
config-owned and needs an explicit reset here so one test's
|
|
UARTSetWireSpeedupIntent(1) cannot leak into the next. */
|
|
UARTSetWireSpeedupIntent(0);
|
|
UARTSetWireSpeedupEffective(1);
|
|
UARTSetLinkMode(JLINK_MODE_LOOPBACK);
|
|
stubIrqMask = IRQ2_ASI; /* J_ASYNENA on unless a test clears it */
|
|
stubPending = 0;
|
|
stubIpl = -1;
|
|
stubTomInt1 = 1 << STUB_IRQ_DSP; /* C_JERENA on unless a test clears it */
|
|
}
|
|
|
|
/* Fire JERRY events one at a time, mirroring the production loop:
|
|
HandleNextEvent() dispatches the slot located by the LAST
|
|
GetTimeToNextEvent() call, so the pair must always run together. */
|
|
static void pump(int max_events)
|
|
{
|
|
int i;
|
|
for (i = 0; i < max_events; i++)
|
|
{
|
|
(void)GetTimeToNextEvent(EVENT_JERRY);
|
|
HandleNextEvent(EVENT_JERRY);
|
|
}
|
|
}
|
|
|
|
static void test_reset_defaults(void)
|
|
{
|
|
uint16_t st;
|
|
fresh();
|
|
st = UARTReadWord(ASISTAT);
|
|
CHECK((st & ST_TBE) != 0, "reset: TBE set");
|
|
CHECK((st & ST_RBF) == 0, "reset: RBF clear");
|
|
CHECK((st & ST_ERROR) == 0, "reset: no error");
|
|
CHECK((st & ST_SERIN) != 0, "reset: SERIN idles at mark");
|
|
CHECK(UARTReadWord(ASICLK) == 0, "reset: ASICLK zero");
|
|
}
|
|
|
|
static void test_baud_timing(void)
|
|
{
|
|
double t, expect;
|
|
fresh();
|
|
UARTWriteWord(ASICLK, 0); /* N=0: frame = 11*16*1 clocks */
|
|
UARTWriteWord(ASIDATA, 0x41);
|
|
t = GetTimeToNextEvent(EVENT_JERRY);
|
|
expect = 11.0 * 16.0 * 1.0 * RISC_CYCLE_IN_USEC;
|
|
CHECK(fabs(t - expect) < 0.001, "N=0 frame time = 176 clocks");
|
|
|
|
fresh();
|
|
UARTWriteWord(ASICLK, 171); /* ~9600 baud on NTSC clock */
|
|
UARTWriteWord(ASIDATA, 0x41);
|
|
t = GetTimeToNextEvent(EVENT_JERRY);
|
|
expect = 11.0 * 16.0 * 172.0 * RISC_CYCLE_IN_USEC;
|
|
CHECK(fabs(t - expect) < 0.01, "N=171 frame time = 302,720 clocks");
|
|
}
|
|
|
|
static void test_loopback_roundtrip(void)
|
|
{
|
|
uint16_t st;
|
|
fresh();
|
|
UARTWriteWord(ASICLK, 0);
|
|
UARTWriteWord(ASIDATA, 0x5A);
|
|
st = UARTReadWord(ASISTAT);
|
|
CHECK((st & ST_TBE) != 0, "straight-to-shift write keeps TBE");
|
|
pump(1); /* TX completes -> byte to jlink */
|
|
pump(1); /* RX frame completes -> RBF */
|
|
st = UARTReadWord(ASISTAT);
|
|
CHECK((st & ST_RBF) != 0, "byte looped back: RBF set");
|
|
CHECK((UARTReadWord(ASIDATA) & 0xFF) == 0x5A, "ASIDATA returns byte");
|
|
st = UARTReadWord(ASISTAT);
|
|
CHECK((st & ST_RBF) == 0, "ASIDATA read clears RBF");
|
|
}
|
|
|
|
static void test_double_buffering(void)
|
|
{
|
|
uint16_t st;
|
|
fresh();
|
|
UARTWriteWord(ASICLK, 0);
|
|
UARTWriteWord(ASIDATA, 0x01); /* -> shift */
|
|
UARTWriteWord(ASIDATA, 0x02); /* -> holding, TBE drops */
|
|
st = UARTReadWord(ASISTAT);
|
|
CHECK((st & ST_TBE) == 0, "holding full: TBE clear");
|
|
pump(1); /* TX#1: byte out, holding -> shift */
|
|
st = UARTReadWord(ASISTAT);
|
|
CHECK((st & ST_TBE) != 0, "holding drained: TBE set");
|
|
pump(2); /* TX#2 + RX#1 (tie order converges) */
|
|
CHECK((UARTReadWord(ASIDATA) & 0xFF) == 0x01, "1st byte received first");
|
|
pump(1); /* RX#2 delivers into now-empty RBF */
|
|
CHECK((UARTReadWord(ASIDATA) & 0xFF) == 0x02, "2nd byte follows");
|
|
/* NOTE: pump counts are exact (4 events scheduled in total) because
|
|
HandleNextEvent() re-fires a stale slot if called on an empty list. */
|
|
}
|
|
|
|
static void test_overrun(void)
|
|
{
|
|
uint16_t st;
|
|
fresh();
|
|
UARTWriteWord(ASICLK, 0);
|
|
UARTWriteWord(ASIDATA, 0x01);
|
|
UARTWriteWord(ASIDATA, 0x02);
|
|
pump(4); /* TX#1, TX#2, RX#1, RX#2 — no reads */
|
|
st = UARTReadWord(ASISTAT);
|
|
CHECK((st & ST_RBF) != 0, "first byte still buffered");
|
|
CHECK((st & ST_OE) != 0, "second byte overruns: OE");
|
|
CHECK((st & ST_ERROR) != 0, "ERROR mirrors OE");
|
|
CHECK((UARTReadWord(ASIDATA) & 0xFF) == 0x01, "old data kept on overrun");
|
|
UARTWriteWord(ASICTRL, CT_CLRERR);
|
|
st = UARTReadWord(ASISTAT);
|
|
CHECK((st & ST_OE) == 0 && (st & ST_ERROR) == 0, "CLRERR clears OE");
|
|
}
|
|
|
|
static void test_disabled_link(void)
|
|
{
|
|
fresh();
|
|
UARTSetLinkMode(JLINK_MODE_DISABLED);
|
|
UARTWriteWord(ASICLK, 0);
|
|
UARTWriteWord(ASIDATA, 0x7E);
|
|
pump(1); /* TX only; nothing echoes back */
|
|
CHECK((UARTReadWord(ASISTAT) & ST_RBF) == 0, "disabled: nothing echoes");
|
|
CHECK((UARTReadWord(ASISTAT) & ST_TBE) != 0, "disabled: TX still drains");
|
|
}
|
|
|
|
static void test_rx_interrupt(void)
|
|
{
|
|
fresh();
|
|
UARTWriteWord(ASICLK, 0);
|
|
UARTWriteWord(ASICTRL, CT_RINTEN);
|
|
UARTWriteWord(ASIDATA, 0x99);
|
|
pump(2); /* TX + RX frames */
|
|
CHECK((stubPending & IRQ2_ASI) != 0, "RINTEN: RX raises IRQ2_ASI");
|
|
CHECK(stubIpl == 2, "RX interrupt asserts 68K IPL2");
|
|
}
|
|
|
|
static void test_rx_interrupt_masked(void)
|
|
{
|
|
fresh();
|
|
stubIrqMask = 0; /* J_ASYNENA off */
|
|
UARTWriteWord(ASICLK, 0);
|
|
UARTWriteWord(ASICTRL, CT_RINTEN);
|
|
UARTWriteWord(ASIDATA, 0x99);
|
|
pump(2);
|
|
CHECK(stubPending == 0, "JINTCTRL gate blocks RX IRQ");
|
|
CHECK(stubIpl == -1, "no IPL2 when masked");
|
|
}
|
|
|
|
/* Regression (#UV voice-modem video corruption): Ultra Vortek enables
|
|
RINTEN and JINTCTRL bit 4 but leaves TOM INT1 = $01 (video only), so
|
|
C_JERENA is clear and the ASI interrupt must never reach the 68K.
|
|
Without this gate every received byte took a spurious level-2
|
|
exception, re-entering the game's display handler mid-field. */
|
|
static void test_rx_interrupt_tom_gate(void)
|
|
{
|
|
fresh();
|
|
stubTomInt1 = 0x01; /* video only: C_JERENA clear */
|
|
UARTWriteWord(ASICLK, 0);
|
|
UARTWriteWord(ASICTRL, CT_RINTEN);
|
|
UARTWriteWord(ASIDATA, 0x99);
|
|
pump(2);
|
|
CHECK((stubPending & IRQ2_ASI) != 0,
|
|
"C_JERENA clear: JERRY still latches ASI pending");
|
|
CHECK(stubIpl == -1, "C_JERENA clear: no 68K IPL2 from UART RX");
|
|
}
|
|
|
|
static void test_rx_interrupt_disabled(void)
|
|
{
|
|
fresh(); /* mask on, RINTEN off */
|
|
UARTWriteWord(ASICLK, 0);
|
|
UARTWriteWord(ASIDATA, 0x99);
|
|
pump(2);
|
|
CHECK(stubPending == 0, "RINTEN off: RX raises nothing");
|
|
}
|
|
|
|
static void test_tx_interrupt_on_holding_drain(void)
|
|
{
|
|
fresh();
|
|
UARTWriteWord(ASICLK, 0);
|
|
UARTWriteWord(ASICTRL, CT_TINTEN);
|
|
UARTWriteWord(ASIDATA, 0x01); /* straight to shift: no IRQ */
|
|
CHECK(stubPending == 0, "straight-to-shift write raises no TX IRQ");
|
|
UARTWriteWord(ASIDATA, 0x02); /* parks in holding */
|
|
pump(1); /* holding drains -> TBE edge */
|
|
CHECK((stubPending & IRQ2_ASI) != 0, "TINTEN: holding drain raises IRQ");
|
|
}
|
|
|
|
/* ---- #498: opt-in netlink wire-latency enhancement ----
|
|
UARTFrameUsec() is the single choke point for BOTH the TX drain and the
|
|
RX arrival, so these cases pin the whole feature: the divisor applies,
|
|
it only applies on an active link, and it changes nothing but time. */
|
|
|
|
/* Emulated microseconds until the pending UART character completes, for
|
|
one byte written at the given ASICLK. Uses ASICLK $56 (86) -- the
|
|
value Ultra Vortek's Voice Modem driver settles at (docs/voice-modem.md),
|
|
i.e. the case the enhancement exists for. */
|
|
static double frame_usec_at(unsigned speedup, int link_mode)
|
|
{
|
|
fresh();
|
|
UARTSetLinkMode(link_mode);
|
|
UARTSetWireSpeedupEffective(speedup);
|
|
UARTWriteWord(ASICLK, 0x56);
|
|
UARTWriteWord(ASIDATA, 0x41);
|
|
return GetTimeToNextEvent(EVENT_JERRY);
|
|
}
|
|
|
|
static void test_wire_speedup_scales_frame_time(void)
|
|
{
|
|
/* Stock reference computed the way uart.c's untouched branch does. */
|
|
double stock = 11.0 * 16.0 * 87.0 * RISC_CYCLE_IN_USEC;
|
|
double t;
|
|
|
|
fresh();
|
|
CHECK(UARTWireSpeedup() == 1, "wire speedup defaults to 1 (off)");
|
|
|
|
t = frame_usec_at(1, JLINK_MODE_LOOPBACK);
|
|
CHECK(fabs(t - stock) < 0.001, "speedup off: stock 576 us character frame");
|
|
|
|
t = frame_usec_at(2, JLINK_MODE_LOOPBACK);
|
|
CHECK(fabs(t - stock / 2.0) < 0.001, "speedup 2x halves the character frame");
|
|
|
|
t = frame_usec_at(4, JLINK_MODE_LOOPBACK);
|
|
CHECK(fabs(t - stock / 4.0) < 0.001, "speedup 4x quarters the character frame");
|
|
}
|
|
|
|
/* The gate that keeps every non-link user on stock timing: with no
|
|
transport selected the divisor must be ignored outright, so a stale
|
|
"4x" left in a config can never perturb a single-player session. */
|
|
static void test_wire_speedup_ignored_without_link(void)
|
|
{
|
|
double stock = 11.0 * 16.0 * 87.0 * RISC_CYCLE_IN_USEC;
|
|
double t = frame_usec_at(4, JLINK_MODE_DISABLED);
|
|
CHECK(fabs(t - stock) < 0.001, "link disabled: 4x ignored, stock timing");
|
|
}
|
|
|
|
static void test_wire_speedup_clamps(void)
|
|
{
|
|
fresh();
|
|
UARTSetWireSpeedupEffective(0);
|
|
CHECK(UARTWireSpeedup() == 1, "divisor 0 clamps to 1");
|
|
/* A RetroArch .cfg is a text file: the option layer never produces
|
|
more than the option's largest value, but a hand-edited config can,
|
|
and an unbounded divisor drives the character time toward zero. */
|
|
UARTSetWireSpeedupEffective(100000);
|
|
CHECK(UARTWireSpeedup() == UART_WIRE_SPEEDUP_MAX,
|
|
"absurd divisor clamps to UART_WIRE_SPEEDUP_MAX");
|
|
}
|
|
|
|
/* "Nothing game-observable changes except timing": the same bytes arrive
|
|
in the same order with the same status-register transitions at 4x as at
|
|
1x. Only the emulated interval between them differs. */
|
|
static void test_wire_speedup_stream_identical(void)
|
|
{
|
|
uint16_t st;
|
|
fresh();
|
|
UARTSetWireSpeedupEffective(4);
|
|
UARTWriteWord(ASICLK, 0x56);
|
|
UARTWriteWord(ASIDATA, 0x01); /* -> shift */
|
|
UARTWriteWord(ASIDATA, 0x02); /* -> holding */
|
|
st = UARTReadWord(ASISTAT);
|
|
CHECK((st & ST_TBE) == 0, "4x: holding full still clears TBE");
|
|
pump(1);
|
|
st = UARTReadWord(ASISTAT);
|
|
CHECK((st & ST_TBE) != 0, "4x: holding drained still sets TBE");
|
|
pump(2);
|
|
CHECK((UARTReadWord(ASIDATA) & 0xFF) == 0x01, "4x: 1st byte received first");
|
|
pump(1);
|
|
CHECK((UARTReadWord(ASIDATA) & 0xFF) == 0x02, "4x: 2nd byte follows");
|
|
CHECK((UARTReadWord(ASISTAT) & ST_OE) == 0, "4x: no overrun on read-per-byte");
|
|
}
|
|
|
|
/* The hazard that decides how far the enhancement may go, and the reason
|
|
the accelerated receiver applies back-pressure. test_overrun() above
|
|
pins the stock behaviour: a character completing into a full RBF is an
|
|
overrun and the NEW byte is lost. That is correct hardware, but the
|
|
reader's polling rate belongs to the game, so dividing the character
|
|
time divides the reader's budget with it -- and a dropped byte in a
|
|
framed lockstep protocol is a desync, i.e. a game-observable change.
|
|
Measured, not theorised: at 4x with the plain hardware rule, Ultra
|
|
Vortek's DSP-poll driver lost the second byte of the $B800 wake reply
|
|
and the modem handshake never completed (0 pad words vs 7044 stock).
|
|
Accelerated, the byte waits on the wire instead. */
|
|
static void test_wire_speedup_no_overrun_loss(void)
|
|
{
|
|
uint16_t st;
|
|
fresh();
|
|
UARTSetWireSpeedupEffective(4);
|
|
UARTWriteWord(ASICLK, 0x56);
|
|
UARTWriteWord(ASIDATA, 0x01);
|
|
UARTWriteWord(ASIDATA, 0x02);
|
|
pump(4); /* TX#1, TX#2, RX#1 -- no reads */
|
|
st = UARTReadWord(ASISTAT);
|
|
CHECK((st & ST_RBF) != 0, "4x: first byte buffered");
|
|
CHECK((st & ST_OE) == 0, "4x: unread RBF holds the wire, no overrun");
|
|
CHECK((UARTReadWord(ASIDATA) & 0xFF) == 0x01, "4x: first byte intact");
|
|
pump(1); /* the held byte now clocks in */
|
|
CHECK((UARTReadWord(ASISTAT) & ST_RBF) != 0, "4x: held byte then arrives");
|
|
CHECK((UARTReadWord(ASIDATA) & 0xFF) == 0x02,
|
|
"4x: second byte DELIVERED, not dropped");
|
|
}
|
|
|
|
/* The invariant that lets UARTKickRx()'s back-pressure test omit a
|
|
JLinkMode() term: dropping the link drains the RX ring, so "speedup set
|
|
but no transport selected" can never coexist with a byte pending on the
|
|
wire. If a future transport change stops clearing the ring on close,
|
|
this fails here rather than silently applying accelerated back-pressure
|
|
while UARTFrameUsec() is on its stock branch. */
|
|
static void test_wire_speedup_link_drop_drains(void)
|
|
{
|
|
fresh();
|
|
UARTSetWireSpeedupEffective(4);
|
|
UARTWriteWord(ASICLK, 0x56);
|
|
UARTWriteWord(ASIDATA, 0x11);
|
|
UARTWriteWord(ASIDATA, 0x22);
|
|
pump(2); /* both TX frames -> loopback ring */
|
|
UARTSetLinkMode(JLINK_MODE_DISABLED); /* pull the cable */
|
|
pump(2);
|
|
CHECK((UARTReadWord(ASISTAT) & ST_RBF) == 0,
|
|
"link dropped: RX ring drained, nothing pending to hold");
|
|
}
|
|
|
|
/* #552: Intent (config request) and Effective (what UARTFrameUsec()
|
|
actually applies) are now two separate values -- this pins the split
|
|
itself, independent of jlink.c's negotiation state machine (which
|
|
never engages here; JLinkDiscActive() is never true for loopback). */
|
|
static void test_wire_speedup_intent_effective_split(void)
|
|
{
|
|
fresh();
|
|
CHECK(UARTWireSpeedupIntent() == 0, "intent defaults to 0 (off)");
|
|
|
|
/* Setting Effective directly (what a successful negotiation would
|
|
do) must NOT imply Intent -- they are independent variables. */
|
|
UARTSetWireSpeedupEffective(UART_WIRE_SPEEDUP_MAX);
|
|
CHECK(UARTWireSpeedupIntent() == 0,
|
|
"effective set does not retroactively set intent");
|
|
CHECK(UARTWireSpeedup() == UART_WIRE_SPEEDUP_MAX,
|
|
"effective set takes effect immediately");
|
|
|
|
/* Turning intent OFF must immediately drop a previously-negotiated
|
|
Effective back to stock -- #552's guide requirement: the user
|
|
disabling the enhancement mid-session must not leave accelerated
|
|
timing running just because a peer had earlier confirmed it. */
|
|
UARTSetWireSpeedupIntent(0);
|
|
CHECK(UARTWireSpeedup() == 1,
|
|
"intent off immediately drops a negotiated effective to stock");
|
|
|
|
/* Turning intent ON, by itself, must NOT raise Effective -- only
|
|
jlink.c's negotiation (or the savestate loader) may do that. This
|
|
is the property that makes "auto" safe: wanting it is not the
|
|
same as having it. */
|
|
UARTSetWireSpeedupIntent(1);
|
|
CHECK(UARTWireSpeedupIntent() == 1, "intent on is recorded");
|
|
CHECK(UARTWireSpeedup() == 1,
|
|
"intent alone never raises effective -- negotiation must");
|
|
}
|
|
|
|
/* #552 THE SHARP EDGE: the negotiated Effective divisor changes
|
|
UARTFrameUsec()'s scheduling, so it must survive a save/load round
|
|
trip byte-for-byte, AND that survival must be observable in the actual
|
|
event-queue timing a loaded core would produce -- not just in the raw
|
|
getter. */
|
|
static void test_wire_speedup_savestate_roundtrip(void)
|
|
{
|
|
uint8_t buf[64];
|
|
size_t savedLen, loadedLen;
|
|
double stock = 11.0 * 16.0 * 87.0 * RISC_CYCLE_IN_USEC;
|
|
double t;
|
|
|
|
/* Side A: negotiate (simulated), save. */
|
|
fresh();
|
|
UARTSetLinkMode(JLINK_MODE_LOOPBACK);
|
|
UARTSetWireSpeedupIntent(1);
|
|
UARTSetWireSpeedupEffective(UART_WIRE_SPEEDUP_MAX);
|
|
savedLen = UARTWireSpeedupStateSave(buf);
|
|
CHECK(savedLen == 4, "state chunk is exactly one uint32 (4 bytes)");
|
|
|
|
/* Side B: a fresh core (or the same one reloading) -- Effective back
|
|
to stock, as UARTReset()/UARTDone() leave it. */
|
|
fresh();
|
|
UARTSetLinkMode(JLINK_MODE_LOOPBACK);
|
|
CHECK(UARTWireSpeedup() == 1, "fresh core: effective is stock before load");
|
|
|
|
loadedLen = UARTWireSpeedupStateLoad(buf);
|
|
CHECK(loadedLen == savedLen, "load consumes exactly what save wrote");
|
|
CHECK(UARTWireSpeedup() == UART_WIRE_SPEEDUP_MAX,
|
|
"negotiated effective survives save/load round trip");
|
|
|
|
/* Not just the getter -- the loaded value must actually change what
|
|
UARTFrameUsec() schedules, the whole reason this needed a state
|
|
version bump in the first place (#552 THE SHARP EDGE). */
|
|
UARTWriteWord(ASICLK, 0x56);
|
|
UARTWriteWord(ASIDATA, 0x41);
|
|
t = GetTimeToNextEvent(EVENT_JERRY);
|
|
CHECK(fabs(t - stock / (double)UART_WIRE_SPEEDUP_MAX) < 0.001,
|
|
"loaded effective value actually reschedules the UART callback");
|
|
}
|
|
|
|
/* #552: a state saved mid-negotiation must not silently re-apply a
|
|
speedup the option no longer requests once reloaded -- Intent is
|
|
config-derived and reapplied independently of the state blob, and
|
|
UARTSetWireSpeedupIntent(0) must win even over a just-loaded
|
|
Effective, exactly like the mid-session disable case above. */
|
|
static void test_wire_speedup_savestate_load_then_intent_off(void)
|
|
{
|
|
uint8_t buf[64];
|
|
|
|
fresh();
|
|
UARTSetWireSpeedupEffective(UART_WIRE_SPEEDUP_MAX);
|
|
UARTWireSpeedupStateSave(buf);
|
|
|
|
fresh();
|
|
UARTWireSpeedupStateLoad(buf);
|
|
CHECK(UARTWireSpeedup() == UART_WIRE_SPEEDUP_MAX,
|
|
"load restores the negotiated value first");
|
|
|
|
/* The option layer (libretro.c netlink_apply) calls this every
|
|
check_variables() regardless of state-load timing -- if the
|
|
CURRENT session's option reads "disabled", this must win. */
|
|
UARTSetWireSpeedupIntent(0);
|
|
CHECK(UARTWireSpeedup() == 1,
|
|
"current-session intent=off overrides a just-loaded negotiated value");
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
vjs.hardwareTypeNTSC = true;
|
|
test_reset_defaults();
|
|
test_baud_timing();
|
|
test_loopback_roundtrip();
|
|
test_double_buffering();
|
|
test_overrun();
|
|
test_disabled_link();
|
|
test_rx_interrupt();
|
|
test_rx_interrupt_masked();
|
|
test_rx_interrupt_tom_gate();
|
|
test_rx_interrupt_disabled();
|
|
test_tx_interrupt_on_holding_drain();
|
|
test_wire_speedup_scales_frame_time();
|
|
test_wire_speedup_ignored_without_link();
|
|
test_wire_speedup_clamps();
|
|
test_wire_speedup_stream_identical();
|
|
test_wire_speedup_no_overrun_loss();
|
|
test_wire_speedup_link_drop_drains();
|
|
test_wire_speedup_intent_effective_split();
|
|
test_wire_speedup_savestate_roundtrip();
|
|
test_wire_speedup_savestate_load_then_intent_off();
|
|
printf(failures ? "FAILED (%d)\n" : "OK\n", failures);
|
|
return failures ? 1 : 0;
|
|
}
|