mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
feat(netlink): replace wire-speed 2x/4x with negotiated auto (#552)
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]>
This commit is contained in:
1 parent
09dc0632a1
commit
b918ce8a63
20 files changed
+1677
-169
No files matched your search
@@ -1057,7 +1057,7 @@ test: export VJ_EXPECT_BUILD := $(shell ./scripts/build-id.sh)
|
||||
# invocations get different values.
|
||||
test: EEPROM_GEN_TOOL := /tmp/vj_gen_eeprom_test_rom_$(shell echo $$PPID)
|
||||
test: EEPROM_FIXTURE := /tmp/vj_eeprom_lifecycle_$(shell echo $$PPID).j64
|
||||
test: test/test_dram_timing test/test_cheat test/test_event_queue test/test_jlink test/test_jlink_tcp test/test_jlink_discover test/test_jlink_netpacket test/test_voicemodem_netpacket test/test_uart_loopback test/test_blitter_simd test/test_dsp_mac40 test/test_titledb test/test_titlehook test/test_biosdb \
|
||||
test: test/test_dram_timing test/test_cheat test/test_event_queue test/test_jlink test/test_jlink_tcp test/test_jlink_discover test/test_jlink_netpacket test/test_voicemodem_netpacket test/test_uart_loopback test/test_jlink_negotiate test/test_blitter_simd test/test_dsp_mac40 test/test_titledb test/test_titlehook test/test_biosdb \
|
||||
$(TARGET) test/test_m68k_ops test/test_m68k_irq_ssp test/test_gpu_ops test/test_dsp_ops \
|
||||
test/test_dsp_unit test/test_hle_bios test/test_subsystem_init \
|
||||
test/test_subsystem_timeline test/test_irq_cascade test/test_boot_patterns \
|
||||
@@ -1107,6 +1107,12 @@ test: test/test_dram_timing test/test_cheat test/test_event_queue test/test_jlin
|
||||
@# relayed between them: no sockets, no ports, no ROM.
|
||||
./test/test_voicemodem_netpacket ./$(TARGET)
|
||||
./test/test_uart_loopback
|
||||
@# #552: end-to-end wire-speedup negotiation against a hand-crafted
|
||||
@# fake peer over real sockets -- proves the interop safety property
|
||||
@# (a TCP-connected peer that never answers on the discovery port
|
||||
@# never gets the emulated timing accelerated at it) that the pure
|
||||
@# unit tests above cannot, since jlink.c is single-instance.
|
||||
./test/test_jlink_negotiate
|
||||
./test/test_uart_core ./$(TARGET)
|
||||
./test/test_netlink_host ./$(TARGET)
|
||||
bash test/tools/netlink_pair_test.sh ./$(TARGET)
|
||||
@@ -1734,6 +1740,10 @@ test/test_uart_loopback: test/test_uart_loopback.c src/jerry/uart.c src/jerry/ua
|
||||
$(CC) -O2 -Wall -std=c99 $(INCFLAGS) \
|
||||
-o $@ test/test_uart_loopback.c src/jerry/uart.c src/jerry/jlink.c src/jerry/jlink_tcp.c src/jerry/jlink_netpacket.c src/jerry/jlink_discover.c src/jerry/voicemodem.c src/core/event.c -lm
|
||||
|
||||
test/test_jlink_negotiate: test/test_jlink_negotiate.c src/jerry/uart.c src/jerry/uart.h src/jerry/jlink.c src/jerry/jlink_tcp.c src/jerry/jlink_netpacket.c src/jerry/jlink_discover.c src/jerry/voicemodem.c src/core/event.c
|
||||
$(CC) -O2 -Wall -std=c99 $(INCFLAGS) \
|
||||
-o $@ test/test_jlink_negotiate.c src/jerry/uart.c src/jerry/jlink.c src/jerry/jlink_tcp.c src/jerry/jlink_netpacket.c src/jerry/jlink_discover.c src/jerry/voicemodem.c src/core/event.c -lm
|
||||
|
||||
test/test_uart_core: test/test_uart_core.c test/harness/harness.c test/harness/harness.h
|
||||
$(CC) -O2 -Wall -std=c99 $(INCFLAGS) -Itest \
|
||||
-o $@ test/test_uart_core.c test/harness/harness.c -ldl -lm
|
||||
|
||||
+143
-19
@@ -397,8 +397,9 @@ Frontend matrix: RetroArch ≥ 1.16 → netpacket netplay; any other frontend
|
||||
|
||||
`UARTFrameUsec()` is the single choke point for the whole link's emulated
|
||||
latency — **both** the TX drain and the RX arrival schedule through it — so
|
||||
`virtualjaguar_netlink_speed` (default off, values 2 / 4) divides exactly
|
||||
that one number. Three things about the shape are load-bearing:
|
||||
`virtualjaguar_netlink_speed` (default off; originally values 2 / 4,
|
||||
replaced by a negotiated `auto` in #552 below) divides exactly that one
|
||||
number. Three things about the shape are load-bearing:
|
||||
|
||||
- **Gated on an active transport, evaluated per call.** With
|
||||
`JLinkMode() == JLINK_MODE_DISABLED` the function takes a branch that is
|
||||
@@ -418,24 +419,27 @@ that one number. Three things about the shape are load-bearing:
|
||||
refusing to start a character while RBF is unread, 4x completes normally
|
||||
(6948). Without back-pressure, only 2x is shippable; with it, the byte
|
||||
stream is provably unchanged and only timing moves.
|
||||
- **Not in the savestate.** Like NTSC/PAL it is a runtime setting, and
|
||||
`event.c` saves the *remaining* time of each queued callback rather than
|
||||
a rate — so a state captured accelerated and reloaded stock plays out the
|
||||
one already-scheduled character at its saved deadline and reschedules at
|
||||
stock timing from there.
|
||||
- **Not in the savestate — AS OF #498; #552 below changes this.** Like
|
||||
NTSC/PAL the *config-level request* is a runtime setting and stays
|
||||
outside the state blob. But once #552 lets the two sides *negotiate* a
|
||||
value at runtime, the negotiated result is no longer purely config-
|
||||
derived, and that part now IS serialized. See the #552 section.
|
||||
|
||||
Symmetry is a documentation matter, not a guard: a lockstep exchange is
|
||||
gated by the sum of both sides' clocking, so a mismatched pair still
|
||||
completes and stays in sync, it simply gets less of the benefit. Measured
|
||||
on the synthetic ping-pong cart at ASICLK 1999 (`netlink_wire_speed_test.sh`),
|
||||
two independent runs: **16.9 / 22.6 / 52.5** and **16.4 / 25.0 / 49.9**
|
||||
exchanges per second for off-off, 4x-off, 4x-4x. One side alone lands
|
||||
between the two symmetric configurations, well short of half the benefit.
|
||||
Ultra Vortek's full-game pair completes the choreography in the mismatched
|
||||
configuration too (6972 pad words), and 4x-4x reproduces across runs
|
||||
(6948, 6920) rather than being bimodal — which matters, because the
|
||||
pre-back-pressure failure was catastrophic (0), so a single pass would not
|
||||
have shown the margin.
|
||||
Symmetry was a documentation matter under #498, not a guard: a lockstep
|
||||
exchange is gated by the sum of both sides' clocking, so a mismatched pair
|
||||
still completes and stays in sync, it simply gets less of the benefit.
|
||||
Measured on the synthetic ping-pong cart at ASICLK 1999
|
||||
(`netlink_wire_speed_test.sh`), two independent runs: **16.9 / 22.6 / 52.5**
|
||||
and **16.4 / 25.0 / 49.9** exchanges per second for off-off, 4x-off, 4x-4x.
|
||||
One side alone lands between the two symmetric configurations, well short
|
||||
of half the benefit. Ultra Vortek's full-game pair completes the
|
||||
choreography in the mismatched configuration too (6972 pad words), and
|
||||
4x-4x reproduces across runs (6948, 6920) rather than being bimodal —
|
||||
which matters, because the pre-back-pressure failure was catastrophic (0),
|
||||
so a single pass would not have shown the margin. #552 removes the player's
|
||||
ability to create this mismatch in the first place (see below) — this
|
||||
measurement stays here as the historical justification for why that
|
||||
mattered enough to build negotiation instead of just better docs.
|
||||
|
||||
Because the wall-clock ladder is measured off two paced processes, the
|
||||
ratio assertions are gated on the probe's own pacing telemetry and SKIP
|
||||
@@ -443,6 +447,126 @@ ratio assertions are gated on the probe's own pacing telemetry and SKIP
|
||||
its frame slots. The liveness assertions — every configuration, including
|
||||
the mismatched one, still exchanges — are unconditional failures.
|
||||
|
||||
## Wire-speedup negotiation (2026-08-21 addendum, #552)
|
||||
|
||||
Replaces the player-facing part of #498 above: `virtualjaguar_netlink_speed`
|
||||
is now `disabled` / `auto` only — no magnitude to pick, and no longer a
|
||||
coordination burden on two people who are, by definition, not at the same
|
||||
console. `auto` means the two *emulator instances* agree with each other
|
||||
at link-up, out-of-band, and fall back to stock timing if the peer
|
||||
rejects, does not understand the request, or does not answer. There is
|
||||
exactly one non-stock divisor now (`UART_WIRE_SPEEDUP_MAX`, still 4), so
|
||||
"negotiate" only ever has to answer one question — is the peer also in
|
||||
auto — not agree on a magnitude. A derive-from-shared-state approach
|
||||
(ASICLK, frame period) was considered first and rejected: `asiClk` is 0
|
||||
after reset and only becomes the game's value once the game writes it, so
|
||||
at link-up the two sides can legitimately disagree purely from boot skew
|
||||
— derivation is not symmetric at the moment negotiation needs it.
|
||||
|
||||
**Channel: the LAN discovery beacon's UDP socket (`jlink_discover.c`,
|
||||
port 42170), not the TCP/netpacket data path.** Three candidates were
|
||||
weighed against one hard requirement — a negotiation message must never
|
||||
be mistakable for emulated UART bytes, in either direction, including on
|
||||
a peer that does not implement this:
|
||||
|
||||
- **The raw byte pipe (TCP or netpacket) — rejected.** Both are pure byte
|
||||
transports today with zero framing; a peer without #552 forwards
|
||||
*everything* it receives straight into `jlinkRing`, which the UART reads
|
||||
as real console traffic. Any negotiation bytes sent this way corrupt an
|
||||
old peer's game state, not just fail to negotiate.
|
||||
- **The netpacket backend specifically — rejected for the same reason,**
|
||||
plus it does not exist at all outside frontend netplay: the core only
|
||||
ever sees a `client_id` from `RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE`,
|
||||
never a peer address, so there is nowhere to address a control message
|
||||
even if framing were safe.
|
||||
- **The discovery beacon socket — chosen.** It is a wholly separate
|
||||
UDP port/protocol an old peer either doesn't bind (netpacket/loopback
|
||||
sessions never start it) or, if bound (tcp_server/tcp_client, shipped
|
||||
since #467/v3.4.0), rejects on sight: `JLinkDiscDecode()` requires an
|
||||
exact magic + version + length match, so a differently-magicked packet
|
||||
(`"VJNG"` vs the beacon's `"VJAG"`, `JLINK_NEG_PKT_LEN` = 8 vs
|
||||
`JLINK_DISC_PKT_LEN` = 40) takes the same silent-drop path a corrupt or
|
||||
hostile packet already took before #552 existed. Nothing negotiation-
|
||||
related ever reaches `jlinkRing`.
|
||||
|
||||
**Protocol** (`src/jerry/jlink.c`, `JLinkNegTick`/`JLinkNegOnRaw`; codec in
|
||||
`jlink_discover.c`): the TCP client already knows the server's address (it
|
||||
dialed it) and periodically sends a hello to the effective discovery port
|
||||
there until it gets one back or exhausts ~8 retries over ~4s. The server is
|
||||
purely reactive — it never needs the client's address ahead of time, because
|
||||
`recvfrom()` hands it over for free — and replies with its own packet as an
|
||||
ack the first time it sees one. Neither side raises
|
||||
`UARTWireSpeedupEffective` above 1 until it has *received* a decodable
|
||||
packet from the other, so the accelerated timing is never applied
|
||||
unilaterally — the asymmetric-benefit measurement above cannot recur by
|
||||
construction, not just by policy.
|
||||
|
||||
Each packet carries a random per-session `senderId`, and a receiver ignores
|
||||
any incoming packet carrying its *own* id back — not part of the negotiation
|
||||
semantics, but required because two cores on one machine sharing the
|
||||
discovery port via `SO_REUSEPORT` (`jlink_discover.c`'s own documented
|
||||
dev/test topology) can otherwise receive their own outbound hello back and
|
||||
"confirm" against themselves. The beacon protocol already solved the
|
||||
analogous problem for itself (name+port self-filtering in `JLinkDiscPoll()`);
|
||||
this is the same class of fix for negotiation.
|
||||
|
||||
**Measured limitation, not just closed as a theoretical risk: same-host
|
||||
negotiation usually does not confirm at all.** `test/test_jlink_negotiate.c`'s
|
||||
first cut modeled two cores on one machine exactly as `jlink_discover.c`
|
||||
documents — both wildcard-bound to the discovery port with `SO_REUSEPORT` —
|
||||
and measured the hello and the ack both hairpinning back to their own sender
|
||||
every time, never crossing over. `SO_REUSEPORT` load-balances a unicast
|
||||
datagram to exactly one member of the sharing group by an internal hash; it
|
||||
has no concept of "the other peer," so there is no guarantee — and this
|
||||
measurement suggests no reasonable expectation — that the hash ever picks
|
||||
the far side over the near side. The `senderId` self-filter above stops a
|
||||
hairpinned packet from being mistaken for a real confirmation (a safety
|
||||
fix), but it does not make same-host confirmation happen (not a liveness
|
||||
fix) — the practical result is that two cores dev-tested against each other
|
||||
on `127.0.0.1` with `auto` on will most likely sit at stock forever, which
|
||||
is safe (identical to "peer never answers") but is a real gap worth knowing
|
||||
about before spending time debugging "why won't auto confirm on localhost".
|
||||
Play across two different machines is unaffected — there is no shared
|
||||
`SO_REUSEPORT` group when the sending socket lives on a different host than
|
||||
the one being dialed. `test/test_jlink_negotiate.c` works around this for
|
||||
CI by having its fake peer send from an unbound socket that never joins the
|
||||
group, which is not something two real jlink.c instances on one machine can
|
||||
do (both bind the fixed discovery port by protocol design).
|
||||
|
||||
**Scope, deliberately conservative:** only negotiates over a single TCP
|
||||
peer (`JLinkTCPPeerCount() == 1`). A CatNet-style multi-drop hub
|
||||
(`tcp_server` with more than one peer) would need every peer to agree, and
|
||||
there is no multi-party protocol here, so it stays stock. Frontend netplay
|
||||
(netpacket) also always stays stock — no address to negotiate with, as
|
||||
above. Loopback stays stock too (`JLinkDiscActive()` is never true there).
|
||||
These are real, current limitations, not merely unproven — say so in the
|
||||
option text rather than implying broader coverage.
|
||||
|
||||
**Savestate: THE SHARP EDGE.** `uartWireSpeedupIntent` (the config
|
||||
request) stays out of the blob, unchanged from #498's reasoning above. But
|
||||
`uartWireSpeedupEffective` — the negotiated result — is now machine-
|
||||
affecting the instant a peer confirms it (it reschedules
|
||||
`UARTRXCallback`/`UARTTXCallback` through `UARTFrameUsec()`), so it is
|
||||
serialized: `STATE_VERSION_TEAMTAP` (v13, extended in place — see
|
||||
`src/core/state.h`), trailing chunk, strictly after Team Tap.
|
||||
`JLinkNegTick()` reconciles this every frame regardless of the loader: a
|
||||
restored Effective value survives only as long as the session is still
|
||||
eligible (still connected, still the SAME connection instance, intent
|
||||
still auto) — so a state saved mid-negotiation and reloaded with the peer
|
||||
gone reverts to stock within one frame rather than silently running
|
||||
ahead of a peer that no longer agrees.
|
||||
|
||||
**Unproven, stated plainly rather than assumed:** #498's asymmetry
|
||||
question (does compressing wire time desync the two sides if only one
|
||||
enables it?) is *mostly* moot under #552 by construction — a side only
|
||||
applies the speedup after the peer proves it will too — but the brief
|
||||
window during negotiation, and the moment either side falls back after a
|
||||
mid-session peer loss, still exist. `test/test_uart_loopback.c` cannot
|
||||
prove or disprove this: it is one process talking to itself and
|
||||
structurally cannot model two peers at different divisors (same
|
||||
limitation the loopback transport has always had for this class of
|
||||
question).
|
||||
|
||||
## Decisions log
|
||||
|
||||
- TCP before netpacket (user, 2026-07-31) — frontend-agnostic + testable first.
|
||||
|
||||
@@ -74,8 +74,9 @@ Desktop/testing shortcuts: environment variables `VJ_NETLINK_HOST` and
|
||||
|
||||
## Making link play feel snappier (optional, not authentic)
|
||||
|
||||
*Network Link Wire Speed* (default **Off**) clocks the emulated serial
|
||||
port 2x or 4x faster than the real hardware ran it.
|
||||
*Network Link Wire Speed* (default **Off**, values **Off** / **Auto**)
|
||||
clocks the emulated serial port faster than the real hardware ran it, when
|
||||
the console on the other end agrees to.
|
||||
|
||||
You do not need it to play. What it fixes is a specific feel: in a
|
||||
strictly lockstep title neither console can draw the next frame until the
|
||||
@@ -90,12 +91,16 @@ That slowness is *real* — a Voice Modem or a JagLink cable behaved exactly
|
||||
this way — which is why this is off by default and labelled an
|
||||
enhancement rather than a fix.
|
||||
|
||||
- **Set it on both consoles, to the same value.** Each side only speeds
|
||||
up its own half of the exchange, so one side alone gives you part of
|
||||
the improvement. A mismatched pair still plays correctly and stays in
|
||||
sync; it just gets less benefit. (Verified, not assumed:
|
||||
`test/tools/netlink_wire_speed_test.sh`.)
|
||||
- Start at 2x. Go to 4x if you still notice it.
|
||||
- **Set *Auto* on either or both consoles — there is nothing to match.**
|
||||
The two cores agree on the speedup between themselves the moment the
|
||||
link comes up. If the other side is running an older core, has this set
|
||||
to *Off*, or never answers, this side quietly stays at authentic timing
|
||||
instead of running ahead on its own — a mismatch is no longer something
|
||||
you can accidentally create.
|
||||
- Only works over a direct *Network Link* (TCP host/client). Frontend
|
||||
netplay (RetroArch's own netplay session) has no channel for the two
|
||||
cores to negotiate over, so link play there always runs authentic
|
||||
timing regardless of this setting.
|
||||
- If a game starts behaving oddly on the link, put it back to Off before
|
||||
reporting anything — link timing bug reports are only meaningful at
|
||||
Off.
|
||||
|
||||
+13
-8
@@ -177,14 +177,19 @@ side.
|
||||
|
||||
This is authentic — a real Voice Modem at 19200 baud was exactly this
|
||||
slow — so it is addressed as an **opt-in enhancement**, not a bug fix:
|
||||
core option `virtualjaguar_netlink_speed` (default Off) divides the
|
||||
character frame time by 2 or 4. The knob is a single one:
|
||||
`UARTFrameUsec()` in `src/jerry/uart.c` schedules both the TX drain and
|
||||
the RX arrival, and it takes the untouched stock branch whenever the
|
||||
option is off *or* no link transport is selected. It changes only *when*
|
||||
bytes appear, never which bytes or in what order, and is not part of the
|
||||
savestate (event.c already saves the remaining time of the queued UART
|
||||
callbacks). Both consoles should set it, to the same value — see
|
||||
core option `virtualjaguar_netlink_speed` (default Off; `auto`, #552)
|
||||
divides the character frame time by a fixed 4x once the two console
|
||||
instances negotiate that out-of-band and agree (see
|
||||
`docs/netlink-design.md`'s negotiation section) — never a magnitude the
|
||||
player picks, and never applied unilaterally by one side. The knob is a
|
||||
single one: `UARTFrameUsec()` in `src/jerry/uart.c` schedules both the TX
|
||||
drain and the RX arrival, and it takes the untouched stock branch
|
||||
whenever the option is off, no link transport is selected, or the peer
|
||||
has not (yet, or ever) confirmed. It changes only *when* bytes appear,
|
||||
never which bytes or in what order. Unlike the config-level "auto"
|
||||
request, the CONFIRMED divisor is machine-affecting the moment a peer
|
||||
agrees (it reschedules the event queue) and IS part of the savestate
|
||||
(`STATE_VERSION_TEAMTAP`/v13, extended in place) — see
|
||||
`docs/netlink-user-guide.md`.
|
||||
|
||||
## What the emulation does (src/jerry/voicemodem.c)
|
||||
|
||||
+100
-17
@@ -1045,6 +1045,13 @@ void retro_set_environment(retro_environment_t cb)
|
||||
* one and swallow the first UP/DOWN edge. */
|
||||
static int netlink_was_up = -1;
|
||||
|
||||
/* Last logged wire-speedup value (#552), same file-scope-for-iOS reasoning
|
||||
* as netlink_was_up: 1 = stock. UARTWireSpeedup() itself already starts
|
||||
* at 1, so no -1 sentinel is needed -- the first retro_run() call simply
|
||||
* compares 1 to 1 and stays silent, which is correct (nothing to report
|
||||
* yet at frame zero). */
|
||||
static unsigned netlink_was_speedup = 1;
|
||||
|
||||
/* Discovered-host option list (task 4, #467). netlink_last_rebuild_ms
|
||||
* paces SET_CORE_OPTIONS_V2 re-registration (a full RetroArch option-
|
||||
* manager teardown, see netlink_rebuild_host_options() below);
|
||||
@@ -1309,7 +1316,7 @@ static void netlink_apply(int mode, const char *opt_value)
|
||||
const char *src = "default";
|
||||
int want_file = 0;
|
||||
int got_file = 0;
|
||||
int speedup; /* #498 wire-latency divisor, 1 = stock */
|
||||
int wantAuto; /* #552: "auto" selected, 0/1 -- see UARTSetWireSpeedupIntent */
|
||||
/* OSD dedup state -- see netlink_osd_last_mode's declaration comment. */
|
||||
char narrate_host[128];
|
||||
int narrate_device;
|
||||
@@ -1334,20 +1341,22 @@ static void netlink_apply(int mode, const char *opt_value)
|
||||
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &pvar) && pvar.value)
|
||||
JLinkSetWaitEnabled(strcmp(pvar.value, "disabled") != 0);
|
||||
|
||||
/* Wire-latency enhancement (#498). Read raw, like netlink_wait and
|
||||
* the mode itself: a per-title default has no business overriding a
|
||||
* deliberately non-authentic timing choice the user made, and the
|
||||
* setting is only meaningful in the same breath as the transport. */
|
||||
/* Wire-latency enhancement (#498, replaced 2x/4x with "auto" in #552).
|
||||
* Read raw, like netlink_wait and the mode itself: a per-title default
|
||||
* has no business overriding a deliberately non-authentic timing
|
||||
* choice the user made, and the setting is only meaningful in the
|
||||
* same breath as the transport. This sets INTENT only -- whether the
|
||||
* emulated timing actually speeds up is decided by jlink.c's
|
||||
* out-of-band negotiation with the peer (JLinkFrameTick), never here:
|
||||
* the two values used to be the same thing when the option picked a
|
||||
* magnitude directly, and #552 is exactly the split that stopped
|
||||
* being true. */
|
||||
pvar.key = "virtualjaguar_netlink_speed";
|
||||
pvar.value = NULL;
|
||||
speedup = 1;
|
||||
wantAuto = 0;
|
||||
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &pvar) && pvar.value)
|
||||
{
|
||||
int n = atoi(pvar.value); /* "disabled" -> 0 -> clamped to 1 */
|
||||
if (n > 1)
|
||||
speedup = n;
|
||||
}
|
||||
UARTSetWireSpeedup((unsigned)speedup);
|
||||
wantAuto = (strcmp(pvar.value, "auto") == 0) ? 1 : 0;
|
||||
UARTSetWireSpeedupIntent((unsigned)wantAuto);
|
||||
|
||||
netlink_resolve_host(host, sizeof(host), &src, &want_file, &got_file);
|
||||
|
||||
@@ -1446,11 +1455,24 @@ static void netlink_apply(int mode, const char *opt_value)
|
||||
* that changes emulated timing has to announce itself, or a link bug
|
||||
* filed from an accelerated session reads as an emulation defect.
|
||||
* Only worth a line when it can actually take effect -- with no
|
||||
* transport selected UARTFrameUsec() takes the stock branch anyway. */
|
||||
if (speedup > 1 && mode != JLINK_MODE_DISABLED)
|
||||
LOG_INF("[NETLINK] wire speed %dx -- emulated UART runs faster than "
|
||||
"real hardware (enhancement; both consoles should match, and "
|
||||
"link timing bug reports are only valid at Off)\n", speedup);
|
||||
* transport selected UARTFrameUsec() takes the stock branch anyway.
|
||||
* #552 replaced the 2x/4x value list with "auto": this line can only
|
||||
* report what was REQUESTED, not what is actually running -- that is
|
||||
* decided moment-to-moment by jlink.c's negotiation with the peer, so
|
||||
* it says so rather than implying a speedup that may still be
|
||||
* pending or may never arrive (peer too old, not in auto, or gone). */
|
||||
if (wantAuto && mode != JLINK_MODE_DISABLED)
|
||||
{
|
||||
if (mode == JLINK_MODE_TCP_SERVER || mode == JLINK_MODE_TCP_CLIENT)
|
||||
LOG_INF("[NETLINK] wire speed auto requested -- negotiating with "
|
||||
"the peer; runs faster than real hardware only once "
|
||||
"confirmed, and only while the link is up (link timing "
|
||||
"bug reports are only valid with this Off)\n");
|
||||
else
|
||||
LOG_INF("[NETLINK] wire speed auto requested, but frontend "
|
||||
"netplay has no channel to negotiate over -- staying at "
|
||||
"authentic hardware timing\n");
|
||||
}
|
||||
|
||||
/* The "From file" preset selected but no usable address in the file is
|
||||
* a silent 127.0.0.1 fallback -- the one that cost a debugging session. */
|
||||
@@ -3598,6 +3620,24 @@ bool retro_serialize(void *data, size_t size)
|
||||
* blob for every state, silently. */
|
||||
buf += JoystickTeamTapStateSave(buf);
|
||||
|
||||
/* v13 (extended in place, #552): negotiated wire-speedup Effective
|
||||
* value. It is machine-affecting the instant a peer has confirmed it
|
||||
* -- it changes UARTFrameUsec(), which schedules the UART TX/RX
|
||||
* event-queue deadlines via SetCallbackTime() -- so a state saved
|
||||
* mid-negotiation must restore it, or the reload silently reverts to
|
||||
* stock timing while the option still reads "auto" and the peer (if
|
||||
* still connected) keeps running the negotiated rate: exactly the
|
||||
* DAC-registers-at-$F1A148 / hi-res-epoch class of silent determinism
|
||||
* bug this project has shipped twice before. Config Intent is NOT
|
||||
* saved here, same as NTSC/PAL -- see uart.h.
|
||||
*
|
||||
* STRICTLY LAST, after Team Tap: same reasoning as Team Tap's own
|
||||
* comment above -- every v12 AND pre-#552 v13 blob develop has
|
||||
* written ends at the paddle or Team Tap chunk respectively, so
|
||||
* appending here (rather than interleaving) keeps all of them
|
||||
* loadable. */
|
||||
buf += UARTWireSpeedupStateSave(buf);
|
||||
|
||||
written = (size_t)(buf - start);
|
||||
if (written > STATE_SIZE)
|
||||
return false;
|
||||
@@ -3766,6 +3806,20 @@ bool retro_unserialize(const void *data, size_t size)
|
||||
else
|
||||
JoystickTeamTapStateReset();
|
||||
|
||||
/* v13 (extended in place, #552): negotiated wire-speedup Effective
|
||||
* value -- see the matching comment on the save side. Older states
|
||||
* (including a pre-#552 v13 blob, e.g. one written between the Team
|
||||
* Tap merge and this one) carry no such field; falling back to stock
|
||||
* here is exactly what a pre-#552 core was, and jlink.c's own
|
||||
* per-frame reconciliation (JLinkNegTick) will re-derive the real
|
||||
* value within one frame regardless -- this call only avoids running
|
||||
* one frame on a stale Effective left over from whatever the session
|
||||
* was doing before the load. */
|
||||
if (version >= STATE_VERSION_TEAMTAP)
|
||||
buf += UARTWireSpeedupStateLoad(buf);
|
||||
else
|
||||
UARTSetWireSpeedupEffective(1);
|
||||
|
||||
/* tomRam8 was restored raw above; recompute the DRAM/refresh timing
|
||||
* that bus_arbiter derives from MEMCON1/MEMCON2 so it matches the
|
||||
* loaded state (dram_row_miss/rom_clocks/dram_refresh_clks from
|
||||
@@ -5061,6 +5115,10 @@ void retro_deinit(void)
|
||||
* core would otherwise start the next session believing the previous
|
||||
* one's peer was still attached and skip the first UP edge. */
|
||||
netlink_was_up = -1;
|
||||
/* #552 wire-speedup edge tracker: same reasoning -- a resident core
|
||||
* would otherwise carry the previous session's "already confirmed"
|
||||
* state into the next one and stay silent on the first negotiation. */
|
||||
netlink_was_speedup = 1;
|
||||
/* OSD narration dedup (task 5, #467): same iOS-no-dlclose reasoning --
|
||||
* a resident core would otherwise believe the new session's first
|
||||
* mode/device/host is identical to the previous session's last one and
|
||||
@@ -5344,6 +5402,31 @@ void retro_run(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* #552: report the OUTCOME of the auto negotiation jlink.c runs every
|
||||
* JLinkFrameTick(), edge-only like the link up/down block just above.
|
||||
* The request-time log line in netlink_apply() can only say "auto was
|
||||
* asked for" -- confirmation (or the peer never answering) happens
|
||||
* asynchronously, frames after any option changed, so it has to be
|
||||
* caught here instead. */
|
||||
{
|
||||
unsigned curSpeedup = UARTWireSpeedup();
|
||||
if (curSpeedup != netlink_was_speedup)
|
||||
{
|
||||
if (curSpeedup > 1)
|
||||
{
|
||||
LOG_INF("[NETLINK] wire speed auto CONFIRMED -- peer agreed, "
|
||||
"emulated UART now %ux faster than real hardware "
|
||||
"(link timing bug reports are only valid at Off)\n",
|
||||
curSpeedup);
|
||||
netlink_osd("Link speed: auto (%ux)", curSpeedup);
|
||||
}
|
||||
else if (netlink_was_speedup > 1)
|
||||
LOG_INF("[NETLINK] wire speed back to stock (link dropped, "
|
||||
"option changed, or peer no longer confirmed)\n");
|
||||
netlink_was_speedup = curSpeedup;
|
||||
}
|
||||
}
|
||||
|
||||
/* Rebuild the host picker only when the peer set actually changed --
|
||||
* never on a timer. A second SET_CORE_OPTIONS_V2 tears down and
|
||||
* rebuilds RetroArch's whole option manager (runloop.c), so doing it
|
||||
|
||||
@@ -383,13 +383,12 @@ struct retro_core_option_v2_definition option_defs_us[] = {
|
||||
"virtualjaguar_netlink_speed",
|
||||
"Network Link Wire Speed (Enhancement)",
|
||||
NULL,
|
||||
"Clocks the emulated serial port faster than the real hardware did, so a link game's lockstep exchange finishes inside one video frame instead of spilling into the next. Ultra Vortek's Voice Modem mode settles at 19200 baud and swaps about ten bytes each way per frame, roughly 5.8 ms of pure wire time per direction, which is why you do not see your own move until the round trip completes. Not authentic -- a real Voice Modem or JagLink cable is exactly this slow -- so it is off by default. BOTH consoles should set it, and to the same value: each side only speeds up its own half of the exchange, so one side alone gives you part of the benefit. If a game starts dropping link data, step it back down. Ignored unless Network Link is actually selected.",
|
||||
"Clocks the emulated serial port faster than the real hardware did, so a link game's lockstep exchange finishes inside one video frame instead of spilling into the next. Ultra Vortek's Voice Modem mode settles at 19200 baud and swaps about ten bytes each way per frame, roughly 5.8 ms of pure wire time per direction, which is why you do not see your own move until the round trip completes. Not authentic -- a real Voice Modem or JagLink cable is exactly this slow -- so it is off by default. 'Auto' has the two consoles agree on the speedup between themselves at link-up: nothing to match by hand, and if the other side is running an older core, isn't in Auto too, or never answers, this side quietly stays at authentic timing instead of running ahead alone. Only takes effect over a direct Network Link (TCP host/client) -- frontend netplay (RetroArch's own netplay session) has no channel for the two cores to negotiate over and always runs authentic timing regardless of this setting. If a game starts dropping link data, turn this off. Ignored unless Network Link is actually selected.",
|
||||
NULL,
|
||||
"network",
|
||||
{
|
||||
{ "disabled", "Off (authentic hardware timing)" },
|
||||
{ "2", "2x faster" },
|
||||
{ "4", "4x faster" },
|
||||
{ "auto", "Auto (negotiated with peer)" },
|
||||
{ NULL, NULL },
|
||||
},
|
||||
"disabled"
|
||||
|
||||
+11
-1
@@ -68,7 +68,17 @@ extern "C" {
|
||||
* adapter's own presence is NOT serialized: user configuration, like
|
||||
* the input-device type (joystick.h). develop only — first bump of
|
||||
* the post-v3.4.0 cycle, so any other in-flight state change this
|
||||
* cycle extends v13 in place rather than bumping again. */
|
||||
* cycle extends v13 in place rather than bumping again.
|
||||
* EXTENDED IN PLACE (still v13): #552 replaced the netlink speed
|
||||
* option's 2x/4x values with a negotiated "auto" and appended one
|
||||
* trailing uint32 — the negotiated wire-speedup Effective divisor —
|
||||
* strictly after the Team Tap chunk. Unlike Team Tap's config-only
|
||||
* "auto" INTENT (never serialized, same as NTSC/PAL), the negotiated
|
||||
* value is machine-affecting the moment a peer confirms it: it
|
||||
* changes UARTFrameUsec(), which schedules the UART TX/RX
|
||||
* event-queue deadlines. A pre-#552 v13 state (Team Tap only) reads
|
||||
* nothing here and the loader falls back to stock (1), which is
|
||||
* exactly what a pre-#552 core was — see UARTWireSpeedupStateLoad(). */
|
||||
#define STATE_MAGIC 0x564A5353 /* "VJSS" */
|
||||
#define STATE_VERSION 13
|
||||
/* Oldest layout retro_unserialize still accepts. States between
|
||||
|
||||
+266
-1
@@ -13,6 +13,7 @@
|
||||
#include "jlink_discover.h"
|
||||
#include "voicemodem.h"
|
||||
#include "state.h"
|
||||
#include "uart.h" /* UARTWireSpeedupIntent/UARTSetWireSpeedupEffective (#552) */
|
||||
|
||||
#define JLINK_RING_SIZE 256
|
||||
|
||||
@@ -100,6 +101,20 @@ static int jlinkTCPPort = 42171;
|
||||
static uint32_t jlinkTxTotal = 0;
|
||||
static uint32_t jlinkRxTotal = 0;
|
||||
|
||||
/* Wire-speedup negotiation (#552) state -- see the big comment ahead of
|
||||
JLinkNegTick() below for the protocol. Declared here (ahead of
|
||||
JLinkClose(), which also touches them) rather than down by that
|
||||
comment, purely for C89 file-scope ordering. */
|
||||
#define JLINK_NEG_RETRY_MS 500u
|
||||
#define JLINK_NEG_MAX_ATTEMPTS 8 /* ~4s ceiling before giving up */
|
||||
static int jlinkNegConfirmed = 0; /* peer proven to also be "auto" */
|
||||
static int jlinkNegGaveUp = 0; /* client: retries exhausted */
|
||||
static int jlinkNegAttempts = 0; /* client: hellos sent so far */
|
||||
static uint32_t jlinkNegLastSendMs = 0;
|
||||
static int jlinkNegWasConnected = 0; /* previous tick's JLinkConnected() */
|
||||
static uint32_t jlinkNegSelfId = 0; /* self-receipt filter, see below */
|
||||
static int jlinkNegSelfIdReady = 0;
|
||||
|
||||
/* Set by JLinkFrameTick when JLinkDiscPoll reports the peer set changed;
|
||||
consumed (read + cleared) by JLinkDiscConsumeChanged so a UI layer can
|
||||
poll cheaply without re-deriving the diff itself. */
|
||||
@@ -242,6 +257,18 @@ void JLinkClose(void)
|
||||
jlinkSamplePending = 0;
|
||||
jlinkLastTxUsec = 0;
|
||||
jlinkReplyEwmaUsec = 0;
|
||||
/* #552: drop any negotiated state along with the connection it was
|
||||
* negotiated for. JLinkNegTick() would self-heal this on the very
|
||||
* next frame regardless (its "not connected" branch resets the same
|
||||
* fields), but doing it here too keeps no stale cross-session state
|
||||
* sitting in these statics between JLinkClose() and that next tick --
|
||||
* the same belt-and-suspenders the fields just above already apply. */
|
||||
jlinkNegConfirmed = 0;
|
||||
jlinkNegAttempts = 0;
|
||||
jlinkNegGaveUp = 0;
|
||||
jlinkNegWasConnected = 0;
|
||||
jlinkNegSelfIdReady = 0;
|
||||
UARTSetWireSpeedupEffective(1);
|
||||
}
|
||||
|
||||
int JLinkMode(void)
|
||||
@@ -390,6 +417,239 @@ void JLinkSetWaitEnabled(int enabled)
|
||||
jlinkWaitEnabled = enabled ? 1 : 0;
|
||||
}
|
||||
|
||||
/* Wire-speedup negotiation (#552). Piggybacks on the LAN discovery
|
||||
socket (jlink_discover.c's JLinkDiscSetRawHandler/JLinkDiscSendTo) --
|
||||
see docs/netlink-design.md and the #552 issue body for why: it is the
|
||||
one channel that is both already bound (for tcp_server/tcp_client) and
|
||||
provably incapable of being mistaken for emulated UART bytes, on a
|
||||
peer that does not implement this included -- a build without #552
|
||||
still has jlink_discover.c from #467, so JLinkDiscDecode() on the far
|
||||
end rejects our differently-magicked packet and the datagram is
|
||||
silently dropped exactly like a hostile/corrupt one always was. Never
|
||||
touches jlinkRing.
|
||||
|
||||
Scope, deliberately conservative rather than forced (see the #552
|
||||
guide): only a single TCP peer (JLinkTCPPeerCount() == 1) negotiates.
|
||||
A CatNet-style multi-drop hub (JLINK_MODE_TCP_SERVER with >1 peer)
|
||||
would need every peer to agree, not just one, and there is no
|
||||
multi-party protocol here -- it simply stays stock. netpacket
|
||||
(frontend netplay) also stays stock: the core only ever sees a
|
||||
client_id from RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE, never a
|
||||
peer address, so there is no address to send a discovery-port
|
||||
negotiation packet to, and reusing the netpacket data channel itself
|
||||
was rejected (any bytes sent over it reach an old peer's UART ring
|
||||
directly -- jlink_netpacket.c has no framing at all). Loopback never
|
||||
reaches here either: JLinkDiscActive() is only ever true for the two
|
||||
TCP modes (see netlink_apply()'s discovery gating in libretro.c).
|
||||
|
||||
There is exactly one non-stock divisor now that #552 replaced the
|
||||
2x/4x value list with disabled/auto (UART_WIRE_SPEEDUP_MAX), so the
|
||||
wire only needs to prove "the peer exists and is also configured for
|
||||
auto" -- no magnitude is exchanged, so there is nothing to clamp
|
||||
beyond what UARTSetWireSpeedupEffective() already clamps to.
|
||||
|
||||
Protocol: the CLIENT already knows the server's address (it dialed
|
||||
it), so it periodically sends a JLinkNegEncode() hello to the
|
||||
server's host at JLinkDiscPort() (the effective discovery port --
|
||||
never the raw JLINK_DISC_PORT macro, so a VJ_DISC_PORT-isolated test
|
||||
dials the same port jlink_discover.c actually bound) until it gets
|
||||
one back or gives up. The SERVER is purely reactive -- JLinkNegOnRaw()
|
||||
below learns the client's address for free from the incoming
|
||||
datagram's source and replies with its own packet as an ack (each
|
||||
side tags its own outgoing packet with a random per-session id and
|
||||
ignores any incoming packet carrying that same id back -- see
|
||||
jlink_discover.h -- so a core can never receive its own hello and
|
||||
"confirm" against itself). Neither side commits to the faster
|
||||
timing (UARTSetWireSpeedupEffective(UART_WIRE_SPEEDUP_MAX))
|
||||
until it has seen a decodable packet from the other -- proof the peer
|
||||
runs #552 and is also in "auto" -- which is exactly the coordination
|
||||
#552 exists to remove needing done by two people, not the safety
|
||||
property the option previously depended on for it.
|
||||
|
||||
KNOWN LIMITATION, measured not assumed (see test/test_jlink_negotiate.c's
|
||||
commit history): two cores on ONE machine, both dialing 127.0.0.1 and
|
||||
both wildcard-binding the discovery port via SO_REUSEPORT (the
|
||||
existing dev/test topology jlink_discover.c documents), will usually
|
||||
never actually confirm -- SO_REUSEPORT load-balances a unicast
|
||||
datagram to exactly one member of the group by an internal hash, and
|
||||
it is entirely plausible for that hash to consistently pick the
|
||||
sender's OWN socket over the other one, so the hello/ack never
|
||||
crosses over. The self-id filter above stops that from being
|
||||
mistaken for a real confirmation; it does not make same-host
|
||||
negotiation succeed. This is safe (falls back to stock, exactly the
|
||||
documented "peer never answers" case) but is a real, current gap for
|
||||
anyone dev-testing two cores against each other on localhost with
|
||||
auto enabled -- it is not merely unproven, it was reproduced. Real
|
||||
play, where the two consoles are on different machines, is
|
||||
unaffected: there is no REUSEPORT ambiguity when the sending socket
|
||||
is not itself a member of the destination host's discovery-port
|
||||
group. VJ_NETLINK_AUTO_DEBUG=1 logs negotiation state transitions
|
||||
(confirmed / gave up / reconciled to stock) to help diagnose either
|
||||
case. */
|
||||
static int JLinkNegEligible(void)
|
||||
{
|
||||
return (jlinkMode == JLINK_MODE_TCP_SERVER
|
||||
|| jlinkMode == JLINK_MODE_TCP_CLIENT)
|
||||
&& JLinkDiscActive()
|
||||
&& UARTWireSpeedupIntent()
|
||||
&& JLinkTCPPeerCount() == 1;
|
||||
}
|
||||
|
||||
/* Lazily generated once per process/session (cleared in JLinkClose() with
|
||||
the rest of the negotiation state, so a fresh session gets a fresh id
|
||||
rather than reusing a value a just-closed session's packets might still
|
||||
be in flight carrying). See jlink_discover.h for why this exists: two
|
||||
cores on one machine legitimately share the discovery port via
|
||||
SO_REUSEPORT, and without this a core can receive its own outbound
|
||||
hello back and "confirm" against itself. */
|
||||
static uint32_t JLinkNegSelfId(void)
|
||||
{
|
||||
if (!jlinkNegSelfIdReady)
|
||||
{
|
||||
srand((unsigned)(JLinkNowMs() ^ (uint32_t)(size_t)&jlinkNegSelfId));
|
||||
jlinkNegSelfId = ((uint32_t)rand() << 16) ^ (uint32_t)rand();
|
||||
jlinkNegSelfIdReady = 1;
|
||||
}
|
||||
return jlinkNegSelfId;
|
||||
}
|
||||
|
||||
/* Registered with jlink_discover.c as its unrecognized-datagram handler;
|
||||
called synchronously from within JLinkDiscPoll(), for BOTH client and
|
||||
server -- either side may receive the other's hello first. */
|
||||
static void JLinkNegOnRaw(const uint8_t *buf, size_t len,
|
||||
const char *from_addr, int from_port)
|
||||
{
|
||||
uint8_t out[JLINK_NEG_PKT_LEN];
|
||||
uint32_t peerId;
|
||||
size_t n;
|
||||
|
||||
if (!JLinkNegDecode(buf, len, &peerId))
|
||||
return; /* not ours -- see jlink_discover.h */
|
||||
if (peerId == JLinkNegSelfId())
|
||||
return; /* our own hello, looped back via SO_REUSEPORT */
|
||||
if (!JLinkNegEligible())
|
||||
return; /* we are not (or no longer) a candidate */
|
||||
if (jlinkNegConfirmed)
|
||||
return; /* already done; bounds the reply ping-pong */
|
||||
|
||||
jlinkNegConfirmed = 1;
|
||||
UARTSetWireSpeedupEffective(UART_WIRE_SPEEDUP_MAX);
|
||||
if (getenv("VJ_NETLINK_AUTO_DEBUG"))
|
||||
fprintf(stderr, "jlink-auto: confirmed with %s:%d (peerId=%08x)\n",
|
||||
from_addr, from_port, peerId);
|
||||
|
||||
/* Reply with OUR OWN id (not an echo of the sender's) so the far side
|
||||
can apply the identical self-filter above. Lost entirely on packet
|
||||
loss is fine -- the client's own retry loop below resends; a
|
||||
duplicate arriving here after jlinkNegConfirmed is already set is a
|
||||
no-op via the guard above. */
|
||||
n = JLinkNegEncode(out, sizeof(out), JLinkNegSelfId());
|
||||
if (n)
|
||||
JLinkDiscSendTo(out, n, from_addr, from_port);
|
||||
}
|
||||
|
||||
/* Called every JLinkFrameTick -- reconciles negotiation state against
|
||||
the CURRENT connection every frame (not once at connect) so a state
|
||||
load, an option toggle, or a peer disconnect all take effect on the
|
||||
very next frame rather than needing a matching special case each.
|
||||
This is also what satisfies "loading a state must not silently
|
||||
re-apply a speedup that is no longer agreed" (#552): a restored
|
||||
Effective value survives only as long as this reconciliation keeps
|
||||
agreeing the session is still eligible and still the SAME connection
|
||||
that earned it (jlinkNegWasConnected forces a fresh handshake, not a
|
||||
trusted carry-over, across any disconnected->connected transition). */
|
||||
static void JLinkNegTick(uint32_t nowMs)
|
||||
{
|
||||
int connected = JLinkConnected();
|
||||
|
||||
/* Test-only mechanism escape hatch, same spirit as jlink.c's own
|
||||
VJ_NETLINK_HOST/VJ_NETLINK_PORT-style overrides: real negotiation
|
||||
needs a real second instance to confirm with, and a harness that
|
||||
runs two real processes on ONE machine (test/tools/netlink_latency.c)
|
||||
hits the SAME-HOST SO_REUSEPORT hazard documented above
|
||||
JLinkNegEligible() -- unicast negotiation between two sockets
|
||||
sharing 127.0.0.1's discovery port cannot be relied on to cross
|
||||
over. VJ_FORCE_WIRE_SPEEDUP=N lets such a harness drive
|
||||
UARTFrameUsec()'s divisor mechanism directly (overrun back-
|
||||
pressure, exchange-rate scaling under REAL event-driven timing)
|
||||
without depending on that negotiation succeeding, and bypasses the
|
||||
rest of this function entirely so the normal reconcile-to-stock
|
||||
logic below can never fight it. Never read by, or reachable from,
|
||||
normal play -- no core option maps to it. */
|
||||
{
|
||||
const char *forced = getenv("VJ_FORCE_WIRE_SPEEDUP");
|
||||
if (forced && forced[0] && atoi(forced) > 1)
|
||||
{
|
||||
UARTSetWireSpeedupEffective((unsigned)atoi(forced));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
JLinkDiscSetRawHandler(JLinkNegOnRaw);
|
||||
|
||||
if (!JLinkNegEligible() || !connected)
|
||||
{
|
||||
if (jlinkNegConfirmed || jlinkNegAttempts || jlinkNegGaveUp)
|
||||
{
|
||||
if (getenv("VJ_NETLINK_AUTO_DEBUG"))
|
||||
fprintf(stderr, "jlink-auto: reconciled to stock (eligible=%d "
|
||||
"connected=%d) -- was confirmed=%d attempts=%d gaveup=%d\n",
|
||||
JLinkNegEligible(), connected, jlinkNegConfirmed,
|
||||
jlinkNegAttempts, jlinkNegGaveUp);
|
||||
jlinkNegConfirmed = 0;
|
||||
jlinkNegAttempts = 0;
|
||||
jlinkNegGaveUp = 0;
|
||||
UARTSetWireSpeedupEffective(1);
|
||||
}
|
||||
jlinkNegWasConnected = connected;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!jlinkNegWasConnected)
|
||||
{
|
||||
/* Fresh connection (including a reconnect to a different peer on
|
||||
the same slot): start the handshake clean rather than trusting
|
||||
whatever a prior connection -- or a loaded savestate -- left
|
||||
behind. */
|
||||
jlinkNegConfirmed = 0;
|
||||
jlinkNegAttempts = 0;
|
||||
jlinkNegGaveUp = 0;
|
||||
UARTSetWireSpeedupEffective(1);
|
||||
}
|
||||
jlinkNegWasConnected = connected;
|
||||
|
||||
if (jlinkNegConfirmed || jlinkNegGaveUp)
|
||||
return;
|
||||
|
||||
/* Only the client reaches out proactively -- it is the side that
|
||||
already has the peer's address (it dialed it). The server is
|
||||
entirely reactive, handled by JLinkNegOnRaw above, which learns
|
||||
the client's address for free from the incoming datagram. */
|
||||
if (jlinkMode != JLINK_MODE_TCP_CLIENT)
|
||||
return;
|
||||
|
||||
if (jlinkNegAttempts > 0
|
||||
&& (uint32_t)(nowMs - jlinkNegLastSendMs) < JLINK_NEG_RETRY_MS)
|
||||
return;
|
||||
|
||||
if (jlinkNegAttempts >= JLINK_NEG_MAX_ATTEMPTS)
|
||||
{
|
||||
jlinkNegGaveUp = 1; /* peer never answered (#552): stay stock */
|
||||
if (getenv("VJ_NETLINK_AUTO_DEBUG"))
|
||||
fprintf(stderr, "jlink-auto: gave up after %d hellos, no reply -- "
|
||||
"staying at stock timing\n", jlinkNegAttempts);
|
||||
return;
|
||||
}
|
||||
{
|
||||
uint8_t out[JLINK_NEG_PKT_LEN];
|
||||
size_t n = JLinkNegEncode(out, sizeof(out), JLinkNegSelfId());
|
||||
if (n)
|
||||
JLinkDiscSendTo(out, n, jlinkTCPHost, JLinkDiscPort());
|
||||
}
|
||||
jlinkNegAttempts++;
|
||||
jlinkNegLastSendMs = nowMs;
|
||||
}
|
||||
|
||||
/* Called once per video frame from retro_run: refill the wait budget
|
||||
from the measured reply latency, and service LAN discovery.
|
||||
|
||||
@@ -406,8 +666,13 @@ void JLinkSetWaitEnabled(int enabled)
|
||||
void JLinkFrameTick(void)
|
||||
{
|
||||
long long budget;
|
||||
uint32_t nowMs = JLinkNowMs();
|
||||
if (JLinkDiscActive())
|
||||
jlinkDiscChanged |= JLinkDiscPoll(JLinkNowMs());
|
||||
jlinkDiscChanged |= JLinkDiscPoll(nowMs);
|
||||
/* #552 wire-speedup negotiation. Runs unconditionally, ahead of the
|
||||
jlinkWaitEnabled early-return below -- negotiation must not depend
|
||||
on the unrelated reply-wait option. */
|
||||
JLinkNegTick(nowMs);
|
||||
if (jlinkDevice == JLINK_DEVICE_VOICEMODEM)
|
||||
VMFrameTick();
|
||||
if (!jlinkWaitEnabled)
|
||||
|
||||
+112
-2
@@ -15,6 +15,17 @@
|
||||
static JLinkPeer discPeers[JLINK_DISC_MAX_PEERS];
|
||||
static int discPeerCount = 0;
|
||||
|
||||
/* #552 raw-datagram hook. Declared unconditionally (not inside the
|
||||
JLINK_DISC_HAVE_NET guard below) so a caller on a socketless build can
|
||||
still register a handler without an #ifdef -- it simply never fires
|
||||
there, same as JLinkDiscActive() always returning 0 on that build. */
|
||||
static JLinkDiscRawFn discRawHandler = NULL;
|
||||
|
||||
void JLinkDiscSetRawHandler(JLinkDiscRawFn fn)
|
||||
{
|
||||
discRawHandler = fn;
|
||||
}
|
||||
|
||||
size_t JLinkDiscEncode(uint8_t *buf, size_t cap, int device, int port,
|
||||
const char *name)
|
||||
{
|
||||
@@ -73,6 +84,43 @@ int JLinkDiscDecode(const uint8_t *buf, size_t len, int *device,
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* #552 negotiation codec -- pure, no sockets. See jlink_discover.h for
|
||||
why this deliberately does NOT share JLinkDiscEncode/Decode's magic or
|
||||
length. */
|
||||
size_t JLinkNegEncode(uint8_t *buf, size_t cap, uint32_t senderId)
|
||||
{
|
||||
if (!buf || cap < JLINK_NEG_PKT_LEN)
|
||||
return 0;
|
||||
memset(buf, 0, JLINK_NEG_PKT_LEN);
|
||||
buf[0] = JLINK_NEG_MAGIC_0; buf[1] = JLINK_NEG_MAGIC_1;
|
||||
buf[2] = JLINK_NEG_MAGIC_2; buf[3] = JLINK_NEG_MAGIC_3;
|
||||
buf[4] = (uint8_t)JLINK_NEG_VERSION;
|
||||
/* Bytes 5-7 stay zero (reserved). senderId at 8-11, raw byte order --
|
||||
never interpreted as a number, only ever memcmp'd against the
|
||||
reader's own id (see jlink_discover.h), so there is no endianness
|
||||
concern crossing hosts. */
|
||||
buf[8] = (uint8_t)(senderId & 0xFFu);
|
||||
buf[9] = (uint8_t)((senderId >> 8) & 0xFFu);
|
||||
buf[10] = (uint8_t)((senderId >> 16) & 0xFFu);
|
||||
buf[11] = (uint8_t)((senderId >> 24) & 0xFFu);
|
||||
return JLINK_NEG_PKT_LEN;
|
||||
}
|
||||
|
||||
int JLinkNegDecode(const uint8_t *buf, size_t len, uint32_t *senderId)
|
||||
{
|
||||
if (!buf || len < JLINK_NEG_PKT_LEN)
|
||||
return 0;
|
||||
if (buf[0] != JLINK_NEG_MAGIC_0 || buf[1] != JLINK_NEG_MAGIC_1
|
||||
|| buf[2] != JLINK_NEG_MAGIC_2 || buf[3] != JLINK_NEG_MAGIC_3)
|
||||
return 0;
|
||||
if (buf[4] != JLINK_NEG_VERSION)
|
||||
return 0;
|
||||
if (senderId)
|
||||
*senderId = (uint32_t)buf[8] | ((uint32_t)buf[9] << 8)
|
||||
| ((uint32_t)buf[10] << 16) | ((uint32_t)buf[11] << 24);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void JLinkDiscPeersReset(void)
|
||||
{
|
||||
memset(discPeers, 0, sizeof(discPeers));
|
||||
@@ -169,6 +217,7 @@ const JLinkPeer *JLinkDiscPeerAt(int i)
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <netdb.h> /* getaddrinfo -- JLinkDiscSendTo (#552) */
|
||||
#endif
|
||||
|
||||
static int discSock = -1;
|
||||
@@ -187,8 +236,14 @@ static char discSelfName[JLINK_DISC_NAME_MAX];
|
||||
run's beacon is silently consumed by the other run's listener. The repo hit
|
||||
the same class before with a fixed 42171 (see netlink_pair_test.sh) and
|
||||
settled on PID-spread ports below 32768; this override is how the discovery
|
||||
tests do the same without moving the shipped protocol port. */
|
||||
static int JLinkDiscPort(void)
|
||||
tests do the same without moving the shipped protocol port.
|
||||
|
||||
Exposed (not static) since #552: jlink.c's negotiation hello/ack
|
||||
(JLinkDiscSendTo) must target the SAME effective port this module
|
||||
actually bound, or a VJ_DISC_PORT-isolated test would bind one port
|
||||
while jlink.c dials the hardcoded 42170 -- silently talking to nobody
|
||||
in an isolated test process, and pointlessly rigid for production too. */
|
||||
int JLinkDiscPort(void)
|
||||
{
|
||||
const char *e;
|
||||
int p;
|
||||
@@ -328,7 +383,21 @@ int JLinkDiscPoll(uint32_t now_ms)
|
||||
if (n <= 0)
|
||||
break;
|
||||
if (!JLinkDiscDecode(pkt, (size_t)n, &dev, &port, name, sizeof(name)))
|
||||
{
|
||||
/* Not a beacon -- offer it to a registered second protocol
|
||||
(#552) before dropping it, exactly like an out-of-spec or
|
||||
hostile packet always has been dropped here. */
|
||||
if (discRawHandler)
|
||||
{
|
||||
char rawAddr[JLINK_DISC_ADDR_MAX];
|
||||
rawAddr[0] = '\0';
|
||||
strncpy(rawAddr, inet_ntoa(from.sin_addr), JLINK_DISC_ADDR_MAX - 1);
|
||||
rawAddr[JLINK_DISC_ADDR_MAX - 1] = '\0';
|
||||
discRawHandler(pkt, (size_t)n, rawAddr,
|
||||
(int)ntohs(from.sin_port));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
/* Ignore our own beacon. Matched on name+port, not source IP:
|
||||
the same machine appears under different addresses depending on
|
||||
which interface the broadcast came back through.
|
||||
@@ -353,11 +422,52 @@ int JLinkDiscPoll(uint32_t now_ms)
|
||||
return changed;
|
||||
}
|
||||
|
||||
/* Unicast send on discSock, independent of discListenOnly -- a
|
||||
listen-only client still needs to reach the host it is negotiating
|
||||
with (#552). to_addr may be a dotted-quad or a name; resolved with
|
||||
getaddrinfo like jlink_tcp.c's client connect (duplicated rather than
|
||||
shared: this file has no dependency on jlink_tcp.c today and the
|
||||
resolve is ~10 lines, not worth a cross-file coupling for). */
|
||||
int JLinkDiscSendTo(const uint8_t *buf, size_t len,
|
||||
const char *to_addr, int to_port)
|
||||
{
|
||||
struct addrinfo hints, *res;
|
||||
struct sockaddr_in to;
|
||||
int rc;
|
||||
|
||||
if (discSock < 0 || !buf || len == 0 || !to_addr || !to_addr[0])
|
||||
return 0;
|
||||
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_DGRAM;
|
||||
res = NULL;
|
||||
rc = getaddrinfo(to_addr, NULL, &hints, &res);
|
||||
if (rc != 0 || !res)
|
||||
return 0;
|
||||
|
||||
memset(&to, 0, sizeof(to));
|
||||
to.sin_family = AF_INET;
|
||||
to.sin_port = htons((unsigned short)to_port);
|
||||
to.sin_addr = ((struct sockaddr_in *)res->ai_addr)->sin_addr;
|
||||
freeaddrinfo(res);
|
||||
|
||||
return sendto(discSock, (const char *)buf, (int)len, 0,
|
||||
(struct sockaddr *)&to, sizeof(to)) >= 0;
|
||||
}
|
||||
|
||||
#else /* no networking */
|
||||
|
||||
int JLinkDiscStart(int a, int b, int c) { (void)a; (void)b; (void)c; return 0; }
|
||||
void JLinkDiscStop(void) {}
|
||||
int JLinkDiscPoll(uint32_t t) { (void)t; return 0; }
|
||||
int JLinkDiscActive(void) { return 0; }
|
||||
int JLinkDiscSendTo(const uint8_t *buf, size_t len, const char *to_addr,
|
||||
int to_port)
|
||||
{
|
||||
(void)buf; (void)len; (void)to_addr; (void)to_port;
|
||||
return 0;
|
||||
}
|
||||
int JLinkDiscPort(void) { return JLINK_DISC_PORT; }
|
||||
|
||||
#endif
|
||||
@@ -22,6 +22,48 @@
|
||||
#define JLINK_DISC_DEV_JAGLINK 0
|
||||
#define JLINK_DISC_DEV_VOICEMODEM 1
|
||||
|
||||
/* Wire-speedup negotiation (#552) -- a second, unrelated small protocol
|
||||
sharing this module's already-bound UDP socket/port rather than opening
|
||||
a socket of its own (no second Local Network permission prompt, no new
|
||||
port to firewall/forward). Distinct magic AND a length far shorter
|
||||
than JLINK_DISC_PKT_LEN, so it can never be mistaken for a beacon
|
||||
packet by JLinkDiscDecode(), in either direction: a build that only
|
||||
knows the beacon format sees buf[0..3] != "VJAG" (or, if it ever did
|
||||
collide on magic, len < JLINK_DISC_PKT_LEN) and takes the existing
|
||||
"not a valid packet, ignore it" path -- the same path an out-of-spec
|
||||
or hostile packet already takes today. Nothing negotiation-related
|
||||
ever reaches the emulated UART ring; that ring is fed exclusively by
|
||||
jlink.c's TCP/netpacket byte transports, never by this socket. */
|
||||
#define JLINK_NEG_MAGIC_0 'V'
|
||||
#define JLINK_NEG_MAGIC_1 'J'
|
||||
#define JLINK_NEG_MAGIC_2 'N'
|
||||
#define JLINK_NEG_MAGIC_3 'G'
|
||||
#define JLINK_NEG_VERSION 1
|
||||
#define JLINK_NEG_PKT_LEN 12 /* magic(4) + version(1) + reserved(3) + senderId(4) */
|
||||
|
||||
/* Pure codec, no sockets -- unit-testable like JLinkDiscEncode/Decode.
|
||||
The packet carries almost no payload -- "this is a #552 auto-negotiate
|
||||
hello/ack" -- because there is exactly one non-stock divisor to agree
|
||||
on (UART_WIRE_SPEEDUP_MAX), so the wire only needs to prove the sender
|
||||
exists and understands the protocol -- see uart.h and jlink.c.
|
||||
|
||||
senderId is the one exception, and it is NOT part of the protocol
|
||||
semantics -- it exists purely so a receiver can tell "this is my own
|
||||
packet, looped back to me" from "this is a real peer". Two cores on
|
||||
ONE machine sharing the discovery port via SO_REUSEPORT is a normal,
|
||||
documented topology here (jlink_discover.c's own JLinkDiscStart()
|
||||
comment); on that topology a socket can receive its OWN outbound
|
||||
datagram back, and without a self-check a lone core would "confirm"
|
||||
wire-speedup negotiation against itself. The beacon protocol already
|
||||
solved the analogous problem for itself (JLinkDiscPoll()'s self-beacon
|
||||
filter, matched on name+port); this is the same fix for negotiation,
|
||||
just cheaper -- a random per-process token instead of a hostname
|
||||
comparison. Random, not sequential: it exists to be unpredictable
|
||||
enough that two independent processes essentially never collide, not
|
||||
to identify a peer or survive a restart. */
|
||||
size_t JLinkNegEncode(uint8_t *buf, size_t cap, uint32_t senderId);
|
||||
int JLinkNegDecode(const uint8_t *buf, size_t len, uint32_t *senderId);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
char name[JLINK_DISC_NAME_MAX];
|
||||
@@ -51,4 +93,29 @@ void JLinkDiscStop(void);
|
||||
int JLinkDiscPoll(uint32_t now_ms);
|
||||
int JLinkDiscActive(void);
|
||||
|
||||
/* Hook for the #552 negotiation protocol (or any future second protocol)
|
||||
to ride this module's socket without jlink_discover.c knowing anything
|
||||
about its semantics. JLinkDiscPoll() calls the registered function for
|
||||
every datagram JLinkDiscDecode() does NOT recognize as a beacon --
|
||||
packets that were silently dropped before this existed, so registering
|
||||
a handler cannot regress beacon behavior. from_addr is a NUL-terminated
|
||||
dotted-quad, valid only for the duration of the call. */
|
||||
typedef void (*JLinkDiscRawFn)(const uint8_t *buf, size_t len,
|
||||
const char *from_addr, int from_port);
|
||||
void JLinkDiscSetRawHandler(JLinkDiscRawFn fn);
|
||||
|
||||
/* Unicast send on the same socket, independent of the beacon/listen role
|
||||
(listen-only clients may still send negotiation packets). to_addr may
|
||||
be a dotted-quad or a hostname (resolved via getaddrinfo, like
|
||||
jlink_tcp.c's client connect). Returns 1 on a successful send, 0 if
|
||||
the discovery socket is not open or the address failed to resolve. */
|
||||
int JLinkDiscSendTo(const uint8_t *buf, size_t len,
|
||||
const char *to_addr, int to_port);
|
||||
|
||||
/* Effective discovery port -- JLINK_DISC_PORT unless VJ_DISC_PORT
|
||||
overrides it (tests only). jlink.c's negotiation hello/ack targets
|
||||
this, not the raw JLINK_DISC_PORT macro, so it always dials whatever
|
||||
port this module actually bound. */
|
||||
int JLinkDiscPort(void);
|
||||
|
||||
#endif
|
||||
+74
-27
@@ -48,8 +48,14 @@ static uint8_t uartTxShift;
|
||||
static uint8_t uartTxBusy; /* shift register clocking out */
|
||||
static uint8_t uartRxBusy; /* an RX frame is in flight */
|
||||
|
||||
/* Wire-latency enhancement (#498). 1 = stock; see UARTSetWireSpeedup. */
|
||||
static unsigned uartWireSpeedup = 1;
|
||||
/* Wire-latency enhancement (#498/#552). See uart.h for the Intent vs
|
||||
Effective split -- Intent is config-derived and never saved; Effective
|
||||
is what UARTFrameUsec() actually applies and IS saved (THE SHARP EDGE:
|
||||
once negotiated at runtime it is machine-affecting state -- it changes
|
||||
the deadlines SetCallbackTime() schedules for UARTRXCallback /
|
||||
UARTTXCallback). */
|
||||
static unsigned uartWireSpeedupIntent = 0; /* 0/1: user wants auto */
|
||||
static unsigned uartWireSpeedupEffective = 1; /* 1 = stock; saved */
|
||||
|
||||
/* One character frame in microseconds at the current divider. */
|
||||
static double UARTFrameUsec(void)
|
||||
@@ -59,7 +65,7 @@ static double UARTFrameUsec(void)
|
||||
branch: with the enhancement off (or no link transport selected)
|
||||
the arithmetic must be textually identical to develop's, so
|
||||
option-off cannot perturb event scheduling by a rounding step. */
|
||||
if (uartWireSpeedup <= 1 || JLinkMode() == JLINK_MODE_DISABLED)
|
||||
if (uartWireSpeedupEffective <= 1 || JLinkMode() == JLINK_MODE_DISABLED)
|
||||
{
|
||||
double cyc = 11.0 * 16.0 * (double)((uint32_t)asiClk + 1u);
|
||||
return cyc * (vjs.hardwareTypeNTSC ? RISC_CYCLE_IN_USEC
|
||||
@@ -67,37 +73,70 @@ static double UARTFrameUsec(void)
|
||||
}
|
||||
{
|
||||
double cyc = 11.0 * 16.0 * (double)((uint32_t)asiClk + 1u)
|
||||
/ (double)uartWireSpeedup;
|
||||
/ (double)uartWireSpeedupEffective;
|
||||
return cyc * (vjs.hardwareTypeNTSC ? RISC_CYCLE_IN_USEC
|
||||
: RISC_CYCLE_PAL_IN_USEC);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhancement knob (#498). Set from the libretro option layer, never
|
||||
from emulated code, and deliberately outside the savestate: like
|
||||
NTSC/PAL it is a runtime setting, and event.c saves the REMAINING time
|
||||
of the queued UART callbacks rather than a rate, so a state captured
|
||||
with the option on and reloaded with it off just plays out the one
|
||||
already-scheduled character at its saved deadline and reschedules at
|
||||
stock timing from there. */
|
||||
void UARTSetWireSpeedup(unsigned divisor)
|
||||
/* Config layer (#552). Set from libretro.c's netlink_apply() on every
|
||||
check_variables(), never from emulated code. wantAuto == 0 also
|
||||
immediately drops any negotiated Effective value back to stock: if the
|
||||
user turns the enhancement off mid-session, the accelerated timing
|
||||
must not linger just because a peer had earlier confirmed it -- and
|
||||
this is the ONLY place UARTSetWireSpeedupEffective(1) needs calling
|
||||
from the config side, since jlink.c's own reconciliation (JLinkFrameTick)
|
||||
independently drops it to stock the instant the link is no longer a
|
||||
confirmed, connected, single-peer "auto" session. */
|
||||
void UARTSetWireSpeedupIntent(unsigned wantAuto)
|
||||
{
|
||||
uartWireSpeedupIntent = wantAuto ? 1u : 0u;
|
||||
if (!uartWireSpeedupIntent)
|
||||
uartWireSpeedupEffective = 1u;
|
||||
}
|
||||
|
||||
unsigned UARTWireSpeedupIntent(void)
|
||||
{
|
||||
return uartWireSpeedupIntent;
|
||||
}
|
||||
|
||||
/* Negotiated value. Written by jlink.c's out-of-band negotiation state
|
||||
machine when a peer confirms it is also in "auto", and by the
|
||||
savestate loader (UARTWireSpeedupStateLoad). Never called from
|
||||
emulated code. */
|
||||
void UARTSetWireSpeedupEffective(unsigned divisor)
|
||||
{
|
||||
if (divisor < 1u)
|
||||
divisor = 1u;
|
||||
/* Upper clamp matches the largest value the option offers. The option
|
||||
layer never produces more, but a RetroArch .cfg is a text file a user
|
||||
can put anything in, and an unbounded divisor drives the character
|
||||
time toward zero -- tens of thousands of UART events per emulated
|
||||
frame. Raise this in step with the option's value list, never
|
||||
independently. */
|
||||
/* Clamp survives a hand-edited .cfg or a corrupt/hostile savestate --
|
||||
an unbounded divisor drives the character time toward zero, tens of
|
||||
thousands of UART events per emulated frame. Raise this in step
|
||||
with UART_WIRE_SPEEDUP_MAX, never independently. */
|
||||
if (divisor > UART_WIRE_SPEEDUP_MAX)
|
||||
divisor = UART_WIRE_SPEEDUP_MAX;
|
||||
uartWireSpeedup = divisor;
|
||||
uartWireSpeedupEffective = divisor;
|
||||
}
|
||||
|
||||
unsigned UARTWireSpeedup(void)
|
||||
{
|
||||
return uartWireSpeedup;
|
||||
return uartWireSpeedupEffective;
|
||||
}
|
||||
|
||||
size_t UARTWireSpeedupStateSave(uint8_t *buf)
|
||||
{
|
||||
uint8_t *start = buf;
|
||||
uint32_t v = (uint32_t)uartWireSpeedupEffective;
|
||||
STATE_SAVE_VAR(buf, v);
|
||||
return (size_t)(buf - start);
|
||||
}
|
||||
|
||||
size_t UARTWireSpeedupStateLoad(const uint8_t *buf)
|
||||
{
|
||||
const uint8_t *start = buf;
|
||||
uint32_t v;
|
||||
STATE_LOAD_VAR(buf, v);
|
||||
UARTSetWireSpeedupEffective(v);
|
||||
return (size_t)(buf - start);
|
||||
}
|
||||
|
||||
static void UARTRaiseIRQ(void)
|
||||
@@ -143,7 +182,7 @@ static void UARTKickRx(void)
|
||||
{
|
||||
if (uartRxBusy || !JLinkRxPending())
|
||||
return;
|
||||
if (uartWireSpeedup > 1 && uartRxFull)
|
||||
if (uartWireSpeedupEffective > 1 && uartRxFull)
|
||||
return;
|
||||
uartRxBusy = 1;
|
||||
SetCallbackTime(UARTRXCallback, UARTFrameUsec(), EVENT_JERRY);
|
||||
@@ -307,17 +346,25 @@ void UARTReset(void)
|
||||
uartTxShift = 0;
|
||||
uartTxBusy = 0;
|
||||
uartRxBusy = 0;
|
||||
/* A fresh per-game reset drops any prior negotiation -- the link (if
|
||||
any) is about to be torn down and reopened by the caller, and a
|
||||
stale Effective value would apply accelerated timing to a session
|
||||
that never negotiated it. Intent is untouched: it is config-owned
|
||||
and JERRYInit() runs AFTER the option layer's check_variables()
|
||||
pass, so clearing it here would silently discard the user's
|
||||
setting (#552, same reasoning #498 already documented for the
|
||||
single-value predecessor of this field). */
|
||||
uartWireSpeedupEffective = 1;
|
||||
}
|
||||
|
||||
void UARTDone(void)
|
||||
{
|
||||
UARTReset();
|
||||
/* Teardown, not per-game reset: the option layer re-applies the divisor
|
||||
on every check_variables(), and UARTReset() runs from JERRYInit()
|
||||
AFTER that, so clearing it there would silently discard the setting.
|
||||
Cleared here so the iOS "cores are never dlclosed, reset every static
|
||||
in deinit" rule holds. */
|
||||
uartWireSpeedup = 1;
|
||||
/* Teardown, not per-game reset: cleared here (both halves) so the
|
||||
iOS "cores are never dlclosed, reset every static in deinit" rule
|
||||
holds -- UARTReset() above already dropped Effective, this adds
|
||||
Intent. */
|
||||
uartWireSpeedupIntent = 0;
|
||||
JLinkClose();
|
||||
}
|
||||
|
||||
|
||||
+39
-8
@@ -15,15 +15,46 @@ void UARTDone(void);
|
||||
void UARTSetLinkMode(int mode); /* JLINK_MODE_* from jlink.h */
|
||||
void UARTPoll(void); /* per frame, after JLinkPoll */
|
||||
|
||||
/* Enhancement (issue #498, NOT authentic): divide the emulated character
|
||||
frame time on an active link so a lockstep pad exchange finishes inside
|
||||
one video frame. 1 = stock hardware timing (the default); only values
|
||||
> 1 change anything, and only while a link transport is selected.
|
||||
Clamped to [1, UART_WIRE_SPEEDUP_MAX] -- keep the max in step with the
|
||||
virtualjaguar_netlink_speed option's value list. */
|
||||
/* Enhancement (issue #498/#552, NOT authentic): divide the emulated
|
||||
character frame time on an active link so a lockstep pad exchange
|
||||
finishes inside one video frame. 1 = stock hardware timing; only a
|
||||
value > 1 changes anything, and only while a link transport is
|
||||
selected. UART_WIRE_SPEEDUP_MAX is now the single non-stock divisor
|
||||
"auto" can ever produce -- #552 replaced the old 2x/4x value list with
|
||||
just disabled/auto, so there is no longer a magnitude for the option
|
||||
layer to pick; the two live JLink instances agree only on whether to
|
||||
use this one compile-time constant at all (see jlink.c's negotiation
|
||||
state machine). Keep this in step with UART_WIRE_SPEEDUP_MAX if it
|
||||
is ever raised.
|
||||
|
||||
Two views of the value, split because they change on different
|
||||
triggers and one of them is machine-visible state (see #552):
|
||||
|
||||
Intent -- config-derived (virtualjaguar_netlink_speed == "auto"),
|
||||
reapplied every check_variables() regardless of link
|
||||
state, and deliberately OUTSIDE the savestate -- same
|
||||
as NTSC/PAL, this is a runtime setting, not part of the
|
||||
emulated machine.
|
||||
Effective -- what UARTFrameUsec() actually uses right now. Only
|
||||
becomes > 1 after jlink.c's out-of-band negotiation
|
||||
confirms the peer is ALSO in "auto"; falls back to 1
|
||||
the instant the link drops, the option is turned off,
|
||||
or a fresh connection has not yet negotiated. This DOES
|
||||
change UARTFrameUsec()'s scheduling of the UART TX/RX
|
||||
callbacks, so unlike Intent it belongs in the savestate
|
||||
(STATE_VERSION_TEAMTAP / v13, extended in place) --
|
||||
otherwise a state saved mid-negotiation would reload
|
||||
running stock timing while the option still reads
|
||||
"auto", silently diverging from a peer that kept the
|
||||
negotiated rate. */
|
||||
#define UART_WIRE_SPEEDUP_MAX 4u
|
||||
void UARTSetWireSpeedup(unsigned divisor);
|
||||
unsigned UARTWireSpeedup(void);
|
||||
void UARTSetWireSpeedupIntent(unsigned wantAuto); /* config layer */
|
||||
unsigned UARTWireSpeedupIntent(void);
|
||||
void UARTSetWireSpeedupEffective(unsigned divisor); /* jlink.c + loader */
|
||||
unsigned UARTWireSpeedup(void); /* effective value */
|
||||
|
||||
size_t UARTWireSpeedupStateSave(uint8_t *buf);
|
||||
size_t UARTWireSpeedupStateLoad(const uint8_t *buf);
|
||||
|
||||
uint16_t UARTReadWord(uint32_t offset);
|
||||
void UARTWriteWord(uint32_t offset, uint16_t data);
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
#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) \
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "jlink_discover.h"
|
||||
|
||||
/* Real-socket negotiation forwarding test (#552) needs plain POSIX
|
||||
sockets, mirroring test_jlink_tcp.c's own guard-free POSIX assumption
|
||||
for this project's test hosts. */
|
||||
#if defined(__unix__) || defined(__APPLE__) || defined(__linux__)
|
||||
#define JLINK_DISC_TEST_HAVE_NET 1
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/select.h>
|
||||
#endif
|
||||
|
||||
static int failures = 0;
|
||||
|
||||
static void check(int cond, const char *what)
|
||||
@@ -111,13 +126,224 @@ static void test_capacity(void)
|
||||
"table caps at JLINK_DISC_MAX_PEERS");
|
||||
}
|
||||
|
||||
/* #552 negotiation codec -- pure, no sockets, same style as the beacon
|
||||
codec tests above. */
|
||||
static void test_neg_roundtrip(void)
|
||||
{
|
||||
uint8_t buf[64];
|
||||
size_t n;
|
||||
uint32_t id = 0;
|
||||
|
||||
n = JLinkNegEncode(buf, sizeof(buf), 0xDEADBEEFu);
|
||||
check(n == JLINK_NEG_PKT_LEN, "encode writes exactly 12 bytes");
|
||||
check(JLinkNegDecode(buf, n, &id) == 1, "decode accepts its own packet");
|
||||
check(id == 0xDEADBEEFu, "senderId survives round-trip");
|
||||
}
|
||||
|
||||
static void test_neg_rejects_bad(void)
|
||||
{
|
||||
uint8_t buf[64];
|
||||
|
||||
JLinkNegEncode(buf, sizeof(buf), 1);
|
||||
check(JLinkNegDecode(buf, JLINK_NEG_PKT_LEN - 1, NULL) == 0,
|
||||
"truncated negotiate packet rejected");
|
||||
buf[0] = 'X';
|
||||
check(JLinkNegDecode(buf, JLINK_NEG_PKT_LEN, NULL) == 0,
|
||||
"wrong magic rejected");
|
||||
buf[0] = JLINK_NEG_MAGIC_0; buf[4] = 99;
|
||||
check(JLinkNegDecode(buf, JLINK_NEG_PKT_LEN, NULL) == 0,
|
||||
"wrong version rejected");
|
||||
}
|
||||
|
||||
/* THE requirement #552 exists to satisfy: a negotiation packet must never
|
||||
be mistakable for a beacon (or vice versa) by the OTHER protocol's
|
||||
decoder -- this is what lets an old peer's JLinkDiscDecode() silently
|
||||
drop a negotiate packet instead of corrupting the peer table, and lets
|
||||
a new peer's JLinkNegDecode() ignore a beacon instead of falsely
|
||||
"confirming" wire-speedup from an unrelated LAN discovery packet. */
|
||||
static void test_neg_and_beacon_never_collide(void)
|
||||
{
|
||||
uint8_t negBuf[64];
|
||||
uint8_t discBuf[64];
|
||||
int dev, port;
|
||||
char name[JLINK_DISC_NAME_MAX];
|
||||
size_t negLen = JLinkNegEncode(negBuf, sizeof(negBuf), 1);
|
||||
size_t discLen = JLinkDiscEncode(discBuf, sizeof(discBuf),
|
||||
JLINK_DISC_DEV_JAGLINK, 42171, "x");
|
||||
|
||||
check(JLinkDiscDecode(negBuf, negLen, &dev, &port, name, sizeof(name)) == 0,
|
||||
"a negotiate packet is never accepted as a beacon");
|
||||
check(JLinkNegDecode(discBuf, discLen, NULL) == 0,
|
||||
"a beacon packet is never accepted as a negotiate packet");
|
||||
}
|
||||
|
||||
#ifdef JLINK_DISC_TEST_HAVE_NET
|
||||
|
||||
/* Real-socket test: a #552 negotiate packet arriving on the discovery
|
||||
port must reach the registered raw handler and must NOT be treated as
|
||||
a beacon (peer table unchanged); a beacon must still work normally and
|
||||
must NOT reach the raw handler. This is the actual interop property
|
||||
the design relies on, exercised over a real loopback socket rather
|
||||
than asserted from the pure codec alone.
|
||||
|
||||
Port is PID-spread via VJ_DISC_PORT (jlink_discover.c already honors
|
||||
this override for exactly this reason -- see its own comment) so a
|
||||
concurrent `make test` run cannot collide on the fixed 42170 protocol
|
||||
port. */
|
||||
static uint8_t rawSeenBuf[64];
|
||||
static size_t rawSeenLen = 0;
|
||||
static int rawSeenCount = 0;
|
||||
static char rawSeenAddr[JLINK_DISC_ADDR_MAX];
|
||||
|
||||
static void on_raw(const uint8_t *buf, size_t len, const char *from_addr,
|
||||
int from_port)
|
||||
{
|
||||
(void)from_port;
|
||||
rawSeenCount++;
|
||||
rawSeenLen = len < sizeof(rawSeenBuf) ? len : sizeof(rawSeenBuf);
|
||||
memcpy(rawSeenBuf, buf, rawSeenLen);
|
||||
strncpy(rawSeenAddr, from_addr, sizeof(rawSeenAddr) - 1);
|
||||
rawSeenAddr[sizeof(rawSeenAddr) - 1] = '\0';
|
||||
}
|
||||
|
||||
static int disc_test_port(void)
|
||||
{
|
||||
return 43000 + (int)(getpid() % 4000);
|
||||
}
|
||||
|
||||
/* Send one UDP datagram to 127.0.0.1:port from a throwaway socket,
|
||||
modeling "a peer on the wire" without going through jlink_discover.c
|
||||
at all -- exactly what a hostile or version-skewed packet looks like
|
||||
from the receiver's point of view. */
|
||||
static void send_raw(int port, const uint8_t *buf, size_t len)
|
||||
{
|
||||
int s = (int)socket(AF_INET, SOCK_DGRAM, 0);
|
||||
struct sockaddr_in to;
|
||||
if (s < 0)
|
||||
return;
|
||||
memset(&to, 0, sizeof(to));
|
||||
to.sin_family = AF_INET;
|
||||
to.sin_addr.s_addr = inet_addr("127.0.0.1");
|
||||
to.sin_port = htons((unsigned short)port);
|
||||
sendto(s, buf, len, 0, (struct sockaddr *)&to, sizeof(to));
|
||||
close(s);
|
||||
}
|
||||
|
||||
/* Give a nonblocking UDP round trip a moment to actually arrive before
|
||||
polling -- loopback is fast but not synchronous. */
|
||||
static void settle(void)
|
||||
{
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 0;
|
||||
tv.tv_usec = 20000;
|
||||
select(0, NULL, NULL, NULL, &tv);
|
||||
}
|
||||
|
||||
static void test_neg_forwarded_not_confused_with_beacon(void)
|
||||
{
|
||||
char portStr[16];
|
||||
int port = disc_test_port();
|
||||
uint8_t negOut[JLINK_NEG_PKT_LEN];
|
||||
uint8_t discOut[JLINK_DISC_PKT_LEN];
|
||||
size_t n;
|
||||
|
||||
sprintf(portStr, "%d", port);
|
||||
#if defined(_WIN32)
|
||||
_putenv_s("VJ_DISC_PORT", portStr);
|
||||
#else
|
||||
setenv("VJ_DISC_PORT", portStr, 1);
|
||||
#endif
|
||||
|
||||
JLinkDiscSetRawHandler(on_raw);
|
||||
check(JLinkDiscStart(1 /* listen_only */, JLINK_DISC_DEV_JAGLINK, 42171) == 1,
|
||||
"discovery socket binds on the isolated test port");
|
||||
|
||||
/* A negotiate packet: must reach the raw handler, must NOT become a
|
||||
peer-table entry (it is not a valid beacon). */
|
||||
JLinkDiscPeersReset();
|
||||
rawSeenCount = 0;
|
||||
n = JLinkNegEncode(negOut, sizeof(negOut), 0x12345678u);
|
||||
send_raw(port, negOut, n);
|
||||
settle();
|
||||
JLinkDiscPoll(1000);
|
||||
check(rawSeenCount == 1, "negotiate packet reached the raw handler");
|
||||
{
|
||||
uint32_t id = 0;
|
||||
check(rawSeenLen == JLINK_NEG_PKT_LEN
|
||||
&& JLinkNegDecode(rawSeenBuf, rawSeenLen, &id) == 1
|
||||
&& id == 0x12345678u,
|
||||
"raw handler received the negotiate packet intact");
|
||||
}
|
||||
check(JLinkDiscPeerCount() == 0,
|
||||
"negotiate packet did NOT create a peer-table entry");
|
||||
|
||||
/* A real beacon: must populate the peer table as before, must NOT
|
||||
reach the raw handler (proves the dispatch only catches what
|
||||
JLinkDiscDecode() itself rejects, never intercepting valid
|
||||
traffic). */
|
||||
JLinkDiscPeersReset();
|
||||
rawSeenCount = 0;
|
||||
n = JLinkDiscEncode(discOut, sizeof(discOut), JLINK_DISC_DEV_JAGLINK,
|
||||
42171, "peer");
|
||||
send_raw(port, discOut, n);
|
||||
settle();
|
||||
JLinkDiscPoll(2000);
|
||||
check(JLinkDiscPeerCount() == 1, "a real beacon still populates the peer table");
|
||||
check(rawSeenCount == 0, "a real beacon never reaches the raw handler");
|
||||
|
||||
/* JLinkDiscSendTo: the unicast send half of the protocol, echoed back
|
||||
to our own throwaway listener as a peer would see it. */
|
||||
{
|
||||
int echoSock = (int)socket(AF_INET, SOCK_DGRAM, 0);
|
||||
struct sockaddr_in sa;
|
||||
uint8_t rbuf[64];
|
||||
int got = -1;
|
||||
|
||||
if (echoSock >= 0)
|
||||
{
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sin_family = AF_INET;
|
||||
sa.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
sa.sin_port = 0;
|
||||
if (bind(echoSock, (struct sockaddr *)&sa, sizeof(sa)) == 0)
|
||||
{
|
||||
socklen_t slen = sizeof(sa);
|
||||
int echoPort;
|
||||
getsockname(echoSock, (struct sockaddr *)&sa, &slen);
|
||||
echoPort = ntohs(sa.sin_port);
|
||||
fcntl(echoSock, F_SETFL, fcntl(echoSock, F_GETFL, 0) | O_NONBLOCK);
|
||||
|
||||
check(JLinkDiscSendTo(negOut, JLINK_NEG_PKT_LEN, "127.0.0.1",
|
||||
echoPort) == 1,
|
||||
"JLinkDiscSendTo reports success");
|
||||
settle();
|
||||
got = (int)recv(echoSock, rbuf, sizeof(rbuf), 0);
|
||||
}
|
||||
close(echoSock);
|
||||
}
|
||||
check(got == (int)JLINK_NEG_PKT_LEN,
|
||||
"JLinkDiscSendTo actually delivers the packet");
|
||||
}
|
||||
|
||||
JLinkDiscStop();
|
||||
JLinkDiscSetRawHandler(NULL);
|
||||
}
|
||||
|
||||
#endif /* JLINK_DISC_TEST_HAVE_NET */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_roundtrip();
|
||||
test_rejects_bad();
|
||||
test_name_never_unterminated();
|
||||
test_peer_table();
|
||||
test_neg_roundtrip();
|
||||
test_neg_rejects_bad();
|
||||
test_neg_and_beacon_never_collide();
|
||||
test_capacity();
|
||||
#ifdef JLINK_DISC_TEST_HAVE_NET
|
||||
test_neg_forwarded_not_confused_with_beacon();
|
||||
#endif
|
||||
if (failures) { printf("test_jlink_discover: %d FAILURE(S)\n", failures); return 1; }
|
||||
printf("test_jlink_discover: all passed\n");
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
/* test_jlink_negotiate.c -- integration test for the #552 wire-speedup
|
||||
negotiation state machine (src/jerry/jlink.c: JLinkNegTick/JLinkNegOnRaw).
|
||||
|
||||
jlink.c is single-instance (file-scope statics, like production), so
|
||||
this test cannot run two REAL jlink.c cores against each other in one
|
||||
process. Instead it drives ONE real client-mode jlink.c/uart.c pair
|
||||
against a hand-crafted fake peer built from plain sockets -- exactly
|
||||
what a real peer looks like from jlink.c's point of view, whether that
|
||||
peer runs #552 or not. This is what proves the interop property #552
|
||||
depends on: a peer that only accepts the TCP connection and never
|
||||
answers on the discovery port (an old build, or one not in "auto")
|
||||
must NEVER see the emulated timing accelerate. */
|
||||
#define _DEFAULT_SOURCE 1 /* usleep/select under -std=c99 on glibc */
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/select.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <fcntl.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"
|
||||
#include "src/jerry/jlink_discover.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;
|
||||
bool JERRYIRQEnabled(int irq) { (void)irq; return false; }
|
||||
void JERRYSetPendingIRQ(int irq) { (void)irq; }
|
||||
void m68k_set_irq(unsigned int level) { (void)level; }
|
||||
int TOMIRQEnabled(int irq) { (void)irq; return 0; }
|
||||
|
||||
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)
|
||||
|
||||
/* Port bands, PID-spread, same convention as test_jlink_tcp.c's own
|
||||
comment: below every OS's ephemeral floor so a stray outgoing
|
||||
connection on a CI runner cannot collide, and distinct from
|
||||
test_jlink_discover.c's own band (43000+) so the two test binaries
|
||||
never fight over a port if a runner happens to launch them together. */
|
||||
static int tcp_port(void) { return 45000 + (int)(getpid() % 1500); }
|
||||
static int disc_port(void) { return 46500 + (int)(getpid() % 1500); }
|
||||
|
||||
/* ---- fake peer: plain sockets, deliberately NOT going through
|
||||
jlink.c/jlink_discover.c at all -- this models "whatever is on the
|
||||
other end of the wire", #552-aware or not. ---- */
|
||||
static int fakeListen = -1;
|
||||
static int fakeAccepted = -1;
|
||||
|
||||
/* FINDING, not just a test workaround: the real client's discSock binds
|
||||
INADDR_ANY:dport with SO_REUSEPORT (jlink_discover.c, so two cores on
|
||||
ONE machine can share the fixed discovery port). A first cut of this
|
||||
fake peer ALSO bound INADDR_ANY:dport (mirroring "two real instances
|
||||
sharing 127.0.0.1") and measured that UNICAST negotiation between two
|
||||
sockets sharing a port via SO_REUSEPORT is NOT reliable: REUSEPORT
|
||||
load-balances a given unicast datagram to exactly one member of the
|
||||
group by an internal hash, and both the client's hello and the fake
|
||||
peer's reply were observed hairpinning back to their own sender
|
||||
instead of crossing over -- every attempt, not intermittently.
|
||||
SO_REUSEPORT's model is interchangeable workers sharing a listen
|
||||
queue, not two distinct peers each expecting delivery; broadcast fans
|
||||
out to every group member correctly (why the existing beacon works),
|
||||
unicast does not.
|
||||
|
||||
A distinct bind address (127.0.0.2, standard BSD priority: specific
|
||||
bind wins over wildcard, independent of REUSEPORT) would sidestep
|
||||
this cleanly, but macOS does not route an unconfigured 127.0.0.0/8
|
||||
address to loopback without `ifconfig lo0 alias` -- root, and not
|
||||
something `make test` should need. So this fake peer instead never
|
||||
binds the discovery port at all: it sends its confirm from a plain
|
||||
unbound (ephemeral-port) socket straight to the client's known
|
||||
127.0.0.1:dport, where the client's discSock is now the ONLY socket
|
||||
bound there, so delivery is unambiguous. That means this fake peer
|
||||
cannot *receive* the client's hello (nothing is listening on dport
|
||||
except the client itself) -- it doesn't need to: the confirming
|
||||
scenario only needs to prove a well-formed foreign packet arriving at
|
||||
the client causes JLinkNegOnRaw() to confirm, and the separate
|
||||
"silent peer" scenario below needs the fake peer to send nothing,
|
||||
which trivially requires no discovery socket either. */
|
||||
static int fakeSend = -1;
|
||||
|
||||
static void fake_peer_start(int tport, int dport)
|
||||
{
|
||||
struct sockaddr_in sa;
|
||||
int one = 1;
|
||||
(void)dport;
|
||||
|
||||
fakeListen = (int)socket(AF_INET, SOCK_STREAM, 0);
|
||||
setsockopt(fakeListen, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sin_family = AF_INET;
|
||||
sa.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
sa.sin_port = htons((unsigned short)tport);
|
||||
bind(fakeListen, (struct sockaddr *)&sa, sizeof(sa));
|
||||
listen(fakeListen, 1);
|
||||
fcntl(fakeListen, F_SETFL, fcntl(fakeListen, F_GETFL, 0) | O_NONBLOCK);
|
||||
|
||||
fakeSend = (int)socket(AF_INET, SOCK_DGRAM, 0);
|
||||
fcntl(fakeSend, F_SETFL, fcntl(fakeSend, F_GETFL, 0) | O_NONBLOCK);
|
||||
|
||||
fakeAccepted = -1;
|
||||
}
|
||||
|
||||
static void fake_peer_stop(void)
|
||||
{
|
||||
if (fakeAccepted >= 0) close(fakeAccepted);
|
||||
if (fakeListen >= 0) close(fakeListen);
|
||||
if (fakeSend >= 0) close(fakeSend);
|
||||
fakeAccepted = fakeListen = fakeSend = -1;
|
||||
}
|
||||
|
||||
/* Accept the real jlink.c client's TCP connect, if it has arrived yet.
|
||||
Never sends or receives UART bytes -- this test only needs the
|
||||
connection UP, not carrying traffic. */
|
||||
static void fake_peer_accept_if_pending(void)
|
||||
{
|
||||
if (fakeAccepted < 0 && fakeListen >= 0)
|
||||
{
|
||||
int s = (int)accept(fakeListen, NULL, NULL);
|
||||
if (s >= 0)
|
||||
fakeAccepted = s;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fixed, arbitrary id for the fake peer -- guaranteed distinct from
|
||||
whatever random id the real client generates for itself, so the
|
||||
client's self-receipt filter (JLinkNegOnRaw's peerId == self check,
|
||||
#552) never mistakes the fake peer's reply for its own hello. */
|
||||
#define FAKE_PEER_NEG_ID 0xF4CE9EE5u
|
||||
|
||||
/* Send one well-formed #552 negotiate packet straight to the client's
|
||||
known discovery socket -- see the design note above for why this does
|
||||
not first receive the client's own hello. Returns 1 if the send
|
||||
syscall accepted it. */
|
||||
static int fake_peer_confirm_if_asked(int dport)
|
||||
{
|
||||
uint8_t buf[64];
|
||||
struct sockaddr_in to;
|
||||
size_t n = JLinkNegEncode(buf, sizeof(buf), FAKE_PEER_NEG_ID);
|
||||
int rc;
|
||||
if (!n)
|
||||
return 0;
|
||||
memset(&to, 0, sizeof(to));
|
||||
to.sin_family = AF_INET;
|
||||
to.sin_addr.s_addr = inet_addr("127.0.0.1");
|
||||
to.sin_port = htons((unsigned short)dport);
|
||||
rc = (int)sendto(fakeSend, buf, n, 0, (struct sockaddr *)&to, sizeof(to));
|
||||
if (getenv("VJ_NEG_DEBUG"))
|
||||
fprintf(stderr, "[negdbg] fake_peer sendto dport=%d rc=%d\n", dport, rc);
|
||||
return rc > 0;
|
||||
}
|
||||
|
||||
/* The "silent peer" scenario needs no socket action at all -- kept as a
|
||||
named no-op so the call sites read the same as the confirming case. */
|
||||
static void fake_peer_ignore_neg_traffic(void)
|
||||
{
|
||||
}
|
||||
|
||||
/* Drive the real client instance's per-frame service loop, mirroring
|
||||
retro_run's JLinkFrameTick(); JLinkPoll(); UARTPoll(); ordering
|
||||
(libretro.c). */
|
||||
static void tick(void)
|
||||
{
|
||||
JLinkFrameTick();
|
||||
JLinkPoll();
|
||||
}
|
||||
|
||||
static void settle_usec(int usec)
|
||||
{
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 0;
|
||||
tv.tv_usec = usec;
|
||||
select(0, NULL, NULL, NULL, &tv);
|
||||
}
|
||||
|
||||
static void start_real_client(int tport, int dport)
|
||||
{
|
||||
char portStr[16];
|
||||
sprintf(portStr, "%d", dport);
|
||||
setenv("VJ_DISC_PORT", portStr, 1);
|
||||
|
||||
InitializeEventList();
|
||||
UARTReset();
|
||||
UARTSetWireSpeedupIntent(0);
|
||||
UARTSetWireSpeedupEffective(1);
|
||||
JLinkClose();
|
||||
|
||||
JLinkSetTCPEndpoint("127.0.0.1", tport);
|
||||
UARTSetWireSpeedupIntent(1); /* option = "auto" */
|
||||
UARTSetLinkMode(JLINK_MODE_TCP_CLIENT);
|
||||
JLinkDiscStart(1 /* listen_only */, JLINK_DISC_DEV_JAGLINK, tport);
|
||||
}
|
||||
|
||||
static void stop_real_client(void)
|
||||
{
|
||||
JLinkDiscStop();
|
||||
JLinkClose();
|
||||
unsetenv("VJ_DISC_PORT");
|
||||
}
|
||||
|
||||
/* Pump both the real client and the fake peer together until either the
|
||||
client confirms (UARTWireSpeedup() > 1) or max_ms elapses. responder
|
||||
decides whether the fake peer answers negotiate packets at all. */
|
||||
static void pump_until_confirmed_or_timeout(int dport, int max_ms, int responder)
|
||||
{
|
||||
int elapsed = 0;
|
||||
while (elapsed < max_ms)
|
||||
{
|
||||
tick();
|
||||
fake_peer_accept_if_pending();
|
||||
if (responder)
|
||||
fake_peer_confirm_if_asked(dport);
|
||||
else
|
||||
fake_peer_ignore_neg_traffic();
|
||||
if (UARTWireSpeedup() > 1)
|
||||
return;
|
||||
settle_usec(10000);
|
||||
elapsed += 10;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- scenarios ---- */
|
||||
|
||||
/* The core positive case: a real peer (our fake, playing the confirming
|
||||
role) is on the other end -- the client's negotiated Effective value
|
||||
must reach UART_WIRE_SPEEDUP_MAX, and quickly (well under the 4s
|
||||
give-up ceiling; the fake peer answers on the very first hello). */
|
||||
static void test_confirms_with_answering_peer(void)
|
||||
{
|
||||
int tport = tcp_port(), dport = disc_port();
|
||||
fake_peer_start(tport, dport);
|
||||
start_real_client(tport, dport);
|
||||
|
||||
pump_until_confirmed_or_timeout(dport, 2000, 1 /* responder answers */);
|
||||
CHECK(UARTWireSpeedup() == UART_WIRE_SPEEDUP_MAX,
|
||||
"client negotiates full speedup with a confirming peer");
|
||||
|
||||
stop_real_client();
|
||||
fake_peer_stop();
|
||||
}
|
||||
|
||||
/* THE interop safety property: TCP is UP (so the link "works" in every
|
||||
sense a pre-#552 build would recognize) but the peer never answers on
|
||||
the discovery port -- exactly what an old core, or a peer with the
|
||||
option off, looks like. The client must NEVER apply the speedup
|
||||
unilaterally, for as long as it keeps trying. */
|
||||
static void test_stays_stock_with_silent_peer(void)
|
||||
{
|
||||
int tport = tcp_port() + 1, dport = disc_port() + 1;
|
||||
fake_peer_start(tport, dport);
|
||||
start_real_client(tport, dport);
|
||||
|
||||
/* Give it several retry cycles' worth of wall time (JLINK_NEG_RETRY_MS
|
||||
is 500ms in jlink.c) without ever answering -- short of the ~4s
|
||||
give-up ceiling so this also proves nothing jumps the gun before
|
||||
giving up, not just that giving up eventually lands on stock. */
|
||||
pump_until_confirmed_or_timeout(dport, 1500, 0 /* responder never answers */);
|
||||
CHECK(UARTWireSpeedup() == 1,
|
||||
"client stays at stock timing the whole time a peer never answers");
|
||||
CHECK(fakeAccepted >= 0,
|
||||
"sanity: the TCP link itself really did come up during this window");
|
||||
|
||||
stop_real_client();
|
||||
fake_peer_stop();
|
||||
}
|
||||
|
||||
/* Reconciliation: once confirmed, dropping the connection must revert
|
||||
Effective to stock within one frame -- pinned directly rather than
|
||||
inferred, since this is the property that keeps a stale negotiated
|
||||
state from lingering across a peer loss (#552's savestate-adjacent
|
||||
safety requirement, exercised here without needing an actual
|
||||
save/load -- test_uart_loopback.c covers the save/load half). */
|
||||
static void test_disconnect_reverts_to_stock(void)
|
||||
{
|
||||
int tport = tcp_port() + 2, dport = disc_port() + 2;
|
||||
fake_peer_start(tport, dport);
|
||||
start_real_client(tport, dport);
|
||||
|
||||
pump_until_confirmed_or_timeout(dport, 2000, 1);
|
||||
CHECK(UARTWireSpeedup() == UART_WIRE_SPEEDUP_MAX,
|
||||
"setup: confirmed before testing the drop");
|
||||
|
||||
/* Pull the fake peer's end of the TCP connection. */
|
||||
close(fakeAccepted);
|
||||
fakeAccepted = -1;
|
||||
|
||||
/* Give the real client's TCP poll a moment to notice the drop, then
|
||||
one more tick to reconcile. */
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < 20 && UARTWireSpeedup() != 1; i++)
|
||||
{
|
||||
tick();
|
||||
settle_usec(20000);
|
||||
}
|
||||
}
|
||||
CHECK(UARTWireSpeedup() == 1,
|
||||
"peer disconnect reverts effective wire speed to stock");
|
||||
|
||||
stop_real_client();
|
||||
fake_peer_stop();
|
||||
}
|
||||
|
||||
/* Config layer: turning the option off mid-negotiation must win
|
||||
immediately, even over an already-confirmed value -- pinned here
|
||||
against the REAL negotiation state machine (test_uart_loopback.c
|
||||
pins the same property against the setter API directly). */
|
||||
static void test_intent_off_overrides_confirmed(void)
|
||||
{
|
||||
int tport = tcp_port() + 3, dport = disc_port() + 3;
|
||||
fake_peer_start(tport, dport);
|
||||
start_real_client(tport, dport);
|
||||
|
||||
pump_until_confirmed_or_timeout(dport, 2000, 1);
|
||||
CHECK(UARTWireSpeedup() == UART_WIRE_SPEEDUP_MAX,
|
||||
"setup: confirmed before testing intent-off");
|
||||
|
||||
UARTSetWireSpeedupIntent(0); /* option layer: user turned it off */
|
||||
CHECK(UARTWireSpeedup() == 1,
|
||||
"intent off immediately drops an already-confirmed value");
|
||||
|
||||
/* One more tick must not resurrect it behind the option's back. */
|
||||
tick();
|
||||
CHECK(UARTWireSpeedup() == 1,
|
||||
"reconciliation does not re-arm after intent is turned off");
|
||||
|
||||
stop_real_client();
|
||||
fake_peer_stop();
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
vjs.hardwareTypeNTSC = true;
|
||||
test_confirms_with_answering_peer();
|
||||
test_stays_stock_with_silent_peer();
|
||||
test_disconnect_reverts_to_stock();
|
||||
test_intent_off_overrides_confirmed();
|
||||
printf(failures ? "FAILED (%d)\n" : "OK\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -25,6 +25,15 @@
|
||||
#include "src/jerry/jlink.h"
|
||||
#include "src/jerry/jlink_tcp.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 TCP backend 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) \
|
||||
|
||||
+117
-7
@@ -57,7 +57,12 @@ static void fresh(void)
|
||||
{
|
||||
InitializeEventList();
|
||||
UARTReset();
|
||||
UARTSetWireSpeedup(1); /* #498 enhancement off unless a test sets it */
|
||||
/* #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;
|
||||
@@ -249,7 +254,7 @@ static double frame_usec_at(unsigned speedup, int link_mode)
|
||||
{
|
||||
fresh();
|
||||
UARTSetLinkMode(link_mode);
|
||||
UARTSetWireSpeedup(speedup);
|
||||
UARTSetWireSpeedupEffective(speedup);
|
||||
UARTWriteWord(ASICLK, 0x56);
|
||||
UARTWriteWord(ASIDATA, 0x41);
|
||||
return GetTimeToNextEvent(EVENT_JERRY);
|
||||
@@ -287,12 +292,12 @@ static void test_wire_speedup_ignored_without_link(void)
|
||||
static void test_wire_speedup_clamps(void)
|
||||
{
|
||||
fresh();
|
||||
UARTSetWireSpeedup(0);
|
||||
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. */
|
||||
UARTSetWireSpeedup(100000);
|
||||
UARTSetWireSpeedupEffective(100000);
|
||||
CHECK(UARTWireSpeedup() == UART_WIRE_SPEEDUP_MAX,
|
||||
"absurd divisor clamps to UART_WIRE_SPEEDUP_MAX");
|
||||
}
|
||||
@@ -304,7 +309,7 @@ static void test_wire_speedup_stream_identical(void)
|
||||
{
|
||||
uint16_t st;
|
||||
fresh();
|
||||
UARTSetWireSpeedup(4);
|
||||
UARTSetWireSpeedupEffective(4);
|
||||
UARTWriteWord(ASICLK, 0x56);
|
||||
UARTWriteWord(ASIDATA, 0x01); /* -> shift */
|
||||
UARTWriteWord(ASIDATA, 0x02); /* -> holding */
|
||||
@@ -335,7 +340,7 @@ static void test_wire_speedup_no_overrun_loss(void)
|
||||
{
|
||||
uint16_t st;
|
||||
fresh();
|
||||
UARTSetWireSpeedup(4);
|
||||
UARTSetWireSpeedupEffective(4);
|
||||
UARTWriteWord(ASICLK, 0x56);
|
||||
UARTWriteWord(ASIDATA, 0x01);
|
||||
UARTWriteWord(ASIDATA, 0x02);
|
||||
@@ -359,7 +364,7 @@ static void test_wire_speedup_no_overrun_loss(void)
|
||||
static void test_wire_speedup_link_drop_drains(void)
|
||||
{
|
||||
fresh();
|
||||
UARTSetWireSpeedup(4);
|
||||
UARTSetWireSpeedupEffective(4);
|
||||
UARTWriteWord(ASICLK, 0x56);
|
||||
UARTWriteWord(ASIDATA, 0x11);
|
||||
UARTWriteWord(ASIDATA, 0x22);
|
||||
@@ -370,6 +375,108 @@ static void test_wire_speedup_link_drop_drains(void)
|
||||
"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;
|
||||
@@ -390,6 +497,9 @@ int main(void)
|
||||
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;
|
||||
}
|
||||
@@ -24,13 +24,30 @@
|
||||
* adaptive inside the core, not user-tuned).
|
||||
*
|
||||
* Both roles also take --asiclk N (the ASICLK divider the synthetic cart
|
||||
* programs, default 1) and --speed disabled|2|4 (the #498
|
||||
* virtualjaguar_netlink_speed enhancement, this side only). Together
|
||||
* they turn this tool into the wire-latency A/B: raise --asiclk until the
|
||||
* emulated character frame, not the 60 fps frame slot, sets the exchange
|
||||
* rate, then compare both-stock / one-side-fast / both-fast. Because
|
||||
* --speed is per side, a mismatched pair is a first-class configuration
|
||||
* here -- see netlink_wire_speed_test.sh.
|
||||
* programs, default 1) and --speed disabled|N (the #498/#552
|
||||
* wire-speedup divisor, this side only, N clamped to
|
||||
* UART_WIRE_SPEEDUP_MAX like the real mechanism). Together they turn
|
||||
* this tool into the wire-latency A/B: raise --asiclk until the emulated
|
||||
* character frame, not the 60 fps frame slot, sets the exchange rate,
|
||||
* then compare both-stock / both-fast.
|
||||
*
|
||||
* --speed drives VJ_FORCE_WIRE_SPEEDUP (a jlink.c test-only escape
|
||||
* hatch), NOT the real virtualjaguar_netlink_speed core option: since
|
||||
* #552 replaced the option's 2x/4x values with a negotiated "auto",
|
||||
* getting a specific divisor to apply for real needs the PEER to
|
||||
* confirm over the discovery-port protocol, and two real processes on
|
||||
* ONE machine sharing 127.0.0.1's discovery port hit the SAME-HOST
|
||||
* SO_REUSEPORT hazard documented on jlink.c's JLinkNegEligible() --
|
||||
* unicast negotiation between sockets sharing a port cannot be relied
|
||||
* on to cross over there, so real negotiation is not a dependable way
|
||||
* to drive this A/B. The escape hatch exercises UARTFrameUsec()'s
|
||||
* divisor mechanism directly and unconditionally instead. One
|
||||
* consequence: since it is unconditional, a MISMATCHED pair (one side
|
||||
* forced, one side not) is no longer a meaningful configuration to test
|
||||
* here -- #552 made that combination unreachable through the real
|
||||
* option and negotiation anyway (a side only ever accelerates once its
|
||||
* peer has confirmed the same), so netlink_wire_speed_test.sh no longer
|
||||
* drives one.
|
||||
*
|
||||
* Exit 0 on pass, 1 on fail/error. Driven by netlink_latency_test.sh
|
||||
* and netlink_wire_speed_test.sh.
|
||||
@@ -201,13 +218,24 @@ int main(int argc, char **argv)
|
||||
fprintf(stderr, "usage: netlink_latency <core> --role echo|probe "
|
||||
"[--port N] [--measure-sec N] [--min-rate X] "
|
||||
"[--max-rate X] [--wait enabled|disabled] "
|
||||
"[--asiclk N] [--speed disabled|2|4]\n");
|
||||
"[--asiclk N] [--speed disabled|N]\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
snprintf(port_env, sizeof(port_env), "VJ_NETLINK_PORT=%s", port);
|
||||
putenv(port_env);
|
||||
putenv((char *)"VJ_NETLINK_HOST=127.0.0.1");
|
||||
/* See the file header comment: --speed drives the jlink.c test-only
|
||||
VJ_FORCE_WIRE_SPEEDUP escape hatch, never the real
|
||||
virtualjaguar_netlink_speed option (which is left at its "disabled"
|
||||
default below -- #552 negotiation is not exercised by this tool). */
|
||||
if (strcmp(speed_opt, "disabled") != 0 && atoi(speed_opt) > 1)
|
||||
{
|
||||
static char speed_env[64];
|
||||
snprintf(speed_env, sizeof(speed_env), "VJ_FORCE_WIRE_SPEEDUP=%s",
|
||||
speed_opt);
|
||||
putenv(speed_env);
|
||||
}
|
||||
|
||||
cfg.frames = 1;
|
||||
cfg.options[0].key = "virtualjaguar_netlink";
|
||||
@@ -217,7 +245,7 @@ int main(int argc, char **argv)
|
||||
cfg.options[2].key = "virtualjaguar_netlink_wait";
|
||||
cfg.options[2].value = wait_opt;
|
||||
cfg.options[3].key = "virtualjaguar_netlink_speed";
|
||||
cfg.options[3].value = speed_opt;
|
||||
cfg.options[3].value = "disabled";
|
||||
cfg.num_options = 4;
|
||||
|
||||
if (!harness_init_from_args(&cfg, argc, argv)) return 1;
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# netlink_wire_speed_test.sh — proves what the #498 wire-speed enhancement
|
||||
# (virtualjaguar_netlink_speed) actually does to link latency, and what it
|
||||
# does when only ONE side turns it on.
|
||||
# netlink_wire_speed_test.sh — proves what the #498/#552 wire-speedup
|
||||
# mechanism (UARTFrameUsec()'s divisor, applied via jlink.c's
|
||||
# VJ_FORCE_WIRE_SPEEDUP test-only escape hatch -- see
|
||||
# test/tools/netlink_latency.c's file header) actually does to link
|
||||
# throughput under REAL event-driven emulation timing.
|
||||
#
|
||||
# Three real core pairs over TCP, same synthetic 68K ping-pong cart, the
|
||||
# only difference being each side's virtualjaguar_netlink_speed:
|
||||
# Two real core pairs over TCP, same synthetic 68K ping-pong cart, the
|
||||
# only difference being whether the wire-speedup divisor is forced on:
|
||||
#
|
||||
# off/off both consoles stock -> baseline exchanges/sec
|
||||
# 4x/off one console accelerated -> in between
|
||||
# 4x/4x both accelerated -> best
|
||||
# off/off both consoles stock -> baseline exchanges/sec
|
||||
# N/N both forced to N (default 4x) -> should beat baseline
|
||||
#
|
||||
# Two things are being asserted, and the second matters more than the first:
|
||||
#
|
||||
# 1. Turning it on raises the exchange rate (it does something).
|
||||
# 2. The MISMATCHED pair still exchanges, and lands BETWEEN the two
|
||||
# symmetric configurations. A lockstep link is gated by the slower
|
||||
# side, so one-sided enablement can only under-deliver the benefit --
|
||||
# it must never break or stall the exchange. If the mismatched run
|
||||
# ever comes back at zero, the option needs a both-sides guard rather
|
||||
# than a documentation note.
|
||||
# #552 retired the MISMATCHED (one-side-only) configuration this script
|
||||
# used to also drive: the real virtualjaguar_netlink_speed option only
|
||||
# ever offers disabled/auto now, and "auto" only ever accelerates a side
|
||||
# once its peer has independently confirmed the same -- a side running
|
||||
# alone at a faster rate cannot happen through real option + negotiation
|
||||
# any more, so there is nothing left to test it against. What "one side
|
||||
# faster, does the link still complete" needs is proven differently now:
|
||||
# test/test_jlink_negotiate.c asserts a client with a silent (never-
|
||||
# confirming) peer stays at stock for as long as it keeps trying, which
|
||||
# is the actual safety property #552 replaced the mismatch tolerance
|
||||
# with.
|
||||
#
|
||||
# The cart is driven at --asiclk 1999 (a ~13.2 ms character frame) on
|
||||
# purpose. At the tool's stock ASICLK of 1 a character costs ~13 us and
|
||||
# NOTHING here would be measurable: the 60 fps frame slot, not the emulated
|
||||
# wire, would set the rate in every configuration and all three runs would
|
||||
# tie. Both sides are also --pace'd, because a free-running echo burns its
|
||||
# emulated wire time faster than real time and would hide exactly the
|
||||
# asymmetry this script exists to measure.
|
||||
# wire, would set the rate in every configuration and both runs would tie.
|
||||
# Both sides are also --pace'd, because a free-running echo burns its
|
||||
# emulated wire time faster than real time and would hide the effect this
|
||||
# script exists to measure.
|
||||
#
|
||||
# --wait disabled throughout: the adaptive reply wait hides NETWORK
|
||||
# latency, which on loopback is ~0. Leaving it on only adds wall-clock
|
||||
@@ -45,6 +48,7 @@ BIN="$(dirname "$0")/netlink_latency"
|
||||
OUT="${TMPDIR:-/tmp}/vj_wire_speed_$$"
|
||||
ASICLK="${VJ_WIRE_ASICLK:-1999}"
|
||||
SECS="${VJ_WIRE_SECS:-2}"
|
||||
SPEEDUP="${VJ_WIRE_SPEEDUP:-4}"
|
||||
|
||||
if [ ! -x "$BIN" ]; then
|
||||
echo "netlink_wire_speed_test: $BIN not built" >&2
|
||||
@@ -53,16 +57,18 @@ if [ ! -x "$BIN" ]; then
|
||||
fi
|
||||
mkdir -p "$OUT"
|
||||
|
||||
# run_pair <tag> <probe_speed> <echo_speed> <port>; echoes exchanges/sec.
|
||||
# run_pair <tag> <speed> <port>; echoes exchanges/sec. Both sides always
|
||||
# get the same --speed: see the header comment for why a mismatch is no
|
||||
# longer a configuration worth driving here.
|
||||
run_pair() {
|
||||
tag="$1"; pspeed="$2"; espeed="$3"; port="$4"
|
||||
tag="$1"; speed="$2"; port="$3"
|
||||
"$BIN" "$CORE" --role echo --port "$port" --asiclk "$ASICLK" \
|
||||
--speed "$espeed" --wait disabled --pace \
|
||||
--speed "$speed" --wait disabled --pace \
|
||||
> "$OUT/$tag.echo.log" 2>&1 &
|
||||
epid=$!
|
||||
sleep 1
|
||||
"$BIN" "$CORE" --role probe --port "$port" --asiclk "$ASICLK" \
|
||||
--speed "$pspeed" --wait disabled --measure-sec "$SECS" \
|
||||
--speed "$speed" --wait disabled --measure-sec "$SECS" \
|
||||
> "$OUT/$tag.probe.log" 2>&1
|
||||
prc=$?
|
||||
wait "$epid" 2>/dev/null || true
|
||||
@@ -73,22 +79,20 @@ run_pair() {
|
||||
fi
|
||||
# "<exchanges/sec> <frames/sec paced>". The pacing figure is the
|
||||
# runner's own achieved frame cadence -- the load telemetry the ratio
|
||||
# assertions below need to tell "the option did nothing" apart from
|
||||
# "this machine could not hold 60 fps in any configuration".
|
||||
# assertion below needs to tell "the mechanism did nothing" apart from
|
||||
# "this machine could not hold 60 fps in either configuration".
|
||||
awk '/exchanges\/sec/ { ex = $2 }
|
||||
/frames\/sec paced/ { fps = $2 }
|
||||
END { print ex, fps }' "$OUT/$tag.probe.log"
|
||||
}
|
||||
|
||||
R=$(run_pair off_off disabled disabled "$BASE_PORT") || exit 1
|
||||
R=$(run_pair off_off "disabled" "$BASE_PORT") || exit 1
|
||||
RATE_OFF=${R%% *}; FPS_OFF=${R##* }
|
||||
R=$(run_pair four_off 4 disabled "$((BASE_PORT+1))") || exit 1
|
||||
RATE_ONE=${R%% *}; FPS_ONE=${R##* }
|
||||
R=$(run_pair four_four 4 4 "$((BASE_PORT+2))") || exit 1
|
||||
RATE_BOTH=${R%% *}; FPS_BOTH=${R##* }
|
||||
R=$(run_pair fast_fast "$SPEEDUP" "$((BASE_PORT+1))") || exit 1
|
||||
RATE_FAST=${R%% *}; FPS_FAST=${R##* }
|
||||
|
||||
printf 'netlink_wire_speed_test: asiclk=%s off/off=%s 4x/off=%s 4x/4x=%s exchanges/sec (paced %s/%s/%s fps)\n' \
|
||||
"$ASICLK" "$RATE_OFF" "$RATE_ONE" "$RATE_BOTH" "$FPS_OFF" "$FPS_ONE" "$FPS_BOTH"
|
||||
printf 'netlink_wire_speed_test: asiclk=%s off/off=%s %sx/%sx=%s exchanges/sec (paced %s/%s fps)\n' \
|
||||
"$ASICLK" "$RATE_OFF" "$SPEEDUP" "$SPEEDUP" "$RATE_FAST" "$FPS_OFF" "$FPS_FAST"
|
||||
|
||||
rc=0
|
||||
check() {
|
||||
@@ -101,39 +105,35 @@ check() {
|
||||
}
|
||||
|
||||
# Liveness is the load-bearing assertion and always a hard failure: a
|
||||
# configuration that stalls the link reads as rate 0, and ruling that out
|
||||
# for the MISMATCHED pair is the whole reason this script exists.
|
||||
# configuration that stalls the link reads as rate 0.
|
||||
check "$RATE_OFF > 0" "off/off exchanges (baseline alive)"
|
||||
check "$RATE_ONE > 0" "4x/off exchanges (mismatched pair does NOT stall)"
|
||||
check "$RATE_BOTH > 0" "4x/4x exchanges"
|
||||
check "$RATE_FAST > 0" "${SPEEDUP}x/${SPEEDUP}x exchanges"
|
||||
|
||||
# The ladder is a wall-clock measurement off two paced processes, so it
|
||||
# needs the same load guard netlink_latency_test.sh uses: on a runner that
|
||||
# could not hold its frame slots, the rates measure the machine, not the
|
||||
# option, and a red line there would be noise. 1.10 rather than a tighter
|
||||
# bound for the same reason -- measured headroom on an idle host is 1.34x
|
||||
# (16.9 -> 22.6) and 2.3x (22.6 -> 52.5), so 1.10 still fails a genuinely
|
||||
# inert option while surviving contention.
|
||||
# mechanism, and a red line there would be noise. 1.10 rather than a
|
||||
# tighter bound for the same reason -- measured headroom on an idle host
|
||||
# is 1.34x-2.3x across the old three-way ladder, so 1.10 still fails a
|
||||
# genuinely inert mechanism while surviving contention.
|
||||
PACE_MIN="${VJ_WIRE_MIN_FPS:-45}"
|
||||
paced=$(awk -v a="$FPS_OFF" -v b="$FPS_ONE" -v c="$FPS_BOTH" -v m="$PACE_MIN" \
|
||||
'BEGIN { print (a >= m && b >= m && c >= m) ? "yes" : "no" }')
|
||||
paced=$(awk -v a="$FPS_OFF" -v b="$FPS_FAST" -v m="$PACE_MIN" \
|
||||
'BEGIN { print (a >= m && b >= m) ? "yes" : "no" }')
|
||||
if [ "$paced" = "yes" ]; then
|
||||
check "$RATE_ONE > $RATE_OFF * 1.10" "one side accelerated beats stock"
|
||||
check "$RATE_BOTH > $RATE_ONE * 1.10" "both sides beats one side"
|
||||
check "$RATE_FAST > $RATE_OFF * 1.10" "forced divisor beats stock"
|
||||
else
|
||||
echo "netlink_wire_speed_test: SKIP ladder assertions (paced" \
|
||||
"$FPS_OFF/$FPS_ONE/$FPS_BOTH fps, need >= $PACE_MIN) -- runner too" \
|
||||
"loaded to demonstrate the effect in either direction; liveness" \
|
||||
"checks above still applied"
|
||||
echo "netlink_wire_speed_test: SKIP ladder assertion (paced" \
|
||||
"$FPS_OFF/$FPS_FAST fps, need >= $PACE_MIN) -- runner too loaded" \
|
||||
"to demonstrate the effect; liveness checks above still applied"
|
||||
if [ -f "$(dirname "$0")/../../scripts/test-skip.sh" ]; then
|
||||
bash "$(dirname "$0")/../../scripts/test-skip.sh" record \
|
||||
"Netlink wire speed ladder (#498)" \
|
||||
"runner paced $FPS_OFF/$FPS_ONE/$FPS_BOTH fps, below $PACE_MIN"
|
||||
"Netlink wire speed ladder (#498/#552)" \
|
||||
"runner paced $FPS_OFF/$FPS_FAST fps, below $PACE_MIN"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
echo "netlink_wire_speed_test: PASS (ports $BASE_PORT-$((BASE_PORT+2)))"
|
||||
echo "netlink_wire_speed_test: PASS (ports $BASE_PORT-$((BASE_PORT+1)))"
|
||||
command rm -rf "$OUT"
|
||||
else
|
||||
echo "netlink_wire_speed_test: logs in $OUT" >&2
|
||||
|
||||
@@ -65,11 +65,22 @@ MAPOPTS=(--option virtualjaguar_uart_device=voicemodem
|
||||
# Wire-speed enhancement (#498), per side, so this script doubles as the
|
||||
# safety gate for it: the whole point of the feature is that it may not
|
||||
# change anything the GAME can see, and the modem choreography plus the
|
||||
# pad-word counts below are the only end-to-end check that holds. Set
|
||||
# both to prove the symmetric case, one to prove that a mismatched pair
|
||||
# still completes (it does -- it just gets less of the benefit).
|
||||
# "disabled" is the option's own default, so an unset run is the stock
|
||||
# baseline this has always measured.
|
||||
# pad-word counts below are the only end-to-end check that holds.
|
||||
# "disabled" is the option's own default, so an unset run (the one
|
||||
# `make test` actually exercises) is the stock baseline this has always
|
||||
# measured -- these two env vars are for a human to override manually.
|
||||
#
|
||||
# #552 replaced the option's 2x/4x values with a negotiated "auto":
|
||||
# VJ_UV_SPEED_SERVER/CLIENT=auto sets real intent on that side, but
|
||||
# whether it actually accelerates now depends on jlink.c's discovery-port
|
||||
# negotiation confirming with the peer -- which, for two real processes
|
||||
# on ONE machine sharing 127.0.0.1's discovery port, is not reliable (see
|
||||
# the SO_REUSEPORT hazard documented on jlink.c's JLinkNegEligible()).
|
||||
# There is no longer a numeric value this script can pass to force a
|
||||
# specific side's timing the way "2"/"4" used to -- the old "one side
|
||||
# faster, does the choreography still complete" question is what #552's
|
||||
# negotiation is designed to make structurally impossible to create by
|
||||
# accident in the first place, not a case to keep exercising here.
|
||||
SPEED_SRV="${VJ_UV_SPEED_SERVER:-disabled}"
|
||||
SPEED_CLI="${VJ_UV_SPEED_CLIENT:-disabled}"
|
||||
|
||||
|
||||
Reference in new issue
Block a user