OXYGENHUB
◆ main enhanced-x16-emulator / src / debug_core.c
22.1 KiB · C · last changed chore(): initial commit 25 days ago source blame history raw
1
// Commander X16 Emulator — debugger core: breakpoints. See debug_core.h.
2
//
3
// Dependency-light on purpose (no SDL, no glue.h) so the decision logic can be
4
// unit-tested without standing up a machine: it reaches for only the CPU
5
// register file, the two bank registers, and a non-intrusive memory read.
6
7
#include "debug_core.h"
8
9
#include <stdlib.h>
10
#include <string.h>
11
12
#include "cpu/registers.h"
13
14
// Declared directly rather than including memory.h/glue.h, which pull in SDL.
15
// Signatures match memory.h exactly.
16
extern struct regs regs;
17
extern bool        is_gen2;
18
uint8_t real_read6502(uint16_t address, uint8_t bank, bool debugOn, int16_t x16Bank);
19
uint8_t memory_get_ram_bank(void);
20
uint8_t memory_get_rom_bank(void);
21
22
#define DC_READ(a) real_read6502((uint16_t)(a), 0, true, -1)
23
24
struct breakpoint *breakPoints    = NULL;
25
int                numBreakpoints = 0;
26
static int         bpCapacity     = 0;
27
28
// Is `addr` inside a window whose contents depend on a bank register?
29
//
30
// Nothing below $A000 is banked. Above it, the answer depends on the machine:
31
// a non-zero program bank on a Gen2 maps its own flat 64K, so the window
32
// registers select nothing there, while on a gen1 the program bank selects
33
// nothing at all (read6502 forces it to zero even with -c816) and the window
34
// applies regardless.
35
static bool
36
addr_is_banked(int addr, uint8_t pbank)
37
{
38
	if (addr < 0xA000)
39
		return false;
40
	return !(is_gen2 && pbank != 0);
41
}
42
43
// Which RAM/ROM window an address sits in, or DEBUG_BANK_ANY where it is not
44
// banked at all.
45
int
46
debug_current_x16_bank(int pc, uint8_t bank)
47
{
48
	if (!addr_is_banked(pc, bank))
49
		return DEBUG_BANK_ANY;
50
	return pc < 0xC000 ? memory_get_ram_bank() : memory_get_rom_bank();
51
}
52
53
// Does a recorded bank selector apply to `addr` right now? See DEBUG_BANK_ANY.
54
static bool
55
bank_selector_matches(int selector, int addr, uint8_t pbank)
56
{
57
	if (!addr_is_banked(addr, pbank))
58
		return true;                     // banks select nothing here
59
	if (selector == DEBUG_BANK_ANY)
60
		return true;                     // whichever bank is mapped
61
	return selector == debug_current_x16_bank(addr, pbank);
62
}
63
64
bool
65
debug_bank_selector_matches(int selector, int addr, uint8_t pbank)
66
{
67
	return bank_selector_matches(selector, addr, pbank);
68
}
69
70
// What to record for an address: outside a banked window there is nothing to
71
// select, so store ANY rather than a number that would be silently ignored.
72
static int
73
normalise_bank(int selector, int addr, uint8_t pbank)
74
{
75
	return addr_is_banked(addr, pbank) ? selector : DEBUG_BANK_ANY;
76
}
77
78
int
79
debug_normalise_bank(int selector, int addr, uint8_t pbank)
80
{
81
	return normalise_bank(selector, addr, pbank);
82
}
83
84
static bool
85
bp_matches(int pc, uint8_t bank, const struct breakpoint *bp)
86
{
87
	return bp->enabled && pc == bp->pc && bank == bp->bank
88
	       && bank_selector_matches(bp->x16Bank, pc, bank);
89
}
90
91
// See debug_core.h. The rule this encodes is the one a caller cannot be trusted
92
// to re-derive: which field a packed bank byte belongs in. Putting it in the
93
// program-bank field on a gen1 builds an entry bp_matches() rejects on every
94
// instruction, because the running program bank is always zero there -- an
95
// accepted breakpoint that never fires, which is worse than a refused one.
96
bool
97
debug_bp_from_ref(long ref, int explicit_bank, long offset, struct breakpoint *out)
98
{
99
	if (!out || ref < 0 || ref > 0xFFFFFF)
100
		return false;
101
	if (explicit_bank != DEBUG_BANK_ANY && (explicit_bank < 0 || explicit_bank > 0xFF))
102
		return false;
103
104
	int     addr    = (int)(ref & 0xFFFF);
105
	uint8_t pbank   = 0;
106
	int     x16bank = explicit_bank;
107
108
	uint8_t top = (uint8_t)((ref >> 16) & 0xFF);
109
	if (top) {
110
		// Two answers to the same question; refuse rather than silently
111
		// preferring one of them.
112
		if (explicit_bank != DEBUG_BANK_ANY)
113
			return false;
114
		if (is_gen2)
115
			pbank = top;
116
		else
117
			x16bank = (int)top;
118
	}
119
120
	addr = (int)(((long)addr + offset) & 0xFFFF);
121
122
	// A bank named where the window registers select nothing can never be
123
	// satisfied. Arming it under ANY instead would put the breakpoint in
124
	// whichever bank happened to be mapped -- not what was asked for.
125
	if (x16bank != DEBUG_BANK_ANY && normalise_bank(x16bank, addr, pbank) == DEBUG_BANK_ANY)
126
		return false;
127
128
	out->pc      = addr;
129
	out->bank    = pbank;
130
	out->x16Bank = x16bank;
131
	out->owners  = 0;
132
	out->enabled = false;
133
	return true;
134
}
135
136
static void cond_forget_all(void);
137
138
// ---------------------------------------------------------------------------
139
//  Ownership
140
// ---------------------------------------------------------------------------
141
//  A bitmask per entry rather than a list: there are seven owners, the set
142
//  operations wanted are exactly bit operations, and the hot path never has to
143
//  look at it at all.
144
145
static uint16_t
146
owner_bit(debug_owner_t owner)
147
{
148
	if (owner < 0 || owner >= DEBUG_OWNER_COUNT)
149
		return 0;
150
	return (uint16_t)(1u << (unsigned)owner);
151
}
152
153
// ---------------------------------------------------------------------------
154
//  Table management
155
// ---------------------------------------------------------------------------
156
157
int
158
debug_bp_find(int pc, uint8_t bank, int x16Bank)
159
{
160
	x16Bank = normalise_bank(x16Bank, pc, bank);
161
	for (int i = 0; i < numBreakpoints; i++) {
162
		if (breakPoints[i].pc == pc && breakPoints[i].bank == bank
163
		    && breakPoints[i].x16Bank == x16Bank)
164
			return i;
165
	}
166
	return -1;
167
}
168
169
int
170
debug_bp_count(void)
171
{
172
	return numBreakpoints;
173
}
174
175
const struct breakpoint *
176
debug_bp_at(int index)
177
{
178
	if (index < 0 || index >= numBreakpoints)
179
		return NULL;
180
	return &breakPoints[index];
181
}
182
183
// Drop entry `idx` from the table. The condition record is deliberately left
184
// alone; see the note on counts in debug_core.h.
185
static void
186
bp_erase(int idx)
187
{
188
	for (int i = idx; i < numBreakpoints - 1; i++) {
189
		breakPoints[i] = breakPoints[i + 1];
190
	}
191
	numBreakpoints--;
192
}
193
194
debug_add_result_t
195
debug_bp_add_for(struct breakpoint bp, debug_owner_t owner)
196
{
197
	const uint16_t bit = owner_bit(owner);
198
199
	bp.x16Bank = normalise_bank(bp.x16Bank, bp.pc, bp.bank);
200
201
	int idx = debug_bp_find(bp.pc, bp.bank, bp.x16Bank);
202
	if (idx >= 0) {
203
		breakPoints[idx].owners |= bit;
204
		// Asking for a breakpoint is asking for it to be armed, so a disabled
205
		// entry comes back. Otherwise a client that set one, disabled it and
206
		// set it again would be quietly ignored.
207
		breakPoints[idx].enabled = true;
208
		return DEBUG_ADD_EXISTED;
209
	}
210
211
	if (numBreakpoints >= bpCapacity) {
212
		int                newCap = bpCapacity == 0 ? 16 : bpCapacity * 2;
213
		struct breakpoint *newArr =
214
		    (struct breakpoint *)realloc(breakPoints, (size_t)newCap * sizeof(struct breakpoint));
215
		if (!newArr)
216
			return DEBUG_ADD_FULL;
217
		breakPoints = newArr;
218
		bpCapacity  = newCap;
219
	}
220
221
	bp.owners  = bit;
222
	bp.enabled = true;
223
224
	breakPoints[numBreakpoints++] = bp;
225
	return DEBUG_ADD_CREATED;
226
}
227
228
bool
229
debug_bp_remove_for(int pc, uint8_t bank, int x16Bank, debug_owner_t owner)
230
{
231
	const uint16_t bit = owner_bit(owner);
232
233
	int idx = debug_bp_find(pc, bank, x16Bank);
234
	if (idx < 0 || !(breakPoints[idx].owners & bit))
235
		return false;
236
237
	breakPoints[idx].owners &= (uint16_t)~bit;
238
	if (breakPoints[idx].owners == 0)
239
		bp_erase(idx);
240
	return true;
241
}
242
243
int
244
debug_bp_clear_owner(debug_owner_t owner)
245
{
246
	const uint16_t bit = owner_bit(owner);
247
	int            disarmed = 0;
248
249
	// Backwards, so erasing an entry cannot shuffle one we have not looked at
250
	// yet down into a slot we have already passed.
251
	for (int i = numBreakpoints - 1; i >= 0; i--) {
252
		if (!(breakPoints[i].owners & bit))
253
			continue;
254
		breakPoints[i].owners &= (uint16_t)~bit;
255
		if (breakPoints[i].owners == 0) {
256
			bp_erase(i);
257
			disarmed++;
258
		}
259
	}
260
	return disarmed;
261
}
262
263
bool
264
debug_bp_delete(int pc, uint8_t bank, int x16Bank)
265
{
266
	int idx = debug_bp_find(pc, bank, x16Bank);
267
	if (idx < 0)
268
		return false;
269
	bp_erase(idx);
270
	return true;
271
}
272
273
bool
274
debug_bp_has_owner(int pc, uint8_t bank, int x16Bank, debug_owner_t owner)
275
{
276
	int idx = debug_bp_find(pc, bank, x16Bank);
277
	return idx >= 0 && (breakPoints[idx].owners & owner_bit(owner)) != 0;
278
}
279
280
void
281
debug_bp_toggle_for(int pc, uint8_t bank, int x16Bank, debug_owner_t owner)
282
{
283
	if (!debug_bp_delete(pc, bank, x16Bank)) {
284
		struct breakpoint bp = { pc, bank, x16Bank, 0, false };
285
		debug_bp_add_for(bp, owner);
286
	}
287
}
288
289
bool
290
debug_bp_set_enabled(int pc, uint8_t bank, int x16Bank, bool enabled)
291
{
292
	int idx = debug_bp_find(pc, bank, x16Bank);
293
	if (idx < 0)
294
		return false;
295
	breakPoints[idx].enabled = enabled;
296
	return true;
297
}
298
299
bool
300
debug_bp_is_enabled(int pc, uint8_t bank, int x16Bank)
301
{
302
	int idx = debug_bp_find(pc, bank, x16Bank);
303
	return idx >= 0 && breakPoints[idx].enabled;
304
}
305
306
void
307
debug_bp_clear_all(void)
308
{
309
	// A full clear is a delete, not a disable, so the conditions and counts go
310
	// with it. Leaving them behind would let a front end that replaces its
311
	// whole breakpoint set (which is how the Debug Adapter Protocol works)
312
	// silently inherit an old condition or ignore count on a breakpoint the
313
	// user thinks they just created.
314
	numBreakpoints = 0;
315
	cond_forget_all();
316
}
317
318
bool
319
debug_bp_is_set(int pc, uint8_t bank)
320
{
321
	for (int i = 0; i < numBreakpoints; i++) {
322
		if (bp_matches(pc, bank, &breakPoints[i]))
323
			return true;
324
	}
325
	return false;
326
}
327
328
// ---------------------------------------------------------------------------
329
//  Conditions and hit counts
330
// ---------------------------------------------------------------------------
331
//  Held in a table parallel to the breakpoints rather than inside struct
332
//  breakpoint, so that struct stays the plain address record the rest of the
333
//  emulator already passes around by value.
334
335
struct bp_cond {
336
	int      pc;
337
	uint8_t  bank;
338
	int      x16Bank;
339
	bool     used;
340
	bool     has_cond;
341
	int      operand;
342
	uint16_t operand_addr;
343
	int      op;
344
	uint32_t value;
345
	uint32_t ignore;
346
	uint32_t hits;
347
};
348
349
static struct bp_cond *bpConds     = NULL;
350
static int             bpCondCount = 0;
351
static int             bpCondCap   = 0;
352
353
static void
354
cond_forget_all(void)
355
{
356
	for (int i = 0; i < bpCondCount; i++) {
357
		bpConds[i].used = false;
358
	}
359
}
360
361
static struct bp_cond *
362
cond_find(int pc, uint8_t bank, int x16Bank){
363
	x16Bank = normalise_bank(x16Bank, pc, bank);
364
	for (int i = 0; i < bpCondCount; i++) {
365
		if (bpConds[i].used && bpConds[i].pc == pc && bpConds[i].bank == bank
366
		    && bpConds[i].x16Bank == x16Bank)
367
			return &bpConds[i];
368
	}
369
	return NULL;
370
}
371
372
static struct bp_cond *
373
cond_ensure(int pc, uint8_t bank, int x16Bank)
374
{
375
	struct bp_cond *c = cond_find(pc, bank, x16Bank);
376
	if (c)
377
		return c;
378
379
	for (int i = 0; i < bpCondCount; i++) {
380
		if (!bpConds[i].used) {
381
			c = &bpConds[i];
382
			break;
383
		}
384
	}
385
	if (!c) {
386
		if (bpCondCount >= bpCondCap) {
387
			int             nc = bpCondCap ? bpCondCap * 2 : 16;
388
			struct bp_cond *na =
389
			    (struct bp_cond *)realloc(bpConds, (size_t)nc * sizeof(struct bp_cond));
390
			if (!na)
391
				return NULL;
392
			bpConds   = na;
393
			bpCondCap = nc;
394
		}
395
		c = &bpConds[bpCondCount++];
396
	}
397
	memset(c, 0, sizeof(*c));
398
	c->used    = true;
399
	c->pc      = pc;
400
	c->bank    = bank;
401
	c->x16Bank = normalise_bank(x16Bank, pc, bank);
402
	return c;
403
}
404
405
static uint32_t
406
cond_operand_value(const struct bp_cond *c)
407
{
408
	switch (c->operand) {
409
		case BPOPERAND_A:  return regs.c;
410
		case BPOPERAND_X:  return regs.x;
411
		case BPOPERAND_Y:  return regs.y;
412
		case BPOPERAND_SP: return regs.sp;
413
		case BPOPERAND_P:  return regs.status;
414
		case BPOPERAND_BYTE: return DC_READ(c->operand_addr);
415
		case BPOPERAND_WORD:
416
			return (uint32_t)DC_READ(c->operand_addr)
417
			       | ((uint32_t)DC_READ((uint16_t)(c->operand_addr + 1)) << 8);
418
	}
419
	return 0;
420
}
421
422
static bool
423
cond_satisfied(const struct bp_cond *c)
424
{
425
	if (!c->has_cond)
426
		return true;
427
428
	// Compare at the width the CPU is actually using for that register right
429
	// now. On a 65C02, and on a 65C816 in emulation mode, A/X/Y are 8-bit; in
430
	// native mode the M and X status bits decide, exactly as they do for the
431
	// instructions being debugged.
432
	//
433
	// Only the register is masked, never the value the user asked about. Mask
434
	// both and "A == $1234" quietly becomes "A == $34" and stops somewhere it
435
	// was never asked to; leaving the value alone means such a condition simply
436
	// cannot be true while A is 8 bits, which is the honest answer.
437
	bool     native = regs.is65c816 && !regs.e;
438
	uint32_t mask;
439
	switch (c->operand) {
440
		case BPOPERAND_A:
441
			mask = (native && !(regs.status & FLAG_MEMORY_WIDTH)) ? 0xFFFFu : 0xFFu;
442
			break;
443
		case BPOPERAND_X:
444
		case BPOPERAND_Y:
445
			mask = (native && !(regs.status & FLAG_INDEX_WIDTH)) ? 0xFFFFu : 0xFFu;
446
			break;
447
		case BPOPERAND_SP:
448
		case BPOPERAND_WORD:
449
			mask = 0xFFFFu;
450
			break;
451
		default:
452
			mask = 0xFFu;
453
			break;
454
	}
455
456
	uint32_t v   = cond_operand_value(c) & mask;
457
	uint32_t cmp = c->value;
458
459
	switch (c->op) {
460
		case BPCMP_EQ: return v == cmp;
461
		case BPCMP_NE: return v != cmp;
462
		case BPCMP_LT: return v < cmp;
463
		case BPCMP_LE: return v <= cmp;
464
		case BPCMP_GT: return v > cmp;
465
		case BPCMP_GE: return v >= cmp;
466
	}
467
	return true;
468
}
469
470
bool
471
debug_bp_on_arrival(int pc, uint8_t bank)
472
{
473
	for (int i = 0; i < numBreakpoints; i++) {
474
		if (!bp_matches(pc, bank, &breakPoints[i]))
475
			continue;
476
477
		struct bp_cond *c =
478
		    cond_ensure(breakPoints[i].pc, breakPoints[i].bank, breakPoints[i].x16Bank);
479
		if (!c)
480
			return true; // out of memory: stopping is the safe way to be wrong
481
482
		if (!cond_satisfied(c))
483
			continue; // condition false, so this breakpoint is not firing
484
485
		c->hits++;
486
		if (c->hits > c->ignore)
487
			return true;
488
		// still inside the ignore count: counted, but keep running
489
	}
490
	return false;
491
}
492
493
void
494
debug_bp_set_condition(int pc, uint8_t bank, int x16Bank, int operand, uint16_t operand_addr,
495
                       int op, uint32_t value)
496
{
497
	struct bp_cond *c = cond_ensure(pc, bank, x16Bank);
498
	if (!c)
499
		return;
500
	c->has_cond     = true;
501
	c->operand      = operand;
502
	c->operand_addr = operand_addr;
503
	c->op           = op;
504
	c->value        = value;
505
}
506
507
void
508
debug_bp_clear_condition(int pc, uint8_t bank, int x16Bank)
509
{
510
	struct bp_cond *c = cond_find(pc, bank, x16Bank);
511
	if (c)
512
		c->has_cond = false; // drop the test, keep the count and ignore budget
513
}
514
515
void
516
debug_bp_forget(int pc, uint8_t bank, int x16Bank)
517
{
518
	struct bp_cond *c = cond_find(pc, bank, x16Bank);
519
	if (c)
520
		c->used = false;
521
}
522
523
void
524
debug_bp_set_ignore(int pc, uint8_t bank, int x16Bank, uint32_t ignore)
525
{
526
	struct bp_cond *c = cond_ensure(pc, bank, x16Bank);
527
	if (c)
528
		c->ignore = ignore;
529
}
530
531
uint32_t
532
debug_bp_get_hits(int pc, uint8_t bank, int x16Bank)
533
{
534
	struct bp_cond *c = cond_find(pc, bank, x16Bank);
535
	return c ? c->hits : 0;
536
}
537
538
void
539
debug_bp_reset_hits(int pc, uint8_t bank, int x16Bank)
540
{
541
	struct bp_cond *c = cond_find(pc, bank, x16Bank);
542
	if (c)
543
		c->hits = 0;
544
}
545
546
bool
547
debug_bp_get_condition(int pc, uint8_t bank, int x16Bank, int *has_cond, int *operand, uint16_t *operand_addr,
548
                       int *op, uint32_t *value, uint32_t *ignore)
549
{
550
	struct bp_cond *c = cond_find(pc, bank, x16Bank);
551
	if (!c)
552
		return false;
553
	if (has_cond)
554
		*has_cond = c->has_cond ? 1 : 0;
555
	if (operand)
556
		*operand = c->operand;
557
	if (operand_addr)
558
		*operand_addr = c->operand_addr;
559
	if (op)
560
		*op = c->op;
561
	if (value)
562
		*value = c->value;
563
	if (ignore)
564
		*ignore = c->ignore;
565
	return true;
566
}
567
568
void
569
debug_core_free(void)
570
{
571
	free(breakPoints);
572
	breakPoints    = NULL;
573
	numBreakpoints = 0;
574
	bpCapacity     = 0;
575
576
	free(bpConds);
577
	bpConds     = NULL;
578
	bpCondCount = 0;
579
	bpCondCap   = 0;
580
581
	debug_wp_clear_all();
582
}
583
584
// ---------------------------------------------------------------------------
585
//  Memory write watchpoints
586
// ---------------------------------------------------------------------------
587
//  A fixed table scanned on every CPU store. Everything here is shaped by that:
588
//  the scan is skipped entirely while the table is empty, each entry is a
589
//  handful of integer compares, and there is no allocation on the path.
590
591
static struct watchpoint watchPoints[MAX_WATCHPOINTS];
592
static int               numWatchpoints = 0;
593
594
// Watchpoints are identified by (addr, bank selector), exactly like
595
// breakpoints. Keying on the address alone would silently throw away the
596
// second of two watches on the same address in different banks -- the very
597
// case bank awareness exists for.
598
static int
599
wp_find(uint16_t addr, int x16Bank)
600
{
601
	x16Bank = normalise_bank(x16Bank, addr, 0);
602
	for (int i = 0; i < numWatchpoints; i++) {
603
		if (watchPoints[i].addr == addr && watchPoints[i].x16Bank == x16Bank)
604
			return i;
605
	}
606
	return -1;
607
}
608
609
int
610
debug_wp_count(void)
611
{
612
	return numWatchpoints;
613
}
614
615
// Read-only access to one watchpoint, for UIs that list them. Returns NULL for
616
// an out-of-range index rather than trusting the caller's bound, since the
617
// table shrinks as watchpoints are removed and a UI's cached count can outlive
618
// it. The table itself stays private: handing out a mutable pointer would let a
619
// caller change addr or x16Bank behind wp_find()'s back, and the identity of a
620
// watchpoint is exactly those two fields.
621
const struct watchpoint *
622
debug_wp_at(int index)
623
{
624
	if (index < 0 || index >= numWatchpoints)
625
		return NULL;
626
	return &watchPoints[index];
627
}
628
629
// Is there a watchpoint with exactly this identity? The counterpart to
630
// debug_wp_covers(), which asks the different question "would a write to this
631
// address fire something" and treats DEBUG_BANK_ANY as a wildcard. A UI that
632
// offers a toggle needs this one: deciding with covers() and then removing with
633
// an exact key means the control can report a watchpoint it is unable to
634
// remove, and the button does nothing.
635
bool
636
debug_wp_exists(uint16_t addr, int x16Bank)
637
{
638
	return wp_find(addr, x16Bank) >= 0;
639
}
640
641
int
642
debug_wp_add(uint16_t addr, uint16_t len, int x16Bank)
643
{
644
	if (wp_find(addr, x16Bank) >= 0)
645
		return -1;
646
	if (numWatchpoints >= MAX_WATCHPOINTS)
647
		return -1;
648
649
	if (len == 0)
650
		len = 1;
651
652
	struct watchpoint *w = &watchPoints[numWatchpoints];
653
	w->addr      = addr;
654
	w->len       = len;
655
	w->x16Bank   = normalise_bank(x16Bank, addr, 0);
656
	w->active    = true;
657
	w->has_value = false;
658
	w->value     = 0;
659
	w->op        = BPCMP_EQ;
660
	return numWatchpoints++;
661
}
662
663
static void
664
wp_erase(int idx)
665
{
666
	for (int i = idx; i < numWatchpoints - 1; i++) {
667
		watchPoints[i] = watchPoints[i + 1];
668
	}
669
	numWatchpoints--;
670
}
671
672
debug_add_result_t
673
debug_wp_add_for(uint16_t addr, uint16_t len, int x16Bank, debug_owner_t owner)
674
{
675
	const uint16_t bit = owner_bit(owner);
676
677
	int idx = wp_find(addr, x16Bank);
678
	if (idx >= 0) {
679
		// Length and value filter are left as the first owner set them: this
680
		// owner is asking to be told about the address, not to redefine
681
		// someone else's watch.
682
		watchPoints[idx].owners |= bit;
683
		return DEBUG_ADD_EXISTED;
684
	}
685
	if (numWatchpoints >= MAX_WATCHPOINTS)
686
		return DEBUG_ADD_FULL;
687
688
	if (len == 0)
689
		len = 1;
690
691
	struct watchpoint *w = &watchPoints[numWatchpoints];
692
	w->addr      = addr;
693
	w->len       = len;
694
	w->x16Bank   = normalise_bank(x16Bank, addr, 0);
695
	w->active    = true;
696
	w->has_value = false;
697
	w->value     = 0;
698
	w->op        = BPCMP_EQ;
699
	w->owners    = bit;
700
	numWatchpoints++;
701
	return DEBUG_ADD_CREATED;
702
}
703
704
bool
705
debug_wp_remove_for(uint16_t addr, int x16Bank, debug_owner_t owner)
706
{
707
	const uint16_t bit = owner_bit(owner);
708
709
	int idx = wp_find(addr, x16Bank);
710
	if (idx < 0 || !(watchPoints[idx].owners & bit))
711
		return false;
712
713
	watchPoints[idx].owners &= (uint16_t)~bit;
714
	if (watchPoints[idx].owners == 0)
715
		wp_erase(idx);
716
	return true;
717
}
718
719
int
720
debug_wp_clear_owner(debug_owner_t owner)
721
{
722
	const uint16_t bit = owner_bit(owner);
723
	int            disarmed = 0;
724
725
	for (int i = numWatchpoints - 1; i >= 0; i--) {
726
		if (!(watchPoints[i].owners & bit))
727
			continue;
728
		watchPoints[i].owners &= (uint16_t)~bit;
729
		if (watchPoints[i].owners == 0) {
730
			wp_erase(i);
731
			disarmed++;
732
		}
733
	}
734
	return disarmed;
735
}
736
737
bool
738
debug_wp_delete(uint16_t addr, int x16Bank)
739
{
740
	int idx = wp_find(addr, x16Bank);
741
	if (idx < 0)
742
		return false;
743
	wp_erase(idx);
744
	return true;
745
}
746
747
bool
748
debug_wp_has_owner(uint16_t addr, int x16Bank, debug_owner_t owner)
749
{
750
	int idx = wp_find(addr, x16Bank);
751
	return idx >= 0 && (watchPoints[idx].owners & owner_bit(owner)) != 0;
752
}
753
754
void
755
debug_wp_clear_all(void)
756
{
757
	numWatchpoints = 0;
758
}
759
760
bool
761
debug_wp_set_value(uint16_t addr, int x16Bank, int op, uint8_t value)
762
{
763
	int idx = wp_find(addr, x16Bank);
764
	if (idx < 0)
765
		return false;
766
	watchPoints[idx].has_value = true;
767
	watchPoints[idx].op        = op;
768
	watchPoints[idx].value     = value;
769
	return true;
770
}
771
772
bool
773
debug_wp_set_active(uint16_t addr, int x16Bank, bool active)
774
{
775
	int idx = wp_find(addr, x16Bank);
776
	if (idx < 0)
777
		return false;
778
	watchPoints[idx].active = active;
779
	return true;
780
}
781
782
// Drop the value filter, so the watchpoint fires on any write again. The
783
// counterpart to debug_wp_set_value(); without it a UI that offers a "only when
784
// the written value is..." toggle can turn the filter on but never off, since
785
// has_value is not otherwise reachable from outside.
786
bool
787
debug_wp_clear_value(uint16_t addr, int x16Bank)
788
{
789
	int idx = wp_find(addr, x16Bank);
790
	if (idx < 0)
791
		return false;
792
	watchPoints[idx].has_value = false;
793
	return true;
794
}
795
796
// Whether this watchpoint applies to the bank mapped right now. The same rule
797
// as breakpoints: $A100 holds a different variable in each bank, so a watch set
798
// for one must not fire on the others -- unless it was recorded as
799
// DEBUG_BANK_ANY, which asks for exactly that.
800
//
801
// Judged on the watchpoint's own start address rather than the address being
802
// written, so a range keeps one meaning throughout. The selector changes
803
// meaning at $C000 (RAM bank below, ROM bank above), and a range spanning that
804
// line would otherwise have its two halves tested against unrelated registers.
805
static bool
806
wp_bank_ok(const struct watchpoint *w)
807
{
808
	return bank_selector_matches(w->x16Bank, w->addr, 0);
809
}
810
811
static bool
812
wp_covers_addr(const struct watchpoint *w, uint16_t addr)
813
{
814
	return w->active && addr >= w->addr && (uint32_t)addr < (uint32_t)w->addr + w->len;
815
}
816
817
bool
818
debug_wp_covers(uint16_t addr)
819
{
820
	for (int i = 0; i < numWatchpoints; i++) {
821
		if (wp_covers_addr(&watchPoints[i], addr) && wp_bank_ok(&watchPoints[i]))
822
			return true;
823
	}
824
	return false;
825
}
826
827
bool
828
debug_wp_check_write(uint16_t addr, uint8_t value)
829
{
830
	for (int i = 0; i < numWatchpoints; i++) {
831
		const struct watchpoint *w = &watchPoints[i];
832
		if (!wp_covers_addr(w, addr) || !wp_bank_ok(w))
833
			continue;
834
		if (!w->has_value)
835
			return true;
836
		switch (w->op) {
837
			case BPCMP_EQ: if (value == w->value) return true; break;
838
			case BPCMP_NE: if (value != w->value) return true; break;
839
			case BPCMP_LT: if (value <  w->value) return true; break;
840
			case BPCMP_LE: if (value <= w->value) return true; break;
841
			case BPCMP_GT: if (value >  w->value) return true; break;
842
			case BPCMP_GE: if (value >= w->value) return true; break;
843
			default: return true;
844
		}
845
	}
846
	return false;
847
}