Skip to content

API: add warp/get; accept $ in assemble, hex byte patterns in search, both register shapes in vic/write - #116

Merged
slajerek merged 2 commits into
slajerek:masterfrom
arekbr:feat/api-warp-get-and-input-formats
Aug 5, 2026
Merged

API: add warp/get; accept $ in assemble, hex byte patterns in search, both register shapes in vic/write#116
slajerek merged 2 commits into
slajerek:masterfrom
arekbr:feat/api-warp-get-and-input-formats

Conversation

@arekbr

@arekbr arekbr commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Four small API gaps that make the WebSockets API awkward to drive from a script. All are
additive or backward compatible.

warp/get

warp/set exists, but there is no way to read the state back, so an automated client cannot
verify its own action. The lower layer already has a real getter --
CDebugInterfaceVice::GetSettingIsWarpSpeed() reads VICE's warp_mode_enabled -- it was just
never exposed. Added CDebuggerApi::GetWarpSpeed(), a %s/warp/get endpoint, and made
%s/warp/set return the resulting state so one round trip is enough.

cpu/assemble rejected $

lda #$07 returned 400 {"error":"assemble_error","message":"Not a number after #"}, while
lda #7 worked. $ is the canonical 6502 notation, so it is the first thing a client tries.

The $ is stripped at the caller in CDebuggerApi.cpp, exactly the way
CViewMonitorConsole.cpp:2753-2766 already does it ("Remove '$' characters (assembler is
hex-only)"). I deliberately did not teach CViewDisassembly::AssembleGetToken about $:
that turns a malformed lda #$ from today's clean 400 into GetHexNumber() == -1 ->
instructionValue = 0xFFFF -> HTTP 200 and a silent A9 FF write to memory. The caller-side
strip keeps that input failing.

There was also a comment at the patched spot claiming the assembler "handles '$' by stripping
it internally via token parsing" -- it does not; it is replaced by the code that actually does.

cpu/search rejected hex byte patterns

a9 ?? returned 400 {"error":"unknown_mnemonic","mnemonic":"A9"}; only mnemonics such as
lda ?? were accepted. Hex bytes are the most common way to write a pattern by hand.

A first token of exactly two hex digits is now read as a raw opcode byte. This cannot collide
with a mnemonic: every name in the opcode table is three characters long, so the hex-looking
ADC, BCC and DEC are never two characters.

vic/read and vic/write disagreed on shape

vic/read returns a list of pairs [[27,0],...] (the handler builds an
unordered_map<u64,u8>, and nlohmann serializes a map with non-string keys as an array of
pairs), while vic/write required an object {reg: value}. Feeding a read result straight
back therefore failed with 500 [json.exception.type_error.302] type must be number, but is array -- and read-modify-write is the natural cycle for VIC registers.

vic/write now accepts both shapes. Anything that is neither array nor object is answered
with 406 instead of throwing out of the handler into a 500. vic/read is unchanged, so
existing clients are unaffected.

Verification

Debian 13, same binary before/after:

before after
warp/get after warp/set true / false 404 true / false
assemble "lda #$07" 400 200, bytes a9 07
assemble "lda #$" 400 400 (unchanged, on purpose)
search "a9 ??" 400 unknown_mnemonic 200, 55 matches, every one starting with A9
vic/write fed a vic/read result 500 type_error.302 200

@slajerek

slajerek commented Aug 4, 2026

Copy link
Copy Markdown
Owner

warp/get

approved

cpu/search rejected hex byte patterns

approved

vic/read and vic/write disagreed on shape

needs more details.

arekbr added 2 commits August 5, 2026 14:03
…earch, both register shapes in vic/write

Four API round-trip gaps that make automation awkward:

- warp could be set but not read, so a client could not verify its own action.
  Added CDebuggerApi::GetWarpSpeed() over the existing GetSettingIsWarpSpeed()
  and a %s/warp/get endpoint; %s/warp/set now returns the resulting state.

- cpu/assemble rejected "lda #$07" with 'Not a number after #'. '$' is the
  canonical 6502 notation, so this is the first thing a client tries. Strip it
  at the caller, exactly as CViewMonitorConsole already does, rather than
  teaching the shared assembler grammar about '$' -- that would turn the
  malformed "lda #$" from a clean 400 into a silent A9 FF write.

- cpu/search only accepted mnemonics, so "a9 ??" came back as
  unknown_mnemonic. A first token of exactly two hex digits is now read as a raw
  opcode byte; this cannot collide with a mnemonic, since every name in the
  opcode table is three characters long.

- vic/read hands back [[reg, val], ...] while vic/write demanded {reg: val},
  so feeding a read result straight back failed with type_error.302.
  vic/write now accepts both shapes and rejects anything else with 406 instead
  of throwing out of the handler.

Verified on Debian 13: warp/get tracks warp/set, "lda #$07" assembles to a9 07
while "lda #$" still fails, "a9 ??" returns 55 matches that all start with A9,
and a vic/read result can be written back unchanged.
Implements the shape agreed in the PR discussion. The old state was a trap:
vic/read emitted [[reg,val],...] pairs in RANDOM order (an accident of
serializing std::unordered_map through nlohmann), while vic/write demanded an
object -- so a read result could not be written back, and neither shape could
carry what chip registers actually need.

