OXYGENHUB
◆ main enhanced-x16-emulator / src / debug_ui / debug_ui.cpp
39.1 KiB · C++ · last changed chore(): initial commit 25 days ago source blame history raw
1
// Commander X16 Emulator — in-process Dear ImGui debugger implementation.
2
//
3
// Owns the ImGui context and the SDL2 + SDLRenderer2 backends. Each
4
// builds a full-window dock space and iterates the self-registered panel
5
// registry (debug_ui_panels.h), so panels are added purely by dropping a file
6
// under panels/.
7
#include "debug_ui.h"
8
#include "debug_ui_panels.h"
9
#include "debug_ui_bridge.h" // regs + execution-control (DEBUGContinue/…/DEBUGIsRunning)
10
#include "debug_ui_settings.h" // user preferences (System → Settings...)
11
12
#include "imgui.h"
13
#include "imgui_internal.h"   // DockBuilder* for the first-run default layout
14
#include "backends/imgui_impl_sdl2.h"
15
#include "backends/imgui_impl_sdlrenderer2.h"
16
17
#include <cstdarg>
18
#include <cstdio>
19
#include <cstring>
20
21
static SDL_Window   *s_window      = nullptr;
22
static SDL_Renderer *s_renderer    = nullptr;
23
static bool          s_initialized = false;
24
static bool          s_enabled     = false;
25
static bool          s_build_default_layout = false; // install a dock layout on first run
26
static bool          s_show_demo    = false;         // Help → ImGui Demo (vendored imgui_demo.cpp)
27
static bool          s_show_settings = false;        // System → Settings...
28
29
static void debug_ui_latch_goto(void); // defined below; called from debug_ui_render
30
static void debug_ui_follow_stop_location(void); // ditto
31
32
// ── Font ladder for per-window zoom ─────────────────────────────────────────
33
// ImGui's default font is rasterised once at a single size; scaling it (which
34
// SetWindowFontScale does) resamples the glyph bitmaps and looks awful. Build a
35
// ladder of real sizes instead, and have the zoom helper choose the nearest, so
36
// zoomed text is always rendered at its native size.
37
//
38
// The face matters as much as the size. ImGui's built-in ProggyClean is a
39
// pixel font: its outlines are drawn to land exactly on the pixel grid at 13px,
40
// where every stem is a solid, fully-opaque column. Rasterised at any other
41
// size the stems straddle pixel boundaries and get antialiased across two
42
// columns, so the text goes both soft AND dim - no pixel reaches full
43
// intensity. That is why zooming still looked washed out even once the
44
// resampling was gone.
45
//
46
// So prefer a genuinely scalable monospace face from the system, which is
47
// hinted to stay crisp at any size, and keep ProggyClean only as a fallback (in
48
// which case 100% still looks exactly as it always did).
49
static const char *s_mono_font_candidates[] = {
50
#if defined(_WIN32)
51
    "C:\\Windows\\Fonts\\consola.ttf",      // Consolas
52
    "C:\\Windows\\Fonts\\CascadiaMono.ttf",
53
    "C:\\Windows\\Fonts\\lucon.ttf",        // Lucida Console
54
#elif defined(__APPLE__)
55
    "/System/Library/Fonts/Menlo.ttc",
56
    "/System/Library/Fonts/Monaco.ttf",
57
#else
58
    "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
59
    "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
60
    "/usr/share/fonts/TTF/DejaVuSansMono.ttf",
61
#endif
62
};
63
64
// The base entry must be first so it stays the default font for anything that
65
// doesn't opt into zooming.
66
static const float s_font_sizes[] = {
67
    13.0f, // base
68
    9.0f, 10.0f, 11.0f, 12.0f, 14.0f, 15.0f, 16.0f, 18.0f,
69
    20.0f, 22.0f, 24.0f, 27.0f, 30.0f, 34.0f, 39.0f,
70
};
71
static ImFont *s_fonts[IM_ARRAYSIZE(s_font_sizes)] = {};
72
static bool    s_scalable_font = false;
73
74
static const char *
75
debug_ui_find_mono_font(void)
76
{
77
    for (int i = 0; i < IM_ARRAYSIZE(s_mono_font_candidates); i++) {
78
        if (FILE *f = fopen(s_mono_font_candidates[i], "rb")) {
79
            fclose(f);
80
            return s_mono_font_candidates[i];
81
        }
82
    }
83
    return nullptr;
84
}
85
86
static void
87
debug_ui_build_fonts(ImGuiIO &io)
88
{
89
    const char *path = debug_ui_find_mono_font();
90
    s_scalable_font  = (path != nullptr);
91
92
    for (int i = 0; i < IM_ARRAYSIZE(s_font_sizes); i++) {
93
        if (s_scalable_font) {
94
            ImFontConfig cfg;
95
            // Oversample horizontally so stems land cleanly; a real outline
96
            // font stays sharp when it is hinted onto the pixel grid.
97
            cfg.OversampleH = 2;
98
            cfg.OversampleV = 1;
99
            cfg.PixelSnapH  = false;
100
            s_fonts[i] = io.Fonts->AddFontFromFileTTF(path, s_font_sizes[i], &cfg);
101
        }
102
        if (!s_fonts[i]) {
103
            // Fallback: ImGui's built-in. Pixel-snapped, since ProggyClean is a
104
            // pixel font and snapping is the best it can do off its native size.
105
            ImFontConfig cfg;
106
            cfg.SizePixels = s_font_sizes[i];
107
            cfg.PixelSnapH = true;
108
            s_fonts[i]     = io.Fonts->AddFontDefault(&cfg);
109
            if (i == 0) {
110
                s_scalable_font = false;
111
            }
112
        }
113
    }
114
}
115
116
// Nearest built font to `size_px`, plus the size it actually is. Used by the
117
// zoom helper; returns nullptr before fonts are built.
118
ImFont *
119
debug_ui_font_for_size(float size_px, float *out_size)
120
{
121
    int   best  = 0;
122
    float bestd = 1e9f;
123
    for (int i = 0; i < IM_ARRAYSIZE(s_font_sizes); i++) {
124
        const float d = s_font_sizes[i] > size_px ? s_font_sizes[i] - size_px
125
                                                  : size_px - s_font_sizes[i];
126
        if (d < bestd) {
127
            bestd = d;
128
            best  = i;
129
        }
130
    }
131
    if (out_size) {
132
        *out_size = s_font_sizes[best];
133
    }
134
    return s_fonts[best];
135
}
136
137
float
138
debug_ui_font_base_size(void)
139
{
140
    // Per-window zoom stacks on top of the global interface scale, so the base
141
    // it multiplies is the already-scaled size.
142
    return s_font_sizes[0] * debug_ui_settings().ui_scale;
143
}
144
145
// ── Global interface scale ──────────────────────────────────────────────────
146
// Per-window zoom only affects a panel's contents; the menu bar, tab bars,
147
// buttons and all the spacing around them come from the style and the default
148
// font, so they stayed put no matter how far a panel was zoomed. Applying the
149
// scale to both makes the whole debugger grow together.
150
//
151
// The style is scaled from a pristine copy each time rather than repeatedly, so
152
// the metrics never drift, and the font is re-selected from the ladder rather
153
// than stretched, so it stays sharp.
154
static ImGuiStyle s_style_base;
155
static bool       s_style_base_valid = false;
156
static float      s_applied_ui_scale = -1.0f;
157
158
static void
159
debug_ui_apply_ui_scale(void)
160
{
161
    if (!s_style_base_valid) {
162
        return;
163
    }
164
    float want = debug_ui_settings().ui_scale;
165
    if (want < 0.5f) want = 0.5f;
166
    if (want > 4.0f) want = 4.0f;
167
    if (want == s_applied_ui_scale) {
168
        return;
169
    }
170
    s_applied_ui_scale = want;
171
172
    ImGuiStyle &style = ImGui::GetStyle();
173
    style             = s_style_base;   // undo any previous scaling
174
    style.ScaleAllSizes(want);
175
176
    // Default font for everything that doesn't opt into per-window zoom: the
177
    // menu bar, tab bars, popups and every panel that just uses the default.
178
    ImGui::GetIO().FontDefault = debug_ui_font_for_size(s_font_sizes[0] * want, nullptr);
179
}
180
181
extern "C" void
182
debug_ui_init(SDL_Window *win, SDL_Renderer *ren)
183
{
184
    if (s_initialized) {
185
        return;
186
    }
187
    s_window   = win;
188
    s_renderer = ren;
189
190
    IMGUI_CHECKVERSION();
191
    ImGui::CreateContext();
192
193
    ImGuiIO &io = ImGui::GetIO();
194
    io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;      // dockable panels
195
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;  // keyboard nav
196
197
    // Our own imgui.ini section for user preferences. Must be installed before
198
    // ImGui loads the ini (i.e. before the first NewFrame) or the saved values
199
    // are discarded as unknown.
200
    debug_ui_settings_register();
201
202
    // Fonts for per-window zoom.
203
    //
204
    // SetWindowFontScale() stretches the glyph bitmaps that were rasterised at
205
    // one size, which is why zoomed text used to smear. Instead, rasterise the
206
    // font at a ladder of real sizes up front and let the zoom helper pick the
207
    // nearest one, so every zoom level is drawn at its native resolution.
208
    // Snapping to these sizes means there is never any resampling.
209
    debug_ui_build_fonts(io);
210
211
    ImGui::StyleColorsDark();
212
213
    // Tabs: the stock dark theme distinguishes selected from unselected tabs
214
    // only by a blue vs. slightly-lighter-blue fill, which is hard to read at a
215
    // glance — especially with several source-file tabs open. Give the selected
216
    // tab a much brighter, warmer fill with a bold accent overline, and push the
217
    // unselected tabs down to a dark neutral grey so the contrast is obvious.
218
    // Set on the style (not per-panel) so EVERY tab bar in the debugger matches.
219
    {
220
        ImGuiStyle &st = ImGui::GetStyle();
221
        const ImVec4 sel      (0.16f, 0.48f, 0.88f, 1.00f); // selected, focused
222
        const ImVec4 selDim   (0.17f, 0.42f, 0.72f, 1.00f); // selected, unfocused bar
223
        const ImVec4 idle     (0.16f, 0.17f, 0.19f, 1.00f); // unselected
224
        const ImVec4 idleDim  (0.12f, 0.13f, 0.14f, 1.00f); // unselected, unfocused bar
225
        const ImVec4 hover    (0.30f, 0.58f, 0.95f, 1.00f);
226
        const ImVec4 overline (1.00f, 0.72f, 0.25f, 1.00f); // amber accent bar
227
228
        st.Colors[ImGuiCol_Tab]                        = idle;
229
        st.Colors[ImGuiCol_TabHovered]                 = hover;
230
        st.Colors[ImGuiCol_TabSelected]                = sel;
231
        st.Colors[ImGuiCol_TabSelectedOverline]        = overline;
232
        st.Colors[ImGuiCol_TabDimmed]                  = idleDim;
233
        st.Colors[ImGuiCol_TabDimmedSelected]          = selDim;
234
        st.Colors[ImGuiCol_TabDimmedSelectedOverline]  = overline;
235
        st.TabBarOverlineSize = 3.0f;  // thicker accent bar on the selected tab
236
        st.TabRounding        = 3.0f;
237
    }
238
239
    // Remember the unscaled style: the interface-scale setting derives from
240
    // this each time it changes, so repeated changes can't compound and drift.
241
    s_style_base       = ImGui::GetStyle();
242
    s_style_base_valid = true;
243
    s_applied_ui_scale = -1.0f; // force the first apply
244
245
    // Detect first run (no imgui.ini yet) so we install a sensible default dock
246
    // layout once. After that ImGui persists the user's own arrangement and we
247
    // leave it alone.
248
    s_build_default_layout = true;
249
    if (const char *ini = io.IniFilename) {
250
        if (FILE *f = fopen(ini, "rb")) {
251
            fclose(f);
252
            s_build_default_layout = false;
253
        }
254
    }
255
256
    ImGui_ImplSDL2_InitForSDLRenderer(win, ren);
257
    ImGui_ImplSDLRenderer2_Init(ren);
258
259
    s_initialized = true;
260
    s_enabled     = true;
261
}
262
263
extern "C" void
264
debug_ui_process_event(const SDL_Event *ev)
265
{
266
    if (!s_initialized || ev == nullptr) {
267
        return;
268
    }
269
    ImGui_ImplSDL2_ProcessEvent(ev);
270
}
271
272
// Menu-bar text fields that always occupy the same width, so values that change
273
// every frame (run state, PC, cycle and instruction counters) don't shove
274
// everything after them sideways as they grow and shrink. `widest` is a
275
// template string sized to the largest value the field can hold.
276
static void
277
menubar_fixed_advance(const char *widest, float start_x)
278
{
279
    const float w = ImGui::CalcTextSize(widest).x;
280
    ImGui::SameLine();
281
    ImGui::SetCursorPosX(start_x + w + ImGui::GetStyle().ItemSpacing.x);
282
}
283
284
static void
285
menubar_fixed_value(const char *widest, const char *fmt, ...)
286
{
287
    char buf[64] = "";
288
    va_list args;
289
    va_start(args, fmt);
290
    vsnprintf(buf, sizeof buf, fmt, args);
291
    va_end(args);
292
293
    const float x = ImGui::GetCursorPosX();
294
    ImGui::TextUnformatted(buf);
295
    menubar_fixed_advance(widest, x);
296
}
297
298
static void
299
menubar_fixed_colored(const char *widest, ImVec4 color, const char *text)
300
{
301
    const float x = ImGui::GetCursorPosX();
302
    ImGui::PushStyleColor(ImGuiCol_Text, color);
303
    ImGui::TextUnformatted(text);
304
    ImGui::PopStyleColor();
305
    menubar_fixed_advance(widest, x);
306
}
307
308
// Execution-control button. The shortcut belongs in the tooltip because these
309
// are the actions people reach for constantly, and there is nowhere else in the
310
// UI that lists the keys.
311
//
312
// The tooltip deliberately survives the disabled state: these buttons are greyed
313
// out most of the time (stepping needs a halted CPU, Pause needs a running one),
314
// and "why can't I click this?" is exactly the question worth answering. Plain
315
// SetItemTooltip() would say nothing there, since ImGui does not consider a
316
// disabled item hovered unless asked.
317
static bool
318
ctrl_button(const char *label, bool enabled, const char *tip)
319
{
320
    ImGui::BeginDisabled(!enabled);
321
    const bool clicked = ImGui::Button(label);
322
    ImGui::EndDisabled();
323
    if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
324
        ImGui::SetTooltip("%s", tip);
325
    }
