modemwars/swiftlink/highRateReport.md
2026-08-23 02:09:40 -05:00

31 KiB

The high rates: 4800, 9600, 19200, 38400, and a rate mismatch

Note on addresses. This report records a run made before the probe-hardening pass, which relocated six internal helpers. Addresses quoted below are the ones those routines had at the time of the run. See ADDRESS_CHANGES.md for the mapping onto the current source.

Note on the recommendation: "must not be selected: 38400" has been overturned. It was written from the four wedges in sections 5 and 6, on a build whose NMI handler could be re-entered without bound. wedgeDiagnosis.md then established that mechanism - 34 nested handler frames decoded off a live page 1, three timed stack descents, and the cycle budget behind them - and the driver now carries a re-entrancy guard. On that build 38400 is selectable again and did not wedge in seven boots, and 9600 and 19200 produce no receive errors and no lost transmit characters at all (portCompatReport.md sections 3 and 4, testReport.md section 11). The measurements below are left exactly as they were taken: they are the record of the build that had the fault.

The reason to care about the top of the baud table is not speed. It is hardware compatibility: a C64 Ultimate's SwiftLink emulation may hand this driver a much faster line than Modem Wars was ever written for, and the question that matters is not how much the driver can carry but whether it survives what it is handed - stays in sync, or degrades and recovers, and never wedges.

Everything here is VICE 3.7.1 (x64sc), NTSC, -acia1mode 1 -acia1base 0xDE00 -acia1irq 1, on 2026-08-22, against a disk built from the current source with ./build.sh (checkAbi.py prints ABI CHECK PASSED). Both emulators in every pair run at true C64 speed - warp is used only for the loader and switched off through the monitor's warp command before the options menu - and each emulator's own free-running cycle counter is read at both edges of every measurement window to prove it. Every window in this report ran at 100.5% of real time or better on both machines.

Script: the new testHighRates.py, which reuses testRealtime.py (the socket null modem, the boot, the hot-key rate selection, the stopwatch check, the transmitter-deadlock probe) and adds what this question needs. Logs: testLogs/highRate4800.txt, highRate9600.txt, highRate19200.txt, highRate38400.txt, highRate38400run2.txt, highRate38400run3.txt, highRate38400run4.txt, highRateMismatch.txt, with per-second byte counts and raw tracepoint output in testLogs/highRate.*.samples.txt / *.trace.txt / *.ring.txt and screenshots in shots/hrh*.png.


The answer, in one table

Rate Hot key Control Both reached phase 3 Sampling intervals carrying nothing Receive-ring overflows Transmit characters lost Byte-sync rounds Outcome
4800 C= + 4 $1A yes none, 0 of 450 0 in 20 s of tracing 0.8% / 1.5% ~1.0/s, 98% completed healthy
9600 C= + 9 $1C yes none, 0 of 442 8 and 41 in 20.6 s 2.1% / 2.1% ~1.7/s, 79% completed degrades, recovers
19200 C= + 0 $1E yes none, 0 of 450 128 and 74 in 20.4 s 2.2% / 2.3% ~1.6/s, 76% completed degrades, recovers
38400 C= + 8 $1F 3 of 4 boots, briefly every interval, one or both directions, for the whole 170-246 s of every boot 1259 in 21 s on the stuck machine - it stops resyncing WEDGES, 4 of 4 boots, once by crashing the CPU
38400 vs 300 - $1F / $15 yes none, 0 of 474 0 0.0% / 0.1% 0.2-0.5/s, completions >= entries healthy - but see section 7

Plain recommendation.

  • Safe to advertise: 300, 1200, 2400, 4800. 4800 behaved the way 2400 does after the transmitter fix: continuous traffic in both directions every second, not one receive-ring overflow, not one bad character, the ring never more than a third full.
  • Document as degrading but usable: 9600 and 19200. Both keep the link alive and both keep the game moving. Both also spend a visible part of every second re-synchronising: the 20-byte receive ring overflows, linkErrorCount forces a byte-sync round, the round completes, the packet layer carries on. Nothing is lost that the ARQ layer does not recover. What they do not buy is throughput - the extra line time goes into resync floods, not into frames.
  • Must not be selected: 38400. (Overturned - see the note at the top of this file.) Four consecutive boots, four wedges, every one inside the first minute after the link came up, none of which recovered in the 170 to 246 seconds each was then watched for. On the fourth boot VICE reported *** Main CPU: JAM at $0007 on one machine - the 6502 executing an illegal opcode in zero page. It is not the old 2400-baud transmitter deadlock, and the driver's own self-heal cannot reach it. Sections 5 and 6 have the stuck bytes.
  • The rate table itself is fine. Every hot key programmed the chip correctly, first press, every time: $1A, $1C, $1E and $1F all appeared in $DE03 with the matching baudIndex. 38400 fails at the protocol, not at the register.

