pico9918-core 1.3.0
TMS9918A / F18A video display processor emulation in C99
Loading...
Searching...
No Matches
gpu_test.c
Go to the documentation of this file.
1/**
2 * \file
3 * \brief pico9918-core - the library-paced GPU
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 * pico9918_gpu_set_clock's contract, which nothing else covers: a rate makes the library
12 * run an armed program from inside the arming write, and zero leaves the GPU to the host.
13 *
14 * The first half is what software detecting an F18A depends on. The probe arms a tiny
15 * self-modifying program and reads its result back within a handful of host cycles, so a
16 * host servicing the GPU once a scanline sees the chip only when a scanline boundary
17 * happens to fall in between - a real flaky-detection bug in two emulators.
18 *
19 * The last section is the DMA engine, which is here because a program triggering one is
20 * the only way to reach it: the ports it answers to are above the address space a host
21 * can write.
22 */
23
24#include "impl/pico9918_priv.h"
25#include "gpu/gpu.h"
26
27#include <stdio.h>
28
29#define PROGRAM_AT 0x2000u
30#define RESULT_AT 0x2100u
31#define MARKER 0xbeefu
32
33static int failures;
34
35static void fail(const char* what, unsigned wanted, unsigned got)
36{
37 ++failures;
38 printf(" FAIL %s: want %04x got %04x\n", what, wanted, got);
39}
40
41static void regWrite(uint8_t reg, uint8_t value)
42{
44}
45
46/* Two writes of 0x1c to R57, which is what an F18A answers to. */
47static void unlock(void)
48{
49 regWrite(0x39, 0x1c);
50 regWrite(0x39, 0x1c);
51}
52
53/*
54 * LI R0, >BEEF 0200 BEEF
55 * MOV R0, @>2100 C800 2100
56 * STST R3 02C3 so the status accessor has something to agree with
57 * IDLE 0340
58 *
59 * Written straight into VRAM rather than through the host bus, which masks to 16K.
60 */
61static void loadProgram(void)
62{
63 static const uint8_t program[] = {0x02, 0x00, 0xbe, 0xef, 0xc8, 0x00,
64 0x21, 0x00, 0x02, 0xc3, 0x03, 0x40};
65
66 for (unsigned i = 0; i < sizeof(program); ++i) tms9918->vram.bytes[PROGRAM_AT + i] = program[i];
67
68 tms9918->vram.bytes[RESULT_AT] = 0;
69 tms9918->vram.bytes[RESULT_AT + 1] = 0;
70}
71
72static uint16_t result(void)
73{
74 return (uint16_t)((tms9918->vram.bytes[RESULT_AT] << 8) | tms9918->vram.bytes[RESULT_AT + 1]);
75}
76
77/* Arm at PROGRAM_AT. Writing R55 is what arms it, so R54 goes first. */
78static void arm(void)
79{
80 regWrite(0x36, (uint8_t)(PROGRAM_AT >> 8));
81 regWrite(0x37, (uint8_t)(PROGRAM_AT & 0xff));
82}
83
84/*
85 * LI R1, >8008 0201 8008 the DMA trigger port
86 * LI R2, >0100 0202 0100
87 * MOV R2, *R1 C442 any write to >8008 starts the transfer
88 * IDLE 0340
89 *
90 * The registers below it are set by the case rather than by the program: what is under
91 * test is the engine, not a program's ability to load eight bytes.
92 */
93static void loadDmaProgram(void)
94{
95 static const uint8_t program[] = {0x02, 0x01, 0x80, 0x08, 0x02, 0x02,
96 0x01, 0x00, 0xc4, 0x42, 0x03, 0x40};
97
98 for (unsigned i = 0; i < sizeof(program); ++i) tms9918->vram.bytes[PROGRAM_AT + i] = program[i];
99}
100
101#define DMA_SRC 0x1000u
102#define DMA_DST 0x1800u
103
104/* One transfer, from a source that is 0x40 counting up. Returns with the destination
105 wherever the engine left it. */
106static void dma(uint32_t src, uint32_t dst, uint8_t width, uint8_t height, uint8_t stride,
107 uint8_t params)
108{
109 for (unsigned i = 0; i < 0x400; ++i)
110 {
111 tms9918->vram.bytes[DMA_SRC + i] = (uint8_t)(0x40 + i);
112 tms9918->vram.bytes[DMA_DST + i] = 0;
113 }
114
115 tms9918->vram.bytes[0x8000] = (uint8_t)(src >> 8);
116 tms9918->vram.bytes[0x8001] = (uint8_t)src;
117 tms9918->vram.bytes[0x8002] = (uint8_t)(dst >> 8);
118 tms9918->vram.bytes[0x8003] = (uint8_t)dst;
119 tms9918->vram.bytes[0x8004] = width;
120 tms9918->vram.bytes[0x8005] = height;
121 tms9918->vram.bytes[0x8006] = stride;
122 tms9918->vram.bytes[0x8007] = params;
123
124 loadDmaProgram();
125 arm();
126}
127
128static void expect(const char* what, uint32_t at, uint8_t wanted)
129{
130 if (tms9918->vram.bytes[at] != wanted) fail(what, wanted, tms9918->vram.bytes[at]);
131}
132
133int main(void)
134{
135 pico9918_init();
137
138 /* 1. no rate is the default, and leaves the GPU to whoever else drives it. The
139 program is armed, so a host's own step_n would still run it - but the arming
140 write must not. */
141 unlock();
142 loadProgram();
143 arm();
144 if (result() != 0) fail("no-rate-ran", 0, result());
145
146 /* and armed is where pico9918_gpu_pc says it is, before a single instruction runs */
147 if (pico9918_gpu_pc(PICO9918_INST_ONLY) != PROGRAM_AT)
148 fail("armed-pc", PROGRAM_AT, pico9918_gpu_pc(PICO9918_INST_ONLY));
149
150 /* and a disassembler can reach the program: the guest's own view stops at 0x3fff, so
151 PROGRAM_AT is in range for both, but only the GPU's reaches past it. */
152 if (pico9918_gpu_mem_value(PICO9918_INST PROGRAM_AT) != 0x02)
153 fail("gpu-mem-program", 0x02, pico9918_gpu_mem_value(PICO9918_INST PROGRAM_AT));
154
155 tms9918->vram.bytes[0x4100] = 0x5a;
156 if (pico9918_gpu_mem_value(PICO9918_INST 0x4100) != 0x5a)
157 fail("gpu-mem-gram", 0x5a, pico9918_gpu_mem_value(PICO9918_INST 0x4100));
158 if (pico9918_vram_value(PICO9918_INST 0x4100) == 0x5a) fail("vram-value-reached-gram", 0, 0x5a);
159
160 /* and it stops where the map does rather than walking into the instance */
162 fail("gpu-mem-past-end", 0, pico9918_gpu_mem_value(PICO9918_INST pico9918_gpu_mem_size()));
163
164 /* 2. and it is only armed, not lost: the host-driven path still works. */
166 if (result() != MARKER) fail("host-driven", MARKER, result());
167
168 /* the program left >BEEF in R0, which is the last word of the map, and R1 is above it
169 in the workspace overflow - so reading either through the 16-bit map cannot work */
170 if (pico9918_gpu_reg_value(PICO9918_INST 0) != MARKER)
171 fail("gpu-r0", MARKER, pico9918_gpu_reg_value(PICO9918_INST 0));
172
173 tms9918->vram.map.wrksp[0] = 0x12;
174 tms9918->vram.map.wrksp[1] = 0x34;
175 if (pico9918_gpu_reg_value(PICO9918_INST 1) != 0x1234)
176 fail("gpu-r1", 0x1234, pico9918_gpu_reg_value(PICO9918_INST 1));
177
178 /* the accessor publishes what STST stored, bit for bit - a non-zero check cannot see
179 the two disagreeing by the eight places the low-byte storage sits at */
181 fail("gpu-status-stst", pico9918_gpu_reg_value(PICO9918_INST 3),
183
184 /* and it is what >BEEF earns from a cleared status: logical greater than alone, since
185 arming zeroed it, MOV keeps only C/OV/P across, and >BEEF is negative and non-zero */
188
189 /* and it moved: a run leaves the point it reached, not the address it was armed at */
190 if (pico9918_gpu_pc(PICO9918_INST_ONLY) == PROGRAM_AT)
191 fail("pc-did-not-move", 0, pico9918_gpu_pc(PICO9918_INST_ONLY));
192
193 /* 3. a rate runs it from inside the arming write, which is the whole point */
195 loadProgram();
196 arm();
197 if (result() != MARKER) fail("armed-not-run", MARKER, result());
198
199 /* 4. the other arming route: R56 bit 0. It resumes from gpuAddress rather than
200 restarting, so R55 sets the address with no rate on - arming without running -
201 and the R56 write is what starts it. */
203 loadProgram();
204 arm();
205 if (result() != 0) fail("r56-early-run", 0, result());
206
208 regWrite(0x38, 1);
209 if (result() != MARKER) fail("r56-not-run", MARKER, result());
210
211 /* 5. a locked device has no GPU to arm - a reset re-locks it. The registers that
212 would arm one are above the eight it admits, so the write is ignored outright,
213 and the rate is still set so this is the lock doing it. */
215
216 /* the reset parks an odd address, which is what the header calls "nothing armed" */
217 if ((pico9918_gpu_pc(PICO9918_INST_ONLY) & 1) == 0)
218 fail("reset-pc-even", 1, pico9918_gpu_pc(PICO9918_INST_ONLY));
219
222 loadProgram();
223 arm();
224 if (result() != 0) fail("locked-ran", 0, result());
225
226 /* and the locked arm did not move it: the registers that would are above the eight */
227 if ((pico9918_gpu_pc(PICO9918_INST_ONLY) & 1) == 0)
228 fail("locked-armed-pc", 1, pico9918_gpu_pc(PICO9918_INST_ONLY));
229
230 /* 6. and unlocking again brings it back, so nothing above latched */
231 unlock();
232 loadProgram();
233 arm();
234 if (result() != MARKER) fail("relocked", MARKER, result());
235
236 /* 7. back to zero, back to the host */
238 loadProgram();
239 arm();
240 if (result() != 0) fail("cleared-rate-ran", 0, result());
241
242 /* 8. the DMA engine's geometry. The source is 0x40 counting up, so where a byte landed
243 says which one it was and therefore which row and column the engine thought it
244 was on. Zero width and height mean 256; stride is a different animal entirely. */
245 unlock();
247
248 dma(DMA_SRC, DMA_DST, 4, 3, 4, 0x00);
249 expect("dma-run-first", DMA_DST, 0x40);
250 expect("dma-run-last", DMA_DST + 11, 0x4b);
251 expect("dma-run-past", DMA_DST + 12, 0x00);
252
253 dma(DMA_SRC, DMA_DST, 4, 3, 16, 0x00);
254 expect("dma-stride-row0", DMA_DST, 0x40);
255 expect("dma-stride-gap", DMA_DST + 4, 0x00);
256 expect("dma-stride-row1", DMA_DST + 16, 0x50);
257 expect("dma-stride-row2", DMA_DST + 32, 0x60);
258
259 /* stride zero is a pitch of zero, not of 256: every row lands on the one before it */
260 dma(DMA_SRC, DMA_DST, 4, 3, 0, 0x00);
261 expect("dma-stride0-row0", DMA_DST, 0x40);
262 expect("dma-stride0-end", DMA_DST + 4, 0x00);
263 expect("dma-stride0-not256", DMA_DST + 256, 0x00);
264
265 /* and a stride the eight-bit difference overflows walks backwards: 200 with a width of
266 8 is a pitch of -56, not +200 */
267 dma(0x1100, 0x1900, 8, 2, 200, 0x00);
268 expect("dma-back-row0", 0x1900, 0x40);
269 expect("dma-back-row1", 0x18c8, 0x08);
270 expect("dma-back-not-forward", 0x19c8, 0x00);
271
272 /* either side of where it overflows, which for a width of 8 is a stride of 135 */
273 dma(0x1100, 0x1900, 8, 2, 134, 0x00);
274 expect("dma-edge-fwd-row1", 0x1986, 0xc6);
275 expect("dma-edge-fwd-gap", 0x1985, 0x00);
276
277 dma(0x1100, 0x1900, 8, 2, 135, 0x00);
278 expect("dma-edge-back-row1", 0x1887, 0xc7);
279 expect("dma-edge-back-not-forward", 0x1987, 0x00);
280
281 /* a width of zero is 256, and with stride zero the difference wraps to a pitch of 256 */
282 dma(DMA_SRC, DMA_DST, 0, 1, 0, 0x00);
283 expect("dma-width256-first", DMA_DST, 0x40);
284 expect("dma-width256-last", DMA_DST + 255, 0x3f);
285 expect("dma-width256-past", DMA_DST + 256, 0x00);
286
287 /* and a height of zero is 256 rows of it */
288 dma(DMA_SRC, DMA_DST, 1, 0, 1, 0x00);
289 expect("dma-height256-first", DMA_DST, 0x40);
290 expect("dma-height256-last", DMA_DST + 255, 0x3f);
291 expect("dma-height256-past", DMA_DST + 256, 0x00);
292
293 /* the top of each register: 255 wide by one, then one wide by 255 */
294 dma(DMA_SRC, DMA_DST, 255, 1, 255, 0x00);
295 expect("dma-width255-first", DMA_DST, 0x40);
296 expect("dma-width255-last", DMA_DST + 254, 0x3e);
297 expect("dma-width255-past", DMA_DST + 255, 0x00);
298
299 dma(DMA_SRC, DMA_DST, 1, 255, 1, 0x00);
300 expect("dma-height255-first", DMA_DST, 0x40);
301 expect("dma-height255-last", DMA_DST + 254, 0x3e);
302 expect("dma-height255-past", DMA_DST + 255, 0x00);
303
304 /* a stride under the width steps back into the row just written */
305 dma(DMA_SRC, DMA_DST, 8, 2, 4, 0x00);
306 expect("dma-narrow-kept", DMA_DST + 3, 0x43);
307 expect("dma-narrow-rewritten", DMA_DST + 4, 0x44);
308 expect("dma-narrow-last", DMA_DST + 11, 0x4b);
309 expect("dma-narrow-past", DMA_DST + 12, 0x00);
310
311 /* only two bits of the parameter byte are decoded, so the other six say nothing */
312 dma(DMA_SRC, DMA_DST, 4, 3, 4, 0xfc);
313 expect("dma-params-spare-first", DMA_DST, 0x40);
314 expect("dma-params-spare-last", DMA_DST + 11, 0x4b);
315 expect("dma-params-spare-past", DMA_DST + 12, 0x00);
316
317 /* both parameter bits at once: a fill that decrements */
318 dma(0x1100, 0x1900, 4, 2, 4, 0x03);
319 expect("dma-fill-dec-first", 0x1900, 0x40);
320 expect("dma-fill-dec-row0", 0x18fd, 0x40);
321 expect("dma-fill-dec-row1", 0x18f9, 0x40);
322 expect("dma-fill-dec-past", 0x18f8, 0x00);
323
324 /* LOAD-BEARING: overlapping forwards, so each byte read has already been written. A
325 block copy would answer 40..47 here, which is what makes this the guard on the fast
326 path being taken only where source and destination are disjoint. */
327 dma(DMA_SRC, DMA_SRC + 2, 8, 1, 8, 0x00);
328 expect("dma-overlap-0", DMA_SRC + 2, 0x40);
329 expect("dma-overlap-2", DMA_SRC + 4, 0x40);
330 expect("dma-overlap-7", DMA_SRC + 9, 0x41);
331
332 /* sixteen bits of address, either end */
333 dma(DMA_SRC, 0xfffe, 4, 1, 4, 0x00);
334 expect("dma-dst-wrap-before", 0xffff, 0x41);
335 expect("dma-dst-wrap-after", 0x0000, 0x42);
336 expect("dma-dst-wrap-last", 0x0001, 0x43);
337
338 tms9918->vram.bytes[0xfffe] = 0x11;
339 tms9918->vram.bytes[0xffff] = 0x22;
340 tms9918->vram.bytes[0x0000] = 0x33;
341 tms9918->vram.bytes[0x0001] = 0x44;
342 dma(0xfffe, DMA_DST, 4, 1, 4, 0x00);
343 expect("dma-src-wrap-before", DMA_DST + 1, 0x22);
344 expect("dma-src-wrap-after", DMA_DST + 2, 0x33);
345 expect("dma-src-wrap-last", DMA_DST + 3, 0x44);
346
347 /* a fill reads its byte once and strides like a copy */
348 dma(DMA_SRC, DMA_DST, 4, 3, 16, 0x01);
349 expect("dma-fill-row0", DMA_DST + 3, 0x40);
350 expect("dma-fill-gap", DMA_DST + 4, 0x00);
351 expect("dma-fill-row2", DMA_DST + 32, 0x40);
352
353 /* decrementing runs both ends backwards, so a row ends below the address it started at */
354 dma(0x1100, 0x1900, 4, 2, 4, 0x02);
355 expect("dma-dec-row0-first", 0x1900, 0x40);
356 expect("dma-dec-row0-last", 0x18fd, 0x3d);
357 expect("dma-dec-row1-first", 0x18fc, 0x3c);
358 expect("dma-dec-row1-last", 0x18f9, 0x39);
359 expect("dma-dec-past", 0x1901, 0x00);
360
361 /* decrementing with a stride of zero cancels the row's own backwards run exactly */
362 dma(0x1100, 0x1900, 4, 3, 0, 0x02);
363 expect("dma-dec-stride0-first", 0x1900, 0x40);
364 expect("dma-dec-stride0-last", 0x18fd, 0x3d);
365 expect("dma-dec-stride0-end", 0x18fc, 0x00);
366 expect("dma-dec-stride0-past", 0x1901, 0x00);
367
368 /* TRAP: the difference is (width - 1) - stride here, so an ordinary stride always makes
369 the pitch negative and only one that underflows walks the rows FORWARDS while each row
370 is still written backwards. 200 with a width of 8 gives (7 - 200) & 0xff = 63, so +56 -
371 the mirror of dma-back-row1 above, which is the same numbers incrementing. */
372 dma(0x1100, 0x1900, 8, 2, 200, 0x02);
373 expect("dma-dec-fwd-row0-first", 0x1900, 0x40);
374 expect("dma-dec-fwd-row0-last", 0x18f9, 0x39);
375 expect("dma-dec-fwd-row1-first", 0x1938, 0x78);
376 expect("dma-dec-fwd-row1-last", 0x1931, 0x71);
377 expect("dma-dec-fwd-gap", 0x1901, 0x00);
378 expect("dma-dec-fwd-not-back", 0x18c8, 0x00);
379
380 /* and its own edge, which is NOT where incrementing turns over: two's complement holds
381 one more negative than positive, so dec flips at 136 where inc flipped at 135 */
382 dma(0x1100, 0x1900, 8, 2, 135, 0x02);
383 expect("dma-dec-edge-back-row1", 0x1879, 0xb9);
384 expect("dma-dec-edge-back-not-forward", 0x1979, 0x00);
385
386 dma(0x1100, 0x1900, 8, 2, 136, 0x02);
387 expect("dma-dec-edge-fwd-row1", 0x1978, 0xb8);
388 expect("dma-dec-edge-fwd-not-back", 0x1878, 0x00);
389
390 printf("%s: library-paced GPU, %d failure(s)\n", failures ? "FAIL" : "PASS", failures);
391 return failures != 0;
392}
uint16_t pico9918_gpu_status(pico9918_t *tms9918)
see the header.
Definition gpu.c:496
uint16_t pico9918_gpu_pc(pico9918_t *tms9918)
see the header.
Definition gpu.c:459
void pico9918_gpu_init(pico9918_t *tms9918)
Initialize the TMS9900 GPU.
Definition gpu.c:388
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
uint16_t pico9918_gpu_reg_value(pico9918_t *tms9918, uint8_t reg)
see the header.
Definition gpu.c:486
bool pico9918_gpu_step_n(pico9918_t *tms9918, uint32_t instructions)
The same pass, capped at instructions, returning true while the program still has work left.
Definition gpu.c:509
void pico9918_gpu_set_clock(pico9918_t *tms9918, uint32_t instructionsPerSecond)
Hand GPU execution to the library, at this many instructions a second.
Definition gpu.c:562
pico9918-core - GPU Interface
#define PICO9918_GPU_IPS_PRO
PICO9918 PRO, RP2350 at 352MHz.
Definition gpu.h:83
#define PICO9918_GPU_ST_LGT
ST0, logical greater than.
Definition gpu.h:193
uint8_t pico9918_vram_value(pico9918_t *tms9918, uint16_t addr)
return a value from vram
Definition pico9918.c:3485
void pico9918_reset(pico9918_t *tms9918)
reset the new TMS9918
Definition pico9918.c:318
#define PICO9918_INST_ONLY
pass the instance as the only argument
Definition pico9918.h:74
#define PICO9918_INST
pass the instance ahead of other arguments
Definition pico9918.h:73
pico9918-core - the private instance layout
void pico9918_write_reg_value_impl(pico9918_t *tms9918, uint8_t regSelect, uint8_t value)
set a register from the second byte of a host register write
Definition pico9918.c:3342