pico9918-core 1.3.0
TMS9918A / F18A video display processor emulation in C99
Loading...
Searching...
No Matches
pixel_test.c
Go to the documentation of this file.
1/**
2 * \file
3 * \brief pico9918-core - the post-palette pixel path
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 * Everything between a palette index and a host's framebuffer, which nothing else
12 * covers: the goldens render a 256-byte line and digest it, and the scene suite
13 * compares palette INDEXES. Neither looks at a wide row's colours, and neither looks
14 * at where in the scanline the picture lands.
15 *
16 * Both are load-bearing for a host. The LUT has two layouts - a pixel pair per entry
17 * for every mode that doubles, and a nibble pair for the 4bpp 80-column line - and a
18 * row that picks the wrong one still renders, in the wrong colours. The geometry is
19 * denominated in 32-bit words holding two pixels, so a host that reads the window at
20 * the wrong offset gets the border fill through the middle of its picture.
21 */
22
23#include "impl/pico9918_priv.h"
24#include "pico9918_frame.h"
25
26#include <stdio.h>
27#include <string.h>
28
29#define H_VIRTUAL 640u
30#define H_BORDER ((H_VIRTUAL - TMS9918_PIXELS_X * 2u) / 2u) /* 64 pixels each side */
31#define SENTINEL 0xbeefu
32
33static pico9918_scanline_params_t params = {H_VIRTUAL, 240, false, 0};
34static PICO9918_PIXEL_T line[H_VIRTUAL + 16];
35
36static int failures;
37
38static void fail(const char* what, unsigned where, unsigned wanted, unsigned got)
39{
40 if (++failures <= 8) printf(" FAIL %s at %u: want %04x got %04x\n", what, where, wanted, got);
41}
42
43static void regWrite(uint8_t reg, uint8_t value)
44{
46}
47
48/* Two writes of 0x1c to R57, which is what an F18A answers to. */
49static void unlock(void)
50{
51 regWrite(0x39, 0x1c);
52 regWrite(0x39, 0x1c);
53}
54
55/* A palette no two entries of which share a colour, so an entry reached through the
56 wrong layout cannot accidentally match the right one. */
57static void distinctPalette(void)
58{
59 for (int i = 0; i < 64; ++i) tms9918->vram.map.pram[i] = (uint16_t)(0xf000 | (i * 0x111));
60}
61
62static uint16_t want(int index)
63{
64 return (uint16_t)PICO9918_PIXEL_FROM_RGB12(tms9918->vram.map.pram[index]);
65}
66
67static void renderLines(uint16_t from, uint16_t to)
68{
69 for (uint16_t y = from; y < to; ++y) pico9918_frame_scanline(PICO9918_INST y, &params, line);
70}
71
72/* A line inside the picture, which is where the rebuild-then-render order applies.
73 Border lines rebuild the LUT on one specific line and render nothing. */
74static uint16_t activeLine(void)
75{
76 return (uint16_t)(pico9918_v_border_impl(PICO9918_INST_ONLY) + 10);
77}
78
79/* ONE line, so a rebuild that arrives a line late is a failure rather than something
80 the next 19 lines paper over. pico9918_frame_scanline rebuilds a dirty LUT BEFORE
81 it renders, so a state change that only becomes visible inside pico9918_scan_line
82 has already missed this line - and this line is a wide row either way, because
83 pico9918_line_bytes() reads the unlock live. */
84static void renderOneActiveLine(void)
85{
86 const uint16_t y = activeLine();
87 renderLines(y, y + 1);
88}
89
90static void text80(void)
91{
92 regWrite(0, 0x04); /* TEXT80 */
93 regWrite(1, 0xd0); /* text, display active */
94 regWrite(2, 0x0c);
95 regWrite(4, 0x01);
96 regWrite(7, 0xf4);
97}
98
99static void checkLineBytes(const char* stage, uint32_t wanted)
100{
101 const uint32_t got = pico9918_line_bytes(PICO9918_INST_ONLY);
102 if (got != wanted) fail(stage, 0, wanted, got);
103}
104
105/*
106 * The doubled layout: 64 entries, each the same pixel in both halves of the word. A
107 * mode that doubles stores one entry per output pair; a wide 80-column row indexes
108 * the same entries with a whole byte and takes one half. So every one of the 64
109 * palette addresses must carry its own colour, twice.
110 *
111 * Indexes 0-15 come out right under EITHER layout - a nibble-pair build writes them
112 * first - so a check that stopped there would pass on the wrong LUT. It is 16-63, the
113 * palette SELECT the 8bpp tier exists to provide, that tells the two apart.
114 */
115static void checkDoubledLayout(const char* stage)
116{
117 for (int i = 0; i < 64; ++i)
118 {
119 const uint32_t entry = pico9918_palette_lut[i];
120 if ((uint16_t)entry != want(i)) fail(stage, (unsigned)i, want(i), (uint16_t)entry);
121 if ((uint16_t)(entry >> 16) != want(i)) fail(stage, (unsigned)i, want(i), (uint16_t)(entry >> 16));
122 }
123}
124
125/*
126 * The 4bpp layout: one byte is two pixels, so entry (high << 4) | low holds two
127 * DIFFERENT colours. The high nibble is the left pixel and lands in the LOW half of
128 * the word, which is where a little-endian pair writer puts the earlier pixel.
129 *
130 * Entries below 16 are the exception: an index under 16 still means one colour, so
131 * they are doubled like any other mode's. Same rule the golden reference states.
132 */
133static void checkPackedLayout(const char* stage)
134{
135 for (unsigned index = 0; index < 256; ++index)
136 {
137 const int high = index < 16 ? (int)index : (int)(index >> 4);
138 const int low = index < 16 ? (int)index : (int)(index & 0x0f);
139
140 const uint32_t entry = pico9918_palette_lut[index];
141 if ((uint16_t)entry != want(high)) fail(stage, index, want(high), (uint16_t)entry);
142 if ((uint16_t)(entry >> 16) != want(low)) fail(stage, index, want(low), (uint16_t)(entry >> 16));
143 }
144}
145
146/*
147 * Where the picture lands. Every mode fills the same window - 512 pixels between two
148 * 64-pixel borders - because a 256-wide mode doubles into it and a wide row already
149 * fills it. Nothing crashes when a host gets this wrong, so only a check like this
150 * says so: the sentinel sweep pins the fill counts, the border sweep pins the offset,
151 * and the pair sweep pins the doubling itself.
152 */
153static void checkGeometry(const char* stage, int doubled)
154{
155 for (unsigned x = 0; x < H_VIRTUAL + 16; ++x) line[x] = SENTINEL;
156 renderOneActiveLine();
157
158 for (unsigned x = 0; x < H_VIRTUAL; ++x)
159 if (line[x] == SENTINEL) fail(stage, x, 0, SENTINEL);
160
161 const uint16_t bg = (uint16_t)(pico9918_border_bg & 0xffff);
162 for (unsigned x = 0; x < H_BORDER; ++x)
163 {
164 if (line[x] != bg) fail(stage, x, bg, line[x]);
165 if (line[H_VIRTUAL - 1 - x] != bg) fail(stage, H_VIRTUAL - 1 - x, bg, line[H_VIRTUAL - 1 - x]);
166 }
167
168 if (doubled)
169 for (unsigned x = 0; x < TMS9918_PIXELS_X * 2u; x += 2)
170 if (line[H_BORDER + x] != line[H_BORDER + x + 1])
171 fail(stage, H_BORDER + x, line[H_BORDER + x], line[H_BORDER + x + 1]);
172}
173
174/*
175 * The CRT-scanline dim. Off, it must not touch a pixel; on, every channel drops one
176 * stop and none of them bleeds - not into the channel below it, and not into the next
177 * pixel's top bit, which is the whole reason the transform carries a mask.
178 *
179 * Reconstructed a channel at a time, so a wrong PICO9918_PIXEL_PAIR_DIM cannot agree
180 * with itself.
181 */
182static uint16_t dimmed(uint16_t src)
183{
184 return (uint16_t)(((src & 0x00f) >> 1) | (((src & 0x0f0) >> 1) & 0x070) |
185 (((src & 0xf00) >> 1) & 0x700));
186}
187
188/* R50 bit 2 is the dim's only owner; a settings block seeds it and never reads back. */
189static void setScanlines(int on)
190{
191 if (on)
192 TMS_REGISTER(tms9918, PICO9918_REG_ENHANCED2) |= PICO9918_R50_VSCANLINES;
193 else
194 TMS_REGISTER(tms9918, PICO9918_REG_ENHANCED2) &= (uint8_t)~PICO9918_R50_VSCANLINES;
195}
196
197/* The setting and the output-line parity gate independently. An odd line at scale 2 is a
198 dim with no render behind it, so the maths is checkable against a seeded buffer. */
199static void checkDimMaths(void)
200{
201 static const uint16_t sample[8] = {0x0000, 0x0fff, 0x0111, 0x0f0f, 0x0777, 0x0888,
202 0x0001, 0x0f00};
203
204 pico9918_v_scale = 2;
205 for (int on = 0; on < 2; ++on)
206 {
207 setScanlines(on);
208
209 for (uint32_t out = 1; out < 4; out += 2)
210 {
211 for (int i = 0; i < 8; ++i) line[i] = sample[i];
212
213 const bool changed = pico9918_frame_output_line(PICO9918_INST out, &params, line);
214 if (changed != (on != 0)) fail("dim-return", out, (unsigned)(on != 0), changed);
215
216 for (int i = 0; i < 8; ++i)
217 {
218 const uint16_t wanted = on ? dimmed(sample[i]) : sample[i];
219 if (line[i] != wanted)
220 fail(on ? "dim-on" : "dim-off", out * 8u + (unsigned)i, wanted, line[i]);
221 }
222 }
223 }
224 setScanlines(0);
225}
226
227/* Double rows: vPixelScale is 1, nothing repeats, and a rule keyed on the repeat index
228 dims nothing at all. Render each line with the setting off and again with it on - the
229 odd ones must come back a stop down, the even ones untouched. */
230static void checkDimScale1(void)
231{
232 static PICO9918_PIXEL_T off[H_VIRTUAL];
233 const uint32_t base = (activeLine() + 1u) & ~1u;
234
235 pico9918_v_scale = 1;
236 for (uint32_t out = base; out < base + 4; ++out)
237 {
238 setScanlines(0);
239 pico9918_frame_output_line(PICO9918_INST out, &params, line);
240 memcpy(off, line, sizeof(off));
241
242 setScanlines(1);
243 pico9918_frame_output_line(PICO9918_INST out, &params, line);
244
245 for (uint32_t i = 0; i < H_VIRTUAL; ++i)
246 {
247 const uint16_t wanted = (out & 1) ? dimmed(off[i]) : off[i];
248 if (line[i] != wanted)
249 {
250 fail((out & 1) ? "scale1-dim" : "scale1-keep", out * 1000u + i, wanted, line[i]);
251 break;
252 }
253 }
254 }
255 setScanlines(0);
256 pico9918_v_scale = 2;
257}
258
259int main(void)
260{
261 const int tier = PICO9918_BUILD_TEXT80_8BPP;
262
263 pico9918_init();
264 distinctPalette();
265
266 /* 1. locked 80 columns is the 4bpp line, tier or no tier */
267 text80();
268 renderLines(0, 120);
269 checkLineBytes("locked-t80", TMS9918_PIXELS_X);
270 checkPackedLayout("locked-t80");
271
272 /* 2. unlocking does not change the MODE, so on a build with the tier nothing but
273 the unlock itself can tell the LUT that its layout has moved */
274 unlock();
275 renderOneActiveLine();
276 checkLineBytes("unlocked-t80", tier ? TMS9918_PIXELS_X * 2u : TMS9918_PIXELS_X);
277 if (tier)
278 {
279 checkDoubledLayout("unlock-mid-frame");
280 checkGeometry("t80-wide-geometry", 0);
281 }
282 else
283 {
284 checkPackedLayout("unlocked-t80-4bpp");
285 }
286
287 /* 3. and a mode that doubles, which is every other one */
288 regWrite(0, 0x00); /* graphics I, still unlocked */
289 regWrite(1, 0xc0);
290 renderLines(0, 120);
291 checkLineBytes("graphics-i", TMS9918_PIXELS_X);
292 checkDoubledLayout("graphics-i");
293 checkGeometry("graphics-i-geometry", 1);
294
295 /* 4. and the CRT-scanline dim a host applies to the repeat of each line */
296 checkDimMaths();
297 checkDimScale1();
298
299 printf("%s: post-palette pixel path, %s 8bpp tier, %d failure(s)\n", failures ? "FAIL" : "PASS",
300 tier ? "with" : "without", failures);
301 return failures != 0;
302}
uint32_t pico9918_line_bytes(pico9918_t *tms9918)
how many bytes of pixels[] this mode fills.
Definition pico9918.c:3518
#define PICO9918_R50_VSCANLINES
F18A only: dim every second raster line.
Definition pico9918.h:360
#define PICO9918_INST_ONLY
pass the instance as the only argument
Definition pico9918.h:74
@ PICO9918_REG_ENHANCED2
GPU triggers, per-position attributes, layer priority.
Definition pico9918.h:237
#define TMS9918_PIXELS_X
active display width, every mode
Definition pico9918.h:376
#define PICO9918_INST
pass the instance ahead of other arguments
Definition pico9918.h:73
bool pico9918_frame_output_line(pico9918_t *tms9918, uint32_t outputLine, pico9918_scanline_params_t *params, PICO9918_PIXEL_T *pixels)
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-core - frame module
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
the host's per-call display parameters, as the scanline sees them