OXYGENHUB
# 9120e87480a6 enhanced-x16-emulator / src / debug_ui / panels / memory_panel.cpp
39.5 KiB · C++ · last changed chore(): initial commit 25 days ago source blame history raw
1
// Commander X16 Emulator — ImGui debugger "Memory" panel.
2
//
3
// A hex/ASCII viewer + editor built on imgui_club's MemoryEditor. Because X16
4
// memory is banked/virtual there is no single contiguous buffer, so we use
5
// MemoryEditor's callback form (ReadFn/WriteFn) with a dummy base pointer and
6
// treat the byte offset as an address.
7
//
8
// Two views (tabs):
9
//   * CPU  — the 64K CPU address space, with RAM ($A000-$BFFF) and ROM
10
//            ($C000-$FFFF) bank selectors, a Goto box, PC + watchpoint
11
//            highlighting, and set/clear of memory-write watchpoints.
12
//   * VRAM — VERA video memory, 0..0x1FFFF.
13
//
14
// All emulator state is reached through debug_ui_bridge.h (never core headers).
15
#include <stddef.h>
16
#include <stdint.h>
17
#include <cctype>
18
#include <cstring>
19
#include <vector>
20
21
#include "imgui.h"
22
#include "debug_ui_panels.h"
23
#include "debug_ui_bridge.h"
24
#include "debug_ui_widgets.h"    // dbgui_value_lines
25
#include "debug_ui_settings.h"   // user preferences (highlight toggle + fade)
26
#include "imgui_memory_editor.h" // imgui_club MemoryEditor
27
28
// ---------------------------------------------------------------------------
29
// debug_ui_write6502 — declared (extern "C") in debug_ui_bridge.h. Defined here
30
// for now; wraps the core debug_write6502() so the edit updates the machine and
31
// does nothing else: no cycles spent, no watchpoint reported. The byte reads as
32
// though it had always been there. For banked RAM/ROM, the write targets the
33
// currently mapped bank, so we temporarily point the mapping at the bank the
34
// user is viewing and restore it. The emulator is paused in the debugger while
35
// editing, so this bank juggling is not observable by the running program.
36
// ---------------------------------------------------------------------------
37
extern "C" void
38
debug_ui_write6502(uint16_t address, uint8_t value, uint8_t bank, int16_t x16Bank)
39
{
40
    if (x16Bank >= 0 && address >= 0xA000 && address < 0xC000) { // banked RAM
41
        uint8_t saved = memory_get_ram_bank();
42
        memory_set_ram_bank((uint8_t)x16Bank);
43
        debug_write6502(address, bank, value);
44
        memory_set_ram_bank(saved);
45
    } else if (x16Bank >= 0 && address >= 0xC000) { // banked ROM / cartridge
46
        uint8_t saved = memory_get_rom_bank();
47
        memory_set_rom_bank((uint8_t)x16Bank);
48
        debug_write6502(address, bank, value);
49
        memory_set_rom_bank(saved);
50
    } else {
51
        debug_write6502(address, bank, value);
52
    }
53
}
54
55
namespace {
56
57
constexpr size_t   CPU_SIZE  = 0x10000;  // 64K CPU address space
58
constexpr size_t   VRAM_SIZE = 0x20000;  // 128K VERA video memory
59
constexpr int      MAX_WP    = MAX_WATCHPOINTS;
60
61
// Background for a selected / navigated-to byte range. The editor's default is
62
// white at 20% alpha, which reads as a washed-out grey and is easy to lose in a
63
// wall of hex. This is close to the Source panel's current-line blue so "this is
64
// what you picked" looks the same across panels, and stays translucent so the
65
// hex digits remain legible on top of it.
66
constexpr ImU32    SEL_HIGHLIGHT = IM_COL32(70, 130, 245, 140);
67
68
// UI-selected banks for the banked CPU regions. Lazily initialised to the CPU's
69
// current mapping so the view opens on what the machine is actually running.
70
int  g_ram_bank   = 0;
71
int  g_rom_bank   = 0;
72
bool g_banks_init = false;
73
74
// The 65C816 CPU bank byte for the CPU tab (X16 GS / gen2). 0 = the classic X16
75
// map (I/O + windowed $A000-$BFFF/$C000-$FFFF banks); 1..num_banks-1 = a flat
76
// 64K RAM page. Only meaningful/shown in gen2.
77
int  g_cpu_bank   = 0;
78
79
uint16_t g_cpu_goto  = 0x0000; // CPU goto target
80
uint32_t g_vram_goto = 0x00000; // VRAM goto target
81
82
// Which bank applies to a CPU address given the current selectors.
83
int16_t
84
cpu_bank_for(uint16_t addr)
85
{
86
    if (addr >= 0xA000 && addr < 0xC000) return (int16_t)g_ram_bank; // RAM bank
87
    if (addr >= 0xC000)                  return (int16_t)g_rom_bank; // ROM bank
88
    return DEBUG_UI_CURRENT_BANK;                                    // low RAM / I/O
89
}
90
91
92
// ---- Recent-write tracking -------------------------------------------------
93
// Stepping shows you the instruction but not its effect, so diff memory and
94
// pulse whatever changed, fading the tint out over a second or two (the
95
// Cheat-Engine idiom). The fade runs on wall-clock time rather than a count of
96
// stops, so it reads the same whether you are single-stepping or watching the
97
// machine run.
98
//
99
// Scope is the CPU address space including whichever banked window is mapped -
100
// i.e. what the program is actually touching. VRAM is deliberately excluded: it
101
// churns every frame and would be a solid wall of colour.
102
//
103
// Cost is bounded by only re-diffing on a new stop, or at a capped rate while
104
// running - never once per frame.
105
struct MemChangeTracker {
106
    uint8_t  prev[0x10000];
107
    float    when[0x10000];   // time the byte last changed; 0 = never
108
    bool     primed = false;
109
    uint32_t last_stop_cycle = 0;
110
    double   last_scan_time  = 0.0;
111
    // Which banks the baseline was captured through, so a change of viewpoint
112
    // can be told apart from the program writing memory.
113
    int      baseline_ram_bank = -1;
114
    int      baseline_rom_bank = -1;
115
    int      baseline_cpu_bank = -1;
116
117
    // The I/O page is not memory and does not belong in a "what changed"
118
    // sweep. Several registers report live hardware state that differs on
119
    // every read (the VERA scanline, VIA timers), so every pass would light
120
    // the whole page up as changed; and a read of one is not always passive --
121
    // $9F3E clocks the next byte out to the SD card. debug_ui_read6502() asks
122
    // the devices not to do that, but a debugger has no reason to be poking at
123
    // I/O to spot memory writes in the first place.
124
    static bool is_io_page(uint16_t a) { return a >= 0x9F00 && a <= 0x9FFF; }
125
126
    void scan(double now)
127
    {
128
        if (!primed) {
129
            for (int a = 0; a < 0x10000; ++a) {
130
                if (is_io_page((uint16_t)a)) continue;
131
                prev[a] = debug_ui_read6502((uint16_t)a, 0, cpu_bank_for((uint16_t)a));
132
            }
133
            memset(when, 0, sizeof when);
134
            primed = true;
135
            return;
136
        }
137
        for (int a = 0; a < 0x10000; ++a) {
138
            if (is_io_page((uint16_t)a)) continue;
139
            const uint8_t v = debug_ui_read6502((uint16_t)a, 0, cpu_bank_for((uint16_t)a));
140
            if (v != prev[a]) {
141
                prev[a] = v;
142
                when[a] = (float)now;
143
            }
144
        }
145
    }
146
147
    void update()
148
    {
149
        const double now = ImGui::GetTime();
150
151
        // Changing which bank the view reads makes the whole windowed region
152
        // look different from the baseline. That is a change of viewpoint, not
153
        // the program writing memory, so re-baseline silently instead of
154
        // flooding the view with red.
155
        if (g_ram_bank != baseline_ram_bank || g_rom_bank != baseline_rom_bank ||
156
            g_cpu_bank != baseline_cpu_bank) {
157
            baseline_ram_bank = g_ram_bank;
158
            baseline_rom_bank = g_rom_bank;
159
            baseline_cpu_bank = g_cpu_bank;
160
            reset();
161
        }
162
163
        if (DEBUGIsPaused()) {
164
            // One diff per stop: re-scanning every frame while parked would
165
            // find nothing and just burn cycles.
166
            if (primed && clockticks6502 == last_stop_cycle) {
167
                return;
168
            }
169
            last_stop_cycle = clockticks6502;
170
            last_scan_time  = now;
171
            scan(now);
172
            return;
173
        }
174
175
        // Free-running: often enough to look live, rarely enough that a 64K
176
        // compare cannot dominate the frame.
177
        if (now - last_scan_time < 0.05) {
178
            return;
179
        }
180
        last_scan_time  = now;
181
        last_stop_cycle = clockticks6502;
182
        scan(now);
183
    }
184
185
    // Tint for an address, or 0 once it has fully faded.
186
    ImU32 color(uint16_t addr, float fade_seconds) const
187
    {
188
        const float t0 = when[addr];
189
        if (t0 <= 0.0f || fade_seconds <= 0.0f) {
190
            return 0;
191
        }
192
        const float age = (float)ImGui::GetTime() - t0;
193
        if (age < 0.0f || age >= fade_seconds) {
194
            return 0;
195
        }
196
        const float k     = 1.0f - (age / fade_seconds);    // 1 at the moment of change
197
        const int   alpha = (int)(30.0f + 195.0f * k * k);  // ease out: a sharp pulse
198
        return IM_COL32(220, 40, 40, alpha);
199
    }
200
201
    void reset()
202
    {
203
        primed = false;
204
        memset(when, 0, sizeof when);
205
    }
206
};
207
208
MemChangeTracker g_mem_changes;
209
210
// ---- MemoryEditor callbacks -----------------------------------------------
211
212
ImU8
213
cpu_read_fn(const ImU8 *, size_t off, void *)
214
{
215
    uint16_t addr = (uint16_t)off;
216
    // gen2 flat RAM page (CPU bank byte != 0): read flat, bypassing the windows.
217
    if (g_cpu_bank != 0)
218
        return debug_ui_read6502(addr, (uint8_t)g_cpu_bank, DEBUG_UI_CURRENT_BANK);
219
    return debug_ui_read6502(addr, 0, cpu_bank_for(addr));
220
}
221
222
void
223
cpu_write_fn(ImU8 *, size_t off, ImU8 d, void *)
224
{
225
    uint16_t addr = (uint16_t)off;
226
    if (g_cpu_bank != 0)
227
        write6502(addr, (uint8_t)g_cpu_bank, d); // flat RAM page
228
    else
229
        debug_ui_write6502(addr, d, 0, cpu_bank_for(addr));
230
}
231
232
// Background tint: green for the current PC, red for watchpoint-covered bytes,
233
// amber for bytes written since the last stop (fading over the following stops).
234
ImU32
235
cpu_bg_color_fn(const ImU8 *, size_t off, void *)
236
{
237
    uint16_t addr = (uint16_t)off;
238
    // PC highlight only when the viewed CPU bank matches the program bank (K).
239
    if (addr == regs.pc && g_cpu_bank == regs.k) return IM_COL32(60, 140, 60, 140);
240
    // Watchpoints only apply to bank 0 (write watchpoints fire on bank-0 writes).
241
    if (g_cpu_bank == 0 && DEBUGCheckWatchPoint(addr)) return IM_COL32(170, 60, 60, 130);
242
    if (debug_ui_settings().mem_highlight_changes && g_cpu_bank == 0) {
243
        ImU32 c = g_mem_changes.color(addr, debug_ui_settings().mem_highlight_seconds);
244
        if (c) return c;
245
    }
246
    return 0;
247
}
248
249
ImU8
250
vram_read_fn(const ImU8 *, size_t off, void *)
251
{
252
    return video_space_read((uint32_t)off);
253
}
254
255
void
256
vram_write_fn(ImU8 *, size_t off, ImU8 d, void *)
257
{
258
    video_space_write((uint32_t)off, d);
259
}
260
261
262
// ---- Banked memory view (RAM or ROM) ---------------------------------------
263
// Browse any RAM/ROM bank's window directly, independent of the bank currently
264
// mapped into the CPU space. Assets (music/art) and code overlays live in
265
// banked RAM; this makes every bank viewable/searchable one at a time, plus an
266
// optional search across all banks at once.
267
int      g_bank_region = 0;      // 0 = RAM ($A000-$BFFF), 1 = ROM ($C000-$FFFF)
268
int      g_bank_sel    = 0;      // selected bank
269
uint16_t g_bank_goto   = 0xA000; // goto target (real address within the window)
270
bool     g_bank_all    = false;  // search across all banks at once
271
272
uint16_t bank_base() { return g_bank_region == 0 ? 0xA000 : 0xC000; }
273
size_t   bank_win()  { return g_bank_region == 0 ? 0x2000 : 0x4000; }
274
275
int
276
bank_count()
277
{
278
    if (g_bank_region == 0)
279
        return num_ram_banks > 0 ? (int)num_ram_banks : 1;
280
    int n = memory_get_num_rom_banks();
281
    return n > 0 ? n : 1;
282
}
283
284
ImU8
285
bank_read_fn(const ImU8 *, size_t off, void *)
286
{
287
    return debug_ui_read6502((uint16_t)(bank_base() + off), 0, (int16_t)g_bank_sel);
288
}
289
290
void
291
bank_write_fn(ImU8 *, size_t off, ImU8 d, void *)
292
{
293
    debug_ui_write6502((uint16_t)(bank_base() + off), d, 0, (int16_t)g_bank_sel);
294
}
295
296
ImU8
297
bank_read_at(uint32_t a)
298
{
299
    return debug_ui_read6502((uint16_t)(bank_base() + a), 0, (int16_t)g_bank_sel);
300
}
301
302
// Flat read across ALL banks of the current region: flat = bank*win + off.
303
ImU8
304
bank_read_at_flat(uint32_t flat)
305
{
306
    size_t   w    = bank_win();
307
    int      bank = (int)(flat / w);
308
    uint32_t off  = (uint32_t)(flat % w);
309
    return debug_ui_read6502((uint16_t)(bank_base() + off), 0, (int16_t)bank);
310
}
311
312
// A dummy base pointer for the callback-driven editors (never dereferenced:
313
// every access in MemoryEditor is guarded by ReadFn/WriteFn).
314
unsigned char g_dummy_base = 0;
315
316
MemoryEditor &
317
cpu_editor()
318
{
319
    static MemoryEditor ed;
320
    static bool init = false;
321
    if (!init) {
322
        ed.ReadFn            = cpu_read_fn;
323
        ed.WriteFn           = cpu_write_fn;
324
        ed.BgColorFn         = cpu_bg_color_fn;
325
        ed.HighlightColor    = SEL_HIGHLIGHT;
326
        ed.OptShowDataPreview = true;
327
        ed.OptShowOptions     = true;
328
        ed.PreviewDataType    = ImGuiDataType_U8; // default the data preview to uint8
329
        init = true;
330
    }
331
    return ed;
332
}
333
334
MemoryEditor &
335
vram_editor()
336
{
337
    static MemoryEditor ed;
338
    static bool init = false;
339
    if (!init) {
340
        ed.ReadFn             = vram_read_fn;
341
        ed.WriteFn            = vram_write_fn;
342
        ed.HighlightColor     = SEL_HIGHLIGHT;
343
        ed.OptShowDataPreview = true;
344
        ed.OptShowOptions     = true;
345
        ed.PreviewDataType    = ImGuiDataType_U8; // default the data preview to uint8
346
        ed.OptAddrDigitsCount = 5; // 0x1FFFF needs 5 hex digits
347
        init = true;
348
    }
349
    return ed;
350
}
351
352
MemoryEditor &
353
bank_editor()
354
{
355
    static MemoryEditor ed;
356
    static bool init = false;
357
    if (!init) {
358
        ed.ReadFn             = bank_read_fn;
359
        ed.WriteFn            = bank_write_fn;
360
        ed.HighlightColor     = SEL_HIGHLIGHT;
361
        ed.OptShowDataPreview = true;
362
        ed.OptShowOptions     = true;
363
        ed.PreviewDataType    = ImGuiDataType_U8;
364
        init = true;
365
    }
366
    return ed;
367
}
368
369
// ---- Search ----------------------------------------------------------------
370
// Find a hex-byte sequence ("A9 00 8D") or an ASCII string in the currently
371
// viewed space, with Find Next / Find Prev and match highlighting.
372
373
struct SearchState {
374
    char buf[64] = "";
375
    bool hex     = true;   // hex-bytes vs ASCII
376
    long last    = -1;     // address of the last match (start of the run)
377
    bool notfound = false; // last search failed
378
};
379
380
SearchState g_cpu_search;
381
SearchState g_vram_search;
382
SearchState g_bank_search;
383
384
// Plain-address readers for the search (bank-aware for CPU space).
385
ImU8 cpu_read_at(uint32_t a)  {
386
    if (g_cpu_bank != 0) return debug_ui_read6502((uint16_t)a, (uint8_t)g_cpu_bank, DEBUG_UI_CURRENT_BANK);
387
    return debug_ui_read6502((uint16_t)a, 0, cpu_bank_for((uint16_t)a));
388
}
389
ImU8 vram_read_at(uint32_t a) { return video_space_read(a); }
390
391
// Parse "A9 00 8D" / "A9008D" into bytes (whitespace ignored, nibbles paired).
392
bool
393
parse_hex_pattern(const char *s, std::vector<uint8_t> &out)
394
{
395
    out.clear();
396
    int hi = -1;
397
    for (const char *p = s; *p; ++p) {
398
        unsigned char c = (unsigned char)*p;
399
        if (isspace(c)) continue;
400
        if (!isxdigit(c)) return false;
401
        int v = (c <= '9') ? (c - '0') : (tolower(c) - 'a' + 10);
402
        if (hi < 0) {
403
            hi = v;
404
        } else {
405
            out.push_back((uint8_t)((hi << 4) | v));
406
            hi = -1;
407
        }
408
    }
409
    if (hi >= 0) out.push_back((uint8_t)hi); // trailing single nibble → low byte
410
    return !out.empty();
411
}
412
413
bool
414
parse_ascii_pattern(const char *s, std::vector<uint8_t> &out)
415
{
416
    out.clear();
417
    for (const char *p = s; *p; ++p) out.push_back((uint8_t)*p);
418
    return !out.empty();
419
}
420
421
// Scan for `pat` starting at candidate position `from`, wrapping over the whole
422
// space. dir >= 0 searches forward, dir < 0 backward. Returns the match start or
423
// -1. Only called on a button press, so a linear scan is fine.
424
long
425
mem_find(ImU8 (*readfn)(uint32_t), size_t size, const std::vector<uint8_t> &pat, long from, int dir)
426
{
427
    size_t m = pat.size();
428
    if (m == 0 || size < m) return -1;
429
    long span = (long)(size - m) + 1;   // candidate start positions [0, size-m]
430
    from = ((from % span) + span) % span;
431
    for (long k = 0; k < span; k++) {
432
        long i = (dir >= 0) ? ((from + k) % span) : (((from - k) % span + span) % span);
433
        bool ok = true;
434
        for (size_t j = 0; j < m; j++) {
435
            if (readfn((uint32_t)i + (uint32_t)j) != pat[j]) { ok = false; break; }
436
        }
437
        if (ok) return i;
438
    }
439
    return -1;
440
}
441
442
// Search UI row for one tab. Highlights the match in `ed` on success.
443
void
444
render_search(MemoryEditor &ed, SearchState &st, ImU8 (*readfn)(uint32_t), size_t size)
445
{
446
    ImGui::SetNextItemWidth(160);
447
    bool enter = ImGui::InputText("##search", st.buf, sizeof(st.buf),
448
                                  ImGuiInputTextFlags_EnterReturnsTrue);
449
    ImGui::SameLine();
450
    ImGui::Checkbox("hex", &st.hex);
451
    ImGui::SameLine();
452
    bool next = ImGui::Button("Find Next") || enter;
453
    ImGui::SameLine();
454
    bool prev = ImGui::Button("Find Prev");
455
456
    if (next || prev) {
457
        std::vector<uint8_t> pat;
458
        bool ok = st.hex ? parse_hex_pattern(st.buf, pat) : parse_ascii_pattern(st.buf, pat);
459
        st.notfound = false;
460
        if (ok) {
461
            long from = (st.last < 0) ? (next ? 0 : (long)size) : (next ? st.last + 1 : st.last - 1);
462
            long m = mem_find(readfn, size, pat, from, next ? +1 : -1);
463
            if (m >= 0) {
464
                st.last = m;
465
                ed.GotoAddrAndHighlight((size_t)m, (size_t)m + pat.size());
466
            } else {
467
                st.notfound = true;
468
            }
469
        } else {
470
            st.notfound = true;
471
        }
472
    }
473
    if (st.notfound) {
474
        ImGui::SameLine();
475
        ImGui::TextColored(ImVec4(0.95f, 0.55f, 0.35f, 1.0f), "no match");
476
    } else if (st.last >= 0) {
477
        ImGui::SameLine();
478
        ImGui::TextDisabled("@ $%lX", (unsigned long)st.last);
479
    }
480
}
481
482
// ---- Tabs ------------------------------------------------------------------
483
484
// Drag-selection over a memory view.
485
//
486
// The vendored MemoryEditor only tracks a single byte (DataPreviewAddr), but it
487
// does publish what the mouse is over, which is enough to build a range on top
488
// of it without forking the widget. A plain click still edits a byte as before;
489
// only an actual drag starts a selection.
490
struct MemSelection {
491
    bool     active  = false;  // a completed selection exists
492
    bool     dragging = false;
493
    size_t   anchor  = 0;
494
    size_t   lo      = 0;
495
    size_t   hi      = 0;      // inclusive
496
497
    size_t   count() const { return active ? (hi - lo + 1) : 0; }
498
499
    void clear() { active = false; dragging = false; }
500
501
    // Call every frame, after the editor has drawn (so MouseHovered is current).
502
    void update(const MemoryEditor &ed)
503
    {
504
        const bool over = ed.MouseHovered;
505
        const size_t at = ed.MouseHoveredAddr;
506
507
        if (over && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
508
            anchor   = at;
509
            dragging = true;
510
            active   = false; // a fresh click drops any previous selection
511
        }
512
        if (dragging && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
513
            if (over) {
514
                lo     = anchor < at ? anchor : at;
515
                hi     = anchor < at ? at : anchor;
516
                active = (hi > lo); // a one-byte "range" is just a click
517
            }
518
        }
519
        if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
520
            dragging = false;
521
        }
522
    }
523
524
    // Call before the editor draws.
525
    //
526
    // Clicking a byte puts it into edit mode, and the resulting InputText
527
    // becomes ImGui's ActiveId. ImGui::IsItemHovered() returns false for every
528
    // other item while something is active, so the editor stops reporting
529
    // MouseHovered as soon as the drag leaves the first byte - which meant a
530
    // selection could never grow past one address.
531
    //
532
    // So cancel edit mode as soon as the mouse actually drags. The InputText is
533
    // then never submitted, nothing takes ActiveId, and hover keeps working for
534
    // the rest of the drag. A plain click never crosses the drag threshold, so
535
    // it still opens the byte editor exactly as before.
536
    void apply(MemoryEditor &ed) const
537
    {
538
        if (active) {
539
            ed.HighlightMin = lo;
540
            ed.HighlightMax = hi + 1; // editor's range is [min, max)
541
        }
542
        if (dragging && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
543
            ed.DataEditingAddr      = (size_t)-1;
544
            ed.DataEditingTakeFocus = false;
545
        }
546
    }
547
};
548
549
// The address a context menu should act on: the selection start if there is
550
// one, else whatever the mouse is over, else the currently selected byte.
551
uint16_t
552
mem_context_addr(const MemoryEditor &ed, const MemSelection &sel, uint16_t fallback)
553
{
554
    if (sel.active) {
555
        return (uint16_t)sel.lo;
556
    }
557
    if (ed.MouseHovered) {
558
        return (uint16_t)ed.MouseHoveredAddr;
559
    }
560
    if (ed.DataPreviewAddr != (size_t)-1) {
561
        return (uint16_t)ed.DataPreviewAddr;
562
    }
563
    return fallback;
564
}
565
566
// Right-click menu over a memory view. Kept generic so every tab can offer the
567
// same actions against its own address space and bank. `base` is added to
568
// editor offsets to form real addresses (0 for the CPU view, the window base
569
// for the banked view).
570
void
571
mem_context_menu(const char *id, MemSelection &sel, int16_t bank, uint16_t addr, uint16_t base)
572
{
573
    // The editor draws into a child window, so open the popup against the
574
    // hovered child rather than the panel window.
575
    if (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows) &&
576
        ImGui::IsMouseReleased(ImGuiMouseButton_Right)) {
577
        ImGui::OpenPopup(id);
578
    }