326
    return clicked;
327
}
328
329
// Top main menu bar: View/Layout/Help menus plus the execution-control toolbar
330
// (Continue/Pause/Step*, a run-state indicator, and the live PC:bank). Buttons
331
// drive the shared debugger execution control (debugger.c) — the same
332
// transitions F5/F10/F11 and DAP use.
333
static void
334
debug_ui_draw_control_bar(void)
335
{
336
    if (!ImGui::BeginMainMenuBar()) {
337
        return;
338
    }
339
340
    // View: toggle each registered panel. This is the ONLY way to reopen a
341
    // panel after its window has been closed (the [x]), so it doubles as the
342
    // panel visibility manager. Iterates the self-registered registry, so new
343
    // panels appear here automatically.
344
    if (ImGui::BeginMenu("View")) {
345
        for (DebugPanel &panel : debug_ui_panels()) {
346
            ImGui::MenuItem(panel.name, nullptr, &panel.open);
347
        }
348
        ImGui::EndMenu();
349
    }
350
351
    // Layout: rebuild the default dock arrangement on demand (recovers from a
352
    // messy imgui.ini or a panel dragged somewhere unhelpful).
353
    if (ImGui::BeginMenu("Layout")) {
354
        if (ImGui::MenuItem("Reset to Default Layout")) {
355
            for (DebugPanel &panel : debug_ui_panels()) {
356
                panel.open = true;             // reopen everything so the reset is visible
357
            }
358
            s_build_default_layout = true;     // applied after DockSpaceOverViewport() this frame
359
        }
360
        ImGui::EndMenu();
361
    }
362
363
    // System: machine-level controls. Reset defers to the emulator loop via the
364
    // SMC reset flag (safe, == Ctrl-R). IRQ/NMI are edge-triggered.
365
    if (ImGui::BeginMenu("System")) {
366
        if (ImGui::MenuItem("Reset", "Ctrl+Shift+F5")) {
367
            smc_requested_reset = true;
368
        }
369
        ImGui::SetItemTooltip("Reset the machine, as the SMC would. Breakpoints are kept.");
370
        if (ImGui::MenuItem("Trigger IRQ")) {
371
            irq6502();
372
        }
373
        if (ImGui::MenuItem("Trigger NMI")) {
374
            machine_nmi();
375
        }
376
        ImGui::Separator();
377
378
        // Behavioural toggles live in Settings, not scattered through menus.
379
        ImGui::MenuItem("Settings...", nullptr, &s_show_settings);
380
        if (ImGui::IsItemHovered()) {
381
            ImGui::SetTooltip("Interrupt following, panel auto-switching, memory change\n"
382
                              "highlighting and other preferences.");
383
        }
384
        ImGui::EndMenu();
385
    }
386
387
    // Help: the vendored ImGui demo is handy while building/inspecting panels.
388
    if (ImGui::BeginMenu("Help")) {
389
        // Not every shortcut has a button - F9 and Ctrl+F10 in particular are
390
        // keyboard-only - so keep the full list somewhere findable.
391
        if (ImGui::BeginMenu("Keyboard Shortcuts")) {
392
            static const struct { const char *key, *what; } keys[] = {
393
                { "F5",            "Continue" },
394
                { "Break",         "Pause" },
395
                { "F11",           "Step Into" },
396
                { "Shift+F11",     "Step Out" },
397
                { "F10",           "Step Over" },
398
                { "Ctrl+F10",      "Run to cursor" },
399
                { "F9",            "Toggle breakpoint at PC" },
400
                { "Ctrl+Shift+F5", "Reset machine" },
401
                { "Ctrl+wheel",    "Zoom the panel under the cursor" },
402
            };
403
            if (ImGui::BeginTable("keys", 2, ImGuiTableFlags_SizingFixedFit)) {
404
                for (const auto &k : keys) {
405
                    ImGui::TableNextRow();
406
                    ImGui::TableNextColumn();
407
                    ImGui::TextUnformatted(k.key);
408
                    ImGui::TableNextColumn();
409
                    ImGui::TextDisabled("%s", k.what);
410
                }
411
                ImGui::EndTable();
412
            }
413
            ImGui::EndMenu();
414
        }
415
        ImGui::Separator();
416
        ImGui::MenuItem("Show ImGui Demo", nullptr, &s_show_demo);
417
        ImGui::Separator();
418
        ImGui::TextDisabled("Dear ImGui %s", IMGUI_VERSION);
419
        ImGui::EndMenu();
420
    }
421
422
    ImGui::Separator();
423
424
    const bool running = DEBUGIsRunning();
425
    const bool paused  = DEBUGIsPaused();
426
427
    if (ctrl_button("Continue", paused,
428
                    "Continue  (F5)\n"
429
                    "Run until the next breakpoint or pause.\n"
430
                    "Only while the machine is halted."))
431
        DEBUGContinue();
432
433
    ImGui::SameLine();
434
    if (ctrl_button("Pause", running,
435
                    "Pause  (Break key)\n"
436
                    "Halt the machine wherever it is.\n"
437
                    "Only while it is running."))
438
        DEBUGPause();
439
440
    ImGui::SameLine();
441
    if (ctrl_button("Step Into", paused,
442
                    "Step Into  (F11)\n"
443
                    "Execute one source line, entering any call. Without C debug\n"
444
                    "info, or with \"Step by line\" off, one instruction.\n"
445
                    "Only while the machine is halted."))
446
        DEBUGStepIntoAuto();
447
    ImGui::SameLine();
448
    if (ctrl_button("Step Over", paused,
449
                    "Step Over  (F10)\n"
450
                    "Execute one source line, running any call to completion.\n"
451
                    "Without C debug info, or with \"Step by line\" off, one\n"
452
                    "instruction. Ctrl+F10 runs to the cursor instead.\n"
453
                    "Only while the machine is halted."))
454
        DEBUGStepOverAuto(DEBUG_OWNER_UI);
455
    ImGui::SameLine();
456
    if (ctrl_button("Step Out", paused,
457
                    "Step Out  (Shift+F11)\n"
458
                    "Run until the current subroutine returns.\n"
459
                    "Only while the machine is halted."))
460
        DEBUGStepOut(DEBUG_OWNER_UI);
461
462
    // ── Speed ───────────────────────────────────────────────────────────────
463
    // Absolute target clock, not a bare percentage: the machine's own clock is
464
    // whatever -mhz selected at startup, so a percentage alone doesn't say how
465
    // fast anything is actually running. Warp disables the throttle entirely,
466
    // so the speed buttons are meaningless while it is on.
467
    ImGui::Separator();
468
    {
469
        const int khz    = timing_get_speed_khz();
470
        const int native = timing_native_khz();
471
472
        ImGui::BeginDisabled(warp_mode);
473
        if (ImGui::SmallButton("-")) {
474
            timing_step_speed(-1);
475
        }
476
        if (ImGui::IsItemHovered()) ImGui::SetTooltip("Slow emulation down");
477
        ImGui::SameLine();
478
479
        // Fixed width: this text changes constantly and anything after it would
480
        // otherwise shuffle sideways every frame.
481
        char speed_text[48];
482
        if (warp_mode) {
483
            snprintf(speed_text, sizeof speed_text, "warp");
484
        } else if (khz >= 1000) {
485
            snprintf(speed_text, sizeof speed_text, "%.2fMHz", khz / 1000.0);
486
        } else {
487
            snprintf(speed_text, sizeof speed_text, "%dkHz", khz);
488
        }
489
        const bool at_native = (khz == native);
490
        const bool too_fast  = (khz > native);
491
        // Blue below the machine's clock, red above it: running faster than the
492
        // hardware ever does is a legitimate thing to want, but the timing you
493
        // observe there is not what the real machine would do, so say so.
494
        const ImVec4 col = warp_mode   ? ImVec4(0.95f, 0.70f, 0.30f, 1.0f)
495
                           : too_fast  ? ImVec4(1.00f, 0.45f, 0.45f, 1.0f)
496
                           : at_native ? ImGui::GetStyle().Colors[ImGuiCol_Text]
497
                                       : ImVec4(0.55f, 0.85f, 1.00f, 1.0f);
498
        menubar_fixed_colored("888.88MHz", col, speed_text);
499
        if (ImGui::IsItemHovered()) {
500
            ImGui::SetTooltip(
501
                "Target CPU clock. This machine was started at %.2f MHz (-mhz %d),\n"
502
                "which is 100%%; the current target is %d%% of that.%s\n\n"
503
                "%.0f cycles/s, roughly %.0f instructions/s (a 6502-family\n"
504
                "instruction averages about 4 cycles).\n\n"
505
                "+/- step through real hardware clocks and the rates between them.",
506
                native / 1000.0, (int)MHZ, timing_get_speed_percent(),
507
                too_fast ? "\n\nFaster than this machine's clock: timing you measure here\n"
508
                           "is NOT what the real hardware would do."
509
                         : "",
510
                khz * 1000.0, khz * 1000.0 / 4.0);
511
        }
512
        ImGui::SameLine();
513
        if (ImGui::SmallButton("+")) {
514
            timing_step_speed(1);
515
        }
516
        if (ImGui::IsItemHovered()) ImGui::SetTooltip("Speed emulation up");
517
518
        // Only worth offering when it would actually change something.
519
        if (!at_native) {
520
            ImGui::SameLine();
521
            if (ImGui::SmallButton("1x")) {
522
                timing_reset_speed();
523
            }
524
            if (ImGui::IsItemHovered())
525
                ImGui::SetTooltip("Back to this machine's own clock (%.2f MHz).",
526
                                  native / 1000.0);
527
        }
528
        ImGui::EndDisabled();
529
530
        ImGui::SameLine();
531
        if (warp_mode) {
532
            ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.55f, 0.35f, 0.10f, 1.0f));
533
            ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.70f, 0.45f, 0.15f, 1.0f));
534
        }
535
        if (ImGui::SmallButton("Warp")) {
536
            machine_toggle_warp();
537
        }
538
        if (warp_mode) {
539
            ImGui::PopStyleColor(2);
540
        }
541
        if (ImGui::IsItemHovered()) {
542
            ImGui::SetTooltip("Run with no speed limit (currently %s)", warp_mode ? "ON" : "off");
543
        }
544
    }
545
546
    // ── Status ──────────────────────────────────────────────────────────────
547
    // Everything from here on changes every frame while stepping, so each field
548
    // reserves the width of its widest value. Without that the whole right-hand
549
    // side of the bar jitters as the numbers grow and shrink.
550
    ImGui::Separator();
551
    if (running) {
552
        menubar_fixed_colored("RUNNING", ImVec4(0.40f, 0.85f, 0.40f, 1.0f), "RUNNING");
553
    } else if (paused) {
554
        menubar_fixed_colored("RUNNING", ImVec4(0.95f, 0.70f, 0.30f, 1.0f), "PAUSED");
555
    } else {
556
        menubar_fixed_value("RUNNING", "%s", "-");
557
    }
558
559
    // Interrupt context: make it obvious when the PC is inside a handler, since
560
    // otherwise stepping through interrupt code looks identical to main code.
561
    ImGui::SameLine();
562
    if (cpu_in_interrupt()) {
563
        char irq_text[16];
564
        if (cpu_irq_depth() > 1)
565
            snprintf(irq_text, sizeof irq_text, "IRQ x%d", cpu_irq_depth());
566
        else
567
            snprintf(irq_text, sizeof irq_text, "IRQ");
568
        menubar_fixed_colored("IRQ x88", ImVec4(1.00f, 0.45f, 0.45f, 1.0f), irq_text);
569
        if (ImGui::IsItemHovered()) {
570
            ImGui::SetTooltip("Executing inside an interrupt handler (depth %d).\n"
571
                              "Returns to $%04X.",
572
                              cpu_irq_depth(), (unsigned)cpu_irq_return_pc());
573
        }
574
    } else {
575
        menubar_fixed_value("IRQ x88", "%s", "");
576
    }
577
578
    ImGui::Separator();
579
    menubar_fixed_value("PC 88:8888", "PC %02X:%04X", (unsigned)regs.k, (unsigned)regs.pc);
580
581
    ImGui::Separator();
582
    menubar_fixed_value("cyc 8888888888", "cyc %u", (unsigned)clockticks6502);
583
    ImGui::SameLine();
584
    menubar_fixed_value("ins 8888888888", "ins %d", instruction_counter);