1. What actually limits this driver, arithmetically

The raw receive ring at $E430 holds 20 characters, only the once-per-frame link tick drains it, and awaitWholePacket $E9D9 refuses to copy a packet body out until the whole payload and its check byte are resident - up to 17 bytes - so the genuine headroom is about three bytes. There is no RTS throttling anywhere in the driver. So the number that decides everything is how many characters can arrive between two drains:

Rate Characters/s Characters per video frame A full 22-byte transmit ring emptied onto the wire takes
300 30 0.50 733 ms = 43.9 frames
1200 120 2.01 183 ms = 11.0 frames
2400 240 4.01 91.7 ms = 5.5 frames
4800 480 8.02 45.8 ms = 2.7 frames
9600 960 16.05 22.9 ms = 1.4 frames
19200 1920 32.09 11.5 ms = 0.69 frames
38400 3840 64.19 5.7 ms = 0.34 frames

Two figures in the existing documents are wrong and are worth correcting here, because the size of the problem depends on them. The brief for this work said a video frame is "roughly 640 character times" at 38400 and spec_uart.md hazard 2 says "about 200"; at 3840 characters a second and 59.826 frames a second it is 64.2. The ring is therefore about three times too small at 38400, not thirty times - which is why 19200, at 32 characters a frame, still works.

The second column is the one that decides where the overflows start. The frame layer fills the 22-byte transmit ring and the NMI empties it at line rate, so a peer's ARQ frame arrives as a burst. At 300 baud that burst is spread over 44 video frames and cannot overflow anything. At 9600 it lands inside one and a half frames, at 19200 inside one, and at 38400 three of them can land inside a single frame.

Relay direction, and how machines are told apart. The null modem counts bytes per connection; conn0 is whichever emulator opened its serial line first, which is machine A (the answer side, the one the script drives first). That is an inference, the same one every earlier report in this directory makes, and where it matters below it is corroborated by something machine-specific - each emulator's own log, or its own monitor socket, or a rate that only one of the two was set to.


2. 4800 baud: healthy

Both machines took C= + 4 at the "PRESS A OR O" prompt; baudIndex $E055 went to 9 and the ACIA control register $DE03 to $1A on both, and both reached connectionPhase 3. Four windows, 246.5 seconds of measured link time.

Window Wall A speed B speed A -> B B -> A resync floods $55 lead bytes
clean 1 75.19 s 101.0% 100.6% 681 B = 9.06 B/s 687 B = 9.14 B/s 0 / 0 255 / 258
clean 2 75.15 s 100.9% 100.5% 3418 B = 45.48 B/s 3053 B = 40.62 B/s 44 / 23 629 / 761
sync tracepoints 76.06 s 100.9% 100.5% 2764 B = 36.34 B/s 2678 B = 35.21 B/s 32 / 36 622 / 659
error tracepoints 20.06 s 103.5% 102.0% 866 B = 43.18 B/s 996 B = 49.66 B/s 8 / 8 140 / 165
  • Not one of the 450 sampling intervals - 225 in each direction - carried zero bytes. The per-second buckets ran 6 to 12 B/s in the quiet window and 12 to 175 B/s in the busy one.
  • Not one receive error of any kind. The countLinkError $E403 tracepoint recorded zero hits on either machine in the 20-second instrumented window, and so did the conditional ring-full tracepoint trace exec $e505 if x > $13.
  • The resyncs that do happen are the packet layer's own, not the UART's. With no $E403 hits at all, the 44 and 23 byte-sync floods in window 2 came from receivePacket's unwindAndResync paths (two $00 in a row, an invalid length code, a bad check byte), not from a dropped character.
  • They complete. Tracepoints on beginByteSyncPhase $E805 and on the instruction that finishes a sync, sta ackPending $E855, counted 78 entries / 76 completions on A and 80 / 78 on B in 76 seconds.
  • The ring stays nearly empty. uartRxCount $E42D, sampled 24 times per machine with the emulator stopped: A min 0, max 7, mean 1.1; B min 0, max 5, mean 0.8. Never at the 20-byte limit.
  • connectionPhase $E040 was seen at both 2 and 3 on both machines, isLinkActive $E03B = $01 at every edge, and txCharActive $E5BB / aciaCommandShadow $E5BE were only ever the two self-consistent pairs $00/$09 and $01/$05. The deadlock probe never fired.