579
    if (!ImGui::BeginPopup(id)) {
580
        return;
581
    }
582
583
    const size_t   n     = sel.count();
584
    const uint16_t start = n ? (uint16_t)(base + sel.lo) : addr;
585
    const uint16_t end   = n ? (uint16_t)(base + sel.hi) : addr;
586
587
    if (n > 1) {
588
        ImGui::TextDisabled("$%04X-$%04X  (%d bytes)", start, end, (int)n);
589
    } else {
590
        ImGui::TextDisabled("$%04X", start);
591
    }
592
    ImGui::Separator();
593
594
    const uint16_t len = (uint16_t)(n > 1 ? n : 1);
595
596
    if (ImGui::MenuItem(n > 1 ? "Add range to watch" : "Add to watch")) {
597
        debug_ui_request_watch(start, bank, len);
598
    }
599
    if (ImGui::IsItemHovered()) {
600
        ImGui::SetTooltip(n > 1 ? "Add %d bytes from $%04X to the CPU panel's watch list\n"
601
                                  "(bank %d). Ranges over 16 bytes become several rows."
602
                                : "Add $%04X to the CPU panel's watch list (bank %d).",
603
                          n > 1 ? (int)n : (int)start, n > 1 ? (int)start : (int)bank, (int)bank);
604
    }
