OXYGENHUB
◆ main enhanced-x16-emulator / tests / test_irq_ctx.c
20.0 KiB · C · last changed chore(): initial commit 25 days ago source blame history raw
9120e874 Andy McCall 1
// Checks for interrupt context tracking (src/cpu/irq_ctx.c).
2
//
3
// The CPU core reports an entry from interrupt6502() and a leave from rti(),
4
// and the two are paired by the stack pointer. That pairing is the whole
5
// design, and it has to survive things a real program does: nested handlers, a
6
// handler that returns further than its own frame, an RTI with no matching
7
// entry, and a reset in the middle of one. Getting it wrong leaves the debugger
8
// permanently convinced the machine is inside an interrupt, or never in one.
9
//
10
// The 6502 stack grows DOWN, so a deeper frame has a LOWER stack pointer, and
11
// an interrupt entry records the pointer BEFORE anything is pushed -- which is
12
// exactly what its matching RTI restores.
13
14
#include "cpu/irq_ctx.h"
15
16
#include <stdbool.h>
17
#include <stdint.h>
18
#include <stdio.h>
19
20
static int failures = 0;
21
22
static void
23
check(bool cond, const char *what)
24
{
25
	if (!cond) {
26
		failures++;
27
		printf("FAIL: %s\n", what);
28
	} else {
29
		printf("ok  : %s\n", what);
30
	}
31
}
32
33
// The vector values the CPU core passes through; their exact numbering does not
34
// matter here, only that it is reported back unchanged.
35
#define VEC_IRQ 0
36
#define VEC_NMI 1
37
#define VEC_BRK 2
38
39
// CPU_IRQ_CTX_MAX comes from the header rather than being restated here: a
40
// private copy that drifted from the real limit would leave the boundary checks
41
// below passing while no longer testing the boundary.
42
43
int
44
main(void)
45
{
46
	// ── Nothing has happened yet ────────────────────────────────────────────
47
	{
48
		cpu_irq_ctx_reset();
49
		check(!cpu_in_interrupt(), "starts outside any interrupt");
50
		check(cpu_irq_depth() == 0, "at depth zero");
51
		check(cpu_irq_count() == 0, "with nothing counted");
52
		check(cpu_irq_return_pc() == 0, "and no return address to report");
53
		check(!cpu_irq_take_entered_flag(), "and nothing just entered");
54
	}
55
56
	// ── One interrupt, taken and returned from ──────────────────────────────
57
	{
58
		cpu_irq_ctx_reset();
59
		cpu_irq_ctx_enter(VEC_IRQ, 0x1234, 0x01FD);
60
61
		check(cpu_in_interrupt(), "reports being inside a handler");
62
		check(cpu_irq_depth() == 1, "at depth one");
63
		check(cpu_irq_count() == 1, "counts the interrupt");
64
		check(cpu_irq_last_vector() == VEC_IRQ, "remembers which vector");
65
		check(cpu_irq_return_pc() == 0x1234, "reports where it will return to");
66
67
		// A balanced RTI restores the stack pointer to the entry value.
68
		cpu_irq_ctx_leave(0x01FD);
69
		check(!cpu_in_interrupt(), "reports leaving the handler");
70
		check(cpu_irq_depth() == 0, "back to depth zero");
71
		check(cpu_irq_count() == 1, "the count survives the return");
72
		check(cpu_irq_return_pc() == 0, "and there is no return address again");
73
	}
74
75
	// ── The just-entered flag is a one-shot ─────────────────────────────────
76
	// A debugger uses this to notice "an interrupt was just taken" while
77
	// stepping. It has to be true exactly once, or every poll would look like a
78
	// fresh interrupt.
79
	{
80
		cpu_irq_ctx_reset();
81
		check(!cpu_irq_take_entered_flag(), "no entry reported before one happens");
82
		cpu_irq_ctx_enter(VEC_IRQ, 0x1000, 0x01FD);
83
		check(cpu_irq_take_entered_flag(), "reports the entry once");
84
		check(!cpu_irq_take_entered_flag(), "and only once");
85
86
		// A second interrupt sets it again.
87
		cpu_irq_ctx_enter(VEC_NMI, 0x2000, 0x01FA);
88
		check(cpu_irq_take_entered_flag(), "a second entry is reported too");
89
	}
90
91
	// ── Nesting ─────────────────────────────────────────────────────────────
92
	// An NMI can interrupt an IRQ handler, and a handler that re-enables
93
	// interrupts can be interrupted again. Each RTI must unwind exactly one.
94
	{
95
		cpu_irq_ctx_reset();
96
		cpu_irq_ctx_enter(VEC_IRQ, 0x1000, 0x01FD);   // outer
97
		cpu_irq_ctx_enter(VEC_NMI, 0x2000, 0x01FA);   // inner, deeper stack
98
99
		check(cpu_irq_depth() == 2, "counts nested handlers");
100
		check(cpu_irq_count() == 2, "counts both interrupts");
101
		check(cpu_irq_last_vector() == VEC_NMI, "reports the innermost vector");
102
		check(cpu_irq_return_pc() == 0x2000, "reports the innermost return address");
103
104
		cpu_irq_ctx_leave(0x01FA);                    // inner RTI
105
		check(cpu_irq_depth() == 1, "unwinds one level per return");
106
		check(cpu_in_interrupt(), "still inside the outer handler");
107
		check(cpu_irq_return_pc() == 0x1000, "and reports its return address again");
108
109
		cpu_irq_ctx_leave(0x01FD);                    // outer RTI
110
		check(cpu_irq_depth() == 0, "unwinds the outer handler too");
111
		check(!cpu_in_interrupt(), "and is outside any interrupt");
112
	}
113
114
	// ── Returning past more than one frame ──────────────────────────────────
115
	// A handler that unwinds several frames at once, or discards them, must not
116
	// leave the depth stuck high for the rest of the session.
117
	{
118
		cpu_irq_ctx_reset();
119
		cpu_irq_ctx_enter(VEC_IRQ, 0x1000, 0x01FD);
120
		cpu_irq_ctx_enter(VEC_IRQ, 0x2000, 0x01FA);
121
		cpu_irq_ctx_enter(VEC_IRQ, 0x3000, 0x01F7);
122
		check(cpu_irq_depth() == 3, "three handlers open");
123
124
		// One RTI that restores the stack all the way back to the outermost
125
		// entry: every frame it returned past is finished.
126
		cpu_irq_ctx_leave(0x01FD);
127
		check(cpu_irq_depth() == 0, "a return past every frame unwinds them all");
128
		check(!cpu_in_interrupt(), "and leaves the machine outside any interrupt");
129
	}
130
131
	// ── An RTI with nothing to match ────────────────────────────────────────
132
	// A program can RTI without us having seen the entry -- an interrupt
133
	// already in flight at startup, or a hand-rolled stack frame. That is not
134
	// an error, and must not drive the depth negative.
135
	{
136
		cpu_irq_ctx_reset();
137
		cpu_irq_ctx_leave(0x01FD);
138
		check(cpu_irq_depth() == 0, "an unmatched return leaves depth at zero");
139
		check(!cpu_in_interrupt(), "and does not report being in a handler");
140
141
		cpu_irq_ctx_leave(0x01FD);
142
		cpu_irq_ctx_leave(0x01FD);
143
		check(cpu_irq_depth() == 0, "repeated unmatched returns stay at zero");
144
145
		// And the tracker still works afterwards.
146
		cpu_irq_ctx_enter(VEC_IRQ, 0x4000, 0x01FD);
147
		check(cpu_irq_depth() == 1 && cpu_irq_return_pc() == 0x4000,
148
		      "tracking still works after an unmatched return");
149
	}
150
151
	// ── Reset abandons anything in flight ───────────────────────────────────
152
	{
153
		cpu_irq_ctx_reset();
154
		cpu_irq_ctx_enter(VEC_IRQ, 0x1000, 0x01FD);
155
		cpu_irq_ctx_enter(VEC_NMI, 0x2000, 0x01FA);
156
		cpu_irq_ctx_reset();
157
158
		check(cpu_irq_depth() == 0, "reset abandons open handlers");
159
		check(!cpu_in_interrupt(), "and reports being outside one");
160
		check(cpu_irq_count() == 0, "and clears the count");
161
		check(cpu_irq_return_pc() == 0, "and the return address");
162
		check(!cpu_irq_take_entered_flag(), "and the just-entered flag");
163
	}
164
165
	// ── Deeper nesting than the tracker records detail for ──────────────────
166
	// Past its limit the depth still has to count correctly and come back down
167
	// to zero; only the per-frame detail is allowed to go missing.
168
	{
169
		cpu_irq_ctx_reset();
170
		uint16_t sp = 0x01FD;
171
		for (int i = 0; i < 40; i++) {
172
			cpu_irq_ctx_enter(VEC_IRQ, (uint16_t)(0x1000 + i), sp);
173
			sp = (uint16_t)(sp - 3);
174
		}
175
		check(cpu_irq_depth() == 40, "counts deeper nesting than it records");
176
		check(cpu_irq_count() == 40, "and counts every interrupt");
177
178
		// Unwind them one balanced return at a time.
179
		for (int i = 39; i >= 0; i--) {
180
			sp = (uint16_t)(sp + 3);
181
			cpu_irq_ctx_leave(sp);
182
		}
183
		check(cpu_irq_depth() == 0, "and unwinds all the way back out");
184
		check(!cpu_in_interrupt(), "ending outside any interrupt");
185
186
		// Just past the recorded frames, an unbalanced return must still pop
187
		// exactly one level rather than cascading on detail it never kept.
188
		cpu_irq_ctx_reset();
189
		sp = 0x01FD;
190
		for (int i = 0; i < CPU_IRQ_CTX_MAX + 1; i++) {
191
			cpu_irq_ctx_enter(VEC_IRQ, (uint16_t)(0x1000 + i), sp);
192
			sp = (uint16_t)(sp - 3);
193
		}
194
		check(cpu_irq_depth() == CPU_IRQ_CTX_MAX + 1, "opens one more than it records");
195
		// An sp far above every recorded frame: were the unrecorded level to
196
		// consult the frames it does not have, this would cascade.
197
		cpu_irq_ctx_leave(0x01FD);
198
		check(cpu_irq_depth() == CPU_IRQ_CTX_MAX,
199
		      "an unrecorded level pops exactly one, without cascading");
200
	}
201
202
	// ── A stack that wraps is out of contract ───────────────────────────────
203
	// Both rules here read the stack pointer: frames must decrease as nesting
204
	// deepens, and a balanced RTI lands exactly on the pointer its entry
205
	// recorded. A guest that pushes past the bottom of page 1 breaks the first
206
	// one -- an outer frame near the bottom ends up numerically ABOVE the frame
207
	// nested inside it, and is indistinguishable from a frame the stack has
208
	// legitimately risen back past.
209
	//
210
	// Nothing based on the stack pointer alone can tell those apart, so this
211
	// picks the case that actually happens: overflowing a 256-byte stack has
212
	// already destroyed the guest's own return addresses, while a BRK reaching a
213
	// warm start happens in every debugging session. The exact behaviour is
214
	// pinned rather than bounded, so changing that trade has to come here and
215
	// edit it deliberately.
216
	{
217
		cpu_irq_ctx_reset();
218
		cpu_irq_ctx_enter(VEC_IRQ, 0x1000, 0x0101);   // outer, near the bottom
219
		cpu_irq_ctx_enter(VEC_NMI, 0x2000, 0x01FE);   // pushes wrapped it round
220
221
		// The wrapped outer frame is retired, because it cannot be told from one
222
		// the stack has passed. This is the accepted false-low.
223
		check(cpu_irq_depth() == 1, "a wrapped outer frame is retired, not kept");
224
225
		cpu_irq_ctx_leave(0x01FE);
226
		check(cpu_irq_depth() == 0, "and the inner return closes what is left");
227
228
		// What makes this survivable, and better than the alternative: the very
229
		// next clean pair reads correctly, so the damage is bounded by the
230
		// wrapped handler's own lifetime rather than lasting until reset.
231
		cpu_irq_ctx_enter(VEC_IRQ, 0x3000, 0x01FD);
232
		check(cpu_irq_depth() == 1, "the next clean interrupt is tracked normally");
233
		check(cpu_irq_return_pc() == 0x3000, "with its own return address");
234
		cpu_irq_ctx_leave(0x01FD);
235
		check(cpu_irq_depth() == 0, "and closes cleanly");
236
	}
237
238
	// ── Retirement still works above the recorded frames ────────────────────
239
	// Only CPU_IRQ_CTX_MAX levels keep a pointer of their own. Judging the ones
240
	// above by the deepest that does is what stops the depth passing the array
241
	// and then never coming back down: an interrupt taken at or above the last
242
	// recorded frame proves everything from there up is gone.
243
	{
244
		cpu_irq_ctx_reset();
245
246
		// Nest past the array, each level below the last.
247
		uint16_t sp = 0x01FD;
248
		for (int i = 0; i < CPU_IRQ_CTX_MAX + 6; i++)
249
			cpu_irq_ctx_enter(VEC_IRQ, 0x1000, sp--);
250
		check(cpu_irq_depth() == CPU_IRQ_CTX_MAX + 6, "nesting can pass the array");
251
252
		// An interrupt arrives partway down, at the level frames[8] recorded.
253
		// Everything from there up is gone -- the six unrecorded levels and the
254
		// seven recorded ones above frames[8] -- while frames[0..7] are still
255
		// live and must survive. Landing between two recorded frames is what
256
		// makes this test the difference between collapsing to the deepest
257
		// surviving frame and simply wiping the depth.
258
		cpu_irq_ctx_enter(VEC_IRQ, 0x2000, 0x01F5);   // == frames[8].sp
259
		check(cpu_irq_depth() == 9,
260
		      "levels above the array collapse onto the deepest surviving frame");
261
		check(cpu_irq_return_pc() == 0x2000, "and the new frame is the innermost");
262
263
		// The frames below it really are still there.
264
		cpu_irq_ctx_leave(0x01F5);
265
		check(cpu_irq_depth() == 8, "leaving it uncovers the frame beneath");
266
267
		// And an interrupt above everything clears the lot.
268
		cpu_irq_ctx_enter(VEC_IRQ, 0x3000, 0x01FD);
269
		check(cpu_irq_depth() == 1, "an interrupt above them all retires the rest");
270
		cpu_irq_ctx_leave(0x01FD);
271
		check(cpu_irq_depth() == 0, "so the depth comes back to zero");
272
	}
273
	// ── Nesting that does not wrap is exact ─────────────────────────────────
274
	// The ordinary case, which is what the rules are for: each nested frame
275
	// sits below the one outside it, and a balanced return unwinds exactly one.
276
	{
277
		cpu_irq_ctx_reset();
278
		cpu_irq_ctx_enter(VEC_IRQ, 0x1000, 0x01FD);
279
		cpu_irq_ctx_enter(VEC_NMI, 0x2000, 0x01F9);
280
		check(cpu_irq_depth() == 2, "nested handlers count two");
281
282
		cpu_irq_ctx_leave(0x01F9);
283
		check(cpu_irq_depth() == 1, "a balanced inner return unwinds only its own");
284
		check(cpu_irq_return_pc() == 0x1000, "and the outer frame is reported again");
285
286
		cpu_irq_ctx_leave(0x01FD);
287
		check(cpu_irq_depth() == 0, "and the outer return closes it");
288
	}
289
290
	// ── A return past several frames unwinds them all ───────────────────────
291
	{
292
		cpu_irq_ctx_reset();
293
		cpu_irq_ctx_enter(VEC_IRQ, 0x1000, 0x01FD);
294
		cpu_irq_ctx_enter(VEC_NMI, 0x2000, 0x01F9);
295
		cpu_irq_ctx_enter(VEC_BRK, 0x3000, 0x01F5);
296
		cpu_irq_ctx_leave(0x01FD);                    // straight back to the outermost
297
		check(cpu_irq_depth() == 0,
298
		      "a return to an outer frame's own entry unwinds everything inside it");
299
	}
300
	// ── The count is cumulative ─────────────────────────────────────────────
301
	{
302
		cpu_irq_ctx_reset();
303
		for (int i = 0; i < 100; i++) {
304
			cpu_irq_ctx_enter(VEC_IRQ, 0x1000, 0x01FD);
305
			cpu_irq_ctx_leave(0x01FD);
306
		}
307
		check(cpu_irq_count() == 100, "counts every interrupt taken, not just open ones");
308
		check(cpu_irq_depth() == 0, "while ending balanced");
309
	}
310
311
	// ── Each vector is reported back unchanged ──────────────────────────────
312
	{
313
		cpu_irq_ctx_reset();
314
		const int vectors[] = { VEC_IRQ, VEC_NMI, VEC_BRK };
315
		bool ok = true;
316
		for (unsigned i = 0; i < sizeof(vectors) / sizeof(vectors[0]); i++) {
317
			cpu_irq_ctx_enter(vectors[i], 0x1000, 0x01FD);
318
			if (cpu_irq_last_vector() != vectors[i])
319
				ok = false;
320
			cpu_irq_ctx_leave(0x01FD);
321
		}
322
		check(ok, "reports back whichever vector was taken");
323
	}
324
325
	// ── A frame abandoned without an RTI ────────────────────────────────────
326
	// A BRK reaching a warm start resets the stack instead of returning, so its
327
	// entry never gets a matching RTI. Later interrupts must still be tracked
328
	// correctly rather than being swallowed by the stale frame, and the depth
329
	// must read HIGH rather than low: too high corrects itself at the next
330
	// matching return, too low claims nothing is running when something is.
331
	{
332
		cpu_irq_ctx_reset();
333
334
		cpu_irq_ctx_enter(VEC_BRK, 0x4000, 0x01F0);   // BRK, never returns
335
		// Warm start resets the stack to the top; an ordinary IRQ then arrives.
336
		cpu_irq_ctx_enter(VEC_IRQ, 0x5000, 0x01FD);
337
338
		// Taking that IRQ is what retires the abandoned BRK frame: the stack
339
		// had risen back above it, which proves it was gone.
340
		check(cpu_irq_depth() == 1, "an abandoned frame is retired, not stacked");
341
342
		cpu_irq_ctx_leave(0x01FD);                    // balanced IRQ return
343
		check(cpu_irq_depth() == 0,
344
		      "so the depth comes back to zero once the IRQ returns");
345
346
		// And it does not accumulate: a second BRK that never returns must not
347
		// leave the machine permanently looking like it is inside an interrupt.
348
		cpu_irq_ctx_enter(VEC_BRK, 0x4100, 0x01F0);
349
		cpu_irq_ctx_enter(VEC_IRQ, 0x5100, 0x01FD);
350
		cpu_irq_ctx_leave(0x01FD);
351
		check(cpu_irq_depth() == 0, "however many times it happens");
352
	}
353
354
	// ── The program bank is part of the return address ──────────────────────
355
	// Native-mode interrupts push the program bank, and the CPU zeroes it on
356
	// entry, so a handler cannot recover it. Recording only the low 16 bits
357
	// would send a step-over to the right offset in the wrong bank.
358
	{
359
		cpu_irq_ctx_reset();
360
361
		cpu_irq_ctx_enter(VEC_IRQ, 0x123456, 0x01FD);
362
		check(cpu_irq_return_pc() == 0x123456,
363
		      "the return address keeps its program bank");
364
365
		cpu_irq_ctx_enter(VEC_NMI, 0x008000, 0x01F9);
366
		check(cpu_irq_return_pc() == 0x008000,
367
		      "and a bank-zero address is unchanged");
368
369
		cpu_irq_ctx_leave(0x01F9);
370
		check(cpu_irq_return_pc() == 0x123456,
371
		      "and the outer one is restored when the inner returns");
372
	}
373
374
	// ── Runaway nesting stays safe ──────────────────────────────────────────
375
	// A BRK handler that BRKs never returns. Once the depth passes the array
376
	// the pointer that retirement judges against is frozen, so interrupts taken
377
	// below it pile up with nothing able to retire them. The saturation in the
378
	// code is what stops a signed counter overflowing, which would be undefined
379
	// behaviour rather than a wrong number.
380
	{
381
		cpu_irq_ctx_reset();
382
383
		// Walk the pointer all the way down, one byte per entry.
384
		for (int sp = 0xFFFF; sp >= 0; sp--)
385
			cpu_irq_ctx_enter(VEC_BRK, 0x6000, (uint16_t)sp);
386
387
		const int deep = cpu_irq_depth();
388
		check(deep > CPU_IRQ_CTX_MAX,
389
		      "nesting can exceed the frames it records detail for");
390
		check(deep == 0x10000, "up to the cap");
391
392
		// Entering again while already at the cap must not move it. Without the
393
		// clamp this simply keeps counting, and a long enough runaway would
394
		// overflow the counter rather than merely report a silly number.
395
		cpu_irq_ctx_enter(VEC_BRK, 0x6000, 0x0000);
396
		check(cpu_irq_depth() == 0x10000, "and stays there when it is passed");
397
		check(cpu_in_interrupt(), "and still reports an interrupt");
398
399
		// No detail is kept past the array, so those come off one at a time;
400
		// the recorded ones then close together when the outermost returns.
401
		for (int i = 0; i < deep - CPU_IRQ_CTX_MAX; i++)
402
			cpu_irq_ctx_leave(0x0000);
403
		check(cpu_irq_depth() == CPU_IRQ_CTX_MAX,
404
		      "undetailed levels come off one at a time");
405
406
		cpu_irq_ctx_leave(0xFFFF);   // back to the outermost frame's own entry
407
		check(cpu_irq_depth() == 0, "and the outermost return closes the rest");
408
	}
409
	// ── A frame abandoned at the same stack level ───────────────────────────
410
	// Two interrupts taken at the identical pointer cannot both be live: the
411
	// stack being back at that level means the first one's frame is gone. This
412
	// is the boundary of the retirement rule, and it has to be inclusive.
413
	{
414
		cpu_irq_ctx_reset();
415
416
		cpu_irq_ctx_enter(VEC_BRK, 0x7000, 0x01FD);   // abandoned
417
		cpu_irq_ctx_enter(VEC_IRQ, 0x7100, 0x01FD);   // same level again
418
		check(cpu_irq_depth() == 1,
419
		      "an interrupt at the same level retires the one before it");
420
421
		cpu_irq_ctx_leave(0x01FD);
422
		check(cpu_irq_depth() == 0, "and returns cleanly to zero");
423
	}
424
425
	// ── A return that matches nothing changes nothing ───────────────────────
426
	// A handler can push a fabricated frame and RTI to itself, landing on a
427
	// pointer no entry recorded, while the interrupt it is running inside is
428
	// still live. Popping on that would report no interrupt during one -- the
429
	// one answer a debugger must never give. Frames only close when something
430
	// proves they have: a return that lands exactly on them, or a later
431
	// interrupt taken at or above them.
432
	{
433
		cpu_irq_ctx_reset();
434
435
		cpu_irq_ctx_enter(VEC_IRQ, 0x8000, 0x01FD);
436
		cpu_irq_ctx_leave(0x01FA);                    // matches no recorded frame
437
		check(cpu_irq_depth() == 1,
438
		      "a return matching no frame leaves the depth alone");
439
		check(cpu_in_interrupt(),
440
		      "so a live handler is never reported as finished");
441
442
		cpu_irq_ctx_leave(0x01FD);                    // the real balanced return
443
		check(cpu_irq_depth() == 0, "and the real return still closes it");
444
	}
445
446
	if (failures) {
447
		printf("\nFAILED (%d failures)\n", failures);
448
		return 1;
449
	}
450
	printf("\nPASSED (0 failures)\n");
451
	return 0;
452
}