Skip to content

rig: run the RR-Net path at turbo, and measure why DHCP survives it - #205

Merged
JC-000 merged 3 commits into
masterfrom
exp/rrnet-turbo-clock
Sep 6, 2026
Merged

rig: run the RR-Net path at turbo, and measure why DHCP survives it#205
JC-000 merged 3 commits into
masterfrom
exp/rrnet-turbo-clock

Conversation

@JC-000

@JC-000 JC-000 commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Two hardware experiments on the U64E, plus the code that made the second one runnable and the documentation that stops both from being misread.

The question

ip65/drivers/c64timer.s timer_init programs CIA2 timer A to 1000 cycles, continuous, and cascades it into timer B; timer_read returns timer B. CIA timers count phi2 cycles, so a "tick" is a millisecond only if phi2 is 1 MHz. ip65/ip65/dhcp.s budgets MAX_DHCP_MESSAGES_SENT = 12 retries with growing backoff, about 15 seconds. If the CIAs scale with a 48 MHz CPU that budget collapses ~48x to ~0.3 s and DHCP cannot complete — which is why tests/rig_vice_https_macos.py runs DHCP at 1x, and it was one of the two reasons the physical RR-Net rig had only ever run at stock speed.

CLAUDE.md's CIA1 TOD result does not settle it, and was deliberately not used. TOD is a different clock domain, fed from mains rather than phi2; that it measured 0.996 of wall rate under turbo transfers nothing to timers A and B.

Experiment 1 — the timer rate (tools/probe_cia_timer_rate.py, new)

clock ticks / wall-second (mean, n=6)
1 MHz 1023.2
48 MHz 1022.9
ratio 1.000

The CIA timers are realtime. Two things make that more than a ratio:

  • The absolute value is right for a reason a broken method would not have reproduced. NTSC phi2 is 1,022,727 Hz and timer A is 1000 cycles, so the prediction is 1022.7 ticks/s; both clocks land inside 0.05% of it. A phi2-scaled timer would have read ~49,000/s.
  • A negative control. The same probe's loop-iteration counter did scale, 6,266/s to 152,396/s (24.3x). Without it, "the timers are realtime" and "the turbo write silently did nothing" are the same measurement. (24.3x rather than 48x because the loop's three CIA register reads are stretched to bus speed — which is itself the mechanism: the CIAs are on the 1 MHz bus.)

Method notes, because the aliasing trap here points the wrong way: timer B's inverted 16-bit up-count wraps every 65536 ticks, which is 65.5 s at 1 kHz but 1.37 s at 48 kHz, so a host reading raw values across a 15 s interval would alias towards the answer "1000/s". The probe therefore never exports a raw value — the C64 accumulates the modular 16-bit difference between consecutive samples into a 32-bit counter, at a loop period of microseconds against a 1.37 s worst case. Torn host reads are excluded by a REQ/SERVED publish handshake. CIA1 TOD is published as a control and read 0.996-1.004 of wall at both clocks.

Experiment 2 — the same fetch at 48 MHz (TURBO_MHZ=48)

43.1 s from G to CONNECTION CLOSED, against 1,979 s at 1 MHz — 46x. Same rig, same build configuration, same boundary, real silicon on both sides, so unlike the emulator comparison this one is like-for-like.

+1.2s  TCP CONNECTED   +7.7s  CH   +14.1s  RX   +15.2s  PROC   +43.1s  CONNECTION CLOSED

DHCP completed on the automatic attempt with zero retries and the C64 held the pinned lease read out of its own net_local_ip — experiment 1 confirmed on the live path, since a compressed budget would have failed here first.

The thing this run existed to risk did not happen: the CS8900a behaved at 48 MHz. HTTP 200 and the exact 24-byte body out of the C64's own buffer, net_last_error $00, ClientHello with SNI on the wire, application data both ways (2 records from the C64, 8 from the host), body never in cleartext with the positive control found twice, 20 frames sourced from 00:0e:3a:64:64:64 and no third MAC. Capture 14,344 → 23,417 bytes across the window. PRG sha256 9d33683a…, byte-identical to an independent build of the same command line.

