OXYGENHUB
# 9120e87480a6 enhanced-x16-emulator / src / debug_ui / panels / breakpoints_panel.cpp
24.1 KiB · C++ · last changed chore(): initial commit 25 days ago source blame history raw
1
// Breakpoints panel — central manager for the debugger's breakpoints, plus a
2
// "run to address" control. Complements the Disassembly/Source panels (which
3
// toggle breakpoints inline) by giving one place to add, review, enable/disable,
4
// and clear them.
5
//
6
// Enable/disable belongs to the core: a disabled breakpoint keeps its entry,
7
// its owners, its condition and its hit count, and simply does not stop the
8
// machine. This panel used to implement it by deleting the entry and
9
// remembering it in a list of its own, which meant a disabled breakpoint
10
// vanished from the table and so showed no gutter marker anywhere else.
11
//
12
// The list below is therefore a view of the core's table, rebuilt each frame,
13
// rather than a superset of it with a lifetime of its own.
14
//
15
// All state is reached through the shared bridge (debug_ui_bridge.h).
16
#include "imgui.h"
17
#include "debug_ui_panels.h"
18
#include "debug_ui_bridge.h"
19
#include "debug_ui_widgets.h"
20
21
#include <cstdint>
22
#include <cstdio>
23
#include <vector>
24
25
namespace {
26
27
struct TrackedBP {
28
	int     pc;
29
	uint8_t bank;
30
	int     x16Bank;
31
	bool    enabled;
32
};
33
34
std::vector<TrackedBP> s_tracked;    // a view of the core's table, rebuilt each frame
35
int                    s_runto_addr = 0; // hex address for the "Run to" box
36
37
// "Add breakpoint" row state. See add_breakpoint_row().
38
int  s_add_addr         = 0;
39
int  s_add_bank         = 0;
40
bool s_add_bank_touched = false; // has the user chosen a bank by hand?
41
42
// A breakpoint is identified by (pc, bank, x16Bank), because the same $A000 in
43
// two RAM banks is two different breakpoints. Keying on (pc, bank) alone
44
// collapsed them into one row: the count under-reported, "Clear All" left the
45
// other one armed and the list repopulated itself on the next frame, and the
46
// enable checkbox flipped back on by itself because the survivor was still
47
// found.
48
void
49
sync_tracked(void)
50
{
51
	s_tracked.clear();
52
	const int n = debug_bp_count();
53
	s_tracked.reserve((size_t)(n < 0 ? 0 : n));
54
	for (int i = 0; i < n; i++) {
55
		const struct breakpoint *b = debug_bp_at(i);
56
		if (!b) {
57
			break;
58
		}
59
		s_tracked.push_back(TrackedBP{b->pc, b->bank, b->x16Bank, b->enabled});
60
	}
61
}
62
63
void
64
set_enabled(TrackedBP &t, bool enable)
65
{
66
	if (DEBUGSetBreakpointEnabled(t.pc, t.bank, t.x16Bank, enable)) {
67
		t.enabled = enable;
68
	}
69
}
70
71
// ---- Conditional-breakpoint editor -----------------------------------------
72
const char *const kOperandNames[] = {"A", "X", "Y", "SP", "P", "byte[addr]", "word[addr]"};
73
const char *const kOpNames[]      = {"==", "!=", "<", "<=", ">", ">="};
74
75
struct CondEdit {
76
	int  pc      = 0;
77
	int  operand = 0;
78
	int  op      = 0;
79
	int  addr    = 0;
80
	int  value   = 0;
81
	int  ignore  = 0;
82
	bool loaded  = false;
83
};
84
CondEdit s_edit;
85
86
// One-line summary of a breakpoint's condition/ignore for the table cell.
87
void
88
cond_summary(char *out, size_t n, const TrackedBP &t)
89
{
90
	int      has = 0, operand = 0, op = 0;
91
	uint16_t oaddr = 0;
92
	uint32_t value = 0, ignore = 0;
93
	bool     have = DEBUGGetBreakpointCondition(t.pc, t.bank, t.x16Bank, &has, &operand, &oaddr, &op, &value, &ignore);
94
	if (have && has) {
95
		if (operand == BP_OPND_BYTE)
96
			snprintf(out, n, "byte[$%04X]%s$%X", oaddr, kOpNames[op], value);
97
		else if (operand == BP_OPND_WORD)
98
			snprintf(out, n, "word[$%04X]%s$%X", oaddr, kOpNames[op], value);
99
		else
100
			snprintf(out, n, "%s%s$%X", kOperandNames[operand], kOpNames[op], value);
101
	} else if (have && ignore > 0) {
102
		snprintf(out, n, "ignore %u", ignore);
103
	} else {
104
		snprintf(out, n, "set...");
105
	}
106
}
107
108
// Per-row popup to edit the condition + ignore count + reset hits.
109
void
110
cond_editor_popup(const TrackedBP &t)
111
{
112
	if (!ImGui::BeginPopup("cond_edit"))
113
		return;
114
	if (!s_edit.loaded) {
115
		int      has = 0, operand = 0, op = 0;
116
		uint16_t oaddr = 0;
117
		uint32_t value = 0, ignore = 0;
118
		DEBUGGetBreakpointCondition(t.pc, t.bank, t.x16Bank, &has, &operand, &oaddr, &op, &value, &ignore);
119
		s_edit.pc      = t.pc;
120
		s_edit.operand = operand;
121
		s_edit.op      = op;
122
		s_edit.addr    = oaddr;
123
		s_edit.value   = (int)value;
124
		s_edit.ignore  = (int)ignore;
125
		s_edit.loaded  = true;
126
	}
127
128
	ImGui::Text("Breakpoint $%04X", (unsigned)(t.pc & 0xFFFF));
129
	ImGui::Separator();
130
	ImGui::TextUnformatted("Stop only when:");
131
	ImGui::SetNextItemWidth(dbgui_combo_width("byte at $"));
132
	ImGui::Combo("##opnd", &s_edit.operand, kOperandNames, IM_ARRAYSIZE(kOperandNames));
133
	ImGui::SetItemTooltip("What to test: a CPU register, or the byte/word at an address.");
134
	ImGui::SameLine();
135
	if (s_edit.operand == BP_OPND_BYTE || s_edit.operand == BP_OPND_WORD) {
136
		ImGui::SetNextItemWidth(dbgui_field_width("FFFF"));
137
		ImGui::InputScalar("##caddr", ImGuiDataType_S32, &s_edit.addr, nullptr, nullptr, "%X",
138
		                   ImGuiInputTextFlags_CharsHexadecimal);
139
		ImGui::SetItemTooltip("Address to read, in HEX. \"10\" means $10 (16), not ten.");
140
		ImGui::SameLine();
141
	}
142
	ImGui::SetNextItemWidth(dbgui_combo_width(">="));
143
	ImGui::Combo("##op", &s_edit.op, kOpNames, IM_ARRAYSIZE(kOpNames));
144
	ImGui::SetItemTooltip("Comparison to apply.");
145
	ImGui::SameLine();
146
	ImGui::SetNextItemWidth(dbgui_field_width("FFFF"));
147
	ImGui::InputScalar("= $##cval", ImGuiDataType_S32, &s_edit.value, nullptr, nullptr, "%X",
148
	                   ImGuiInputTextFlags_CharsHexadecimal);
149
	ImGui::SetItemTooltip("Value to compare against, in HEX (hence the $).\n"
150
	                      "\"10\" means $10 (16), not ten.");
151
	ImGui::SameLine();
152
	ImGui::TextDisabled("(hex)");
153
	ImGui::SetItemTooltip("Both the address and value fields above are hexadecimal.");
154
155
	ImGui::SetNextItemWidth(dbgui_field_width("99999") + ImGui::GetFrameHeight() * 2.0f);
156
	ImGui::InputInt("ignore first N hits", &s_edit.ignore);
157
	ImGui::SetItemTooltip("Decimal, unlike the fields above. Let the breakpoint pass this\n"
158
	                      "many matching hits before it actually stops - useful for reaching\n"
159
	                      "a particular iteration of a loop.");
160
	if (s_edit.ignore < 0)
161
		s_edit.ignore = 0;
162
163
	ImGui::Text("Hit count: %u", DEBUGGetBreakpointHits(t.pc, t.bank, t.x16Bank));
164
	ImGui::SetItemTooltip("Times this breakpoint's condition has matched since it was set or\n"
165
	                      "last reset. Only counts while the machine is running - sitting at\n"
166
	                      "the breakpoint does not add to it.");
167
	ImGui::SameLine();
168
	if (ImGui::SmallButton("Reset hits"))
169
		DEBUGResetBreakpointHits(t.pc, t.bank, t.x16Bank);
170
171
	ImGui::Separator();
172
	if (ImGui::Button("Apply")) {
173
		DEBUGSetBreakpointCondition(t.pc, t.bank, t.x16Bank, s_edit.operand, (uint16_t)s_edit.addr,
174
		                            s_edit.op, (uint32_t)s_edit.value);
175
		DEBUGSetBreakpointIgnore(t.pc, t.bank, t.x16Bank, (uint32_t)s_edit.ignore);
176
		ImGui::CloseCurrentPopup();
177
	}
178
	ImGui::SameLine();
179
	if (ImGui::Button("Clear condition")) {
180
		DEBUGClearBreakpointCondition(t.pc, t.bank, t.x16Bank);
181
		DEBUGSetBreakpointIgnore(t.pc, t.bank, t.x16Bank, (uint32_t)s_edit.ignore);
182
		ImGui::CloseCurrentPopup();
183
	}
184
	ImGui::SameLine();
185
	if (ImGui::Button("Close"))
186
		ImGui::CloseCurrentPopup();
187
	ImGui::EndPopup();
188
}
189
190
// ---------------------------------------------------------------------------
191
// Breakpoints - break when execution reaches an address.
192
// ---------------------------------------------------------------------------
193
194
// Highest bank number that exists for the window `addr` falls in. RAM banking is
195
// sized at runtime by -ram, so this is a real limit, not a constant.
196
int
197
max_bank_for(uint16_t addr)
198
{
199
	const int n = (addr >= 0xC000) ? memory_get_num_rom_banks() : (int)num_ram_banks;
200
	return (n > 0 ? n : 1) - 1;
201
}
202
203
int
204
clamp_bank(int bank, uint16_t addr)
205
{
206
	const int hi = max_bank_for(addr);
207
	return bank < 0 ? 0 : (bank > hi ? hi : bank);
208
}
209
210
// Add a breakpoint by typing its address.
211
//
212
// Everywhere else in the debugger a breakpoint is set by pointing at a line -
213
// the gutter, or F9 on the cursor - which is no help for an address you cannot
214
// currently see: ROM, a bank that is not mapped in, or code that has not been
215
// reached yet and so has no disassembly to click. The Memory-writes section
216
// below has always had a row like this; execution breakpoints did not, and the
217
// only address box on this panel drove "Run to", which is a one-shot resume and
218
// not a breakpoint at all.
219
void
220
add_breakpoint_row(void)
221
{
222
	const uint16_t addr   = (uint16_t)(s_add_addr & 0xFFFF);
223
	const bool     banked = addr >= 0xA000;
224
	const int      live   = debug_current_x16_bank((int)addr, 0);
225
226
	// Until the user picks a bank the field tracks the mapping the machine is
227
	// in, so typing an address alone sets exactly the breakpoint clicking the
228
	// gutter would have set. Leaving the banked window forgets the choice; a
229
	// chosen bank is re-clamped because the address can move between the RAM
230
	// and ROM windows under it, and those are different depths.
231
	if (!banked) {
232
		s_add_bank_touched = false;
233
	} else if (!s_add_bank_touched) {
234
		s_add_bank = live >= 0 ? live : 0;
235
	} else {
236
		s_add_bank = clamp_bank(s_add_bank, addr);
237
	}
238
239
	ImGui::TextUnformatted("Break at $");
240
	ImGui::SameLine(0.0f, 0.0f);
241
	ImGui::SetNextItemWidth(dbgui_field_width("FFFF"));
242
	ImGui::InputScalar("##bpaddr", ImGuiDataType_S32, &s_add_addr, nullptr, nullptr, "%04X",
243
	                   ImGuiInputTextFlags_CharsHexadecimal |
244
	                       ImGuiInputTextFlags_AutoSelectAll);
245
	const bool submit = dbgui_committed_with_enter();
246
	ImGui::SetItemTooltip("Address to break at, in HEX. \"10\" means $10 (16), not ten.\n"
247
	                      "Press Enter to add.");
248
249
	// $A000 and up is read through a bank window, so an address there is not one
250
	// breakpoint but one per bank. Below it there is nothing to choose.
251
	ImGui::SameLine();
252
	ImGui::BeginDisabled(!banked);
253
	ImGui::TextUnformatted("in bank");
254
	ImGui::SameLine();
255
	ImGui::SetNextItemWidth(dbgui_field_width("255") + ImGui::GetFrameHeight() * 2.0f);
256
	int bank = s_add_bank;
257
	if (ImGui::InputInt("##bpbank", &bank)) {
258
		s_add_bank         = clamp_bank(bank, addr);
259
		s_add_bank_touched = true;
260
	}
261
	ImGui::SetItemTooltip("Which bank the address lives in, in DECIMAL. Defaults to the bank\n"
262
	                      "mapped in right now - the one the gutter would have used.\n"
263
	                      "$A000-$BFFF selects a RAM bank, $C000-$FFFF a ROM bank.");
264
	ImGui::EndDisabled();
265
266
	ImGui::SameLine();
267
	const bool add = ImGui::Button("Add##bp") || submit;
268
	ImGui::SetItemTooltip("You can also click the gutter in the Disassembly or Source panel,\n"
269
	                      "or press F9 on the cursor line.");
270
	ImGui::SameLine();
271
	if (addr >= 0xC000) {
272
		ImGui::TextDisabled("(ROM)");
273
	} else if (banked) {
274
		ImGui::TextDisabled("(RAM)");
275
	} else {
276
		ImGui::TextDisabled("(unbanked)");
277
	}
278
	ImGui::SetItemTooltip("Addresses below $A000 are the same memory whatever is banked in,\n"
279
	                      "so the bank does not apply to them.");
280
281
	if (add) {
282
		struct breakpoint bp = {};
283
		bp.pc                = (int)addr;
284
		bp.bank              = 0;
285
		bp.x16Bank           = banked ? s_add_bank : DEBUG_BANK_ANY;
286
		DEBUGAddBreakPoint(bp);
287
		sync_tracked(); // show it in the table below this frame, not next
288
	}
289
}
290
291
void
292
draw_breakpoints()
293
{
294
	// --- Add by address ---------------------------------------------------
295
	add_breakpoint_row();
296
297
	ImGui::Separator();
298
299
	// --- Breakpoint list ------------------------------------------------
300
	ImGui::Text("Breakpoints: %d", (int)s_tracked.size());
301
	ImGui::SameLine();
302
	ImGui::BeginDisabled(s_tracked.empty());
303
	if (ImGui::SmallButton("Enable All")) {
304
		for (TrackedBP &t : s_tracked) set_enabled(t, true);
305
	}
306
	ImGui::SameLine();
307
	if (ImGui::SmallButton("Disable All")) {
308
		for (TrackedBP &t : s_tracked) set_enabled(t, false);
309
	}
310
	ImGui::SameLine();
311
	bool clear_all = ImGui::SmallButton("Clear All");
312
	ImGui::EndDisabled();
313
314
	int remove_idx = -1;  // deferred per-row delete
315
316
	if (ImGui::BeginTable("bp_table", 6,
317
	        ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp |
318
	            DBGUI_TABLE_FLAGS_RESIZABLE)) {
319
		ImGui::TableSetupColumn("On",      ImGuiTableColumnFlags_WidthFixed,
320
		                        dbgui_col_fit("On", ImGui::GetFrameHeight()));
321
		ImGui::TableSetupColumn("Address", ImGuiTableColumnFlags_WidthFixed,
322
		                        dbgui_col_width("Address", "$FFFF"));
323
		ImGui::TableSetupColumn("Bank",    ImGuiTableColumnFlags_WidthFixed,
324
		                        dbgui_col_width("Bank", "255"));
325
		ImGui::TableSetupColumn("Hits",    ImGuiTableColumnFlags_WidthFixed,
326
		                        dbgui_col_width("Hits", "99999"));
327
		ImGui::TableSetupColumn("Condition", ImGuiTableColumnFlags_WidthStretch);
328
		ImGui::TableSetupColumn("",        ImGuiTableColumnFlags_WidthFixed,
329
		                        dbgui_col_fit("", dbgui_field_width("x")));
330
		ImGui::TableHeadersRow();
331
332
		for (int i = 0; i < (int)s_tracked.size(); i++) {
333
			TrackedBP &t = s_tracked[i];
334
			ImGui::TableNextRow();
335
			ImGui::PushID(i);
336
337
			ImGui::TableNextColumn();
338
			bool en = t.enabled;
339
			if (ImGui::Checkbox("##en", &en)) {
340
				set_enabled(t, en);
341
			}
342
343
			ImGui::TableNextColumn();
344
			if (t.enabled) {
345
				ImGui::Text("$%04X", (unsigned)(t.pc & 0xFFFF));
346
			} else {
347
				ImGui::TextDisabled("$%04X", (unsigned)(t.pc & 0xFFFF));
348
			}
349
350
			ImGui::TableNextColumn();
351
			if (t.x16Bank >= 0) {
352
				ImGui::Text("%d", t.x16Bank);
353
			} else {
354
				ImGui::TextDisabled("-");
355
			}
356
357
			ImGui::TableNextColumn();
358
			ImGui::Text("%u", DEBUGGetBreakpointHits(t.pc, t.bank, t.x16Bank));
359
360
			ImGui::TableNextColumn();
361
			char summary[48];
362
			cond_summary(summary, sizeof summary, t);
363
			if (ImGui::SmallButton(summary)) {
364
				s_edit.loaded = false;
365
				ImGui::OpenPopup("cond_edit");
366
			}
367
			cond_editor_popup(t);
368
369
			ImGui::TableNextColumn();
370
			if (ImGui::SmallButton("x")) {
371
				remove_idx = i;
372
			}
373
			ImGui::PopID();
374
		}
375
		ImGui::EndTable();
376
	}
377
378
	if (s_tracked.empty()) {
379
		ImGui::TextDisabled("No breakpoints.");
380
		ImGui::SetItemTooltip("Type an address above, or click the gutter in the Disassembly\n"
381
		                      "or Source panel.");
382
	}
383
384
	// Apply deferred mutations after the table is built. Explicit deletes
385
	// also free the condition/hit entry (enable/disable must NOT).
386
	if (clear_all) {
387
		for (TrackedBP &t : s_tracked) {
388
			DEBUGRemoveBreakPoint(t.pc, t.bank, t.x16Bank);
389
			DEBUGForgetBreakpoint(t.pc, t.bank, t.x16Bank);
390
		}
391
		s_tracked.clear();
392
	} else if (remove_idx >= 0 && remove_idx < (int)s_tracked.size()) {
393
		DEBUGRemoveBreakPoint(s_tracked[remove_idx].pc, s_tracked[remove_idx].bank, s_tracked[remove_idx].x16Bank);
394
		DEBUGForgetBreakpoint(s_tracked[remove_idx].pc, s_tracked[remove_idx].bank, s_tracked[remove_idx].x16Bank);
395
		s_tracked.erase(s_tracked.begin() + remove_idx);
396
	}
397
398
	// --- Run to address ---------------------------------------------------
399
	// Sets a one-shot internal breakpoint and resumes; the CPU halts when it
400
	// reaches the address. Only meaningful while paused.
401
	//
402
	// Below the list, not above it: this leaves nothing behind and never
403
	// appears in the table, so leading with it made the panel's only address
404
	// box the one thing on it that does not set a breakpoint.
405
	ImGui::Separator();
406
	ImGui::TextUnformatted("Run to $");
407
	ImGui::SetItemTooltip("One-shot: resume, stop once at this address, and forget it.\n"
408
	                      "Nothing is added to the list above.");
409
	ImGui::SameLine(0.0f, 0.0f);
410
	ImGui::SetNextItemWidth(dbgui_field_width("FFFF"));
411
	ImGui::InputScalar("##runto", ImGuiDataType_S32, &s_runto_addr,
412
	                   nullptr, nullptr, "%04X",
413
	                   ImGuiInputTextFlags_CharsHexadecimal |
414
	                       ImGuiInputTextFlags_AutoSelectAll);
415
	const bool runto_submit = dbgui_committed_with_enter();
416
	ImGui::SetItemTooltip("Address to run to, in HEX. Press Enter to go.");
417
	ImGui::SameLine();
418
	ImGui::BeginDisabled(!DEBUGIsPaused());
419
	if (ImGui::Button("Go") || (runto_submit && DEBUGIsPaused())) {
420
		DEBUGRunTo((uint16_t)(s_runto_addr & 0xFFFF), 0, DEBUG_OWNER_UI);
421
	}
422
	ImGui::EndDisabled();
423
	if (!DEBUGIsPaused()) {
424
		ImGui::SameLine();
425
		ImGui::TextDisabled("(pause first)");
426
	}
427
}
428
429
// ---------------------------------------------------------------------------
430
// Watchpoints — break when the program WRITES to an address or range.
431
//
432
// These are a different trigger from the PC breakpoints above (which fire when
433
// execution reaches an address), but they are the same idea to a user: a
434
// condition that stops the machine. They used to be creatable from the memory
435
// view with no way to see or manage them, which is why they live here now.
436
//
437
// Note the trigger is a write, not a change: storing the same value back still
438
// fires. The optional value filter narrows that.
439
// ---------------------------------------------------------------------------
440
const char *
441
wp_op_name(int op)
442
{
443
	static const char *names[] = { "==", "!=", "<", "<=", ">", ">=" };
444
	return (op >= 0 && op < 6) ? names[op] : "==";
445
}
446
447
void
448
draw_watchpoints()
449
{
450
	// --- Add ---------------------------------------------------------------
451
	static int wp_addr = 0;
452
	static int wp_len  = 1;
453
454
	ImGui::TextUnformatted("Break on write to $");
455
	ImGui::SameLine(0.0f, 0.0f);
456
	ImGui::SetNextItemWidth(dbgui_field_width("FFFF"));
457
	ImGui::InputScalar("##wpaddr", ImGuiDataType_S32, &wp_addr, nullptr, nullptr, "%04X",
458
	                   ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_AutoSelectAll);
459
	if (ImGui::IsItemHovered())
460
		ImGui::SetTooltip("First address to cover (hex).");
461
	ImGui::SameLine();
462
	ImGui::SetNextItemWidth(dbgui_field_width("65535") + ImGui::GetFrameHeight() * 2.0f);
463
	ImGui::InputInt("len", &wp_len);
464
	if (wp_len < 1) wp_len = 1;
465
	if (wp_len > 0xFFFF) wp_len = 0xFFFF;
466
	if (ImGui::IsItemHovered())
467
		ImGui::SetTooltip("How many consecutive bytes to cover. A write anywhere in the\n"
468
		                  "range triggers the break.");
469
	ImGui::SameLine();
470
	ImGui::BeginDisabled(debug_wp_count() >= MAX_WATCHPOINTS);
471
	if (ImGui::Button("Add")) {
472
		DEBUGAddWatchPoint((uint16_t)(wp_addr & 0xFFFF), (uint16_t)wp_len);
473
	}
474
	ImGui::EndDisabled();
475
	if (ImGui::IsItemHovered())
476
		ImGui::SetTooltip("You can also right-click a byte (or a drag-selected range)\n"
477
		                  "in the Memory panel and choose \"Break on write\".");
478
	ImGui::SameLine();
479
	ImGui::TextDisabled("(%d / %d used)", debug_wp_count(), MAX_WATCHPOINTS);
480
481
	ImGui::Separator();
482
483
	if (debug_wp_count() == 0) {
484
		ImGui::TextDisabled("None set.");
485
		ImGui::SetItemTooltip("Add one above, or right-click an address (or a drag-selected\n"
486
		                      "range) in the Memory panel and choose \"Break on write\".");
487
		return;
488
	}
489
490
	// --- List --------------------------------------------------------------
491
	int remove_addr = -1;
492
	int remove_bank = DEBUG_BANK_ANY;
493
494
	if (ImGui::BeginTable("wp_table", 6,
495
	        ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp |
496
	            DBGUI_TABLE_FLAGS_RESIZABLE)) {
497
		ImGui::TableSetupColumn("On",      ImGuiTableColumnFlags_WidthFixed,
498
		                        dbgui_col_fit("On", ImGui::GetFrameHeight()));
499
		ImGui::TableSetupColumn("Address", ImGuiTableColumnFlags_WidthFixed,
500
		                        dbgui_col_width("Address", "$FFFF"));
501
		ImGui::TableSetupColumn("Len",     ImGuiTableColumnFlags_WidthFixed,
502
		                        dbgui_col_width("Len", "65535"));
503
		ImGui::TableSetupColumn("Range",   ImGuiTableColumnFlags_WidthFixed,
504
		                        dbgui_col_width("Range", "$FFFF-$FFFF"));
505
		ImGui::TableSetupColumn("Only when written value", ImGuiTableColumnFlags_WidthStretch);
506
		ImGui::TableSetupColumn("",        ImGuiTableColumnFlags_WidthFixed,
507
		                        dbgui_col_fit("", dbgui_field_width("x")));
508
		ImGui::TableHeadersRow();
509
510
		for (int i = 0; i < debug_wp_count(); i++) {
511
			const struct watchpoint *w = debug_wp_at(i);
512
			if (!w)
513
				break;
514
			// Edits go through the core's setters rather than writing the
515
			// table, which is private to debug_core.c. They are keyed on the
516
			// watchpoint's OWN bank, not DEBUG_BANK_ANY: a watch set on one
517
			// RAM bank's $A100 would otherwise not be found, and the edit
518
			// would silently do nothing.
519
			const uint16_t w_addr = w->addr;
520
			const int      w_bank = w->x16Bank;
521
			ImGui::PushID(i);
522
			ImGui::TableNextRow();
523
524
			ImGui::TableSetColumnIndex(0);
525
			bool active = w->active;
526
			if (ImGui::Checkbox("##on", &active))
527
				debug_wp_set_active(w_addr, w_bank, active);
528
			if (ImGui::IsItemHovered())
529
				ImGui::SetTooltip("Disable without deleting.");
530
531
			ImGui::TableSetColumnIndex(1);
532
			ImGui::Text("$%04X", w->addr);
533
			if (ImGui::IsItemHovered())
534
				ImGui::SetTooltip("Click to show this address in the Memory panel.");
535
			if (ImGui::IsItemClicked())
536
				debug_ui_request_goto(w->addr, 0);
537
538
			ImGui::TableSetColumnIndex(2);
539
			ImGui::Text("%u", (unsigned)w->len);
540
541
			ImGui::TableSetColumnIndex(3);
542
			if (w->len > 1)
543
				ImGui::TextDisabled("$%04X-$%04X", w->addr, (unsigned)(w->addr + w->len - 1));
544
			else
545
				ImGui::TextDisabled("-");
546
547
			// Optional filter: only break when the value being written matches.
548
			ImGui::TableSetColumnIndex(4);
549
			bool has = w->has_value;
550
			if (ImGui::Checkbox("##hasval", &has)) {
551
				if (has)
552
					debug_wp_set_value(w_addr, w_bank, w->op, w->value);
553
				else
554
					debug_wp_clear_value(w_addr, w_bank);
555
			}
556
			if (ImGui::IsItemHovered())
557
				ImGui::SetTooltip("Only break when the value being written satisfies a test,\n"
558
				                  "rather than on every write.");
559
			if (w->has_value) {
560
				ImGui::SameLine();
561
				ImGui::SetNextItemWidth(dbgui_combo_width(">="));
562
				int op = w->op;
563
				if (ImGui::BeginCombo("##op", wp_op_name(op))) {
564
					for (int k = 0; k < 6; k++) {
565
						if (ImGui::Selectable(wp_op_name(k), op == k))
566
							debug_wp_set_value(w_addr, w_bank, k, w->value);
567
					}
568
					ImGui::EndCombo();
569
				}
570
				ImGui::SameLine();
571
				ImGui::SetNextItemWidth(dbgui_field_width("FF"));
572
				int val = w->value;
573
				if (ImGui::InputScalar("##val", ImGuiDataType_S32, &val, nullptr, nullptr, "%02X",
574
				                       ImGuiInputTextFlags_CharsHexadecimal |
575
				                           ImGuiInputTextFlags_AutoSelectAll)) {
576
					debug_wp_set_value(w_addr, w_bank, w->op, (uint8_t)(val & 0xFF));
577
				}
578
				if (ImGui::IsItemHovered())
579
					ImGui::SetTooltip("Value to compare the written byte against (hex).");
580
			} else {
581
				ImGui::SameLine();
582
				ImGui::TextDisabled("any write");
583
			}
584
585
			ImGui::TableSetColumnIndex(5);
586
			if (ImGui::SmallButton("x")) {
587
				remove_addr = w->addr;
588
				remove_bank = w_bank;
589
			}
590
			if (ImGui::IsItemHovered())
591
				ImGui::SetTooltip("Delete this watchpoint.");
592
593
			ImGui::PopID();
594
		}
595
		ImGui::EndTable();
596
	}
597
598
	if (remove_addr >= 0) {
599
		debug_wp_delete((uint16_t)remove_addr, remove_bank);
600
	}
601
}
602
603
void
604
breakpoints_panel_render(bool *p_open)
605
{
606
	if (ImGui::Begin("Breakpoints", p_open)) {
607
		dbgui_window_zoom("breakpoints");
608
		sync_tracked();
609
610
		// Collapsible sections rather than tabs or fixed-height panes: these are
611
		// two different triggers for the same job (stop the machine) and you
612
		// usually want both in view, but either can be folded away when you are
613
		// only using one. Matches how the CPU panel is organised.
614
		//
615
		// "Watchpoint" is avoided as a label because the CPU panel's Watch list
616
		// already owns that word for something else entirely - values you are
617
		// keeping an eye on, which never stop execution.
618
		char exec_hdr[80];
619
		snprintf(exec_hdr, sizeof exec_hdr, "Execution (%d)###exec", (int)s_tracked.size());
620
		const bool exec_open = ImGui::CollapsingHeader(exec_hdr, ImGuiTreeNodeFlags_DefaultOpen);
621
		ImGui::SetItemTooltip("Break when the PC reaches an address.\n\n"
622
		                      "Type an address below, click the gutter in the Disassembly or\n"
623
		                      "Source panel, or press F9 on the cursor line. \"Run to $\" at the\n"
624
		                      "bottom is a one-shot stop that sets nothing.");
625
		if (exec_open) {
626
			draw_breakpoints();
627
		}
628
629
		char wp_hdr[80];
630
		snprintf(wp_hdr, sizeof wp_hdr, "Memory writes (%d)###wp", debug_wp_count());
631
		const bool wp_open = ImGui::CollapsingHeader(wp_hdr, ImGuiTreeNodeFlags_DefaultOpen);
632
		ImGui::SetItemTooltip("Break when the program writes to an address - the way to find\n"
633
		                      "what is clobbering a value.\n\n"
634
		                      "Any store counts, including one that writes the same value back,\n"
635
		                      "so this is break-on-write, not break-on-change. Use the value\n"
636
		                      "filter on a row to narrow it.");
637
		if (wp_open) {
638
			draw_watchpoints();
639
		}
640
	}
641
	dbgui_window_end();
642
}
643
644
DebugPanelRegistration s_reg("Breakpoints", breakpoints_panel_render, true);
645
646
} // namespace