OXYGENHUB
◆ main enhanced-x16-emulator / src / debug_server.c
139.4 KiB · C · last changed feat: the RRDC control port (-controlport), contract 0.6: GET/POST /perf from the Performance panel's measurements, built by perf_json.c with DAP's x16/perfStats; a guest marker region named in advance; a framed guest's regions carried across the frame boundary and an interrupt after $F1 counted as work (#1) 25 days ago source blame history raw
1
// Commander X16 Emulator — Debug Adapter Protocol (DAP) Server
2
// Speaks DAP over TCP for direct IDE integration (Visual Studio, VS Code, etc.)
3
// Transport: Content-Length framed JSON over TCP, default port 9009
4
5
#include <SDL.h>
6
#include "debug_server.h"
7
#include "dbg_info.h"
8
#include "debugger.h"
9
#include "disasm.h"
10
#include "code_map.h"
11
#include "perf_budget.h"
12
#include "vera_bandwidth.h"
13
#include "perf_json.h"
14
#include "glue.h"
15
#include "memory.h"
16
#include "source_view.h"
17
#ifdef _WIN32
18
#include <ws2tcpip.h>
19
#else
20
#include <netinet/tcp.h>
21
#endif
22
#include "video.h"
23
#include "keyboard.h"
24
#include "joystick.h"
25
26
#include "cpu/fake6502.h"
27
28
#include <stdio.h>
29
#include <stdlib.h>
30
#include <string.h>
31
#include <ctype.h>
32
#include <cjson/cJSON.h>
33
34
// Platform socket abstraction
35
#ifdef _WIN32
36
  #include <winsock2.h>
37
  #include <ws2tcpip.h>
38
  #pragma comment(lib, "ws2_32.lib")
39
  typedef SOCKET socket_t;
40
  #define SOCKET_INVALID INVALID_SOCKET
41
  #define CLOSE_SOCKET closesocket
42
  #define SOCK_ERRNO WSAGetLastError()
43
  #define WOULD_BLOCK (SOCK_ERRNO == WSAEWOULDBLOCK)
44
  static bool wsa_initialized = false;
45
#else
46
  #include <sys/socket.h>
47
  #include <netinet/in.h>
48
  #include <arpa/inet.h>
49
  #include <fcntl.h>
50
  #include <unistd.h>
51
  #include <errno.h>
52
  typedef int socket_t;
53
  #define SOCKET_INVALID (-1)
54
  #define CLOSE_SOCKET close
55
  #define SOCK_ERRNO errno
56
  #define WOULD_BLOCK (SOCK_ERRNO == EAGAIN || SOCK_ERRNO == EWOULDBLOCK)
57
#endif
58
59
// Receive buffer (large enough for DAP messages with headers)
60
#define RECV_BUF_SIZE 65536
61
static char recv_buf[RECV_BUF_SIZE];
62
static int  recv_buf_len = 0;
63
64
// Send buffer
65
#define SEND_BUF_SIZE 65536
66
static char send_buf[SEND_BUF_SIZE];
67
68
// Server state
69
static socket_t listen_sock = SOCKET_INVALID;
70
static socket_t client_sock = SOCKET_INVALID;
71
static bool     server_enabled = false;
72
static int      server_port = 0;
73
74
// DAP sequence counter (for events we send)
75
static int dap_seq = 1;
76
77
// DAP variable reference IDs
78
#define VARREF_REGISTERS  1
79
#define VARREF_ZEROPAGE   2
80
#define VARREF_STACK      3
81
#define VARREF_VREGS      4   // X16 16-bit virtual registers R0-R15 (ZP $02-$21)
82
83
// Breakpoint tracking for DAP (maps DAP bp IDs to addresses)
84
#define MAX_DAP_BREAKPOINTS 256
85
static struct {
86
    int  dap_id;       // DAP breakpoint ID
87
    uint16_t addr;     // resolved address
88
    char file[256];    // source file
89
    int  line;         // source line
90
    bool verified;
91
    uint8_t bank;      // program bank (65816); addresses are 24-bit
92
    int  x16Bank;      // the selector it was armed with; a removal that passes
93
                       // DEBUG_BANK_ANY will not match a bank-pinned entry
94
    // Kept so the breakpoint can be armed again exactly as the client asked for
95
    // it when an overlay reload forces it to be re-resolved. Without these, a
96
    // reload quietly turned a bank-pinned conditional breakpoint into an
97
    // unconditional one that fires in every bank.
98
    char cond[192];
99
    char hit_cond[64];
100
} dap_bps[MAX_DAP_BREAKPOINTS];
101
static int num_dap_bps = 0;
102
static int next_dap_bp_id = 1;
103
104
// Function/instruction breakpoints tracked separately so each re-set clears its
105
// own set. All three add into the same core table, which now records which of
106
// them asked -- so each can clear what it asked for without knowing or caring
107
// what anyone else wanted at the same address.
108
static uint16_t func_bp_addrs[128];
109
static uint8_t  func_bp_banks[128];
110
static int      num_func_bps = 0;
111
static uint16_t instr_bp_addrs[128];
112
static uint8_t  instr_bp_banks[128];
113
static int      num_instr_bps = 0;
114
115
static bool     dap_session_active = false;
116
// Whether the stop the machine is currently sitting in has been announced. A
117
// breakpoint can hit while the client is still configuring, and configurationDone
118
// would then report the same halt a second time under a different reason.
119
static bool     dap_stop_announced = false;
120
121
// Is there a local debugger UI whose user owns the run state?
122
//
123
// When there is, a client connecting or going away must not resume the machine:
124
// somebody is sitting at a breakpoint looking at it. When there is not -- a
125
// headless -debugport run -- nothing else can ever resume it, so a session
126
// ending has to.
127
//
128
// Both front ends count, and both need their own test. -imgui does not set
129
// debug_window_enabled, and -debugport sets debugger_enabled by itself, so
130
// neither flag alone answers the question: testing only the first ignored the
131
// graphical debugger entirely and resumed the machine out from under it.
132
static bool
133
dap_local_ui_owns_run_state(void)
134
{
135
	return (debug_window_enabled && debugger_enabled)
136
	       || (imgui_debugger_enabled && video_debug_ui_available());
137
}
138
139
// Forward declarations
140
static void send_dap_event(const char *event_name, cJSON *body);
141
static void send_dap_message(cJSON *json);
142
static int dap_apply_bp_condition(dbg_addr_t addr, const char *cond_str, const char *hit_str);
143
144
// Re-resolve unverified breakpoints after new .dbg file loads
145
static void retry_unverified_breakpoints(void) {
146
    for (int i = 0; i < num_dap_bps; i++) {
147
        if (!dap_bps[i].verified) {
148
            dbg_addr_t addr = 0;
149
            if (dbg_info_source_to_addr(dap_bps[i].file, dap_bps[i].line, &addr)) {
150
                // Re-arm it exactly as the client asked for it. Re-applying the
151
                // condition also recovers the bank pin, which decides the
152
                // selector the core entry is keyed on -- arming with ANY here
153
                // turned a bank-pinned conditional breakpoint into one that
154
                // fires in every bank on its first arrival.
155
                int bank_pin = dap_apply_bp_condition(addr,
156
                                                      dap_bps[i].cond,
157
                                                      dap_bps[i].hit_cond);
158
                dap_bps[i].addr = (uint16_t)(addr & 0xFFFF);
159
                dap_bps[i].bank = (uint8_t)(addr >> 16);
160
                dap_bps[i].verified = true;
161
                dap_bps[i].x16Bank = bank_pin;
162
                struct breakpoint hw_bp;
163
                hw_bp.pc = (int)(addr & 0xFFFF);
164
                hw_bp.bank = (uint8_t)(addr >> 16);
165
                hw_bp.x16Bank = bank_pin;
166
                debug_bp_add_for(hw_bp, DEBUG_OWNER_DAP_SOURCE);
167
                printf("[dap] Resolved pending breakpoint: %s:%d -> $%04X\n",
168
                       dap_bps[i].file, dap_bps[i].line, (unsigned)addr);
169
                // Say so. This is reached after invalidate_breakpoints_in_range()
170
                // has told the client the breakpoint is unverified, so staying
171
                // quiet would leave it showing one that is in fact armed.
172
                if (client_sock != SOCKET_INVALID) {
173
                    cJSON *body = cJSON_CreateObject();
174
                    cJSON *bp = cJSON_CreateObject();
175
                    cJSON_AddNumberToObject(bp, "id", dap_bps[i].dap_id);
176
                    cJSON_AddBoolToObject(bp, "verified", true);
177
                    cJSON_AddNumberToObject(bp, "line", dap_bps[i].line);
178
                    cJSON_AddItemToObject(body, "breakpoint", bp);
179
                    cJSON_AddStringToObject(body, "reason", "changed");
180
                    send_dap_event("breakpoint", body);
181
                }
182
            }
183
        }
184
    }
185
}
186
187
// Invalidate breakpoints whose addresses fall within a range being unloaded
188
static void invalidate_breakpoints_in_range(uint32_t start, uint32_t end) {
189
    // Compared as full 24-bit addresses, program bank included. Clamping the
190
    // range to 16 bits and comparing only the low half meant reloading a
191
    // bank-1 module left its breakpoints armed and stale, while reloading a
192
    // bank-0 range invalidated unrelated breakpoints in every other bank.
193
    for (int i = 0; i < num_dap_bps; i++) {
194
        const uint32_t bp_addr =
195
            ((uint32_t)dap_bps[i].bank << 16) | dap_bps[i].addr;
196
        if (dap_bps[i].verified && bp_addr >= start && bp_addr <= end) {
197
            // Drop this session's claim on the core entry. It goes only if
198
            // nothing else wants it, so a -bp or an F9 breakpoint at the same
199
            // address survives a module reload that has nothing to do with it.
200
            debug_bp_remove_for(dap_bps[i].addr, dap_bps[i].bank, dap_bps[i].x16Bank,
201
                                DEBUG_OWNER_DAP_SOURCE);
202
203
            dap_bps[i].addr = 0;
204
            dap_bps[i].bank = 0;
205
            printf("[dap] Invalidated breakpoint: %s:%d (range $%06X-$%06X unloaded)\n",
206
                   dap_bps[i].file, dap_bps[i].line, start, end);
207
208
            // Notify VS that breakpoint is no longer verified
209
            if (client_sock != SOCKET_INVALID) {
210
                cJSON *body = cJSON_CreateObject();
211
                cJSON *bp = cJSON_CreateObject();
212
                cJSON_AddNumberToObject(bp, "id", dap_bps[i].dap_id);
213
                cJSON_AddBoolToObject(bp, "verified", false);
214
                cJSON_AddStringToObject(bp, "message", "Module unloaded");
215
                cJSON_AddNumberToObject(bp, "line", dap_bps[i].line);
216
                cJSON_AddItemToObject(body, "breakpoint", bp);
217
                cJSON_AddStringToObject(body, "reason", "changed");
218
                send_dap_event("breakpoint", body);
219
            }
220
        }
221
    }
222
}
223
224
// Public wrappers so the shared runtime-load handler (debugger.c) can drive the
225
// DAP-only breakpoint bookkeeping around a module swap. Both are no-ops when no
226
// DAP client is connected, so the native source window works without DAP.
227
void debug_server_invalidate_breakpoints_in_range(uint32_t start, uint32_t end) {
228
    if (!server_enabled || client_sock == SOCKET_INVALID) return;
229
    invalidate_breakpoints_in_range(start, end);
230
}
231
232
void debug_server_retry_unverified_breakpoints(void) {
233
    if (!server_enabled || client_sock == SOCKET_INVALID) return;
234
    retry_unverified_breakpoints();
235
}
236
237
// ─── Platform helpers ───────────────────────────────────────────────
238
239
static bool set_nonblocking(socket_t sock) {
240
#ifdef _WIN32
241
    u_long mode = 1;
242
    return ioctlsocket(sock, FIONBIO, &mode) == 0;
243
#else
244
    int flags = fcntl(sock, F_GETFL, 0);
245
    if (flags < 0) return false;
246
    return fcntl(sock, F_SETFL, flags | O_NONBLOCK) == 0;
247
#endif
248
}
249
250
static bool platform_init(void) {
251
#ifdef _WIN32
252
    if (!wsa_initialized) {
253
        WSADATA wsa;
254
        if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
255
            fprintf(stderr, "[dap] WSAStartup failed: %d\n", WSAGetLastError());
256
            return false;
257
        }
258
        wsa_initialized = true;
259
    }
260
#endif
261
    return true;