585
586
    ImGui::EndMainMenuBar();
587
}
588
589
// ── Default panel placement ─────────────────────────────────────────────────
590
// One table drives BOTH the first-run dock layout and the placement of panels
591
// that are new to an *existing* imgui.ini. When you add a panel to the registry,
592
// add it here too; anything not listed just floats.
593
enum DebugDockSlot { DOCK_CENTER = 0, DOCK_RIGHT, DOCK_BOTTOM, DOCK_SLOT_COUNT };
594
595
struct DebugPanelPlacement {
596
    const char   *window; // MUST match the panel's ImGui::Begin() title
597
    DebugDockSlot slot;
598
};
599
600
static const DebugPanelPlacement s_placement[] = {
601
    {"Disassembly",  DOCK_CENTER},
602
    {"Source",       DOCK_CENTER},
603
    {"CPU",          DOCK_RIGHT},
604
    {"Breakpoints",  DOCK_RIGHT},
605
    {"Symbols",      DOCK_RIGHT},
606
    {"Call Stack",   DOCK_RIGHT},
607
    {"Memory",       DOCK_BOTTOM},
608
    {"VERA",         DOCK_BOTTOM},
609
    {"PSG",          DOCK_BOTTOM},
610
    {"YM2151",       DOCK_BOTTOM},
611
    {"PCM",          DOCK_BOTTOM},
612
    {"Performance",  DOCK_BOTTOM},
613
    // Added last in this slot so Memory stays the tab the bottom group opens
614
    // on -- the loop below takes the first entry per slot as that slot's front.
615
    {"I/O",          DOCK_BOTTOM},
616
};
617
618
// A representative, long-established window for each slot. Used to find the
619
// slot's *live* dock node: the node ids minted by debug_ui_build_default_layout
620
// only exist on a fresh run, whereas a returning user's nodes come from
621
// imgui.ini and can be anything.
622
static const char *s_slot_anchor[DOCK_SLOT_COUNT] = { "Disassembly", "CPU", "Memory" };
623
624
// Resolve a slot to the dock node it currently occupies, or 0 if unknown.
625
static ImGuiID
626
debug_ui_slot_node(DebugDockSlot slot)
627
{
628
    const char *anchor = s_slot_anchor[slot];
629
    if (ImGuiWindow *w = ImGui::FindWindowByName(anchor)) {
630
        if (w->DockId != 0) {
631
            return w->DockId;
632
        }
633
    }
634
    // The anchor may not have been submitted yet this frame; its saved settings
635
    // still carry the node id.
636
    if (ImGuiWindowSettings *s = ImGui::FindWindowSettingsByID(ImHashStr(anchor))) {
637
        return s->DockId;
638
    }
639
    return 0;
640
}
641
642
// The tab id ImGui will mint for a window, without needing the window to exist.
643
// ImGuiWindow's constructor does TabId = GetID("#TAB") seeded with the window
644
// id (imgui.cpp), and on a first run no panel has been submitted yet at the
645
// point the default layout is built.
646
static ImGuiID
647
debug_ui_window_tab_id(const char *window)
648
{
649
    if (ImGuiWindow *w = ImGui::FindWindowByName(window)) {
650
        return w->TabId;
651
    }
652
    return ImHashStr("#TAB", 0, ImHashStr(window));
653
}
654
655
// Give a panel its intended home, but ONLY when imgui.ini has no entry for it.
656
//
657
// This is the fix for "I updated and the new panels were all floating": a panel
658
// added after the user's layout was saved has no saved dock id, so ImGui floats
659
// it and the user has to dock it by hand. ImGuiCond_FirstUseEver is exactly the
660
// right guard — ImGui clears that condition for any window it loaded settings
661
// for (see CreateNewWindow), so an existing arrangement is never touched, and a
662
// panel the user deliberately floated stays floating.
663
static void
664
debug_ui_place_new_panel(const char *window)
665
{
666
    for (const DebugPanelPlacement &p : s_placement) {
667
        if (strcmp(p.window, window) == 0) {
668
            if (ImGuiID node = debug_ui_slot_node(p.slot)) {
669
                ImGui::SetNextWindowDockID(node, ImGuiCond_FirstUseEver);
670
            }
671
            return;
672
        }
673
    }
674
}
675
676
// First-run only (or on Layout → Reset): arrange the panels into a usable
677
// layout instead of a pile of overlapping windows. Disassembly + Source tabbed
678
// in the center, CPU + Breakpoints tabbed on the right, Memory + VERA + the
679
// audio panels tabbed across the bottom.
680
//
681
// Which tab each group *opens on* is set explicitly at the end. Left alone,
682
// ImGui selects whichever tab was added last (DockNodeUpdateTabBar), and that
683
// order is an accident of static registration order across the panel
684
// translation units — which is why a fresh layout came up showing PCM at the
685
// bottom and Call Stack on the right rather than Memory and CPU.
686
static void
687
debug_ui_build_default_layout(ImGuiID dockspace_id)
688
{
689
    ImGui::DockBuilderRemoveNode(dockspace_id);
690
    ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
691
    ImGui::DockBuilderSetNodeSize(dockspace_id, ImGui::GetMainViewport()->WorkSize);
692
693
    ImGuiID center = dockspace_id;
694
    ImGuiID right  = ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, 0.28f, nullptr, &center);