3. 9600 and 19200: they degrade, and they recover

Same method, one boot each, four windows each. These two are reported together because they differ only in degree.

9600 baud ($DE03 = $1C, baudIndex 12)

Window Wall A -> B B -> A resync floods $55 per-second range
clean 1 75.18 s 3122 B = 41.53 B/s 2734 B = 36.37 B/s 28 / 28 652 / 617 6-345 / 6-276
clean 2 75.23 s 3647 B = 48.48 B/s 3985 B = 52.97 B/s 32 / 43 700 / 705 12-200 / 12-275
sync tracepoints 75.53 s 4313 B = 57.11 B/s 4214 B = 55.79 B/s 31 / 59 810 / 730 14-259 / 14-228
error tracepoints 20.61 s 1017 B = 49.35 B/s 884 B = 42.90 B/s 9 / 9 223 / 164 12-68 / 13-72

19200 baud ($DE03 = $1E, baudIndex 15)

Window Wall A -> B B -> A resync floods $55 per-second range
clean 1 75.13 s 4702 B = 62.59 B/s 5377 B = 71.57 B/s 72 / 47 717 / 980 12-306 / 14-377
clean 2 75.14 s 4885 B = 65.01 B/s 4406 B = 58.64 B/s 58 / 50 770 / 808 14-371 / 12-300
sync tracepoints 75.78 s 4623 B = 61.00 B/s 4014 B = 52.97 B/s 66 / 30 749 / 659 12-237 / 12-142
error tracepoints 20.35 s 1188 B = 58.38 B/s 1292 B = 63.49 B/s 21 / 10 207 / 312 18-170 / 18-217

The three things that decide the verdict, at both rates:

  • No sampling interval carried nothing. 0 of 442 at 9600 and 0 of 450 at 19200, counting both directions, including inside the windows where the ring was overflowing.
  • The errors are real, and every one is a ring overflow. In the instrumented windows the unconditional $E403 tracepoint and the conditional $E505 if x > $13 one returned identical counts: 8 and 8 on A, 41 and 41 on B at 9600; 128 and 128 on A, 74 and 74 on B at 19200. Not one character was rejected for a bad status. That is the 20-byte ring being outrun by a burst, exactly as spec_uart.md hazard 2 predicts, and it is the same signature rate1200Report.md found at 1200
    • there are simply far more of them.
  • Every degradation was followed by a recovery. $E805 / $E855: 124 entries / 101 completions on A and 128 / 100 on B at 9600; 100 / 87 on A and 135 / 91 on B at 19200 - roughly 1.3 to 1.8 byte-sync rounds a second, two thirds to seven eighths of which reached sta ackPending and went back to the packet phase inside the same window (81% and 78% at 9600, 87% and 67% at 19200). (A round that does not reach $E855 is not a loss; the next one starts immediately, and $E855 under-counts anyway, because restartByteSync $E80F re-enters a sync without passing $E805.) connectionPhase was found at both 2 and 3 on both machines, and uartRxCount sampled with the emulator stopped never once read the 20-byte limit: A max 14 / B max 4 at 9600, A max 10 / B max 4 at 19200.