The one red check, which is left red

24/25. check_tls_connected failed with tls_state_max = 5 (CERT_VERIFY). It is a sampling oracle: src/tls13.s:303-304 sets tls_state = CONNECTED right after the traffic-key derivation, tls_close (:379) writes it back to IDLE at :382-383, and at 48 MHz the client Finished, GET, response and close all fit inside one poll. tls_last_state is no fallback — sta tls_last_state appears exactly once, at :310, under @error:.

The wire settles it independently: client Finished at +41.95 s, and the GET, response and close all complete by +42.34 s — a 0.39 s window against a 1 s poll, where the 1 MHz run had 17.7 s against a 5 s poll. tls_state_max = 5 is exactly what a 0.4 s window predicts, and the ~45x ratio between those windows matches the 46x speed-up.

Softening it to make a turbo run green is the exact failure this repo has recorded three times (#158, #161, #176), so it stays red and the limitation is documented in the rig, the notes and the README. The handshake's completion at turbo is inference from converging evidence, not the direct observation the 1 MHz run had, and every place that quotes the run says so. Issue #204 tracks the C64-side high-water latch that would make it direct again; scoped after the release, because it is a change to shipped source in the TLS state machine purely for test observability.

What is in the diff

  • tools/probe_cia_timer_rate.py — new. Assembles its own 271-byte 6502 probe with ca65/ld65 at runtime (verified byte-identical to the image actually run), so the measurement is reproducible rather than asserted.

  • tests/rig_ip65_rrnet_hw.pyTURBO_MHZ knob (default 1), turbo set before the reset with a 3 s settle and asserted against the device's own report, 1 s phase cadence at turbo, and docstring sections recording both the retired objection and the one that still stands.

    Two things do change at the default, both called out in the docstring and in CLAUDE.md: the settle went sleep(1.0)sleep(3.0) (harmless, and in the safe direction), and the new clock assertion fires unconditionally, so a stock re-run reports 25 checks, not the first passing run's 24. The extra one sits in the host-side-precondition group and is not a cartridge check; the first run's 24/8/16/15 decomposition stands as history. The assertion earns its place by catching a device that silently ignores the clock write — the same failure the probe's negative control guards against from the other side.

  • Probe provenance, re-verified after the final edits. The image assemble() produces from the committed docstring is 271 bytes, sha256 c1d8bd07adc90691027673a6c8382a7425cb8f7d556be407fa42f769f8cbf3ad, and compares byte-identical to the binary actually written to the U64E during experiment 1. Checked once when the tool was written and again after every subsequent edit to the file, most recently at b6320d0.

  • tests/rig_vice_https_macos.pyguard only. Warp accelerates the whole emulated machine, CIAs included, so the hardware-turbo result does not license removing its 1x DHCP assist. This is the misreading most likely to cost someone an afternoon, so the warning lives where they would go to make it.

  • README.md, CLAUDE.md, docs/engineering-notes.md — the result, its scope and its one weakness.

Scope, unchanged except for the clock

One device, one cartridge, one local listener, the same port-only deviation from the shipped image, and still no server-name validation on ip65. This adds a second clock and nothing else. Not known: whether any of it holds on the C64 Ultimate, at 64 MHz, or on a more marginal CS8900a.

Adversarially reviewed and cleared to merge. The reviewer reproduced the probe's provenance in one command from the committed source (271 bytes, sha256 c1d8bd07…, match confirmed), independently parsed the turbo capture (94 records, 20 from 00:0e:3a:64:64:64, no third MAC, clean DORA with zero retries), and confirmed from the wire that the one red check is an oracle artifact rather than a client defect: client Finished at +41.95 s with the GET, response and close all complete by +42.34 s — a 0.39 s window against a 1 s poll, where the 1 MHz run had 17.7 s against a 5 s poll. Three findings from that review are fixed in b6320d0 and 96cf025.

JC-000 and others added 3 commits September 6, 2026 06:29
The physical RR-Net rig had run only at stock 1 MHz, for two stated
reasons. One of them was a suspicion rather than a measurement, and it is
now measured.

ip65's `timer_read` returns CIA2 timer B, cascaded off timer A at 1000
cycles by `ip65/drivers/c64timer.s`. CIA timers count phi2, so if the
CIAs scaled with a 48 MHz CPU the ~15 s / 12-retry DHCP budget in
`ip65/ip65/dhcp.s` would collapse to ~0.3 s. CLAUDE.md's CIA1 TOD figure
does not answer that — TOD is a different clock domain — so
`tools/probe_cia_timer_rate.py` measures timers A/B directly against host
wall clock. U64E, n=6 intervals per clock: 1023.2 ticks/wall-second at
1 MHz, 1022.9 at 48 MHz, ratio 1.000, both inside 0.05% of the 1022.7/s
that NTSC phi2 predicts. The probe carries its own negative control — its
loop counter scaled 24.3x across the same clocks — because without one a
turbo write that silently did nothing looks identical.

The probe never exports a raw timer value: timer B's inverted 16-bit
up-count wraps every 1.37 s at 48 kHz, which would alias a 15 s interval
towards the answer under test, so the C64 accumulates modular 16-bit
deltas into a 32-bit counter and publishes it through a REQ/SERVED
handshake that cannot be read torn.

`TURBO_MHZ` then runs the same image at turbo, with a 1 s phase cadence
so the timeline does not collapse to three points. First run: 43.1 s 'G'
to CONNECTION CLOSED against 1,979 s at 1 MHz, DHCP on the automatic
attempt, and the CS8900a behaved — which was the open risk. 24 of 25
checks passed; `check_tls_connected` is a sampling oracle and turbo
outruns it, so it is left red and documented rather than softened, and
issue #204 tracks the C64-side latch that would fix it. The default stays
1 MHz, because that is the clock the product ships at.

`tests/rig_vice_https_macos.py` gains a guard against the obvious
misreading: warp accelerates the CIAs too, so its 1x DHCP assist is not
licensed away by a hardware-turbo result.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…argin rationale

Three review fixes, none of which changes behaviour.

The `tls_close` store was cited as `src/tls13.s:380` in the rig, the notes
and CLAUDE.md. Verified against the source: the routine is at :379, `lda
#TLS_STATE_IDLE` at :382 and `sta tls_state` at :383, so the cite is now
`:379`, storing IDLE at `:382-383`. The CONNECTED store is `:303-304`, not
a bare `:303`. The substantive claim is unaffected — `sta tls_last_state`
appears only at :310 under `@error:`, so a clean run leaves it 0 and it is
no fallback.

The new clock assertion fires unconditionally, including at the default
1 MHz, so a stock re-run reports 25 checks where the first passing run
reported 24. Both places that state the decomposition now say so, and both
keep the first run's numbers as history. `RES.check()` call sites go 18 to
19 with it; the 15 delegated verdicts are unchanged.

The probe's 1.37 s wrap figure reads like an error against the ~65 s its
own result implies, because the margin is budgeted against the hypothesis
being refuted rather than the one expected. Said explicitly, along with why
it matters: under the scaling hypothesis a raw 16-bit read across a 15 s
interval would alias *towards* "about 1000/s" and manufacture the realtime
answer it was meant to test for.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ord the line-cite rule

tests/README.md carried a third copy of the rig's check decomposition and
was the one site the 18->19 correction missed — the same defect this
branch exists to fix, a count restated in more places than it is
maintained. Rather than correct it to 19, that sentence now keeps the
qualitative point (not every check is a fact about the cartridge) and
defers the numbers to the rig's own docstring, which is where they move
when a check is added.

Also recorded, in engineering-notes beside the cites it applies to: five
wrong line numbers into src/tls13.s were produced today by four parties,
every one by reading a symbol tool's position output as a 1-based file
line. The rule is to confirm any generated line number against a 1-based
read of the file before it goes into a document; citing the span rather
than the instruction is the cheap second defence, and the ground-truth
list of the ten `sta tls_state` sites is there so the next reader does not
have to re-derive it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@JC-000
JC-000 merged commit bce4203 into master Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant