# Ultimate Tool C64 Protocols Ultimate Tool currently uses three independent fixed-memory protocols: - `UTD2`: C64-to-host diagnostic events plus cooperative liveness in 256 bytes - `UTX2`: C64-to-host cooperative crash context and optional exact site in 32 bytes - `UTC1`: one host-to-C64 startup command in 64 bytes The project manifest declares their addresses. The Rust-MOS fixture reserves `$C000-$C0FF`, `$C100-$C13F`, and `$C140-$C15F` as NOLOAD linker regions. All multibyte integers are little-endian. ## Diagnostic mailbox (`UTD2` version 2) The host continues to parse legacy `UTD1` version 1 mailboxes. UTD1 has no heartbeat or crash terminal/event, so it cannot support opt-in hang detection. ### Header | Offset | Size | Meaning | | --- | ---: | --- | | 0 | 4 | ASCII magic `UTD2` | | 4 | 1 | Protocol version, `2` | | 5 | 1 | Header size, `16` | | 6 | 1 | Record size, `60` | | 7 | 1 | Record count, `4` | | 8 | 2 | Latest published sequence; zero means no event | | 10 | 1 | Terminal state: `0` running, `1` pass, `2` fail, `3` crash | | 11 | 2 | Wrapping cooperative heartbeat counter | | 13 | 3 | Reserved and zero | ### Event records Four records begin at offset 16. | Record offset | Size | Meaning | | --- | ---: | --- | | 0 | 2 | Event sequence | | 2 | 1 | Event kind | | 3 | 1 | Flags | | 4 | 2 | Project-defined numeric code | | 6 | 1 | Payload length, at most 52 | | 7 | 1 | Reserved and zero | | 8 | 52 | Printable ASCII payload | Event kinds are ready, log, assertion pass, assertion fail, test pass, test fail, and crash (`1` through `7` respectively). A crash terminal requires exactly one crash event and no pass/fail terminal event; every other terminal rejects crash events. Sequences use `1…65535`, skip reserved sequence zero, and wrap to `1`. The C64 advances the ring slot independently from the numeric sequence so all four records remain available across that wrap. The publisher clears a selected record sequence, writes its metadata and payload, publishes the record sequence, writes a terminal state when applicable, and publishes the header sequence last. No event may follow a terminal event. `DeveloperSession::heartbeat()` advances the 16-bit heartbeat independently of event publication and wraps through zero. The host begins monitoring only after `ready` and startup-command acknowledgement. A heartbeat may advance between the two stability reads without invalidating an otherwise stable event publication; the host uses the heartbeat from the second read. `DeveloperSession::crash(code, label)` publishes an explicit terminal crash without an exact origin. The `panic_halt` helper reclaims an initialized UTD2 mailbox on the terminal panic path, publishes code `0x0001` and the stable label `panic`, then halts. `crash_here!(&mut session, code, label)` is the opt-in exact-site path described below. The host reads the complete 256-byte mailbox twice and compares all publication state. UTD2 heartbeat bytes are excluded from that equality check because they are independently published liveness state. A stable snapshot must expose a contiguous ring ending at the published sequence. The host accumulates and deduplicates events, but any missing sequence is a hard protocol failure. After the first valid mailbox, three consecutive malformed stable reads fail the run and preserve the raw read pair. ## Crash context (`UTX2` version 2) Manifest schema 4 may opt into a fixed crash context plus bounded memory captures. The SDK clears the 32-byte context during initialization. A cooperative crash records the context before publishing its terminal event. The host continues to accept UTX1; those contexts have no exact site. | Offset | Size | Meaning | | --- | ---: | --- | | 0 | 4 | ASCII magic `UTX2` | | 4 | 1 | Protocol version, `2` | | 5 | 1 | Context size, `32` | | 6 | 1 | Publication state: `1` writing, `2` complete | | 7 | 1 | Register semantics: `1` capture-helper entry | | 8 | 1 | Accumulator | | 9 | 1 | X register | | 10 | 1 | Y register | | 11 | 1 | Processor status as pushed by `PHP` | | 12 | 1 | Hardware stack pointer before the capture helper call | | 13 | 2 | Crash code, which must match the crash event | | 15 | 1 | Site semantics: `0` unavailable, `1` instrumented callsite | | 16 | 2 | Runtime address after the instrumentation helper call, or zero | | 18 | 14 | Reserved and zero | The register values are the values available on entry to the SDK's assembly capture helper. They are deliberately labeled `capture_entry`; they are not represented as an exact snapshot of the instruction that caused a Rust panic. As on a 6510, `PHP` forces the break and unused bits in the saved status representation. `crash_here!` invokes a dedicated 6510 assembly helper directly from the instrumented function. The helper reads only its own JSR return slot, adds one to normalize the 6510 return address to the following instruction, and records that address before normal crash publication. It never scans arbitrary stack words. `DeveloperSession::crash` and `panic_halt` publish UTX2 with site semantics zero and address zero. UTX1 uses the same bytes through offset 14 and requires bytes 15–31 to be zero. After a valid crash terminal/event, the host reads the context twice and requires identical, complete publications. It then reads only: - The configured number of hardware-stack bytes beginning at `$0100 + SP + 1`, truncated at the end of the stack page - Explicit manifest-declared zero-page ranges - Explicit manifest-declared global ranges Configuration limits the stack to 64 bytes, each named range to 64 bytes, eight ranges of each kind, and 256 captured bytes in total. Zero-page ranges must remain below `$0100`; selected globals cannot overlap C64 I/O or protocol regions. This slice does not perform arbitrary memory reads, source-address resolution, stack walking, or register reconstruction. Raw context, stack, zero-page, and global bytes are preserved alongside schema-2 `crash-state.json`. Schema-1 crash-state evidence remains readable. Each memory record includes its address, length, evidence filename, and SHA-256. ## Startup command (`UTC1` version 1) The one-shot command slot has a 12-byte header followed by 52 combined bytes of command name and payload. | Offset | Size | Meaning | | --- | ---: | --- | | 0 | 4 | ASCII magic `UTC1` | | 4 | 1 | Protocol version, `1` | | 5 | 1 | Slot size, `64` | | 6 | 1 | State: `1` waiting, `2` submitted, `3` accepted, `4` rejected | | 7 | 1 | Reserved and zero | | 8 | 2 | Request sequence | | 10 | 1 | Command-name length; nonzero when submitted | | 11 | 1 | Payload length | | 12 | 52 | Printable ASCII command name followed by payload | The device initializes the slot and publishes `waiting` after the diagnostic mailbox is ready. The host requires both the ready event and two identical waiting-slot reads before writing. For VICE, the binary monitor stops execution while Memory Set writes the full slot. For an Ultimate, the runner pauses the CPU at the advertised application safe point, posts the 64 bytes through DMA, and resumes even when the write fails. Neither adapter retries an ambiguously accepted write. The SDK validates the complete submitted slot, changes its state to accepted, and returns an owned command to the application. Unknown application-level commands are reported through normal diagnostic failure events. The current fixture accepts command name `scenario` with payload `pass`, `fail`, `crash`, `panic`, `hang`, `timeout`, or `malformed`. ## Timeout classification Manifest schema 3 adds optional per-test `heartbeat_timeout_ms`. It must be positive, shorter than the overall timeout, and at least three times the largest configured target polling interval. - A valid crash terminal/event is classified `crash`. - A configured heartbeat that stops advancing is classified `hang`. - An advancing heartbeat whose overall deadline expires is classified `timeout`. - A missing `ready` event remains a readiness-stage timeout. Without `heartbeat_timeout_ms`, the runner retains generic deadline behavior and never relabels an uninstrumented program as hung. ## DMA boundary The fixture performs no UCI transfers. The ready/waiting state is therefore a valid quiescent checkpoint for the single coordinated write. Future transports must add explicit application-level safe-point negotiation before mixing UCI activity with host DMA.