303 lines
18 KiB
Markdown
303 lines
18 KiB
Markdown
# The Commodore-key hot keys, exercised in VICE
|
|
|
|
> **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.
|
|
|
|
`testReport.md` left one hole in the driver's hot-key layer:
|
|
|
|
> **The C= + C carrier override could not be exercised.** `forceCarrier()` sent `ctrl+c` four times
|
|
> to each machine and `carrierOverrideFlags` (`$E04A`) read back `0` every time, so I cannot say that
|
|
> hot key works.
|
|
|
|
That is closed here. Everything below was observed on 2026-08-21 with VICE 3.7.1 (`x64sc`, NTSC),
|
|
one machine, `../Modem_Wars_SwiftLink.d64`, driven by `swiftlink/testHotkeys.py`. Nothing is
|
|
inferred from the source unless it says so. The full session log is
|
|
`swiftlink/testLogs/hotkeyRun.txt`.
|
|
|
|
**Summary of what is now verified:** the Commodore key under VICE's default C64 keymap is **Tab**;
|
|
C= + C toggles `carrierOverrideFlags` `$E04A` bit 6 on and off; with the override on the carrier
|
|
sampler reports carrier without ever reading DCD, and it does so even when the DCD input says "no
|
|
carrier", where with the override off the same input produces "no carrier"; and C= + 2 followed by
|
|
C= + 3 reprogram the ACIA control register `$DE03` to `$18` (2400 baud) and back to `$15` (300).
|
|
|
|
---
|
|
|
|
## 1. How you press the Commodore key under VICE - determined by experiment
|
|
|
|
The game does not use the KERNAL. `scanKeyboard` `$0DB7` reads the CIA1 matrix itself, once per
|
|
raster IRQ, and it reports the Commodore key only when matrix **row 7, column 5** is pulled low. So
|
|
the whole question is which X keysym VICE's active keymap puts at 7/5, and the only way to be sure is
|
|
to press candidates and watch a byte the module writes.
|
|
|
|
`testHotkeys.py` holds each candidate together with `C` and reads two bytes while the keys are still
|
|
down: `carrierOverrideFlags` `$E04A`, which only the C= + C hot key touches, and `lastHotkeyCode`
|
|
`$E046`, which the module writes on every new key code it sees. Every key is pressed with
|
|
`xdotool keydown`, held for 900 ms - about 54 of the game's once-a-frame scans at true C64 speed -
|
|
and then released, because a tap is not reliably visible to a once-a-frame scanner.
|
|
|
|
| host key held with C | `$E04A` before -> after | `$E046` while held | verdict |
|
|
|---|---|---|---|
|
|
| `Control_L` | `$00` -> `$00` | `$FF` | nothing reaches the module at all |
|
|
| **`Tab`** | `$00` -> `$40` | `$C3` | **C= + C fired: this is the Commodore key** |
|
|
| `Super_L` | `$40` -> `$40` | `$C3` | the letter arrives as a plain key; no hot key |
|
|
|
|
(`Super_L` starts from `$40` because the `Tab` row above had just turned the override on; what
|
|
matters in its row is that the value does not change.)
|
|
|
|
That is reproducible: the same three lines came out of three separate boots.
|
|
|
|
**Why the earlier `ctrl+c` attempt could never have worked**, and it is not just the tap:
|
|
|
|
* The monitor reports `KeymapIndex=0` with `KeymapSymFile="gtk3_sym.vkm"` - index 0 is the
|
|
**symbolic** map, so `gtk3_sym.vkm` is the live one.
|
|
* In that file, `Tab 7 5 8200` (`8192` = "key is (left) cbm on emulated machine") and
|
|
`Control_L 7 2 16392` (`16384` = "key is (left) ctrl"). Left Control is the C64 **CTRL** key, not
|
|
the Commodore key. Only the *positional* map `gtk3_pos.vkm` puts C= on `Control_L`
|
|
(`Control_L 7 5 8200`), and it puts CTRL on Tab.
|
|
* Row 7 column 2 - CTRL - is `$FF` in the game's own `keyMatrixCodeTable` `$0D67`, and `scanKeyboard`
|
|
answers `$FF` = "no key at all" the moment it finds that cell (`$0E37 bne L_0E3E`). So holding
|
|
left Control does not merely fail to be C=, it blinds the whole scan. That is exactly what the
|
|
`$E046` = `$FF` column above shows.
|
|
|
|
The `Super_L` row is the control case worth having: `Super_L` appears in neither `gtk3_sym.vkm` nor
|
|
`gtk3_pos.vkm`, so VICE ignores it, the `C` arrives on its own, `lastHotkeyCode` becomes `$C3` - and `$E04A` does **not** change,
|
|
because `$E31F cpx #$80` fails. "The key got in" and "the hot key fired" are therefore two different
|
|
observations, and the sweep separates them.
|
|
|
|
Two further keys were tried in an earlier boot and are recorded for the next person:
|
|
|
|
* `Alt_L` - `$E046` stayed `$FF`, and worse, **every key sent after it was swallowed**: holding Alt
|
|
opens the GTK menu bar, which takes a keyboard grab. The rest of that run was junk. Do not put
|
|
Alt in a candidate sweep; `testHotkeys.py` no longer does.
|
|
* `Control_R` - no effect, `$E046` = `$FF`, but this was measured after `Alt_L` had already wedged
|
|
that run, so it proves nothing.
|
|
|
|
That boot's log is `testLogs/hotkeyKeymapSweep.txt`; its `Control_L`, `Tab` and `Super_L` rows match
|
|
the table above exactly.
|
|
|
|
## 2. C= + C toggles the override on, and off again
|
|
|
|
Taken at the "GET OPPONENT ON PHONE AND PRESS A OR O" prompt, where the module is resident
|
|
(`$E012` reads `JMP $0DB7`, the game's `scanKeyboard`) and the raster IRQ is already calling `$E015`
|
|
every frame:
|
|
|
|
| when | `$E04A` |
|
|
|---|---|
|
|
| before | `$00` |
|
|
| after one C= + C | `$40` |
|
|
| after a second C= + C | `$00` |
|
|
|
|
`$40` is `carrierOverrideToggleMask` `$E04B`, i.e. bit 6, and the second press clears it again, which
|
|
is what `$E339 eor carrierOverrideFlags` is supposed to do. The auto-repeat filter at `$E312` does
|
|
not get in the way as long as the key is released between presses: with nothing held, `scanKeyboard`
|
|
returns `$FF`, and `lastHotkeyCode` `$E046` is reloaded with `$FF`, so the next `$C3` is a fresh code.
|
|
|
|
Note that the hot key works before the link is open (`isLinkActive` `$E03B` = `$00` in all three rows
|
|
above). The override flag is set; it simply has nothing to act on yet, because the carrier sampler
|
|
returns early at `$E5E7` while the link is down.
|
|
|
|
## 3. What the override does to the link state the game reads
|
|
|
|
The link was then opened for real - `A` at the answer/originate prompt, then space - and came up on
|
|
the first attempt: `aciaCommandShadow` `$E5BE` = `$05`, `$E03B` `isLinkActive` = `$01`, control
|
|
register `$DE03` = `$15` (8N1, internal clock, 300 baud), `connectionPhase` `$E040` = `$02`. The far
|
|
end of the serial line is a TCP sink that accepts the connection and never answers, so the link stays
|
|
in byte sync; 10697 bytes came out of the emulated 6551 during the run, beginning
|
|
`0d 41 54 51 30 56 31 58 31 41 0d` = CR `ATQ0V1X1A` CR, the answer-mode Hayes string, followed by the
|
|
`$00` stream of `beginByteSyncPhase`.
|
|
|
|
Two things about the game screen while all of this was going on, so the pictures are not
|
|
misleading: with a peer that never answers, the game puts up "PHONE TROUBLE."
|
|
(`shots/hotkeys03linkOpen.png`) and later "PICK UP PHONE THEN PRESS SPACE."
|
|
(`shots/hotkeys05borderHeld.png`), while the module itself stays in `connectionPhase` `$02` with
|
|
`isLinkActive` = `$01` throughout. The hot keys keep working in both of those states -
|
|
`inputLockoutTimer` `$0B7D` read `$00` at every measurement, so the raster IRQ was polling the
|
|
keyboard through `$E015` the whole time.
|
|
|
|
With that running, C= + C on and off again:
|
|
|
|
| state | `$E03B` isLinkActive | `$E03C` linkStatus | `$E03D` linkStatusSample | `$E5BC` aciaStatusSave |
|
|
|---|---|---|---|---|
|
|
| override off | `$01` | `$78` | `$78` | `$90` |
|
|
| override on (read immediately) | `$01` | `$40` | `$40` | `$90` |
|
|
| override on, six seconds later | `$01` | `$40` | `$40` | `$90` |
|
|
| override off again | `$01` | `$78` | `$78` | `$90` |
|
|
| override off, six seconds later | `$01` | `$78` | `$78` | `$90` |
|
|
|
|
Those two values are the driver's own fingerprints, from `$E5F8`-`$E602`:
|
|
|
|
* `$78` = a **real** carrier sample. `#$F0` -> `SEC` -> `ROR` = `$F8`, minus bit 7 because the line
|
|
has been quiet (no character has arrived recently, so `rearmCarrierTimer` `$E5FF` strips it).
|
|
* `$40` = an **override** sample. The override branch enters the same `SEC` / `ROR` with `$80`, so
|
|
it produces `$C0`, minus the same bit 7 = `$40`.
|
|
|
|
So the override is not merely "carrier present" - it is visibly a different route to it, and the
|
|
round trip back to `$78` shows the second keypress really cleared the flag. `$E03B` is not
|
|
carrier-derived and stays `$01` throughout, exactly as `spec_link.md` says.
|
|
|
|
## 4. The case a null-modem cable with no DCD wire depends on
|
|
|
|
**VICE cannot produce "no carrier" on its own.** On a SwiftLink the modem's DCD arrives on 6551
|
|
status **bit 6** (CMD swapped DCD and DSR at the chip), and that is the bit the carrier sampler
|
|
`serviceCarrierAndSuspendRequest` `$E5D6` tests with `BIT aciaStatusSave`. Every status byte VICE's emulated 6551 produced in this
|
|
project - `$30` with the device closed, `$10`, `$50`, `$88`, `$90`, `$98` while it is open - has bit 6
|
|
clear. VICE drives bit 5, the 6551's own DCD bit, and that only tracks whether the rs232 device is
|
|
open. So in VICE the driver always sees carrier, and the override can never be the difference
|
|
between a link and no link *by itself*.
|
|
|
|
It can be made the difference where the driver actually reads DCD: `aciaStatusSave` `$E5BC`, the
|
|
shadow the NMI keeps and the only DCD the carrier sampler ever sees. The test breaks at `$E5E9` -
|
|
the `LDA carrierOverrideFlags` one instruction before the DCD test - pokes `$E5BC` to `$40` (bit 6
|
|
set = DCD deasserted = no carrier), and single steps the sampler. Both passes below used exactly
|
|
that same forced input, and `$E5BC` was read back as `$40` after each one, so nothing refreshed it in
|
|
between.
|
|
|
|
**Override on** (`$E04A` = `$40`) - the DCD test is never executed:
|
|
|
|
```
|
|
.C:e5e9 AD 4A E0 LDA $E04A A:01
|
|
.C:e5ec 0A ASL A A:40 <- carrierOverrideFlags = $40 ...
|
|
.C:e5ed 30 09 BMI $E5F8 A:80 <- ... so bit 6 is now bit 7: branch taken,
|
|
.C:e5f8 38 SEC A:80 $E5F1 BIT aciaStatusSave is skipped entirely
|
|
.C:e5f9 6A ROR A A:80
|
|
.C:e5fa EE 45 E0 INC $E045 A:C0 <- sample = $C0: carrier, by override
|
|
.C:e5fd 10 05 BPL $E604 A:C0 <- quiet line, so bit 7 comes off
|
|
.C:e5ff 8D 45 E0 STA $E045 A:C0
|
|
.C:e602 29 7F AND #$7F A:C0
|
|
.C:e604 8D 3D E0 STA $E03D A:40 <- published: bit 6 set = carrier present
|
|
.C:e607 AD BB E5 LDA $E5BB A:40 <- on into startNextTxChar, and back out
|
|
.C:e60a D0 56 BNE $E662 A:01
|
|
.C:e662 60 RTS A:01
|
|
.C:e3ce AD 3D E0 LDA $E03D A:01
|
|
.C:e3d1 CD 3C E0 CMP $E03C A:40
|
|
.C:e3d4 F0 0D BEQ $E3E3 A:40 <- agrees with linkStatus, debounce reset
|
|
```
|
|
readback: `$E03D` = `$40`, `$E03C` = `$40`, `$E5BC` = `$40`.
|
|
|
|
**Override off** (`$E04A` = `$00`), same forced `$E5BC` = `$40`:
|
|
|
|
```
|
|
.C:e5ec 0A ASL A A:00
|
|
.C:e5ed 30 09 BMI $E5F8 A:00 <- not taken
|
|
.C:e5ef A9 F0 LDA #$F0 A:00
|
|
.C:e5f1 2C BC E5 BIT $E5BC A:F0 <- V := bit 6 of $40 = 1
|
|
.C:e5f4 50 02 BVC $E5F8 A:F0 <- not taken: DCD says no carrier
|
|
.C:e5f6 A9 00 LDA #$00 A:F0
|
|
.C:e5f8 38 SEC A:00
|
|
.C:e5f9 6A ROR A A:00
|
|
.C:e5fa EE 45 E0 INC $E045 A:80 <- sample = $80: no carrier
|
|
.C:e5fd 10 05 BPL $E604 A:80
|
|
.C:e5ff 8D 45 E0 STA $E045 A:80
|
|
.C:e602 29 7F AND #$7F A:80
|
|
.C:e604 8D 3D E0 STA $E03D A:00 <- published: bit 6 clear = no carrier
|
|
```
|
|
readback: `$E03D` = `$00`, `$E03C` = `$78` (unchanged - `pollCarrierState` needs 240 disagreeing
|
|
frames before it accepts a new value), `$E5BC` = `$40`.
|
|
|
|
**That is the whole point of the hot key, in two traces.** Identical DCD input; with the override
|
|
off the driver publishes "no carrier" and the game would tear the session down four seconds later
|
|
(`pollMenuSession` `$56B1` watches `$E03C` bit 6); with the override on it publishes "carrier" and
|
|
never looks at the line at all. A three-wire null-modem cable, where DCD is not wired and the
|
|
cartridge's pull-up already reads as carrier, does not need this - but a cable that does carry a real
|
|
DCD from a peer that is not asserting it now demonstrably does.
|
|
|
|
## 5. A baud hot key really reprograms the ACIA
|
|
|
|
Same session, link up, at true C64 speed:
|
|
|
|
| after | `$E055` baudIndex | `$E056` liveControlByte | `$DE03` ACIA control |
|
|
|---|---|---|---|
|
|
| (before) | `$00` | `$15` | `$15` |
|
|
| C= + 2 | `$06` | `$18` | `$18` |
|
|
| C= + 3 | `$00` | `$15` | `$15` |
|
|
|
|
`$18` is entry 6 of `baudEntryTable` `$E6FD` - 2400 baud on a SwiftLink's doubled clock - and it is a
|
|
value no earlier test in this project had ever written; `testReport.md` recorded only `$1E` (the
|
|
probe pattern) and `$15`. C= + 3 puts it back to 300. Both the index and the register move
|
|
together, so the backwards walk of `baudEntryTable` at `$E344` and `loadBaudParameters` `$E353` are
|
|
both doing what section 3 of the README claims.
|
|
|
|
## 6. The border indicator, which is the C= key's other job
|
|
|
|
`readKeyAndHandleModemHotkeys` also paints `VIC_BORDER` from `linkStatusBorderTable` `$E05F` while
|
|
the Commodore key is held, indexed by `connectionPhase*2 + carrier`. With the link up in
|
|
`connectionPhase` `$02` and `linkStatus` `$78` (carrier) the index is 5, whose table byte is `$04` =
|
|
red, with the low bit clear so it is painted only while C= is held.
|
|
|
|
A checkpoint on the module's own `STA VIC_BORDER` at `$E30C`, armed first and Tab pressed afterwards:
|
|
|
|
```
|
|
#1 (Stop on exec e30c)
|
|
.C:e30c 8D 20 D0 STA $D020 - A:02 X:80 Y:00 SP:de N.-.....
|
|
```
|
|
|
|
`A` = `$02` is red - `$04` from the table, halved by the `LSR` at `$E305` - and `X` = `$80` is
|
|
`scanKeyboard`'s "the Commodore key is held" flag, which is what took the `BMI` at `$E308`. So the
|
|
paint really happens; the code that does it runs from the raster IRQ, so it repeats every frame for
|
|
as long as the key is down.
|
|
|
|
Reading `$D020` is not how to see it: it was `$FC`, the game's own border colour, both at that
|
|
breakpoint (where the store had not executed yet) and again after the key was released, so the game
|
|
rewrites the border later in the same frame and a monitor peek at an arbitrary moment nearly always
|
|
catches the game's value. What the effect looks like on screen with the key held was not captured -
|
|
the screenshot in `shots/` was taken after the key came up.
|
|
|
|
## 7. Three things about the rig that the next person needs
|
|
|
|
1. **VICE 3.7.1 has no `WarpMode` resource but its monitor does have a `warp` command.**
|
|
`testReport.md` only tried `resourceset "WarpMode"/"Warp"/"WarpModeEnabled"`, all rejected, and
|
|
concluded every run stays in warp. `warp on`, `warp off` and `warp` (query, answers
|
|
"Warp mode is on./off.") all work. It matters more than it sounds, because **`-warp` on the
|
|
command line does not survive the autostart**: the first run of this test, launched exactly like
|
|
every run in `testReport.md`, sat for over eleven minutes at "loading under warp" and never
|
|
reached the menu, while the same script with an explicit `warp on` issued before the `$0800`
|
|
breakpoint reached the options menu in about 50 seconds, twice in a row. VICE's own log says
|
|
`AUTOSTART: aborting.` a second or two in - this disk's fast loader leaves the KERNAL routines
|
|
VICE is watching - and `-autostart-warp` turning warp off at that point is the obvious
|
|
explanation, though that part is inference, not measurement.
|
|
That is very probably the "90 seconds to over five and a half minutes" variation `testReport.md`
|
|
recorded. `testHotkeys.py` now warps the loader explicitly and issues `warp off` afterwards, so
|
|
every key in this report was pressed against a machine running at true C64 speed. (Between the
|
|
two traces in section 4 the emulated cycle counter advanced 73.8 M cycles = 72 s of NTSC C64
|
|
time, over a stretch of wall clock of the same order - not the hundreds of seconds warp would
|
|
have produced.)
|
|
2. **Do not stop the emulator while a key is held.** A key pressed with `xdotool keydown` and then
|
|
left down across a monitor stop does not survive the stop. This was the difference between two
|
|
otherwise identical runs: with Tab pressed *first* and the `$E30C` checkpoint armed during the
|
|
resulting monitor stop, the checkpoint never fired; with the checkpoint armed first and Tab
|
|
pressed after the machine was running again, it fired immediately (section 6). Anything that
|
|
needs a key to be down *while the CPU runs* must arm its breakpoints first and press afterwards.
|
|
Reads taken during a stop are still fine - they show what the held key already did.
|
|
3. **`Alt_L` wedges the run.** Holding Alt opens the GTK menu bar and takes a keyboard grab; every
|
|
`xdotool` key after that goes to the menu and the emulator sees nothing. `testHotkeys.py`
|
|
verifies key delivery before and after the sweep (`[keys] plain C: lastHotkeyCode = $C3`) so this
|
|
failure mode announces itself instead of silently producing zeros.
|
|
|
|
## 8. What this does not say
|
|
|
|
* **Still nothing on real hardware.** This is VICE 3.7.1 with `-acia1mode 1 -acia1base 0xDE00
|
|
-acia1irq 1`.
|
|
* **The "no carrier" DCD was injected, not emulated.** VICE never raises 6551 status bit 6, so the
|
|
value was written into `aciaStatusSave` `$E5BC` at a breakpoint. That is the exact byte the
|
|
carrier sampler reads and the exact bit the source documents, but a real SwiftLink with a real
|
|
peer dropping DCD has not been observed.
|
|
* **Only one machine.** The peer was a TCP sink that never answers, so the link stayed in
|
|
`connectionPhase` `$02`; the override was not tested with two machines actually playing.
|
|
* **Only C= + C, C= + 2, C= + 3 and C= on its own were pressed.** C= + H (DTR), C= + P (hang up),
|
|
C= + A / C= + O, C= + RETURN and the other four baud keys are still untested.
|
|
* **The disk-load suspend handshake, W65C51N boards and the `$DF00` fallback** remain untested, as
|
|
`testReport.md` already said.
|
|
|
|
## Files
|
|
|
|
| file | what it is |
|
|
|---|---|
|
|
| `testHotkeys.py` | the test: keymap sweep, override toggle, border checkpoint, forced-DCD traces, baud hot keys |
|
|
| `testLogs/hotkeyRun.txt` | the session this report cites, monitor traffic included |
|
|
| `testLogs/hotkeyKeymapSweep.txt` | the earlier boot whose sweep also tried `Alt_L` and `Control_R` |
|
|
| `shots/hotkeys01menu.png` | the title screen, five seconds after the loader finished |
|
|
| `shots/hotkeys02modemPrompt.png` | "GET OPPONENT ON PHONE AND..." - where the keymap sweep was done |
|
|
| `shots/hotkeys03linkOpen.png` | after A and space, with the link open and the module in phase 2: the game says "PHONE TROUBLE." |
|
|
| `shots/hotkeys04overrideOn.png` | the same screen with the carrier override on |
|
|
| `shots/hotkeys05borderHeld.png` | the screen the border checkpoint was taken on (the key is already released, so the border is the game's own colour) |
|