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
53#if PICO9918_BUILD_LAYER_MASK
54
55static const uint8_t solidRow[8] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
56
57static void fillPattern(uint32_t at, uint32_t bytes, uint8_t seed)
58{
59 for (uint32_t i = 0; i < bytes; ++i)
60 {
61 const uint8_t byte = (uint8_t)(seed + i * 37u);
62 pico9918_debug_write(PICO9918_INST at + i, &byte, 1);
63 }
64}
65
66/* locked Graphics II, where the colour and pattern tables are separable at all */
67static void sceneLockedGm2(void)
68{
69 fillPattern(0x0000, 0x1800, 0x5a); /* pattern pages */
70 fillPattern(0x2000, 0x1800, 0xc3); /* colour pages */
71 fillPattern(0x3800, 0x0300, 0x11); /* names */
72
73 pico9918_debug_reg_write(PICO9918_INST TMS_REG_0, 0x02);
74 pico9918_debug_reg_write(PICO9918_INST TMS_REG_1, 0xe0);
75 pico9918_debug_reg_write(PICO9918_INST TMS_REG_NAME_TABLE, 0x0e);
76 pico9918_debug_reg_write(PICO9918_INST TMS_REG_COLOR_TABLE, 0xff);
77 pico9918_debug_reg_write(PICO9918_INST TMS_REG_PATTERN_TABLE, 0x03);
78 pico9918_debug_reg_write(PICO9918_INST TMS_REG_FG_BG_COLOR, 0x01);
79}
80
81static void unlock(void)
82{
83 tms9918->isUnlocked = true;
84 tms9918->lockedMask = 0x3f;
85}
86
87/* the same content with the display bit clear, which is the only state blanking changes */
88static void sceneBlanked(void)
89{
90 sceneLockedGm2();
91 pico9918_debug_reg_write(PICO9918_INST TMS_REG_1, 0xa0);
92}
93
94/* tile layer 2 alone: tile 1 off at the register, so only the mask's own bit is in play */
95static void sceneTile2Only(void)
96{
97 sceneLockedGm2();
98 unlock();
99 pico9918_debug_reg_write(PICO9918_INST TMS_REG_0, 0x00);
102 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_NAME_TABLE2, 0x0f);
103}
104
105/* the bitmap layer alone, over a backdrop both tile layers have been turned off at */
106static void sceneBmlOnly(void)
107{
108 sceneLockedGm2();
109 unlock();
110 pico9918_debug_reg_write(PICO9918_INST TMS_REG_0, 0x00);
113 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_BML_BASE, 0x40);
114 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_BML_WIDTH, 0);
115 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_BML_HEIGHT, 192);
116 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_BML_TOP_ROW, 0);
117}
118
119/* six sprites overlapping on one line: enough to latch collision and the fifth together */
120static void sceneSprites(void)
121{
122 sceneLockedGm2();
123 pico9918_debug_reg_write(PICO9918_INST TMS_REG_SPRITE_ATTR_TABLE, 0x70);
124 pico9918_debug_reg_write(PICO9918_INST TMS_REG_SPRITE_PATT_TABLE, 0x00);
125 pico9918_debug_write(PICO9918_INST 0x0000, solidRow, sizeof(solidRow));
126
127 for (unsigned i = 0; i < 6; ++i)
128 {
129 const uint8_t attr[4] = {16, (uint8_t)(20 + i * 4), 0, 0x0f};
130 pico9918_debug_write(PICO9918_INST 0x3800 + i * 4, attr, sizeof(attr));
131 }
132 pico9918_debug_write(PICO9918_INST 0x3818, solidRow, 1);
133}
134
135/* one line of a scene under one mask, as a digest. The reset is inside; the mask is set
136 after it, because a reset does not clear one. */
137static uint32_t suppressDigest(uint32_t mask, void (*scene)(void), uint16_t y, uint8_t* status)
138{
139 uint32_t digest = 2166136261u;
140 uint8_t got;
141
143 scene();
144 pico9918_debug_set_suppress(PICO9918_INST mask);
145
147 if (status) *status = got;
148
149 for (uint32_t i = 0; i < pico9918_line_bytes(PICO9918_INST_ONLY); ++i)
150 digest = (digest ^ pico9918_line_source(PICO9918_INST_ONLY)[i]) * 16777619u;
151
152 return digest;
153}
154
155#endif
156
157int main(void)
158{
159 const uint32_t size = pico9918_gpu_mem_size();
160
161 pico9918_init();
162
163 /* 1. the regions are where the union puts them, not where a table says they are. Each
164 is probed at its first byte and at its last, so an off-by-one in either bound of
165 the run shows up as a wrong end rather than passing on the midpoint. */
166 expectRegion("base-first", 0, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_WRITABLE, 0x5000);
167 expectRegion("base-last", 0x4fff, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_WRITABLE, 0x5000);
168 expectRegion("pram-first", 0x5000,
170 expectRegion("pram-last", 0x5fff,
172 expectRegion("regs-first", 0x6000, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_REGISTERS, 0x6040);
173 expectRegion("regs-last", 0x603f, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_REGISTERS, 0x6040);
174 expectRegion("gram2-first", 0x6040, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_WRITABLE, 0xb000);
175 expectRegion("gram2-last", 0xafff, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_WRITABLE, 0xb000);
176 expectRegion("status-first", 0xb000, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_STATUS, 0xb010);
177 expectRegion("status-last", 0xb00f, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_STATUS, 0xb010);
178 expectRegion("gram4-first", 0xb010, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_WRITABLE, size);
179 expectRegion("wrksp-last", size - 1, PICO9918_DEBUG_READABLE | PICO9918_DEBUG_WRITABLE, size);
180
181 /* and the workspace overflow is inside the map, which is the whole reason the map is
182 not 64KB - R1-R15 live past 0xFFFF and a register pane has to reach them */
183 if (size <= 0x10000) fail("map-stops-at-64k", 0x10001, size);
184
185 /* 2. past the end terminates a walk rather than looping: 0 flags, end == addr */
186 expectRegion("past-end", size, 0, size);
187 expectRegion("far-past-end", 0xfffffff0u, 0, 0xfffffff0u);
188
189 /* 3. a walk covers the map exactly once - every byte in one region, no gap, no
190 overlap, and it terminates. This is the check that a hand-written table cannot
191 pass by accident. */
192 uint32_t at = 0, regions = 0;
193 while (at < size)
194 {
195 uint32_t end = 0;
196 if (pico9918_debug_region(at, &end) == 0) fail("walk-hole", 1, at);
197 if (end <= at)
198 {
199 fail("walk-stuck", at + 1, end);
200 break;
201 }
202 at = end;
203 ++regions;
204 }
205 if (at != size) fail("walk-end", size, at);
206 if (regions != 6) fail("walk-regions", 6, regions);
207
208 /* a null end is legal, because a caller that only wants the flags should not have to
209 invent somewhere to put a number it will not read */
210 if (pico9918_debug_region(0x5000, NULL) !=
212 fail("region-null-end", 1, 0);
213
214 /* 4. the span reads the backing state, and reads the same bytes the scalar accessor
215 does - one sentinel per region, written straight into the instance so the read is
216 not being compared against the path that wrote it */
217 static const uint32_t probes[] = {0x0000, 0x3fff, 0x4000, 0x5000, 0x6000,
218 0x6040, 0xb000, 0xb010, 0xffff};
219 for (unsigned i = 0; i < sizeof(probes) / sizeof(probes[0]); ++i)
220 {
221 uint8_t got = 0;
222
223 ((uint8_t*)&tms9918->vram)[probes[i]] = (uint8_t)(0x5a + i);
224 if (pico9918_debug_read(PICO9918_INST probes[i], &got, 1) != 1) fail("probe-short", 1, 0);
225 if (got != (uint8_t)(0x5a + i)) fail("probe-value", (uint8_t)(0x5a + i), got);
226 if (got != pico9918_gpu_mem_value(PICO9918_INST probes[i]))
227 fail("probe-vs-scalar", got, pico9918_gpu_mem_value(PICO9918_INST probes[i]));
228 }
229
230 /* the workspace overflow reads through the span too, which walking vram.bytes cannot
231 do - that array stops at 0xFFFF */
232 tms9918->vram.map.wrksp[4] = 0xa5;
233 {
234 uint8_t got = 0;
235
236 if (pico9918_debug_read(PICO9918_INST 0x10004, &got, 1) != 1) fail("wrksp-short", 1, 0);
237 if (got != 0xa5) fail("wrksp-value", 0xa5, got);
238 }
239
240 /* 5. a span crossing a region boundary is NOT short - only the write refuses windows,
241 and a memory pane reading 0x5FF0-0x600F must see all 32 bytes */
242 {
243 uint8_t buf[32];
244
245 memset(buf, 0, sizeof(buf));
246 if (pico9918_debug_read(PICO9918_INST 0x5ff0, buf, sizeof(buf)) != sizeof(buf))
247 fail("read-across-window", (unsigned long)sizeof(buf), 0);
248 }
249
250 /* 6. the edges, each of which is a decision rather than a consequence */
251 {
252 uint8_t buf[16];
253
254 if (pico9918_debug_read(PICO9918_INST 0, buf, 0) != 0) fail("read-zero-len", 0, 1);
255 if (pico9918_debug_read(PICO9918_INST 0, NULL, sizeof(buf)) != 0) fail("read-null", 0, 1);
256 if (pico9918_debug_read(PICO9918_INST size, buf, sizeof(buf)) != 0) fail("read-at-end", 0, 1);
257 if (pico9918_debug_read(PICO9918_INST size + 1, buf, sizeof(buf)) != 0)
258 fail("read-past-end", 0, 1);
259
260 /* short rather than wrapped: four bytes left, sixteen asked for */
261 const size_t got = pico9918_debug_read(PICO9918_INST size - 4, buf, sizeof(buf));
262 if (got != 4) fail("read-short-at-end", 4, (unsigned long)got);
263 }
264
265 /* 7. the span write lands in the backing state, and stops at the first byte it will
266 not write rather than skipping it and carrying on */
267 {
268 uint8_t buf[32];
269
270 memset(buf, 0x77, sizeof(buf));
271 if (pico9918_debug_write(PICO9918_INST 0x1000, buf, sizeof(buf)) != sizeof(buf))
272 fail("write-vram", (unsigned long)sizeof(buf), 0);
273 if (((uint8_t*)&tms9918->vram)[0x1000] != 0x77) fail("write-vram-first", 0x77, 0);
274 if (((uint8_t*)&tms9918->vram)[0x101f] != 0x77) fail("write-vram-last", 0x77, 0);
275
276 /* into the register window from below: short at 0x6000, and the file is untouched */
277 ((uint8_t*)&tms9918->vram)[0x6000] = 0x11;
278 if (pico9918_debug_write(PICO9918_INST 0x5ff0, buf, sizeof(buf)) != 0x10)
279 fail("write-into-regs", 0x10, 0);
280 if (((uint8_t*)&tms9918->vram)[0x6000] != 0x11) fail("write-regs-leaked", 0x11, 0x77);
281
282 /* starting inside it: nothing at all, which is how a caller tells it was refused */
283 if (pico9918_debug_write(PICO9918_INST 0x6010, buf, sizeof(buf)) != 0)
284 fail("write-in-regs", 0, 1);
285
286 /* and the status window the same way, from both sides */
287 ((uint8_t*)&tms9918->vram)[0xb000] = 0x22;
288 if (pico9918_debug_write(PICO9918_INST 0xaff0, buf, sizeof(buf)) != 0x10)
289 fail("write-into-status", 0x10, 0);
290 if (((uint8_t*)&tms9918->vram)[0xb000] != 0x22) fail("write-status-leaked", 0x22, 0x77);
291 if (pico9918_debug_write(PICO9918_INST 0xb008, buf, sizeof(buf)) != 0)
292 fail("write-in-status", 0, 1);
293
294 /* a run that crosses two writable regions is NOT short - PRAM abuts plain GRAM and
295 they differ only in what a write there owes afterwards */
296 if (pico9918_debug_write(PICO9918_INST 0x4ff0, buf, sizeof(buf)) != sizeof(buf))
297 fail("write-across-pram", (unsigned long)sizeof(buf), 0);
298
299 /* the edges, same set as the read */
300 if (pico9918_debug_write(PICO9918_INST 0, buf, 0) != 0) fail("write-zero-len", 0, 1);
301 if (pico9918_debug_write(PICO9918_INST 0, NULL, sizeof(buf)) != 0) fail("write-null", 0, 1);
302 if (pico9918_debug_write(PICO9918_INST size, buf, sizeof(buf)) != 0) fail("write-at-end", 0, 1);
303
304 const size_t tail = pico9918_debug_write(PICO9918_INST size - 4, buf, sizeof(buf));
305 if (tail != 4) fail("write-short-at-end", 4, (unsigned long)tail);
306 }
307
308 /* 8. and a write into PRAM leaves the palette owing a republish, or the edit takes and
309 the picture does not change */
310 {
311 const uint8_t entry[2] = {0x0f, 0x0a};
312
313 tms9918->palDirty = 0;
314 if (pico9918_debug_write(PICO9918_INST 0x5000, entry, sizeof(entry)) != sizeof(entry))
315 fail("write-pram", 2, 0);
316 if (!tms9918->palDirty) fail("write-pram-not-dirty", 1, 0);
317
318 /* a write that misses PRAM must NOT raise it, or the flag says nothing */
319 tms9918->palDirty = 0;
320 if (pico9918_debug_write(PICO9918_INST 0x1000, entry, sizeof(entry)) != sizeof(entry))
321 fail("write-vram-again", 2, 0);
322 if (tms9918->palDirty) fail("write-vram-dirtied-palette", 0, 1);
323
324 /* and a refused write raises nothing either, having done nothing */
325 tms9918->palDirty = 0;
326 if (pico9918_debug_write(PICO9918_INST 0x6000, entry, sizeof(entry)) != 0)
327 fail("write-regs-again", 0, 1);
328 if (tms9918->palDirty) fail("write-regs-dirtied-palette", 0, 1);
329 }
330
331 /* 9. the register read is the file's own byte. R30 on a LOCKED device is the case that
332 matters: the guest's read folds it to three bits and answers R6, and a pane
333 showing that is showing the wrong register with no way to tell. */
334 TMS_REGISTER(tms9918, 6) = 0x66;
335 TMS_REGISTER(tms9918, 30) = 0x30;
336 if (tms9918->isUnlocked) fail("locked-precondition", 0, 1);
337 if (pico9918_debug_reg(PICO9918_INST 30) != 0x30)
338 fail("reg-locked-30", 0x30, pico9918_debug_reg(PICO9918_INST 30));
340 fail("reg-guest-folds", 0x66, pico9918_reg_value(PICO9918_INST (pico9918_register_t)30));
341 if (pico9918_debug_reg(PICO9918_INST 64) != 0) fail("reg-out-of-range", 0, 1);
342
343 /* and it agrees with the same byte through the map, which is the other way to it */
344 if (pico9918_debug_reg(PICO9918_INST 30) != pico9918_gpu_mem_value(PICO9918_INST 0x6000 + 30))
345 fail("reg-vs-map", 0x30, pico9918_gpu_mem_value(PICO9918_INST 0x6000 + 30));
346
347 /* 10. the palette comes back in host order, undoing the big-endian storage - written
348 through the library's own path so the test is not just bswapping its own bswap */
349 tms9918->vram.map.pram[7] = __builtin_bswap16(0x0abc);
350 if (pico9918_debug_palette(PICO9918_INST 7) != 0x0abc)
351 fail("palette-host-order", 0x0abc, pico9918_debug_palette(PICO9918_INST 7));
352 if (pico9918_debug_palette(PICO9918_INST 64) != 0) fail("palette-out-of-range", 0, 1);
353
354 /* and it round-trips with the span write, which is what a palette editor does. The
355 order in the map is the low byte first and it holds R, the second holding GB - so
356 0x0acb is stored 0a cb, not cb 0a. */
357 {
358 const uint8_t entry[2] = {0x0a, 0xcb};
359
360 if (pico9918_debug_write(PICO9918_INST 0x5000 + 7 * 2, entry, sizeof(entry)) != 2)
361 fail("palette-write", 2, 0);
362 if (pico9918_debug_palette(PICO9918_INST 7) != 0x0acb)
363 fail("palette-round-trip", 0x0acb, pico9918_debug_palette(PICO9918_INST 7));
364 }
365
366 /* 11. the address latch is EFFECTIVE. A 4K chip with R1's 16K bit clear permutes the
367 address rather than masking it, so a mask would name a different byte - this is
368 the case the plain mask gets wrong. */
369 tms9918->currentAddress = 0x1234;
370 if (pico9918_debug_vram_address(PICO9918_INST_ONLY) !=
372 fail("vram-addr", pico9918_cpu_vram_addr_impl(PICO9918_INST 0x1234),
373 pico9918_debug_vram_address(PICO9918_INST_ONLY));
374
375 /* and the counter runs past the bus width between accesses, where the raw field is not
376 an address at all */
377 tms9918->currentAddress = 0x1ffff;
378 if (pico9918_debug_vram_address(PICO9918_INST_ONLY) > 0xffff) fail("vram-addr-wide", 0, 1);
379
380 /* 12. the GPU PC moves without the program starting, and without the register file or
381 the armed state moving with it */
382 {
383 const uint8_t r54 = pico9918_debug_reg(PICO9918_INST PICO9918_REG_GPU_PC_MSB);
384 const uint8_t r55 = pico9918_debug_reg(PICO9918_INST PICO9918_REG_GPU_PC_LSB);
385
386 tms9918->restart = 0;
387 pico9918_debug_gpu_set_pc(PICO9918_INST 0x2468);
388 if (pico9918_gpu_pc(PICO9918_INST_ONLY) != 0x2468)
389 fail("set-pc", 0x2468, pico9918_gpu_pc(PICO9918_INST_ONLY));
390 if (pico9918_debug_gpu_armed(PICO9918_INST_ONLY)) fail("set-pc-armed-it", 0, 1);
391 if (pico9918_debug_reg(PICO9918_INST PICO9918_REG_GPU_PC_MSB) != r54) fail("set-pc-r54", r54, 0);
392 if (pico9918_debug_reg(PICO9918_INST PICO9918_REG_GPU_PC_LSB) != r55) fail("set-pc-r55", r55, 0);
393
394 /* odd is masked even, the way the register path masks it */
395 pico9918_debug_gpu_set_pc(PICO9918_INST 0x2469);
396 if (pico9918_gpu_pc(PICO9918_INST_ONLY) != 0x2468)
397 fail("set-pc-odd", 0x2468, pico9918_gpu_pc(PICO9918_INST_ONLY));
398
399 /* and an armed program stays armed - this redirects one, it does not disarm one */
400 tms9918->restart = 1;
401 if (!pico9918_debug_gpu_armed(PICO9918_INST_ONLY)) fail("armed", 1, 0);
402 pico9918_debug_gpu_set_pc(PICO9918_INST 0x1000);
403 if (!pico9918_debug_gpu_armed(PICO9918_INST_ONLY)) fail("set-pc-disarmed-it", 1, 0);
404 tms9918->restart = 0;
405 }
406
407 /* 13. the register write is the physical store. Each case is one postcondition, and
408 the preserved half is checked by snapshotting the instance and diffing it - a
409 list of fields written out by hand is a list that goes stale. */
410 {
411 /* R30 on a LOCKED device: the bug that removed the first attempt at a public
412 register write, and the one a "write R57, check R1" test cannot see */
413 TMS_REGISTER(tms9918, 6) = 0x66;
414 if (tms9918->isUnlocked) fail("write-locked-precondition", 0, 1);
415 if (!pico9918_debug_reg_write(PICO9918_INST 30, 0x3c)) fail("write-reg-30", 1, 0);
416 if (pico9918_debug_reg(PICO9918_INST 30) != 0x3c)
417 fail("write-reg-30-value", 0x3c, pico9918_debug_reg(PICO9918_INST 30));
418 if (pico9918_debug_reg(PICO9918_INST 6) != 0x66)
419 fail("write-reg-30-hit-6", 0x66, pico9918_debug_reg(PICO9918_INST 6));
420
421 /* above the file: nothing happens and it says so */
422 TMS_REGISTER(tms9918, 0) = 0x0d;
423 if (pico9918_debug_reg_write(PICO9918_INST 64, 0xff)) fail("write-reg-64", 0, 1);
424 if (pico9918_debug_reg(PICO9918_INST 0) != 0x0d) fail("write-reg-64-wrapped", 0x0d, 0);
425
426 /* the palette is left owing a republish */
427 tms9918->palDirty = 0;
428 pico9918_debug_reg_write(PICO9918_INST 7, 0x21);
429 if (!tms9918->palDirty) fail("write-reg-not-dirty", 1, 0);
430
431 /* and the cached mode is current WITHOUT a scanline having run, which is the case
432 pico9918_frame.c hits: it reads the mode before it calls the scanline that would
433 refresh it. Both mode registers, since either can change the answer. */
434 pico9918_debug_reg_write(PICO9918_INST TMS_REG_0, 0);
435 pico9918_debug_reg_write(PICO9918_INST TMS_REG_1, TMS_R1_MODE_TEXT | TMS_R1_DISP_ACTIVE);
436 if (pico9918_display_mode(PICO9918_INST_ONLY) != TMS_MODE_TEXT)
437 fail("write-reg-mode-stale", TMS_MODE_TEXT, pico9918_display_mode(PICO9918_INST_ONLY));
438 pico9918_debug_reg_write(PICO9918_INST TMS_REG_1, TMS_R1_DISP_ACTIVE);
439 if (pico9918_display_mode(PICO9918_INST_ONLY) != TMS_MODE_GRAPHICS_I)
440 fail("write-reg-mode-back", TMS_MODE_GRAPHICS_I, pico9918_display_mode(PICO9918_INST_ONLY));
441
442 /* R0's M4 is honoured while still locked, so it moves the mode from the other side */
443 pico9918_debug_reg_write(PICO9918_INST TMS_REG_0, TMS_R0_MODE_TEXT_80);
444 if (pico9918_display_mode(PICO9918_INST_ONLY) != TMS_MODE_TEXT80)
445 fail("write-reg-mode-r0", TMS_MODE_TEXT80, pico9918_display_mode(PICO9918_INST_ONLY));
446 pico9918_debug_reg_write(PICO9918_INST TMS_REG_0, 0);
447
448 /* /INT follows R1's enable at once, rather than at the next active scanline */
449 TMS_STATUS(tms9918, PICO9918_SR_STATUS) |= PICO9918_SR0_INT;
450 pico9918_debug_reg_write(PICO9918_INST TMS_REG_1, TMS_R1_DISP_ACTIVE | TMS_R1_INT_ENABLE);
451 if (!tms9918->frameInt) fail("write-reg-int-not-raised", 1, 0);
452 pico9918_debug_reg_write(PICO9918_INST TMS_REG_1, TMS_R1_DISP_ACTIVE);
453 if (tms9918->frameInt) fail("write-reg-int-not-dropped", 0, 1);
454 TMS_STATUS(tms9918, PICO9918_SR_STATUS) &= (uint8_t)~PICO9918_SR0_INT;
455
456 /* THE UNLOCK LATCH IS PRESERVED. Written once and written twice leave the same byte
457 and the same count, so the byte cannot say which - and typing into a register pane
458 is not the handshake. Both writes must leave a locked device locked. */
459 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_UNLOCK, 0x1c);
460 if (tms9918->isUnlocked) fail("write-r57-once-unlocked", 0, 1);
461 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_UNLOCK, 0x1c);
462 if (tms9918->isUnlocked) fail("write-r57-twice-unlocked", 0, 1);
463 if (tms9918->lockedMask != 0x07) fail("write-r57-moved-mask", 0x07, tms9918->lockedMask);
464 if (pico9918_debug_reg(PICO9918_INST PICO9918_REG_UNLOCK) != 0x1c)
465 fail("write-r57-value", 0x1c, pico9918_debug_reg(PICO9918_INST PICO9918_REG_UNLOCK));
466
467 /* and the actions are not taken. R55 arms a program, R56 runs one, R63 begins a
468 firmware update, R50 bit 7 resets the file - through here, none of them do. */
469 pico9918_debug_gpu_set_pc(PICO9918_INST 0x2000);
470 tms9918->restart = 0;
471 tms9918->flash = 0;
472 tms9918->configDirty = false;
473 TMS_REGISTER(tms9918, 9) = 0x99;
474
475 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_GPU_PC_LSB, 0x40);
476 if (pico9918_debug_gpu_armed(PICO9918_INST_ONLY)) fail("write-r55-armed", 0, 1);
477 if (pico9918_gpu_pc(PICO9918_INST_ONLY) != 0x2000)
478 fail("write-r55-moved-pc", 0x2000, pico9918_gpu_pc(PICO9918_INST_ONLY));
479
481 if (pico9918_debug_gpu_armed(PICO9918_INST_ONLY)) fail("write-r56-armed", 0, 1);
482
483 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_FLASH_CONTROL, 0x80);
484 if (tms9918->flash) fail("write-r63-flashed", 0, 1);
485
487 if (pico9918_debug_reg(PICO9918_INST 9) != 0x99)
488 fail("write-r50-reset-file", 0x99, pico9918_debug_reg(PICO9918_INST 9));
489 if (tms9918->configDirty) fail("write-r50-config-dirty", 0, 1);
490
491 /* R30 of zero stays zero: the device folds it to the sprite maximum, this does not */
492 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_MAX_SCAN_SPRITES, 0);
493 if (pico9918_debug_reg(PICO9918_INST PICO9918_REG_MAX_SCAN_SPRITES) != 0)
494 fail("write-r30-zero-folded", 0,
495 pico9918_debug_reg(PICO9918_INST PICO9918_REG_MAX_SCAN_SPRITES));
496
497 /* R15 does not snap the timers into the status file */
498 TMS_STATUS(tms9918, 0x0f) = 0xee;
499 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_STATUS_SELECT, 0x45);
500 if (TMS_STATUS(tms9918, 0x0f) != 0xee) fail("write-r15-snapped", 0xee, TMS_STATUS(tms9918, 0x0f));
501 }
502
503 /* 14. and the preserved half, which the cases above cannot cover: every field NOT
504 named in the contract, including the ones nobody thought to name. The whole
505 instance is snapshotted, one neutral register written, and the bytes that moved
506 compared against the two the contract allows. A field added to pico9918_t later
507 and touched here fails this without anyone updating a list. */
508 {
509 uint8_t* const before = malloc(sizeof(pico9918_t));
510 const uint8_t* const live = (const uint8_t*)tms9918;
511 const uint32_t regByte = 0x6000 + 20;
512 const uint32_t dirty = (uint32_t)offsetof(pico9918_t, palDirty);
513 unsigned moved = 0;
514
515 if (!before) return fail("snapshot-alloc", 1, 0), 1;
516
517 TMS_REGISTER(tms9918, 20) = 0x00;
518 tms9918->palDirty = 0;
519 memcpy(before, tms9918, sizeof(pico9918_t));
520
521 if (!pico9918_debug_reg_write(PICO9918_INST 20, 0x5e)) fail("snapshot-write", 1, 0);
522
523 for (uint32_t i = 0; i < (uint32_t)sizeof(pico9918_t); ++i)
524 {
525 if (before[i] == live[i]) continue;
526 ++moved;
527 if (i != regByte && i != dirty) fail("write-reg-touched-offset", regByte, i);
528 }
529 if (moved != 2) fail("write-reg-moved-count", 2, moved);
530
531 free(before);
532 }
533
534#if PICO9918_BUILD_LAYER_MASK
535 /* 15. suppression is a view over the renderer: nothing suppressed must not change the
536 picture, and something suppressed must not change what a guest can read. SR0's
537 collision and fifth-sprite bits are the hard half of the second. */
538 {
539 /* each bit in a scene built so that layer, and nothing else, is what it can remove */
540 static const struct
541 {
542 const char* name;
543 uint32_t bit;
544 void (*scene)(void);
545 } cases[] = {
546 {"tile1", PICO9918_SUPPRESS_TILE1, sceneLockedGm2},
547 {"gm2-colour", PICO9918_SUPPRESS_GM2_COLOUR, sceneLockedGm2},
548 {"gm2-pattern", PICO9918_SUPPRESS_GM2_PATTERN, sceneLockedGm2},
549 {"blanking", PICO9918_SUPPRESS_BLANKING, sceneBlanked },
550 {"tile2", PICO9918_SUPPRESS_TILE2, sceneTile2Only},
551 {"bitmap", PICO9918_SUPPRESS_BITMAP, sceneBmlOnly },
552 {"sprites", PICO9918_SUPPRESS_SPRITES, sceneSprites },
553 };
554 uint32_t gm2[3];
555 uint8_t spriteStatus[2];
556
557 for (unsigned i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i)
558 {
559 const uint32_t plain = suppressDigest(0, cases[i].scene, 20, NULL);
560
561 if (suppressDigest(0, cases[i].scene, 20, NULL) != plain)
562 fail("suppress-scene-unstable", plain, suppressDigest(0, cases[i].scene, 20, NULL));
563 if (suppressDigest(cases[i].bit, cases[i].scene, 20, NULL) == plain)
564 fail(cases[i].name, 0, plain);
565 }
566
567 /* the Graphics II pair share an emitter, so only this would notice them transposed */
568 gm2[0] = suppressDigest(0, sceneLockedGm2, 20, NULL);
569 gm2[1] = suppressDigest(PICO9918_SUPPRESS_GM2_COLOUR, sceneLockedGm2, 20, NULL);
570 gm2[2] = suppressDigest(PICO9918_SUPPRESS_GM2_PATTERN, sceneLockedGm2, 20, NULL);
571 if (gm2[1] == gm2[2]) fail("suppress-gm2-transposed", gm2[1], gm2[2]);
572
573 /* blanking suppressed has to give back the picture the display bit would have */
574 if (suppressDigest(PICO9918_SUPPRESS_BLANKING, sceneBlanked, 20, NULL) != gm2[0])
575 fail("suppress-blanking-differs", gm2[0],
576 suppressDigest(PICO9918_SUPPRESS_BLANKING, sceneBlanked, 20, NULL));
577
578 /* the sprite layer is the one whose suppression must not reach the status file */
579 suppressDigest(0, sceneSprites, 20, &spriteStatus[0]);
580 suppressDigest(PICO9918_SUPPRESS_SPRITES, sceneSprites, 20, &spriteStatus[1]);
581
582 if ((spriteStatus[0] & (PICO9918_SR0_COLLISION | PICO9918_SR0_5S)) !=
584 fail("suppress-scene-no-status", PICO9918_SR0_COLLISION | PICO9918_SR0_5S, spriteStatus[0]);
585 if (spriteStatus[0] != spriteStatus[1])
586 fail("suppress-sprites-moved-status", spriteStatus[0], spriteStatus[1]);
587
588 pico9918_debug_set_suppress(PICO9918_INST 0x80000000u);
589 if (pico9918_debug_suppress(PICO9918_INST_ONLY) != 0x80000000u)
590 fail("suppress-unknown-bit", 0x80000000u, pico9918_debug_suppress(PICO9918_INST_ONLY));
591
592 pico9918_debug_set_suppress(PICO9918_INST 0);
593 }
594#endif
595
596 /* 16. the status write is the physical store too, but SR0 is latched in two places and
597 SR0/SR1 decide /INT, so "physical store" is three postconditions rather than one.
598 The last case is section 14's diff again, on the register that owes nothing. */
599 {
600 pico9918_debug_reg_write(PICO9918_INST TMS_REG_0, 0);
601 pico9918_debug_reg_write(PICO9918_INST TMS_REG_1, TMS_R1_DISP_ACTIVE);
602 pico9918_debug_status_write(PICO9918_INST PICO9918_SR_STATUS, 0);
603
604 for (uint8_t sr = 0; sr < 16; ++sr)
605 {
606 if (!pico9918_debug_status_write(PICO9918_INST sr, (uint8_t)(0xa0 + sr)))
607 fail("status-write-refused", sr, 0);
608 }
609 for (uint8_t sr = 0; sr < 16; ++sr)
610 {
611 if (pico9918_status_value(PICO9918_INST sr) != (uint8_t)(0xa0 + sr))
612 fail("status-write-value", (uint8_t)(0xa0 + sr), pico9918_status_value(PICO9918_INST sr));
613 }
614
615 /* above the file: nothing happens and it says so. SR15 is the neighbour it would
616 wrap onto, the number being masked everywhere else in this API. */
617 if (pico9918_debug_status_write(PICO9918_INST 16, 0x11)) fail("status-write-16", 0, 1);
618 if (pico9918_status_value(PICO9918_INST 15) != 0xaf)
619 fail("status-write-16-wrapped", 0xaf, pico9918_status_value(PICO9918_INST 15));
620
621 /* SR0'S SHADOW. The frame path merges into it and publishes the result, so a write
622 that moved only the published byte survives until the guest's next read and is
623 then replaced by the value that was there before the edit. */
624 pico9918_debug_reg_write(PICO9918_INST PICO9918_REG_STATUS_SELECT, 0);
625 tms9918->frameStatus = 0x1f;
626 pico9918_debug_status_write(PICO9918_INST PICO9918_SR_STATUS, PICO9918_SR0_COLLISION);
627 if (tms9918->frameStatus != PICO9918_SR0_COLLISION)
628 fail("status-write-shadow", PICO9918_SR0_COLLISION, tms9918->frameStatus);
630 fail("status-write-guest-read", PICO9918_SR0_COLLISION, pico9918_read_status(PICO9918_INST_ONLY));
632 fail("status-write-shadow-stale", 0, pico9918_status_value(PICO9918_INST PICO9918_SR_STATUS));
633
634 /* /INT follows from this side as well, which is the same predicate section 13 drives
635 from the register side. Both of its status terms, since either alone decides it. */
636 pico9918_debug_reg_write(PICO9918_INST TMS_REG_1, TMS_R1_DISP_ACTIVE | TMS_R1_INT_ENABLE);
637 pico9918_debug_status_write(PICO9918_INST PICO9918_SR_STATUS, PICO9918_SR0_INT);
638 if (!tms9918->frameInt) fail("status-write-int-not-raised", 1, 0);
639 pico9918_debug_status_write(PICO9918_INST PICO9918_SR_STATUS, 0);
640 if (tms9918->frameInt) fail("status-write-int-not-dropped", 0, 1);
641
642 pico9918_debug_reg_write(PICO9918_INST TMS_REG_0, TMS_R0_INT_SCANLINE);
643 pico9918_debug_status_write(PICO9918_INST PICO9918_SR_IDENT, PICO9918_SR1_HF);
644 if (!tms9918->frameInt) fail("status-write-hf-not-raised", 1, 0);
645 pico9918_debug_status_write(PICO9918_INST PICO9918_SR_IDENT, 0);
646 if (tms9918->frameInt) fail("status-write-hf-not-dropped", 0, 1);
647 pico9918_debug_reg_write(PICO9918_INST TMS_REG_0, 0);
648
649 /* and nothing else moves: one byte for a status register that is neither of those */
650 {
651 uint8_t* const before = malloc(sizeof(pico9918_t));
652 const uint8_t* const live = (const uint8_t*)tms9918;
653 const uint32_t statusByte = 0xB000 + PICO9918_SR_SECONDS_LSB;
654 unsigned moved = 0;
655
656 if (!before) return fail("status-snapshot-alloc", 1, 0), 1;
657
658 pico9918_debug_status_write(PICO9918_INST PICO9918_SR_SECONDS_LSB, 0x00);
659 memcpy(before, tms9918, sizeof(pico9918_t));
660
661 pico9918_debug_status_write(PICO9918_INST PICO9918_SR_SECONDS_LSB, 0x71);
662
663 for (uint32_t i = 0; i < (uint32_t)sizeof(pico9918_t); ++i)
664 {
665 if (before[i] == live[i]) continue;
666 ++moved;
667 if (i != statusByte) fail("status-write-touched-offset", statusByte, i);
668 }
669 if (moved != 1) fail("status-write-moved-count", 1, moved);
670
671 free(before);
672 }
673 }
674
675 printf("%s: debugger surface, %d failure(s)\n", failures ? "FAIL" : "PASS", failures);
676 return failures ? 1 : 0;
677}
uint16_t pico9918_gpu_pc(pico9918_t *tms9918)
see the header.
Definition gpu.c:540
uint32_t pico9918_gpu_mem_size(void)
see the header.
Definition gpu.c:560
uint8_t pico9918_gpu_mem_value(pico9918_t *tms9918, uint32_t addr)
see the header.
Definition gpu.c:567
pico9918_mode_t pico9918_display_mode(pico9918_t *tms9918)
current display mode
Definition pico9918.c:3587
const uint8_t * pico9918_line_source(pico9918_t *tms9918)
where the scanline just generated actually is.
Definition pico9918.c:3617
uint8_t pico9918_status_value(pico9918_t *tms9918, pico9918_status_register_t reg)
return a status register value without the side effects of reading it
Definition pico9918.c:3427
uint32_t pico9918_line_bytes(pico9918_t *tms9918)
how many bytes of pixels[] this mode fills.
Definition pico9918.c:3606
uint8_t pico9918_scan_line(pico9918_t *tms9918, uint16_t y)
generate a scanline
Definition pico9918.c:3335
void pico9918_reset(pico9918_t *tms9918)
reset the new TMS9918
Definition pico9918.c:378
uint8_t pico9918_read_status(pico9918_t *tms9918)
read from the status register
Definition pico9918.c:439
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:3420
#define PICO9918_R49_TILE2_ENABLE
register 49 bits: tile layer 2, row count, and the enhanced colour modes
Definition pico9918.h:356
#define PICO9918_SR0_5S
more sprites on a line than the limit allows
Definition pico9918.h:290
#define TMS_R1_MODE_TEXT
40-column text
Definition pico9918.h:320
#define PICO9918_R56_GPU_RUN
register 56 bit: the GPU trigger
Definition pico9918.h:379
#define TMS_R0_MODE_TEXT_80
80-column text, with R1's text mode.
Definition pico9918.h:304
#define PICO9918_INST_ONLY
pass the instance as the only argument
Definition pico9918.h:86
#define PICO9918_R50_TILE1_OFF
stop drawing tile layer 1
Definition pico9918.h:372
#define PICO9918_R50_RESET
register 50 bits: GPU triggers and the remaining layer controls
Definition pico9918.h:369
#define TMS_R1_DISP_ACTIVE
render the active display
Definition pico9918.h:314
#define PICO9918_SR0_INT
status register 0 bits.
Definition pico9918.h:289
pico9918_register_t
the eight TMS9918 registers, by number and by what each one holds
Definition pico9918.h:212
@ PICO9918_REG_BML_BASE
bitmap layer base address, in 64-byte units
Definition pico9918.h:243
@ PICO9918_REG_NAME_TABLE2
tile layer 2 name table base
Definition pico9918.h:231
@ PICO9918_REG_ENHANCED1
tile layer 2, 30-row mode, ECM levels, real Y
Definition pico9918.h:250
@ PICO9918_REG_GPU_PC_MSB
GPU program counter, high byte.
Definition pico9918.h:253
@ PICO9918_REG_STATUS_SELECT
which status register S1 reads back, and the counter controls
Definition pico9918.h:233
@ PICO9918_REG_ENHANCED2
GPU triggers, per-position attributes, layer priority.
Definition pico9918.h:251
@ PICO9918_REG_UNLOCK
0x1c twice unlocks the F18A personality; any other value locks
Definition pico9918.h:256
@ PICO9918_REG_BML_WIDTH
bitmap layer width in pixels
Definition pico9918.h:246
@ PICO9918_REG_MAX_SCAN_SPRITES
sprites drawn per scanline before the limit bites
Definition pico9918.h:241
@ PICO9918_REG_GPU_PC_LSB
GPU program counter, low byte - writing it also starts the GPU.
Definition pico9918.h:254
@ PICO9918_REG_BML_TOP_ROW
bitmap layer top row
Definition pico9918.h:245
@ PICO9918_REG_BML_CONTROL
bitmap layer enable, priority, transparency, fat pixels
Definition pico9918.h:242
@ PICO9918_REG_GPU_CONTROL
GPU load and trigger.
Definition pico9918.h:255
@ PICO9918_REG_FLASH_CONTROL
PICO9918 only: flash operation control.
Definition pico9918.h:259
@ PICO9918_REG_BML_HEIGHT
bitmap layer height in rows
Definition pico9918.h:247
@ PICO9918_SR_STATUS
the TMS9918A status: interrupt, 5th sprite, collision, sprite number
Definition pico9918.h:270
@ PICO9918_SR_SECONDS_LSB
second counter, low byte
Definition pico9918.h:280
@ PICO9918_SR_IDENT
chip identity, blanking, and the scanline interrupt flag
Definition pico9918.h:271
#define TMS_R0_INT_SCANLINE
assert /INT when the raster reaches the line in R19.
Definition pico9918.h:308
#define PICO9918_SR1_HF
status register 1 bits.
Definition pico9918.h:295
#define PICO9918_R31_BML_ENABLE
register 31 bits: the bitmap layer
Definition pico9918.h:344
#define TMS_R1_INT_ENABLE
assert /INT at end of frame
Definition pico9918.h:315
#define PICO9918_SR0_COLLISION
two sprites overlapped on an opaque pixel
Definition pico9918.h:291
#define PICO9918_INST
pass the instance ahead of other arguments
Definition pico9918.h:85
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