pico9918-core 1.3.0
TMS9918A / F18A video display processor emulation in C99
Loading...
Searching...
No Matches
shim.c
Go to the documentation of this file.
1/**
2 * \file
3 * \brief the live harness's desktop backend
4 *
5 * Project: pico9918
6 *
7 * Copyright (c) 2026 Troy Schrapel
8 *
9 * This code is licensed under the MIT license
10 *
11 * https://github.com/visrealm/pico9918
12 *
13 * `test/live/` drives a board over SWD: it writes the scene into the instance's
14 * memory, waits, and reads back the rows the renderer produced. Every one of those
15 * three is memory access plus a render, and none of them is device-specific - so
16 * this speaks the same three operations over a pipe against an in-process library.
17 *
18 * What that buys is the correctness half of the suite without a board: 111 scenes
19 * and their frozen references, on a desktop and in CI. What it cannot buy is the
20 * other half. The device measures microseconds and which lines did not fit, and
21 * neither has any meaning here. There is no timing hook in this build at all - see
22 * liveDesktopOps.h - so the numbers cannot be produced by accident.
23 *
24 * THE ROWS ARE THE SAME ARTIFACT, and structurally rather than by agreement: the
25 * capture arrives through PICO9918_LINE_CAPTURE, the hook the firmware's own harness
26 * uses, at the same call site inside pico9918_frame_scanline, from the same pointer.
27 * This file renders through the frame path the firmware renders through; it does
28 * not reimplement it.
29 *
30 * Protocol: one command a line on stdin, a reply a line on stdout, and payloads as
31 * raw bytes rather than hex - a full VRAM write is 16 KB and a capture 240 KB.
32 *
33 * \verbatim
34 * off field offsets, one "name value" a line, then "end"
35 * palette the library's default palette, 64 little-endian uint16
36 * w <hexaddr> <len> write <len> raw bytes that follow, instance-relative
37 * r <hexaddr> <len> read: "data <len>" then <len> raw bytes
38 * frames <n> render n frames, discarding them
39 * capture render one frame: "capture <rows> <width>" then the rows
40 * gpu <hexaddr> start a GPU program at <hexaddr> on its own thread, with
41 * the raster running under it for as long as it does
42 * gpupoll "gpu running", or "gpu done <microseconds>"
43 * gpustop clear the GPU's run flag, which stops it where it is
44 * quit
45 * \endverbatim
46 *
47 * Addresses are instance-relative, so the harness needs no notion of where the
48 * instance lives - which is also what keeps the two backends' address arithmetic
49 * identical.
50 */
51
52#include "impl/pico9918_priv.h"
53
54#include "gpu/gpu.h"
55#include "pico9918.h"
56#include "pico9918_frame.h"
57
58#include <stddef.h>
59#include <stdio.h>
60#include <stdlib.h>
61#include <string.h>
62
63#ifdef _WIN32
64#include <fcntl.h>
65#include <io.h>
66#include <windows.h>
67#else
68#include <pthread.h>
69#include <time.h>
70#endif
71
72/* The host's VGA geometry, and it has to be the host's: vBorder is derived from it,
73 and vBorder is what decides which display row a capture row index means. 640x480
74 at hPixelScale 1 is what the shipping VGA mode gives; the vertical pair is
75 in/out and the library rewrites it on a progressive build, exactly as it does for
76 the firmware in renderer.c. */
77#define H_VIRTUAL_PIXELS 640
78#define V_DISPLAY_LINES 480
79#define FRAME_RATE_HZ 60.0f
80#define CORE_TEMP_C 30.0f
81
82/* Frames of grace the library gives an un-driven display before the splash gives
83 way to the diagnostics screen. A device has been running for thousands of frames
84 by the time a harness attaches, so a capture from frame 0 would be a capture of
85 the splash. Started past it deliberately rather than by rendering 600 frames of
86 artwork nobody reads. */
87#define FRAMES_AT_START 1000
88
89/* the widest line any tier renders, times the tallest frame any mode asks for */
90#define CAPTURE_BYTES (512 * 512)
91
92/* 640 pixels, plus room for the guard words the border fill writes past the picture.
93 PICO9918_PIXEL_T is 32-bit on desktop and 16-bit on the device, so the fill's word
94 count covers half the line here - which is why nothing in this file reads pixels.
95 The captured rows are palette INDICES, upstream of every pixel-path difference
96 between the two platforms, including the two known desktop pixel defects. */
97static PICO9918_PIXEL_T pixels[1024];
98
99static uint8_t capture[CAPTURE_BYTES];
100static uint32_t rows = 0;
101static uint32_t width = 0;
102static uint32_t missed = 0;
103
104static int vPixelScale = 1;
105static int vVirtualPixels = V_DISPLAY_LINES;
106static uint32_t triggerLine = V_DISPLAY_LINES;
107/* the vertical scale the frame in the buffer ran under, which is not necessarily the
108 one the next frame will - see buildView */
109static int viewScale = 2;
110
111/* Every frame lands here, not only the ones a `capture` asks for - the window wants
112 all of them, and a `capture` wants the last one, so one buffer serves both. Which
113 is also why there is no "am I capturing" flag to get wrong: the bytes the harness
114 compares are whatever the frame it asked for produced. */
115void liveDesktopCaptureRow(uint16_t y, uint16_t height, uint16_t width_, const uint8_t* indices)
116{
117 rows = height;
118 width = width_;
119 /* A row past the buffer is counted rather than clamped silently: a mode taller or
120 wider than this file expects has to be visible as a failure, not as a short
121 capture that reads like a dropped line. */
122 if ((uint32_t)y * width_ + width_ > CAPTURE_BYTES)
123 {
124 ++missed;
125 return;
126 }
127 memcpy(capture + (uint32_t)y * width_, indices, width_);
128}
129
130/* One renderer at a time. A board has core 1 and only core 1 drawing; here the raster
131 thread further down and whatever command is in flight both call in, and they share
132 the capture buffer, the frame geometry and the vertical scale that goes with it. */
133#ifdef _WIN32
134static SRWLOCK renderLock = SRWLOCK_INIT;
135#define RENDER_LOCK() AcquireSRWLockExclusive(&renderLock)
136#define RENDER_UNLOCK() ReleaseSRWLockExclusive(&renderLock)
137#else
138static pthread_mutex_t renderLock = PTHREAD_MUTEX_INITIALIZER;
139#define RENDER_LOCK() pthread_mutex_lock(&renderLock)
140#define RENDER_UNLOCK() pthread_mutex_unlock(&renderLock)
141#endif
142
143/* One frame, in the order the firmware's VGA layer calls it: every visible line, the
144 end-of-scanline trigger on the line the geometry named, the porch, then the end of
145 frame. The geometry a frame runs under is the PREVIOUS frame's, which is not a
146 simplification - pico9918_frame_end is the only thing that recomputes it on the
147 device too, and the host arms the trigger register from its return value.
148
149 Take the lock over a whole sequence, not a frame at a time, wherever the frames
150 have to belong to each other - a capture is two of them and the counters between. */
151static void renderFrameUnlocked(void)
152{
153 viewScale = vPixelScale;
154 pico9918_scanline_params_t params = {H_VIRTUAL_PIXELS, (uint16_t)vVirtualPixels, false, 0};
155 for (int y = 0; y < vVirtualPixels; ++y)
156 {
157 pico9918_frame_scanline((uint16_t)y, &params, pixels);
158 if ((uint32_t)y == triggerLine) pico9918_frame_end_of_scanline();
159 }
161
162 pico9918_frame_display_t display = {V_DISPLAY_LINES, false, vPixelScale, vVirtualPixels};
163 pico9918_frame_geometry_t geom = pico9918_frame_end(CORE_TEMP_C, FRAME_RATE_HZ, &display);
164 vPixelScale = display.vPixelScale;
165 vVirtualPixels = display.vVirtualPixels;
166 triggerLine = geom.triggerScanline;
167}
168
169static void renderFrame(void)
170{
171 RENDER_LOCK();
172 renderFrameUnlocked();
173 RENDER_UNLOCK();
174}
175
176/* The frame as a picture, for the viewer: 512 columns by 384 or 480 lines, which is
177 what the glass shows however many bytes carried it.
178
179 Expanded here rather than in the viewer for one reason - it is free here. The
180 palette is already in PRAM beside the indices, and a viewer at 60 frames a second
181 would otherwise spend its whole budget turning 200,000 indices into RGB. What
182 crosses the pipe is a finished P6 PPM, which Tk loads from raw bytes directly.
183
184 The three geometries are the same three the harness's own PNG writer resolves: a
185 512-byte line is one index a pixel, a 256-byte line in 80-column text is two
186 four-bit indices a byte, and every other 256-byte line is one index doubled
187 across. Vertically every row is two lines unless R0 is doubling rows. */
188static uint8_t view[32 + 512 * 480 * 3];
189
190static size_t buildView(void)
191{
192 uint8_t rgb[64][3];
193 for (int i = 0; i < 64; ++i)
194 {
195 const uint16_t v = __builtin_bswap16(tms9918->vram.map.pram[i]); /* 0xFRGB */
196 rgb[i][0] = (uint8_t)(((v >> 8) & 0xF) * 0x11);
197 rgb[i][1] = (uint8_t)(((v >> 4) & 0xF) * 0x11);
198 rgb[i][2] = (uint8_t)((v & 0xF) * 0x11);
199 }
200
201 /* The height comes from the frame, NOT from R0's row-doubling bit. The two
202 disagree for exactly one frame after a mode change - the registers are current
203 while the geometry is still the previous frame's - and a 60-row scene giving way
204 to a 24-row one then asks for 960 lines. `viewScale` is the scale the frame
205 actually ran under, so this cannot skew. The 80-column bit is read live and is
206 right to be: the renderer picks the mode up within the frame, unlike the
207 geometry. */
208 const int packed = width == 256 && (TMS_REGISTER(tms9918, 0x00) & 0x04);
209 const int lines = (int)rows * viewScale;
210 if (!rows || lines > 480) return 0;
211
212 int at = snprintf((char*)view, 32, "P6\n512 %d\n255\n", lines);
213 for (int line = 0; line < lines; ++line)
214 {
215 const uint8_t* row = capture + (size_t)(line / viewScale) * width;
216 for (int x = 0; x < 512; ++x)
217 {
218 uint8_t index;
219 if (width == 512)
220 index = row[x];
221 else if (packed)
222 index = (x & 1) ? (row[x >> 1] & 0x0F) : (uint8_t)(row[x >> 1] >> 4);
223 else
224 index = row[x >> 1];
225 const uint8_t* c = rgb[index & 0x3F];
226 view[at++] = c[0];
227 view[at++] = c[1];
228 view[at++] = c[2];
229 }
230 }
231 return (size_t)at;
232}
233
234/* -------------------------------------------------------------------------
235 * The GPU, on a thread of its own.
236 *
237 * Not for speed - it is because that is the shape of the machine. On a board core
238 * 0 runs the GPU program while core 1 renders, so the drawing appears a frame at a
239 * time as the program works. Run inline here, a program would render nothing until
240 * it finished, and being able to watch one draw is most of why a harness would run
241 * a program that takes twenty-three million instructions.
242 *
243 * A second thread renders under it, which is core 1's half of the same shape, and it
244 * is not optional: a program can WAIT on the raster, and one that does gets nothing
245 * back from a host whose raster only moves when a command asks for a frame.
246 *
247 * The threads share VRAM with no lock, and that is the fidelity rather than an
248 * oversight: on the device they share it across two cores with no lock either.
249 * Every access is a byte, so a reader sees the old value or the new one, and a
250 * half-drawn frame is the truth about a half-drawn picture. Rendering is the one
251 * thing that IS locked, and only against this process's own second renderer - see
252 * renderFrame.
253 * ---------------------------------------------------------------------- */
254static volatile int gpuBusy = 0;
255
256typedef struct
257{
258#ifdef _WIN32
259 HANDLE handle;
260#else
261 pthread_t id;
262#endif
263 int live;
264} thread_t;
265
266#ifdef _WIN32
267#define THREAD_BODY(name) static DWORD WINAPI name(LPVOID unused)
268#define THREAD_DONE return 0
269typedef LPTHREAD_START_ROUTINE thread_body_t;
270
271static int threadStart(thread_t* t, thread_body_t body)
272{
273 t->handle = CreateThread(NULL, 0, body, NULL, 0, NULL);
274 t->live = t->handle != NULL;
275 return t->live;
276}
277
278static void threadJoin(thread_t* t)
279{
280 if (!t->live) return;
281 WaitForSingleObject(t->handle, INFINITE);
282 CloseHandle(t->handle);
283 t->live = 0;
284}
285#else
286#define THREAD_BODY(name) static void* name(void* unused)
287#define THREAD_DONE return NULL
288typedef void* (*thread_body_t)(void*);
289
290static int threadStart(thread_t* t, thread_body_t body)
291{
292 t->live = pthread_create(&t->id, NULL, body, NULL) == 0;
293 return t->live;
294}
295
296static void threadJoin(thread_t* t)
297{
298 if (!t->live) return;
299 pthread_join(t->id, NULL);
300 t->live = 0;
301}
302#endif
303
304static thread_t gpuThread;
305static thread_t rasterThread;
306
307THREAD_BODY(gpuBody)
308{
309 (void)unused;
311 gpuBusy = 0;
312 THREAD_DONE;
313}
314
315/* The raster, for as long as a program runs.
316 *
317 * A program that waits on the scanline register at >7000 - to page a bitmap in the
318 * vertical blank, say - only ever sees it move if something is rendering. A board
319 * always has core 1 doing that; nothing here did, so such a program waited forever
320 * and the harness called it a timeout.
321 *
322 * Flat out rather than at 60Hz, which is the one place this deliberately parts with
323 * the device. A board's raster is paced by its display, so a program that waits on it
324 * waits in real time. A harness has no display to wait for, and pacing this would
325 * charge a program's wall clock to the frames it waits through rather than to the
326 * work it does. */
327THREAD_BODY(rasterBody)
328{
329 (void)unused;
330 while (gpuBusy) renderFrame();
331 THREAD_DONE;
332}
333
334static void gpuReap(void)
335{
336 threadJoin(&gpuThread);
337 threadJoin(&rasterThread);
338}
339
340static void gpuStart(uint16_t addr)
341{
342 gpuReap(); /* whatever ran last, before its thread handles are overwritten */
343 tms9918->gpuAddress = addr;
344 tms9918->restart = 1;
346 gpuBusy = 1;
347 if (threadStart(&gpuThread, gpuBody))
348 {
349 /* after the GPU thread, so a failure to spawn it cannot leave this one spinning
350 on a gpuBusy nothing will ever clear */
351 threadStart(&rasterThread, rasterBody);
352 }
353 else
354 {
355 /* no thread to be had: the program still runs, it just cannot be watched, and a
356 program that waits on the raster will not come back */
358 gpuBusy = 0;
359 }
360}
361
362static uint8_t* base(void)
363{
364 return (uint8_t*)tms9918;
365}
366
367static void reply(const char* text)
368{
369 fputs(text, stdout);
370 fputc('\n', stdout);
371 fflush(stdout);
372}
373
374static int readExactly(void* into, size_t n)
375{
376 return fread(into, 1, n, stdin) == n;
377}
378
379static void writeExactly(const void* from, size_t n)
380{
381 fwrite(from, 1, n, stdout);
382 fflush(stdout);
383}
384
385int main(void)
386{
387#ifdef _WIN32
388 /* payloads are raw bytes, and text mode would translate 0x0a in a palette index */
389 _setmode(_fileno(stdin), _O_BINARY);
390 _setmode(_fileno(stdout), _O_BINARY);
391#endif
392
393 pico9918_init();
395 for (int i = 0; i < FRAMES_AT_START; ++i) renderFrame();
396
397 char line[256];
398 while (fgets(line, sizeof(line), stdin))
399 {
400 char command[32] = {0};
401 int consumed = 0;
402 if (sscanf(line, "%31s%n", command, &consumed) < 1) continue;
403
404 /* the address is hex and the length decimal, so each is parsed where it is used
405 rather than through one format string that has to guess the base */
406 char* rest = line + consumed;
407 unsigned long addr = strtoul(rest, &rest, 16);
408 unsigned long count = strtoul(rest, &rest, 10);
409
410 if (!strcmp(command, "quit")) break;
411
412 if (!strcmp(command, "off"))
413 {
414 printf("instance %u\n", (unsigned)sizeof(*tms9918));
415 printf("vram %u\n", (unsigned)offsetof(struct pico9918_s, vram));
416 printf("config %u\n", (unsigned)offsetof(struct pico9918_s, config));
417 printf("isUnlocked %u\n", (unsigned)offsetof(struct pico9918_s, isUnlocked));
418 printf("lockedMask %u\n", (unsigned)offsetof(struct pico9918_s, lockedMask));
419 printf("palDirty %u\n", (unsigned)offsetof(struct pico9918_s, palDirty));
420 printf("configDirty %u\n", (unsigned)offsetof(struct pico9918_s, configDirty));
421 reply("end");
422 }
423 else if (!strcmp(command, "palette"))
424 {
425 uint16_t entries[64];
426 for (int i = 0; i < 64; ++i) entries[i] = pico9918_default_palette(i);
427 printf("data %u\n", (unsigned)sizeof(entries));
428 fflush(stdout);
429 writeExactly(entries, sizeof(entries));
430 }
431 else if (!strcmp(command, "w"))
432 {
433 if (addr + count > sizeof(*tms9918))
434 {
435 reply("error out of range");
436 continue;
437 }
438 if (!readExactly(base() + addr, count)) return 1;
439 reply("ok");
440 }
441 else if (!strcmp(command, "r"))
442 {
443 if (addr + count > sizeof(*tms9918))
444 {
445 reply("error out of range");
446 continue;
447 }
448 printf("data %lu\n", count);
449 fflush(stdout);
450 writeExactly(base() + addr, count);
451 }
452 else if (!strcmp(command, "frames"))
453 {
454 /* its one argument is a count, so it is decimal - `addr` above is not it */
455 unsigned long n = strtoul(line + consumed, NULL, 10);
456 for (unsigned long i = 0; i < (n ? n : 1); ++i) renderFrame();
457 reply("ok");
458 }
459 else if (!strcmp(command, "view"))
460 {
461 /* one frame, as a picture. The viewer paces itself: the render is well under a
462 millisecond, so where 60 frames a second is decided is the side with a clock
463 that can wait, not this one. */
464 RENDER_LOCK();
465 renderFrameUnlocked();
466 size_t n = buildView(); /* reads the buffer that frame just filled */
467 RENDER_UNLOCK();
468 if (!n)
469 {
470 reply("error nothing rendered yet");
471 continue;
472 }
473 printf("view %u\n", (unsigned)n);
474 fflush(stdout);
475 writeExactly(view, n);
476 }
477 else if (!strcmp(command, "gpu"))
478 {
479 gpuStart((uint16_t)addr);
480 reply("ok");
481 }
482 else if (!strcmp(command, "gpupoll"))
483 {
484 /* The microseconds are the library's own accumulator - the one the diagnostics
485 overlay reports from on a board - not a stopwatch held out here. */
486 if (gpuBusy)
487 reply("gpu running");
488 else
489 {
490 gpuReap();
491 printf("gpu done %lu\n", (unsigned long)pico9918_gpu_time(0));
492 fflush(stdout);
493 }
494 }
495 else if (!strcmp(command, "gpustop"))
496 {
497 /* The same switch the device's harness uses and the same one a program uses to
498 finish: run9900 tests this byte every instruction. */
499 TMS_REGISTER(tms9918, 0x38) = 0;
500 reply("ok");
501 }
502 else if (!strcmp(command, "capture"))
503 {
504 /* One frame first, discarded. The geometry a frame runs under is the previous
505 frame's - pico9918_frame_end is what recomputes it - so a scene that changed
506 the row count still renders at the old height for one more frame. The device
507 gets this for free: its capture arms at the next frame boundary, so a whole
508 frame always separates the scene from the picture. Without it twelve scenes
509 come back at the PREVIOUS scene's height. */
510 RENDER_LOCK();
511 renderFrameUnlocked();
512
513 rows = width = missed = 0;
514 renderFrameUnlocked();
515 RENDER_UNLOCK();
516 if (missed)
517 {
518 reply("error capture overflowed");
519 continue;
520 }
521 printf("capture %lu %lu\n", (unsigned long)rows, (unsigned long)width);
522 fflush(stdout);
523 writeExactly(capture, (size_t)rows * width);
524 }
525 else
526 {
527 reply("error unknown command");
528 }
529 }
530 return 0;
531}
void pico9918_gpu_reset_time(void)
Reset the internal GPU time accumulator to 0.
Definition gpu.c:421
void pico9918_gpu_init(pico9918_t *tms9918)
Initialize the TMS9900 GPU.
Definition gpu.c:388
void pico9918_gpu_step(pico9918_t *tms9918)
One pass of that loop: run a pending trigger to completion, then dispatch any flash and config-action...
Definition gpu.c:430
uint32_t pico9918_gpu_time(uint32_t totalTime)
Return the GPU's CPU time in microseconds.
Definition gpu.c:412
pico9918-core - GPU Interface
uint16_t pico9918_default_palette(int index)
a default palette entry, 0xargb
Definition pico9918.c:3536
pico9918-core - core interface
void pico9918_frame_porch(pico9918_t *tms9918)
see the header.
bool pico9918_frame_scanline(pico9918_t *tms9918, uint16_t y, const pico9918_scanline_params_t *params, PICO9918_PIXEL_T *pixels)
see the header.
pico9918_frame_geometry_t pico9918_frame_end(pico9918_t *tms9918, float tempC, float frameRateHz, pico9918_frame_display_t *display)
see the header.
void pico9918_frame_end_of_scanline(pico9918_t *tms9918)
see the header.
pico9918-core - frame module
pico9918-core - the private instance layout
the host's mutable vertical display parameters, as the end-of-frame geometry sees them
uint16_t vVirtualPixels
(in, and out when yScale > 1)
uint8_t vPixelScale
(in, and out when yScale > 1)
the vertical geometry the end of frame derives
uint32_t triggerScanline
vBorder + vPixels
the host's per-call display parameters, as the scanline sees them