605
606
    if (ImGui::MenuItem("Copy address")) {
607
        char buf[24];
608
        if (n > 1)
609
            snprintf(buf, sizeof buf, "$%04X-$%04X", start, end);
610
        else
611
            snprintf(buf, sizeof buf, "$%04X", start);
612
        ImGui::SetClipboardText(buf);
613
    }
614
615
    ImGui::Separator();
616
617
    // A write watchpoint halts the machine when the running program STORES to
618
    // the address - the way to find out what is clobbering a value. Note it
619
    // triggers on any write, even one that stores the same value back, so it is
620
    // "break on write" rather than "break on change".
621
    //
622
    // Tested with the same key it is added and removed under. Asking
623
    // DEBUGCheckWatchPoint() instead answers a different question -- "would a
624
    // write here fire anything", which is wildcard-aware -- so a watchpoint set
625
    // on one specific bank made this read "Stop breaking on write" while the
626
    // removal, being an exact match on ANY, found nothing and did nothing.
627
    const bool has_wp = debug_wp_exists(start, DEBUG_BANK_ANY);
628
    if (ImGui::MenuItem(has_wp ? "Stop breaking on write" : "Break on write")) {
629
        if (has_wp)
630
            DEBUGRemoveWatchPoint(start);
631
        else
632
            DEBUGAddWatchPoint(start, len);
633
    }
