OXYGENHUB
◆ main enhanced-x16-emulator / src / debug_ui / debug_ui_settings.cpp
12.7 KiB · C++ · last changed chore(): initial commit 25 days ago source blame history raw
1
// Commander X16 Emulator — debugger user preferences. See debug_ui_settings.h.
2
3
#include "imgui.h"
4
#include "imgui_internal.h" // ImGuiSettingsHandler
5
#include "debug_ui_settings.h"
6
#include "debug_ui_bridge.h"
7
8
#include <stdio.h>
9
#include <string.h>
10
11
DebugUiSettings &
12
debug_ui_settings()
13
{
14
    static DebugUiSettings s;
15
    return s;
16
}
17
18
// ---------------------------------------------------------------------------
19
// imgui.ini persistence
20
//
21
// ImGui lets an application add its own [Type][Name] section to the ini file.
22
// One entry per setting keeps the file readable and means an unknown key from a
23
// newer build is simply ignored rather than corrupting anything.
24
// ---------------------------------------------------------------------------
25
namespace {
26
27
struct SettingField {
28
    const char *key;
29
    bool       *b;
30
    float      *f;
31
    int        *i;
32
};
33
34
// Built lazily because it points into the settings singleton.
35
const SettingField *
36
fields(int *count)
37
{
38
    DebugUiSettings &s = debug_ui_settings();
39
    static const SettingField table[] = {
40
        { "AutoSwitchDisasm",    &s.auto_switch_disasm,     nullptr, nullptr },
41
        { "AutoSwitchSource",    &s.auto_switch_source,     nullptr, nullptr },
42
        { "FollowInterrupts",    &s.follow_interrupts,      nullptr, nullptr },
43
        { "BreakOnInterrupt",    &s.break_on_interrupt,     nullptr, nullptr },
44
        { "MemHighlightChanges", &s.mem_highlight_changes,  nullptr, nullptr },
45
        { "AudioHoldOnPause",    &s.audio_hold_on_pause,    nullptr, nullptr },
46
        { "MemHighlightSeconds", nullptr, &s.mem_highlight_seconds, nullptr },
47
        { "UiScale",             nullptr, &s.ui_scale, nullptr },
48
        { "AllowBreakingChanges", &s.allow_breaking_changes, nullptr, nullptr },
49
        { "IoTraceCapture",      &s.io_trace_capture,       nullptr, nullptr },
50
        { "IoTraceCapacity",     nullptr, nullptr, &s.io_trace_capacity },
51
        { "IoCapVia",            &s.io_cap_via,             nullptr, nullptr },
52
        { "IoCapVera",           &s.io_cap_vera,            nullptr, nullptr },
53
        { "IoCapSpi",            &s.io_cap_spi,             nullptr, nullptr },
54
        { "IoCapYm",             &s.io_cap_ym,              nullptr, nullptr },
55
        { "IoCapEmu",            &s.io_cap_emu,             nullptr, nullptr },
56
        { "IoCapMidi",           &s.io_cap_midi,            nullptr, nullptr },
57
        { "IoCapOpenBus",        &s.io_cap_openbus,         nullptr, nullptr },
58
        { "IoCapSd",             &s.io_cap_sd,              nullptr, nullptr },
59
        { "IoCapFiles",          &s.io_cap_files,           nullptr, nullptr },
60
        { "IoCapI2c",            &s.io_cap_i2c,             nullptr, nullptr },
61
        { "IoCapJoy",            &s.io_cap_joy,             nullptr, nullptr },
62
        { "IoFatAutoIndex",      &s.io_fat_autoindex,       nullptr, nullptr },
63
        { "PerfAlwaysOn",        &s.perf_always_on,         nullptr, nullptr },
64
        { "PerfTargetFps",       nullptr, &s.perf_target_fps, nullptr },
65
        { "PerfIdleMode",        nullptr, nullptr, &s.perf_idle_mode },
66
        { "PerfCapacity",        nullptr, nullptr, &s.perf_capacity },
67
        { "PerfGraphFrames",     nullptr, nullptr, &s.perf_graph_frames },
68
    };
69
    *count = (int)(sizeof(table) / sizeof(table[0]));
70
    return table;
71
}
72
73
void *
74
settings_read_open(ImGuiContext *, ImGuiSettingsHandler *, const char *name)
75
{
76
    // Single anonymous section; returning non-null tells ImGui to feed us lines.
77
    return strcmp(name, "Prefs") == 0 ? (void *)1 : nullptr;
78
}
79
80
void
81
settings_read_line(ImGuiContext *, ImGuiSettingsHandler *, void *, const char *line)
82
{
83
    char key[64];
84
    float value = 0.0f;
85
    if (sscanf(line, "%63[^=]=%f", key, &value) != 2) {
86
        return;
87
    }
88
    int                 n = 0;
89
    const SettingField *t = fields(&n);
90
    for (int i = 0; i < n; i++) {
91
        if (strcmp(t[i].key, key) != 0) {
92
            continue;
93
        }
94
        if (t[i].b) {
95
            *t[i].b = (value != 0.0f);
96
        } else if (t[i].f) {
97
            *t[i].f = value;
98
        } else if (t[i].i) {
99
            *t[i].i = (int)value;
100
        }
101
        return;
102
    }
103
}
104
105
// Push the loaded execution prefs into the debugger core, which owns the
106
// actual behaviour.
107
void
108
settings_apply_to_core()
109
{
110
    DebugUiSettings &s = debug_ui_settings();
111
    DEBUGSetFollowInterrupts(s.follow_interrupts);
112
    DEBUGSetBreakOnInterrupt(s.break_on_interrupt);
113
}
114
115
void
116
settings_apply_all(ImGuiContext *, ImGuiSettingsHandler *)
117
{
118
    settings_apply_to_core();
119
}
120
121
void
122
settings_write_all(ImGuiContext *, ImGuiSettingsHandler *handler, ImGuiTextBuffer *buf)
123
{
124
    int                 n = 0;
125
    const SettingField *t = fields(&n);
126
    buf->appendf("[%s][Prefs]\n", handler->TypeName);
127
    for (int i = 0; i < n; i++) {
128
        if (t[i].b) {
129
            buf->appendf("%s=%d\n", t[i].key, *t[i].b ? 1 : 0);
130
        } else if (t[i].f) {
131
            buf->appendf("%s=%.3f\n", t[i].key, *t[i].f);
132
        } else if (t[i].i) {
133
            buf->appendf("%s=%d\n", t[i].key, *t[i].i);
134
        }
135
    }
136
    buf->append("\n");
137
}
138
139
} // namespace
140
141
void
142
debug_ui_settings_register(void)
143
{
144
    ImGuiContext *ctx = ImGui::GetCurrentContext();
145
    if (!ctx) {
146
        return;
147
    }
148
    ImGuiSettingsHandler h;
149
    h.TypeName   = "X16Debugger";
150
    h.TypeHash   = ImHashStr("X16Debugger");
151
    h.ReadOpenFn = settings_read_open;
152
    h.ReadLineFn = settings_read_line;
153
    h.ApplyAllFn = settings_apply_all;
154
    h.WriteAllFn = settings_write_all;
155
    ctx->SettingsHandlers.push_back(h);
156
}
157
158
// ---------------------------------------------------------------------------
159
// Settings window
160
// ---------------------------------------------------------------------------
161
namespace {
162
163
// Checkbox that marks the ini dirty so the change is persisted.
164
bool
165
pref_checkbox(const char *label, bool *v, const char *help)
166
{
167
    const bool changed = ImGui::Checkbox(label, v);
168
    if (help && ImGui::IsItemHovered()) {
169
        ImGui::SetTooltip("%s", help);
170
    }
171
    if (changed) {
172
        ImGui::MarkIniSettingsDirty();
173
    }
174
    return changed;
175
}
176
177
} // namespace
178
179
void
180
debug_ui_settings_mark_dirty(void)
181
{
182
    ImGui::MarkIniSettingsDirty();
183
}
184
185
void
186
debug_ui_draw_settings_window(bool *p_open)
187
{
188
    if (!*p_open) {
189
        return;
190
    }
191
    ImGui::SetNextWindowSize(ImVec2(520, 420), ImGuiCond_FirstUseEver);
192
    if (!ImGui::Begin("Settings", p_open)) {
193
        ImGui::End();
194
        return;
195
    }
196
197
    DebugUiSettings &s = debug_ui_settings();
198
199
    ImGui::TextDisabled("Preferences are saved in imgui.ini and restored next run.");
200
    ImGui::Separator();
201
202
    if (ImGui::CollapsingHeader("Appearance", ImGuiTreeNodeFlags_DefaultOpen)) {
203
        ImGui::TextDisabled("Scales the whole debugger - menus, tabs, buttons and spacing,");
204
        ImGui::TextDisabled("not just panel text. Ctrl+wheel over a panel zooms that panel");
205
        ImGui::TextDisabled("on top of this.");
206
        ImGui::SetNextItemWidth(220);
207
        if (ImGui::SliderFloat("Interface scale", &s.ui_scale, 0.75f, 2.50f, "%.2fx")) {
208
            debug_ui_settings_mark_dirty();
209
        }
210
        if (ImGui::IsItemHovered()) {
211
            ImGui::SetTooltip("The font is re-rasterised at the new size rather than stretched,\n"
212
                              "so text stays sharp. Useful on a high-DPI display where the\n"
213
                              "debugger would otherwise be unreadably small.");
214
        }
215
        ImGui::SameLine();
216
        if (ImGui::SmallButton("Reset##uiscale")) {
217
            s.ui_scale = 1.0f;
218
            debug_ui_settings_mark_dirty();
219
        }
220
    }
221
222
    if (ImGui::CollapsingHeader("Navigation", ImGuiTreeNodeFlags_DefaultOpen)) {
223
        ImGui::TextDisabled("Keep the visible panel pointed at wherever execution stopped.");
224
        pref_checkbox("Switch to Disassembly when there is no source", &s.auto_switch_disasm,
225
                      "Stopping in code the debug info doesn't cover (KERNAL ROM, a\n"
226
                      "trampoline, an interrupt handler in ROM) leaves the Source panel\n"
227
                      "showing an unrelated file. With this on, the Disassembly panel is\n"
228
                      "brought to the front instead.");
229
        pref_checkbox("Switch back to Source when there is source", &s.auto_switch_source,
230
                      "Returning to code the debug info covers brings the Source panel\n"
231
                      "back to the front.");
232
    }
233
234
    if (ImGui::CollapsingHeader("Execution", ImGuiTreeNodeFlags_DefaultOpen)) {
235
        if (pref_checkbox("Follow interrupts while stepping", &s.follow_interrupts,
236
                          "An interrupt that fires in the middle of a step normally runs to\n"
237
                          "completion invisibly. With this on, stepping stops at the\n"
238
                          "handler's first instruction so you can watch it.")) {
239
            DEBUGSetFollowInterrupts(s.follow_interrupts);
240
        }
241
        if (pref_checkbox("Break on every interrupt", &s.break_on_interrupt,
242
                          "Stop the machine whenever any interrupt is taken, even while\n"
243
                          "free-running. Useful for catching the first entry to a handler.")) {
244
            DEBUGSetBreakOnInterrupt(s.break_on_interrupt);
245
        }
246
    }
247
248
    if (ImGui::CollapsingHeader("Memory", ImGuiTreeNodeFlags_DefaultOpen)) {
249
        pref_checkbox("Highlight changed bytes", &s.mem_highlight_changes,
250
                      "Pulse the background of a byte when its value changes, so the\n"
251
                      "effect of an instruction is obvious while stepping.");
252
        ImGui::BeginDisabled(!s.mem_highlight_changes);
253
        ImGui::SetNextItemWidth(160);
254
        if (ImGui::SliderFloat("Fade time", &s.mem_highlight_seconds, 0.25f, 5.0f, "%.2f s")) {
255
            ImGui::MarkIniSettingsDirty();
256
        }
257
        if (ImGui::IsItemHovered()) {
258
            ImGui::SetTooltip("How long a changed byte stays tinted before fading out.");
259
        }
260
        ImGui::EndDisabled();
261
    }
262
263
    if (ImGui::CollapsingHeader("Audio", ImGuiTreeNodeFlags_DefaultOpen)) {
264
        pref_checkbox("Hold audio panels while paused", &s.audio_hold_on_pause,
265
                      "Pausing silences the machine, so the PSG/YM2151/PCM meters and\n"
266
                      "scopes would otherwise read as silence. With this on they keep\n"
267
                      "showing the state from the moment execution stopped.");
268
    }
269
270
    if (ImGui::CollapsingHeader("I/O trace", ImGuiTreeNodeFlags_DefaultOpen)) {
271
        ImGui::TextDisabled("Feeds the Activity log in the I/O panel. This is the one");
272
        ImGui::TextDisabled("debugger feature the running machine pays for, so it can be");
273
        ImGui::TextDisabled("turned off outright.");
274
        pref_checkbox("Capture I/O events", &s.io_trace_capture,
275
                      "Record register accesses and decoded device events.\n"
276
                      "With this off the I/O panel still shows live device state;\n"
277
                      "it just has no history to show.");
278
279
        ImGui::SetNextItemWidth(220);
280
        if (ImGui::SliderInt("Events retained", &s.io_trace_capacity, 256, 65536)) {
281
            debug_ui_settings_mark_dirty();
282
        }
283
        if (ImGui::IsItemHovered()) {
284
            ImGui::SetTooltip("Size of the ring buffer. Changing it discards what is\n"
285
                              "currently held, because ordering against a new ring is\n"
286
                              "not meaningful.");
287
        }
288
289
        ImGui::TextDisabled("Per-device capture is on the I/O panel's Activity tab.");
290
        pref_checkbox("Index the filesystem in an SD card image", &s.io_fat_autoindex,
291
                      "Parse the FAT inside an attached image when it is attached, so\n"
292
                      "block traffic resolves to filenames. The emulator itself never\n"
293
                      "does this - to it an SD card is 512-byte blocks and nothing more.\n\n"
294
                      "Off means the SD tab still works, it just reports raw LBAs.");
295
    }
296
297
    if (ImGui::CollapsingHeader("Safety", ImGuiTreeNodeFlags_DefaultOpen)) {
298
        pref_checkbox("Allow breaking modifications", &s.allow_breaking_changes,
299
                      "Off by default. Unlocks the few register edits that change machine\n"
300
                      "state the running program has already committed to - it will usually\n"
301
                      "crash rather than cope.\n\n"
302
                      "Currently gates the E (emulation) flag on the 65C816. Clearing E\n"
303
                      "moves interrupts from $FFFE to $FFEE, and the stock X16 ROM has no\n"
304
                      "native-mode vectors there, so the next IRQ (60 Hz) runs off into\n"
305
                      "whatever bytes happen to sit at $FFEE.\n\n"
306
                      "Turn this on for hand-driving the CPU, not for a live program.");
307
        if (s.allow_breaking_changes) {
308
            ImGui::SameLine();
309
            ImGui::TextColored(ImVec4(0.95f, 0.70f, 0.30f, 1.0f), "(unlocked)");
310
        }
311
    }
312
313
    ImGui::End();
314
}