262
}
263
264
static void platform_cleanup(void) {
265
#ifdef _WIN32
266
    if (wsa_initialized) {
267
        WSACleanup();
268
        wsa_initialized = false;
269
    }
270
#endif
271
}
272
273
// ─── DAP message sending ────────────────────────────────────────────
274
275
static void disconnect_client(void);
276
static void dap_release_session_state(void);
277
278
static void send_dap_message(cJSON *json) {
279
    if (client_sock == SOCKET_INVALID) {
280
        cJSON_Delete(json);
281
        return;
282
    }
283
    char *body = cJSON_PrintUnformatted(json);
284
    if (body) {
285
        int body_len = (int)strlen(body);
286
        int header_len = snprintf(send_buf, sizeof(send_buf),
287
                                  "Content-Length: %d\r\n\r\n", body_len);
288
        if (header_len + body_len >= SEND_BUF_SIZE) {
289
            // Too large to frame. Dropping it silently left the client waiting
290
            // on a request_seq that would never be answered -- a source file
291
            // over 64K is enough to do it, and the editor shows a tab that
292
            // never finishes loading. The receive side already drops a client
293
            // it cannot buffer; do the same rather than hang.
294
            fprintf(stderr, "[dap] response of %d bytes exceeds the send buffer; dropping client\n",
295
                    header_len + body_len);
296
            cJSON_free(body);
297
            cJSON_Delete(json);
298
            disconnect_client();
299
            return;
300
        }
301
        {
302
            memcpy(send_buf + header_len, body, body_len);
303
            int total = header_len + body_len;
304
            int offset = 0;
305
            // A client that stops reading fills the socket buffer, and this is
306
            // the emulator's only thread. Retrying forever parks the machine on
307
            // a peer that may never come back, so give up after a bounded wait
308
            // and drop the client instead: a dead session is recoverable, a
309
            // frozen emulator is not.
310
            int stalls = 0;
311
            const int max_stalls = 500;          // ~500ms of a peer not reading
312
            while (offset < total) {
313
                int n = send(client_sock, send_buf + offset, total - offset, 0);
314
                if (n > 0) {
315
                    offset += n;
316
                    stalls = 0;
317
                } else if (n < 0 && WOULD_BLOCK) {
318
                    if (++stalls > max_stalls) {
319
                        fprintf(stderr, "[dap] client is not reading; dropping it\n");
320
                        disconnect_client();
321
                        break;
322
                    }
323
                    // SDL's, rather than Sleep/usleep behind a platform split:
324
                    // usleep needs a feature-test macro this file does not set,
325
                    // which only the -Werror Linux builds noticed.
326
                    SDL_Delay(1);
327
                } else {
328
                    break; // real error
329
                }
330
            }
331
        }
332
        cJSON_free(body);
333
    }
334
    cJSON_Delete(json);
335
}
336
337
static void send_dap_response(int request_seq, const char *command, bool success, cJSON *body) {
338
    cJSON *resp = cJSON_CreateObject();
339
    cJSON_AddNumberToObject(resp, "seq", dap_seq++);
340
    cJSON_AddStringToObject(resp, "type", "response");
341
    cJSON_AddNumberToObject(resp, "request_seq", request_seq);
342
    cJSON_AddBoolToObject(resp, "success", success);
343
    cJSON_AddStringToObject(resp, "command", command);
344
    if (body) {
345
        cJSON_AddItemToObject(resp, "body", body);
346
    }
347
    send_dap_message(resp);
348
}
349
350
static void send_dap_error_response(int request_seq, const char *command, const char *message) {
351
    cJSON *body = cJSON_CreateObject();
352
    cJSON *error_obj = cJSON_CreateObject();
353
    cJSON_AddNumberToObject(error_obj, "id", 1);
354
    cJSON_AddStringToObject(error_obj, "format", message);
355
    cJSON_AddItemToObject(body, "error", error_obj);
356
357
    cJSON *resp = cJSON_CreateObject();
358
    cJSON_AddNumberToObject(resp, "seq", dap_seq++);
359
    cJSON_AddStringToObject(resp, "type", "response");
360
    cJSON_AddNumberToObject(resp, "request_seq", request_seq);
361
    cJSON_AddBoolToObject(resp, "success", false);
362
    cJSON_AddStringToObject(resp, "command", command);
363
    cJSON_AddStringToObject(resp, "message", message);
364
    cJSON_AddItemToObject(resp, "body", body);
365
    send_dap_message(resp);
366
}
367
368
static void send_dap_event(const char *event_name, cJSON *body) {
369
    cJSON *event = cJSON_CreateObject();
370
    cJSON_AddNumberToObject(event, "seq", dap_seq++);
371
    cJSON_AddStringToObject(event, "type", "event");
372
    cJSON_AddStringToObject(event, "event", event_name);
373
    if (body) {
374
        cJSON_AddItemToObject(event, "body", body);
375
    }
376
    send_dap_message(event);
377
}
378
379
// ContinuedEvent — tell the client execution resumed (all threads).
380
static void send_continued_event(void) {
381
    dap_stop_announced = false;   // whatever we were stopped in is over
382
    if (client_sock == SOCKET_INVALID) return;
383
    cJSON *body = cJSON_CreateObject();
384
    cJSON_AddNumberToObject(body, "threadId", 1);
385
    cJSON_AddBoolToObject(body, "allThreadsContinued", true);
386
    send_dap_event("continued", body);
387
}
388
389
// OutputEvent — forward text to the client's debug console. Public API so the
390
// emulator can pipe program/console output to an attached DAP client.
391
void debug_server_output(const char *category, const char *text) {
392
    if (!server_enabled || client_sock == SOCKET_INVALID || !text) return;
393
    cJSON *body = cJSON_CreateObject();
394
    cJSON_AddStringToObject(body, "category", category ? category : "console");
395
    cJSON_AddStringToObject(body, "output", text);
396
    send_dap_event("output", body);
397
}
398
399
// Pending events to send on next poll (avoid back-to-back sends)
400
// Announces the single emulated CPU as a DAP thread. Sent after launch and
401
// after attach, which are the two ways a session starts.
402
static void send_dap_event_thread_started(void);
403
404
// Set by launch when the client asked to be given control before anything runs.
405
static bool dap_stop_on_entry = false;
406
407
// ─── DAP command handlers ───────────────────────────────────────────
408
409
static int handle_dap_initialize(int seq, cJSON *args) {
410
    (void)args;
411
    cJSON *caps = cJSON_CreateObject();
412
    cJSON_AddBoolToObject(caps, "supportsConfigurationDoneRequest", true);
413
    cJSON_AddBoolToObject(caps, "supportsSetVariable", true);
414
    cJSON_AddBoolToObject(caps, "supportsRestartRequest", true);
415
    cJSON_AddBoolToObject(caps, "supportsTerminateRequest", true);
416
    cJSON_AddBoolToObject(caps, "supportsReadMemoryRequest", true);
417
    cJSON_AddBoolToObject(caps, "supportsWriteMemoryRequest", true);
418
    cJSON_AddBoolToObject(caps, "supportsDisassembleRequest", true);
419
    cJSON_AddBoolToObject(caps, "supportTerminateDebuggee", true);
420
    cJSON_AddBoolToObject(caps, "supportsEvaluateForHovers", true);
421
    cJSON_AddBoolToObject(caps, "supportsSteppingGranularity", true);
422
    cJSON_AddBoolToObject(caps, "supportsGotoTargetsRequest", true);
423
    cJSON_AddBoolToObject(caps, "supportsSetExpression", false);
424
    cJSON_AddBoolToObject(caps, "supportsValueFormattingOptions", true);
425
    cJSON_AddBoolToObject(caps, "supportsFunctionBreakpoints", true);
426
    // Newly implemented in this build:
427
    cJSON_AddBoolToObject(caps, "supportsDataBreakpoints", true);
428
    cJSON_AddBoolToObject(caps, "supportsInstructionBreakpoints", true);
429
    cJSON_AddBoolToObject(caps, "supportsLoadedSourcesRequest", true);
430
    cJSON_AddBoolToObject(caps, "supportsBreakpointLocationsRequest", true);
431
    cJSON_AddBoolToObject(caps, "supportsStepInTargetsRequest", true);
432
    cJSON_AddBoolToObject(caps, "supportsConditionalBreakpoints", true);
433
    cJSON_AddBoolToObject(caps, "supportsHitConditionalBreakpoints", true);
434
    // Non-standard, so that a client can tell this emulator's performance
435
    // budget surface from one that would reject the requests.
436
    cJSON_AddBoolToObject(caps, "supportsX16PerfStats", true);
437
    send_dap_response(seq, "initialize", true, caps);
438
439
    dap_session_active = true;
440
441
    // Per the DAP spec this follows the initialize response immediately: a
442
    // client is entitled to wait for it before sending configurationDone, so
443
    // deferring it to the next request deadlocks anything strict about that.
444
    send_dap_event("initialized", NULL);
445
    return 0;
446
}
447
448
static int handle_dap_launch(int seq, cJSON *args) {
449
    // Load .dbg file if specified in launch arguments (additive, don't clear existing)
450
    if (args) {
451
        cJSON *dbg_file = cJSON_GetObjectItemCaseSensitive(args, "dbgFile");
452
        if (dbg_file && cJSON_IsString(dbg_file)) {
453
            if (dbg_info_load(dbg_file->valuestring) == 0) {
454
                printf("[dap] Loaded debug info: %s\n", dbg_file->valuestring);
455
            } else {
456
                fprintf(stderr, "[dap] Warning: failed to load debug info: %s\n",
457
                        dbg_file->valuestring);
458
            }
459
        }
460
        cJSON *soe = cJSON_GetObjectItemCaseSensitive(args, "stopOnEntry");
461
        dap_stop_on_entry = (soe && cJSON_IsTrue(soe));
462
    }
463
    send_dap_response(seq, "launch", true, NULL);
464
465
    send_dap_event_thread_started();
466
467
    return 0;
468
}
469
470
static int handle_dap_configuration_done(int seq) {
471
    send_dap_response(seq, "configurationDone", true, NULL);
472
473
    // Sending can drop the client -- a peer that stops reading is disconnected
474
    // rather than allowed to freeze the emulator -- so there may be no session
475
    // left to configure. Carrying on would halt a headless machine with nobody
476
    // able to resume it.
477
    if (client_sock == SOCKET_INVALID) {
478
        dap_stop_on_entry = false;
479
        return 0;
480
    }
481
482
    // Already halted, because a breakpoint set moments ago has hit or a
483
    // previous client left it that way. Report where we actually are; resuming
484
    // would drive past a stop the client should see, and saying nothing leaves
485
    // it showing a running target it cannot pause. Checked before stopOnEntry
486
    // so the same halt is not announced twice under two different reasons.
487
    if (currentMode == DMODE_STOP) {
488
        dap_stop_on_entry = false;
489
        // Unless it has already been reported -- a breakpoint that hit while
490
        // the client was still configuring emits its own stopped event, and
491
        // announcing the same halt again under a different reason confuses a
492
        // client into thinking it stopped twice.
493
        if (!dap_stop_announced) debug_server_notify_stopped("breakpoint");
494
        return 1;
495
    }
496
497
    // The client wants control before anything executes, and is waiting for the
498
    // stopped event to know it has it.
499
    if (dap_stop_on_entry) {
500
        dap_stop_on_entry = false;
501
        currentMode = DMODE_STOP;
502
        debug_server_notify_stopped("entry");
503
        return 1;
504
    }
505
506
    currentMode = DMODE_RUN;
507
    return 1;
508
}
509
510
static int handle_dap_threads(int seq) {
511
    cJSON *body = cJSON_CreateObject();
512
    cJSON *threads = cJSON_CreateArray();
513
    cJSON *thread = cJSON_CreateObject();
514
    cJSON_AddNumberToObject(thread, "id", 1);
515
    cJSON_AddStringToObject(thread, "name", "6502 CPU");
516
    cJSON_AddItemToArray(threads, thread);
517
    cJSON_AddItemToObject(body, "threads", threads);
518
    send_dap_response(seq, "threads", true, body);
519
    return 0;
520
}
521
522
// Build a DAP stack frame object for a code address. `bank` is the 65C816
523
// program bank (0 on the 65C02); it makes the instructionPointerReference a
524
// full 24-bit address so disassembly opens in the right bank on GS.
525
static cJSON *build_stack_frame(int id, uint16_t addr, uint8_t bank, bool is_current) {
526
    cJSON *frame = cJSON_CreateObject();
527
    cJSON_AddNumberToObject(frame, "id", id);
528
529
    const char *src_file = NULL;
530
    int         src_line = 0;
531
    bool        has_source = dbg_info_addr_to_source_banked(addr, (int)memory_get_ram_bank(),
532
                                                            &src_file, &src_line);
533
534
    const char *label = NULL;
535
    char        name_buf[80];
536
    const char *afmt = (bank != 0 || is_gen2) ? "%s ($%02X:%04X)" : "%s ($%04X)";
537
    const char *nfmt = (bank != 0 || is_gen2) ? "$%02X:%04X" : "$%04X";
538
    if (dbg_info_addr_to_label(addr, &label) && label) {
539
        if (bank != 0 || is_gen2) snprintf(name_buf, sizeof(name_buf), afmt, label, bank, addr);
540
        else                      snprintf(name_buf, sizeof(name_buf), afmt, label, addr);
541
    } else {
542
        if (bank != 0 || is_gen2) snprintf(name_buf, sizeof(name_buf), nfmt, bank, addr);
543
        else                      snprintf(name_buf, sizeof(name_buf), nfmt, addr);
544
    }
545
    cJSON_AddStringToObject(frame, "name", name_buf);
546
    cJSON_AddNumberToObject(frame, "line", has_source ? src_line : 0);
547
    cJSON_AddNumberToObject(frame, "column", 1);
548
549
    if (has_source) {
550
        cJSON *source = cJSON_CreateObject();
551
        cJSON_AddStringToObject(source, "path", src_file);
552
        const char *basename = strrchr(src_file, '/');
553
        if (!basename) basename = strrchr(src_file, '\\');
554
        cJSON_AddStringToObject(source, "name", basename ? basename + 1 : src_file);
555
        cJSON_AddItemToObject(frame, "source", source);
556
    } else {
557
        cJSON *hint = cJSON_CreateObject();
558
        cJSON_AddStringToObject(hint, "kind", is_current ? "normal" : "subtle");
559
        cJSON_AddItemToObject(frame, "presentationHint", hint);
560
        cJSON *source = cJSON_CreateObject();
561
        cJSON_AddStringToObject(source, "name", name_buf);
562
        cJSON_AddNumberToObject(source, "sourceReference", 0);
563
        cJSON_AddStringToObject(source, "origin", "disassembly");
564
        cJSON_AddItemToObject(frame, "source", source);
565
    }
566
567
    char instr_ref[16];
568
    if (bank != 0 || is_gen2)
569
        snprintf(instr_ref, sizeof(instr_ref), "0x%06X", ((uint32_t)bank << 16) | addr);
570
    else
571
        snprintf(instr_ref, sizeof(instr_ref), "0x%04X", addr);
572
    cJSON_AddStringToObject(frame, "instructionPointerReference", instr_ref);
573
    return frame;
574
}
575
576
static int handle_dap_stack_trace(int seq, cJSON *args) {
577
    int startFrame = 0, levels = 0;
578
    if (args) {
579
        cJSON *sf = cJSON_GetObjectItemCaseSensitive(args, "startFrame");
580
        if (sf && cJSON_IsNumber(sf)) startFrame = (int)sf->valuedouble;
581
        cJSON *lv = cJSON_GetObjectItemCaseSensitive(args, "levels");
582
        if (lv && cJSON_IsNumber(lv)) levels = (int)lv->valuedouble;
583
    }
584
585
    // Heuristically unwind the hardware stack (no frame pointer on 6502): frame 0
586
    // is the current PC, then scan for return-address pairs preceded by a JSR.
587
    uint16_t addrs[64];
588
    int      nf = 0;
589
    addrs[nf++] = regs.pc;
590
591
    bool     native = regs.is65c816 && !regs.e;
592
    uint32_t sp     = regs.sp;
593
    uint32_t start  = native ? (sp + 1) : (0x100 + (sp & 0xFF) + 1);
594
    uint32_t end    = native ? (sp + 1 + 256) : 0x200;
595
    for (uint32_t a = start; a + 1 < end && nf < 64;) {
596
        uint8_t  lo     = debug_read6502((uint16_t)a, 0, USE_CURRENT_X16_BANK);
597
        uint8_t  hi     = debug_read6502((uint16_t)(a + 1), 0, USE_CURRENT_X16_BANK);
598
        uint16_t pushed = (uint16_t)(lo | (hi << 8));
599
        uint8_t  opc    = debug_read6502((uint16_t)(pushed - 2), regs.k, USE_CURRENT_X16_BANK);
600
        if (opc == 0x20) { addrs[nf++] = (uint16_t)(pushed + 1); a += 2; }
601
        else             { a += 1; }
602
    }
603
604
    cJSON *body   = cJSON_CreateObject();
605
    cJSON *frames = cJSON_CreateArray();
606
    int    total  = nf;
607
    if (startFrame < 0) startFrame = 0;
608
    if (startFrame > nf) startFrame = nf;
609
    int    last   = (levels > 0) ? (startFrame + levels) : nf;
610
    if (last > nf) last = nf;
611
    for (int i = startFrame; i < last; i++) {
612
        cJSON_AddItemToArray(frames, build_stack_frame(i + 1, addrs[i], regs.k, i == 0));
613
    }
614
    cJSON_AddItemToObject(body, "stackFrames", frames);
615
    cJSON_AddNumberToObject(body, "totalFrames", total);
616
    send_dap_response(seq, "stackTrace", true, body);
617
    return 0;
618
}
619
620
static int handle_dap_scopes(int seq, cJSON *args) {
621
    (void)args;
622
    cJSON *body = cJSON_CreateObject();
623
    cJSON *scopes = cJSON_CreateArray();
624
625
    // Registers scope
626
    cJSON *reg_scope = cJSON_CreateObject();
627
    cJSON_AddStringToObject(reg_scope, "name", "Registers");
628
    cJSON_AddNumberToObject(reg_scope, "variablesReference", VARREF_REGISTERS);
629
    cJSON_AddBoolToObject(reg_scope, "expensive", false);
630
    cJSON_AddStringToObject(reg_scope, "presentationHint", "registers");
631
    cJSON_AddItemToArray(scopes, reg_scope);
632
633
    // Virtual registers scope — the X16's sixteen 16-bit pseudo-registers R0-R15
634
    // (a software convention shadowed in zero page $02-$21; used by the KERNAL/
635
    // VERA/GRAPH APIs and cc65). Presented like CPU registers so they can be
636
    // watched/edited as first-class 16-bit registers.
637
    cJSON *vreg_scope = cJSON_CreateObject();
638
    cJSON_AddStringToObject(vreg_scope, "name", "Virtual Regs (R0-R15)");
639
    cJSON_AddNumberToObject(vreg_scope, "variablesReference", VARREF_VREGS);
640
    cJSON_AddBoolToObject(vreg_scope, "expensive", false);
641
    cJSON_AddStringToObject(vreg_scope, "presentationHint", "registers");
642
    cJSON_AddItemToArray(scopes, vreg_scope);
643
644
    // Zero Page scope
645
    cJSON *zp_scope = cJSON_CreateObject();
646
    cJSON_AddStringToObject(zp_scope, "name", "Zero Page");
647
    cJSON_AddNumberToObject(zp_scope, "variablesReference", VARREF_ZEROPAGE);
648
    cJSON_AddBoolToObject(zp_scope, "expensive", false);
649
    cJSON_AddItemToArray(scopes, zp_scope);
650
651
    // Stack scope
652
    cJSON *stk_scope = cJSON_CreateObject();
653
    cJSON_AddStringToObject(stk_scope, "name", "Stack");
654
    cJSON_AddNumberToObject(stk_scope, "variablesReference", VARREF_STACK);
655
    cJSON_AddBoolToObject(stk_scope, "expensive", false);
656
    cJSON_AddItemToArray(scopes, stk_scope);
657
658
    cJSON_AddItemToObject(body, "scopes", scopes);
659
    send_dap_response(seq, "scopes", true, body);
660
    return 0;
661
}
662
663
static void add_variable(cJSON *vars, const char *name, const char *value, const char *type) {
664
    cJSON *var = cJSON_CreateObject();
665
    cJSON_AddStringToObject(var, "name", name);
666
    cJSON_AddStringToObject(var, "value", value);
667
    if (type) cJSON_AddStringToObject(var, "type", type);
668
    cJSON_AddNumberToObject(var, "variablesReference", 0);
669
    cJSON_AddItemToArray(vars, var);
670
}
671
672
// Parse a number from a DAP value string: "$hex", "0xhex", or decimal.
673
static uint32_t dap_parse_num(const char *s) {
674
    if (!s) return 0;
675
    while (*s == ' ') s++;
676
    if (s[0] == '$')
677
        return (uint32_t)strtoul(s + 1, NULL, 16);
678
    if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X'))
679
        return (uint32_t)strtoul(s + 2, NULL, 16);
680
    return (uint32_t)strtoul(s, NULL, 0);
681
}
682
683
static bool dap_has_prefix_i(const char *s, const char *prefix) {
684
    while (*prefix) {
685
        if (tolower((unsigned char)*s) != tolower((unsigned char)*prefix)) return false;
686
        s++; prefix++;
687
    }
688
    return true;
689
}
690
691
// Parse a DAP memoryReference. A bare number is a CPU address (24-bit
692
// bank:addr); a "vram:" prefix selects VERA's 17-bit video address space, which
693
// is not in the CPU map and so cannot be reached through a CPU address at all.
694
// VRAM references default to hex, since every VRAM address in the docs and the
695
// debugger is written that way.
696
//
697
// Fails rather than falling back to zero on a reference it cannot read. The old
698
// behaviour ran the string through strtoul and used whatever came out, so a
699
// typo, a "$C000" and a "vram:1B000" all quietly returned the bytes at CPU
700
// address 0 with success:true -- the wrong memory, with nothing in the response
701
// to say so.
702
static bool dap_parse_mem_ref(const char *s, bool *is_vram, uint32_t *out) {
703
    if (!s) return false;
704
    while (*s == ' ') s++;
705
706
    *is_vram = false;
707
    if (dap_has_prefix_i(s, "vram:")) {
708
        *is_vram = true;
709
        s += 5;
710
        while (*s == ' ') s++;
711
    }
712
713
    int base = *is_vram ? 16 : 0;
714
    if (s[0] == '$') {
715
        s++;
716
        base = 16;
717
    } else if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
718
        s += 2;
719
        base = 16;
720
    }
721
722
    char *endp = NULL;
723
    unsigned long v = strtoul(s, &endp, base);
724
    if (!endp || endp == s) return false;
725
    while (*endp == ' ') endp++;
726
    if (*endp) return false;
727
728
    *out = (uint32_t)v;
729
    return true;
