OXYGENHUB
◆ rrdc-perf enhanced-x16-emulator / src / debug_ui / panels / cpu_panel.cpp
38.8 KiB · C++ · last changed chore(): initial commit 25 days ago source blame history raw
1
// CPU panel — live registers, status flags, stack, and address watches for the
2
// in-process ImGui debugger.
3
//
4
// Everything lives in the single "CPU" window (one CollapsingHeader per area)
5
// so the whole panel is contributed by this one already-registered .cpp.
6
#include <cstdint>
7
#include <cstdio>
8
#include <vector>
9
10
#include "imgui.h"
11
#include "debug_ui_panels.h"
12
#include "debug_ui_bridge.h" // extern "C" struct regs regs; debug_ui_read6502()
13
#include "debug_ui_widgets.h" // dbgui_hover_value_tooltip / dbgui_value_lines
14
#include "debug_ui_settings.h" // allow_breaking_changes
15
16
extern "C" {
17
#include "disasm.h" // decode whatever the native IRQ vector points at
18
}
19
20
namespace {
21
22
// Hex text-entry flags shared by every register/address input field.
23
const ImGuiInputTextFlags kHexFlags =
24
    ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase;
25
26
// ----------------------------------------------------------------------------
27
// CPU-mode helpers. The X16 boots a 65C02; the 65C816 adds a native mode
28
// (regs.e == 0) with selectable 8/16-bit accumulator (M flag) and index (X
29
// flag) widths. In emulation mode (regs.e == 1) it behaves like the 6502, so
30
// the widths are forced to 8-bit regardless of the stored M/X status bits.
31
// ----------------------------------------------------------------------------
32
bool cpu_native() { return regs.is65c816 && !regs.e; }
33
bool acc_is16()   { return cpu_native() && !(regs.status & FLAG_MEMORY_WIDTH); }
34
bool idx_is16()   { return cpu_native() && !(regs.status & FLAG_INDEX_WIDTH); }
35
36
// ----------------------------------------------------------------------------
37
// Native-mode interrupt vector probe.
38
//
39
// interrupt6502() picks the vector base from E: $FFF0 in emulation, $FFE0 in
40
// native (+ the vector number, so IRQ is $FFFE vs $FFEE). A ROM written for a
41
// 6502-class machine has no reason to populate the native slots, and the stock
42
// X16 ROM does not - $FFEE just contains whatever code bytes live at that
43
// offset. Clearing E and resuming therefore runs off into garbage at the next
44
// IRQ, which is 60 Hz here.
45
//
46
// Rather than assert that from memory, read it live and show the user what is
47
// actually there, disassembled, so the warning is about *their* ROM.
48
// ----------------------------------------------------------------------------
49
struct VectorProbe {
50
    uint16_t native_irq;      // word at $FFEE
51
    uint16_t emu_irq;         // word at $FFFE
52
    char     native_disasm[64]; // first instruction at native_irq
53
    bool     looks_absent;    // heuristic: native slot is not a real vector
54
};
55
56
uint16_t read_word(uint16_t addr)
57
{
58
    return (uint16_t)debug_ui_read6502(addr, 0, -1) |
59
           ((uint16_t)debug_ui_read6502((uint16_t)(addr + 1), 0, -1) << 8);
60
}
61
62
VectorProbe probe_vectors()
63
{
64
    VectorProbe p{};
65
    p.native_irq = read_word(0xFFEE);
66
    p.emu_irq    = read_word(0xFFFE);
67
68
    p.native_disasm[0] = '\0';
69
    // eff_addr is dereferenced unconditionally by disasm(), so it must be real.
70
    int32_t eff = -1;
71
    disasm(p.native_irq, 0, nullptr, p.native_disasm, sizeof p.native_disasm, -1,
72
           (uint8_t)(FLAG_MEMORY_WIDTH | FLAG_INDEX_WIDTH), &eff);
73
74
    // Heuristic, and labelled as one in the UI. On a machine whose ROM really
75
    // handles native interrupts the two vectors would be set up alike - the X16
76
    // points its emulation IRQ at a RAM vector ($038B). A native slot pointing
77
    // into ROM while the emulation one points at RAM means the native slot was
78
    // never written and we are just reading ROM code.
79
    const bool emu_in_ram    = p.emu_irq < 0xC000;
80
    const bool native_in_rom = p.native_irq >= 0xC000;
81
    p.looks_absent = (emu_in_ram && native_in_rom) || (p.native_irq == 0x0000) ||
82
                     (p.native_irq == 0xFFFF);
83
    return p;
84
}
85
86
// Switch the emulation flag the way XCE does, so the machine is left in a state
87
// the core agrees with. Poking regs.e alone is not enough: while E is set the
88
// CPU force-sets M and X on every instruction, and emulation mode also pins the
89
// stack to page 1 and the index high bytes to zero.
90
void set_emulation_flag(bool emulation)
91
{
92
    if (!regs.is65c816 || (regs.e != 0) == emulation) {
93
        return;
94
    }
95
    regs.e = emulation ? 1 : 0;
96
    if (emulation) {
97
        regs.status |= FLAG_MEMORY_WIDTH | FLAG_INDEX_WIDTH; // 8-bit A and X/Y
98
        regs.xh = 0;
99
        regs.yh = 0;
100
        regs.sp = (uint16_t)(0x0100 | (regs.sp & 0x00FF));   // stack lives in page 1
101
    }
102
    // Leaving emulation deliberately leaves M/X set: real hardware does the
103
    // same, so A/X/Y stay 8-bit until the program (or the user) clears them.
104
}
105
106
// Changing E out from under a running program is a genuinely destructive poke —
107
// it is not just a display mode. Emulation and native disagree about the
108
// interrupt vectors ($FFFE vs $FFEE), about how many bytes an interrupt pushes
109
// (native also pushes PB, so a mismatched RTI returns to a garbage address),
110
// about whether the stack is confined to page 1, and — once M is cleared —
111
// about instruction lengths. So: only offer it while the machine is paused, and
112
// make the user confirm the first time.
113
bool s_emu_confirm_open   = false;  // a confirmation is queued for this frame
114
bool s_emu_confirm_target = false;  // value the user asked for
115
bool s_emu_skip_confirm   = false;  // "don't ask again" for this session
116
117
void draw_emulation_toggle(const char *label)
118
{
119
    if (!regs.is65c816) {
120
        return;
121
    }
122
    const bool paused   = DEBUGIsPaused();
123
    const bool unlocked = debug_ui_settings().allow_breaking_changes;
124
    const bool editable = paused && unlocked;
125
126
    bool e = regs.e != 0;
127
    ImGui::BeginDisabled(!editable);
128
    if (ImGui::Checkbox(label, &e)) {
129
        if (s_emu_skip_confirm) {
130
            set_emulation_flag(e);
131
        } else {
132
            s_emu_confirm_open   = true;
133
            s_emu_confirm_target = e;
134
        }
135
    }
136
    ImGui::EndDisabled();
137
138
    if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
139
        if (editable) {
140
            ImGui::SetTooltip("E - Emulation flag\n"
141
                              "1 = 6502 emulation mode; 0 = 65C816 native.\n\n"
142
                              "Applies the same state changes as XCE. Editing is unlocked;\n"
143
                              "see the confirmation for what this will do to the ROM.");
144
        } else if (!unlocked) {
145
            ImGui::SetTooltip("E - Emulation flag (read-only)\n"
146
                              "1 = 6502 emulation mode; 0 = 65C816 native.\n\n"
147
                              "The running program owns this flag. Changing it moves interrupts\n"
148
                              "from $FFFE to $FFEE, and a 6502-era ROM has no handler there, so\n"
149
                              "the machine crashes at the next IRQ.\n\n"
150
                              "Enable System > Settings > Safety > \"Allow breaking\n"
151
                              "modifications\" if you are hand-driving the CPU.");
152
        } else {
153
            ImGui::SetTooltip("E - Emulation flag (pause to edit)\n"
154
                              "Switching CPU mode mid-stream breaks the running program's\n"
155
                              "stack frames and interrupt vectors.");
156
        }
157
    }
158
}
159
160
// Rendered once per frame from cpu_panel_render(), outside the PushID scopes of
161
// the two places that offer the toggle.
162
void draw_emulation_confirm_popup()
163
{
164
    static const char *kTitle = "Switch CPU mode?";
165
166
    if (s_emu_confirm_open) {
167
        ImGui::OpenPopup(kTitle);
168
        s_emu_confirm_open = false;
169
    }
170
171
    ImVec2 center = ImGui::GetMainViewport()->GetCenter();
172
    ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
173
    if (!ImGui::BeginPopupModal(kTitle, nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
174
        return;
175
    }
176
177
    const bool to_emulation = s_emu_confirm_target;
178
    ImGui::TextColored(ImVec4(0.95f, 0.70f, 0.30f, 1.0f),
179
                       "Switch to %s mode?", to_emulation ? "6502 emulation" : "65C816 native");
180
    ImGui::Separator();
181
182
    // Read the actual vectors out of the machine in front of the user, rather
183
    // than asserting what a ROM "usually" does.
184
    const VectorProbe vp = probe_vectors();
185
    if (!to_emulation) {
186
        ImGui::TextUnformatted("Interrupts vector by mode. On this machine, right now:");
187
        ImGui::Spacing();
188
        if (ImGui::BeginTable("vec_tbl", 3,
189
                              ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
190
                                  ImGuiTableFlags_SizingFixedFit)) {
191
            ImGui::TableSetupColumn("Mode");
192
            ImGui::TableSetupColumn("IRQ vector");
193
            ImGui::TableSetupColumn("Points to");
194
            ImGui::TableHeadersRow();
195
196
            ImGui::TableNextRow();
197
            ImGui::TableNextColumn(); ImGui::TextUnformatted("Emulation (now)");
198
            ImGui::TableNextColumn(); ImGui::TextUnformatted("$FFFE");
199
            ImGui::TableNextColumn(); ImGui::Text("$%04X", vp.emu_irq);
200
201
            ImGui::TableNextRow();
202
            ImGui::TableNextColumn();
203
            ImGui::TextColored(ImVec4(1.00f, 0.45f, 0.45f, 1.0f), "Native (after)");
204
            ImGui::TableNextColumn();
205
            ImGui::TextColored(ImVec4(1.00f, 0.45f, 0.45f, 1.0f), "$FFEE");
206
            ImGui::TableNextColumn();
207
            ImGui::TextColored(ImVec4(1.00f, 0.45f, 0.45f, 1.0f), "$%04X  %s",
208
                               vp.native_irq, vp.native_disasm);
209
            ImGui::EndTable();
210
        }
211
        ImGui::Spacing();
212
        if (vp.looks_absent) {
213
            ImGui::TextColored(ImVec4(1.00f, 0.45f, 0.45f, 1.0f),
214
                               "This ROM does not appear to provide a native IRQ handler.");
215
            ImGui::TextUnformatted(
216
                "$FFEE holds ROM code, not a vector, so the next IRQ - 60 times a\n"
217
                "second - will execute whatever is at the address above. Expect the\n"
218
                "machine to crash to the monitor almost immediately after resuming.");
219
            ImGui::TextDisabled("(Judged by the emulation vector pointing at RAM while the native\n"
220
                                "one points into ROM. Check the addresses yourself above.)");
221
        } else {
222
            ImGui::TextUnformatted("The native vector looks populated, but verify it is really a\n"
223
                                   "handler before resuming.");
224
        }
225
        ImGui::Spacing();
226
        ImGui::TextUnformatted("Also note:");
227
    } else {
228
        ImGui::TextUnformatted(
229
            "This changes the CPU out from under the running program. Unless it was\n"
230
            "about to make the same switch itself, expect it to break:");
231
    }
232
    ImGui::Bullet();
233
    ImGui::TextUnformatted("Native interrupts also push the program bank, so an RTI that\n"
234
                           "doesn't match its entry mode returns to a garbage address.");
235
    ImGui::Bullet();
236
    ImGui::TextUnformatted("Emulation confines the stack to page 1; native does not.");
237
    ImGui::Bullet();
238
    ImGui::TextUnformatted("In native, clearing M changes instruction lengths, so the PC\n"
239
                           "stream desyncs from the code that was assembled for 8-bit.");
240
    ImGui::Separator();
241
    ImGui::TextDisabled("Fine for experiments and for driving the CPU by hand; not something\n"
242
                        "a live program will survive.");
243
    ImGui::Spacing();
244
    ImGui::Checkbox("Don't ask again this session", &s_emu_skip_confirm);
245
    ImGui::Spacing();
246
247
    if (ImGui::Button("Switch anyway", ImVec2(130, 0))) {
248
        set_emulation_flag(to_emulation);
249
        ImGui::CloseCurrentPopup();
250
    }
251
    ImGui::SameLine();
252
    if (ImGui::Button("Cancel", ImVec2(90, 0))) {
253
        s_emu_skip_confirm = false;  // cancelling shouldn't arm the skip
254
        ImGui::CloseCurrentPopup();
255
    }
256
    ImGui::EndPopup();
257
}
258
259
// Direct-page base + offset, matching the CPU's direct_page_add() (support.h).
260
// The X16 r0-r15 pseudo-registers live at D+$02..D+$21; D is 0 under the KERNAL
261
// ABI but can be non-zero in 65C816 native (GS) code, so honor it.
262
uint16_t dp_add(uint16_t offset)
263
{
264
    if (regs.e && (regs.dp & 0x00FF) == 0)
265
        return (uint16_t)((regs.dp & 0xFF00) | (uint8_t)((uint8_t)(regs.dp & 0xFF) + (offset & 0xFF)));
266
    return (uint16_t)(regs.dp + offset);
267
}
268
269
char ascii_of(uint8_t b) { return (b >= 0x20 && b < 0x7f) ? (char)b : '.'; }
270
271
// Value of a single hex digit, or -1 if the character isn't one.
272
int hex_digit(char c)
273
{
274
    if (c >= '0' && c <= '9') return c - '0';
275
    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
276
    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
277
    return -1;
278
}
279
280
// Width (in pixels) sized to hold `digits` hex characters plus a little padding.
281
float hex_width(int digits)
282
{
283
    return ImGui::CalcTextSize("F").x * (float)(digits + 1) +
284
           ImGui::GetStyle().FramePadding.x * 2.0f + 4.0f;
285
}
286
287
bool reg_u8(const char *label, uint8_t *p, const char *desc = nullptr)
288
{
289
    ImGui::SetNextItemWidth(hex_width(2));
290
    bool changed = ImGui::InputScalar(label, ImGuiDataType_U8, p, nullptr, nullptr, "%02X", kHexFlags);
291
    dbgui_hover_value_tooltip(desc, *p, 1);
292
    return changed;
293
}
294
295
bool reg_u16(const char *label, uint16_t *p, const char *desc = nullptr)
296
{
297
    ImGui::SetNextItemWidth(hex_width(4));
298
    bool changed = ImGui::InputScalar(label, ImGuiDataType_U16, p, nullptr, nullptr, "%04X", kHexFlags);
299
    dbgui_hover_value_tooltip(desc, *p, 2);
300
    return changed;
301
}
302
303
// A single P-status flag rendered as a checkbox that reads/writes regs.status.
304
// `label` carries a "##id" suffix so single-letter flags keep unique ImGui IDs.
305
// `name`/`desc` drive a hover tooltip that explains what the flag means.
306
void flag_checkbox(const char *label, uint8_t mask, const char *name, const char *desc)
307
{
308
    bool v = (regs.status & mask) != 0;
309
    if (ImGui::Checkbox(label, &v)) {
310
        if (v) {
311
            regs.status |= mask;
312
        } else {
313
            regs.status = (uint8_t)(regs.status & ~mask);
314
        }
315
    }
316
    if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
317
        int bit = 0;
318
        for (uint8_t m = mask; m > 1; m >>= 1) {
319
            bit++;
320
        }
321
        ImGui::BeginTooltip();
322
        ImGui::Text("%s  (bit %d, mask $%02X)", name, bit, mask);
323
        ImGui::Separator();
324
        ImGui::TextUnformatted(desc);
325
        ImGui::TextDisabled("current value: %d", (regs.status & mask) ? 1 : 0);
326
        ImGui::EndTooltip();
327
    }
328
}
329
330
// ----------------------------------------------------------------------------
331
// Registers
332
// ----------------------------------------------------------------------------
333
void draw_registers()
334
{
335
    if (!ImGui::CollapsingHeader("Registers", ImGuiTreeNodeFlags_DefaultOpen)) {
336
        return;
337
    }
338
    ImGui::PushID("registers");
339
340
    const bool is816  = regs.is65c816;
341
    const bool native = cpu_native();
342
343
    const char *mode = !is816 ? "65C02 (6502)"
344
                              : (regs.e ? "65C816 (emulation)" : "65C816 (native)");
345
    ImGui::Text("Mode: %s", mode);
346
    // Everything about *why* the register set looks the way it does lives here,
347
    // rather than as a row of grey annotations next to the fields.
348
    if (is816) {
349
        ImGui::SetItemTooltip(
350
            "M=%d  X=%d  E=%d\n\n"
351
            "The register set follows the CPU mode.\n"
352
            "%s\n\n"
353
            "M (P bit 5) picks the accumulator width, X (P bit 4) the index\n"
354
            "width; 1 = 8-bit. DP, DB and K are live in both modes.",
355
            (regs.status >> 5) & 1, (regs.status >> 4) & 1, regs.e ? 1 : 0,
356
            regs.e ? "Emulation mode re-asserts M and X before every instruction, so\n"
357
                     "A/X/Y are 8-bit and cannot be widened while E is set. The program\n"
358
                     "leaves emulation mode with XCE."
359
                   : "Native mode does not widen the registers by itself - exactly as on\n"
360
                     "hardware, the program selects the widths with REP/SEP.");
361
    } else {
362
        ImGui::SetItemTooltip("A/X/Y are 8-bit. The 65C816-only registers (C, B, DP, DB, K, E)\n"
363
                              "do not exist on the 65C02 and are not shown.");
364
    }
365
    ImGui::Separator();
366
367
    reg_u16("PC##rPC", &regs.pc, "PC — Program Counter\nAddress of the next instruction to execute.");
368
    // SP alongside PC rather than on its own row: it is one field and the two
369
    // are read together.
370
    ImGui::SameLine();
371
    {
372
        // regs.sp is the full 16-bit stack address in every mode - push8() does
373
        // write6502(regs.sp, ...) directly - and incsp()/decsp() only ever touch
374
        // the low byte while E is set. So outside 65C816 native the high byte
375
        // MUST stay $01, or an edit here would quietly move the stack into zero
376
        // page and the CPU would keep it there.
377
        uint16_t sp_disp = (uint16_t)(regs.sp | (is816 ? 0 : 0x0100));
378
        if (reg_u16("SP##rSP", &sp_disp,
379
                    native ? "SP - Stack Pointer (16-bit)\n65C816 native: the stack can live anywhere."
380
                           : "SP - Stack Pointer\nConfined to page 1 ($01xx): pushes and pulls wrap inside\n"
381
                             "the page. The high byte is held at $01 for you.")) {
382
            regs.sp = native ? sp_disp : (uint16_t)(0x0100 | (sp_disp & 0x00FF));
383
        }
384
    }
385
386
    // Accumulator, ordered A B C: the 8-bit accumulator, its hidden high byte,
387
    // then the two combined. All three are the same storage (LOW_HIGH_UNION), so
388
    // none of them is ever stale or read-only.
389
    //
390
    // C is deliberately shown in emulation mode too. 16-bit *arithmetic* needs
391
    // M=0 and so is native-only, but XBA, TCD, TDC, TCS and TSC all move the
392
    // full 16 bits with no M or E check (verified in instructions.h), so C is
393
    // live state in emulation as well.
394
    if (is816) {
395
        reg_u8("A##rA", &regs.a,
396
               "A - accumulator, low 8 bits of C.\n"
397
               "The whole accumulator when M selects 8-bit.");
398
        ImGui::SameLine();
399
        reg_u8("B##rB", &regs.b,
400
               "B - accumulator high byte.\n"
401
               "XBA swaps it with A - which works in emulation mode too, so B is\n"
402
               "real state whatever the accumulator width is.");
403
        ImGui::SameLine();
404
        char cdesc[288];
405
        snprintf(cdesc, sizeof cdesc,
406
                 "C - the full 16-bit accumulator, B:A = $%02X%02X\n\n"
407
                 "%s\n\n"
408
                 "Not native-only: XBA, TCD, TDC, TCS and TSC move all 16 bits\n"
409
                 "regardless of M, and work in emulation mode.",
410
                 regs.b, regs.a,
411
                 acc_is16() ? "M selects 16-bit, so C is the arithmetic accumulator."
412
                            : "16-bit arithmetic (LDA, ADC, ...) needs M=0, which needs\n"
413
                              "native mode, so right now arithmetic uses A alone.");
414
        reg_u16("C##rC", &regs.c, cdesc);
415
    } else {
416
        reg_u8("A##rA", &regs.a, "A - accumulator (8-bit)");
417
    }
418
419
    // Index registers X/Y widen with the X flag (16-bit in native when X=0).
420
    if (idx_is16()) {
421
        reg_u16("X##rX", &regs.x, "X — index register (16-bit)");
422
        ImGui::SameLine();
423
        reg_u16("Y##rY", &regs.y, "Y — index register (16-bit)");
424
    } else {
425
        reg_u8("X##rX", &regs.xl, "X — index register (8-bit)");
426
        ImGui::SameLine();
427
        reg_u8("Y##rY", &regs.yl, "Y — index register (8-bit)");
428
    }
429
430
    // Width is visible from the M/X checkboxes and explained on the Mode line, so
431
    // don't repeat it as a grey annotation. The one thing that needs a control is
432
    // the manual widen, and only when it is both possible and unlocked.
433
    if (is816 && native && (!acc_is16() || !idx_is16()) &&
434
        debug_ui_settings().allow_breaking_changes) {
435
        if (ImGui::SmallButton("Widen now (REP #$30)")) {
436
            regs.status &= (uint8_t)~(FLAG_MEMORY_WIDTH | FLAG_INDEX_WIDTH);
437
        }
438
        ImGui::SetItemTooltip(
439
            "Clear M and X, the same thing REP #$30 does, to give 16-bit\n"
440
            "A/X/Y. Real hardware only widens when the program asks;\n"
441
            "this is a manual poke, so the running code may not expect it.\n\n"
442
            "It also only sticks while E stays clear: setting E re-asserts\n"
443
            "M and X before every instruction.");
444
    }
445
446
    // 65C816-only registers appear only in 65C816 modes (not shown at all on the
447
    // 65C02). DP/DB/K/E all exist in emulation as well as native: emulation
448
    // narrows the register widths, it does not switch these off.
449
    if (is816) {
450
        ImGui::Separator();
451
        // Direct Page is live in BOTH modes - direct_page_add() reads regs.dp
452
        // either way. Emulation only changes how the offset wraps. Hiding it in
453
        // emulation would hide something that is actively moving the X16's
454
        // virtual registers R0-R15, which are direct-page relative.
455
        reg_u16("DP##rDP", &regs.dp,
456
                native ? "DP - Direct Page register\nBase of the direct (zero) page."
457
                       : "DP - Direct Page register\nBase of the direct (zero) page. Still live in emulation\n"
458
                         "mode: when its low byte is 0 the offset wraps inside the page\n"
459
                         "(6502 behaviour), otherwise it adds normally.");
460
        ImGui::SameLine();
461
        reg_u8("DB##rDB", &regs.db,
462
               "DB - Data Bank register\nHigh byte (bank) for data memory accesses.");
463
        ImGui::SameLine();
464
        reg_u8("K##rK", &regs.k,
465
               "K - Program Bank register\nHigh byte (bank) of the 24-bit program counter.");
466
    }
467
468
    ImGui::PopID();
469
}
470
// ----------------------------------------------------------------------------
471
// Virtual registers R0-R15 — the X16's sixteen 16-bit pseudo-registers, a
472
// software convention shadowed in zero page ($02-$21, little-endian) and used
473
// throughout the KERNAL / VERA / GRAPH APIs (and by cc65). Shown like CPU
474
// registers so they can be watched and edited live as 16-bit values.
475
// ----------------------------------------------------------------------------
476
void draw_vregs()
477
{
478
    if (!ImGui::CollapsingHeader("Virtual Regs (R0-R15)")) {
479
        return;
480
    }
481
    ImGui::PushID("vregs");
482
    ImGui::TextDisabled("X16 16-bit pseudo-registers (direct-page +$02-$21, little-endian)");
483
484
    for (int n = 0; n < 16; n++) {
485
        uint16_t addr = dp_add((uint16_t)(0x02 + 2 * n));
486
        uint8_t  lo   = debug_ui_read6502(addr, 0, DEBUG_UI_CURRENT_BANK);
487
        uint8_t  hi   = debug_ui_read6502((uint16_t)(addr + 1), 0, DEBUG_UI_CURRENT_BANK);
488
        uint16_t val  = (uint16_t)(lo | (hi << 8));
489
490
        char label[16];
491
        snprintf(label, sizeof label, "R%-2d##vr%d", n, n);
492
        ImGui::SetNextItemWidth(hex_width(4));
493
        if (ImGui::InputScalar(label, ImGuiDataType_U16, &val, nullptr, nullptr, "%04X", kHexFlags)) {
494
            debug_ui_write6502(addr, (uint8_t)(val & 0xFF), 0, DEBUG_UI_CURRENT_BANK);
495
            debug_ui_write6502((uint16_t)(addr + 1), (uint8_t)(val >> 8), 0, DEBUG_UI_CURRENT_BANK);
496
        }
497
        char desc[112];
498
        snprintf(desc, sizeof desc,
499
                 "R%d - X16 virtual register\n@ $%04X/$%04X (direct-page relative, little-endian)",
500
                 n, addr, (uint16_t)(addr + 1));
501
        dbgui_hover_value_tooltip(desc, val, 2);
502
503
        if (n % 2 == 0) {
504
            ImGui::SameLine(0.0f, 24.0f); // two registers per row
505
        }
506
    }
507
    ImGui::PopID();
508
}
509
510
// ----------------------------------------------------------------------------
511
// Status flags
512
// ----------------------------------------------------------------------------
513
void draw_flags()
514
{
515
    if (!ImGui::CollapsingHeader("Status Flags", ImGuiTreeNodeFlags_DefaultOpen)) {
516
        return;
517
    }
518
    ImGui::PushID("flags");
519
520
    ImGui::Text("P = $%02X", regs.status);
521
    {
522
        char bits[48];
523
        dbgui_format_binary(bits, sizeof bits, regs.status, 8);
524
        ImGui::SetItemTooltip(
525
            "P - Processor status register\n"
526
            "$%02X = %s\n"
527
            "Bit 7..0: %s\n\n"
528
            "Each bit is editable as a checkbox below. On the 65C816 bits 5 and 4\n"
529
            "are the M and X width selects; on the 65C02 they are the unused and\n"
530
            "Break bits, which is why the layout changes with the CPU.",
531
            regs.status, bits,
532
            regs.is65c816 ? "N V M X D I Z C" : "N V - B D I Z C");
533
    }
534
535
    flag_checkbox("N##fN", FLAG_SIGN, "N — Negative",
536
                  "Set when bit 7 of the result is set (result looks negative).");
537
    ImGui::SameLine();
538
    flag_checkbox("V##fV", FLAG_OVERFLOW, "V — Overflow",
539
                  "Set on signed arithmetic overflow (ADC/SBC), or by BIT/CLV.");
540
    ImGui::SameLine();
541
    if (regs.is65c816) {
542
        // While E is set the CPU re-forces M and X to 1 before every
543
        // instruction, so editing them here would silently revert. Disable them
544
        // instead of pretending the edit took.
545
        const bool widths_locked = regs.e != 0;
546
        ImGui::BeginDisabled(widths_locked);
547
        flag_checkbox("M##fM", FLAG_MEMORY_WIDTH, "M — Memory/Accumulator width",
548
                      "65C816 native only. 0 = 16-bit accumulator/memory, 1 = 8-bit.");
549
        ImGui::SameLine();
550
        flag_checkbox("X##fX", FLAG_INDEX_WIDTH, "X — Index register width",
551
                      "65C816 native only. 0 = 16-bit X/Y, 1 = 8-bit. Forced 1 in emulation.");
552
        ImGui::EndDisabled();
553
        if (widths_locked && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
554
            ImGui::SetTooltip("Forced to 1 while E (emulation) is set — the CPU rewrites\n"
555
                              "both flags before every instruction. Clear E to change them.");
556
        }
557
        ImGui::SameLine();
558
    } else {
559
        flag_checkbox("-##fConst", FLAG_CONSTANT, "- — Unused",
560
                      "Unused on the 65C02; conventionally reads as 1.");
561
        ImGui::SameLine();
562
        flag_checkbox("B##fBrk", FLAG_BREAK, "B — Break",
563
                      "Distinguishes a BRK/PHP push from an IRQ/NMI push on the stack.");
564
        ImGui::SameLine();
565
    }
566
    flag_checkbox("D##fD", FLAG_DECIMAL, "D — Decimal",
567
                  "Enables BCD (binary-coded decimal) mode for ADC/SBC.");
568
    ImGui::SameLine();
569
    flag_checkbox("I##fI", FLAG_INTERRUPT, "I — IRQ disable",
570
                  "1 masks maskable IRQs; NMIs are unaffected.");
571
    ImGui::SameLine();
572
    flag_checkbox("Z##fZ", FLAG_ZERO, "Z — Zero",
573
                  "Set when the result of an operation is zero.");
574
    ImGui::SameLine();
575
    flag_checkbox("C##fC", FLAG_CARRY, "C — Carry",
576
                  "Carry/borrow out of bit 7 (bit 15 in 16-bit), and the bit shifted by shifts/rotates.");
577
578
    if (regs.is65c816) {
579
        draw_emulation_toggle("E  (Emulation)##fE");
580
    }
581
582
    ImGui::PopID();
583
}
584
585
// ----------------------------------------------------------------------------
586
// Stack
587
// ----------------------------------------------------------------------------
588
void draw_stack()
589
{
590
    if (!ImGui::CollapsingHeader("Stack", ImGuiTreeNodeFlags_DefaultOpen)) {
591
        return;
592
    }
593
    ImGui::PushID("stack");
594
595
    // On the 6502/65C02 the SP is an 8-bit offset into page 1; on the 65C816 it
596
    // is already the full 16-bit stack address.
597
    uint16_t sp_actual = (uint16_t)(regs.sp | (regs.is65c816 ? 0 : 0x100));
598
    uint16_t sp_top    = (uint16_t)(sp_actual + 1); // most recently pushed byte
599
    bool page1_stack   = !cpu_native();             // 6502/emulation stay in page 1
600
601
    ImGui::Text("SP = $%04X   top-of-stack = $%04X", sp_actual, sp_top);
602
603
    static int rows = 16;
604
    ImGui::SetNextItemWidth(160.0f);
605
    ImGui::SliderInt("rows", &rows, 4, 64);
606
607
    const ImVec4 hi(1.0f, 0.85f, 0.35f, 1.0f);
608
    float line = ImGui::GetTextLineHeightWithSpacing();
609
    int visible = rows < 16 ? rows : 16;
610
611
    if (ImGui::BeginChild("stack_scroll", ImVec2(0.0f, line * (visible + 1.5f)), true)) {
612
        if (ImGui::BeginTable("stack_tbl", 3,
613
                              ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
614
                                  DBGUI_TABLE_FLAGS_RESIZABLE)) {
615
            ImGui::TableSetupColumn("Addr");
616
            ImGui::TableSetupColumn("Byte");
617
            ImGui::TableSetupColumn("ASCII");
618
            ImGui::TableHeadersRow();
619
620
            for (int i = 0; i < rows; i++) {
621
                uint32_t addr = (uint32_t)sp_top + (uint32_t)i;
622
                if (page1_stack && addr > 0x01FF) {
623
                    break; // used stack cannot extend past $01FF in page 1
624
                }
625
                uint16_t a16  = (uint16_t)addr;
626
                uint8_t  byte = debug_ui_read6502(a16, 0, DEBUG_UI_CURRENT_BANK);
627
                bool     top  = (i == 0);
628
629
                ImGui::TableNextRow();
630
                ImGui::TableSetColumnIndex(0);
631
                if (top) {
632
                    ImGui::TextColored(hi, "> $%04X", a16);
633
                } else {
634
                    ImGui::Text("  $%04X", a16);
635
                }
636
                ImGui::TableSetColumnIndex(1);
637
                if (top) {
638
                    ImGui::TextColored(hi, "%02X", byte);
639
                } else {
640
                    ImGui::Text("%02X", byte);
641
                }
642
                ImGui::TableSetColumnIndex(2);
643
                if (top) {
644
                    ImGui::TextColored(hi, "%c", ascii_of(byte));
645
                } else {
646
                    ImGui::Text("%c", ascii_of(byte));
647
                }
648
            }
649
            ImGui::EndTable();
650
        }
651
    }
652
    ImGui::EndChild();
653
654
    ImGui::PopID();
655
}
656
657
// ----------------------------------------------------------------------------
658
// Interrupts — whether we are inside a handler, and how far away the next
659
// interrupt is. Stepping through code gives no clue that an interrupt is about
660
// to fire, so surface the prediction next to the registers.
661
// ----------------------------------------------------------------------------
662
void draw_interrupts()
663
{
664
    if (!ImGui::CollapsingHeader("Interrupts", ImGuiTreeNodeFlags_DefaultOpen)) {
665
        return;
666
    }
667
    ImGui::PushID("irq");
668
669
    if (cpu_in_interrupt()) {
670
        const int  vec  = cpu_irq_last_vector();
671
        const char *what = vec == 0x0A ? "NMI" : (vec == 0x06 ? "BRK" : (vec == 0x04 ? "COP" : "IRQ"));
672
        ImGui::TextColored(ImVec4(1.00f, 0.45f, 0.45f, 1.0f),
673
                           "In %s handler (depth %d)", what, cpu_irq_depth());
674
        ImGui::SameLine();
675
        ImGui::TextDisabled("returns to $%04X", (unsigned)cpu_irq_return_pc());
676
    } else {
677
        ImGui::TextDisabled("Not in an interrupt");
678
    }
679
680
    // VERA interrupt state + prediction. IEN/ISR tell us what is armed and
681
    // pending; video_next_irq() converts the scan position into a countdown.
682
    uint8_t  ien = 0, isr = 0;
683
    uint16_t irqline = 0;
684
    video_get_irq_state(&ien, &isr, &irqline);
685
686
    ImGui::Text("VERA IEN $%02X", ien);
687
    ImGui::SameLine();
688
    ImGui::TextDisabled("[%s%s%s%s]",
689
                        (ien & 1) ? "VSYNC " : "", (ien & 2) ? "LINE " : "",
690
                        (ien & 4) ? "SPRCOL " : "", (ien & 8) ? "AFLOW" : "");
691
    ImGui::SameLine();
692
    ImGui::Text("ISR $%02X", isr);
693
    if (ien & 2) {
694
        ImGui::SameLine();
695
        ImGui::TextDisabled("line=%u", (unsigned)irqline);
696
    }
697
698
    uint32_t cycles = 0;
699
    uint8_t  source = 0;
700
    if (video_next_irq((float)MHZ, &cycles, &source)) {
701
        // Convert to an instruction estimate too — when stepping, "how many
702
        // more steps" is the useful unit. ~3.5 cycles/instruction is a decent
703
        // average for 6502-family code; label it as approximate.
704
        const unsigned insns = (unsigned)(cycles / 4);
705
        ImGui::Text("Next: %s in %u cyc", source == 1 ? "VSYNC" : "LINE", (unsigned)cycles);
706
        ImGui::SameLine();
707
        ImGui::TextDisabled("(~%u instructions)", insns);
708
        if (ImGui::IsItemHovered()) {
709
            ImGui::SetTooltip("Cycles until the next enabled VERA interrupt, from the\n"
710
                              "current raster position. The instruction figure is a rough\n"
711
                              "estimate at ~4 cycles each.");
712
        }
713
    } else if (regs.status & FLAG_INTERRUPT) {
714
        ImGui::TextDisabled("No VERA interrupt armed (and I flag masks IRQs)");
715
    } else {
716
        ImGui::TextDisabled("No VERA interrupt armed");
717
    }
718
719
    if (regs.status & FLAG_INTERRUPT) {
720
        ImGui::SameLine();
721
        ImGui::TextColored(ImVec4(0.95f, 0.70f, 0.30f, 1.0f), "I");
722
        if (ImGui::IsItemHovered()) {
723
            ImGui::SetTooltip("The I flag is set: maskable IRQs are blocked right now.");
724
        }
725
    }
726
727
    ImGui::TextDisabled("%u interrupts taken", (unsigned)cpu_irq_count());
728
729
    ImGui::PopID();
730
}
731
732
// ----------------------------------------------------------------------------
733
// Watch list — user-managed address watches (address-based; symbolic evaluation
734
// is the DAP server's job, out of scope here).
735
// ----------------------------------------------------------------------------
736
struct WatchEntry {
737
    uint16_t addr = 0x0000;
738
    int      len  = 1;   // bytes to show, 1..16
739
    int      bank = -1;  // x16 RAM/ROM bank, -1 == current
740
};
741
742
// The list outlives the collapsing header so a request that arrives while the
743
// header is closed is still honoured (and pops the header open to show it).
744
std::vector<WatchEntry> g_watch_entries;
745
746
void draw_watch()
747
{
748
    // "Add to watch" from the memory view's context menu, possibly a range.
749
    bool added = false;
750
    {
751
        uint16_t waddr = 0;
752
        int16_t  wbank = -1;
753
        uint16_t wlen  = 1;
754
        if (debug_ui_take_watch_request(&waddr, &wbank, &wlen)) {
755
            // A watch row shows at most 16 bytes, so a longer range becomes
756
            // several consecutive rows rather than being silently truncated.
757
            uint16_t left = wlen ? wlen : 1;
758
            uint16_t at   = waddr;
759
            while (left > 0 && g_watch_entries.size() < 64) {
760
                WatchEntry w;
761
                w.addr = at;
762
                w.len  = left > 16 ? 16 : (int)left;
763
                w.bank = wbank;
764
                g_watch_entries.push_back(w);
765
                at   = (uint16_t)(at + w.len);
766
                left = (uint16_t)(left - w.len);
767
            }
768
            added = true;
769
        }
770
    }
771
    if (added) {
772
        ImGui::SetNextItemOpen(true); // reveal what was just added
773
    }
774
775
    if (!ImGui::CollapsingHeader("Watch", ImGuiTreeNodeFlags_DefaultOpen)) {
776
        return;
777
    }
778
    ImGui::PushID("watch");
779
780
    std::vector<WatchEntry> &entries = g_watch_entries;
781
782
    if (ImGui::Button("+ Add")) {
783
        entries.push_back(WatchEntry());
784
    }
785
    ImGui::SameLine();
786
    ImGui::TextDisabled("address-based watches (bank -1 = current)");
787
788
    if (ImGui::BeginTable("watch_tbl", 6,
789
                          ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders |
790
                              ImGuiTableFlags_SizingFixedFit | DBGUI_TABLE_FLAGS_RESIZABLE)) {
791
        ImGui::TableSetupColumn("");
792
        ImGui::TableSetupColumn("Addr");
793
        ImGui::TableSetupColumn("Len");
794
        ImGui::TableSetupColumn("Bank");
795
        ImGui::TableSetupColumn("Hex");
796
        ImGui::TableSetupColumn("Dec / ASCII");
797
        ImGui::TableHeadersRow();
798
799
        int remove = -1;
800
        for (int i = 0; i < (int)entries.size(); i++) {
801
            ImGui::PushID(i);
802
            WatchEntry &w = entries[i];
803
804
            ImGui::TableNextRow();
805
806
            ImGui::TableSetColumnIndex(0);
807
            if (ImGui::SmallButton("x")) {
808
                remove = i;
809
            }
810
811
            ImGui::TableSetColumnIndex(1);
812
            ImGui::SetNextItemWidth(hex_width(4));
813
            ImGui::InputScalar("##addr", ImGuiDataType_U16, &w.addr, nullptr, nullptr,
814
                               "%04X", kHexFlags);
815
816
            ImGui::TableSetColumnIndex(2);
817
            ImGui::SetNextItemWidth(hex_width(3));
818
            ImGui::InputInt("##len", &w.len, 0);
819
            if (w.len < 1) w.len = 1;
820
            if (w.len > 16) w.len = 16;
821
822
            ImGui::TableSetColumnIndex(3);
823
            ImGui::SetNextItemWidth(hex_width(3));
824
            ImGui::InputInt("##bank", &w.bank, 0);
825
            if (w.bank < -1) w.bank = -1;
826
            if (w.bank > 255) w.bank = 255;
827
828
            uint8_t bytes[16];
829
            for (int j = 0; j < w.len; j++) {
830
                bytes[j] = debug_ui_read6502((uint16_t)(w.addr + j), 0, (int16_t)w.bank);
831
            }
832
833
            ImGui::TableSetColumnIndex(4);
834
            // Editable: type new hex bytes and the whole span is written back
835
            // through the normal write path (I/O side effects, watchpoints),
836
            // honouring this row's bank. Only commit on Enter/focus-loss so
837
            // half-typed input is never written.
838
            char hexbuf[16 * 3 + 1];
839
            int  hp = 0;
840
            for (int j = 0; j < w.len; j++) {
841
                hp += snprintf(hexbuf + hp, sizeof(hexbuf) - hp, "%02X ", bytes[j]);
842
            }
843
            if (hp > 0 && hexbuf[hp - 1] == ' ') {
844
                hexbuf[hp - 1] = '\0'; // drop the trailing separator while editing
845
            }
846
            ImGui::SetNextItemWidth(hex_width(w.len * 3 + 1));
847
            if (ImGui::InputText("##hex", hexbuf, sizeof(hexbuf),
848
                                 kHexFlags | ImGuiInputTextFlags_EnterReturnsTrue |
849
                                     ImGuiInputTextFlags_AutoSelectAll)) {
850
                // Accept "AA BB CC", "AABBCC" or a short prefix; missing bytes
851
                // are left untouched.
852
                const char *p = hexbuf;
853
                for (int j = 0; j < w.len; j++) {
854
                    while (*p == ' ' || *p == '\t')
855
                        p++;
856
                    if (!*p)
857
                        break;
858
                    int hi = hex_digit(*p++);
859
                    if (hi < 0)
860
                        break;
861
                    int lo = hex_digit(*p);
862
                    uint8_t v;
863
                    if (lo < 0) {
864
                        v = (uint8_t)hi;          // single digit = low nibble
865
                    } else {
866
                        v = (uint8_t)((hi << 4) | lo);
867
                        p++;
868
                    }
869
                    debug_ui_write6502((uint16_t)(w.addr + j), v, 0, (int16_t)w.bank);
870
                }
871
            }
872
            if (ImGui::IsItemHovered() && !ImGui::IsItemActive()) {
873
                uint32_t hv = 0;
874
                int      hb = w.len < 4 ? w.len : 4;
875
                for (int j = 0; j < hb; j++)
876
                    hv |= (uint32_t)bytes[j] << (8 * j); // little-endian
877
                dbgui_hover_value_tooltip("Watch value (little-endian) - editable", hv, hb);
878
            }
879
880
            ImGui::TableSetColumnIndex(5);
881
            uint32_t val = 0;
882
            for (int j = 0; j < w.len && j < 4; j++) {
883
                val |= (uint32_t)bytes[j] << (8 * j); // little-endian
884
            }
885
            char abuf[17];
886
            for (int j = 0; j < w.len; j++) {
887
                abuf[j] = ascii_of(bytes[j]);
888
            }
889
            abuf[w.len] = '\0';
890
            ImGui::Text("%u  \"%s\"", val, abuf);
891
892
            ImGui::PopID();
893
        }
894
        ImGui::EndTable();
895
896
        if (remove >= 0) {
897
            entries.erase(entries.begin() + remove);
898
        }
899
    }
900
901
    ImGui::PopID();
902
}
903
904
void
905
cpu_panel_render(bool *p_open)
906
{
907
    if (ImGui::Begin("CPU", p_open)) {
908
        dbgui_window_zoom("cpu");
909
        draw_registers();
910
        draw_vregs();
911
        draw_flags();
912
        draw_interrupts();
913
        draw_stack();
914
        draw_watch();
915
        draw_emulation_confirm_popup();
916
    }
917
    dbgui_window_end();
918
}
919
920
} // namespace
921
922
static DebugPanelRegistration s_reg("CPU", cpu_panel_render, true);