From 3c4051d865386393a62db94b8edb330036925da2 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:18:32 -0500 Subject: [PATCH 1/6] fix(uci): widen uci_fence to keep 35% margin at 64 MHz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The C64 Ultimate tops out at 64 MHz, where the old 2525-cycle fence (INNER=100, tuned for 48 MHz) shrinks to ~39.5 us — only ~4% over the FPGA's ~38 us floor. Bump UCI_FENCE_INNER to 131 (~3300 cycles): 51.6 us at 64 MHz (35.7% margin), 68.8 us at 48 MHz, ~3.3 ms at 1 MHz. Still negligible for networking at every speed. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UZmMaxyyykvYbCvJAc4QVW --- CLAUDE.md | 10 ++++++---- src/net/uci/uci_regs.inc | 10 +++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5313992..612acb1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -224,10 +224,12 @@ corrupt the UCI command protocol. **Fix:** A nested delay-loop macro `uci_fence` (defined in `src/net/uci/uci_regs.inc`) is inserted after every read/write to UCI registers `$DF1C-$DF1F`. Parameters: `UCI_FENCE_OUTER = 5`, -`UCI_FENCE_INNER = 100`, yielding ~2525 cycles (~52 us at 48 MHz, -35% safety margin). 14 bytes per fence site, 24 fence sites total -(11 write + 13 read). At 1 MHz the same loop costs ~2.5 ms per -access — negligible for networking. +`UCI_FENCE_INNER = 131`, yielding ~3300 cycles (~51.6 us at 64 MHz, +35% safety margin at the C64 Ultimate's top speed; ~68.8 us at +48 MHz). 14 bytes per fence site, 24 fence sites total (11 write + +13 read). At 1 MHz the same loop costs ~3.3 ms per access — +negligible for networking. (The pre-C64U value INNER=100 gave 35% +margin at 48 MHz but only ~4% at 64 MHz.) 48 MHz turbo is fully supported and verified on real U64E hardware. diff --git a/src/net/uci/uci_regs.inc b/src/net/uci/uci_regs.inc index b328ea7..399c281 100644 --- a/src/net/uci/uci_regs.inc +++ b/src/net/uci/uci_regs.inc @@ -60,14 +60,18 @@ UCI_TARGET_NETWORK = $03 ; network stack ; Tuned empirically via binary search at 48 MHz: ; OUTER=3 INNER=121 (~1830 cycles, ~38 µs at 48 MHz) = FAIL ; OUTER=3 INNER=122 (~1845 cycles, ~38.4 µs at 48 MHz) = PASS (minimum) -; OUTER=5 INNER=100 (~2525 cycles, ~52 µs at 48 MHz) = chosen (35% margin) +; OUTER=5 INNER=100 (~2525 cycles, ~52 µs at 48 MHz) = old choice (35% +; margin at 48 MHz, but only ~4% at 64 MHz) +; OUTER=5 INNER=131 (~3300 cycles, ~51.6 µs at 64 MHz) = chosen — keeps +; the 35% margin at the C64 Ultimate's 64 MHz top +; speed (68.8 µs at 48 MHz, ~3.3 ms at 1 MHz) ; -; At 1 MHz the overhead is ~2.5 ms per access — acceptable for networking. +; At 1 MHz the overhead is ~3.3 ms per access — acceptable for networking. ; The macro preserves A and X via the stack, costing ~14 bytes per call ; site (vs 256 for the NOP sled that still wasn't enough). ; ============================================================================= UCI_FENCE_OUTER = 5 ; outer loop iterations -UCI_FENCE_INNER = 100 ; inner loop iterations +UCI_FENCE_INNER = 131 ; inner loop iterations .macro uci_fence pha ; save A From aac0d96536b0365bf7a8818ac856071d3e83499a Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:26:35 -0500 Subject: [PATCH 2/6] feat(uci): interface-fallback DHCP acquire for C64 Ultimate WiFi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The C64 Ultimate has Ethernet AND WiFi; GET_IPADDR(iface=0) returns 0.0.0.0 when the box is on WiFi, which the old single-shot net_dhcp_acquire surfaced as DHCP FAILED (UCI_ERR_NO_IP). Probe interface indices 0..3 and take the first non-zero lease; clean up (drain + ack) after a CMD_FAILED probe so the next one starts from idle. Clear net_last_error on success so no-lease residue from earlier probes doesn't sit next to a good acquire. Costs ~38 B in NET_CODE — boundary with NET_BSS_TAIL shifted by $40 ($1B26 -> $1B66); NET_BSS_TAIL occupancy is $625 so $75 slack remains. Verified on C64 Ultimate firmware 1.1.0 (WiFi): phase2_check PASS, IP acquired on iface 1. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UZmMaxyyykvYbCvJAc4QVW --- cfg/c64-https-uci.cfg | 7 ++++-- src/net/uci/net.s | 58 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/cfg/c64-https-uci.cfg b/cfg/c64-https-uci.cfg index ed4352b..d25005a 100644 --- a/cfg/c64-https-uci.cfg +++ b/cfg/c64-https-uci.cfg @@ -76,13 +76,16 @@ MEMORY { # but only used ~$125 — folding it into NET_BSS_TAIL frees ~370 B # for the libs/nistcurves spill-over). ip65 has had an equivalent # NET_BSS_TAIL since Phase C.4 — UCI now adopts the same pattern. - NET_CODE: start = $2000, size = $1B26, file = %O, define = yes, fill = yes, fillval = $00; + # Grown $40 (iface-fallback loop in net_dhcp_acquire, C64U WiFi + # support) — donated from NET_BSS_TAIL, whose occupancy tops out at + # $625 (LIB_NISTCURVES_P256_BSS) leaving $B5 slack pre-donation. + NET_CODE: start = $2000, size = $1B66, file = %O, define = yes, fill = yes, fillval = $00; # NET_BSS_TAIL spans NET_CODE end through the start of CRYPTO_OVERLAY, # subsuming the historical UCI_BSS_REGION at $4000-$41FF (UCI_BSS now # rides on the head of this combined region). Holds 1.7 KB total — # ld65 distributes UCI_BSS (~293 B) + LIB_NISTCURVES_P256_BSS (~1.5 # KB) + a stub UCI_BSS_REGION definition. - NET_BSS_TAIL: start = $3B26, size = $06DA, file = %O, define = yes, fill = yes, fillval = $00; + NET_BSS_TAIL: start = $3B66, size = $069A, file = %O, define = yes, fill = yes, fillval = $00; # UCI_BSS_REGION kept as a zero-size alias so any external tooling # that resolves the symbol via labels.txt still finds it (the cfg's # `define = yes` emits __UCI_BSS_REGION_START__ etc.). diff --git a/src/net/uci/net.s b/src/net/uci/net.s index 1250d53..4296d68 100644 --- a/src/net/uci/net.s +++ b/src/net/uci/net.s @@ -333,22 +333,36 @@ net_poll: ; ============================================================================= ; net_dhcp_acquire — read the firmware-assigned IP via UCI GET_IPADDR ; -; The U64E firmware runs DHCP autonomously before the PRG is launched, so -; our job is to READ the result, not to perform DHCP ourselves. Sequence: +; The Ultimate firmware runs DHCP autonomously before the PRG is launched, +; so our job is to READ the result, not to perform DHCP ourselves. Sequence +; (per interface): ; -; wait_idle -> begin_cmd(NETWORK) -> put(CMD_GET_IPADDR) -> put(iface=0) +; wait_idle -> begin_cmd(NETWORK) -> put(CMD_GET_IPADDR) -> put(iface) ; -> push_wait -> check_err -> read 12 bytes -> drain resp ; -> drain status -> ack ; +; Interface fallback: the U64E has a single interface (index 0), but the +; C64 Ultimate has Ethernet AND WiFi — a box on WiFi returns 0.0.0.0 for +; index 0. We probe indices 0..NET_DHCP_MAX_IFACE-1 and take the first +; one with a non-zero lease. A CMD_FAILED on an out-of-range index is +; cleaned up (drain + ack) and treated like "no lease on this interface". +; ; The 12-byte response layout is IP(4) + Netmask(4) + Gateway(4). We copy -; the first 4 bytes into net_local_ip. If all four are zero we treat the -; call as having failed (no DHCP lease) and return C=1. +; the first 4 bytes into net_local_ip. If all probed interfaces yield a +; zero IP we return C=1 with net_last_error = UCI_ERR_NO_IP (or +; UCI_ERR_CMD_FAILED if the last probe failed at the command layer). ; ; Clobbers: A, X, Y ; Output: C=0 on success (net_local_ip populated), C=1 on failure ; (net_last_error contains the specific failure code). ; ============================================================================= +NET_DHCP_MAX_IFACE = 4 ; probe interface indices 0..3 + net_dhcp_acquire: + lda #$00 + sta @iface_idx ; SMC-style local, no-ZP file convention + +@next_iface: jsr uci_wait_idle bcs @dhcp_wait_to ; FPGA wedged — bail with C=1 @@ -358,9 +372,9 @@ net_dhcp_acquire: lda #UCI_CMD_GET_IPADDR jsr uci_put_byte - ; Interface index 0 — matches the build_get_ip helper in - ; c64-test-harness/src/c64_test_harness/uci_network.py. - lda #$00 + ; Interface index — 0 first (only iface on U64E; Ethernet on the + ; C64 Ultimate), then 1.. (C64U WiFi) until one has a lease. + lda @iface_idx jsr uci_put_byte jsr uci_push_wait @@ -370,12 +384,24 @@ net_dhcp_acquire: jsr uci_check_err bcc @no_err + ; Command failed for this interface (e.g. index out of range on + ; single-interface firmware). Clean up response/status state so + ; the next probe starts from idle, then advance. lda #UCI_ERR_CMD_FAILED sta net_last_error + jsr uci_drain_resp + bcs @dhcp_wait_to + jsr uci_drain_status + bcs @dhcp_wait_to + jsr uci_ack + jmp @advance + @dhcp_wait_to: sec rts +@iface_idx: .byte 0 + @no_err: ; Read the 12-byte response into uci_ipaddr_resp. lda # Date: Sun, 19 Jul 2026 19:48:39 -0500 Subject: [PATCH 3/6] fix(uci): retune uci_fence for C64 Ultimate burst floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The C64U (firmware 1.1.0, core 1.49) needs more inter-access time than the U64E's empirical ~38 us, and only under sustained CMD_DATA bursts: at 51.6 us spacing (INNER=131 @ 64 MHz) GET_IPADDR works but TCP_CONNECT's ~15-byte hostname push is silently lost (UCI_ERR_NO_SOCKET, no SYN ever leaves the box). Floor bracketed at 64 MHz: 51.6 us FAIL / 62.9 us PASS / 100 us PASS. Ship INNER=217 (5450 cycles = 85.2 us @ 64 MHz) for 35% margin over the worst-case floor; 113.5 us @ 48 MHz, ~5.5 ms @ 1 MHz — negligible for networking on both devices. E2e verified on C64U: 64 MHz PASS (64.7 s), 48 MHz PASS (73.0 s). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UZmMaxyyykvYbCvJAc4QVW --- src/net/uci/uci_regs.inc | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/net/uci/uci_regs.inc b/src/net/uci/uci_regs.inc index 399c281..4de700f 100644 --- a/src/net/uci/uci_regs.inc +++ b/src/net/uci/uci_regs.inc @@ -57,21 +57,29 @@ UCI_TARGET_NETWORK = $03 ; network stack ; the FPGA needs). Instead we use a nested delay loop: ; total cycles ≈ OUTER * (INNER * 5 + 5) ; -; Tuned empirically via binary search at 48 MHz: +; Tuned empirically via binary search at 48 MHz on U64E: ; OUTER=3 INNER=121 (~1830 cycles, ~38 µs at 48 MHz) = FAIL ; OUTER=3 INNER=122 (~1845 cycles, ~38.4 µs at 48 MHz) = PASS (minimum) -; OUTER=5 INNER=100 (~2525 cycles, ~52 µs at 48 MHz) = old choice (35% -; margin at 48 MHz, but only ~4% at 64 MHz) -; OUTER=5 INNER=131 (~3300 cycles, ~51.6 µs at 64 MHz) = chosen — keeps -; the 35% margin at the C64 Ultimate's 64 MHz top -; speed (68.8 µs at 48 MHz, ~3.3 ms at 1 MHz) +; OUTER=5 INNER=100 (~2525 cycles, ~52 µs at 48 MHz) = U64E-era choice ; -; At 1 MHz the overhead is ~3.3 ms per access — acceptable for networking. +; Re-tuned at 64 MHz on the C64 Ultimate (firmware 1.1.0, core 1.49), +; whose floor is HIGHER than the U64E's ~38 µs — and only bites under +; sustained CMD_DATA bursts (TCP_CONNECT hostname push; short commands +; like GET_IPADDR survive spacings that lose the connect command): +; OUTER=5 INNER=131 (3300 cycles, 51.6 µs at 64) = FAIL (NO_SOCKET — +; DHCP fine, TCP_CONNECT never latched) +; OUTER=5 INNER=160 (4025 cycles, 62.9 µs at 64) = PASS +; OUTER=5 INNER=255 (6400 cycles, 100.0 µs at 64) = PASS +; OUTER=5 INNER=217 (5450 cycles, 85.2 µs at 64) = chosen — 35% +; margin over the worst-case floor (62.9 µs); +; 113.5 µs at 48 MHz, ~5.5 ms at 1 MHz +; +; At 1 MHz the overhead is ~5.5 ms per access — acceptable for networking. ; The macro preserves A and X via the stack, costing ~14 bytes per call ; site (vs 256 for the NOP sled that still wasn't enough). ; ============================================================================= UCI_FENCE_OUTER = 5 ; outer loop iterations -UCI_FENCE_INNER = 131 ; inner loop iterations +UCI_FENCE_INNER = 217 ; inner loop iterations .macro uci_fence pha ; save A From 6ab7ad68e6da8e202004051ba5e35f5d3c9581db Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:48:39 -0500 Subject: [PATCH 4/6] =?UTF-8?q?test(uci):=20set=20turbo=20before=20boot=20?= =?UTF-8?q?=E2=80=94=20C64U=20runtime-switch=20quirk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the C64 Ultimate, a REST config CPU-speed change while the PRG is running can glitch the UCI bridge so the next pushed command is silently lost — reproduced 2x as UCI_ERR_NO_SOCKET on the first TCP_CONNECT after a 1->64 MHz switch, even with a 100 us fence (1->48 happened to survive). Move set_turbo_mhz before reset/run_prg so the machine boots at target speed and never switches mid-session; also makes boot speed deterministic instead of inheriting the previous run's config. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UZmMaxyyykvYbCvJAc4QVW --- tools/uci/test_https_local.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tools/uci/test_https_local.py b/tools/uci/test_https_local.py index 0ec3a7b..2e54ce5 100644 --- a/tools/uci/test_https_local.py +++ b/tools/uci/test_https_local.py @@ -1185,6 +1185,19 @@ def main() -> int: ) return 3 + # --- Set turbo BEFORE booting the PRG --- + # C64 Ultimate quirk (firmware 1.1.0, core 1.49): a runtime CPU + # speed change via the REST config API can glitch the UCI bridge + # so that the next pushed command is silently lost — observed as + # UCI_ERR_NO_SOCKET on the first TCP_CONNECT after a 1->64 MHz + # switch (2x reproduced; 1->48 happened to survive). Booting at + # the target speed avoids the mid-session switch entirely and + # also makes boot speed deterministic (it used to be whatever + # the previous run left in the device config). + print(f"Setting turbo to {TURBO_MHZ} MHz...") + set_turbo_mhz(client, TURBO_MHZ) + time.sleep(0.5) + print("Resetting machine...") client.reset() time.sleep(2.5) @@ -1200,12 +1213,9 @@ def main() -> int: if init_flag == 0: print("WARNING: net_initialized is 0 — auto-init may have failed") - # --- Flip to 48 MHz turbo BEFORE we trigger the stub --- - print(f"Setting turbo to {TURBO_MHZ} MHz...") - set_turbo_mhz(client, TURBO_MHZ) - time.sleep(0.5) + # (Turbo was set before boot — see the C64U quirk note above.) - # --- Start 6510 debug-stream capture (after turbo, before trigger) --- + # --- Start 6510 debug-stream capture (before trigger) --- if DEBUG_CAPTURE_ENABLED: try: set_debug_stream_mode(client, DEBUG_MODE_6510) From 96de0dc79f6820b7a6230d2cf2c89c0c3d91bf3c Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:48:39 -0500 Subject: [PATCH 5/6] docs: C64 Ultimate notes + 48/64 MHz benchmarks New bench device: C64 Ultimate Starlight (10.53.21.158, fw 1.1.0, core 1.49, NTSC, WiFi). Documents the 64 MHz speed enum, the runtime speed-switch quirk, the wider fence floor, multi-interface GET_IPADDR, REU-disabled default + set_reu() incompatibility, and the new P-256 e2e numbers: 73.0 s @ 48 MHz, 64.7 s @ 64 MHz. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UZmMaxyyykvYbCvJAc4QVW --- CLAUDE.md | 78 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 612acb1..1669b56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -224,14 +224,63 @@ corrupt the UCI command protocol. **Fix:** A nested delay-loop macro `uci_fence` (defined in `src/net/uci/uci_regs.inc`) is inserted after every read/write to UCI registers `$DF1C-$DF1F`. Parameters: `UCI_FENCE_OUTER = 5`, -`UCI_FENCE_INNER = 131`, yielding ~3300 cycles (~51.6 us at 64 MHz, -35% safety margin at the C64 Ultimate's top speed; ~68.8 us at -48 MHz). 14 bytes per fence site, 24 fence sites total (11 write + -13 read). At 1 MHz the same loop costs ~3.3 ms per access — -negligible for networking. (The pre-C64U value INNER=100 gave 35% -margin at 48 MHz but only ~4% at 64 MHz.) - -48 MHz turbo is fully supported and verified on real U64E hardware. +`UCI_FENCE_INNER = 217`, yielding ~5450 cycles (85.2 us at 64 MHz — +35% margin over the C64 Ultimate's empirically-bracketed floor; +113.5 us at 48 MHz). 14 bytes per fence site, 24 fence sites total +(11 write + 13 read). At 1 MHz the same loop costs ~5.5 ms per +access — negligible for networking. + +The C64 Ultimate (firmware 1.1.0, core 1.49) needs MORE inter-access +time than the U64E's ~38 us, and the tighter floor only bites under +sustained CMD_DATA bursts: at 51.6 us spacing DHCP/GET_IPADDR works +but TCP_CONNECT's ~15-byte hostname push is silently lost +(UCI_ERR_NO_SOCKET, firmware never opens the socket). Floor bracketed +at 64 MHz: 51.6 us FAIL / 62.9 us PASS — see the tuning matrix in +`uci_regs.inc`. The U64E-era INNER=100 (52.6 us at 48 MHz) was never +observed failing on U64E but sits below the C64U floor; INNER=217 is +safe on both devices at every speed. + +48 MHz turbo is fully supported and verified on real U64E hardware; +64 MHz is supported and verified on the C64 Ultimate (see "C64 +Ultimate notes" below). + +### C64 Ultimate notes + +A second UCI-capable device joined the bench 2026-07-19: a **C64 +Ultimate "Starlight Edition"** (product "C64 Ultimate", firmware +1.1.0, FPGA 122, core 1.49, NTSC mode, WiFi-connected) at +10.53.21.158 — `U64_HOST=10.53.21.158`. Differences from the U64E +that this codebase now handles: + + - **64 MHz turbo** — the C64U's CPU Speed enum adds "64" (and drops + " 5"). c64-test-harness's `CPU_SPEED_BY_MHZ` carries the superset. + E2e verified at 64 MHz (see benchmarks below). + - **Runtime speed-switch quirk** — changing CPU speed via the REST + config API while the PRG is running can glitch the UCI bridge so + the NEXT pushed command is silently lost (reproduced 2x as + UCI_ERR_NO_SOCKET on the first TCP_CONNECT after a 1→64 switch, + even with a 100 us fence; a 1→48 switch happened to survive). + `tools/uci/test_https_local.py` now sets turbo BEFORE + reset/run_prg so the machine boots at target speed and never + switches mid-session. Mirror that pattern in new scripts + (`test_https_local_p384.py` still uses the old late-switch order — + fix when the P-384 build unblocks). + - **Wider fence floor** — see the delay-loop fence section above + (INNER=217 accommodates both devices). + - **Multiple network interfaces** — Ethernet AND WiFi. GET_IPADDR + (iface=0) returns 0.0.0.0 on a WiFi-connected box; + `net_dhcp_acquire` probes iface 0..3 and takes the first lease. + - **REU ships disabled** — fresh C64U config has `RAM Expansion + Unit: Disabled`; without it sibling-nistcurves `fp_mul` silently + computes garbage (same failure mode as the VICE `-reu` gotcha). + Enable via REST config write. NOTE: the C64U has no `"REU"` + Cartridge preset (presets list is just `[""]`), so the harness's + `set_reu()` helper — which also sets `Cartridge: "REU"` — is + incompatible as written; set `RAM Expansion Unit: Enabled` + directly. Config writes are runtime-only (revert on power cycle). + - Same UCI register map, ID byte $C9, command set, and DeviceLock / + enable_uci flow as the U64E — boot_check/phase2/phase3/e2e scripts + run unmodified. ### Memory layout under UCI @@ -490,7 +539,18 @@ Under the current `libs/nistcurves@v0.3.0` pin (post-PR #55, c64-lib-contract-aligned) the U64E 48 MHz handshake measures **82.1 s** end-to-end (verified 2026-05-20 against the local listener; the prior v0.2.0 measurement was 86.7 s, and the pre-Phase-C.4 in-tree path was -~110 s). v0.3.0's hot-path code is essentially unchanged from v0.2.0; +~110 s). + +On the **C64 Ultimate** (10.53.21.158, see "C64 Ultimate notes"), +measured 2026-07-19 with the INNER=217 fence and boot-at-speed flow: + + - 48 MHz: **73.0 s** end-to-end (faster than the U64E's 82.1 s at + the same clock — different FPGA core) + - 64 MHz: **64.7 s** end-to-end — first >48 MHz datapoint. The + 48→64 ratio (0.89) is well short of the ideal 0.75, so at these + speeds a growing share of wall-clock is speed-invariant (network + I/O, fence overhead, FPGA-side command latency); raw CPU crypto + is no longer the whole story. v0.3.0's hot-path code is essentially unchanged from v0.2.0; the small wall-clock improvement is within measurement noise across runs. It is fine for the local listener used by the e2e harness (600 s budget, ample headroom). Further speedups live in the sibling From 20baba6889559b20ae3d7a666cba557d30e3d8dc Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Sun, 19 Jul 2026 20:07:35 -0500 Subject: [PATCH 6/6] =?UTF-8?q?bench+docs:=2048/64=20MHz=20verify=20scalin?= =?UTF-8?q?g=20analysis=20=E2=80=94=20REU=20DMA=20is=20the=20wall?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bench_ecdsa_u64e.py: the post-W1 MemoryPolicy blocks the bench's DMA writes of vector inputs into CRYPTO_BSS (now in CRYPTO_COLD_SHADOW); pass override= with a reason instead of widening the policy. Measured on C64U: ecdsa_verify_256 53.8 s @ 48 MHz / 47.4 s @ 64 MHz. T(f) = D + C/f fits both e2e and verify pairs to 0.1 s: verify D = 28.4 s speed-invariant vs C = 1.22 Gcycles CPU-scaled. D matches fp_mul's REU row-fetch volume (~27 MB/verify) at the stock ~1 MB/s DMA rate that turbo does not scale (consistent with the 16 ms overlay-swap datapoint). e2e adds ~11.4 s of UCI/network D. Conclusion documented in CLAUDE.md: >48 MHz clocks are mostly wasted on this fp_mul; the fix is library-side. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UZmMaxyyykvYbCvJAc4QVW --- CLAUDE.md | 29 +++++++++++++++++++++++++---- tools/uci/bench_ecdsa_u64e.py | 14 +++++++++----- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1669b56..3a71184 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -547,10 +547,31 @@ measured 2026-07-19 with the INNER=217 fence and boot-at-speed flow: - 48 MHz: **73.0 s** end-to-end (faster than the U64E's 82.1 s at the same clock — different FPGA core) - 64 MHz: **64.7 s** end-to-end — first >48 MHz datapoint. The - 48→64 ratio (0.89) is well short of the ideal 0.75, so at these - speeds a growing share of wall-clock is speed-invariant (network - I/O, fence overhead, FPGA-side command latency); raw CPU crypto - is no longer the whole story. v0.3.0's hot-path code is essentially unchanged from v0.2.0; + 48→64 ratio (0.89) is well short of the ideal 0.75. + +**Why turbo stops paying (measured 2026-07-19):** isolated +`ecdsa_verify_256` bench (`bench_ecdsa_u64e.py`, RFC 6979 vector, n=3 +medians on the C64U) gives 53.8 s @ 48 MHz / 47.4 s @ 64 MHz. Fitting +T(f) = D + C/f to both pairs: + + CPU-scaled C speed-invariant D + ECDSA verify 1.22 Gcycles 28.4 s (53% of wall @ 48) + full HTTPS e2e 1.59 Gcycles 39.8 s + + D is self-consistent to 0.1 s from either endpoint. The 28.4 s + verify-side D matches the sibling fp_mul's REU row-fetch traffic: + each 256-bit multiply DMAs up to 32 rows x 512 B = 16 KB from REU + banks 0/1, and REU DMA runs at the stock ~1 MB/s bus rate + regardless of CPU turbo (independently evidenced by the P-384 + overlay swap: 2x7.5 KB in ~16 ms at 48 MHz = ~1.04 us/B). ~28 s + = ~27 MB of row DMA per verify at that rate. The remaining + ~11.4 s of e2e D is UCI firmware/network latency. Above ~48 MHz + the verify is majority-DMA-bound; the projected ceiling with this + fp_mul is T(inf) ~= D = 28 s no matter the clock. Getting + meaningfully faster requires cutting REU traffic in the sibling + library (fetch-free on-chip square-table mul a la c64-x25519 — + breakeven vs row DMA is ~2.5 MHz — or narrower row transfers), + tracked as a c64-nist-curves issue candidate. v0.3.0's hot-path code is essentially unchanged from v0.2.0; the small wall-clock improvement is within measurement noise across runs. It is fine for the local listener used by the e2e harness (600 s budget, ample headroom). Further speedups live in the sibling diff --git a/tools/uci/bench_ecdsa_u64e.py b/tools/uci/bench_ecdsa_u64e.py index c31976b..358a184 100644 --- a/tools/uci/bench_ecdsa_u64e.py +++ b/tools/uci/bench_ecdsa_u64e.py @@ -180,16 +180,20 @@ def _prepare_vector_buffers(transport: Ultimate64Transport, """DMA-write all ecdsa_* input buffers from a JSON vector dict.""" # Zero-fill first (buffers are 48 bytes each to handle P-384 hypothetical); # verify_256 reads the first 32 only but pre-zeroing is paranoid-safe. + # These buffers legitimately live inside CRYPTO_COLD_SHADOW + # ($A000-$BFFF) post-W1, which the MemoryPolicy reserves — override + # with a reason rather than widening the policy for everyone. + _OVR = "ecdsa vector input buffers (CRYPTO_BSS in CRYPTO_COLD_SHADOW)" zero48 = bytes(48) for name in ("ecdsa_hash", "ecdsa_sig_r", "ecdsa_sig_s", "ecdsa_pubkey_x", "ecdsa_pubkey_y"): - transport.write_memory(labels[name], zero48) + transport.write_memory(labels[name], zero48, override=_OVR) # Write the 32-byte big-endian values verbatim. def _write_be32(name: str, hex_str: str) -> None: raw = bytes.fromhex(hex_str) assert len(raw) == 32, f"{name}: expected 32 bytes, got {len(raw)}" - transport.write_memory(labels[name], raw) + transport.write_memory(labels[name], raw, override=_OVR) _write_be32("ecdsa_hash", vec["hash"]) _write_be32("ecdsa_sig_r", vec["sig_r"]) @@ -198,9 +202,9 @@ def _write_be32(name: str, hex_str: str) -> None: _write_be32("ecdsa_pubkey_y", vec["pubkey_y"]) # Control bytes. - transport.write_memory(labels["ecdsa_curve_id"], bytes([0])) - transport.write_memory(labels["ecdsa_hash_len"], bytes([32])) - transport.write_memory(labels["ecdsa_sig_len"], bytes([32])) + transport.write_memory(labels["ecdsa_curve_id"], bytes([0]), override=_OVR) + transport.write_memory(labels["ecdsa_hash_len"], bytes([32]), override=_OVR) + transport.write_memory(labels["ecdsa_sig_len"], bytes([32]), override=_OVR) def _run_single(transport: Ultimate64Transport,