634
    if (ImGui::IsItemHovered()) {
635
        ImGui::SetTooltip("Halt the machine as soon as the program writes to %s,\n"
636
                          "so you can see what is doing the writing.\n\n"
637
                          "Fires on any store, including one that writes back the same\n"
638
                          "value - it is not a change detector.\n"
639
                          "(%d of %d in use.)",
640
                          n > 1 ? "this range" : "this address", debug_wp_count(), MAX_WP);
641
    }
642
643
    if (sel.active) {
644
        ImGui::Separator();
645
        if (ImGui::MenuItem("Clear selection")) {
646
            sel.clear();
647
        }
648
    }
649
650
    ImGui::EndPopup();
651
}
652
653
// Hover tooltip for the byte under the mouse (uses MemoryEditor's public
654
// MouseHovered / MouseHoveredAddr). Shows the byte as hex/dec/signed/bin/ASCII
655
// plus the 16-bit little-endian word at that address, for quick inspection.
656
// `base` is added to the (0-based) editor offset for the displayed address.
657
void
658
mem_hover_tooltip(MemoryEditor &ed, ImU8 (*readfn)(uint32_t), size_t size, int addr_digits, uint32_t base)
659
{
660
    if (!ed.MouseHovered)
661
        return;
662
    uint32_t off = (uint32_t)ed.MouseHoveredAddr;
663
    uint8_t  b   = readfn(off);
664
    uint32_t w   = b;
665
    if (off + 1 < size)
666
        w |= (uint32_t)readfn(off + 1) << 8;
667
    ImGui::BeginTooltip();
668
    ImGui::Text("Addr  : $%0*X", addr_digits, (unsigned)(base + off));
669
    ImGui::Separator();
670
    ImGui::TextDisabled("byte");
671
    dbgui_value_lines(b, 1);
672
    ImGui::Separator();
673
    ImGui::TextDisabled("word (LE)");
674
    dbgui_value_lines(w, 2);
675
    ImGui::EndTooltip();
676
}
677
678
void
679
render_cpu_tab()
680
{
681
    MemoryEditor &ed = cpu_editor();
682
683
    // Diff the CPU space whenever execution stops, so changed bytes can be
684
    // tinted (see MemChangeTracker).
685
    if (debug_ui_settings().mem_highlight_changes) {
686
        g_mem_changes.update();
687
    }
688
689
    if (!g_banks_init) {
690
        g_ram_bank   = memory_get_ram_bank();
691
        g_rom_bank   = memory_get_rom_bank();
692
        g_banks_init = true;
693
    }
694
695
    // Honor a cross-panel goto request (from Symbols / Call Stack panels).
696
    {
697
        uint16_t gaddr;
698
        uint8_t  gbank;
699
        if (debug_ui_peek_goto(&gaddr, &gbank)) {
700
            g_cpu_goto = gaddr;
701
            if (is_gen2) g_cpu_bank = gbank; // gbank is the 65C816 program bank (K)
702
            ed.GotoAddrAndHighlight(gaddr, gaddr);
703
        }
704
    }
705
706
    // Status line.
707
    if (is_gen2)
708
        ImGui::Text("K $%02X  PC $%02X:%04X   A $%02X  X $%02X  Y $%02X  SP $%04X",
709
                    regs.k, regs.k, regs.pc, regs.a, regs.xl, regs.yl, regs.sp);
710
    else
711
        ImGui::Text("PC $%04X   A $%02X  X $%02X  Y $%02X  SP $%04X",
712
                    regs.pc, regs.a, regs.xl, regs.yl, regs.sp);
713
714
    // Goto / navigation.
715
    //
716
    // Deliberately NOT EnterReturnsTrue: with that flag ImGui only applies the
717
    // typed text when Enter is pressed and discards it on any other
718
    // deactivation, so typing an address and then clicking Goto threw the
719
    // address away and jumped to the previous one. Without it the value is
720
    // applied as it is edited, and IsItemDeactivatedAfterEdit() gives us the
721
    // Enter/commit trigger. AutoSelectAll so typing replaces rather than
722
    // appends to the address already shown.
723
    ImGui::SetNextItemWidth(80);
724
    ImGui::InputScalar("##cpugoto", ImGuiDataType_U16, &g_cpu_goto, nullptr, nullptr,
725
                       "%04X",
726
                       ImGuiInputTextFlags_CharsHexadecimal |
727
                           ImGuiInputTextFlags_AutoSelectAll);
728
    bool go = ImGui::IsItemDeactivatedAfterEdit();
729
    if (ImGui::IsItemHovered())
730
        ImGui::SetTooltip("Address to jump to (hex). Press Enter or click Goto.");
731
    ImGui::SameLine();
732
    if (ImGui::Button("Goto")) go = true;
733
    if (ImGui::IsItemHovered())
734
        ImGui::SetTooltip("Scroll the view to the address on the left.");
735
    ImGui::SameLine();
736
    if (ImGui::Button("Goto PC")) { g_cpu_goto = regs.pc; if (is_gen2) g_cpu_bank = regs.k; go = true; }
737
    if (ImGui::IsItemHovered())
738
        ImGui::SetTooltip("Scroll to the current program counter ($%04X).", regs.pc);
739
    if (go) ed.GotoAddrAndHighlight(g_cpu_goto, g_cpu_goto);
740
741
    // Highlighting is a preference, so it lives in System > Settings rather than
742
    // being duplicated here. Clearing is an action, so it belongs on the panel.
743
    DebugUiSettings &st = debug_ui_settings();
744
    if (st.mem_highlight_changes) {
745
        ImGui::SameLine(0, 16);
746
        if (ImGui::SmallButton("Clear highlights")) {
747
            g_mem_changes.reset();
748
        }
749
        if (ImGui::IsItemHovered())
750
            ImGui::SetTooltip("Forget the current change highlights and re-baseline from\n"
751
                              "here. Changed bytes pulse red and fade over %.2f s;\n"
752
                              "turn that off or retune it in System > Settings.",
753
                              st.mem_highlight_seconds);
754
    }
755
756
    // CPU bank byte (X16 GS / gen2): 0 = classic map (I/O + windowed banks),
757
    // 1..num_banks-1 = a flat 64K RAM page selected by the 65C816 bank byte.
758
    if (is_gen2) {
759
        int bank_max = num_banks > 0 ? (int)num_banks - 1 : 255;
760
        ImGui::SetNextItemWidth(120);
761
        if (ImGui::InputInt("CPU bank (K)", &g_cpu_bank)) {
762
            if (g_cpu_bank < 0) g_cpu_bank = 0;
763
            if (g_cpu_bank > bank_max) g_cpu_bank = bank_max;
764
        }
765
        ImGui::SameLine();
766
        if (ImGui::SmallButton("Bank 0")) g_cpu_bank = 0;
767
        ImGui::SameLine();
768
        if (ImGui::SmallButton("Sync K")) g_cpu_bank = regs.k;
769
        ImGui::SameLine();
770
        ImGui::TextDisabled("(?)");
771
        if (ImGui::IsItemHovered())
772
            ImGui::SetTooltip("X16 GS 24-bit RAM. Bank 0 is the classic map (I/O at $9F00,\n"
773
                              "windowed $A000/$C000 banks below). Banks 1-%d are flat 64K RAM\n"
774
                              "pages addressed by the 65C816 bank byte. The RAM/ROM window\n"
775
                              "selectors below only apply in bank 0.", bank_max);
776
    }
777
778
    // Bank selectors.
779
    int ram_max = num_ram_banks > 0 ? (int)num_ram_banks - 1 : 255;
780
    ImGui::SetNextItemWidth(120);
781
    if (ImGui::InputInt("RAM bank ($A000)", &g_ram_bank)) {
782
        if (g_ram_bank < 0) g_ram_bank = 0;
783
        if (g_ram_bank > ram_max) g_ram_bank = ram_max;
784
    }
785
    if (ImGui::IsItemHovered())
786
        ImGui::SetTooltip("Which RAM bank this view reads through the $A000-$BFFF window.\n"
787
                          "Independent of the bank the machine currently has mapped\n"
788
                          "(currently %d), so you can inspect one bank while it runs in\n"
789
                          "another.", (int)memory_get_ram_bank());
790
    ImGui::SameLine();
791
    ImGui::SetNextItemWidth(120);
792
    if (ImGui::InputInt("ROM bank ($C000)", &g_rom_bank)) {
793
        if (g_rom_bank < 0) g_rom_bank = 0;
794
        if (g_rom_bank > 255) g_rom_bank = 255;
795
    }
796
    if (ImGui::IsItemHovered())
797
        ImGui::SetTooltip("Which ROM bank this view reads through the $C000-$FFFF window.\n"
798
                          "The machine currently has bank %d mapped.", (int)memory_get_rom_bank());
799
    ImGui::SameLine();
800
    if (ImGui::SmallButton("Sync banks")) {
801
        g_ram_bank = memory_get_ram_bank();
802
        g_rom_bank = memory_get_rom_bank();
803
    }
804
    if (ImGui::IsItemHovered())
805
        ImGui::SetTooltip("Snap the two selectors above to whatever the machine has mapped\n"
806
                          "right now (RAM %d, ROM %d), so the view matches what the running\n"
807
                          "program sees.", (int)memory_get_ram_bank(), (int)memory_get_rom_bank());
808
809
    // Write watchpoints used to be set from a row here. That is now done by
810
    // right-clicking the byte (or a drag-selected range, which supplies the
811
    // length), and the Breakpoints panel lists and manages them - so the row
812
    // was three-way redundant and is gone.
813
814
    // Search.
815
    render_search(ed, g_cpu_search, cpu_read_at, CPU_SIZE);
816
817
    ImGui::Separator();
818
    // In gen2 show 24-bit addresses (bank<<16 | offset); 16-bit otherwise.
819
    uint32_t addr_base = is_gen2 ? ((uint32_t)g_cpu_bank << 16) : 0u;
820
    ed.OptAddrDigitsCount = is_gen2 ? 6 : 0; // 0 = auto (16-bit)
821
    static MemSelection sel;
822
    sel.apply(ed);
823
    ed.DrawContents(&g_dummy_base, CPU_SIZE, addr_base);
824
    sel.update(ed);
825
    mem_hover_tooltip(ed, cpu_read_at, CPU_SIZE, is_gen2 ? 6 : 4, addr_base);
826
    {
827
        const uint16_t at = mem_context_addr(ed, sel, g_cpu_goto);
828
        mem_context_menu("cpu_ctx", sel, cpu_bank_for(at), at, 0);
829
    }
830
    if (sel.count() > 1) {
831
        ImGui::TextDisabled("Selected $%04X-$%04X (%d bytes) - right-click for actions.",
832
                            (unsigned)sel.lo, (unsigned)sel.hi, (int)sel.count());
833
    }
834
}
835
836
void
837
render_vram_tab()
838
{
839
    MemoryEditor &ed = vram_editor();
840
841
    ImGui::TextDisabled("VERA video memory  $00000-$1FFFF");
842
    ImGui::SetNextItemWidth(90);
843
    bool go = false;
844
    ImGui::InputScalar("##vramgoto", ImGuiDataType_U32, &g_vram_goto, nullptr, nullptr,
845
                       "%05X",
846
                       ImGuiInputTextFlags_CharsHexadecimal |
847
                           ImGuiInputTextFlags_AutoSelectAll);
848
    go = ImGui::IsItemDeactivatedAfterEdit();
849
    if (ImGui::IsItemHovered())
850
        ImGui::SetTooltip("VRAM address to jump to (hex, $00000-$1FFFF).");
851
    ImGui::SameLine();
852
    if (ImGui::Button("Goto")) go = true;
853
    if (go) {
854
        if (g_vram_goto >= VRAM_SIZE) g_vram_goto = VRAM_SIZE - 1;
855
        ed.GotoAddrAndHighlight(g_vram_goto, g_vram_goto);
856
    }
857
858
    // Search.
859
    render_search(ed, g_vram_search, vram_read_at, VRAM_SIZE);
860
861
    ImGui::Separator();
862
    ed.DrawContents(&g_dummy_base, VRAM_SIZE, 0x0000);
863
    mem_hover_tooltip(ed, vram_read_at, VRAM_SIZE, 5, 0);
864
}
865
866
// Banked-memory search: within the selected bank, or (all-banks) flat across
867
// every bank of the current region, jumping to the matching bank on a hit.
868
void
869
render_bank_search(MemoryEditor &ed, uint16_t base, size_t win, int nbanks)
870
{
871
    SearchState &st = g_bank_search;
872
    ImGui::SetNextItemWidth(160);
873
    bool enter = ImGui::InputText("##banksearch", st.buf, sizeof(st.buf),
874
                                  ImGuiInputTextFlags_EnterReturnsTrue);
875
    ImGui::SameLine();
876
    ImGui::Checkbox("hex", &st.hex);
877
    ImGui::SameLine();
878
    if (ImGui::Checkbox("all banks", &g_bank_all))
879
        st.last = -1; // mode changed: last-match index semantics differ
880
    ImGui::SameLine();
881
    bool next = ImGui::Button("Find Next") || enter;
882
    ImGui::SameLine();
883
    bool prev = ImGui::Button("Find Prev");
884
885
    if (next || prev) {
886
        std::vector<uint8_t> pat;
887
        bool ok = st.hex ? parse_hex_pattern(st.buf, pat) : parse_ascii_pattern(st.buf, pat);
888
        st.notfound = false;
889
        if (!ok) {
890
            st.notfound = true;
891
        } else if (g_bank_all) {
892
            size_t total = (size_t)nbanks * win;
893
            long   from  = (st.last < 0) ? (next ? 0 : (long)total)
894
                                         : (next ? st.last + 1 : st.last - 1);
895
            long m = mem_find(bank_read_at_flat, total, pat, from, next ? +1 : -1);
896
            if (m >= 0) {
897
                st.last    = m;
898
                g_bank_sel = (int)((size_t)m / win);
899
                long off   = (long)((size_t)m % win);
900
                ed.GotoAddrAndHighlight((size_t)off, (size_t)off + pat.size());
901
            } else {
902
                st.notfound = true;
903
            }
904
        } else {
905
            long from = (st.last < 0) ? (next ? 0 : (long)win)
906
                                      : (next ? st.last + 1 : st.last - 1);
907
            long m = mem_find(bank_read_at, win, pat, from, next ? +1 : -1);
908
            if (m >= 0) {
909
                st.last = m;
910
                ed.GotoAddrAndHighlight((size_t)m, (size_t)m + pat.size());
911
            } else {
912
                st.notfound = true;
913
            }
914
        }
915
    }
916
917
    if (st.notfound) {
918
        ImGui::SameLine();
919
        ImGui::TextColored(ImVec4(0.95f, 0.55f, 0.35f, 1.0f), "no match");
920
    } else if (st.last >= 0 && g_bank_all) {
921
        ImGui::SameLine();
922
        ImGui::TextDisabled("bank %d @ $%04X", (int)((size_t)st.last / win),
923
                            (unsigned)(base + (size_t)st.last % win));
924
    } else if (st.last >= 0) {
925
        ImGui::SameLine();
926
        ImGui::TextDisabled("@ $%04X", (unsigned)(base + (unsigned)st.last));
927
    }
928
}
929
930
// Banked memory: view any RAM/ROM bank's window directly (independent of what is
931
// mapped in), stepping through all banks with the selector, and optionally
932
// searching across every bank at once.
933
void
934
render_bank_tab()
935
{
936
    MemoryEditor &ed = bank_editor();
937
938
    // Region toggle (recompute bank geometry immediately after).
939
    ImGui::RadioButton("RAM  $A000-$BFFF", &g_bank_region, 0);
940
    ImGui::SameLine();
941
    ImGui::RadioButton("ROM  $C000-$FFFF", &g_bank_region, 1);
942
943
    int      nbanks   = bank_count();
944
    int      bank_max = nbanks - 1;
945
    uint16_t base     = bank_base();
946
    size_t   win      = bank_win();
947
    if (g_bank_sel > bank_max)
948
        g_bank_sel = bank_max;
949
    if (g_bank_sel < 0)
950
        g_bank_sel = 0;
951
    if (g_bank_goto < base || g_bank_goto >= base + win)
952
        g_bank_goto = base;
953
954
    ImGui::Text("%s   bank %d of %d   (%d KB each = %d KB total)",
955
                g_bank_region == 0 ? "Banked RAM" : "Banked ROM", g_bank_sel, nbanks,
956
                (int)(win / 1024), (int)(nbanks * win / 1024));
957
    ImGui::TextDisabled("Browse assets/code in any bank, independent of the mapped bank.");
958
959
    // Bank selector: Prev / number / Next.
960
    if (ImGui::Button("< Prev") && g_bank_sel > 0)
961
        g_bank_sel--;
962
    ImGui::SameLine();
963
    ImGui::SetNextItemWidth(90);
964
    ImGui::InputInt("Bank", &g_bank_sel);
965
    ImGui::SameLine();
966
    if (ImGui::Button("Next >") && g_bank_sel < bank_max)
967
        g_bank_sel++;
968
    if (g_bank_sel < 0)
969
        g_bank_sel = 0;
970
    if (g_bank_sel > bank_max)
971
        g_bank_sel = bank_max;
972
    ImGui::SameLine();
973
    ImGui::TextDisabled("(mapped: RAM %d / ROM %d)", (int)memory_get_ram_bank(),
974
                        (int)memory_get_rom_bank());
975
976
    // Goto within the window (accepts a real address in the region).
977
    ImGui::SetNextItemWidth(80);
978
    ImGui::InputScalar("##bankgoto", ImGuiDataType_U16, &g_bank_goto, nullptr, nullptr,
979
                       "%04X",
980
                       ImGuiInputTextFlags_CharsHexadecimal |
981
                           ImGuiInputTextFlags_AutoSelectAll);
982
    bool go = ImGui::IsItemDeactivatedAfterEdit();
983
    if (ImGui::IsItemHovered())
984
        ImGui::SetTooltip("Address within this bank's window ($%04X-$%04X).", base,
985
                          (unsigned)(base + win - 1));
986
    ImGui::SameLine();
987
    if (ImGui::Button("Goto"))
988
        go = true;
989
    if (go) {
990
        uint16_t a = g_bank_goto;
991
        if (a < base)
992
            a = base;
993
        if (a >= base + win)
994
            a = (uint16_t)(base + win - 1);
995
        ed.GotoAddrAndHighlight((size_t)(a - base), (size_t)(a - base));
996
    }
997
998
    render_bank_search(ed, base, win, nbanks);
999
1000
    ImGui::Separator();
1001
    ed.ReadOnly = (g_bank_region == 1); // ROM is not editable
1002
    static MemSelection bsel;
1003
    bsel.apply(ed);
1004
    ed.DrawContents(&g_dummy_base, win, base);
1005
    bsel.update(ed);
1006
    mem_hover_tooltip(ed, bank_read_at, win, 4, base);
1007
    // Editor offsets are window-relative here, so add the window base back to
1008
    // get a real address for the menu.
1009
    {
1010
        const uint16_t at =
1011
            (uint16_t)(base + mem_context_addr(ed, bsel, (uint16_t)(g_bank_goto - base)));
1012
        mem_context_menu("bank_ctx", bsel, (int16_t)g_bank_sel, at, base);
1013
    }
1014
    if (bsel.count() > 1) {
1015
        ImGui::TextDisabled("Selected $%04X-$%04X (%d bytes) - right-click for actions.",
1016
                            (unsigned)(base + bsel.lo), (unsigned)(base + bsel.hi),
1017
                            (int)bsel.count());
1018
    }
1019
}
1020
1021
void
1022
memory_panel_render(bool *p_open)
1023
{
1024
    if (ImGui::Begin("Memory", p_open)) {
1025
        dbgui_window_zoom("memory");
1026
        // A pending cross-panel goto targets the CPU view: bring that tab forward.
1027
        ImGuiTabItemFlags cpu_flags =
1028
            debug_ui_peek_goto(nullptr, nullptr) ? ImGuiTabItemFlags_SetSelected : 0;
1029
        if (ImGui::BeginTabBar("##mem_tabs", ImGuiTabBarFlags_DrawSelectedOverline)) {
1030
            if (ImGui::BeginTabItem("CPU", nullptr, cpu_flags)) {
1031
                render_cpu_tab();
1032
                ImGui::EndTabItem();
1033
            }
1034
            if (ImGui::BeginTabItem("Banked")) {
1035
                render_bank_tab();
1036
                ImGui::EndTabItem();
1037
            }
1038
            if (ImGui::BeginTabItem("VRAM")) {
1039
                render_vram_tab();
1040
                ImGui::EndTabItem();
1041
            }
1042
            ImGui::EndTabBar();
1043
        }
1044
    }
1045
    dbgui_window_end();
1046
}
1047
1048
} // namespace
1049
1050
static DebugPanelRegistration s_reg("Memory", memory_panel_render, true);