And they buy nothing. The $55 frame-lead counts, a floor on how many ARQ frames crossed the wire, are 629-761 per window at 4800, 617-705 at 9600 and 717-980 at 19200. The bytes on the wire climb from 45 B/s to 65 B/s as the rate goes up, and what climbs with them is the $00 byte-sync flood. Counting only the three 75-second windows of each run, so that the lengths are comparable: 0 to 44 floods per window at 4800, 28 to 59 at 9600, 30 to 72 at 19200. Above 4800 the extra line rate is spent re-synchronising, not playing.

4. The other degradation: the driver overwrites its own outgoing characters

VICE logs Acia1: ACIA: data register written although data has not been sent yet. every time the data register is written before the previous character has left - one transmitted character lost each time, on emulation and on a real 6551 alike. Each emulator writes its own log, so the loss counts below belong to a named machine with certainty; the denominators come from the relay and so inherit the conn0 = A inference of section 1. realtimeReport.md saw a handful of these at 2400; they scale hard with rate:

Rate A lost of offered B lost of offered
4800 132 16207 = 0.8% 226 14689 = 1.5%
9600 387 18829 = 2.1% 403 19054 = 2.1%
19200 542 24165 = 2.2% 528 22600 = 2.3%
38400 vs 300 1 14769 = 0.0% 6 9719 = 0.1%

(The 38400/38400 boots produced 0 to 52 apiece, but they went silent within a minute, so their totals mean nothing.) Every one of these is a byte the peer never sees, so it is a checksum failure and a retransmit at the far end - which is a large part of why the resync rate in section 3 climbs the way it does. How the driver comes to write a busy data register was not traced; the documented commNmiHandler re-entrancy race is the obvious suspect and nothing measured here confirms it.

5. 38400 baud: it wedges, four times out of four

Four boots, four wedges, no recoveries. Every one happened within about a minute of the link coming up - before the first 75-second window opened - and none showed any sign of coming back during the 170 to 246 seconds of measurement that followed. The four are not identical, which is itself worth recording: this is not one reproducible bug with one signature.

Boot Both reached phase 3? A -> B, per window B -> A, per window How it ended up
1 h384 no ({'A': 2, 'B': 2} after 180 s) 998 / 1000 / 998 / 272 B, all $00/$FF, no $55 at all 0 / 0 / 0 / 0 A stuck in byte sync; B's game back on the plain OPTIONS menu, its ring full at 20/20
2 h384b yes 0 / 0 / 0 / 0 0 / 0 / 0 / 0 both frozen in phase 3, both rings full at 20/20, total silence both ways for 245 s
3 h384c yes 4494 / 4495 / 4495 / 1259 B, every byte $00, 59.83 B/s = exactly one byte per video frame 0 / 0 / 0 / 0 A flooding byte sync for ever; B frozen in phase 3 on "OPPONENT PICKING GAME/MAP."
4 h384d yes 0 / 0 / 0 1000 / 1000 / 266 B of $00/$FF at 13.3 B/s A's CPU jammed; B alone in byte sync for ever

Boot 3's error window is the clearest measurement of what the line does to a stuck machine: in 20.4 seconds machine B's receiver counted 1259 rejected characters and the conditional tracepoint says 1259 of them were the 20-byte ring being full. linkErrorCount $E047 is useless as a total here because it is one byte that simply wraps - across boot 3's four window edges it read 208, 21, 94, 206.

6. The stuck bytes

realtimeReport.md pinned the 2400-baud fault to one byte: txCharActive $E5BB = $01 while aciaCommandShadow $E5BE said the transmit interrupt was off. This is not that fault. In every 38400 wedge those two bytes were self-consistent - $00 / $09, "idle, nothing in the chip" - testRealtime.isTxDeadlocked reported False at every edge, and the repair probe that would have written $00 over $E5BB never ran. The driver's own healStuckTransmitter, added for the 2400 fault, has nothing to heal here.

6.1 Boot 3: uartPendingCount out of range

Machine B, read through the monitor with the emulator stopped, six times over six seconds, every reading identical, and unchanged across four windows spanning 245 seconds:

