Phase-Chain Serialization Format
Wire layout for the variable-length multi-phase mission continuity buffer
that the cartridge lifecycle FSM (GWP-259) writes into
DeckState.phase_chain[] whenever a mission is suspended mid-flight or
crosses a Hot Swap boundary. On re-insertion of the matching cart the
runtime deserializes the buffer to resume the mission with phase context
intact.
cartridge-lifecycle.md— the FSM that owns this buffer.deck-state.md—phase_chain[256]/phase_chain_lenfield semantics inside Universal Deck State.adr/ADR-0019— the SD-card-via-USB-MSC physical model that drives insert/eject events.
Design intent
Section titled “Design intent”The phase chain is the only piece of cross-phase state that has to survive a cartridge swap, so the layout must be:
- Byte-stable across compilers and toolchains. The same
PhaseChainvalue must serialize to the same bytes whether built on macOS clang, Linux gcc, or the Pi cross-toolchain. No struct packing tricks, no compiler-defined alignment, no host-endian reads. - Versioned. A byte-level format version lets the runtime recognize and reject buffers it cannot interpret (and a future v2 layout can grandfather v1 detection cleanly).
- Self-describing. Magic bytes guard against a fresh-deck buffer (zeroed flash) being misinterpreted as a chain.
- Bounded. Total serialized size must fit in the existing 256-byte
DeckState.phase_chain[]so no UDS layout change is required.
Container layout (little-endian)
Section titled “Container layout (little-endian)”Offset Size Field Notes------ ---- ---------------- ------------------------------------------0x00 2 magic 0x504C ("PL" — Phase Link)0x02 1 version 0x01 (PHASE_CHAIN_FORMAT_VERSION)0x03 1 count 0..15 records (PHASE_CHAIN_MAX_RECORDS)0x04 4 expected_cart_id le32; 0 = no cart pinned0x08 1 suspended_flag 1 = unsafe pull mid-mission 0 = clean phase-boundary suspend0x09 3 reserved must be zero (deserializer rejects nonzero)0x0C N×R records[] each PHASE_CHAIN_RECORD_SIZE = 16 bytesRecord layout (16 bytes each)
Section titled “Record layout (16 bytes each)”Offset Size Field Notes------ ---- ------------ ----------------------------------------------0x00 1 phase_index 0..255; cart-defined0x01 1 phase_kind cart-defined; 0 = generic intermediate0x02 2 payload_len le16; <= PHASE_CHAIN_PAYLOAD_BYTES (12)0x04 12 payload[] fixed-width opaque blob; bytes past payload_len are zero on a properly constructed recordRecords always occupy 16 bytes regardless of payload_len — the
fixed-width record keeps the layout byte-stable and sidesteps the
“variable-stride records inside a fixed-size buffer” footgun.
Size budget
Section titled “Size budget”| Field | Bytes |
|---|---|
| Header | 12 |
| Records (15 × 16) | 240 |
| Total at maximum | 252 |
DeckState.phase_chain[] capacity | 256 |
| Headroom | 4 |
The ADR-0006-stamped phase_chain_len field is uint8_t (0..255), so
the 252-byte ceiling is itself just inside phase_chain_len’s
representable range.
Defensive validation
Section titled “Defensive validation”The deserializer rejects:
buf_size < PHASE_CHAIN_HEADER_SIZE(12)magic != 0x504Cversion != 0x01count > PHASE_CHAIN_MAX_RECORDS(15)- Any nonzero byte in the reserved region (
0x09..0x0B) buf_size < 12 + count * 16- Any record with
payload_len > 12
On any rejection the deserializer zeroes the caller’s PhaseChain and
returns false. Garbage in, defined-output zeros — never a partial
fill, never an out-of-bounds read.
Constants reference
Section titled “Constants reference”| Symbol | Value |
|---|---|
PHASE_CHAIN_MAGIC | 0x504C |
PHASE_CHAIN_FORMAT_VERSION | 0x01 |
PHASE_CHAIN_HEADER_SIZE | 12 |
PHASE_CHAIN_RECORD_SIZE | 16 |
PHASE_CHAIN_PAYLOAD_BYTES | 12 |
PHASE_CHAIN_MAX_RECORDS | 15 |
Authoritative C header:
kn86-emulator/src/cartridge_fsm.h.
Round-trip stability
Section titled “Round-trip stability”The test suite includes a 100-iteration stability check
(phase_chain_serialization_stable_100x) that asserts byte-identical
re-encoding after each decode. Any future change to the layout must keep
this test green or bump PHASE_CHAIN_FORMAT_VERSION.