695
    ImGuiID bottom = ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, 0.34f, nullptr, &center);
696
697
    const ImGuiID node[DOCK_SLOT_COUNT] = { center, right, bottom };
698
    for (const DebugPanelPlacement &p : s_placement) {
699
        ImGui::DockBuilderDockWindow(p.window, node[p.slot]);
700
    }
701
    ImGui::DockBuilderFinish(dockspace_id);
702
703
    // The first panel listed for a slot is the one that slot opens on.
704
    const char *front[DOCK_SLOT_COUNT] = {};
705
    for (const DebugPanelPlacement &p : s_placement) {
706
        if (front[p.slot] != nullptr) {
707
            continue;
708
        }
709
        front[p.slot] = p.window;
710
        if (ImGuiDockNode *n = ImGui::DockBuilderGetNode(node[p.slot])) {
711
            n->SelectedTabId = debug_ui_window_tab_id(p.window);
712
        }
713
    }
714
715
    // On a reset the previously focused panel is still the nav window, and
716
    // DockNodeUpdateTabBar hands its node's selection back to it — which would
717
    // undo the choice above for whichever group it sits in. Focusing the center
718
    // panel keeps that override on the one node whose default it agrees with.
719
    if (front[DOCK_CENTER]) {
720
        ImGui::SetWindowFocus(front[DOCK_CENTER]);
721
    }