Byte Value What it means
uartPendingCount $E0A5 $FF 255 characters "queued for the UART that have not finished transmitting"
uartTxCount $E414 $00 ...while the 22-byte transmit ring is empty
uartRxCount $E42D $14 = 20 the receive ring is full, and stays full
uartRxReadIndex / uartRxWriteIndex $E42E/$E42F $06 / $0D frozen - nothing pushed, nothing popped
connectionPhase $E040 3 frozen in the packet phase
isLinkActive $E03B $01 the link is still "up"
txCharActive $E5BB / aciaCommandShadow $E5BE $00 / $09 self-consistent: not the 2400 deadlock
nmiSuspendRequest/nmiSuspendAck $E039/$E03A $00 / $00 the disk-load suspend handshake is not stuck
$DE03 / $DE02 $1F / $09 the 6551 is correctly programmed at 38400, receiver interrupt on
$0BA5 $00 still a modem game, not switched to solo
$01 $35 I/O banked in - the module is reachable
PC wanders ($41C5, $C4EF, $C4F6) the CPU is running game code, not spinning in the driver

uartPendingCount $E0A5 = $FF with an empty transmit ring is the byte that is stuck, and it is exactly the failure spec_uart.md hazard 1 warns about: "make sure it can reach 0. If it never reaches 0, serviceLinkTick $E76E never clears $E0A4/$E0A6, sendPacketFsm $E26E never completes an exchange, and the game locks in lock-step forever." Machine B's screen agrees - it sits on "OPPONENT PICKING GAME/MAP.", waiting for a peer that can never satisfy it. Boot 2 shows the same byte out of range on both machines at once, $39 on A and $79 on B, both rings full, both ends silent.

The driver has a mechanism meant to prevent exactly this. startNextTxChar $E607 resynchronises the count whenever it runs with the ring dry:

        lda     uartTxCount             ; E60C  is there really something in the ring?
        bne     L_E616                  ; E60F  yes - decide whether it may go out
        sta     uartPendingCount        ; E611  the ring is dry: A = 0 resynchronises the count

and the once-per-frame $E00C service falls into it from $E604 on every raster IRQ the link is up. On the wedged machine the ring is dry and the count is not zero, and it stayed that way for 245 seconds - so that resynchronisation was not running, which means the game above had stopped calling $E00C. Which of the two stopped first was not determined.

6.2 Boot 4: the CPU jammed

Machine A, six readings over six seconds, then two more after the repair probe - every one identical:

  ADDR A  X  Y  SP 00 01 NV-BDIZC
.;0007 00 01 00 4a 5e 15 00110100

and its own emulator log says so outright:

***    Main CPU: JAM at $0007

The program counter is in zero page, it does not move between samples while the cycle counter advances by 1.38 million, and the two processor-port bytes have been overwritten: $00 (the data direction register) reads $5E instead of $2F and $01 reads $15. None of the other fifteen emulator logs in this report records a JAM.

The rest of that machine at the same moment:

Byte Value
uartRxCount $E42D, read/write index $14 = 20 (full), $10 / $10, frozen
uartTxCount $E414 $00 - transmit ring empty
uartPendingCount $E0A5 $00
$DE01 status / $DE02 command / $DE03 control $9C / $05 / $1F
aciaCommandShadow $E5BE / txCharActive $E5BB $09 / $00
$E039 / $E03A $00 / $00

The line worth staring at is the command register. The chip says $05 - the transmit interrupt is armed - while the driver's shadow says $09, transmit interrupt off, and txCharActive says nothing of ours is in the chip. Status $9C has bit 7 (IRQ), bit 4 (TDRE) and bit 3 (RDRF) set. A 6551 with the transmit interrupt enabled and TDRE true asserts /IRQ again as soon as the status read releases it, and this driver's NMI is the only thing that reads that status. That the CPU ended up executing zero page with its bank register smashed is consistent with runaway NMI re-entrancy - the race the source documents at commNmiHandler - but nothing here measures the path from one to the other, and this shape occurred once in four boots.

6.3 The repair probe: nothing in the driver brings it back

realtimeReport.md proved its diagnosis of the 2400 deadlock by writing a single $00 over $E5BB from the monitor and watching the machine come back to life. The same probe was run here, on boot 4: $00 written over uartPendingCount $E0A5, twenty seconds observed, then $00 over txCharActive $E5BB as well and another twenty seconds.

  20 s after clearing uartPendingCount $E0A5: conn0 0 bytes, conn1 288 bytes
  20 s after clearing txCharActive $E5BB too: conn0 0 bytes, conn1 294 bytes

