pico9918-core 1.3.0
TMS9918A / F18A video display processor emulation in C99
Loading...
Searching...
No Matches
pico9918_frame.c
Go to the documentation of this file.
1/**
2 * \file
3 * \brief pico9918-core - frame module
4 *
5 * Copyright (c) 2021 Troy Schrapel
6 *
7 * This code is licensed under the MIT license
8 *
9 * https://github.com/visrealm/pico9918-core
10 *
11 * Per-frame / per-scanline sequencing: the interrupt/status latch merge, the
12 * vertical porch, the end-of-frame interrupt and trigger line, the true end of
13 * frame, and the vertical geometry it derives.
14 *
15 * It lands in a .c rather than on the Impl inline surface because a real function
16 * here costs nothing extra: every call site already reaches it with a `bl`.
17 * Inlining it into its three callers - one of them the per-scanline path - would
18 * ADD per-scanline code, not remove a call. The entries the host reaches through a
19 * VGA function pointer are calls for the same reason.
20 */
21
22#include "impl/pico9918_priv.h"
23#include "pico9918_frame.h"
24/* pico9918_gpu_trigger, the ONLY GPU entry this module uses - so gpu/gpu.h is not
25 included here; the GPU time accessors it declares belong to the diagnostics
26 overlay. */
28#include "overlay/diag.h"
29#include "overlay/splash.h"
30
31/* Frame counter and dropped-frame accounting - declared on the Impl surface, which
32 carries the ownership and placement rationale. The window itself is a file static
33 because nothing outside this module reads it; only the running total is exposed. */
34int pico9918_frame_count = 0;
35int pico9918_dropped_frames_count = 0;
36static bool dropped_frames[16] = {0};
37
38/* Vertical geometry and the display-enable latch. Also declared on the Impl
39 surface - same rationale, same measurement. */
40int pico9918_v_pixels = 192;
41uint32_t pico9918_v_border = 0;
42bool pico9918_valid_writes = false;
43
44/* What pico9918_frame_output_line maps an output line through. Seeded progressive. */
45uint8_t pico9918_v_scale = 2;
46uint16_t pico9918_v_virtual = 240;
47
48#if PICO9918_DIAG_GPU_FRAME_COUNTER
49/* GPU frames observed at end of frame. Opt-in, and it sits with the accumulation
50 that feeds it: the increment is per-frame and the reset is on console reset,
51 both in this module. */
52uint32_t pico9918_gpu_frame_count = 0;
53#endif
54
55/* Host late-config-reload hook - see the header for the contract, and for why this
56 is a pointer rather than a tier-1 op. pico9918.h carries why only the storage
57 differs between the two builds. */
58#if PICO9918_SINGLE_INSTANCE
59static struct
60{
61 pico9918_config_reload_fn fn;
62 void* userdata;
63} configReload;
64#define CONFIG_RELOAD_CB configReload
65#else
66#define CONFIG_RELOAD_CB tms9918->configReload
67#endif
68
69/* The border colour the border-fill DMA reads. THE SCRATCH PLACEMENT IS
70 LOAD-BEARING: `.scratch_y` is one of the RP2040's two single-cycle SRAM banks,
71 and a silent fallthrough to striped SRAM is a real per-scanline loss that no
72 functional test can see.
73
74 Not static, because the fill instance that reads it is configured in
75 pico9918.c's initLookups(), alongside the library's own fill - one init site
76 for both, never lazily. Declared on the Impl surface with the module's other
77 globals rather than reached for by an extern at the use site. */
78PICO9918_SECTION_SCRATCH_Y(buffer) uint32_t pico9918_border_bg;
79
80void pico9918_frame_set_config_reload_callback(PICO9918_INST_ARG pico9918_config_reload_fn cb, void* userdata)
81{
82 CONFIG_RELOAD_CB.fn = cb;
83 CONFIG_RELOAD_CB.userdata = userdata;
84}
85
86static inline void configReloadFire(PICO9918_INST_ONLY_ARG)
87{
88 if (CONFIG_RELOAD_CB.fn) CONFIG_RELOAD_CB.fn(tms9918, CONFIG_RELOAD_CB.userdata);
89}
90
91/**
92 * \brief merge newly raised status flags into the SR0 latch, publish it, and bring the
93 * /INT pin into agreement. See the header for the parameter contract.
94 *
95 * The internal sequence is load-bearing at two points and must not be reordered:
96 *
97 * - the three-way merge below is the v1.2.0 semantics, not a simplification of
98 * it. Each branch differs in what it lets through and what it preserves;
99 * - the status publish (PICO9918_HOST_STATUS_VISIBLE) happens BEFORE the pin sync,
100 * so a host CPU that takes the interrupt cannot read a stale status.
101 *
102 * A scanline that changes neither SR0 nor the pin skips all three. That is an
103 * equivalence rather than an approximation: set_status writes both SR0 copies, which
104 * every writer in the library keeps in lockstep, so rewriting the value already there
105 * changes neither; sync_int already compares against the latched pin, and the guard's
106 * second term is exactly its own test; and the published word moves only with SR0 or
107 * with the read-ahead byte, which belongs to the host's bus handlers and is published
108 * by them after every access. A host that latches SR0 behind the library's back -
109 * pico9918_set_status(), say - therefore owns its own publish; the frame path no
110 * longer covers for it on the next scanline.
111 */
112
113/** \brief the three-way SR0 merge, alone: what the latch becomes, with no side effects */
114static inline uint8_t frameMergeStatus(uint8_t currentStatus, uint8_t tempStatus)
115{
116 if (currentStatus & PICO9918_SR0_INT)
117 {
118 return (uint8_t)(currentStatus | (tempStatus & PICO9918_SR0_COLLISION));
119 }
120 if (currentStatus & PICO9918_SR0_5S)
121 {
122 // 5S already latched - preserve existing ID, OR in any new flags (INT, 5S, COL)
123 return (uint8_t)(currentStatus | (tempStatus & 0xe0));
124 }
125 return (uint8_t)((currentStatus & 0xe0) | tempStatus);
126}
127
129{
130 /* LOAD-BEARING: tested BEFORE the critical section, never inside it. That window's
131 length is the host's sustained read floor, so a scanline with nothing to publish
132 must not open one at all. Unmasked is sound because the only concurrent writer is
133 the host's status-read handler, which clears and syncs for itself. */
134 const uint8_t seenStatus = pico9918_frame_status_impl(PICO9918_INST_ONLY);
135 if (frameMergeStatus(seenStatus, tempStatus) == seenStatus &&
137 {
138 return;
139 }
140
141 PICO9918_HOST_ENTER_CRITICAL();
142 const uint8_t currentStatus = pico9918_frame_status_impl(PICO9918_INST_ONLY);
143 const uint8_t mergedStatus = frameMergeStatus(currentStatus, tempStatus);
144
146 PICO9918_HOST_STATUS_VISIBLE();
147
149 PICO9918_HOST_EXIT_CRITICAL();
150}
151
152/** \brief see the header. */
154{
155 tms9918->vram.map.blanking = 1; // V
156 tms9918->vram.map.scanline = 255; // F18A value for vsync
157 TMS_STATUS(tms9918, PICO9918_SR_RASTER_LINE) = 255;
158}
159
160/**
161 * \brief see the header.
162 *
163 * The configDirty consumption is a CHECK-THEN-CLEAR on a non-volatile flag written
164 * from the other core (the register path sets it on core 1, the host's config load
165 * on core 0) and consumed here. Deliberately left alone: the worst case is a config
166 * apply deferred by one frame.
167 */
169{
170 pico9918_set_frame_done_int_impl(PICO9918_INST true);
171 TMS_STATUS(tms9918, PICO9918_SR_IDENT) |= PICO9918_SR1_BLANK;
172 if (TMS_REGISTER(tms9918, PICO9918_REG_ENHANCED2) & PICO9918_R50_GPU_VSYNC)
173 {
174 pico9918_gpu_trigger(PICO9918_INST_ONLY);
175 }
176
177 if (tms9918->configDirty)
178 {
179 tms9918->configDirty = false;
180
181 /* invokes the config-applied callback, which owns the VGA-side write */
184 }
185
187}
188
189/** \brief see the header. It takes no display line: the body never reads one. */
191{
192 if (!pico9918_frame_done_int_impl(PICO9918_INST_ONLY))
193 {
194 bool droppedFrame = pico9918_frame_status_impl(PICO9918_INST_ONLY) & PICO9918_SR0_INT;
195 pico9918_dropped_frames_count += droppedFrame - dropped_frames[pico9918_frame_count & 0xf];
196 dropped_frames[pico9918_frame_count & 0xf] = droppedFrame;
197
199 }
200}
201
202/**
203 * \brief see the header.
204 *
205 * Three things about this expression are deliberately NOT tidied:
206 *
207 * - the yScale-conditional structure. vPixelScale and vVirtualPixels are
208 * rewritten only when yScale > 1, and the vPixels doubling is gated on the SAME
209 * condition. Under interlace the host owns the first two and vPixels must NOT
210 * double - the two fields already supply the second set of lines;
211 * - the shift-by-bool algebra (`yScale - (bool)doubleRows`, `<< (bool)doubleRows`).
212 * The golden frame surface's reference deliberately decomposes this into
213 * explicit cases so the two do not share the algebra; rewriting it here to look
214 * like the reference would destroy that independence;
215 * - the SIGNED intermediate for the border. SCART NTSC in row-30 mode makes it
216 * negative and the narrowing to uint32_t is the shipping behaviour, pinned at
217 * geom-scart-ntsc-row30. A known defect, ruled won't-fix: the host's unsigned
218 * border test then sends all 220 lines down the border path. Do not "fix" it
219 * here.
220 *
221 * yScale is derived from `interlaced` rather than from a build-time DISPLAY_YSCALE.
222 * A non-SCART build's DISPLAY_YSCALE is always 2 with interlaced false, so the
223 * interlace-derived form covers both cases exactly and the library needs no
224 * build-time host macro.
225 */
227{
228 const int yScale = display->interlaced ? 1 : 2;
229
230 if (yScale > 1)
231 {
232 bool doubleRows = TMS_REGISTER(tms9918, TMS_REG_0) & TMS_R0_DOUBLE_ROWS;
233 display->vPixelScale = yScale - (bool)doubleRows;
234 display->vVirtualPixels = (display->displayPixels / yScale) << (bool)doubleRows;
235 }
236
237 /* Outside the conditional so both arms publish; under interlace these are the host's. */
238 pico9918_v_scale = display->vPixelScale;
239 pico9918_v_virtual = display->vVirtualPixels;
240
241 int baseRows = (TMS_REGISTER(tms9918, PICO9918_REG_ENHANCED1) & PICO9918_R49_ROW30) ? 30 : 24;
242 pico9918_v_pixels = baseRows << 3;
243 if (yScale > 1 && (TMS_REGISTER(tms9918, TMS_REG_0) & TMS_R0_DOUBLE_ROWS)) pico9918_v_pixels <<= 1;
244 pico9918_v_border = (display->vVirtualPixels - pico9918_v_pixels) / 2;
245
247 g.vPixels = pico9918_v_pixels;
248 g.vBorder = pico9918_v_border;
249 g.triggerScanline = pico9918_v_border + pico9918_v_pixels;
250 return g;
251}
252
253/**
254 * \brief see the header. It takes no frame number: the cadence and the thresholds all run
255 * off this module's own frame counter, which - unlike the VGA layer's frame number -
256 * resets on console reset.
257 *
258 * The frame count is re-READ at each use rather than cached in a local. That is not
259 * a style choice: the host's tier-1 critical section deliberately does not mask the
260 * reset GPIO IRQ, which zeroes the counter, so a console reset landing mid-function
261 * is observable and a cached copy would hide it.
262 */
265{
266 ++pico9918_frame_count;
267#if PICO9918_DIAG_GPU_FRAME_COUNTER
268 pico9918_gpu_frame_count += (TMS_STATUS(tms9918, PICO9918_SR_GPU) & 0x80) != 0;
269#endif
270
271 /* The slice is per scanline, so it moves with the line count and the refresh. */
272 pico9918_gpu_note_frame(PICO9918_INST display->vVirtualPixels, frameRateHz);
273
274 {
275 static float tempAccum = 0.0f;
276 tempAccum += tempC;
277 if ((pico9918_frame_count & 0x3f) == 0) // every 64th frame
278 {
279 tempAccum /= 64.0f;
281 uint8_t t4 = (uint8_t)(tempAccum * 4.0f + 0.5f);
282 TMS_STATUS(tms9918, PICO9918_SR_TEMPERATURE) = t4;
283 tempAccum = 0.0f;
284 }
285 }
286
287 if (!pico9918_valid_writes)
288 {
289 // has the display been enabled?
290 if ((pico9918_valid_writes = (TMS_REGISTER(tms9918, TMS_REG_1) & TMS_R1_DISP_ACTIVE)) != 0)
291 {
293 if (pico9918_frame_count > PICO9918_FRAME_STARTUP_FRAMES)
294 {
295 // reset diagnostics and other settings back to defaults
296 configReloadFire(PICO9918_INST_ONLY);
297 }
298 }
299 }
300
301 if (tms9918->config[PICO9918_CONF_DIAG] && PICO9918_HAS(tms9918, PICO9918_FEAT_OVERLAY))
302 {
303 pico9918_diag_set_frame_rate(frameRateHz);
304 pico9918_diag_update(PICO9918_INST pico9918_frame_count);
305 }
306
307 // a missed last row leaves the end-of-frame interrupt owing
308 if (!pico9918_frame_done_int_impl(PICO9918_INST_ONLY))
309 {
311 }
312
314}
315
316/**
317 * \brief see the header. The per-scanline path - the function every gate in this project
318 * exists to protect.
319 *
320 * The host's pending-display banner is deliberately not here. It is host code - host flash
321 * state, a host trigger byte, centring against the host's own buffer width - and its only
322 * ordering constraints are pixel ones: after the border fill and the splash, before the
323 * diagnostics overlay. The host's overlay tail already sits there, and what runs between
324 * touches no pixels, so the framebuffer is bit-identical either way.
325 *
326 * The return value is the border flag, and the host needs it to place that banner. "y is
327 * small" is not the same test: in row-30 progressive mode vBorder is 0, so the rows the
328 * banner occupies are active ones and a host that guessed would paint over the display.
329 *
330 * FIVE THINGS HERE MUST NOT BE TIDIED:
331 *
332 * - the `bg` store BEFORE the border WAIT. The previous line's right-border fill may
333 * still be reading the source word when this line overwrites it. Benign - the
334 * value is the same on all but the frame a background register changes - and
335 * "fixing" it by waiting first adds a per-scanline stall the goldens cannot see;
336 * - the row-30 border test. The `TRAP:` below is a recorded, user-ruled won't-fix;
337 * - the SCART-NTSC negative-border underflow. vBorder is unsigned and row-30 on that
338 * timing makes it 4294967286, so this test sends all 220 lines down the border
339 * path and renders none. Pinned deliberately by the golden frame surface at
340 * geom-scart-ntsc-row30. Do not fix it here;
341 * - the frame count re-READ at each use rather than cached, for the reason
342 * pico9918_frame_end above states: the reset GPIO IRQ is not masked and zeroes it;
343 * - the `y -= vBorder` in BOTH arms. It looks like it belongs after the branch, but
344 * the border arm's own body reads the unadjusted y (the bottom-border scanline
345 * register, the palette-regenerate trigger and the splash all do), so hoisting it
346 * would change all three.
347 */
348bool __time_critical_func(pico9918_frame_scanline)(PICO9918_INST_ARG uint16_t y,
349 const pico9918_scanline_params_t* params, PICO9918_PIXEL_T* pixels)
350{
351 const uint32_t halfHBorder = (params->hVirtualPixels - TMS9918_PIXELS_X * 2) / 4;
352
353 // for interlaced modes, bit 12 of y carries the field number (0=Field1, 1=Field2)
354 const uint8_t field = (y >> 12) & 1;
355 y = y & 0x0fff; // virtual line within the field (0..N-1)
356
357 uint32_t* dPixels = (uint32_t*)pixels;
358
359 /* 512 bytes for 80 columns on a board with the 8bpp tier, 256 everywhere else */
360 const uint32_t lineBytes = pico9918_line_bytes_impl(PICO9918_INST_ONLY);
361 const bool packedNibbles =
362 pico9918_display_mode_impl(PICO9918_INST_ONLY) == TMS_MODE_TEXT80 && lineBytes == TMS9918_PIXELS_X;
363
364 /* LOAD-BEARING: PRAM, not the LUT, which no top border has rebuilt yet. */
365 pico9918_border_bg = PICO9918_PIXEL_PAIR(PICO9918_PIXEL_FROM_RGB12(tms9918->vram.map.pram
366 [(TMS_REGISTER(tms9918, TMS_REG_FG_BG_COLOR) & 0x0f) |
367 (packedNibbles ? 0 : (TMS_REGISTER(tms9918, PICO9918_REG_PALETTE_SELECT) & PICO9918_R24_TILE1_PS) << 4)]));
368
369 if (y == 0)
370 {
371 pico9918_set_frame_done_int_impl(PICO9918_INST false);
372 }
373
374 PICO9918_FILL32_WAIT(PICO9918_FILL_BORDER);
375
376 /*** top and bottom borders ***/
377 // TRAP: none of this runs in ROW30 mode. A ruled won't-fix, not an oversight - see
378 // the note above this function, which lists it among what must not be tidied.
379 if (y < pico9918_v_border_impl(PICO9918_INST_ONLY) ||
380 y >= (pico9918_v_border_impl(PICO9918_INST_ONLY) + pico9918_v_pixels_impl(PICO9918_INST_ONLY)))
381 {
382 PICO9918_FILL32_SET_COUNT(PICO9918_FILL_BORDER, params->hVirtualPixels / 2);
383 PICO9918_FILL32_TRIGGER(PICO9918_FILL_BORDER, dPixels);
384 tms9918->vram.map.blanking = 1; // V
385 if ((y >= pico9918_v_border_impl(PICO9918_INST_ONLY) + pico9918_v_pixels_impl(PICO9918_INST_ONLY)))
386 {
387 tms9918->vram.map.scanline = y - pico9918_v_border_impl(PICO9918_INST_ONLY);
388 TMS_STATUS(tms9918, PICO9918_SR_RASTER_LINE) = tms9918->vram.map.scanline;
389 }
390
391 if (PICO9918_HAS(tms9918, PICO9918_FEAT_OVERLAY) &&
392 (!pico9918_valid_writes_impl(PICO9918_INST_ONLY) ||
393 (pico9918_frame_count_impl(PICO9918_INST_ONLY) < 600)))
394 {
395 PICO9918_FILL32_WAIT(PICO9918_FILL_BORDER);
396
397 pico9918_splash_render(y, pico9918_frame_count_impl(PICO9918_INST_ONLY),
398 pico9918_v_border_impl(PICO9918_INST_ONLY),
399 pico9918_v_pixels_impl(PICO9918_INST_ONLY), params->vVirtualPixels, pixels);
400
401 if (pico9918_frame_count_impl(PICO9918_INST_ONLY) > PICO9918_FRAME_STARTUP_FRAMES)
402 {
403 tms9918->config[PICO9918_CONF_DIAG] = true;
404 tms9918->config[PICO9918_CONF_DIAG_REGISTERS] = true;
405 tms9918->config[PICO9918_CONF_DIAG_PERFORMANCE] = true;
406 tms9918->config[PICO9918_CONF_DIAG_PALETTE] = true;
407 tms9918->config[PICO9918_CONF_DIAG_ADDRESS] = true;
408 }
409 }
410
411 if ((y == pico9918_v_border_impl(PICO9918_INST_ONLY) - 1) && pico9918_palette_dirty(PICO9918_INST_ONLY))
412 {
413 pico9918_palette_regenerate(PICO9918_INST_ONLY);
414 }
415
416 if (TMS_REGISTER(tms9918, PICO9918_REG_ENHANCED2) & PICO9918_R50_GPU_HSYNC)
417 {
418 pico9918_gpu_trigger(PICO9918_INST_ONLY);
419 }
420
421 /* Border lines too: a program paging in the vertical blank waits on exactly these. */
422 pico9918_gpu_service(PICO9918_INST_ONLY);
423
424 return true;
425 }
426
427 uint32_t lineStart = PICO9918_HOST_TIME_US();
428
429 y -= pico9918_v_border_impl(PICO9918_INST_ONLY);
430 tms9918->vram.map.blanking = 0;
431 tms9918->vram.map.scanline = y;
432 TMS_STATUS(tms9918, PICO9918_SR_RASTER_LINE) = y;
433
434 /*** left border ***/
435 PICO9918_FILL32_SET_COUNT(PICO9918_FILL_BORDER, halfHBorder);
436 PICO9918_FILL32_TRIGGER(PICO9918_FILL_BORDER, dPixels);
437
438 /*** main display region ***/
439 if (pico9918_palette_dirty(PICO9918_INST_ONLY)) pico9918_palette_regenerate(PICO9918_INST_ONLY);
440
441 uint16_t tmsY =
442 pico9918_frame_map_line_impl(PICO9918_INST y, field, params->interlaced, params->interlacedFieldOrder);
443
444 /* the previous line's capture reads the buffer this render is about to overwrite */
445 PICO9918_LINE_CAPTURE_WAIT();
446
447 uint32_t renderTime = PICO9918_HOST_TIME_US();
448 uint8_t tempStatus = pico9918_scan_line(PICO9918_INST tmsY);
449 renderTime = PICO9918_HOST_TIME_US() - renderTime;
450
451 const uint8_t* lineSource = pico9918_line_source_impl(PICO9918_INST_ONLY);
452 PICO9918_LINE_CAPTURE(y, pico9918_v_pixels_impl(PICO9918_INST_ONLY), lineBytes, lineSource);
453
454 /*** F18A status register updates ***/
455 TMS_STATUS(tms9918, PICO9918_SR_IDENT) &= (uint8_t)~PICO9918_SR1_BLANK;
456
457 if (tms9918->vram.map.scanline &&
458 (TMS_REGISTER(tms9918, PICO9918_REG_HORZ_INT_LINE) == tms9918->vram.map.scanline))
459 {
460 TMS_STATUS(tms9918, PICO9918_SR_IDENT) |= PICO9918_SR1_HF;
461 }
462
463 if (TMS_REGISTER(tms9918, PICO9918_REG_ENHANCED2) & PICO9918_R50_GPU_HSYNC)
464 {
465 pico9918_gpu_trigger(PICO9918_INST_ONLY);
466 }
467
469
470 PICO9918_FILL32_WAIT(PICO9918_FILL_BORDER);
471
472 tms9918->vram.map.blanking = 1; // H
473
474 // convert all pixel data from color index to the host pixel format
475#if PICO9918_TEXT80_8BPP
476 if (lineBytes != TMS9918_PIXELS_X)
477 PICO9918_EXPAND_INDEXED_WIDE(dPixels + halfHBorder, lineSource, lineBytes, pico9918_palette_lut);
478 else
479#endif
480 PICO9918_EXPAND_INDEXED(dPixels + halfHBorder, lineSource, TMS9918_PIXELS_X, pico9918_palette_lut);
481
482 // right border
483 PICO9918_FILL32_TRIGGER(PICO9918_FILL_BORDER, dPixels + halfHBorder + TMS9918_PIXELS_X);
484
485 if (tms9918->config[PICO9918_CONF_DIAG] && PICO9918_HAS(tms9918, PICO9918_FEAT_OVERLAY))
486 {
487 PICO9918_FILL32_WAIT(PICO9918_FILL_BORDER);
488 pico9918_diag_render(PICO9918_INST y + pico9918_v_border_impl(PICO9918_INST_ONLY),
489 params->vVirtualPixels, pixels);
490 }
491
492 PICO9918_LINE_NOTE_TIME(y, PICO9918_HOST_TIME_US() - lineStart);
493
494 if (tms9918->config[PICO9918_CONF_DIAG_PERFORMANCE])
495 pico9918_diag_update_render_time(renderTime, PICO9918_HOST_TIME_US() - lineStart);
496
497 /* A long program's slice for this line; the arming write already ran the short ones. */
498 pico9918_gpu_service(PICO9918_INST_ONLY);
499
500 return false;
501}
502
503/* One stop over a whole line, borders included. Parity by output line, not repeat index -
504 that is stuck at zero when vPixelScale is 1. */
505static bool dimLine(PICO9918_INST_ARG PICO9918_PIXEL_T* pixels, uint32_t count,
506 uint32_t outputLine)
507{
508 if (!(outputLine & 1) || !(TMS_REGISTER(tms9918, PICO9918_REG_ENHANCED2) & PICO9918_R50_VSCANLINES))
509 return false;
510
511 uint32_t* pairs = (uint32_t*)pixels;
512 for (uint32_t i = 0; i < count / 2; ++i) pairs[i] = PICO9918_PIXEL_PAIR_DIM(pairs[i]);
513 return true;
514}
515
516/** \brief see the header. A repeat re-reads the host's buffer rather than re-rendering. */
519 pico9918_scanline_params_t* params, PICO9918_PIXEL_T* pixels)
520{
521 const uint32_t scale = pico9918_v_scale ? pico9918_v_scale : 1;
522 bool fresh = false;
523
524 params->vVirtualPixels = pico9918_v_virtual;
525
526 if (outputLine % scale == 0)
527 {
528 const uint16_t y = (uint16_t)(outputLine / scale);
529
530 if (pico9918_frame_scanline(PICO9918_INST y, params, pixels) &&
531 tms9918->config[PICO9918_CONF_DIAG] && PICO9918_HAS(tms9918, PICO9918_FEAT_OVERLAY))
533
534 fresh = true;
535 }
536
537 bool changed = dimLine(PICO9918_INST pixels, params->hVirtualPixels, outputLine) || fresh;
538
539#if PICO9918_BUILD_RUNTIME_CHIP
540 if (tms9918->chip == PICO9918_CHIP_F18A)
541 changed |= pico9918_f18a_badge_render((uint16_t)outputLine,
542 pico9918_frame_count_impl(PICO9918_INST_ONLY), pixels);
543#endif
544
545 return changed;
546}
void pico9918_diag_set_temperature(float tempC)
core temperature, degrees C
Definition diag.c:260
void pico9918_diag_update(pico9918_t *tms9918, uint32_t frameCount)
recompute the panel values - call once per frame
Definition diag.c:280
void pico9918_diag_config_updated(pico9918_t *tms9918)
rebuild the panel row table - call whenever the PICO9918_CONF_DIAG* bytes change
Definition diag.c:541
void pico9918_diag_render(pico9918_t *tms9918, uint16_t y, uint32_t vVirtualPixels, PICO9918_PIXEL_T *pixels)
render the diagnostics panels for border row y
Definition diag.c:596
void pico9918_diag_set_frame_rate(float frameRateHz)
Host display timing, Hz.
Definition diag.c:274
void pico9918_diag_update_render_time(uint32_t renderTime, uint32_t frameTime)
accumulate one scanline's render and total time, in microseconds
Definition diag.c:393
pico9918-core - Diagnostics overlay
uint8_t pico9918_scan_line(pico9918_t *tms9918, uint16_t y)
generate a scanline
Definition pico9918.c:3335
#define PICO9918_SR0_5S
more sprites on a line than the limit allows
Definition pico9918.h:290
#define PICO9918_R50_GPU_HSYNC
trigger the GPU every scanline
Definition pico9918.h:370
#define PICO9918_R50_VSCANLINES
F18A only: dim every second raster line.
Definition pico9918.h:374
#define PICO9918_SR1_BLANK
the raster is in blanking
Definition pico9918.h:296
#define PICO9918_INST_ARG
declare the instance ahead of other parameters
Definition pico9918.h:83
#define PICO9918_INST_ONLY
pass the instance as the only argument
Definition pico9918.h:86
#define PICO9918_R24_TILE1_PS
tile layer 1 palette select
Definition pico9918.h:333
#define TMS_R1_DISP_ACTIVE
render the active display
Definition pico9918.h:314
#define PICO9918_R50_GPU_VSYNC
trigger the GPU every frame
Definition pico9918.h:371
#define PICO9918_SR0_INT
status register 0 bits.
Definition pico9918.h:289
@ PICO9918_REG_ENHANCED1
tile layer 2, 30-row mode, ECM levels, real Y
Definition pico9918.h:250
@ PICO9918_REG_ENHANCED2
GPU triggers, per-position attributes, layer priority.
Definition pico9918.h:251
@ PICO9918_REG_HORZ_INT_LINE
scanline the horizontal interrupt fires on
Definition pico9918.h:234
@ PICO9918_REG_PALETTE_SELECT
sub-palette for sprites and each tile layer
Definition pico9918.h:235
@ PICO9918_SR_RASTER_LINE
the line currently being drawn
Definition pico9918.h:273
@ PICO9918_SR_IDENT
chip identity, blanking, and the scanline interrupt flag
Definition pico9918.h:271
@ PICO9918_SR_TEMPERATURE
PICO9918 only: core temperature, as degrees C times four.
Definition pico9918.h:283
@ PICO9918_SR_GPU
GPU running and its status byte.
Definition pico9918.h:272
#define PICO9918_INST_ONLY_ARG
declare the instance as the only parameter
Definition pico9918.h:84
#define TMS9918_PIXELS_X
active display width, every mode
Definition pico9918.h:390
#define TMS_R0_DOUBLE_ROWS
PICO9918 only: twice the rows, drawn interlaced.
Definition pico9918.h:307
#define PICO9918_SR1_HF
status register 1 bits.
Definition pico9918.h:295
#define PICO9918_SR0_COLLISION
two sprites overlapped on an opaque pixel
Definition pico9918.h:291
#define PICO9918_R49_ROW30
30 rows of tiles rather than 24
Definition pico9918.h:357
@ PICO9918_CHIP_F18A
an F18A: unlock, enhanced renderer, GPU
Definition pico9918.h:167
#define PICO9918_INST
pass the instance ahead of other arguments
Definition pico9918.h:85
#define PICO9918_DLLEXPORT
the linkage every public entry point carries - see LINKAGE MODES above
Definition pico9918.h:50
bool pico9918_frame_output_line(pico9918_t *tms9918, uint32_t outputLine, pico9918_scanline_params_t *params, PICO9918_PIXEL_T *pixels)
see the header.
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.
void pico9918_frame_raise_end_of_frame_int(pico9918_t *tms9918)
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.
static uint8_t frameMergeStatus(uint8_t currentStatus, uint8_t tempStatus)
merge newly raised status flags into the SR0 latch, publish it, and bring the /INT pin into agreement...
void pico9918_frame_end_of_scanline(pico9918_t *tms9918)
see the header.
void pico9918_frame_update_interrupts(pico9918_t *tms9918, uint8_t tempStatus)
merge newly raised status flags into the SR0 latch, publish it, and bring the /INT pin into agreement
pico9918_frame_geometry_t pico9918_frame_geometry(pico9918_t *tms9918, pico9918_frame_display_t *display)
see the header.
pico9918-core - frame module
#define PICO9918_FRAME_STARTUP_FRAMES
frames of startup grace before an un-enabled display is taken to mean "nothing is driving this VDP",...
void pico9918_frame_set_config_reload_callback(pico9918_t *tms9918, pico9918_config_reload_fn cb, void *userdata)
register the host's late-config-reload hook
pico9918-core - GPU privileged surface
pico9918-core - the private instance layout
void pico9918_config_apply(pico9918_t *tms9918)
apply the config block's VDP-side effects: registers 50 and 30, the palette unpack,...
PICO9918_INLINE void pico9918_frame_sync_int_impl(pico9918_t *tms9918)
THE single implementation of "a status register was just read" - shared by the public read (pico9918_...
PICO9918_INLINE_HOT void pico9918_set_status_impl(pico9918_t *tms9918, uint8_t status)
set status flag
PICO9918_INLINE_HOT bool pico9918_interrupt_status_impl(pico9918_t *tms9918)
whether /INT should be asserted
void pico9918_splash_allow_hide(void)
allow the splash to animate back out - the host calls this once the display has been enabled
Definition splash.c:64
void pico9918_splash_render(uint16_t y, uint32_t frameCount, uint32_t vBorder, uint32_t vPixels, uint32_t vVirtualPixels, PICO9918_PIXEL_T *pixels)
render the splash logo into the scanline buffer, if row y falls in the logo band.
Definition splash.c:74
pico9918-core - Splash overlay
the host's mutable vertical display parameters, as the end-of-frame geometry sees them
uint16_t vVirtualPixels
(in, and out when yScale > 1)
the vertical geometry the end of frame derives
uint32_t vBorder
top border offset, in virtual lines
uint32_t triggerScanline
vBorder + vPixels
int vPixels
active VDP display lines
the host's per-call display parameters, as the scanline sees them
uint16_t vVirtualPixels
virtual lines per field
uint16_t hVirtualPixels
full scanline width; exactly this many pixels are written