OXYGENHUB
◆ main enhanced-x16-emulator / src / debug_ui / panels / callstack_panel.cpp
6.7 KiB · C++ · last changed chore(): initial commit 25 days ago source blame history raw
1
// Commander X16 Emulator — ImGui "Call Stack" panel.
2
//
3
// The 6502/65C816 has no frame pointer, so this heuristically unwinds the
4
// hardware stack: frame 0 is the current PC, then it scans the stack for
5
// candidate return addresses whose preceding instruction is a JSR ($20). Each
6
// plausible caller is resolved to a symbol + source line. Click a frame to jump
7
// the Disassembly + Memory views there. Heuristic by nature (a stray stack byte
8
// pair can occasionally look like a return address), so treat deep frames as a
9
// hint, not gospel — but the JSR-precedes check keeps false positives low.
10
#include "imgui.h"
11
#include "debug_ui_panels.h"
12
#include "debug_ui_bridge.h"
13
#include "debug_ui_widgets.h" // regs, debug_ui_read6502, dbg_info_*, request_goto
14
15
#include <stdint.h>
16
#include <stdio.h>
17
18
extern "C" {
19
#include "dbg_info.h" // dbg_info_addr_to_source_nearest (not in the bridge)
20
}
21
22
namespace {
23
24
struct Frame {
25
    uint16_t addr;  // frame 0: current PC; callers: return address
26
    bool     is_pc; // frame 0 marker
27
};
28
29
void
30
callstack_panel_render(bool *p_open)
31
{
32
    if (!ImGui::Begin("Call Stack", p_open)) {
33
        dbgui_window_end();
34
        return;
35
    }
36
    dbgui_window_zoom("callstack");
37
38
    const uint8_t bank   = regs.k;
39
    const bool    native = regs.is65c816 && !regs.e;
40
41
    // Frame 0 = current PC; then heuristically unwind.
42
    Frame frames[64];
43
    int   nf     = 0;
44
    frames[nf++] = {regs.pc, true};
45
46
    // Stack scan range. 6502/emulation: page 1 ($0100-$01FF). Native: 16-bit SP,
47
    // scan a bounded window upward from SP+1.
48
    uint32_t sp    = regs.sp;
49
    uint32_t start = native ? (sp + 1) : (0x100 + (sp & 0xFF) + 1);
50
    uint32_t end   = native ? (sp + 1 + 256) : 0x200;
51
52
    for (uint32_t a = start; a + 1 < end && nf < 64;) {
53
        uint8_t  lo     = debug_ui_read6502((uint16_t)a, 0, DEBUG_UI_CURRENT_BANK);
54
        uint8_t  hi     = debug_ui_read6502((uint16_t)(a + 1), 0, DEBUG_UI_CURRENT_BANK);
55
        uint16_t pushed = (uint16_t)(lo | (hi << 8)); // JSR pushes (return-1)
56
        // A JSR ($20) at pushed-2 means execution resumes at pushed+1.
57
        uint8_t opc = debug_ui_read6502((uint16_t)(pushed - 2), bank, DEBUG_UI_CURRENT_BANK);
58
        if (opc == 0x20) {
59
            frames[nf++] = {(uint16_t)(pushed + 1), false};
60
            a += 2; // consume the return-address pair
61
        } else {
62
            a += 1;
63
        }
64
    }
65
66
    if (!dbg_info_is_loaded()) {
67
        ImGui::TextDisabled("(no .dbg loaded — addresses only; load debug info for source/symbols)");
68
    }
69
    ImGui::TextDisabled("Heuristic unwind (JSR-based). Click a frame to go there.");
70
71
    if (ImGui::BeginTable("callstack", 3,
72
                          ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
73
                              ImGuiTableFlags_ScrollY | DBGUI_TABLE_FLAGS_RESIZABLE)) {
74
        ImGui::TableSetupColumn("#", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("#", "99"));
75
        ImGui::TableSetupColumn("Address / Symbol", ImGuiTableColumnFlags_WidthStretch);
76
        ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_WidthStretch);
77
        ImGui::TableHeadersRow();
78
79
        for (int i = 0; i < nf; i++) {
80
            const Frame &f = frames[i];
81
            ImGui::TableNextRow();
82
            ImGui::PushID(i);
83
84
            ImGui::TableSetColumnIndex(0);
85
            if (f.is_pc)
86
                ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.35f, 1.0f), "%d", i);
87
            else
88
                ImGui::Text("%d", i);
89
90
            ImGui::TableSetColumnIndex(1);
91
            const char *label = nullptr;
92
            char        buf[128];
93
            bool        has_src =
94
                dbg_info_addr_to_source_banked(f.addr, (int)memory_get_ram_bank(), nullptr, nullptr);
95
            const char *nosrc = has_src ? "" : "  (no src)";
96
97
            // Frame 0 is the PC and often sits exactly on a label. Caller frames
98
            // are return addresses - the instruction after a JSR - which almost
99
            // never are, so an exact lookup left every caller anonymous: a frame
100
            // showed its symbol only while it was frame 0, then blanked the
101
            // moment a deeper call pushed it down. Fall back to the label the
102
            // address falls inside and show the offset from it.
103
            uint16_t    label_at = 0;
104
            if (dbg_info_addr_to_label(f.addr, &label) && label) {
105
                snprintf(buf, sizeof buf, "$%04X  %s%s", f.addr, label, nosrc);
106
            } else if (has_src && debug_ui_addr_to_label_nearest(f.addr, &label, &label_at) && label) {
107
                // Only when the address is really covered by the debug info -
108
                // the nearest label of an uncovered address is meaningless.
109
                snprintf(buf, sizeof buf, "$%04X  %s+%u", f.addr, label,
110
                         (unsigned)(f.addr - label_at));
111
            } else {
112
                snprintf(buf, sizeof buf, "$%04X%s", f.addr, nosrc);
113
            }
114
            if (ImGui::Selectable(buf, f.is_pc, ImGuiSelectableFlags_SpanAllColumns))
115
                debug_ui_request_goto(f.addr, bank);
116
117
            ImGui::TableSetColumnIndex(2);
118
            const char *file = nullptr;
119
            int         line = 0;
120
            if (dbg_info_addr_to_source_banked(f.addr, (int)memory_get_ram_bank(), &file, &line) && file) {
121
                const char *base = file;
122
                for (const char *p = file; *p; ++p)
123
                    if (*p == '/' || *p == '\\')
124
                        base = p + 1;
125
                ImGui::Text("%s:%d", base, line);
126
            } else {
127
                // No debug info for this frame — say WHY rather than just "-",
128
                // so a KERNAL call or a low-RAM far-call thunk (e.g. JSRFAR at
129
                // $02xx) is obvious instead of looking like missing symbols.
130
                const char *what;
131
                if (f.addr >= 0xC000)
132
                    what = "[KERNAL/ROM] no source";
133
                else if (f.addr >= 0xA000)
134
                    what = "[banked] no source";
135
                else if (f.addr < 0x0800)
136
                    what = "[stub/KERNAL RAM] no source";
137
                else
138
                    what = "[NOSRC]";
139
                ImGui::TextColored(ImVec4(0.70f, 0.70f, 0.78f, 1.0f), "%s", what);
140
                if (ImGui::IsItemHovered())
141
                    ImGui::SetTooltip("$%04X is not covered by the loaded .dbg.\n"
142
                                      "$C000-$FFFF is banked ROM (KERNAL/BASIC);\n"
143
                                      "$0200-$07FF usually holds KERNAL RAM routines\n"
144
                                      "such as the JSRFAR far-call trampoline.", f.addr);
145
            }
146
147
            ImGui::PopID();
148
        }
149
        ImGui::EndTable();
150
    }
151
152
    dbgui_window_end();
153
}
154
155
DebugPanelRegistration s_reg("Call Stack", callstack_panel_render, true);
156
157
} // namespace