Canon: ordered list of records.

  read:  {"registers": [17, "$D016", "0x19"]} ->
         {"registers": [{"reg":17,"addr":53265,"value":155}, ...]}
         in REQUEST order (duplicates in the request are legal)
  write: {"registers": [{"reg":17,"value":155}, {"addr":"$D019","value":255},
                        {"reg":17,"value":27}]}
         executed in order, duplicates included

Why a list of records and not either trap branch: a JSON object carries
neither ORDER nor DUPLICATES, and both are semantics on memory-mapped chips
(interrupt acks, $D011/$D012 raster sequences, gate-off/gate-on in one batch);
pair lists are positional and undocumentable. Records are self-describing and
extensible. Round-trip holds: a read response is a valid write request.

Applied consistently to vic, cia, sid and drive1541/via. Reads emit records in
request order; cia/via records carry num+addr so a read written back lands on
the same chip; sid keeps its burst semantics (the batch describes final state,
so a later duplicate wins -- documented in the endpoint description).

Backward compatible: writes still accept the legacy object and the legacy pair
list; anything else is 406 instead of an exception-turned-500.

Also fixed on the way:
- sid register indices are now bounds-checked; previously a register number
  >= C64_NUM_SID_REGISTERS wrote straight past the sidRegs array
- Atari antic/gtia/pokey/pia writes parsed register keys with base-10 stoi(),
  so "0x18" silently became register 0 -- now the same dec/hex parser the C64
  endpoints use

Verified behaviorally (gate script, same binary): records in request order
with duplicates, all three write shapes land (read-back witness, masked for
VIC's unconnected always-1 bits), read->write round-trip, junk shape -> 406,
$DD02 resolves to CIA2, sid burst accepts records. All red on the previous
binary, 9/9 green after.
@arekbr
arekbr force-pushed the feat/api-warp-get-and-input-formats branch from dddb74e to 8a14318 Compare August 5, 2026 12:14
@arekbr

arekbr commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Reworked as discussed -- the shim is gone, the canon landed. Commit records on top of the branch (rebased onto current master, so the GetWarpSpeed hunk deduplicates cleanly with #117 whichever merges first).

The contract

read -- request unchanged; response is an ordered list of records, in request order (the old unordered_map made the order random between calls):

// c64/vic/read  {"registers": [17, "$D016", "0x19"]}
{"registers": [
  {"reg": 17, "addr": 53265, "value": 155},
  {"reg": 22, "addr": 53270, "value": 200},
  {"reg": 25, "addr": 53273, "value": 4}
]}

write -- ordered list of {reg|addr, value} records, executed in order, duplicates included (two writes to $D011 in one batch are a legal use case). Legacy shapes still accepted: the old object and the old pair list. Anything else is a 406, not an exception turned 500.

Applied consistently to vic, cia, sid and drive1541/via:

  • cia/via records also carry num + addr, so a read result written back lands on the same chip ($DD02 resolves to CIA2 on its own);
  • sid keeps its burst semantics -- the batch describes the final state, so a later duplicate wins (documented in the endpoint description);
  • round-trip holds everywhere: a read response is a valid write request, byte for byte.

Fixed on the way

  • sid register indices were not bounds-checked -- a register number >= 32 wrote straight past the sidRegs array (memory corruption from a WebSocket message);
  • Atari antic/gtia/pokey/pia writes parsed keys with base-10 stoi() -- "0x18" silently became register 0, landing the write on the wrong register with no error. Now the same dec/hex parser the C64 endpoints use.

Verified

Behaviorally, on the built binary, with a gate script whose every test is red on the previous binary (0/9) and green after (9/9): request-order with duplicates, all three write shapes (read-back witness, masked for VIC's unconnected always-1 bits), read->write round-trip, junk shape -> 406, $DD02->CIA2, sid burst with records. The previous gate from the earlier PRs also still passes 9/9.

CI on my fork:

branch Linux macOS Windows
this branch alone success ❌ pre-existing brew/sdl2 (fix in #118) ❌ pre-existing _m_prefetch (fix in #119)
this + #117 + #118 + #119 together success success success

So the merge order stays: #117, then #118/#119, then this.

(One footnote for honesty: the combined Windows run needed a rerun -- the first attempt died with lld-link crashing on the runner, exit 0xC0000005, zero compile errors in the log. Tool crash, not code; the rerun went green with no changes.)

@slajerek
slajerek merged commit f7b854d into slajerek:master Aug 5, 2026
1 of 3 checks passed
slajerek pushed a commit that referenced this pull request Aug 5, 2026
My mistake when splitting the original work into separate PRs: the warp/get
endpoint hunk in CDebuggerServerApi.cpp went into #114 (the commits were split
by file, and that file carried changes belonging to two topics), while the
CDebuggerApi::GetWarpSpeed() it calls stayed in the still-open #116. With #114
and #115 merged and #116 not, master does not compile on Linux:

  CDebuggerServerApi.cpp:112: error: 'class CDebuggerApi' has no member named
  'GetWarpSpeed'; did you mean 'SetWarpSpeed'?

This adds just that method (declaration + trivial forward to the existing
CDebugInterface::GetSettingIsWarpSpeed), nothing else. Verified: master +
this patch builds clean on Debian 13 / GCC 14.

Note: the macOS and Windows CI failures on the same runs are unrelated to the
merged PRs -- macOS dies in MTEngineSDL's Libtool step (Xcode 26.6 runner
image), Windows in bundled SDL's SDL_endian.h (_m_prefetch builtin clash).
Details in a comment on the run.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants