pico9918-core 1.3.0
TMS9918A / F18A video display processor emulation in C99
Loading...
Searching...
No Matches
debug_test.c
Go to the documentation of this file.
1/**
2 * \file
3 * \brief pico9918-core - the debugger surface
4 *
5 * Copyright (c) 2026 Troy Schrapel
6 *
7 * This code is licensed under the MIT license
8 *
9 * https://github.com/visrealm/pico9918-core
10 *
11 * What is under test is nowhere near the copy loops. It is that the regions land where the
12 * union actually puts its fields; that region, read and write agree about where each one
13 * stops, since a pane splitting bulk work on those boundaries has to land on the same
14 * bytes the transfer does; and that the register write does the four things it promises
15 * and not a fifth.
16 *
17 * That last one is checked twice. Once case by case - R30 on a locked device landing at
18 * R30 rather than R6, R55 not arming a program, R57 not moving the unlock latch - and
19 * once by snapshotting the whole instance across a write and asserting that exactly two
20 * bytes moved. The second is the one that catches state nobody thought to name, including
21 * state added to pico9918_t after this was written.
22 *
23 * The edges are here because every one of them is a coin-flip an implementation would
24 * otherwise settle silently: zero length, a null buffer, exactly at the end, past it, and
25 * a length that would run off the end of the map.
26 */
27
28#include "impl/pico9918_priv.h"
29#include "pico9918_debug.h"
30
31#include <stddef.h>
32#include <stdio.h>
33#include <stdlib.h>
34#include <string.h>
35
36static int failures;
37
38static void fail(const char* what, unsigned long wanted, unsigned long got)
39{
40 ++failures;
41 printf(" FAIL %s: want %lx got %lx\n", what, wanted, got);
42}
43
44static void expectRegion(const char* what, uint32_t addr, uint32_t wantFlags, uint32_t wantEnd)
45{
46 uint32_t end = 0xffffffffu;
47 uint32_t flags = pico9918_debug_region(addr, &end);
48
49 if (flags != wantFlags) fail(what, wantFlags, flags);
50 if (end != wantEnd) fail(what, wantEnd, end);
51}
52
53int main(void)
54{
55 const uint32_t size = pico9918_gpu_mem_size();
56
57 pico9918_init();
58
59 /* 1. the regions are where the union puts them, not where a table says they are. Each
60 is probed at its first byte and at its last, so an off-by-one in either bound of
61 the run shows up as a wrong end rather than passing on the midpoint. */
62 expectRegion("base-first", 0, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_WRITABLE, 0x5000);
63 expectRegion("base-last", 0x4fff, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_WRITABLE, 0x5000);
64 expectRegion("pram-first", 0x5000,
66 expectRegion("pram-last", 0x5fff,
68 expectRegion("regs-first", 0x6000, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_REGISTERS, 0x6040);
69 expectRegion("regs-last", 0x603f, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_REGISTERS, 0x6040);
70 expectRegion("gram2-first", 0x6040, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_WRITABLE, 0xb000);
71 expectRegion("gram2-last", 0xafff, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_WRITABLE, 0xb000);
72 expectRegion("status-first", 0xb000, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_STATUS, 0xb010);
73 expectRegion("status-last", 0xb00f, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_STATUS, 0xb010);
74 expectRegion("gram4-first", 0xb010, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_WRITABLE, size);
75 expectRegion("wrksp-last", size - 1, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_WRITABLE, size);
76
77 /* and the workspace overflow is inside the map, which is the whole reason the map is
78 not 64KB - R1-R15 live past 0xFFFF and a register pane has to reach them */
79 if (size <= 0x10000) fail("map-stops-at-64k", 0x10001, size);
80
81 /* 2. past the end terminates a walk rather than looping: 0 flags, end == addr */
82 expectRegion("past-end", size, 0, size);
83 expectRegion("far-past-end", 0xfffffff0u, 0, 0xfffffff0u);
84
85 /* 3. a walk covers the map exactly once - every byte in one region, no gap, no
86 overlap, and it terminates. This is the check that a hand-written table cannot
87 pass by accident. */
88 uint32_t at = 0, regions = 0;
89 while (at < size)
90 {
91 uint32_t end = 0;
92 if (pico9918_debug_region(at, &end) == 0) fail("walk-hole", 1, at);
93 if (end <= at)
94 {
95 fail("walk-stuck", at + 1, end);
96 break;
97 }
98 at = end;
99 ++regions;
100 }
101 if (at != size) fail("walk-end", size, at);
102 if (regions != 6) fail("walk-regions", 6, regions);
103
104 /* a null end is legal, because a caller that only wants the flags should not have to
105 invent somewhere to put a number it will not read */
106 if (pico9918_debug_region(0x5000, NULL) !=
108 fail("region-null-end", 1, 0);
109
110 /* 4. the span reads the backing state, and reads the same bytes the scalar accessor
111 does - one sentinel per region, written straight into the instance so the read is
112 not being compared against the path that wrote it */
113 static const uint32_t probes[] = {0x0000, 0x3fff, 0x4000, 0x5000, 0x6000,
114 0x6040, 0xb000, 0xb010, 0xffff};
115 for (unsigned i = 0; i < sizeof(probes) / sizeof(probes[0]); ++i)
116 {
117 uint8_t got = 0;
118
119 ((uint8_t*)&tms9918->vram)[probes[i]] = (uint8_t)(0x5a + i);
120 if (pico9918_debug_read(PICO9918_INST probes[i], &got, 1) != 1) fail("probe-short", 1, 0);
121 if (got != (uint8_t)(0x5a + i)) fail("probe-value", (uint8_t)(0x5a + i), got);
122 if (got != pico9918_gpu_mem_value(PICO9918_INST probes[i]))
123 fail("probe-vs-scalar", got, pico9918_gpu_mem_value(PICO9918_INST probes[i]));
124 }
125
126 /* the workspace overflow reads through the span too, which walking vram.bytes cannot
127 do - that array stops at 0xFFFF */
128 tms9918->vram.map.wrksp[4] = 0xa5;
129 {
130 uint8_t got = 0;
131
132 if (pico9918_debug_read(PICO9918_INST 0x10004, &got, 1) != 1) fail("wrksp-short", 1, 0);
133 if (got != 0xa5) fail("wrksp-value", 0xa5, got);
134 }
135
136 /* 5. a span crossing a region boundary is NOT short - only the write refuses windows,
137 and a memory pane reading 0x5FF0-0x600F must see all 32 bytes */
138 {
139 uint8_t buf[32];
140
141 memset(buf, 0, sizeof(buf));
142 if (pico9918_debug_read(PICO9918_INST 0x5ff0, buf, sizeof(buf)) != sizeof(buf))
143 fail("read-across-window", (unsigned long)sizeof(buf), 0);
144 }
145
146 /* 6. the edges, each of which is a decision rather than a consequence */
147 {
148 uint8_t buf[16];
149
150 if (pico9918_debug_read(PICO9918_INST 0, buf, 0) != 0) fail("read-zero-len", 0, 1);
151 if (pico9918_debug_read(PICO9918_INST 0, NULL, sizeof(buf)) != 0) fail("read-null", 0, 1);
152 if (pico9918_debug_read(PICO9918_INST size, buf, sizeof(buf)) != 0) fail("read-at-end", 0, 1);
153 if (pico9918_debug_read(PICO9918_INST size + 1, buf, sizeof(buf)) != 0)
154 fail("read-past-end", 0, 1);
155
156 /* short rather than wrapped: four bytes left, sixteen asked for */
157 const size_t got = pico9918_debug_read(PICO9918_INST size - 4, buf, sizeof(buf));
158 if (got != 4) fail("read-short-at-end", 4, (unsigned long)got);
159 }
160
161 /* 7. the span write lands in the backing state, and stops at the first byte it will
162 not write rather than skipping it and carrying on */
163 {
164 uint8_t buf[32];
165
166 memset(buf, 0x77, sizeof(buf));
167 if (pico9918_debug_write(PICO9918_INST 0x1000, buf, sizeof(buf)) != sizeof(buf))
168 fail("write-vram", (unsigned long)sizeof(buf), 0);
169 if (((uint8_t*)&tms9918->vram)[0x1000] != 0x77) fail("write-vram-first", 0x77, 0);
170 if (((uint8_t*)&tms9918->vram)[0x101f] != 0x77) fail("write-vram-last", 0x77, 0);
171
172 /* into the register window from below: short at 0x6000, and the file is untouched */
173 ((uint8_t*)&tms9918->vram)[0x6000] = 0x11;
174 if (pico9918_debug_write(PICO9918_INST 0x5ff0, buf, sizeof(buf)) != 0x10)
175 fail("write-into-regs", 0x10, 0);
176 if (((uint8_t*)&tms9918->vram)[0x6000] != 0x11) fail("write-regs-leaked", 0x11, 0x77);
177
178 /* starting inside it: nothing at all, which is how a caller tells it was refused */
179 if (pico9918_debug_write(PICO9918_INST 0x6010, buf, sizeof(buf)) != 0)
180 fail("write-in-regs", 0, 1);
181
182 /* and the status window the same way, from both sides */
183 ((uint8_t*)&tms9918->vram)[0xb000] = 0x22;
184 if (pico9918_debug_write(PICO9918_INST 0xaff0, buf, sizeof(buf)) != 0x10)
185 fail("write-into-status", 0x10, 0);
186 if (((uint8_t*)&tms9918->vram)[0xb000] != 0x22) fail("write-status-leaked", 0x22, 0x77);
187 if (pico9918_debug_write(PICO9918_INST 0xb008, buf, sizeof(buf)) != 0)
188 fail("write-in-status", 0, 1);
189
190 /* a run that crosses two writable regions is NOT short - PRAM abuts plain GRAM and
191 they differ only in what a write there owes afterwards */
192 if (pico9918_debug_write(PICO9918_INST 0x4ff0, buf, sizeof(buf)) != sizeof(buf))
193 fail("write-across-pram", (unsigned long)sizeof(buf), 0);
194
195 /* the edges, same set as the read */
196 if (pico9918_debug_write(PICO9918_INST 0, buf, 0) != 0) fail("write-zero-len", 0, 1);
197 if (pico9918_debug_write(PICO9918_INST 0, NULL, sizeof(buf)) != 0) fail("write-null", 0, 1);
198 if (pico9918_debug_write(PICO9918_INST size, buf, sizeof(buf)) != 0) fail("write-at-end", 0, 1);
199
200 const size_t tail = pico9918_debug_write(PICO9918_INST size - 4, buf, sizeof(buf));
201 if (tail != 4) fail("write-short-at-end", 4, (unsigned long)tail);
202 }
203
204 /* 8. and a write into PRAM leaves the palette owing a republish, or the edit takes and
205 the picture does not change */
206 {
207 const uint8_t entry[2] = {0x0f, 0x0a};
208
209 tms9918->palDirty = 0;
210 if (pico9918_debug_write(PICO9918_INST 0x5000, entry, sizeof(entry)) != sizeof(entry))
211 fail("write-pram", 2, 0);
212 if (!tms9918->palDirty) fail("write-pram-not-dirty", 1, 0);
213
214 /* a write that misses PRAM must NOT raise it, or the flag says nothing */
215 tms9918->palDirty = 0;
216 if (pico9918_debug_write(PICO9918_INST 0x1000, entry, sizeof(entry)) != sizeof(entry))
217 fail("write-vram-again", 2, 0);
218 if (tms9918->palDirty) fail("write-vram-dirtied-palette", 0, 1);
219
220 /* and a refused write raises nothing either, having done nothing */
221 tms9918->palDirty = 0;
222 if (pico9918_debug_write(PICO9918_INST 0x6000, entry, sizeof(entry)) != 0)
223 fail("write-regs-again", 0, 1);
224 if (tms9918->palDirty) fail("write-regs-dirtied-palette", 0, 1);
225 }
226
227 /* 9. the register read is the file's own byte. R30 on a LOCKED device is the case that
228 matters: the guest's read folds it to three bits and answers R6, and a pane
229 showing that is showing the wrong register with no way to tell. */
230 TMS_REGISTER(tms9918, 6) = 0x66;
231 TMS_REGISTER(tms9918, 30) = 0x30;
232 if (tms9918->isUnlocked) fail("locked-precondition", 0, 1);
233 if (pico9918_debug_reg(PICO9918_INST 30) != 0x30)
234 fail("reg-locked-30", 0x30, pico9918_debug_reg(PICO9918_INST 30));
236 fail("reg-guest-folds", 0x66, pico9918_reg_value(PICO9918_INST (pico9918_register_t)30));
237 if (pico9918_debug_reg(PICO9918_INST 64) != 0) fail("reg-out-of-range", 0, 1);
238
239 /* and it agrees with the same byte through the map, which is the other way to it */
240 if (pico9918_debug_reg(PICO9918_INST 30) != pico9918_gpu_mem_value(PICO9918_INST 0x6000 + 30))
241 fail("reg-vs-map", 0x30, pico9918_gpu_mem_value(PICO9918_INST 0x6000 + 30));
242
243 /* 10. the palette comes back in host order, undoing the big-endian storage - written
244 through the library's own path so the test is not just bswapping its own bswap */
245 tms9918->vram.map.pram[7] = __builtin_bswap16(0x0abc);
246 if (pico9918_debug_palette(PICO9918_INST 7) != 0x0abc)
247 fail("palette-host-order", 0x0abc, pico9918_debug_palette(PICO9918_INST 7));
248 if (pico9918_debug_palette(PICO9918_INST 64) != 0) fail("palette-out-of-range", 0, 1);
249
250 /* and it round-trips with the span write, which is what a palette editor does. The
251 order in the map is the low byte first and it holds R, the second holding GB - so
252 0x0acb is stored 0a cb, not cb 0a. */
253 {
254 const uint8_t entry[2] = {0x0a, 0xcb};
255
256 if (pico9918_debug_write(PICO9918_INST 0x5000 + 7 * 2, entry, sizeof(entry)) != 2)
257 fail("palette-write", 2, 0);
258 if (pico9918_debug_palette(PICO9918_INST 7) != 0x0acb)
259 fail("palette-round-trip", 0x0acb, pico9918_debug_palette(PICO9918_INST 7));
260 }
261
262 /* 11. the address latch is EFFECTIVE. A 4K chip with R1's 16K bit clear permutes the
263 address rather than masking it, so a mask would name a different byte - this is
264 the case the plain mask gets wrong. */
265 tms9918->currentAddress = 0x1234;
266 if (pico9918_debug_vram_address(PICO9918_INST_ONLY) !=
268 fail("vram-addr", pico9918_cpu_vram_addr_impl(PICO9918_INST 0x1234),
269 pico9918_debug_vram_address(PICO9918_INST_ONLY));
270
271 /* and the counter runs past the bus width between accesses, where the raw field is not
272 an address at all */
273 tms9918->currentAddress = 0x1ffff;
274 if (pico9918_debug_vram_address(PICO9918_INST_ONLY) > 0xffff) fail("vram-addr-wide", 0, 1);
275
276 /* 12. the GPU PC moves without the program starting, and without the register file or
277 the armed state moving with it */
278 {
279 const uint8_t r54 = pico9918_debug_reg(PICO9918_INST PICO9918_REG_GPU_PC_MSB);
280 const uint8_t r55 = pico9918_debug_reg(PICO9918_INST PICO9918_REG_GPU_PC_LSB);
281
282 tms9918->restart = 0;
283 pico9918_debug_gpu_set_pc(PICO9918_INST 0x2468);
284 if (pico9918_gpu_pc(PICO9918_INST_ONLY) != 0x2468)
285 fail("set-pc", 0x2468, pico9918_gpu_pc(PICO9918_INST_ONLY));
286 if (pico9918_debug_gpu_armed(PICO9918_INST_ONLY)) fail("set-pc-armed-it", 0, 1);
287 if (pico9918_debug_reg(PICO9918_INST PICO9918_REG_GPU_PC_MSB) != r54) fail("set-pc-r54", r54, 0);
288 if (pico9918_debug_reg(PICO9918_INST PICO9918_REG_GPU_PC_LSB) != r55) fail("set-pc-r55", r55, 0);
289
290 /* odd is masked even, the way the register path masks it */
291 pico9918_debug_gpu_set_pc(PICO9918_INST 0x2469);
292 if (pico9918_gpu_pc(PICO9918_INST_ONLY) != 0x2468)
293 fail("set-pc-odd", 0x2468, pico9918_gpu_pc(PICO9918_INST_ONLY));
294
295 /* and an armed program stays armed - this redirects one, it does not disarm one */
296 tms9918->restart = 1;
297 if (!pico9918_debug_gpu_armed(PICO9918_INST_ONLY)) fail("armed", 1, 0);
298 pico9918_debug_gpu_set_pc(PICO9918_INST 0x1000);
299 if (!pico9918_debug_gpu_armed(PICO9918_INST_ONLY)) fail("set-pc-disarmed-it", 1, 0);
300 tms9918->restart = 0;
301 }
302
303 /* 13. the register write is the physical store. Each case is one postcondition, and
304 the preserved half is checked by snapshotting the instance and diffing it - a
305 list of fields written out by hand is a list that goes stale. */
306 {
307 /* R30 on a LOCKED device: the bug that removed the first attempt at a public
308 register write, and the one a "write R57, check R1" test cannot see */
309 TMS_REGISTER(tms9918, 6) = 0x66;
310 if (tms9918->isUnlocked) fail("write-locked-precondition", 0, 1);
311 if (!pico9918_debug_reg_write(PICO9918_INST 30, 0x3c)) fail("write-reg-30", 1, 0);
312 if (pico9918_debug_reg(PICO9918_INST 30) != 0x3c)
313 fail("write-reg-30-value", 0x3c, pico9918_debug_reg(PICO9918_INST 30));
314 if (pico9918_debug_reg(PICO9918_INST 6) != 0x66)
315 fail("write-reg-30-hit-6", 0x66, pico9918_debug_reg(PICO9918_INST 6));
316
317 /* above the file: nothing happens and it says so */
318 TMS_REGISTER(tms9918, 0) = 0x0d;
319 if (pico9918_debug_reg_write(PICO9918_INST 64, 0xff)) fail("write-reg-64", 0, 1);
320 if (pico9918_debug_reg(PICO9918_INST 0) != 0x0d) fail("write-reg-64-wrapped", 0x0d, 0);
321
322 /* the palette is left owing a republish */
323 tms9918->palDirty = 0;
324 pico9918_debug_reg_write(PICO9918_INST 7, 0x21);
325 if (!tms9918->palDirty) fail("write-reg-not-dirty", 1, 0);
326
327 /* and the cached mode is current WITHOUT a scanline having run, which is the case
328 pico9918_frame.c hits: it reads the mode before it calls the scanline that would
329 refresh it. Both mode registers, since either can change the answer. */
330 pico9918_debug_reg_write(PICO9918_INST TMS_REG_0, 0);
331 pico9918_debug_reg_write(PICO9918_INST TMS_REG_1, TMS_R1_MODE_TEXT | TMS_R1_DISP_ACTIVE);
332 if (pico9918_display_mode(PICO9918_INST_ONLY) != TMS_MODE_TEXT)
333 fail("write-reg-mode-stale", TMS_MODE_TEXT, pico9918_display_mode(PICO9918_INST_ONLY));
334 pico9918_debug_reg_write(PICO9918_INST TMS_REG_1, TMS_R1_DISP_ACTIVE);
335 if (pico9918_display_mode(PICO9918_INST_ONLY) != TMS_MODE_GRAPHICS_I)
336 fail("write-reg-mode-back", TMS_MODE_GRAPHICS_I, pico9918_display_mode(PICO9918_INST_ONLY));
337
338 /* R0's M4 is honoured while still locked, so it moves the mode from the other side */
339 pico9918_debug_reg_write(PICO9918_INST TMS_REG_0, TMS_R0_MODE_TEXT_80);
340 if (pico9918_display_mode(PICO9918_INST_ONLY) != TMS_MODE_TEXT80)
341 fail("write-reg-mode-r0", TMS_MODE_TEXT80, pico9918_display_mode(PICO9918_INST_ONLY));
342 pico9918_debug_reg_write(PICO9918_INST TMS_REG_0, 0);
343
344 /* /INT follows R1's enable at once, rather than at the next active scanline */
345 TMS_STATUS(tms9918, PICO9918_SR_STATUS) |= PICO9918_SR0_INT;
346 pico9918_debug_reg_write(PICO9918_INST TMS_REG_1, TMS_R1_DISP_ACTIVE | TMS_R1_INT_ENABLE);
347 if (!tms9918->frameInt) fail("write-reg-int-not-raised", 1, 0);
348 pico9918_debug_reg_write(PICO9918_INST TMS_REG_1, TMS_R1_DISP_ACTIVE);
349 if (tms9918->frameInt) fail("write-reg-int-not-dropped", 0, 1);
350 TMS_STATUS(tms9918, PICO9918_SR_STATUS) &= (uint8_t)~PICO9918_SR0_INT;
351
352 /* THE UNLOCK LATCH IS PRESERVED. Written once and written twice leave the same byte
353 and the same count, so the byte cannot say which - and typing into a register pane
354 is not the handshake. Both writes must leave a locked device locked. */
355 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_UNLOCK, 0x1c);
356 if (tms9918->isUnlocked) fail("write-r57-once-unlocked", 0, 1);
357 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_UNLOCK, 0x1c);
358 if (tms9918->isUnlocked) fail("write-r57-twice-unlocked", 0, 1);
359 if (tms9918->lockedMask != 0x07) fail("write-r57-moved-mask", 0x07, tms9918->lockedMask);
360 if (pico9918_debug_reg(PICO9918_INST PICO9918_REG_UNLOCK) != 0x1c)
361 fail("write-r57-value", 0x1c, pico9918_debug_reg(PICO9918_INST PICO9918_REG_UNLOCK));
362
363 /* and the actions are not taken. R55 arms a program, R56 runs one, R63 begins a
364 firmware update, R50 bit 7 resets the file - through here, none of them do. */
365 pico9918_debug_gpu_set_pc(PICO9918_INST 0x2000);
366 tms9918->restart = 0;
367 tms9918->flash = 0;
368 tms9918->configDirty = false;
369 TMS_REGISTER(tms9918, 9) = 0x99;
370
371 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_GPU_PC_LSB, 0x40);
372 if (pico9918_debug_gpu_armed(PICO9918_INST_ONLY)) fail("write-r55-armed", 0, 1);
373 if (pico9918_gpu_pc(PICO9918_INST_ONLY) != 0x2000)
374 fail("write-r55-moved-pc", 0x2000, pico9918_gpu_pc(PICO9918_INST_ONLY));
375
377 if (pico9918_debug_gpu_armed(PICO9918_INST_ONLY)) fail("write-r56-armed", 0, 1);
378
379 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_FLASH_CONTROL, 0x80);
380 if (tms9918->flash) fail("write-r63-flashed", 0, 1);
381
383 if (pico9918_debug_reg(PICO9918_INST 9) != 0x99)
384 fail("write-r50-reset-file", 0x99, pico9918_debug_reg(PICO9918_INST 9));
385 if (tms9918->configDirty) fail("write-r50-config-dirty", 0, 1);
386
387 /* R30 of zero stays zero: the device folds it to the sprite maximum, this does not */
388 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_MAX_SCAN_SPRITES, 0);
389 if (pico9918_debug_reg(PICO9918_INST PICO9918_REG_MAX_SCAN_SPRITES) != 0)
390 fail("write-r30-zero-folded", 0,
391 pico9918_debug_reg(PICO9918_INST PICO9918_REG_MAX_SCAN_SPRITES));
392
393 /* R15 does not snap the timers into the status file */
394 TMS_STATUS(tms9918, 0x0f) = 0xee;
395 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_STATUS_SELECT, 0x45);
396 if (TMS_STATUS(tms9918, 0x0f) != 0xee) fail("write-r15-snapped", 0xee, TMS_STATUS(tms9918, 0x0f));
397 }
398
399 /* 14. and the preserved half, which the cases above cannot cover: every field NOT
400 named in the contract, including the ones nobody thought to name. The whole
401 instance is snapshotted, one neutral register written, and the bytes that moved
402 compared against the two the contract allows. A field added to pico9918_t later
403 and touched here fails this without anyone updating a list. */
404 {
405 uint8_t* const before = malloc(sizeof(pico9918_t));
406 const uint8_t* const live = (const uint8_t*)tms9918;
407 const uint32_t regByte = 0x6000 + 20;
408 const uint32_t dirty = (uint32_t)offsetof(pico9918_t, palDirty);
409 unsigned moved = 0;
410
411 if (!before) return fail("snapshot-alloc", 1, 0), 1;
412
413 TMS_REGISTER(tms9918, 20) = 0x00;
414 tms9918->palDirty = 0;
415 memcpy(before, tms9918, sizeof(pico9918_t));
416
417 if (!pico9918_debug_reg_write(PICO9918_INST 20, 0x5e)) fail("snapshot-write", 1, 0);
418
419 for (uint32_t i = 0; i < (uint32_t)sizeof(pico9918_t); ++i)
420 {
421 if (before[i] == live[i]) continue;
422 ++moved;
423 if (i != regByte && i != dirty) fail("write-reg-touched-offset", regByte, i);
424 }
425 if (moved != 2) fail("write-reg-moved-count", 2, moved);
426
427 free(before);
428 }
429
430 printf("%s: debugger surface, %d failure(s)\n", failures ? "FAIL" : "PASS", failures);
431 return failures ? 1 : 0;
432}
uint16_t pico9918_gpu_pc(pico9918_t *tms9918)
see the header.
Definition gpu.c:459
uint32_t pico9918_gpu_mem_size(void)
see the header.
Definition gpu.c:469
uint8_t pico9918_gpu_mem_value(pico9918_t *tms9918, uint32_t addr)
see the header.
Definition gpu.c:476
pico9918_mode_t pico9918_display_mode(pico9918_t *tms9918)
current display mode
Definition pico9918.c:3499
uint8_t pico9918_reg_value(pico9918_t *tms9918, pico9918_register_t reg)
return a register value - see the header for the locked-device aliasing
Definition pico9918.c:3329
#define TMS_R1_MODE_TEXT
40-column text
Definition pico9918.h:306
#define PICO9918_R56_GPU_RUN
register 56 bit: the GPU trigger
Definition pico9918.h:365
#define TMS_R0_MODE_TEXT_80
80-column text, with R1's text mode.
Definition pico9918.h:290
#define PICO9918_INST_ONLY
pass the instance as the only argument
Definition pico9918.h:74
#define PICO9918_R50_RESET
register 50 bits: GPU triggers and the remaining layer controls
Definition pico9918.h:355
#define TMS_R1_DISP_ACTIVE
render the active display
Definition pico9918.h:300
#define PICO9918_SR0_INT
status register 0 bits.
Definition pico9918.h:275
pico9918_register_t
the eight TMS9918 registers, by number and by what each one holds
Definition pico9918.h:198
@ PICO9918_REG_GPU_PC_MSB
GPU program counter, high byte.
Definition pico9918.h:239
@ PICO9918_REG_STATUS_SELECT
which status register S1 reads back, and the counter controls
Definition pico9918.h:219
@ PICO9918_REG_ENHANCED2
GPU triggers, per-position attributes, layer priority.
Definition pico9918.h:237
@ PICO9918_REG_UNLOCK
0x1c twice unlocks the F18A personality; any other value locks
Definition pico9918.h:242
@ PICO9918_REG_MAX_SCAN_SPRITES
sprites drawn per scanline before the limit bites
Definition pico9918.h:227
@ PICO9918_REG_GPU_PC_LSB
GPU program counter, low byte - writing it also starts the GPU.
Definition pico9918.h:240
@ PICO9918_REG_GPU_CONTROL
GPU load and trigger.
Definition pico9918.h:241
@ PICO9918_REG_FLASH_CONTROL
PICO9918 only: flash operation control.
Definition pico9918.h:245
@ PICO9918_SR_STATUS
the TMS9918A status: interrupt, 5th sprite, collision, sprite number
Definition pico9918.h:256
#define TMS_R1_INT_ENABLE
assert /INT at end of frame
Definition pico9918.h:301
#define PICO9918_INST
pass the instance ahead of other arguments
Definition pico9918.h:73
pico9918-core - debugger access
#define PICO9918_DEBUG_STATUS
the status file, which the library owns
#define PICO9918_DEBUG_WRITABLE
pico9918_debug_write stores here
#define PICO9918_DEBUG_REGISTERS
the register file - use pico9918_debug_reg_write
#define PICO9918_DEBUG_READABLE
pico9918_debug_read returns real bytes here
#define PICO9918_DEBUG_PALETTE
PRAM - a write here republishes the palette.
pico9918-core - the private instance layout
PICO9918_INLINE_HOT uint32_t pico9918_cpu_vram_addr_impl(pico9918_t *tms9918, uint32_t addr)
where a CPU-side VRAM access lands