722
}
723
724
extern "C" void
725
debug_ui_render(void)
726
{
727
    if (!s_initialized) {
728
        return;
729
    }
730
731
    // Interface scale is read from settings and applied to the style + default
732
    // font. Must happen before NewFrame, which latches the current font.
733
    debug_ui_apply_ui_scale();
734
735
    ImGui_ImplSDLRenderer2_NewFrame();
736
    ImGui_ImplSDL2_NewFrame();
737
    ImGui::NewFrame();
738
    // Publish any cross-panel goto requested last frame so every panel sees it.
739
    debug_ui_latch_goto();
740
741
    // Point the user at wherever execution actually stopped (Source vs
742
    // Disassembly), before panels render so the focus applies this frame.
743
    debug_ui_follow_stop_location();
744
745
    // Execution-control bar first: it lives in the main menu bar, which shrinks
746
    // the viewport work area so the dock space sits neatly below it.
747
    debug_ui_draw_control_bar();
748
749
    // Full-window dock space so every panel is dockable/draggable. On first run
750
    // (no imgui.ini) install a default layout; afterwards ImGui restores the
751
    // user's saved arrangement.
752
    ImGuiID dockspace_id = ImGui::DockSpaceOverViewport();
753
    if (s_build_default_layout) {
754
        s_build_default_layout = false;
755
        debug_ui_build_default_layout(dockspace_id);
756
    }
757
758
    // Iterate the self-registered panel registry. No central list to edit:
759
    // each panel added a file-static DebugPanelRegistration (see
760
    // debug_ui_panels.h). Panels missing from imgui.ini get docked into their
761
    // intended slot first, so a newly added panel never turns up floating.
762
    for (DebugPanel &panel : debug_ui_panels()) {
763
        if (panel.open && panel.render) {
764
            debug_ui_place_new_panel(panel.name);
765
            panel.render(&panel.open);
766
        }
767
    }
768
769
    // Optional vendored demo window (Help → Show ImGui Demo).
770
    if (s_show_demo) {
771
        ImGui::ShowDemoWindow(&s_show_demo);
772
    }
773
774
    // User preferences (System → Settings...).
775
    debug_ui_draw_settings_window(&s_show_settings);
776
777
    ImGui::Render();
778
779
    ImGuiIO &io = ImGui::GetIO();
780
    SDL_RenderSetScale(s_renderer, io.DisplayFramebufferScale.x, io.DisplayFramebufferScale.y);
781
    SDL_SetRenderDrawColor(s_renderer, 24, 24, 24, 255);
782
    SDL_RenderClear(s_renderer);
783
    ImGui_ImplSDLRenderer2_RenderDrawData(ImGui::GetDrawData(), s_renderer);
784
    SDL_RenderPresent(s_renderer);
785
}
786
787
extern "C" void
788
debug_ui_shutdown(void)
789
{
790
    if (!s_initialized) {
791
        return;
792
    }
793
    ImGui_ImplSDLRenderer2_Shutdown();
794
    ImGui_ImplSDL2_Shutdown();
795
    ImGui::DestroyContext();
796
797
    s_window      = nullptr;
798
    s_renderer    = nullptr;
799
    s_initialized = false;
800
    s_enabled     = false;
801
}
802
803
extern "C" bool
804
debug_ui_is_enabled(void)
805
{
806
    return s_enabled;
807
}
808
809
extern "C" void
810
debug_ui_set_enabled(bool enabled)
811
{
812
    s_enabled = enabled;
813
}
814
815
extern "C" SDL_Renderer *
816
debug_ui_get_renderer(void)
817
{
818
    return s_renderer;
819
}
820
821
extern "C" bool
822
debug_ui_want_capture_keyboard(void)
823
{
824
    if (!s_initialized) {
825
        return false;
826
    }
827
    return ImGui::GetIO().WantCaptureKeyboard;
828
}
829
830
// True only while an ImGui text field is actively accepting text (an InputText
831
// is focused). Unlike WantCaptureKeyboard — which is true whenever the debugger
832
// window merely has keyboard-nav focus — this stays false when the user is just
833
// looking at panels, so the F5/F9/F10/F11 debug shortcuts (which ImGui never
834
// uses for text or nav) still fire. Only genuine text entry (goto/search boxes)
835
// suppresses them.
836
extern "C" bool
837
debug_ui_want_text_input(void)
838
{
839
    if (!s_initialized) {
840
        return false;
841
    }
842
    return ImGui::GetIO().WantTextInput;
843
}
844
845
// ── Run-to-cursor + PC breakpoint toggle (keyboard shortcuts) ───────────────
846
// The Disassembly panel publishes the instruction under the mouse here every
847
// frame; the Ctrl+F10 shortcut consumes it. These live here (not in a panel) so
848
// the C event pump in video.c can reach them through the plain C ABI, and so
849
// they share the same breakpoint/run-to plumbing every panel uses (the bridge).
850
static uint16_t s_cursor_addr  = 0;
851
static uint8_t  s_cursor_bank  = 0;
852
static bool     s_cursor_valid = false;
853
854
extern "C" void
855
debug_ui_set_cursor(uint16_t addr, uint8_t bank, bool valid)
856
{
857
    s_cursor_addr  = addr;
858
    s_cursor_bank  = bank;
859
    s_cursor_valid = valid;
860
}
861
862
extern "C" bool
863
debug_ui_run_to_cursor(void)
864
{
865
    if (!s_cursor_valid || !DEBUGIsPaused()) {
866
        return false;
867
    }
868
    DEBUGRunTo(s_cursor_addr, s_cursor_bank, DEBUG_OWNER_UI);
869
    return true;
870
}
871
872
extern "C" void
873
debug_ui_toggle_breakpoint_at_pc(void)
874
{
875
    // Toggle: remove one at (pc,bank,x16Bank) if present, else add one there.
876
    // The bank has to be worked out before the remove, not just for the add:
877
    // debug_bp_find() matches x16Bank exactly, so removing with a different
878
    // selector than the one it was stored under finds nothing.
879
    int x16 = -1;
880
    // Match getCurrentBank()/x16bank_for(): a banked window only applies in
881
    // program bank 0 (getCurrentBank returns -1 for any k != 0), so guarding
882
    // on regs.k == 0 keeps the breakpoint matchable instead of dead.
883
    if (regs.pc >= 0xA000 && regs.k == 0) {
884
        x16 = (regs.pc < 0xC000) ? (int)memory_get_ram_bank()
885
                                 : (int)memory_get_rom_bank();
886
    }
887
    // The core's own toggle, so this window's F9 behaves exactly like the SDL
888
    // debugger's: delete whatever is there whoever asked for it, or create one
889
    // owned by the UI. Deleting only the UI's claim would leave a breakpoint a
890
    // DAP client also wanted armed, and the key looking broken.
891
    debug_bp_toggle_for((int)regs.pc, regs.k, x16, DEBUG_OWNER_UI);
892
}
893
894
// ── Cross-panel "goto" service ──────────────────────────────────────────────
895
// A one-shot navigation target published by e.g. the Symbols or Call Stack
896
// panels (debug_ui_request_goto) and consumed by the Disassembly + Memory
897
// panels (debug_ui_peek_goto). A request made during frame N is latched at the
898
// top of frame N+1 and stays visible to EVERY panel for that whole frame, so
899
// panel render order doesn't matter.
900
static uint16_t s_goto_req_addr    = 0;
901
static uint8_t  s_goto_req_bank    = 0;
902
static bool     s_goto_req_pending = false;
903
static uint16_t s_goto_addr        = 0;
904
static uint8_t  s_goto_bank        = 0;
905
static bool     s_goto_active      = false;
906
907
extern "C" void
908
debug_ui_request_goto(uint16_t addr, uint8_t bank)
909
{
910
    s_goto_req_addr    = addr;
911
    s_goto_req_bank    = bank;
912
    s_goto_req_pending = true;
913
}
914
915
extern "C" bool
916
debug_ui_peek_goto(uint16_t *addr, uint8_t *bank)
917
{
918
    if (!s_goto_active) {
919
        return false;
920
    }
921
    if (addr) *addr = s_goto_addr;
922
    if (bank) *bank = s_goto_bank;
923
    return true;
924
}
925
926
// Latch a pending goto request into the frame-visible slot. Called once at the
927
// top of debug_ui_render before any panel runs.
928
static void
929
debug_ui_latch_goto(void)
930
{
931
    s_goto_active = s_goto_req_pending;
932
    if (s_goto_req_pending) {
933
        s_goto_addr        = s_goto_req_addr;
934
        s_goto_bank        = s_goto_req_bank;
935
        s_goto_req_pending = false;
936
    }
937
}
938
939
// ── Follow the stop location between Source and Disassembly ─────────────────
940
// Breaking somewhere the debug info doesn't cover (KERNAL ROM, a trampoline, an
941
// interrupt handler) leaves the Source panel showing an unrelated file while the
942
// PC is elsewhere entirely. Bring the panel that can actually show the code to
943
// the front instead.
944
//
945
// Only acts when the stop location changes, so it never fights a tab the user
946
// deliberately selected, and only while paused, so it doesn't thrash on every
947
// frame of a free run.
948
static void
949
debug_ui_follow_stop_location(void)
950
{
951
    const DebugUiSettings &st = debug_ui_settings();
952
    if (!st.auto_switch_disasm && !st.auto_switch_source) {
953
        return;
954
    }
955
956
    static bool     had_stop = false;
957
    static uint16_t last_pc  = 0;
958
    static uint8_t  last_k   = 0;
959
960
    if (!DEBUGIsPaused()) {
961
        had_stop = false; // re-arm: the next stop is a fresh location
962
        return;
963
    }
964
    if (had_stop && regs.pc == last_pc && regs.k == last_k) {
965
        return;
966
    }
967
    had_stop = true;
968
    last_pc  = regs.pc;
969
    last_k   = regs.k;
970
971
    const char *file = nullptr;
972
    int         line = 0;
973
    const bool  has_source =
974
        (regs.k == 0) &&
975
        dbg_info_addr_to_source_banked(regs.pc, (int)memory_get_ram_bank(), &file, &line) &&
976
        file != nullptr;
977
978
    if (has_source) {
979
        if (st.auto_switch_source) {
980
            ImGui::SetWindowFocus("Source");
981
        }
982
    } else {
983
        if (st.auto_switch_disasm) {
984
            ImGui::SetWindowFocus("Disassembly");
985
        }
986
    }
987
}
988
989
// ── Cross-panel "add to watch" ──────────────────────────────────────────────
990
// The watch list lives in the CPU panel, but the natural place to ask for a
991
// watch is wherever you found the address. Same one-frame latch as the goto
992
// request above so panel render order doesn't matter.
993
static uint16_t s_watch_req_addr    = 0;
994
static int16_t  s_watch_req_bank    = -1;
995
static uint16_t s_watch_req_len     = 1;
996
static bool     s_watch_req_pending = false;
997
998
extern "C" void
999
debug_ui_request_watch(uint16_t addr, int16_t bank, uint16_t len)
1000
{
1001
    s_watch_req_addr    = addr;
1002
    s_watch_req_bank    = bank;
1003
    s_watch_req_len     = len ? len : 1;
1004
    s_watch_req_pending = true;
1005
}
1006
1007
extern "C" bool
1008
debug_ui_take_watch_request(uint16_t *addr, int16_t *bank, uint16_t *len)
1009
{
1010
    if (!s_watch_req_pending) {
1011
        return false;
1012
    }
1013
    if (addr) *addr = s_watch_req_addr;
1014
    if (bank) *bank = s_watch_req_bank;
1015
    if (len)  *len  = s_watch_req_len;
1016
    s_watch_req_pending = false;
1017
    return true;
1018
}
1019