730
}
731
732
// Non-intrusive VERA register read for debug views. AUDIO_CTRL (0x1B) runs
733
// audio_render() and SPI_DATA (0x1E) starts a real SPI transfer *on read*
734
// (video_read ignores debugOn for them), so never poll those — return 0.
735
static uint8_t vera_read_dbg(uint8_t reg) {
736
    reg &= 0x1F;
737
    if (reg == 0x1B || reg == 0x1E) return 0;
738
    return video_read(reg, true);
739
}
740
741
// Direct-page base + offset, matching the CPU's direct_page_add() (support.h).
742
// The X16 r0-r15 pseudo-registers live at D+$02..D+$21; D is 0 under the KERNAL
743
// ABI but can be non-zero in 65C816 native (GS) code, so honor it.
744
static uint16_t dap_dp_add(uint16_t offset) {
745
    if (regs.e && (regs.dp & 0x00FF) == 0)
746
        return (uint16_t)((regs.dp & 0xFF00) | (uint8_t)((uint8_t)(regs.dp & 0xFF) + (offset & 0xFF)));
747
    return (uint16_t)(regs.dp + offset);
748
}
749
750
static int handle_dap_variables(int seq, cJSON *args) {
751
    int ref = 0;
752
    if (args) {
753
        cJSON *ref_item = cJSON_GetObjectItemCaseSensitive(args, "variablesReference");
754
        if (ref_item && cJSON_IsNumber(ref_item)) ref = (int)ref_item->valuedouble;
755
    }
756
757
    cJSON *body = cJSON_CreateObject();
758
    cJSON *vars = cJSON_CreateArray();
759
    char buf[32];
760
761
    if (ref == VARREF_REGISTERS) {
762
        // PC is 24-bit in gen2/GS (program bank K : 16-bit PC).
763
        if (is_gen2 || (regs.is65c816 && regs.k != 0))
764
            snprintf(buf, sizeof(buf), "$%02X:%04X", regs.k, regs.pc);
765
        else
766
            snprintf(buf, sizeof(buf), "$%04X (%d)", regs.pc, regs.pc);
767
        add_variable(vars, "PC", buf, "uint16");
768
769
        // A/X/Y: show 16-bit in native mode, 8-bit in emulation mode
770
        if (regs.is65c816 && !regs.e) {
771
            // Native mode: A width depends on M flag, X/Y on X flag
772
            if (regs.status & FLAG_MEMORY_WIDTH) {
773
                snprintf(buf, sizeof(buf), "$%02X (%d)", regs.a, regs.a);
774
                add_variable(vars, "A", buf, "uint8");
775
            } else {
776
                snprintf(buf, sizeof(buf), "$%04X (%d)", regs.c, regs.c);
777
                add_variable(vars, "A", buf, "uint16");
778
            }
779
            if (regs.status & FLAG_INDEX_WIDTH) {
780
                snprintf(buf, sizeof(buf), "$%02X (%d)", (uint8_t)regs.x, (uint8_t)regs.x);
781
                add_variable(vars, "X", buf, "uint8");
782
                snprintf(buf, sizeof(buf), "$%02X (%d)", (uint8_t)regs.y, (uint8_t)regs.y);
783
                add_variable(vars, "Y", buf, "uint8");
784
            } else {
785
                snprintf(buf, sizeof(buf), "$%04X (%d)", regs.x, regs.x);
786
                add_variable(vars, "X", buf, "uint16");
787
                snprintf(buf, sizeof(buf), "$%04X (%d)", regs.y, regs.y);
788
                add_variable(vars, "Y", buf, "uint16");
789
            }
790
            snprintf(buf, sizeof(buf), "$%04X (%d)", regs.sp, regs.sp);
791
            add_variable(vars, "SP", buf, "uint16");
792
            snprintf(buf, sizeof(buf), "$%04X (%d)", regs.dp, regs.dp);
793
            add_variable(vars, "D", buf, "uint16");
794
            snprintf(buf, sizeof(buf), "$%02X (%d)", regs.db, regs.db);
795
            add_variable(vars, "DBR", buf, "uint8");
796
            snprintf(buf, sizeof(buf), "$%02X (%d)", regs.k, regs.k);
797
            add_variable(vars, "PBR", buf, "uint8");
798
        } else {
799
            // Emulation mode: all 8-bit
800
            snprintf(buf, sizeof(buf), "$%02X (%d)", regs.a, regs.a);
801
            add_variable(vars, "A", buf, "uint8");
802
            snprintf(buf, sizeof(buf), "$%02X (%d)", (uint8_t)regs.x, (uint8_t)regs.x);
803
            add_variable(vars, "X", buf, "uint8");
804
            snprintf(buf, sizeof(buf), "$%02X (%d)", (uint8_t)regs.y, (uint8_t)regs.y);
805
            add_variable(vars, "Y", buf, "uint8");
806
            snprintf(buf, sizeof(buf), "$%02X (%d)", (uint8_t)regs.sp, (uint8_t)regs.sp);
807
            add_variable(vars, "SP", buf, "uint8");
808
        }
809
810
        // Status flags
811
        uint8_t p = regs.status;
812
        if (regs.is65c816 && !regs.e) {
813
            snprintf(buf, sizeof(buf), "$%02X [%c%c%c%c%c%c%c%c]", p,
814
                     (p & 0x80) ? 'N' : 'n', (p & 0x40) ? 'V' : 'v',
815
                     (p & 0x20) ? 'M' : 'm', (p & 0x10) ? 'X' : 'x',
816
                     (p & 0x08) ? 'D' : 'd', (p & 0x04) ? 'I' : 'i',
817
                     (p & 0x02) ? 'Z' : 'z', (p & 0x01) ? 'C' : 'c');
818
        } else {
819
            snprintf(buf, sizeof(buf), "$%02X [%c%c-%c%c%c%c%c]", p,
820
                     (p & 0x80) ? 'N' : 'n', (p & 0x40) ? 'V' : 'v',
821
                     (p & 0x10) ? 'B' : 'b',
822
                     (p & 0x08) ? 'D' : 'd', (p & 0x04) ? 'I' : 'i',
823
                     (p & 0x02) ? 'Z' : 'z', (p & 0x01) ? 'C' : 'c');
824
        }
825
        add_variable(vars, "P (Status)", buf, "uint8");
826
827
        snprintf(buf, sizeof(buf), "$%02X (%d)", memory_get_ram_bank(), memory_get_ram_bank());
828
        add_variable(vars, "RAM Bank", buf, "uint8");
829
830
        snprintf(buf, sizeof(buf), "$%02X (%d)", memory_get_rom_bank(), memory_get_rom_bank());
831
        add_variable(vars, "ROM Bank", buf, "uint8");
832
833
    } else if (ref == VARREF_ZEROPAGE) {
834
        // Show zero page in groups of 16 bytes
835
        for (int row = 0; row < 16; row++) {
836
            char name[16], value[80];
837
            int base = row * 16;
838
            snprintf(name, sizeof(name), "$%02X-$%02X", base, base + 15);
839
            char *p = value;
840
            for (int col = 0; col < 16; col++) {
841
                uint8_t b = debug_read6502((uint16_t)(base + col), 0, USE_CURRENT_X16_BANK);
842
                p += snprintf(p, sizeof(value) - (p - value), "%02X ", b);
843
            }
844
            add_variable(vars, name, value, NULL);
845
        }
846
847
    } else if (ref == VARREF_STACK) {
848
        // Show stack contents from SP+1 to $FF (what's currently on the stack)
849
        uint8_t sp = (uint8_t)regs.sp;
850
        int depth = 0xFF - sp;
851
        if (depth > 32) depth = 32; // cap display
852
        for (int i = 0; i < depth; i++) {
853
            char name[16], value[16];
854
            uint16_t addr = 0x0100 + sp + 1 + i;
855
            uint8_t b = debug_read6502(addr, 0, USE_CURRENT_X16_BANK);
856
            snprintf(name, sizeof(name), "$%04X", addr);
857
            snprintf(value, sizeof(value), "$%02X (%d)", b, b);
858
            add_variable(vars, name, value, "uint8");
859
        }
860
861
    } else if (ref == VARREF_VREGS) {
862
        // X16 virtual registers R0-R15: 16-bit little-endian at direct-page +
863
        // $02 + 2*n (D is 0 under the KERNAL ABI, non-zero possible in GS native).
864
        for (int n = 0; n < 16; n++) {
865
            char name[8], value[48];
866
            uint16_t a  = dap_dp_add((uint16_t)(0x02 + 2 * n));
867
            uint8_t  lo = debug_read6502(a, 0, USE_CURRENT_X16_BANK);
868
            uint8_t  hi = debug_read6502((uint16_t)(a + 1), 0, USE_CURRENT_X16_BANK);
869
            uint16_t v  = (uint16_t)(lo | (hi << 8));
870
            snprintf(name, sizeof(name), "R%d", n);
871
            snprintf(value, sizeof(value), "$%04X (%d)  @$%04X", v, v, a);
872
            add_variable(vars, name, value, "uint16");
873
        }
874
    }
875
876
    cJSON_AddItemToObject(body, "variables", vars);
877
    send_dap_response(seq, "variables", true, body);
878
    return 0;
879
}
880
881
// Case-insensitive prefix test (pfx must be lowercase).
882
static bool dap_ci_prefix(const char *s, const char *pfx) {
883
    for (; *pfx; s++, pfx++) {
884
        char a = (*s >= 'A' && *s <= 'Z') ? (char)(*s + 32) : *s;
885
        if (a != *pfx) return false;
886
    }
887
    return true;
888
}
889
890
// Parse a single "OPERAND OP VALUE" term: OPERAND is A/X/Y/SP/P, byte[$addr],
891
// word[$addr] or a bare $addr (byte); OP is == != < <= > >= (or =). Fills the
892
// outputs and returns true on success. Codes match debugger.c's bp_cond enums
893
// (A=0..WORD=6; EQ=0..GE=5).
894
static bool dap_parse_value_term(const char *s, int *out_operand, uint16_t *out_oaddr,
895
                                 int *out_op, uint32_t *out_value) {
896
    while (*s == ' ') s++;
897
    int      operand = -1;
898
    uint16_t oaddr   = 0;
899
    #define ISALNUM(c) (((c) >= '0' && (c) <= '9') || ((c) >= 'a' && (c) <= 'z') || ((c) >= 'A' && (c) <= 'Z'))
900
    if (!strncmp(s, "byte[", 5)) { operand = 5; oaddr = (uint16_t)dap_parse_num(s + 5); const char *b = strchr(s, ']'); s = b ? b + 1 : s + strlen(s); }
901
    else if (!strncmp(s, "word[", 5)) { operand = 6; oaddr = (uint16_t)dap_parse_num(s + 5); const char *b = strchr(s, ']'); s = b ? b + 1 : s + strlen(s); }
902
    else if (*s == '$' || (*s == '0' && (s[1] == 'x' || s[1] == 'X'))) { operand = 5; oaddr = (uint16_t)dap_parse_num(s); while (*s && *s != ' ' && *s != '=' && *s != '!' && *s != '<' && *s != '>') s++; }
903
    else if ((s[0] == 'A' || s[0] == 'a') && !ISALNUM(s[1])) { operand = 0; s++; }
904
    else if ((s[0] == 'X' || s[0] == 'x') && !ISALNUM(s[1])) { operand = 1; s++; }
905
    else if ((s[0] == 'Y' || s[0] == 'y') && !ISALNUM(s[1])) { operand = 2; s++; }
906
    else if ((s[0] == 'S' || s[0] == 's') && (s[1] == 'P' || s[1] == 'p')) { operand = 3; s += 2; }
907
    else if ((s[0] == 'P' || s[0] == 'p') && !ISALNUM(s[1])) { operand = 4; s++; }
908
    #undef ISALNUM
909
    if (operand < 0) return false;
910
    while (*s == ' ') s++;
911
    int op = -1;
912
    if (!strncmp(s, "==", 2)) { op = 0; s += 2; }
913
    else if (!strncmp(s, "!=", 2)) { op = 1; s += 2; }
914
    else if (!strncmp(s, "<=", 2)) { op = 3; s += 2; }
915
    else if (!strncmp(s, ">=", 2)) { op = 5; s += 2; }
916
    else if (*s == '=') { op = 0; s++; }
917
    else if (*s == '<') { op = 2; s++; }
918
    else if (*s == '>') { op = 4; s++; }
919
    if (op < 0) return false;
920
    while (*s == ' ') s++;
921
    *out_operand = operand;
922
    *out_oaddr   = oaddr;
923
    *out_op      = op;
924
    *out_value   = dap_parse_num(s);
925
    return true;
926
}
927
928
// If a term is "bank[==]N" or "rombank[==]N" (case-insensitive), return the bank
929
// number; else -1. Used to pin a breakpoint to a specific RAM/ROM bank.
930
static int dap_parse_bank_term(const char *s) {
931
    while (*s == ' ') s++;
932
    if (dap_ci_prefix(s, "rombank")) s += 7;
933
    else if (dap_ci_prefix(s, "bank")) s += 4;
934
    else return -1;
935
    // The keyword must be followed by a separator / number / end — reject longer
936
    // identifiers like "banker" so they don't silently parse as bank 0.
937
    if ((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z')) return -1;
938
    while (*s == ' ' || *s == '=' || *s == '#') s++;
939
    if (!*s) return -1;
940
    return (int)(dap_parse_num(s) & 0xFF);
941
}
942
943
// Parse an optional leading comparison then a number ("== $05", "!=0", "$05",
944
// "5"). Default op is EQ. Returns false if no number is present.
945
static bool dap_parse_simple_cmp(const char *s, int *out_op, uint32_t *out_val) {
946
    while (*s == ' ') s++;
947
    int op = 0; // default EQ
948
    if (!strncmp(s, "==", 2)) { op = 0; s += 2; }
949
    else if (!strncmp(s, "!=", 2)) { op = 1; s += 2; }
950
    else if (!strncmp(s, "<=", 2)) { op = 3; s += 2; }
951
    else if (!strncmp(s, ">=", 2)) { op = 5; s += 2; }
952
    else if (*s == '=') { op = 0; s++; }
953
    else if (*s == '<') { op = 2; s++; }
954
    else if (*s == '>') { op = 4; s++; }
955
    while (*s == ' ') s++;
956
    if (!*s) return false;
957
    *out_op  = op;
958
    *out_val = dap_parse_num(s);
959
    return true;
960
}
961
962
// Parse a DAP source-breakpoint "condition"/"hitCondition" and apply it to the
963
// conditional-breakpoint core for the breakpoint at (addr, bank 0). The condition
964
// may combine a bank pin and a value test with "&&", e.g.
965
//   "bank == 0 && byte[$1234] != 0"  or  "A == $05"  or  "rombank==3".
966
// The value term (A/X/Y/SP/P, byte[$a], word[$a], $a  OP  value; only one is
967
// applied) drives the conditional-BP core; the bank term is returned so the
968
// caller can pin the breakpoint's x16Bank. hitCondition's number N means "stop
969
// on the Nth hit". Returns the bank pin (0..255) or -1 if none was given.
970
static int dap_apply_bp_condition(dbg_addr_t addr, const char *cond_str, const char *hit_str) {
971
    // The condition record is keyed on the same (pc, bank, x16Bank) triple as
972
    // the breakpoint. Applying it at bank 0 while the breakpoint is armed at
973
    // addr >> 16 wrote it under a key nothing looks up, and arrival then built
974
    // a fresh empty record -- so the breakpoint fired unconditionally on its
975
    // first hit.
976
    const uint16_t pc   = (uint16_t)(addr & 0xFFFF);
977
    const uint8_t  bank = (uint8_t)(addr >> 16);
978
    // Work out the bank pin FIRST, across the whole expression, before applying
979
    // anything keyed on it. The terms are applied in the order written, so
980
    // "A == 5 && bank == 3" used to store the value condition against
981
    // x16Bank = ANY while the breakpoint itself was armed for bank 3; arrival
982
    // then found no matching condition record and the breakpoint fired
983
    // unconditionally. Writing the same two terms the other way round worked,
984
    // which is not a distinction the user should be able to feel.
985
    int bank_pin = -1;
986
    char buf[192];
987
    buf[0] = '\0';
988
    if (cond_str && cond_str[0]) {
989
        strncpy(buf, cond_str, sizeof(buf) - 1);
990
        buf[sizeof(buf) - 1] = '\0';
991
        char *scan = buf;
992
        while (scan && *scan) {
993
            char *amp = strstr(scan, "&&");
994
            char saved = '\0';
995
            if (amp) { saved = *amp; *amp = '\0'; }
996
            int b = dap_parse_bank_term(scan);
997
            if (b >= 0) bank_pin = b;
998
            if (amp) { *amp = saved; scan = amp + 2; } else { scan = NULL; }
999
        }
1000
    }
1001
    // Only the banked window has a bank to pin. Applied here rather than by the
1002
    // caller so that the conditions below are keyed the same way the breakpoint
1003
    // will be.
1004
    // Normalised exactly as the core will store it. Testing pc < $A000 alone
1005
    // disagreed on a gen2 machine with a non-zero program bank, where an
1006
    // address above $A000 is not banked either -- so the core stored ANY while
1007
    // we recorded the raw pin, and the two keys stopped matching.
1008
    bank_pin = debug_normalise_bank(bank_pin, pc, bank);
1009
1010
    debug_bp_clear_condition(pc, bank, bank_pin);
1011
    debug_bp_set_ignore(pc, bank, bank_pin, 0);
1012
    // Stated in full every time, deliberately. Removing a breakpoint leaves its
1013
    // condition record behind -- that is how a hit count survives an
1014
    // enable/disable toggle -- so a client re-sending a breakpoint without a
1015
    // condition would otherwise inherit the one it had before. The hit count
1016
    // itself is left alone: an editor re-sends a whole file's breakpoints on
1017
    // every keystroke, and a count that restarted each time could not drive a
1018
    // hit condition at all.
1019
1020
    if (buf[0]) {
1021
        bool value_set = false;
1022
        char *p = buf;
1023
        while (p && *p) {
1024
            char *amp = strstr(p, "&&");
1025
            if (amp) *amp = '\0';                 // terminate this term
1026
            if (dap_parse_bank_term(p) < 0 && !value_set) {
1027
                int operand, op; uint16_t oaddr; uint32_t val;
1028
                if (dap_parse_value_term(p, &operand, &oaddr, &op, &val)) {
1029
                    debug_bp_set_condition(pc, bank, bank_pin, operand, oaddr, op, val);
1030
                    value_set = true;
1031
                }
1032
            }
1033
            p = amp ? amp + 2 : NULL;
1034
        }
1035
    }
1036
1037
    if (hit_str && hit_str[0]) {
1038
        const char *h = hit_str;
1039
        while (*h && !(*h >= '0' && *h <= '9')) h++;
1040
        uint32_t n = (uint32_t)strtoul(h, NULL, 0);
1041
        if (n > 0) debug_bp_set_ignore(pc, bank, bank_pin, n - 1); // stop on the Nth hit
1042
    }
1043
    return bank_pin;
1044
}
1045
1046
static int handle_dap_set_breakpoints(int seq, cJSON *args) {
1047
    if (!args) {
1048
        send_dap_error_response(seq, "setBreakpoints", "missing arguments");
1049
        return 0;
1050
    }
1051
1052
    cJSON *source = cJSON_GetObjectItemCaseSensitive(args, "source");
1053
    cJSON *bps = cJSON_GetObjectItemCaseSensitive(args, "breakpoints");
1054
    if (!source || !bps) {
1055
        send_dap_error_response(seq, "setBreakpoints", "missing source or breakpoints");
1056
        return 0;
1057
    }
1058
1059
    cJSON *path_item = cJSON_GetObjectItemCaseSensitive(source, "path");
1060
    const char *src_path = (path_item && cJSON_IsString(path_item)) ? path_item->valuestring : "";
1061
1062
    // Drop this file's breakpoints, then restate what this owner still wants.
1063
    //
1064
    // Removing them one at a time looks simpler and is wrong. Source files are
1065
    // matched to addresses by basename, so src/main.s:10 and overlay/main.s:10
1066
    // resolve to the SAME core entry -- while this loop matches on the full
1067
    // path. One owner's two claims on an address are one bit, so removing the
1068
    // first would disarm a breakpoint the other file still wants, leaving the
1069
    // client showing a verified breakpoint that no longer stops anything.
1070
    //
1071
    // So: forget every source claim and re-assert the ones that survive. Other
1072
    // owners are untouched throughout, and conditions and hit counts live on
1073
    // the address rather than the claim, so nothing is lost by re-adding.
1074
    for (int i = num_dap_bps - 1; i >= 0; i--) {
1075
        if (strcmp(dap_bps[i].file, src_path) == 0) {
1076
            for (int j = i; j < num_dap_bps - 1; j++) {
1077
                dap_bps[j] = dap_bps[j + 1];
1078
            }
1079
            num_dap_bps--;
1080
        }
1081
    }
1082
1083
    debug_bp_clear_owner(DEBUG_OWNER_DAP_SOURCE);
1084
    for (int i = 0; i < num_dap_bps; i++) {
1085
        // A pending entry holds address 0 and was never armed; re-adding it
1086
        // would arm an unrelated breakpoint at $0000.
1087
        if (!dap_bps[i].verified)
1088
            continue;
1089
        struct breakpoint keep = { dap_bps[i].addr, dap_bps[i].bank, dap_bps[i].x16Bank, 0, false };
1090
        debug_bp_add_for(keep, DEBUG_OWNER_DAP_SOURCE);
1091
    }
1092
1093
    // Set new breakpoints
1094
    cJSON *result_body = cJSON_CreateObject();
1095
    cJSON *result_bps = cJSON_CreateArray();
1096
1097
    int bp_count = cJSON_GetArraySize(bps);
1098
    for (int i = 0; i < bp_count && num_dap_bps < MAX_DAP_BREAKPOINTS; i++) {
1099
        cJSON *bp_item = cJSON_GetArrayItem(bps, i);
1100
        cJSON *line_item = cJSON_GetObjectItemCaseSensitive(bp_item, "line");
1101
        int line = (line_item && cJSON_IsNumber(line_item)) ? (int)line_item->valuedouble : 0;
1102
1103
        cJSON *result_bp = cJSON_CreateObject();
1104
        int bp_id = next_dap_bp_id++;
1105
1106
        // Kept verbatim so the breakpoint can be re-armed exactly as asked for
1107
        // if an overlay reload forces it to be resolved again.
1108
        const char *cond_str = "";
1109
        const char *hit_str  = "";
1110
        {
1111
            cJSON *c = cJSON_GetObjectItemCaseSensitive(bp_item, "condition");
1112
            if (c && cJSON_IsString(c) && c->valuestring) cond_str = c->valuestring;
1113
            cJSON *h = cJSON_GetObjectItemCaseSensitive(bp_item, "hitCondition");
1114
            if (h && cJSON_IsString(h) && h->valuestring) hit_str = h->valuestring;
1115
        }
1116
1117
        // Try to resolve source:line to address
1118
        dbg_addr_t addr = 0;
1119
        bool resolved = dbg_info_source_to_addr(src_path, line, &addr);
1120
1121
        if (resolved) {
1122
            // The condition is parsed first so a "bank==N" term pins x16Bank at
1123
            // add time; the value term and hit count go to the core as well.
1124
            int bank_pin = dap_apply_bp_condition(addr, cond_str, hit_str);
1125
            struct breakpoint hw_bp;
1126
            hw_bp.pc = (int)(addr & 0xFFFF);
1127
            hw_bp.bank = (uint8_t)(addr >> 16);
1128
            hw_bp.x16Bank = bank_pin;
1129
            hw_bp.owners = 0;
1130
            hw_bp.enabled = false;
1131
            const debug_add_result_t added = debug_bp_add_for(hw_bp, DEBUG_OWNER_DAP_SOURCE);
1132
1133
            if (added == DEBUG_ADD_FULL) {
1134
                // Nothing was armed, so say so rather than reporting a verified
1135
                // breakpoint the machine will never stop on.
1136
                cJSON_AddNumberToObject(result_bp, "id", bp_id);
1137
                cJSON_AddBoolToObject(result_bp, "verified", false);
1138
                cJSON_AddStringToObject(result_bp, "message",
1139
                                        "Out of memory; breakpoint not armed");
1140
                cJSON_AddNumberToObject(result_bp, "line", line);
1141
                cJSON *bp_source = cJSON_CreateObject();
1142
                cJSON_AddStringToObject(bp_source, "path", src_path);
1143
                cJSON_AddItemToObject(result_bp, "source", bp_source);
1144
                cJSON_AddItemToArray(result_bps, result_bp);
1145
                continue;
1146
            }
1147
1148
            dap_bps[num_dap_bps].dap_id = bp_id;
1149
            dap_bps[num_dap_bps].addr = (uint16_t)(addr & 0xFFFF);
1150
            dap_bps[num_dap_bps].bank = (uint8_t)(addr >> 16);
1151
            dap_bps[num_dap_bps].x16Bank = bank_pin;
1152
            strncpy(dap_bps[num_dap_bps].file, src_path, sizeof(dap_bps[num_dap_bps].file) - 1);
1153
            dap_bps[num_dap_bps].line = line;
1154
            dap_bps[num_dap_bps].verified = true;
1155
            strncpy(dap_bps[num_dap_bps].cond, cond_str, sizeof(dap_bps[num_dap_bps].cond) - 1);
1156
            dap_bps[num_dap_bps].cond[sizeof(dap_bps[num_dap_bps].cond) - 1] = '\0';
1157
            strncpy(dap_bps[num_dap_bps].hit_cond, hit_str, sizeof(dap_bps[num_dap_bps].hit_cond) - 1);
1158
            dap_bps[num_dap_bps].hit_cond[sizeof(dap_bps[num_dap_bps].hit_cond) - 1] = '\0';
1159
            num_dap_bps++;
1160
1161
1162
            cJSON_AddNumberToObject(result_bp, "id", bp_id);
1163
            cJSON_AddBoolToObject(result_bp, "verified", true);
1164
            cJSON_AddNumberToObject(result_bp, "line", line);
1165
1166
            // Add source info
1167
            cJSON *bp_source = cJSON_CreateObject();
1168
            cJSON_AddStringToObject(bp_source, "path", src_path);
1169
            cJSON_AddItemToObject(result_bp, "source", bp_source);
1170
        } else {
1171
            // Nothing describes this line yet -- typically the guest has not
1172
            // loaded the program. Say so rather than claiming it is verified:
1173
            // retry_unverified_breakpoints() arms it when the .dbg arrives and
1174
            // sends a verified event then, so the client's view catches up on
1175
            // its own. Reporting it verified here made an unarmable breakpoint
1176
            // look identical to a working one.
1177
            cJSON_AddNumberToObject(result_bp, "id", bp_id);
1178
            cJSON_AddBoolToObject(result_bp, "verified", false);
1179
            cJSON_AddStringToObject(result_bp, "message",
1180
                                    "No debug info for this line yet; will arm when it loads");
1181
            cJSON_AddNumberToObject(result_bp, "line", line);
1182
            cJSON *bp_source = cJSON_CreateObject();
1183
            cJSON_AddStringToObject(bp_source, "path", src_path);
1184
            cJSON_AddItemToObject(result_bp, "source", bp_source);
1185
1186
            // Store as pending — will set hardware BP when matching .dbg loads
1187
            dap_bps[num_dap_bps].dap_id = bp_id;
1188
            dap_bps[num_dap_bps].addr = 0;
1189
            dap_bps[num_dap_bps].bank = 0;
1190
            dap_bps[num_dap_bps].x16Bank = DEBUG_BANK_ANY;
1191
            strncpy(dap_bps[num_dap_bps].file, src_path, sizeof(dap_bps[num_dap_bps].file) - 1);
1192
            dap_bps[num_dap_bps].line = line;
1193
            dap_bps[num_dap_bps].verified = false;
1194
            strncpy(dap_bps[num_dap_bps].cond, cond_str, sizeof(dap_bps[num_dap_bps].cond) - 1);
1195
            dap_bps[num_dap_bps].cond[sizeof(dap_bps[num_dap_bps].cond) - 1] = '\0';
1196
            strncpy(dap_bps[num_dap_bps].hit_cond, hit_str, sizeof(dap_bps[num_dap_bps].hit_cond) - 1);
1197
            dap_bps[num_dap_bps].hit_cond[sizeof(dap_bps[num_dap_bps].hit_cond) - 1] = '\0';
1198
            num_dap_bps++;
1199
        }
1200
        cJSON_AddItemToArray(result_bps, result_bp);
1201
    }
1202
1203
    cJSON_AddItemToObject(result_body, "breakpoints", result_bps);
1204
    send_dap_response(seq, "setBreakpoints", true, result_body);
1205
    return 0;
1206
}
1207
1208
static int handle_dap_continue(int seq, cJSON *args) {
1209
    (void)args;
1210
    // Cleared first so the resume hook stays quiet: this request sends its own
1211
    // continued event after the response, and the hook exists for resumes the
1212
    // client did not ask for.
1213
    dap_stop_announced = false;
1214
    DEBUGContinue();
1215
    cJSON *body = cJSON_CreateObject();
1216
    cJSON_AddBoolToObject(body, "allThreadsContinued", true);
1217
    send_dap_response(seq, "continue", true, body);
1218
    send_continued_event();
1219
    return 1;
1220
}
1221
1222
// Stepping is delegated to the debugger's own execution control rather than
1223
// reimplemented here. Its step-over knows the call opcodes of every CPU variant
1224
// the emulator supports -- the version this replaces recognised only JSR $20,
1225
// so on a 65816 it stepped INTO a JSL -- and its step-out finds the return
1226
// address by unwinding rather than assuming it is at SP+1, which is wrong the
1227
// moment the routine has pushed anything. It also parks its breakpoint in
1228
// `stepBreakPoint`, separate from the user's table, so stepping can no longer
1229
// collide with a breakpoint the user set at the same address. That target
1230
// carries its owner, so a session teardown can retract a step it asked for
1231
// without touching one the user started at the keyboard.
1232
static int handle_dap_next(int seq, cJSON *args) {
1233
    (void)args;
1234
    dap_stop_announced = false;   // this request implies execution continues
1235
    DEBUGStepOverAuto(DEBUG_OWNER_DAP_SOURCE);
1236
    send_dap_response(seq, "next", true, NULL);
1237
    return 1;
1238
}
1239
1240
static int handle_dap_step_in(int seq, cJSON *args) {
1241
    (void)args;
1242
    dap_stop_announced = false;   // this request implies execution continues
1243
    DEBUGStepIntoAuto();
1244
    send_dap_response(seq, "stepIn", true, NULL);
1245
    return 1;
1246
}
1247
1248
static int handle_dap_step_out(int seq, cJSON *args) {
1249
    (void)args;
1250
    dap_stop_announced = false;   // this request implies execution continues
1251
    DEBUGStepOut(DEBUG_OWNER_DAP_SOURCE);
1252
    send_dap_response(seq, "stepOut", true, NULL);
1253
    return 1;
1254
}
1255
1256
static int handle_dap_pause(int seq, cJSON *args) {
1257
    (void)args;
1258
    const bool wasRunning = (currentMode != DMODE_STOP);
1259
    // Emits the stopped event itself on the RUN->STOP transition.
1260
    DEBUGBreakToDebugger();
1261
    send_dap_response(seq, "pause", true, NULL);
1262
    // Already halted -- a previous client left it that way, or a breakpoint got
1263
    // there first. There is no transition for DEBUGBreakToDebugger() to report,
1264
    // and the protocol still owes this request a stopped event; without one the
1265
    // client waits forever for a pause that has in fact already happened.
1266
    if (!wasRunning && !dap_stop_announced) {
1267
        debug_server_notify_stopped("pause");
1268
    }
1269
    return 1;
1270
}
1271
1272
static int handle_dap_disconnect(int seq, cJSON *args) {
1273
    // Default to terminating since the emulator is always launched for debugging
1274
    bool terminate = true;
1275
    if (args) {
1276
        cJSON *term_item = cJSON_GetObjectItemCaseSensitive(args, "terminateDebuggee");
1277
        if (term_item && cJSON_IsBool(term_item)) {
1278
            terminate = cJSON_IsTrue(term_item);
1279
        }
1280
    }
1281
    send_dap_response(seq, "disconnect", true, NULL);
1282
1283
    if (terminate) {
1284
        SDL_Event quit_event;
1285
        quit_event.type = SDL_QUIT;
1286
        SDL_PushEvent(&quit_event);
1287
    } else {
1288
        // Keep emulator running — prevent disconnect_client from killing it
1289
        dap_session_active = false;
1290
        // Release what the session installed BEFORE resuming. Resuming first
1291
        // meant the machine ran on with this client's breakpoints still armed
1292
        // until the socket actually closed, and stopping at one of them then
1293
        // left it halted with the session already marked inactive -- so the
1294
        // socket cleanup would not resume it either.
1295
        dap_release_session_state();
1296
        // Resume only when there is no interactive debugger UI to drive the run
1297
        // state; otherwise leave it as the user left it (see disconnect_client).
1298
        if (currentMode == DMODE_STOP && !dap_local_ui_owns_run_state()) {
1299
            currentMode = DMODE_RUN;
1300
        }
1301
    }
1302
    return 0;
1303
}
1304
1305
static int handle_dap_restart(int seq, cJSON *args) {
1306
    (void)args;
1307
    machine_reset();
1308
    send_dap_response(seq, "restart", true, NULL);
1309
    return 1;
1310
}
1311
1312
// ─── Run to Cursor / Set Next Statement ────────────────────────────
1313
1314
static int handle_dap_goto_targets(int seq, cJSON *args) {
1315
    // Returns possible goto targets for a source location
1316
    if (!args) {
1317
        send_dap_error_response(seq, "gotoTargets", "missing arguments");
1318
        return 0;
1319
    }
1320
1321
    cJSON *source = cJSON_GetObjectItemCaseSensitive(args, "source");
1322
    cJSON *line_item = cJSON_GetObjectItemCaseSensitive(args, "line");
1323
    if (!source || !line_item) {
1324
        send_dap_error_response(seq, "gotoTargets", "missing source or line");
1325
        return 0;
1326
    }
1327
1328
    cJSON *path = cJSON_GetObjectItemCaseSensitive(source, "path");
1329
    int line = line_item->valueint;
1330
1331
    // Look up the address for this source line using debug info
1332
    dbg_addr_t addr = 0;
1333
    bool found = false;
1334
    if (path && cJSON_IsString(path)) {
1335
        found = dbg_info_source_to_addr(path->valuestring, line, &addr);
1336
    }
1337
1338
    cJSON *body = cJSON_CreateObject();
1339
    cJSON *targets = cJSON_CreateArray();
1340
1341
    if (found) {
1342
        cJSON *target = cJSON_CreateObject();
1343
        cJSON_AddNumberToObject(target, "id", (int)addr);
1344
        char label[32];
1345
        snprintf(label, sizeof(label), "$%04X", addr);
1346
        cJSON_AddStringToObject(target, "label", label);
1347
        cJSON_AddNumberToObject(target, "line", line);
1348
        cJSON_AddItemToArray(targets, target);
1349
    }
1350
1351
    cJSON_AddItemToObject(body, "targets", targets);
1352
    send_dap_response(seq, "gotoTargets", true, body);
1353
    return 0;
1354
}
1355
1356
static int handle_dap_goto(int seq, cJSON *args) {
1357
    // Set Next Statement: move PC to target address
1358
    if (!args) {
1359
        send_dap_error_response(seq, "goto", "missing arguments");
1360
        return 0;
1361
    }
1362
1363
    cJSON *target_id = cJSON_GetObjectItemCaseSensitive(args, "targetId");
1364
    if (!target_id || !cJSON_IsNumber(target_id)) {
1365
        send_dap_error_response(seq, "goto", "missing targetId");
1366
        return 0;
1367
    }
1368
1369
    uint16_t addr = (uint16_t)target_id->valueint;
1370
    regs.pc = addr;
1371
1372
    send_dap_response(seq, "goto", true, NULL);
1373
1374
    // Send stopped event at new location
1375
    cJSON *body = cJSON_CreateObject();
1376
    cJSON_AddStringToObject(body, "reason", "goto");
1377
    cJSON_AddNumberToObject(body, "threadId", 1);
1378
    cJSON_AddBoolToObject(body, "allThreadsStopped", true);
1379
    send_dap_event("stopped", body);
1380
1381
    return 0;
1382
}
1383
1384
static int handle_dap_evaluate(int seq, cJSON *args) {
1385
    if (!args) {
1386
        send_dap_error_response(seq, "evaluate", "missing arguments");
1387
        return 0;
1388
    }
1389
    cJSON *expr_item = cJSON_GetObjectItemCaseSensitive(args, "expression");
1390
    if (!expr_item || !cJSON_IsString(expr_item)) {
1391
        send_dap_error_response(seq, "evaluate", "missing expression");
1392
        return 0;
1393
    }
1394
    const char *expr = expr_item->valuestring;
1395
1396
    // Parse expression: $XXXX = memory peek, register names, bp_* commands
1397
    char result[1024] = "???";
1398
1399
    // Breakpoint management via evaluate
1400
    if (strncmp(expr, "bp_add ", 7) == 0) {
1401
        uint16_t addr = (uint16_t)strtoul(expr + 7, NULL, 16);
1402
        struct breakpoint bp;
1403
        bp.pc = addr;
1404
        bp.bank = 0;
1405
        bp.x16Bank = -1;
1406
        bp.owners = 0;
1407
        bp.enabled = false;
1408
        // Owned by the console, so it goes away with the session even when the
1409
        // table below is full and nothing here remembers it. An untracked one
1410
        // used to outlive the client that asked for it and could halt a
1411
        // headless machine with nobody able to resume it.
1412
        const debug_add_result_t added = debug_bp_add_for(bp, DEBUG_OWNER_DAP_CONSOLE);
1413
        if (added == DEBUG_ADD_FULL) {
1414
            snprintf(result, sizeof(result), "out of memory; no breakpoint at $%04X", addr);
1415
        } else {
1416
            // Also recorded here, when there is room, so the breakpoint has an
1417
            // id to report hits against.
1418
            if (num_dap_bps < MAX_DAP_BREAKPOINTS) {
1419
                dap_bps[num_dap_bps].dap_id  = next_dap_bp_id++;
1420
                dap_bps[num_dap_bps].addr    = addr;
1421
                dap_bps[num_dap_bps].bank    = 0;
1422
                dap_bps[num_dap_bps].x16Bank = -1;
1423
                dap_bps[num_dap_bps].line    = 0;
1424
                dap_bps[num_dap_bps].file[0] = '\0';
1425
                dap_bps[num_dap_bps].cond[0] = '\0';
1426
                dap_bps[num_dap_bps].hit_cond[0] = '\0';
1427
                dap_bps[num_dap_bps].verified = true;
1428
                num_dap_bps++;
1429
            }
1430
            snprintf(result, sizeof(result), "breakpoint added at $%04X", addr);
1431
        }
1432
    } else if (strncmp(expr, "bp_remove ", 10) == 0) {
1433
        uint16_t addr = (uint16_t)strtoul(expr + 10, NULL, 16);
1434
        for (int i = num_dap_bps - 1; i >= 0; i--) {
1435
            if (dap_bps[i].addr == addr && dap_bps[i].bank == 0) {
1436
                for (int j = i; j < num_dap_bps - 1; j++) dap_bps[j] = dap_bps[j + 1];
1437
                num_dap_bps--;
1438
            }
1439
        }
1440
        // Typed by a person, so it takes the breakpoint away whoever asked for
1441
        // it -- the same rule as F9 in the debug window. Being told "removed"
1442
        // and finding it still armed because something else also wanted it is
1443
        // not an answer anyone can act on.
1444
        bool gone = debug_bp_delete(addr, 0, DEBUG_BANK_ANY);
1445
        if (gone)
1446
            snprintf(result, sizeof(result), "breakpoint removed at $%04X", addr);
1447
        else
1448
            snprintf(result, sizeof(result), "no breakpoint at $%04X", addr);
1449
    } else if (strcmp(expr, "bp_list") == 0) {
1450
        int len = 0;
1451
        const int n = debug_bp_count();
1452
        len += snprintf(result + len, sizeof(result) - len, "%d breakpoints:", n);
1453
        for (int i = 0; i < n && len < (int)sizeof(result) - 12; i++) {
1454
            const struct breakpoint *b = debug_bp_at(i);
1455
            len += snprintf(result + len, sizeof(result) - len, " $%04X%s",
1456
                            (unsigned)b->pc, b->enabled ? "" : "(off)");
1457
        }
1458
    } else if (strcmp(expr, "bp_clear") == 0) {
1459
        // Typed by a person, and "clear all breakpoints" means all of them --
1460
        // including any set with -bp or F9. The core's own clear takes the
1461
        // condition and hit-count records with them; zeroing numBreakpoints
1462
        // behind its back left those behind, and a breakpoint later re-created
1463
        // at the same address inherited a stale hit count -- an "ignore 5" that
1464
        // had already been reached fired on the second arrival, not the sixth.
1465
        debug_bp_clear_all();
1466
        num_dap_bps   = 0;
1467
        num_func_bps  = 0;
1468
        num_instr_bps = 0;
1469
        snprintf(result, sizeof(result), "all breakpoints cleared");
1470
    } else if (strcmp(expr, "reset") == 0) {
1471
        machine_reset();
1472
        snprintf(result, sizeof(result), "CPU reset");
1473
    } else if (expr[0] == '$' || expr[0] == '0') {
1474
        // Memory address (24-bit: high byte is the 65C816 bank byte).
1475
        uint32_t addr;
1476
        if (expr[0] == '$') {
1477
            addr = (uint32_t)strtoul(expr + 1, NULL, 16);
1478
        } else {
1479
            addr = (uint32_t)strtoul(expr, NULL, 16);
1480
        }
1481
        addr &= 0xFFFFFF;
1482
        uint8_t val = debug_read6502((uint16_t)(addr & 0xFFFF), (uint8_t)(addr >> 16), USE_CURRENT_X16_BANK);
1483
        snprintf(result, sizeof(result), "$%02X (%d)", val, val);
1484
    } else if (!strcmp(expr, "A") || !strcmp(expr, "a")) {
1485
        if (regs.is65c816 && !regs.e && !(regs.status & FLAG_MEMORY_WIDTH))
1486
            snprintf(result, sizeof(result), "$%04X (%d)", regs.c, regs.c);
1487
        else
1488
            snprintf(result, sizeof(result), "$%02X (%d)", regs.a, regs.a);
1489
    } else if (!strcmp(expr, "X") || !strcmp(expr, "x")) {
1490
        if (regs.is65c816 && !regs.e && !(regs.status & FLAG_INDEX_WIDTH))
1491
            snprintf(result, sizeof(result), "$%04X (%d)", regs.x, regs.x);
1492
        else
1493
            snprintf(result, sizeof(result), "$%02X (%d)", (uint8_t)regs.x, (uint8_t)regs.x);
1494
    } else if (!strcmp(expr, "Y") || !strcmp(expr, "y")) {
1495
        if (regs.is65c816 && !regs.e && !(regs.status & FLAG_INDEX_WIDTH))
1496
            snprintf(result, sizeof(result), "$%04X (%d)", regs.y, regs.y);
1497
        else
1498
            snprintf(result, sizeof(result), "$%02X (%d)", (uint8_t)regs.y, (uint8_t)regs.y);
1499
    } else if (!strcmp(expr, "SP") || !strcmp(expr, "sp")) {
1500
        snprintf(result, sizeof(result), "$%02X (%d)", (uint8_t)regs.sp, (uint8_t)regs.sp);
1501
    } else if (!strcmp(expr, "PC") || !strcmp(expr, "pc")) {
1502
        if (is_gen2 || (regs.is65c816 && regs.k != 0))
1503
            snprintf(result, sizeof(result), "$%02X:%04X", regs.k, regs.pc);
1504
        else
1505
            snprintf(result, sizeof(result), "$%04X (%d)", regs.pc, regs.pc);
1506
    } else if (!strcmp(expr, "P") || !strcmp(expr, "p")) {
1507
        snprintf(result, sizeof(result), "$%02X", regs.status);
1508
    } else if (!strcmp(expr, "regs_all")) {
1509
        snprintf(result, sizeof(result), "PC=$%04X A=$%02X X=$%02X Y=$%02X SP=$%02X P=$%02X",
1510
                 regs.pc, regs.a, (uint8_t)regs.x, (uint8_t)regs.y, (uint8_t)regs.sp, regs.status);
1511
    } else if (strncmp(expr, "watch_add ", 10) == 0) {
1512
        // watch_add ADDR [LEN]  — add memory write watchpoint
1513
        char *endp;
1514
        uint16_t addr = (uint16_t)strtoul(expr + 10, &endp, 16);
1515
        uint16_t len = 1;
1516
        if (*endp == ' ') len = (uint16_t)strtoul(endp + 1, NULL, 16);
1517
        if (len == 0) len = 1;
1518
        // Owned by the console, so it goes away with the session. An untracked
1519
        // one outlived the client that made it and could halt a headless
1520
        // machine with nobody able to resume it.
1521
        const debug_add_result_t added =
1522
            debug_wp_add_for(addr, len, DEBUG_BANK_ANY, DEBUG_OWNER_DAP_CONSOLE);
1523
        if (added == DEBUG_ADD_FULL)
1524
            snprintf(result, sizeof(result), "watchpoint table full (max %d)", MAX_WATCHPOINTS);
1525
        else
1526
            snprintf(result, sizeof(result), "watchpoint added at $%04X len %d", addr, len);
1527
    } else if (strncmp(expr, "watch_remove ", 13) == 0) {
1528
        uint16_t addr = (uint16_t)strtoul(expr + 13, NULL, 16);
1529
        // Typed by a person, so it takes the watch away whoever asked for it.
1530
        if (debug_wp_delete(addr, DEBUG_BANK_ANY))
1531
            snprintf(result, sizeof(result), "watchpoint removed at $%04X", addr);
1532
        else
1533
            snprintf(result, sizeof(result), "no watchpoint at $%04X", addr);
1534
    } else if (!strcmp(expr, "watch_list")) {
1535
        int len = 0;
1536
        int n   = debug_wp_count();
1537
        len += snprintf(result + len, sizeof(result) - len, "%d watchpoints:", n);
1538
        for (int i = 0; i < n && len < (int)sizeof(result) - 20; i++) {
1539
            const struct watchpoint *wp = debug_wp_at(i);
1540
            if (!wp) break;
1541
            len += snprintf(result + len, sizeof(result) - len, " $%04X(%d)",
1542
                           wp->addr, wp->len);
1543
        }
1544
    } else if (!strcmp(expr, "watch_clear")) {
1545
        debug_wp_clear_all();   // the console command clears everyone's, ours included
1546
        snprintf(result, sizeof(result), "all watchpoints cleared");
1547
    } else if (strncmp(expr, "vram ", 5) == 0) {
1548
        // vram ADDR [COUNT] — read VERA VRAM bytes (17-bit address)
1549
        char *endp;
1550
        uint32_t addr = (uint32_t)strtoul(expr + 5, &endp, 16);
1551
        int count = 1;
1552
        if (*endp == ' ') count = (int)strtoul(endp + 1, NULL, 10);
1553
        if (count < 1) count = 1;
1554
        if (count > 128) count = 128;
1555
        int len = 0;
1556
        len += snprintf(result + len, sizeof(result) - len, "$%05X:", addr);
1557
        for (int i = 0; i < count && len < (int)sizeof(result) - 5; i++) {
1558
            uint8_t val = video_space_read((addr + i) & 0x1FFFF);
1559
            len += snprintf(result + len, sizeof(result) - len, " %02X", val);
1560
        }
1561
    } else if (strncmp(expr, "vera_reg", 8) == 0) {
1562
        // vera_reg — dump all VERA registers ($9F20-$9F3F)
1563
        int len = 0;
1564
        for (int i = 0; i < 32 && len < (int)sizeof(result) - 10; i++) {
1565
            uint8_t val = vera_read_dbg((uint8_t)i);
1566
            len += snprintf(result + len, sizeof(result) - len, "%02X ", val);
1567
        }
1568
    }
1569
1570
    cJSON *body = cJSON_CreateObject();
1571
    cJSON_AddStringToObject(body, "result", result);
1572
    cJSON_AddNumberToObject(body, "variablesReference", 0);
1573
    send_dap_response(seq, "evaluate", true, body);
1574
    return 0;
1575
}
1576
1577
static int handle_dap_read_memory(int seq, cJSON *args) {
1578
    if (!args) {
1579
        send_dap_error_response(seq, "readMemory", "missing arguments");
1580
        return 0;
1581
    }
1582
    cJSON *ref_item = cJSON_GetObjectItemCaseSensitive(args, "memoryReference");
1583
    cJSON *count_item = cJSON_GetObjectItemCaseSensitive(args, "count");
1584
    cJSON *offset_item = cJSON_GetObjectItemCaseSensitive(args, "offset");
1585
1586
    if (!ref_item || !cJSON_IsString(ref_item) || !count_item || !cJSON_IsNumber(count_item)) {
1587
        send_dap_error_response(seq, "readMemory", "missing memoryReference or count");
1588
        return 0;
1589
    }
1590
1591
    uint32_t addr = 0;
1592
    bool is_vram = false;
1593
    if (!dap_parse_mem_ref(ref_item->valuestring, &is_vram, &addr)) {
1594
        send_dap_error_response(seq, "readMemory", "unparseable memoryReference (expected a CPU address like 0xC000, or vram:1B000)");
1595
        return 0;
1596
    }
1597
    int offset = (offset_item && cJSON_IsNumber(offset_item)) ? (int)offset_item->valuedouble : 0;
1598
    int count = (int)count_item->valuedouble;
1599
    const uint32_t addr_mask = is_vram ? 0x1FFFF : 0xFFFFFF;
1600
    addr = (uint32_t)((int)addr + offset) & addr_mask;
1601
    if (count > 4096) count = 4096;
1602
    if (count < 0) count = 0;
1603
1604
    // Build base64 data (DAP readMemory uses base64)
1605
    // For simplicity, send as hex-encoded data with unreadableBytes=0
1606
    static const char b64chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1607
    unsigned char *raw = (unsigned char *)malloc(count);
1608
    char *b64 = (char *)malloc((count * 4 / 3) + 8);
1609
    if (!raw || !b64) {
1610
        free(raw); free(b64);
1611
        send_dap_error_response(seq, "readMemory", "memory allocation failed");
1612
        return 0;
1613
    }
1614
1615
    // 24-bit linear read: the high byte of the address is the 65C816 bank byte.
1616
    // For bank 0 this is the classic X16 map (I/O + windowed banks following the
1617
    // live mapping); banks 1-255 are flat RAM in gen2/GS. Spans bank boundaries.
1618
    // VRAM is a separate 17-bit space behind VERA, so it wraps within itself.
1619
    for (int i = 0; i < count; i++) {
1620
        uint32_t cur = (addr + (uint32_t)i) & addr_mask;
1621
        raw[i] = is_vram ? video_space_read(cur)
1622
                         : debug_read6502((uint16_t)(cur & 0xFFFF), (uint8_t)(cur >> 16), USE_CURRENT_X16_BANK);
1623
    }
1624
1625
    // Base64 encode
1626
    int b64_len = 0;
1627
    for (int i = 0; i < count; i += 3) {
1628
        int remaining = count - i;
1629
        unsigned int triplet = raw[i] << 16;
1630
        if (remaining > 1) triplet |= raw[i + 1] << 8;
1631
        if (remaining > 2) triplet |= raw[i + 2];
1632
1633
        b64[b64_len++] = b64chars[(triplet >> 18) & 0x3F];
1634
        b64[b64_len++] = b64chars[(triplet >> 12) & 0x3F];
1635
        b64[b64_len++] = (remaining > 1) ? b64chars[(triplet >> 6) & 0x3F] : '=';
1636
        b64[b64_len++] = (remaining > 2) ? b64chars[triplet & 0x3F] : '=';
1637
    }
1638
    b64[b64_len] = '\0';
1639
1640
    cJSON *body = cJSON_CreateObject();
1641
    char addr_str[16];
1642
    // Echoed in the space it was read from, so a client can page through VRAM by
1643
    // feeding this straight back with an offset.
1644
    if (is_vram)
1645
        snprintf(addr_str, sizeof(addr_str), "vram:0x%05X", addr & 0x1FFFF);
1646
    else if (is_gen2)
1647
        snprintf(addr_str, sizeof(addr_str), "0x%06X", addr & 0xFFFFFF);
1648
    else
1649
        snprintf(addr_str, sizeof(addr_str), "0x%04X", addr & 0xFFFF);
1650
    cJSON_AddStringToObject(body, "address", addr_str);
1651
    cJSON_AddNumberToObject(body, "unreadableBytes", 0);
1652
    cJSON_AddStringToObject(body, "data", b64);
1653
1654
    free(raw);
1655
    free(b64);
1656
    send_dap_response(seq, "readMemory", true, body);
1657
    return 0;
1658
}
1659
1660
// Decode a single base64 character to its 6-bit value, or -1 if not a data char.
1661
static int dap_b64_decode_char(char ch) {
1662
    if (ch >= 'A' && ch <= 'Z') return ch - 'A';
1663
    if (ch >= 'a' && ch <= 'z') return ch - 'a' + 26;
1664
    if (ch >= '0' && ch <= '9') return ch - '0' + 52;
1665
    if (ch == '+') return 62;
1666
    if (ch == '/') return 63;
1667
    return -1; // '=', whitespace, or anything else
1668
}
1669
1670
static int handle_dap_write_memory(int seq, cJSON *args) {
1671
    if (!args) {
1672
        send_dap_error_response(seq, "writeMemory", "missing arguments");
1673
        return 0;
1674
    }
1675
    cJSON *ref_item = cJSON_GetObjectItemCaseSensitive(args, "memoryReference");
1676
    cJSON *data_item = cJSON_GetObjectItemCaseSensitive(args, "data");
1677
    cJSON *offset_item = cJSON_GetObjectItemCaseSensitive(args, "offset");
1678
1679
    if (!ref_item || !cJSON_IsString(ref_item) || !data_item || !cJSON_IsString(data_item)) {
1680
        send_dap_error_response(seq, "writeMemory", "missing memoryReference or data");
1681
        return 0;
1682
    }
1683
1684
    uint32_t addr = 0;
1685
    bool is_vram = false;
1686
    if (!dap_parse_mem_ref(ref_item->valuestring, &is_vram, &addr)) {
1687
        send_dap_error_response(seq, "writeMemory", "unparseable memoryReference (expected a CPU address like 0xC000, or vram:1B000)");
1688
        return 0;
1689
    }
1690
    int offset = (offset_item && cJSON_IsNumber(offset_item)) ? (int)offset_item->valuedouble : 0;
1691
    const uint32_t addr_mask = is_vram ? 0x1FFFF : 0xFFFFFF;
1692
    addr = (uint32_t)((int)addr + offset) & addr_mask;
1693
1694
    const char *b64 = data_item->valuestring;
1695
    int b64_len = (int)strlen(b64);
1696
    unsigned char *raw = (unsigned char *)malloc((b64_len / 4) * 3 + 4);
1697
    if (!raw) {
1698
        send_dap_error_response(seq, "writeMemory", "memory allocation failed");
1699
        return 0;
1700
    }
1701
1702
    // Base64 decode
1703
    int out = 0;
1704
    int acc = 0, bits = -8;
1705
    for (int i = 0; i < b64_len; i++) {
1706
        int d = dap_b64_decode_char(b64[i]);
1707
        if (d < 0) continue; // skip padding/whitespace
1708
        acc = (acc << 6) | d;
1709
        bits += 6;
1710
        if (bits >= 0) {
1711
            raw[out++] = (unsigned char)((acc >> bits) & 0xFF);
1712
            bits -= 8;
1713
        }
1714
    }
1715
1716
    // Write into emulated RAM using 24-bit linear addressing: the high byte of
1717
    // the address is the 65C816 bank byte (bank 0 = classic X16 map incl. the
1718
    // windowed banks + I/O side effects; banks 1-255 = flat RAM in gen2/GS).
1719
    // A vram: reference goes straight into VERA's 17-bit space instead, which is
1720
    // the only way to write it -- the CPU reaches VRAM only through the data
1721
    // port, one auto-incrementing byte at a time.
1722
    for (int i = 0; i < out; i++) {
1723
        uint32_t cur = (addr + (uint32_t)i) & addr_mask;
1724
        if (is_vram) {
1725
            video_space_write(cur, raw[i]);
1726
        } else {
1727
            debug_write6502((uint16_t)(cur & 0xFFFF), (uint8_t)(cur >> 16), raw[i]);
1728
        }
1729
    }
1730
1731
    free(raw);
1732
1733
    cJSON *body = cJSON_CreateObject();
1734
    cJSON_AddNumberToObject(body, "bytesWritten", out);
1735
    cJSON_AddNumberToObject(body, "offset", 0);
1736
    send_dap_response(seq, "writeMemory", true, body);
1737
    return 0;
1738
}
1739
1740
// ─── Custom: x16/sendKey — inject a key press for automated testing ──
1741
//
1742
// Arguments:
1743
//   key:       SDL key name (e.g. "Return", "A", "Space", "Left", "F1") or a
1744
//              numeric SDL scancode.
1745
//   action:    "press" (default) sends down+up, "down" holds, "up" releases.
1746
//   modifiers: optional array of key names/scancodes held around the key
1747
//              (e.g. ["Left Shift"]). Applied as down before / up after.
1748
static SDL_Scancode dap_resolve_scancode(cJSON *item) {
1749
    if (!item) return SDL_SCANCODE_UNKNOWN;
1750
    if (cJSON_IsNumber(item)) {
1751
        int sc = (int)item->valuedouble;
1752
        if (sc < 0 || sc >= SDL_NUM_SCANCODES) return SDL_SCANCODE_UNKNOWN;
1753
        return (SDL_Scancode)sc;
1754
    }
1755
    if (cJSON_IsString(item) && item->valuestring && item->valuestring[0]) {
1756
        return SDL_GetScancodeFromName(item->valuestring);
1757
    }
1758
    return SDL_SCANCODE_UNKNOWN;
1759
}
1760
1761
static void dap_key_event(SDL_Scancode sc, bool down) {
1762
    handle_keyboard(down, SDL_GetKeyFromScancode(sc), sc);
1763
}
1764
1765
static int handle_x16_send_key(int seq, cJSON *args) {
1766
    if (!args) {
1767
        send_dap_error_response(seq, "x16/sendKey", "missing arguments");
1768
        return 0;
1769
    }
1770
    SDL_Scancode sc = dap_resolve_scancode(cJSON_GetObjectItemCaseSensitive(args, "key"));
1771
    if (sc == SDL_SCANCODE_UNKNOWN) {
1772
        send_dap_error_response(seq, "x16/sendKey", "unknown or missing 'key'");
1773
        return 0;
1774
    }
1775
1776
    const char *action = "press";
1777
    cJSON *action_item = cJSON_GetObjectItemCaseSensitive(args, "action");
1778
    if (action_item && cJSON_IsString(action_item)) action = action_item->valuestring;
1779
1780
    // Resolve modifiers (held down around the key for press/down).
1781
    SDL_Scancode mods[8];
1782
    int num_mods = 0;
1783
    cJSON *mod_arr = cJSON_GetObjectItemCaseSensitive(args, "modifiers");
1784
    if (mod_arr && cJSON_IsArray(mod_arr)) {
1785
        cJSON *m;
1786
        cJSON_ArrayForEach(m, mod_arr) {
1787
            if (num_mods >= 8) break;
1788
            SDL_Scancode msc = dap_resolve_scancode(m);
1789
            if (msc != SDL_SCANCODE_UNKNOWN) mods[num_mods++] = msc;
1790
        }
1791
    }
1792
1793
    bool do_down = (!strcmp(action, "press") || !strcmp(action, "down"));
1794
    bool do_up   = (!strcmp(action, "press") || !strcmp(action, "up"));
1795
1796
    if (do_down) {
1797
        for (int i = 0; i < num_mods; i++) dap_key_event(mods[i], true);
1798
        dap_key_event(sc, true);
1799
    }
1800
    if (do_up) {
1801
        dap_key_event(sc, false);
1802
        for (int i = num_mods - 1; i >= 0; i--) dap_key_event(mods[i], false);
1803
    }
1804
1805
    cJSON *body = cJSON_CreateObject();
1806
    cJSON_AddNumberToObject(body, "scancode", sc);
1807
    cJSON_AddStringToObject(body, "action", action);
1808
    send_dap_response(seq, "x16/sendKey", true, body);
1809
    return 0;
1810
}
1811
1812
// ─── Custom: x16/type — type a UTF-8/PETSCII string into the keyboard ──
1813
//
1814
// Arguments: text — the string to type. Routed through the same path as a
1815
// clipboard paste, so it lands in the KERNAL keyboard buffer (ideal for BASIC
1816
// lines, filenames, etc.). Supports the "\Xnn" hex escape the paste path does.
1817
static int handle_x16_type(int seq, cJSON *args) {
1818
    cJSON *text_item = args ? cJSON_GetObjectItemCaseSensitive(args, "text") : NULL;
1819
    if (!text_item || !cJSON_IsString(text_item)) {
1820
        send_dap_error_response(seq, "x16/type", "missing 'text'");
1821
        return 0;
1822
    }
1823
    // machine_paste takes ownership and frees with SDL_free when handle_free=true.
1824
    char *copy = SDL_strdup(text_item->valuestring);
1825
    if (!copy) {
1826
        send_dap_error_response(seq, "x16/type", "out of memory");
1827
        return 0;
1828
    }
1829
    machine_paste(copy, true);
1830
1831
    cJSON *body = cJSON_CreateObject();
1832
    cJSON_AddNumberToObject(body, "length", (double)strlen(text_item->valuestring));
1833
    send_dap_response(seq, "x16/type", true, body);
1834
    return 0;
1835
}
1836
1837
// ─── Custom: x16/joystick — drive a SNES controller port from a script ──
1838
//
1839
// Arguments:
1840
//   index:   slot 0-3 (required).
1841
//   buttons: array of names held down. The array is the complete held set, so
1842
//            anything absent is released -- that is what lets a script hold a
1843
//            direction across frames and then let go in a single request.
1844
//   mask:    raw 16-bit active-low mask (0 = pressed), an alternative to
1845
//            `buttons` for replaying a captured state verbatim.
1846
//   enabled: false releases the slot back to a physical controller.
1847
//
1848
// A request carrying neither `buttons` nor `mask` reports the slot's state
1849
// without changing it, so a test can assert what it just pressed.
1850
1851
static const struct {
1852
    const char *name;
1853
    uint16_t    bit;
1854
    bool        alias;
1855
} dap_joy_buttons[] = {
1856
    {"up",     JOY_BIT_DPAD_UP,        false},
1857
    {"down",   JOY_BIT_DPAD_DOWN,      false},
1858
    {"left",   JOY_BIT_DPAD_LEFT,      false},
1859
    {"right",  JOY_BIT_DPAD_RIGHT,     false},
1860
    {"a",      JOY_BIT_A,              false},
1861
    {"b",      JOY_BIT_B,              false},
1862
    {"x",      JOY_BIT_X,              false},
1863
    {"y",      JOY_BIT_Y,              false},
1864
    {"start",  JOY_BIT_START,          false},
1865
    {"select", JOY_BIT_SELECT,         false},
1866
    {"l",      JOY_BIT_LEFT_SHOULDER,  false},
1867
    {"r",      JOY_BIT_RIGHT_SHOULDER, false},
1868
    // Aliases: accepted on input, never used to name a button on output.
1869
    {"back",   JOY_BIT_SELECT,         true},
1870
    {"lshoulder", JOY_BIT_LEFT_SHOULDER,  true},
1871
    {"rshoulder", JOY_BIT_RIGHT_SHOULDER, true},
1872
};
1873
1874
#define DAP_JOY_BUTTON_COUNT ((int)(sizeof(dap_joy_buttons) / sizeof(dap_joy_buttons[0])))
1875
1876
static bool dap_str_ieq(const char *a, const char *b) {
1877
    if (!a || !b) return false;
1878
    while (*a && *b) {
1879
        if (tolower((unsigned char)*a) != tolower((unsigned char)*b)) return false;
1880
        a++; b++;
1881
    }
1882
    return *a == *b;
1883
}
1884
1885
static uint16_t dap_joy_bit_from_name(const char *name) {
1886
    for (int i = 0; i < DAP_JOY_BUTTON_COUNT; i++) {
1887
        if (dap_str_ieq(name, dap_joy_buttons[i].name)) return dap_joy_buttons[i].bit;
1888
    }
1889
    return 0;
1890
}
1891
1892
// The held set, named. Active-low, so a cleared bit is a pressed button.
1893
static cJSON *dap_joy_buttons_array(uint16_t mask) {
1894
    cJSON *arr = cJSON_CreateArray();
1895
    for (int i = 0; i < DAP_JOY_BUTTON_COUNT; i++) {
1896
        if (dap_joy_buttons[i].alias) continue;
1897
        if (!(mask & dap_joy_buttons[i].bit)) {
1898
            cJSON_AddItemToArray(arr, cJSON_CreateString(dap_joy_buttons[i].name));
1899
        }
1900
    }
1901
    return arr;
1902
}
1903
1904
static int handle_x16_joystick(int seq, cJSON *args) {
1905
    if (!args) {
1906
        send_dap_error_response(seq, "x16/joystick", "missing arguments");
1907
        return 0;
1908
    }
1909
1910
    cJSON *index_item = cJSON_GetObjectItemCaseSensitive(args, "index");
1911
    if (!index_item || !cJSON_IsNumber(index_item)) {
1912
        send_dap_error_response(seq, "x16/joystick", "missing or non-numeric 'index'");
1913
        return 0;
1914
    }
1915
    int index = (int)index_item->valuedouble;
1916
    if (index < 0 || index >= NUM_JOYSTICKS) {
1917
        send_dap_error_response(seq, "x16/joystick", "'index' out of range (expected 0-3)");
1918
        return 0;
1919
    }
1920
1921
    cJSON *enabled_item = cJSON_GetObjectItemCaseSensitive(args, "enabled");
1922
    cJSON *mask_item    = cJSON_GetObjectItemCaseSensitive(args, "mask");
1923
    cJSON *buttons_item = cJSON_GetObjectItemCaseSensitive(args, "buttons");
1924
1925
    if (buttons_item && !cJSON_IsArray(buttons_item)) {
1926
        send_dap_error_response(seq, "x16/joystick", "'buttons' must be an array of names");
1927
        return 0;
1928
    }
1929
    if (mask_item && !cJSON_IsNumber(mask_item)) {
1930
        send_dap_error_response(seq, "x16/joystick", "'mask' must be a number");
1931
        return 0;
1932
    }
1933
1934
    if (enabled_item && cJSON_IsBool(enabled_item) && !cJSON_IsTrue(enabled_item)) {
1935
        joystick_clear_virtual(index);
1936
    } else if (buttons_item || mask_item) {
1937
        // Start from "nothing pressed" so an absent button is a released one.
1938
        uint16_t mask = 0xFFFF;
1939
        if (mask_item) {
1940
            mask = (uint16_t)(unsigned)mask_item->valuedouble;
1941
        }
1942
        if (buttons_item) {
1943
            cJSON *b;
1944
            cJSON_ArrayForEach(b, buttons_item) {
1945
                if (!cJSON_IsString(b) || !b->valuestring) continue;
1946
                uint16_t bit = dap_joy_bit_from_name(b->valuestring);
1947
                if (bit == 0) {
1948
                    char err[96];
1949
                    snprintf(err, sizeof(err), "unknown button '%s'", b->valuestring);
1950
                    send_dap_error_response(seq, "x16/joystick", err);
1951
                    return 0;
1952
                }
1953
                mask &= (uint16_t)~bit;
1954
            }
1955
        }
1956
        joystick_set_virtual(index, mask);
1957
    } else if (enabled_item && cJSON_IsBool(enabled_item)) {
1958
        // enabled:true on its own plugs a controller in with nothing held. That
1959
        // is a distinct state from an empty port -- a port being driven runs its
1960
        // shift register out, so the KERNAL reports a controller present -- and
1961
        // it is what a game that waits for one to be connected needs to see.
1962
        joystick_debug_state_t cur;
1963
        joystick_debug_get_state(&cur);
1964
        joystick_set_virtual(index,
1965
                             joystick_virtual_active(index) ? cur.slots[index].button_mask : 0xFFFF);
1966
    }
1967
    // Otherwise: no enabled/buttons/mask, so this is a query and changes nothing.
1968
1969
    joystick_debug_state_t js;
1970
    joystick_debug_get_state(&js);
1971
1972
    cJSON *body = cJSON_CreateObject();
1973
    cJSON_AddNumberToObject(body, "index", index);
1974
    cJSON_AddNumberToObject(body, "mask", js.slots[index].button_mask);
1975
    cJSON_AddItemToObject(body, "buttons", dap_joy_buttons_array(js.slots[index].button_mask));
1976
    cJSON_AddBoolToObject(body, "enabled", js.slots[index].virtual_active);
1977
    cJSON_AddBoolToObject(body, "slotEnabled", js.slots[index].enabled);
1978
    cJSON_AddBoolToObject(body, "controllerBound", js.slots[index].controller_bound);
1979
    cJSON_AddNumberToObject(body, "shift", js.slots[index].shift_mask);
1980
    cJSON_AddNumberToObject(body, "data", js.data);
1981
    cJSON_AddBoolToObject(body, "latch", js.latch);
1982
    send_dap_response(seq, "x16/joystick", true, body);
1983
    return 0;
1984
}
1985
1986
// ─── Custom: x16/registers — all CPU + KERNAL + VERA state ─────────
1987
1988
static int handle_x16_registers(int seq, cJSON *args) {
1989
    (void)args;
1990
    cJSON *body = cJSON_CreateObject();
1991
1992
    // CPU registers
1993
    cJSON *cpu = cJSON_CreateObject();
1994
    cJSON_AddNumberToObject(cpu, "pc", regs.pc);
1995
    cJSON_AddNumberToObject(cpu, "a", regs.c);       // full 16-bit accumulator
1996
    cJSON_AddNumberToObject(cpu, "x", regs.x);       // full 16-bit X
1997
    cJSON_AddNumberToObject(cpu, "y", regs.y);       // full 16-bit Y
1998
    cJSON_AddNumberToObject(cpu, "sp", regs.sp);
1999
    cJSON_AddNumberToObject(cpu, "dp", regs.dp);     // direct page
2000
    cJSON_AddNumberToObject(cpu, "dbr", regs.db);    // data bank
2001
    cJSON_AddNumberToObject(cpu, "pbr", regs.k);     // program bank
2002
    cJSON_AddNumberToObject(cpu, "status", regs.status);
2003
    cJSON_AddNumberToObject(cpu, "e", regs.e);       // emulation flag
2004
    cJSON_AddBoolToObject(cpu, "is65c816", regs.is65c816);
2005
    cJSON_AddNumberToObject(cpu, "ramBank", memory_get_ram_bank());
2006
    cJSON_AddNumberToObject(cpu, "romBank", memory_get_rom_bank());
2007
    cJSON_AddItemToObject(body, "cpu", cpu);
2008
2009
    // KERNAL r0-r15 pseudo-registers (ZP $02-$21, 16-bit little-endian)
2010
    cJSON *kregs = cJSON_CreateArray();
2011
    for (int i = 0; i < 16; i++) {
2012
        uint8_t lo = debug_read6502(0x02 + i * 2, 0, USE_CURRENT_X16_BANK);
2013
        uint8_t hi = debug_read6502(0x03 + i * 2, 0, USE_CURRENT_X16_BANK);
2014
        cJSON_AddItemToArray(kregs, cJSON_CreateNumber(lo | (hi << 8)));
2015
    }
2016
    cJSON_AddItemToObject(body, "kernal", kregs);
2017
2018
    // VERA registers ($9F20-$9F3F, read via video_read)
2019
    cJSON *vera = cJSON_CreateObject();
2020
    uint8_t addr0_l = video_read(0, true);
2021
    uint8_t addr0_m = video_read(1, true);
2022
    uint8_t addr0_h = video_read(2, true);
2023
    uint32_t addr0 = addr0_l | (addr0_m << 8) | ((addr0_h & 0x01) << 16);
2024
    uint8_t addr0_inc = (addr0_h >> 3) & 0x0F;
2025
    cJSON_AddNumberToObject(vera, "addr0", addr0);
2026
    cJSON_AddNumberToObject(vera, "addr0Inc", addr0_inc);
2027
2028
    uint8_t ctrl = video_read(5, true);
2029
    cJSON_AddNumberToObject(vera, "ctrl", ctrl);
2030
2031
    // If ADDRSEL=1 in CTRL, read addr1 via the alt port
2032
    // VERA ADDR1 is accessible when ADDRSEL bit is set
2033
    // We read the raw register values for display
2034
    cJSON_AddNumberToObject(vera, "ien", video_read(6, true));
2035
    cJSON_AddNumberToObject(vera, "isr", video_read(7, true));
2036
    cJSON_AddNumberToObject(vera, "irqLineL", video_read(8, true));
2037
2038
    // Display composer (DCSEL-banked in HW; read via video_get_dc_value so the
2039
    // values are correct regardless of the guest's current DCSEL). These index
2040
    // reg_composer[]: 0-3 = VIDEO/HSCALE/VSCALE/BORDER, 4-7 = HSTART/HSTOP/VSTART/VSTOP.
2041
    cJSON_AddNumberToObject(vera, "dcVideo", video_get_dc_value(0));
2042
    cJSON_AddNumberToObject(vera, "dcHScale", video_get_dc_value(1));
2043
    cJSON_AddNumberToObject(vera, "dcVScale", video_get_dc_value(2));
2044
    cJSON_AddNumberToObject(vera, "dcBorder", video_get_dc_value(3));
2045
    cJSON_AddNumberToObject(vera, "dcHStartL", video_get_dc_value(4));
2046
    cJSON_AddNumberToObject(vera, "dcHStopL", video_get_dc_value(5));
2047
    cJSON_AddNumberToObject(vera, "dcVStartL", video_get_dc_value(6));
2048
    cJSON_AddNumberToObject(vera, "dcVStopL", video_get_dc_value(7));
2049
2050
    // Layer 0 registers live at VERA $0D-$13 (reg_layer[0][0..6]); no side effects.
2051
    cJSON *l0 = cJSON_CreateObject();
2052
    cJSON_AddNumberToObject(l0, "config", video_read(0x0D, true));
2053
    cJSON_AddNumberToObject(l0, "mapBase", video_read(0x0E, true));
2054
    cJSON_AddNumberToObject(l0, "tileBase", video_read(0x0F, true));
2055
    cJSON_AddNumberToObject(l0, "hScroll", video_read(0x10, true) | (video_read(0x11, true) << 8));
2056
    cJSON_AddNumberToObject(l0, "vScroll", video_read(0x12, true) | (video_read(0x13, true) << 8));
2057
    cJSON_AddItemToObject(vera, "l0", l0);
2058
2059
    // Layer 1 registers live at VERA $14-$1A (reg_layer[1][0..6]); no side effects.
2060
    cJSON *l1 = cJSON_CreateObject();
2061
    cJSON_AddNumberToObject(l1, "config", video_read(0x14, true));
2062
    cJSON_AddNumberToObject(l1, "mapBase", video_read(0x15, true));
2063
    cJSON_AddNumberToObject(l1, "tileBase", video_read(0x16, true));
2064
    cJSON_AddNumberToObject(l1, "hScroll", video_read(0x17, true) | (video_read(0x18, true) << 8));
2065
    cJSON_AddNumberToObject(l1, "vScroll", video_read(0x19, true) | (video_read(0x1A, true) << 8));
2066
    cJSON_AddItemToObject(vera, "l1", l1);
2067
2068
    // Audio: AUDIO_CTRL ($1B) runs audio_render() on read, so suppress it
2069
    // (reads back 0); AUDIO_RATE ($1C) is side-effect-free.
2070
    cJSON_AddNumberToObject(vera, "audioCtrl", vera_read_dbg(0x1B));
2071
    cJSON_AddNumberToObject(vera, "audioRate", video_read(0x1C, true));
2072
2073
    cJSON_AddItemToObject(body, "vera", vera);
2074
2075
    send_dap_response(seq, "x16/registers", true, body);
2076
    return 0;
2077
}
2078
2079
// ─── Performance budget (x16/perfStats, x16/perfConfig) ─────────────────────
2080
// Exposes the guest cycle budget accounting from perf_budget.c so that tooling
2081
// -- a VS Code extension, a CI job, a profiling script -- can watch for
2082
// regressions without a human reading a panel.
2083
//
2084
// Statistics are answered while the machine runs free: nothing here stops it,
2085
// and the server is polled from the emulation thread, so the numbers cannot be
2086
// torn by a concurrent update.
2087
2088
static int handle_x16_perf_stats(int seq, cJSON *args) {
2089
    // A client asking for statistics is a client that wants them collected.
2090
    // Arming here rather than making it a separate round trip means the common
2091
    // case -- attach, poll, read -- just works.
2092
    if (!perf_budget_owner_wants(PERF_OWNER_DAP) && !perf_budget_is_enabled()) {
2093
        perf_budget_arm(PERF_OWNER_DAP, true);
2094
    }
2095
    if (!vera_bandwidth_owner_wants(VERA_BW_OWNER_DAP) && !vera_bandwidth_is_enabled()) {
2096
        vera_bandwidth_arm(VERA_BW_OWNER_DAP, true);
2097
    }
2098
2099
    cJSON *body = cJSON_CreateObject();
2100
    cJSON_AddItemToObject(body, "budget", perf_json_budget());
2101
    cJSON_AddItemToObject(body, "bandwidth", perf_json_bandwidth());
2102
2103
    perf_frame_t last;
2104
    if (perf_budget_last_frame(&last)) {
2105
        cJSON_AddItemToObject(body, "current", perf_json_frame(&last));
2106
    }
2107
2108
    // Windows: whatever the client asked for, else a useful default spread.
2109
    static const float defaults[] = {1.0f, 10.0f, 30.0f, 60.0f, 0.0f};
2110
    cJSON *req = args ? cJSON_GetObjectItemCaseSensitive(args, "windows") : NULL;
2111
    cJSON *windows = cJSON_CreateArray();
2112
    const int nreq = (req && cJSON_IsArray(req)) ? cJSON_GetArraySize(req) : 0;
2113
    const int nwin = nreq > 0 ? nreq : (int)(sizeof defaults / sizeof defaults[0]);
2114
    for (int i = 0; i < nwin; i++) {
2115
        const float secs = nreq > 0 ? perf_json_window_seconds(cJSON_GetArrayItem(req, i))
2116
                                    : defaults[i];
2117
        perf_window_t w;
2118
        if (!perf_budget_window(secs, &w)) continue;
2119
2120
        char name[32];
2121
        perf_json_window_name(secs, name, sizeof name);
2122
        cJSON *o = cJSON_CreateObject();
2123
        cJSON_AddStringToObject(o, "name", name);
2124
        cJSON_AddNumberToObject(o, "seconds", secs);
2125
        cJSON_AddNumberToObject(o, "frames", w.frames);
2126
        cJSON_AddNumberToObject(o, "budgetCycles", w.budget_cycles);
2127
        cJSON_AddItemToObject(o, "work", perf_json_stat(&w.work));
2128
        cJSON_AddItemToObject(o, "budgetWork", perf_json_stat(&w.budget_work));
2129
        cJSON_AddItemToObject(o, "total", perf_json_stat(&w.total));
2130
        cJSON_AddItemToObject(o, "irq", perf_json_stat(&w.irq));
2131
        cJSON_AddItemToObject(o, "hostUs", perf_json_stat(&w.host_us));
2132
        cJSON_AddNumberToObject(o, "overruns", w.overruns);
2133
        cJSON_AddNumberToObject(o, "overrunPct", w.overrun_pct);
2134
        cJSON_AddNumberToObject(o, "worstOverrunCycles", w.worst_overrun);
2135
        cJSON_AddNumberToObject(o, "worstFrame", w.worst_frame);
2136
        cJSON_AddNumberToObject(o, "meanUtilizationPct", w.mean_utilization);
2137
        cJSON_AddNumberToObject(o, "p95UtilizationPct", w.p95_utilization);
2138
        cJSON_AddNumberToObject(o, "maxUtilizationPct", w.max_utilization);
2139
        cJSON_AddItemToArray(windows, o);
2140
    }
2141
    cJSON_AddItemToObject(body, "windows", windows);
2142
2143
    cJSON *incz = args ? cJSON_GetObjectItemCaseSensitive(args, "includeZones") : NULL;
2144
    if (!incz || !cJSON_IsFalse(incz)) {
2145
        cJSON *zones = cJSON_CreateArray();
2146
        const int nz = perf_budget_zone_count();
2147
        for (int i = 0; i < nz; i++) {
2148
            perf_zone_t z;
2149
            if (!perf_budget_zone_get(i, &z)) continue;
2150
            cJSON *o = cJSON_CreateObject();
2151
            cJSON_AddNumberToObject(o, "id", i);
2152
            cJSON_AddStringToObject(o, "name", z.name);
2153
            cJSON_AddBoolToObject(o, "isMarker", z.is_marker);
2154
            cJSON_AddBoolToObject(o, "isIdle", z.is_idle);
2155
            if (z.is_marker) {
2156
                cJSON_AddNumberToObject(o, "markerId", z.marker_id);
2157
            } else {
2158
                cJSON_AddNumberToObject(o, "start", z.start);
2159
                cJSON_AddNumberToObject(o, "end", z.end);
2160
                cJSON_AddNumberToObject(o, "bank", z.bank);
2161
            }
2162
            if (i < PERF_RING_ZONES) {
2163
                perf_frame_t lf;
2164
                if (perf_budget_last_frame(&lf))
2165
                    cJSON_AddNumberToObject(o, "current", lf.zone[i]);
2166
            }
2167
            perf_stat_t st;
2168
            if (perf_budget_zone_window(i, 30.0f, &st))
2169
                cJSON_AddItemToObject(o, "window30s", perf_json_stat(&st));
2170
            cJSON_AddItemToArray(zones, o);
2171
        }
2172
        cJSON_AddItemToObject(body, "zones", zones);
2173
    }
2174
2175
    cJSON *incf = args ? cJSON_GetObjectItemCaseSensitive(args, "includeFrames") : NULL;
2176
    if (incf && cJSON_IsNumber(incf) && incf->valuedouble > 0) {
2177
        int want = (int)incf->valuedouble;
2178
        const int have = perf_budget_frame_count();
2179
        if (want > have) want = have;
2180
        cJSON *frames = cJSON_CreateArray();
2181
        // Oldest first, so a client can append them to a series it is keeping.
2182
        for (int i = want - 1; i >= 0; i--) {
2183
            perf_frame_t f;
2184
            if (perf_budget_frame_at(i, &f))
2185
                cJSON_AddItemToArray(frames, perf_json_frame(&f));
2186
        }
2187
        cJSON_AddItemToObject(body, "frames", frames);
2188
    }
2189
2190
    send_dap_response(seq, "x16/perfStats", true, body);
2191
    return 0;
2192
}
2193
2194
// Whether the client wants pushed overrun notifications, and when the last one
2195
// went out. Coalescing is the point: a program that misses its budget usually
2196
// misses it for many frames running, and one event per frame would be a flood
2197
// that says nothing the first one did not.
2198
static bool     perf_overrun_events = false;
2199
static uint32_t perf_overrun_last_ms = 0;
2200
#define PERF_OVERRUN_EVENT_INTERVAL_MS 1000
2201
2202
static int handle_x16_perf_config(int seq, cJSON *args) {
2203
    if (!args) {
2204
        send_dap_error_response(seq, "x16/perfConfig", "missing arguments");
2205
        return 0;
2206
    }
2207
2208
    cJSON *item = cJSON_GetObjectItemCaseSensitive(args, "enabled");
2209
    if (cJSON_IsBool(item)) {
2210
        const bool want = cJSON_IsTrue(item);
2211
        if (!perf_budget_arm(PERF_OWNER_DAP, want)) {
2212
            send_dap_error_response(seq, "x16/perfConfig",
2213
                                    "could not allocate the history ring");
2214
            return 0;
2215
        }
2216
        // Turning profiling on is as clear a statement of intent as there is,
2217
        // so overrun events follow it unless the client says otherwise below.
2218
        perf_overrun_events = want;
2219
        // The bandwidth accounting follows the same switch. Two requests to
2220
        // answer one question ("profile this machine") would be a surface
2221
        // nobody wants to remember.
2222
        vera_bandwidth_arm(VERA_BW_OWNER_DAP, want);
2223
    }
2224
2225
    item = cJSON_GetObjectItemCaseSensitive(args, "targetFps");
2226
    if (cJSON_IsNumber(item) && item->valuedouble > 0)
2227
        perf_budget_set_target_fps((float)item->valuedouble);
2228
2229
    item = cJSON_GetObjectItemCaseSensitive(args, "idleMode");
2230
    if (cJSON_IsString(item) && item->valuestring) {
2231
        if (!strcmp(item->valuestring, "auto"))         perf_budget_set_idle_mode(PERF_IDLE_AUTO);
2232
        else if (!strcmp(item->valuestring, "markers")) perf_budget_set_idle_mode(PERF_IDLE_MARKERS);
2233
        else if (!strcmp(item->valuestring, "none"))    perf_budget_set_idle_mode(PERF_IDLE_NONE);
2234
    }
2235
2236
    item = cJSON_GetObjectItemCaseSensitive(args, "historyFrames");
2237
    if (cJSON_IsNumber(item) && item->valuedouble >= 2)
2238
        perf_budget_set_capacity((int)item->valuedouble);
2239
2240
    item = cJSON_GetObjectItemCaseSensitive(args, "overrunEvents");
2241
    if (cJSON_IsBool(item))
2242
        perf_overrun_events = cJSON_IsTrue(item);
2243
2244
    // Zones, when given, REPLACE the address zones wholesale: a client that
2245
    // owns the zone list wants to declare it, not diff it. Marker regions the
2246
    // guest opened are not the client's to remove, but zone_clear takes them
2247
    // too -- they are re-created by the next marker write.
2248
    item = cJSON_GetObjectItemCaseSensitive(args, "zones");
2249
    if (cJSON_IsArray(item)) {
2250
        perf_budget_zone_clear();
2251
        const int n = cJSON_GetArraySize(item);
2252
        for (int i = 0; i < n; i++) {
2253
            cJSON *z = cJSON_GetArrayItem(item, i);
2254
            if (!cJSON_IsObject(z)) continue;
2255
            cJSON *jn = cJSON_GetObjectItemCaseSensitive(z, "name");
2256
            cJSON *js = cJSON_GetObjectItemCaseSensitive(z, "start");
2257
            cJSON *je = cJSON_GetObjectItemCaseSensitive(z, "end");
2258
            cJSON *jb = cJSON_GetObjectItemCaseSensitive(z, "bank");
2259
            cJSON *ji = cJSON_GetObjectItemCaseSensitive(z, "idle");
2260
            if (!cJSON_IsNumber(js) || !cJSON_IsNumber(je)) continue;
2261
            perf_budget_zone_add(cJSON_IsString(jn) ? jn->valuestring : "zone",
2262
                                 (uint16_t)js->valuedouble, (uint16_t)je->valuedouble,
2263
                                 cJSON_IsNumber(jb) ? (int16_t)jb->valuedouble : (int16_t)-1,
2264
                                 cJSON_IsTrue(ji));
2265
        }
2266
    }
2267
2268
    item = cJSON_GetObjectItemCaseSensitive(args, "reset");
2269
    if (cJSON_IsTrue(item)) {
2270
        perf_budget_reset();
2271
        vera_bandwidth_reset();
2272
    }
2273
2274
    cJSON *body = cJSON_CreateObject();
2275
    cJSON_AddItemToObject(body, "budget", perf_json_budget());
2276
    cJSON_AddItemToObject(body, "bandwidth", perf_json_bandwidth());
2277
    cJSON_AddBoolToObject(body, "overrunEvents", perf_overrun_events);
2278
    send_dap_response(seq, "x16/perfConfig", true, body);
2279
    return 0;
2280
}
2281
2282
// Push accumulated budget overruns to the client. Called once per poll, which
2283
// is once per emulated frame.
2284
static void perf_poll_overruns(void) {
2285
    if (!perf_overrun_events || client_sock == SOCKET_INVALID) return;
2286
    if (!perf_budget_is_enabled()) return;
2287
2288
    const uint32_t now = SDL_GetTicks();
2289
    if (now - perf_overrun_last_ms < PERF_OVERRUN_EVENT_INTERVAL_MS) return;
2290
2291
    perf_overrun_report_t r;
2292
    if (!perf_budget_drain_overruns(&r)) {
2293
        // Nothing to report. The clock is still advanced so that a run of clean
2294
        // seconds does not make the first bad one fire instantly.
2295
        perf_overrun_last_ms = now;
2296
        return;
2297
    }
2298
    perf_overrun_last_ms = now;
2299
2300
    cJSON *body = cJSON_CreateObject();
2301
    cJSON_AddNumberToObject(body, "sinceLastEventFrames", r.frames);
2302
    cJSON_AddNumberToObject(body, "overruns", r.overruns);
2303
    cJSON_AddNumberToObject(body, "worstOverrunCycles", r.worst_overrun);
2304
    cJSON_AddNumberToObject(body, "worstFrame", r.worst_frame);
2305
    cJSON_AddNumberToObject(body, "worstUtilizationPct", r.worst_utilization);
2306
    cJSON_AddNumberToObject(body, "budgetCycles", perf_budget_budget_cycles());
2307
    send_dap_event("x16/perfBudgetOverrun", body);
2308
}
2309
2310
static int handle_dap_disassemble(int seq, cJSON *args) {
2311
    if (!args) {
2312
        send_dap_error_response(seq, "disassemble", "missing arguments");
2313
        return 0;
2314
    }
2315
    cJSON *ref_item = cJSON_GetObjectItemCaseSensitive(args, "memoryReference");
2316
    cJSON *count_item = cJSON_GetObjectItemCaseSensitive(args, "instructionCount");
2317
    cJSON *offset_item = cJSON_GetObjectItemCaseSensitive(args, "offset");
2318
2319
    if (!ref_item || !cJSON_IsString(ref_item)) {
2320
        send_dap_error_response(seq, "disassemble", "missing memoryReference");
2321
        return 0;
2322
    }
2323
2324
    uint32_t ref = (uint32_t)strtoul(ref_item->valuestring, NULL, 0);
2325
    int offset = (offset_item && cJSON_IsNumber(offset_item)) ? (int)offset_item->valuedouble : 0;
2326
    int count = (count_item && cJSON_IsNumber(count_item)) ? (int)count_item->valuedouble : 10;
2327
    uint32_t ref24 = (uint32_t)(((int)ref + offset)) & 0xFFFFFF;
2328
    uint16_t addr = (uint16_t)(ref24 & 0xFFFF);
2329
    uint8_t  bank = (uint8_t)(ref24 >> 16); // 65C816 program bank
2330
    if (count > 200) count = 200;
2331
    if (count < 0) count = 0;
2332
2333
    // Decode via the anchored code map so operand widths follow the recorded
2334
    // 65C816 M/X/E flag state (the naive path assumed a fixed width and drifted).
2335
    uint8_t rambank = memory_get_ram_bank();
2336
    uint8_t rombank = memory_get_rom_bank();
2337
    static code_map_line_t lines[200];
2338
    int n = code_map_disasm_forward(addr, bank, rambank, rombank, count, lines, 200, NULL);
2339
2340
    cJSON *instructions = cJSON_CreateArray();
2341
    for (int i = 0; i < n; i++) {
2342
        const code_map_line_t *ln = &lines[i];
2343
2344
        cJSON *inst = cJSON_CreateObject();
2345
        char addr_str[16];
2346
        if (bank != 0 || is_gen2)
2347
            snprintf(addr_str, sizeof(addr_str), "0x%06X", ((uint32_t)bank << 16) | ln->addr);
2348
        else
2349
            snprintf(addr_str, sizeof(addr_str), "0x%04X", ln->addr);
2350
        cJSON_AddStringToObject(inst, "address", addr_str);
2351
        cJSON_AddStringToObject(inst, "instruction", ln->text);
2352
2353
        // Build instructionBytes hex string
2354
        char bytes_str[32] = "";
2355
        int blen = 0;
2356
        for (int b = 0; b < ln->size && b < 4; b++) {
2357
            blen += snprintf(bytes_str + blen, sizeof(bytes_str) - blen, "%02X ", ln->bytes[b]);
2358
        }
2359
        if (blen > 0) bytes_str[blen - 1] = '\0'; // trim trailing space
2360
        cJSON_AddStringToObject(inst, "instructionBytes", bytes_str);
2361
2362
        // Add source location if dbg info is available
2363
        const char *src_file = NULL;
2364
        int src_line = 0;
2365
        if (dbg_info_addr_to_source(ln->addr, &src_file, &src_line)) {
2366
            cJSON *source = cJSON_CreateObject();
2367
            cJSON_AddStringToObject(source, "path", src_file);
2368
            cJSON_AddItemToObject(inst, "source", source);
2369
            cJSON_AddNumberToObject(inst, "line", src_line);
2370
        }
2371
2372
        cJSON_AddItemToArray(instructions, inst);
2373
    }
2374
2375
    cJSON *body = cJSON_CreateObject();
2376
    cJSON_AddItemToObject(body, "instructions", instructions);
2377
    send_dap_response(seq, "disassemble", true, body);
2378
    return 0;
2379
}
2380
2381
// ─── DAP command dispatch ───────────────────────────────────────────
2382
2383
// ─── setVariable ─────────────────────────────────────────────────────
2384
static int handle_dap_set_variable(int seq, cJSON *args) {
2385
    if (!args) { send_dap_error_response(seq, "setVariable", "missing arguments"); return 0; }
2386
    cJSON *ref_i  = cJSON_GetObjectItemCaseSensitive(args, "variablesReference");
2387
    cJSON *name_i = cJSON_GetObjectItemCaseSensitive(args, "name");
2388
    cJSON *val_i  = cJSON_GetObjectItemCaseSensitive(args, "value");
2389
    int ref = (ref_i && cJSON_IsNumber(ref_i)) ? (int)ref_i->valuedouble : 0;
2390
    const char *name   = (name_i && cJSON_IsString(name_i)) ? name_i->valuestring : "";
2391
    const char *valstr = (val_i && cJSON_IsString(val_i)) ? val_i->valuestring : "";
2392
    uint32_t v = dap_parse_num(valstr);
2393
    bool ok = true;
2394
    bool a16 = regs.is65c816 && !regs.e && !(regs.status & FLAG_MEMORY_WIDTH);
2395
    bool x16 = regs.is65c816 && !regs.e && !(regs.status & FLAG_INDEX_WIDTH);
2396
2397
    if (ref == VARREF_REGISTERS) {
2398
        if      (!strcmp(name, "PC")) regs.pc = (uint16_t)v;
2399
        else if (!strcmp(name, "A"))  { if (a16) regs.c = (uint16_t)v; else regs.a = (uint8_t)v; }
2400
        else if (!strcmp(name, "X"))  { if (x16) regs.x = (uint16_t)v; else regs.xl = (uint8_t)v; }
2401
        else if (!strcmp(name, "Y"))  { if (x16) regs.y = (uint16_t)v; else regs.yl = (uint8_t)v; }
2402
        else if (!strcmp(name, "SP")) regs.sp = regs.is65c816 ? (uint16_t)v : (uint16_t)((regs.sp & 0xFF00) | (v & 0xFF));
2403
        else if (!strcmp(name, "D"))  regs.dp = (uint16_t)v;
2404
        else if (!strcmp(name, "DBR")) regs.db = (uint8_t)v;
2405
        else if (!strcmp(name, "PBR")) regs.k = (uint8_t)v;
2406
        else if (name[0] == 'P')      regs.status = (uint8_t)v; // "P (Status)"
2407
        else if (!strcmp(name, "RAM Bank")) memory_set_ram_bank((uint8_t)v);
2408
        else if (!strcmp(name, "ROM Bank")) memory_set_rom_bank((uint8_t)v);
2409
        else ok = false;
2410
    } else if (ref == VARREF_STACK) {
2411
        debug_write6502((uint16_t)dap_parse_num(name), 0, (uint8_t)v); // name is "$XXXX"
2412
    } else if (ref == VARREF_VREGS) {
2413
        // name is "Rn" (n = 0..15) → 16-bit little-endian at direct-page + $02 + 2*n.
2414
        if ((name[0] == 'R' || name[0] == 'r') && name[1]) {
2415
            int n = atoi(name + 1);
2416
            if (n >= 0 && n <= 15) {
2417
                uint16_t a = dap_dp_add((uint16_t)(0x02 + 2 * n));
2418
                debug_write6502(a, 0, (uint8_t)(v & 0xFF));
2419
                debug_write6502((uint16_t)(a + 1), 0, (uint8_t)((v >> 8) & 0xFF));
2420
            } else ok = false;
2421
        } else ok = false;
2422
    } else {
2423
        ok = false; // zero-page rows are 16-byte groups — not individually settable
2424
    }
2425
2426
    if (!ok) { send_dap_error_response(seq, "setVariable", "cannot set this variable"); return 0; }
2427
    char result[40];
2428
    snprintf(result, sizeof result, "$%X (%u)", v, v);
2429
    cJSON *body = cJSON_CreateObject();
2430
    cJSON_AddStringToObject(body, "value", result);
2431
    cJSON_AddNumberToObject(body, "variablesReference", 0);
2432
    send_dap_response(seq, "setVariable", true, body);
2433
    return 0;
2434
}
2435
2436
// ─── terminate (end debugging; keep the emulator running) ────────────
2437
static int handle_dap_terminate(int seq, cJSON *args) {
2438
    (void)args;
2439
    send_dap_response(seq, "terminate", true, NULL);
2440
    dap_session_active = false;
2441
    // Released before resuming, for the same reason as the disconnect request:
2442
    // running on with this client's breakpoints still armed halts the machine
2443
    // again moments later, and by then the session is marked inactive so the
2444
    // socket cleanup will not resume it.
2445
    dap_release_session_state();
2446
    // Same rule as a client disconnecting: only take the machine out of a stop
2447
    // when there is no local debugger whose user owns the run state. Otherwise
2448
    // "Stop Debugging" in the editor runs the emulator out from under someone
2449
    // who is halted in the SDL debug window inspecting it.
2450
    if (!dap_local_ui_owns_run_state()) {
2451
        currentMode = DMODE_RUN;
2452
    }
2453
    return 1;
2454
}
2455
2456
// ─── attach (the emulator is already running; mirror launch) ─────────
2457
static int handle_dap_attach(int seq, cJSON *args) {
2458
    if (args) {
2459
        cJSON *dbg_file = cJSON_GetObjectItemCaseSensitive(args, "dbgFile");
2460
        if (dbg_file && cJSON_IsString(dbg_file)) dbg_info_load(dbg_file->valuestring);
2461
    }
2462
    send_dap_response(seq, "attach", true, NULL);
2463
    send_dap_event_thread_started();
2464
    return 0;
2465
}
2466
2467
// ─── setFunctionBreakpoints (label breakpoints via .dbg) ─────────────
2468
static int handle_dap_set_function_breakpoints(int seq, cJSON *args) {
2469
    // "Here is the complete list" -- so let go of everything this kind of
2470
    // breakpoint asked for, and start again. Anything another owner also wants
2471
    // stays armed for them; nothing here has to know who they are.
2472
    debug_bp_clear_owner(DEBUG_OWNER_DAP_FUNCTION);
2473
    num_func_bps = 0;
2474
2475
    cJSON *body = cJSON_CreateObject();
2476
    cJSON *arr  = cJSON_CreateArray();
2477
    cJSON *bps  = args ? cJSON_GetObjectItemCaseSensitive(args, "breakpoints") : NULL;
2478
    int    n    = (bps && cJSON_IsArray(bps)) ? cJSON_GetArraySize(bps) : 0;
2479
    for (int i = 0; i < n; i++) {
2480
        cJSON *fb = cJSON_GetArrayItem(bps, i);
2481
        cJSON *nm = cJSON_GetObjectItemCaseSensitive(fb, "name");
2482
        cJSON *rb = cJSON_CreateObject();
2483
        dbg_addr_t addr;
2484
        bool verified = false;
2485
        if (nm && cJSON_IsString(nm) && dbg_info_label_to_addr(nm->valuestring, &addr) && num_func_bps < 128) {
2486
            struct breakpoint bp = { (int)(addr & 0xFFFF), (uint8_t)(addr >> 16), -1, 0, false };
2487
            if (debug_bp_add_for(bp, DEBUG_OWNER_DAP_FUNCTION) != DEBUG_ADD_FULL) {
2488
                func_bp_banks[num_func_bps] = (uint8_t)(addr >> 16);
2489
                func_bp_addrs[num_func_bps++] = (uint16_t)(addr & 0xFFFF);
2490
                cJSON_AddNumberToObject(rb, "id", next_dap_bp_id++);
2491
                cJSON_AddNumberToObject(rb, "instructionReference", addr);
2492
                verified = true;
2493
            }
2494
        }
2495
        cJSON_AddBoolToObject(rb, "verified", verified);
2496
        cJSON_AddItemToArray(arr, rb);
2497
    }
2498
    cJSON_AddItemToObject(body, "breakpoints", arr);
2499
    send_dap_response(seq, "setFunctionBreakpoints", true, body);
2500
    return 0;
2501
}
2502
2503
// ─── setInstructionBreakpoints ───────────────────────────────────────
2504
2505
// One field of a "bb:aaaa" reference. Unprefixed digits are hex, which is what
2506
// -bp means by the same notation; dap_parse_num()'s base-0 rule would read the
2507
// bank in "33:A555" as decimal.
2508
static uint32_t dap_parse_hex_field(const char *s) {
2509
    while (*s == ' ') s++;
2510
    if (s[0] == '$')
2511
        return (uint32_t)strtoul(s + 1, NULL, 16);
2512
    if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X'))
2513
        return (uint32_t)strtoul(s + 2, NULL, 16);
2514
    return (uint32_t)strtoul(s, NULL, 16);
2515
}
2516
2517
// Resolve an instructionReference into a core breakpoint, or fail.
2518
//
2519
// The X16 bank was the dimension this path did not have. A reference was read as
2520
// `pc | program_bank << 16` with x16Bank left at ANY, but read6502 forces the
2521
// program bank to zero off a Gen2, so any non-zero top byte built an entry whose
2522
// bank could never equal the running one: it was rejected on every instruction
2523
// while the client had been told "verified": true. Breakpoints in banked RAM --
2524
// exactly the ones worth setting from an editor -- silently did nothing.
2525
//
2526
// Which field a bank byte belongs in is debug_bp_from_ref()'s to decide, so that
2527
// this and -bp cannot drift apart on it. Only the syntax is here: "bb:aaaa" is
2528
// accepted and means what -bp means by it.
2529
static bool dap_parse_instruction_ref(const char *s, long offset, struct breakpoint *out) {
2530
    if (!s) return false;
2531
    while (*s == ' ') s++;
2532
2533
    const char *colon = strchr(s, ':');
2534
    if (colon) {
2535
        unsigned long bank = dap_parse_hex_field(s);
2536
        unsigned long a    = dap_parse_hex_field(colon + 1);
2537
        if (bank > 0xFF || a > 0xFFFF) return false;
2538
        return debug_bp_from_ref((long)a, (int)bank, offset, out);
2539
    }
2540
    return debug_bp_from_ref((long)dap_parse_num(s), DEBUG_BANK_ANY, offset, out);
2541
}
2542
2543
static int handle_dap_set_instruction_breakpoints(int seq, cJSON *args) {
2544
    debug_bp_clear_owner(DEBUG_OWNER_DAP_INSTRUCTION);
2545
    num_instr_bps = 0;
2546
2547
    cJSON *body = cJSON_CreateObject();
2548
    cJSON *arr  = cJSON_CreateArray();
2549
    cJSON *bps  = args ? cJSON_GetObjectItemCaseSensitive(args, "breakpoints") : NULL;
2550
    int    n    = (bps && cJSON_IsArray(bps)) ? cJSON_GetArraySize(bps) : 0;
2551
    for (int i = 0; i < n; i++) {
2552
        cJSON *ib  = cJSON_GetArrayItem(bps, i);
2553
        cJSON *ir  = cJSON_GetObjectItemCaseSensitive(ib, "instructionReference");
2554
        cJSON *off = cJSON_GetObjectItemCaseSensitive(ib, "offset");
2555
        cJSON *rb  = cJSON_CreateObject();
2556
        long   offset = (off && cJSON_IsNumber(off)) ? (long)off->valuedouble : 0;
2557
        struct breakpoint bp;
2558
        bool verified = false;
2559
        if (ir && cJSON_IsString(ir)
2560
            && dap_parse_instruction_ref(ir->valuestring, offset, &bp)
2561
            && num_instr_bps < 128) {
2562
            if (debug_bp_add_for(bp, DEBUG_OWNER_DAP_INSTRUCTION) != DEBUG_ADD_FULL) {
2563
                instr_bp_banks[num_instr_bps] = bp.bank;
2564
                instr_bp_addrs[num_instr_bps++] = (uint16_t)bp.pc;
2565
                cJSON_AddNumberToObject(rb, "id", next_dap_bp_id++);
2566
                verified = true;
2567
            }
2568
        }
2569
        cJSON_AddBoolToObject(rb, "verified", verified);
2570
        // Verified used to be the only thing said about a breakpoint, which made
2571
        // "armed" and "accepted but inert" look identical from the client side.
2572
        if (!verified)
2573
            cJSON_AddStringToObject(rb, "message",
2574
                                    "address could not be resolved to an armable breakpoint");
2575
        cJSON_AddItemToArray(arr, rb);
2576
    }
2577
    cJSON_AddItemToObject(body, "breakpoints", arr);
2578
    send_dap_response(seq, "setInstructionBreakpoints", true, body);
2579
    return 0;
2580
}
2581
2582
// ─── dataBreakpointInfo (memory write watchpoint on an address) ──────
2583
static int handle_dap_data_breakpoint_info(int seq, cJSON *args) {
2584
    cJSON *name_i = args ? cJSON_GetObjectItemCaseSensitive(args, "name") : NULL;
2585
    cJSON *body = cJSON_CreateObject();
2586
    if (name_i && cJSON_IsString(name_i)) {
2587
        uint16_t addr = (uint16_t)dap_parse_num(name_i->valuestring);
2588
        char dataId[16]; snprintf(dataId, sizeof dataId, "%u", addr);
2589
        char desc[64];   snprintf(desc, sizeof desc, "write $%04X", addr);
2590
        cJSON_AddStringToObject(body, "dataId", dataId);
2591
        cJSON_AddStringToObject(body, "description", desc);
2592
        cJSON *at = cJSON_CreateArray();
2593
        cJSON_AddItemToArray(at, cJSON_CreateString("write"));
2594
        cJSON_AddItemToObject(body, "accessTypes", at);
2595
        cJSON_AddBoolToObject(body, "canPersist", false);
2596
    } else {
2597
        cJSON_AddNullToObject(body, "dataId");
2598
        cJSON_AddStringToObject(body, "description", "not available");
2599
    }
2600
    send_dap_response(seq, "dataBreakpointInfo", true, body);
2601
    return 0;
2602
}
2603
2604
// ─── setDataBreakpoints (maps to emulator write watchpoints) ─────────
2605
static int handle_dap_set_data_breakpoints(int seq, cJSON *args) {
2606
    // The client's complete list, so let go of the previous one. Watches the
2607
    // console made are owned separately and are left alone, as are any from -wp.
2608
    debug_wp_clear_owner(DEBUG_OWNER_DAP_SOURCE);
2609
2610
    cJSON *body = cJSON_CreateObject();
2611
    cJSON *arr  = cJSON_CreateArray();
2612
    cJSON *bps  = args ? cJSON_GetObjectItemCaseSensitive(args, "breakpoints") : NULL;
2613
    int    n    = (bps && cJSON_IsArray(bps)) ? cJSON_GetArraySize(bps) : 0;
2614
    for (int i = 0; i < n; i++) {
2615
        cJSON *db = cJSON_GetArrayItem(bps, i);
2616
        cJSON *id = cJSON_GetObjectItemCaseSensitive(db, "dataId");
2617
        cJSON *rb = cJSON_CreateObject();
2618
        bool verified = false;
2619
        if (id && cJSON_IsString(id)) {
2620
            uint16_t addr = (uint16_t)dap_parse_num(id->valuestring);
2621
            if (debug_wp_add_for(addr, 1, DEBUG_BANK_ANY, DEBUG_OWNER_DAP_SOURCE)
2622
                != DEBUG_ADD_FULL) {
2623
                verified = true;
2624
                // Optional written-value filter via the DAP dataBreakpoint's
2625
                // `condition` (e.g. "== $05", "!=0", "$FF"): only break when the
2626
                // byte written to addr compares true.
2627
                cJSON *c = cJSON_GetObjectItemCaseSensitive(db, "condition");
2628
                if (c && cJSON_IsString(c) && c->valuestring[0]) {
2629
                    int op; uint32_t val;
2630
                    if (dap_parse_simple_cmp(c->valuestring, &op, &val))
2631
                        debug_wp_set_value(addr, DEBUG_BANK_ANY, op, (uint8_t)val);
2632
                }
2633
            }
2634
        }
2635
        cJSON_AddBoolToObject(rb, "verified", verified);
2636
        cJSON_AddItemToArray(arr, rb);
2637
    }
2638
    cJSON_AddItemToObject(body, "breakpoints", arr);
2639
    send_dap_response(seq, "setDataBreakpoints", true, body);
2640
    return 0;
2641
}
2642
2643
// ─── loadedSources (files from the loaded .dbg) ──────────────────────
2644
static int handle_dap_loaded_sources(int seq, cJSON *args) {
2645
    (void)args;
2646
    cJSON *body = cJSON_CreateObject();
2647
    cJSON *arr  = cJSON_CreateArray();
2648
    int n = dbg_info_file_count();
2649
    for (int i = 0; i < n; i++) {
2650
        const char *nm = NULL;
2651
        if (dbg_info_file_at(i, &nm) && nm) {
2652
            const char *base = nm;
2653
            for (const char *p = nm; *p; ++p) if (*p == '/' || *p == '\\') base = p + 1;
2654
            cJSON *src = cJSON_CreateObject();
2655
            cJSON_AddStringToObject(src, "name", base);
2656
            cJSON_AddStringToObject(src, "path", nm);
2657
            cJSON_AddItemToArray(arr, src);
2658
        }
2659
    }
2660
    cJSON_AddItemToObject(body, "sources", arr);
2661
    send_dap_response(seq, "loadedSources", true, body);
2662
    return 0;
2663
}
2664
2665
// ─── source (return file content by path) ────────────────────────────
2666
static int handle_dap_source(int seq, cJSON *args) {
2667
    const char *path = NULL;
2668
    if (args) {
2669
        cJSON *src = cJSON_GetObjectItemCaseSensitive(args, "source");
2670
        if (src) {
2671
            cJSON *p = cJSON_GetObjectItemCaseSensitive(src, "path");
2672
            if (!p) p = cJSON_GetObjectItemCaseSensitive(src, "name");
2673
            if (p && cJSON_IsString(p)) path = p->valuestring;
2674
        }
2675
    }
2676
    const source_file_t *sf = path ? source_view_get(path) : NULL;
2677
    if (sf && sf->found) {
2678
        size_t total = 1;
2679
        for (int i = 0; i < sf->count; i++) total += strlen(sf->lines[i]) + 1;
2680
        char *buf = (char *)malloc(total);
2681
        cJSON *body = cJSON_CreateObject();
2682
        if (buf) {
2683
            size_t o = 0;
2684
            for (int i = 0; i < sf->count; i++) {
2685
                size_t l = strlen(sf->lines[i]);
2686
                memcpy(buf + o, sf->lines[i], l); o += l;
2687
                buf[o++] = '\n';
2688
            }
2689
            buf[o] = '\0';
2690
            cJSON_AddStringToObject(body, "content", buf);
2691
            free(buf);
2692
        } else {
2693
            cJSON_AddStringToObject(body, "content", "");
2694
        }
2695
        send_dap_response(seq, "source", true, body);
2696
    } else {
2697
        send_dap_error_response(seq, "source", "source not available");
2698
    }
2699
    return 0;
2700
}
2701
2702
// ─── breakpointLocations (which lines in a range map to code) ────────
2703
static int handle_dap_breakpoint_locations(int seq, cJSON *args) {
2704
    const char *path = NULL; int line = 0, endLine = 0;
2705
    if (args) {
2706
        cJSON *src = cJSON_GetObjectItemCaseSensitive(args, "source");
2707
        if (src) { cJSON *p = cJSON_GetObjectItemCaseSensitive(src, "path"); if (p && cJSON_IsString(p)) path = p->valuestring; }
2708
        cJSON *l  = cJSON_GetObjectItemCaseSensitive(args, "line");    if (l && cJSON_IsNumber(l))  line = (int)l->valuedouble;
2709
        cJSON *el = cJSON_GetObjectItemCaseSensitive(args, "endLine"); if (el && cJSON_IsNumber(el)) endLine = (int)el->valuedouble;
2710
    }
2711
    if (endLine < line) endLine = line;
2712
    cJSON *body = cJSON_CreateObject();
2713
    cJSON *arr  = cJSON_CreateArray();
2714
    for (int ln = line; path && ln <= endLine; ln++) {
2715
        dbg_addr_t addr;
2716
        if (dbg_info_source_to_addr(path, ln, &addr)) {
2717
            cJSON *loc = cJSON_CreateObject();
2718
            cJSON_AddNumberToObject(loc, "line", ln);
2719
            cJSON_AddItemToArray(arr, loc);
2720
        }
2721
    }
2722
    cJSON_AddItemToObject(body, "breakpoints", arr);
2723
    send_dap_response(seq, "breakpointLocations", true, body);
2724
    return 0;
2725
}
2726
2727
// ─── stepInTargets (the call destination at a JSR/JSL) ───────────────
2728
static int handle_dap_step_in_targets(int seq, cJSON *args) {
2729
    (void)args;
2730
    cJSON *body = cJSON_CreateObject();
2731
    cJSON *arr  = cJSON_CreateArray();
2732
    uint8_t opc = debug_read6502(regs.pc, regs.k, USE_CURRENT_X16_BANK);
2733
    if (opc == 0x20 || opc == 0x22) { // JSR abs / JSL
2734
        uint16_t target = debug_read6502((uint16_t)(regs.pc + 1), regs.k, USE_CURRENT_X16_BANK) |
2735
                          (debug_read6502((uint16_t)(regs.pc + 2), regs.k, USE_CURRENT_X16_BANK) << 8);
2736
        const char *label = NULL;
2737
        char lbl[64];
2738
        if (dbg_info_addr_to_label(target, &label) && label)
2739
            snprintf(lbl, sizeof lbl, "%s ($%04X)", label, target);
2740
        else
2741
            snprintf(lbl, sizeof lbl, "$%04X", target);
2742
        cJSON *t = cJSON_CreateObject();
2743
        cJSON_AddNumberToObject(t, "id", 1);
2744
        cJSON_AddStringToObject(t, "label", lbl);
2745
        cJSON_AddItemToArray(arr, t);
2746
    }
2747
    cJSON_AddItemToObject(body, "targets", arr);
2748
    send_dap_response(seq, "stepInTargets", true, body);
2749
    return 0;
2750
}
2751
2752
// Process a single DAP message. Returns 1 if execution mode was changed.
2753
static int dispatch_dap(const char *json_body) {
2754
    cJSON *root = cJSON_Parse(json_body);
2755
    if (!root) {
2756
        fprintf(stderr, "[dap] Failed to parse JSON: %.80s\n", json_body);
2757
        return 0;
2758
    }
2759
2760
    cJSON *type_item = cJSON_GetObjectItemCaseSensitive(root, "type");
2761
    cJSON *seq_item = cJSON_GetObjectItemCaseSensitive(root, "seq");
2762
    cJSON *cmd_item = cJSON_GetObjectItemCaseSensitive(root, "command");
2763
    cJSON *args = cJSON_GetObjectItemCaseSensitive(root, "arguments");
2764
2765
    if (!type_item || !cJSON_IsString(type_item) || strcmp(type_item->valuestring, "request") != 0) {
2766
        // Only handle requests
2767
        cJSON_Delete(root);
2768
        return 0;
2769
    }
2770
2771
    int seq = (seq_item && cJSON_IsNumber(seq_item)) ? (int)seq_item->valuedouble : 0;
2772
    const char *cmd = (cmd_item && cJSON_IsString(cmd_item)) ? cmd_item->valuestring : "";
2773
2774
    int result = 0;
2775
2776
    printf("[dap] << %s (seq=%d)\n", cmd, seq);
2777
2778
    if (!strcmp(cmd, "initialize"))         result = handle_dap_initialize(seq, args);
2779
    else if (!strcmp(cmd, "launch"))        result = handle_dap_launch(seq, args);
2780
    else if (!strcmp(cmd, "configurationDone")) result = handle_dap_configuration_done(seq);
2781
    else if (!strcmp(cmd, "threads"))       result = handle_dap_threads(seq);
2782
    else if (!strcmp(cmd, "stackTrace"))    result = handle_dap_stack_trace(seq, args);
2783
    else if (!strcmp(cmd, "scopes"))        result = handle_dap_scopes(seq, args);
2784
    else if (!strcmp(cmd, "variables"))     result = handle_dap_variables(seq, args);
2785
    else if (!strcmp(cmd, "setBreakpoints"))result = handle_dap_set_breakpoints(seq, args);
2786
    else if (!strcmp(cmd, "continue"))      result = handle_dap_continue(seq, args);
2787
    else if (!strcmp(cmd, "next"))          result = handle_dap_next(seq, args);
2788
    else if (!strcmp(cmd, "stepIn"))        result = handle_dap_step_in(seq, args);
2789
    else if (!strcmp(cmd, "stepOut"))       result = handle_dap_step_out(seq, args);
2790
    else if (!strcmp(cmd, "pause"))         result = handle_dap_pause(seq, args);
2791
    else if (!strcmp(cmd, "disconnect"))    result = handle_dap_disconnect(seq, args);
2792
    else if (!strcmp(cmd, "restart"))       result = handle_dap_restart(seq, args);
2793
    else if (!strcmp(cmd, "gotoTargets"))   result = handle_dap_goto_targets(seq, args);
2794
    else if (!strcmp(cmd, "goto"))          result = handle_dap_goto(seq, args);
2795
    else if (!strcmp(cmd, "evaluate"))      result = handle_dap_evaluate(seq, args);
2796
    else if (!strcmp(cmd, "readMemory"))    result = handle_dap_read_memory(seq, args);
2797
    else if (!strcmp(cmd, "writeMemory"))   result = handle_dap_write_memory(seq, args);
2798
    else if (!strcmp(cmd, "x16/registers")) result = handle_x16_registers(seq, args);
2799
    else if (!strcmp(cmd, "x16/sendKey"))   result = handle_x16_send_key(seq, args);
2800
    else if (!strcmp(cmd, "x16/type"))      result = handle_x16_type(seq, args);
2801
    else if (!strcmp(cmd, "x16/joystick"))  result = handle_x16_joystick(seq, args);
2802
    else if (!strcmp(cmd, "x16/perfStats")) result = handle_x16_perf_stats(seq, args);
2803
    else if (!strcmp(cmd, "x16/perfConfig")) result = handle_x16_perf_config(seq, args);
2804
2805
    else if (!strcmp(cmd, "disassemble"))   result = handle_dap_disassemble(seq, args);
2806
    else if (!strcmp(cmd, "setVariable"))   result = handle_dap_set_variable(seq, args);
2807
    else if (!strcmp(cmd, "terminate"))     result = handle_dap_terminate(seq, args);
2808
    else if (!strcmp(cmd, "attach"))        result = handle_dap_attach(seq, args);
2809
    else if (!strcmp(cmd, "setInstructionBreakpoints")) result = handle_dap_set_instruction_breakpoints(seq, args);
2810
    else if (!strcmp(cmd, "dataBreakpointInfo")) result = handle_dap_data_breakpoint_info(seq, args);
2811
    else if (!strcmp(cmd, "setDataBreakpoints")) result = handle_dap_set_data_breakpoints(seq, args);
2812
    else if (!strcmp(cmd, "loadedSources")) result = handle_dap_loaded_sources(seq, args);
2813
    else if (!strcmp(cmd, "source"))        result = handle_dap_source(seq, args);
2814
    else if (!strcmp(cmd, "breakpointLocations")) result = handle_dap_breakpoint_locations(seq, args);
2815
    else if (!strcmp(cmd, "stepInTargets")) result = handle_dap_step_in_targets(seq, args);
2816
    else if (!strcmp(cmd, "setExceptionBreakpoints")) {
2817
        // VS always sends this — just acknowledge
2818
        send_dap_response(seq, "setExceptionBreakpoints", true, NULL);
2819
    }
2820
    else if (!strcmp(cmd, "setFunctionBreakpoints")) {
2821
        result = handle_dap_set_function_breakpoints(seq, args);
2822
    }
2823
    else {
2824
        fprintf(stderr, "[dap] Unhandled command: %s\n", cmd);
2825
        send_dap_error_response(seq, cmd, "command not supported");
2826
    }
2827
2828
    cJSON_Delete(root);
2829
    return result;
2830
}
2831
2832
// ─── Connection management ──────────────────────────────────────────
2833
2834
// Everything the session installed in the machine. Run before resuming, so a
2835
// breakpoint set by a client that has gone cannot stop a headless emulator with
2836
// nobody left to notice.
2837
static void dap_release_session_state(void) {
2838
    // Everything this session asked for, by name. Each entry goes only if no
2839
    // other owner still wants it, so a -bp breakpoint or one the user set with
2840
    // F9 at the same address survives -- without this file having to know that
2841
    // they exist.
2842
    debug_bp_clear_owner(DEBUG_OWNER_DAP_SOURCE);
2843
    debug_bp_clear_owner(DEBUG_OWNER_DAP_FUNCTION);
2844
    debug_bp_clear_owner(DEBUG_OWNER_DAP_INSTRUCTION);
2845
    debug_bp_clear_owner(DEBUG_OWNER_DAP_CONSOLE);
2846
    debug_wp_clear_owner(DEBUG_OWNER_DAP_SOURCE);
2847
    debug_wp_clear_owner(DEBUG_OWNER_DAP_CONSOLE);
2848
2849
    num_dap_bps   = 0;
2850
    num_func_bps  = 0;
2851
    num_instr_bps = 0;
2852
2853
    // A step-over or step-out THIS session started has a breakpoint of its own,
2854
    // and nothing else can retract it. One the local debugger started belongs to
2855
    // the user at the keyboard and is left alone; the debugger decides which is
2856
    // which, since it is the one holding the step.
2857
    DEBUGCancelStepFor(DEBUG_OWNER_DAP_SOURCE);
2858
2859
    dap_stop_on_entry = false;
2860
    dap_stop_announced = false;
2861
}
2862
2863
static void disconnect_client(void) {
2864
    if (client_sock != SOCKET_INVALID) {
2865
        CLOSE_SOCKET(client_sock);
2866
        client_sock = SOCKET_INVALID;
2867
        // Clear the contents, not just the length. A dropped oversized frame
2868
        // otherwise stayed in the buffer as a NUL-terminated string, and the
2869
        // next client -- accepted before it has written anything -- had that
2870
        // header re-parsed and was dropped on connect, for the life of the
2871
        // process.
2872
        recv_buf_len = 0;
2873
        recv_buf[0] = '\0';
2874
        // Everything the session installed goes before anything else, so a
2875
        // breakpoint it set cannot stop a machine it is no longer watching.
2876
        dap_release_session_state();
2877
        dap_seq = 1;
2878
        printf("[dap] Client disconnected\n");
2879
2880
        // Client dropped without sending a proper "disconnect" request. Never
2881
        // kill the emulator — just end the DAP session.
2882
        //
2883
        // Only auto-resume when there is NO interactive debugger UI: in that
2884
        // case a headless, DAP-only session would otherwise stay frozen forever
2885
        // with nothing able to resume it. When the ImGui/SDL debugger is up the
2886
        // user owns the run state (they may have deliberately paused, or be
2887
        // mid-step), so a stray client connect/disconnect must not resume the
2888
        // machine out from under them.
2889
        if (dap_session_active) {
2890
            dap_session_active = false;
2891
            // A UI that was asked for AND is still running: the guest can clear
2892
            // debugger_enabled by writing $9FB0, which suppresses the overlay,
2893
            // and leaving the machine stopped for a window nobody can see would
2894
            // strand it.
2895
            bool has_ui = dap_local_ui_owns_run_state();
2896
            if (currentMode == DMODE_STOP && !has_ui) {
2897
                printf("[dap] Session ended, resuming emulator\n");
2898
                currentMode = DMODE_RUN;
2899
            } else {
2900
                printf("[dap] Session ended (run state left unchanged)\n");
2901
            }
2902
        }
2903
    }
2904
}
2905
2906
static void try_accept(void) {
2907
    if (client_sock != SOCKET_INVALID) return;
2908
2909
    struct sockaddr_in client_addr;
2910
    int addr_len = sizeof(client_addr);
2911
#ifdef _WIN32
2912
    socket_t new_sock = accept(listen_sock, (struct sockaddr *)&client_addr, &addr_len);
2913
#else
2914
    socket_t new_sock = accept(listen_sock, (struct sockaddr *)&client_addr, (socklen_t *)&addr_len);
2915
#endif
2916
    if (new_sock == SOCKET_INVALID) return;
2917
2918
    set_nonblocking(new_sock);
2919
    client_sock = new_sock;
2920
    recv_buf_len = 0;
2921
    dap_seq = 1;
2922
    printf("[dap] Client connected from %s:%d\n",
2923
           inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
2924
}
2925
2926
// ─── DAP framing: Content-Length parser ─────────────────────────────
2927
2928
// Try to extract a complete DAP message from the receive buffer.
2929
// Copies the JSON body into `out` (NUL-terminated) and sets *consumed.
2930
// Returns false if no complete message is available yet.
2931
//
2932
// The body is copied rather than terminated in place. Writing the NUL at
2933
// header_size + content_length lands on the first byte of the NEXT message
2934
// whenever two arrive in one read -- which is the normal case, since a client
2935
// sends setBreakpoints/setFunctionBreakpoints/configurationDone as a burst
2936
// without waiting for each response. That byte then survived compaction as a
2937
// leading NUL, so every later strstr() for the header terminator saw an empty
2938
// string and the connection stopped dispatching until it overflowed.
2939
static bool try_extract_dap_message(char *out, size_t out_size, int *consumed) {
2940
    // Look for Content-Length header
2941
    char *header_end = strstr(recv_buf, "\r\n\r\n");
2942
    if (!header_end) return false;
2943
2944
    int header_size = (int)(header_end - recv_buf) + 4; // include \r\n\r\n
2945
2946
    // Parse Content-Length
2947
    int content_length = -1;
2948
    char *cl = recv_buf;
2949
    while (cl < header_end) {
2950
        if (strncmp(cl, "Content-Length:", 15) == 0) {
2951
            cl += 15;
2952
            while (*cl == ' ' || *cl == '\t') cl++;
2953
            content_length = atoi(cl);
2954
            break;
2955
        }
2956
        // Skip to next line
2957
        char *nl = strchr(cl, '\n');
2958
        if (!nl || nl >= header_end) break;
2959
        cl = nl + 1;
2960
    }
2961
2962
    if (content_length < 0) {
2963
        fprintf(stderr, "[dap] Missing Content-Length header\n");
2964
        *consumed = header_size;
2965
        return false;
2966
    }
2967
    if (content_length >= (int)out_size) {
2968
        // Too large to be one of ours. The body may not even have arrived yet,
2969
        // so consuming header + length here would skip bytes that are not in
2970
        // the buffer and then parse the tail of the body as a fresh header.
2971
        // There is no way to resynchronise on a stream we cannot buffer.
2972
        fprintf(stderr, "[dap] message of %d bytes is too large; dropping client\n",
2973
                content_length);
2974
        disconnect_client();
2975
        *consumed = 0;
2976
        return false;
2977
    }
2978
2979
    // Check if we have the complete body
2980
    if (recv_buf_len < header_size + content_length) return false;
2981
2982
    memcpy(out, recv_buf + header_size, (size_t)content_length);
2983
    out[content_length] = '\0';
2984
    *consumed = header_size + content_length;
2985
    return true;
2986
}
2987
2988
// ─── Public API ─────────────────────────────────────────────────────
2989
2990
int debug_server_init(int port) {
2991
    if (!platform_init()) return -1;
2992
2993
    listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
2994
    if (listen_sock == SOCKET_INVALID) {
2995
        fprintf(stderr, "[dap] Failed to create socket\n");
2996
        return -1;
2997
    }
2998
2999
    int optval = 1;
3000
    setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&optval, sizeof(optval));
3001
3002
    struct sockaddr_in addr;
3003
    memset(&addr, 0, sizeof(addr));
3004
    addr.sin_family = AF_INET;
3005
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
3006
    addr.sin_port = htons((uint16_t)port);
3007
3008
    if (bind(listen_sock, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
3009
        fprintf(stderr, "[dap] Failed to bind to port %d: error %d\n", port, SOCK_ERRNO);
3010
        CLOSE_SOCKET(listen_sock);
3011
        listen_sock = SOCKET_INVALID;
3012
        return -1;
3013
    }
3014
3015
    if (listen(listen_sock, 1) != 0) {
3016
        fprintf(stderr, "[dap] Failed to listen: error %d\n", SOCK_ERRNO);
3017
        CLOSE_SOCKET(listen_sock);
3018
        listen_sock = SOCKET_INVALID;
3019
        return -1;
3020
    }
3021
3022
    set_nonblocking(listen_sock);
3023
    server_enabled = true;
3024
    server_port = port;
3025
    printf("[dap] Debug Adapter Protocol server listening on 127.0.0.1:%d\n", port);
3026
    return 0;
3027
}
3028
3029
static void send_dap_event_thread_started(void) {
3030
    cJSON *tb = cJSON_CreateObject();
3031
    cJSON_AddStringToObject(tb, "reason", "started");
3032
    cJSON_AddNumberToObject(tb, "threadId", 1);
3033
    send_dap_event("thread", tb);
3034
}
3035
3036
int debug_server_poll(void) {
3037
    if (!server_enabled) return 0;
3038
3039
    try_accept();
3040
    if (client_sock == SOCKET_INVALID) return 0;
3041
3042
    // Push any budget overruns before reading: a client that never polls still
3043
    // learns that the guest is missing its frame.
3044
    perf_poll_overruns();
3045
3046
    // Receive data (non-blocking)
3047
    int space = RECV_BUF_SIZE - recv_buf_len - 2; // -2 for null terminator space
3048
    if (space <= 0) {
3049
        fprintf(stderr, "[dap] Receive buffer overflow, disconnecting\n");
3050
        disconnect_client();
3051
        return 0;
3052
    }
3053
3054
    int n = recv(client_sock, recv_buf + recv_buf_len, space, 0);
3055
    if (n > 0) {
3056
        recv_buf_len += n;
3057
        recv_buf[recv_buf_len] = '\0';
3058
    } else if (n == 0) {
3059
        disconnect_client();
3060
        return 0;
3061
    } else {
3062
        if (!WOULD_BLOCK) {
3063
            disconnect_client();
3064
            return 0;
3065
        }
3066
    }
3067
3068
    // Process all complete DAP messages in the buffer
3069
    int mode_changed = 0;
3070
    for (;;) {
3071
        static char body[RECV_BUF_SIZE];
3072
        int consumed = 0;
3073
        if (!try_extract_dap_message(body, sizeof body, &consumed)) {
3074
            if (consumed > 0) {
3075
                int remaining = recv_buf_len - consumed;
3076
                if (remaining > 0) memmove(recv_buf, recv_buf + consumed, remaining);
3077
                if (remaining < 0) remaining = 0;
3078
                recv_buf_len = remaining;
3079
                recv_buf[recv_buf_len] = '\0';
3080
                continue;
3081
            }
3082
            break;
3083
        }
3084
3085
        // Compact before dispatching. A handler can call back into the socket
3086
        // (an event, or an error response), and leaving a consumed message in
3087
        // the buffer would let it be seen twice.
3088
        int remaining = recv_buf_len - consumed;
3089
        if (remaining > 0) {
3090
            memmove(recv_buf, recv_buf + consumed, remaining);
3091
        }
3092
        recv_buf_len = remaining > 0 ? remaining : 0;
3093
        recv_buf[recv_buf_len] = '\0';
3094
3095
        if (dispatch_dap(body)) {
3096
            mode_changed = 1;
3097
        }
3098
3099
    }
3100
3101
    return mode_changed;
3102
}
3103
3104
void debug_server_note_resumed(void) {
3105
    // A resume the client did not ask for -- someone pressed F5 or F10 in the
3106
    // SDL debug window while a session was attached. It is still watching a
3107
    // machine it believes is halted, so tell it. Resumes that came from a DAP
3108
    // request have already sent their own event and cleared the flag.
3109
    if (dap_stop_announced && server_enabled && client_sock != SOCKET_INVALID) {
3110
        send_continued_event();
3111
        return;   // send_continued_event() clears the flag
3112
    }
3113
    dap_stop_announced = false;
3114
}
3115
3116
void debug_server_notify_stopped(const char *reason) {
3117
    if (!server_enabled || client_sock == SOCKET_INVALID) return;
3118
    dap_stop_announced = true;
3119
3120
    cJSON *body = cJSON_CreateObject();
3121
3122
    // Map our reason strings to DAP reason strings
3123
    if (!strcmp(reason, "breakpoint")) {
3124
        cJSON_AddStringToObject(body, "reason", "breakpoint");
3125
        // Find which DAP breakpoint was hit
3126
        cJSON *hit_ids = cJSON_CreateArray();
3127
        for (int i = 0; i < num_dap_bps; i++) {
3128
            // Both halves of the key, and only breakpoints that are actually
3129
            // armed: pending ones hold addr 0 and were reported as hit every
3130
            // time the machine stopped at $0000.
3131
            if (dap_bps[i].verified && dap_bps[i].addr == regs.pc &&
3132
                dap_bps[i].bank == regs.k) {
3133
                cJSON_AddItemToArray(hit_ids, cJSON_CreateNumber(dap_bps[i].dap_id));
3134
            }
3135
        }
3136
        if (cJSON_GetArraySize(hit_ids) > 0) {
3137
            cJSON_AddItemToObject(body, "hitBreakpointIds", hit_ids);
3138
        } else {
3139
            cJSON_Delete(hit_ids);
3140
        }
3141
    } else if (!strcmp(reason, "step")) {
3142
        cJSON_AddStringToObject(body, "reason", "step");
3143
    } else if (!strcmp(reason, "user")) {
3144
        cJSON_AddStringToObject(body, "reason", "pause");
3145
    } else {
3146
        cJSON_AddStringToObject(body, "reason", reason);
3147
    }
3148
3149
    cJSON_AddNumberToObject(body, "threadId", 1);
3150
    cJSON_AddBoolToObject(body, "allThreadsStopped", true);
3151
    send_dap_event("stopped", body);
3152
}
3153
3154
bool debug_server_has_client(void) {
3155
    return server_enabled && client_sock != SOCKET_INVALID;
3156
}
3157
3158
bool debug_server_is_enabled(void) {
3159
    return server_enabled;
3160
}
3161
3162
void debug_server_shutdown(void) {
3163
    if (client_sock != SOCKET_INVALID) {
3164
        // Signal the debuggee exited, then that the session terminated.
3165
        cJSON *eb = cJSON_CreateObject();
3166
        cJSON_AddNumberToObject(eb, "exitCode", 0);
3167
        send_dap_event("exited", eb);
3168
        send_dap_event("terminated", NULL);
3169
        CLOSE_SOCKET(client_sock);
3170
        client_sock = SOCKET_INVALID;
3171
    }
3172
    if (listen_sock != SOCKET_INVALID) {
3173
        CLOSE_SOCKET(listen_sock);
3174
        listen_sock = SOCKET_INVALID;
3175
    }
3176
    server_enabled = false;
3177
    dbg_info_free();
3178
    platform_cleanup();
3179
    printf("[dap] Shut down\n");
3180
}