conn1 is the machine that was still talking; 288 and 294 bytes in twenty seconds is the 13.3 B/s it was already producing, unchanged. conn0, the wedged machine, produced nothing either time. That is the honest result and it is a negative one: on this boot the wedged CPU had jammed, and no value written into any driver byte can restart a jammed 6502, so the probe could not have worked. The boot-3 shape - uartPendingCount = $FF on a machine whose CPU is still running - is the one worth probing, and it has not been. That run predates the probe, and the shape did not recur in the boot that had it.

7. The rate mismatch: 38400 against 300

One machine given C= + 8, the other left at the disk default of 300, a link opened between them, 245.8 seconds measured. Confirmed at every window edge: $DE03 = $1F on A and $15 on B, baudIndex 18 and 0.

Window Wall A (38400) -> B B (300) -> A overflows $55 resync floods
clean 1 75.18 s 2274 B = 30.25 B/s 2226 B = 29.61 B/s 0 950 / 738 0 / 0
clean 2 75.21 s 2335 B = 31.05 B/s 2160 B = 28.72 B/s 0 787 / 834 0 / 1
sync tracepoints 75.34 s 2701 B = 35.85 B/s 1900 B = 25.22 B/s 0 824 / 502 14 / 0
error tracepoints 20.04 s 750 B = 37.43 B/s 590 B = 29.45 B/s 0 299 / 62 0 / 0

Both ends reached connectionPhase 3, not one of the 474 sampling intervals carried nothing, there was not a single receive-ring overflow or bad character at either end, and neither machine wedged. It is the healthiest run in this whole report. uartRxCount sampled 24 times per machine read max 0 on the fast side and max 2 on the slow side. The byte-sync tracepoints counted 12 entries / 13 completions on A and 35 / 35 on B - the lowest resync rate of any pair measured here. (Completions can exceed entries because restartByteSync $E80F re-enters a sync without passing $E805.)

Two things explain that, and the second is why the result must be read carefully.

  • The pair simply ran at the slower of the two rates. The slow side is pinned at 28.7-29.6 B/s, which is 300 baud's 8N1 ceiling to within a byte, and the fast side tracks it at 30-37 B/s. The fast side never actually floods, because the packet layer is lock-step - it will not send the next thing until the previous exchange completes, so the peer's rate paces it. That is genuine backpressure, and it is worth knowing that the ARQ layer throttles a fast end for free.
  • VICE's ACIA is byte level. It paces whole characters at whatever rate the control register asks for but does not shift them bit by bit, so two ends set to different rates do not garble each other here. On real hardware a 300-baud receiver sampling a 38400-baud line would raise framing and overrun errors on nearly every character; every one of those goes through reportLinkError $E3BC into countLinkError $E403, and a non-zero linkErrorCount makes runLinkStateMachine restart byte sync whatever phase it was in. The symptom on hardware would be the one README.md section 4 already describes: a link stuck in phase 2 for ever, border red, never green.

So the honest statement is: on this rig a rate mismatch does not wedge either end and does not even hurt - it degrades to the slower rate and keeps playing. What it cannot tell you is whether a real 38400-baud line into a real 300-baud receiver wedges, because the error storm that would drive it cannot be produced here. What it does establish is that the slow side survives being handed a peer whose timing is nothing like its own, and that neither state machine falls over on the mismatch itself.

For the Ultimate specifically, that splits the worry in two. If the device honours the control register - as VICE and a real SwiftLink do - the two ends agree and this test says nothing bad happens. If it ignores the baud bits and runs at its own default, the failure mode is the garbage-on-the-line case above, which this rig cannot reproduce; the mitigation is the same either way, and it is the recommendation at the top: do not offer 38400 at all, and treat 9600 and 19200 as degraded modes.


8. What went wrong in the rig, and what it cost

  • Two runs were lost to the module-residency trap rate1200Report.md section 6 documented, and it is worth repeating because it bit again. testTwoMachines.answerModemPrompts uses "$E5BE is non-zero" as its "the link is open" test and testRealtime.selectRate uses "$E013/$E014 reads B7 0D" as its "the module is resident" test, and while the 1541 is still fetching track 34 at true C64 speed, $E000-$EFFF still holds the previous overlay. The first 19200 and 38400 attempts read $E5BB-$E5BE = E8 84 18 AC on machine A and took $AC for an open link; that machine then never selected its rate and never opened a link at all. Both runs were killed, their emulators and Xvfb displays cleaned up by hand, and testHighRates.moduleResident now waits for all sixteen bytes of the $E000 jump table first - which is what test1200AutoBaud.waitForModule already did. Neither lost run is used for anything above.
  • 4800 and 9600 were measured with two emulator pairs on the box at once, as were 38400 boot 2 and the mismatch; the rest ran alone. This is recorded because it is the sort of thing that invalidates a real-time measurement - but it did not: every window in the report, parallel or not, reports both machines at 100.5% of real time or better against their own cycle counters. The residency failures above came from the fixed sleeps in the boot sequence being too short when the host was loaded, not from the measurement windows.
  • The instrumented windows are not free, and are kept separate for that reason. A clean window carries no tracepoints at all, so its byte counts are the honest throughput; the $E805/$E855 window and the short $E403/$E505 window carry theirs. The error windows consistently read 102-103.5% of real time rather than 100.5-101%, which is pause-and-resume overhead landing inside a shorter window, not the emulator running fast.
  • $DE03 sometimes reads $00 and $DE01 $FF on a machine that is running perfectly well. That is not the chip being reprogrammed; it is the monitor reading through a CPU that happens to have the I/O area banked out at that instant - readFilmByte, writeFilmByte and copyPageUnderIo set $01 = $34 for up to 2600 cycles at a time. Two of the four 38400 boots caught a machine that way at every window edge, which briefly looked like a driver fault and is not one. Any future test that reads the cartridge page through the monitor should read $01 alongside it; wedgeDump does.

9. What this does not show

  • VICE, not hardware. No physical SwiftLink, no real cable, and above all no C64 Ultimate - the device this whole exercise is about is one this report has never touched.
  • VICE's ACIA is byte level, so no framing or parity error can occur on this rig at all. "Every error was a ring overflow" is a statement about this emulation; on a real line the same tracepoints would be expected to show bad characters too, and section 7 is limited by this more than anything else here.
  • One boot per rate for 4800, 9600 and 19200. 245 seconds each is enough to say the link keeps moving and recovers, and not enough to say it always will. 38400 got four boots because it failed.
  • The 38400 wedge is characterised, not explained. What is established is which bytes are stuck at what values in two of the four shapes, that the driver's own uartPendingCount resynchronisation is not running while they are, that one boot ended in a CPU JAM, and that the 2400-baud repair does not apply. How uartPendingCount leaves the range the layers above wait on was not traced - $FF is what a dec uartPendingCount produces from zero and the NMI has such a decrement at $E6C9, but nothing measured connects the two. Nor was the path from $DE02 = $05 against a $09 shadow to a jammed CPU traced. Both need the NMI instrumented, which stops the emulator and destroys the timing being measured.
  • The uartPendingCount repair probe has not been run against the shape it was written for. See section 6.3.
  • No stock user-port peer, and no complete game. As in every earlier report, both machines are SwiftLink builds, and they reach the GAME TYPE menu and stay there.
  • Nothing was fixed. This report characterises; no driver source file was changed, and checkAbi.py passes on the image every run above was made against.

10. Files

File What it is
testHighRates.py the test: per-machine rate selection, clean and instrumented windows, uartRxCount sampling, the wedge dump and the uartPendingCount probe
testLogs/highRate4800.txt, highRate9600.txt, highRate19200.txt the three rates above 2400 that survive
testLogs/highRate38400.txt, ...run2.txt, ...run3.txt, ...run4.txt the four 38400 boots, in the order of section 5
testLogs/highRateMismatch.txt 38400 on one machine, 300 on the other
testLogs/highRate.*.samples.txt per-second byte counts, both directions, every window
testLogs/highRate.*.trace.txt raw tracepoint output, with the cycle counter that attributes a hit to a machine
testLogs/highRate.*.ring.txt the uartRxCount / phase / transmitter samples behind sections 2, 3 and 5
shots/hrh*.png the menu, the linked pair and the end of every window, for each run