pico9918-core 1.3.0
TMS9918A / F18A video display processor emulation in C99
Loading...
Searching...
No Matches
pico9918_frame.h
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 * The frame module owns per-frame and per-scanline sequencing.
12 */
13
14#ifndef _PICO9918_FRAME_H
15#define _PICO9918_FRAME_H
16
17#include "pico9918.h"
18
19/* PICO9918_PIXEL_T, for the scanline entry point's output buffer. Same dependency
20 overlay/splash.h already takes, and for the same reason - the pixel type
21 is host policy and lives on the platform surface. */
22#include "impl/platform.h"
23
24#ifdef __cplusplus
25extern "C"
26{
27#endif
28
29 /**
30 * \brief merge newly raised status flags into the SR0 latch, publish it, and
31 * bring the /INT pin into agreement
32 *
33 * tempStatus: flags raised by the scanline just rendered (PICO9918_SR0_INT / _5S / _COL
34 * plus, when 5S is newly set, the sprite number in the low 5 bits)
35 *
36 * Runs on every active scanline and at the end-of-frame trigger line. The body
37 * runs inside PICO9918_HOST_ENTER/EXIT_CRITICAL because the host's bus-interface
38 * handlers mutate the same latch - see the threading contract in
39 * impl/platform.h.
40 */
43
44 /**
45 * \brief vertical porch: blank the display and park the scanline counter at
46 * the F18A vsync value
47 *
48 * Called once per frame, at the start of the vertical porch. Touches only VDP
49 * state, so it needs nothing from the host.
50 */
53
54 /**
55 * \brief raise this frame's end-of-frame interrupt: latch doneInt, set the SR1
56 * vsync bit, trigger the GPU if R50 bit 5 asks for it, consume a pending config
57 * change, then merge PICO9918_SR0_INT into the SR0 latch
58 *
59 * Unconditional - the caller decides whether the frame still owes an interrupt by
60 * testing pico9918_frame_done_int_impl(). Called at the trigger line (from
61 * pico9918_frame_end_of_scanline) and again as a fallback at true end of frame, for a
62 * frame whose trigger line was never reached.
63 *
64 * Exported as part of the frame contract: a host driving frames itself needs the
65 * fallback, though every caller in this tree is inside the module.
66 */
69
70 /**
71 * \brief end-of-frame trigger line: if this frame has not raised its interrupt
72 * yet, account for the dropped frame and raise the interrupt
73 *
74 * Called once per frame, from the trigger scanline the host was told to fire on
75 * (the first line past the display region). Does nothing on a frame whose
76 * interrupt was already raised.
77 *
78 * The VGA callback that reaches this passes the display line; it is not a
79 * parameter here because the body never used it.
80 */
83
84 /**
85 * \brief the host's mutable vertical display parameters, as the end-of-frame
86 * geometry sees them
87 *
88 * FIELD WIDTHS ARE DELIBERATE and must not be widened for tidiness. They mirror
89 * the host's own declarations because the arithmetic below NARROWS through them:
90 * SCART NTSC in row-30 mode computes a negative border, which the uint32_t in
91 * pico9918_frame_geometry_t then converts to 4294967286. That conversion is the
92 * shipping behaviour and is pinned by the golden frame surface
93 * (geom-scart-ntsc-row30); a model using int throughout would compute -10, a
94 * value no device ever produces.
95 *
96 * Ownership split, which is the whole reason this is in/out rather than in:
97 * displayPixels host-owned input - the mode's vertical active line count
98 * interlaced host-owned input - selects the scale the geometry runs at
99 * vPixelScale host-owned under interlace; REWRITTEN by the library when the
100 * vVirtualPixels build is progressive (yScale > 1). See pico9918_frame_geometry.
101 */
102 typedef struct
103 {
104 int displayPixels; /**< vertical active lines of the host's mode (in) */
105 bool interlaced; /**< (in) */
106 uint8_t vPixelScale; /**< (in, and out when yScale > 1) */
107 uint16_t vVirtualPixels; /**< (in, and out when yScale > 1) */
109
110 /**
111 * \brief the vertical geometry the end of frame derives
112 *
113 * vBorder is UNSIGNED on purpose - see the narrowing note above.
114 * triggerScanline inherits the same wrap.
115 */
116 typedef struct
117 {
118 int vPixels; /**< active VDP display lines */
119 uint32_t vBorder; /**< top border offset, in virtual lines */
120 uint32_t triggerScanline; /**< vBorder + vPixels */
122
123 /**
124 * \brief recompute the vertical display geometry from R0's double-rows bit and
125 * R49's row-30 bit, and publish it as this module's vPixels / vBorder
126 *
127 * display: the host's mutable vertical parameters, read and - on a progressive
128 * build only - written. Under interlace (yScale 1) vPixelScale and
129 * vVirtualPixels are NOT touched: the host set them up and owns them.
130 *
131 * Returns the derived geometry, including the scanline the host must arm its
132 * end-of-frame trigger on. The host applies both: any rewritten display fields
133 * and the trigger line are host plumbing (a VGA parameter block and a trigger
134 * register here), so the library computes and returns rather than reaching out.
135 *
136 * Called once per frame from pico9918_frame_end, and exported so the golden frame
137 * surface can drive it directly - it is the surface's candidate path.
138 */
141
142 /**
143 * \brief true end of frame: advance the frame counter, fold in this frame's
144 * temperature reading, latch the first display enable, refresh the diagnostics
145 * panel, raise a still-owed end-of-frame interrupt, and recompute the geometry
146 *
147 * tempC: this frame's core temperature in degrees C. The host owns the sensor;
148 * the averaging cadence and the SR13 publish are the library's.
149 * frameRateHz: the host's display timing, needed by the diagnostics panel. Read
150 * only when the panel is enabled, but passed unconditionally - it is a
151 * register-resident float on the host side, so a conditional read would
152 * cost the host a branch to save nothing.
153 * display: as pico9918_frame_geometry, which this calls last.
154 *
155 * Returns that geometry, so the host applies the display fields and arms the
156 * trigger line exactly once per frame.
157 *
158 * The late config reload reaches the host through the tier-2 hook below, NOT
159 * through a flag tested on return: it must land at its original point in this
160 * sequence, ahead of the diagnostics refresh and the interrupt fallback, both of
161 * which read config bytes the reload rewrites.
162 */
165 pico9918_frame_display_t* display);
166
167/**
168 * \brief frames of startup grace before an un-enabled display is taken to mean
169 * "nothing is driving this VDP", at which point the splash gives way to the
170 * diagnostics screen
171 *
172 * Both halves of that behaviour are inside this module: the end of frame tests it
173 * to decide whether the config needs reloading once the display finally comes up,
174 * and the scanline's border path tests it to force the PICO9918_CONF_DIAG* bytes
175 * on. They must use the SAME number - a reload that fires at a different frame
176 * than the forcing did would either restore settings that were never overridden
177 * or leave overridden ones in place.
178 *
179 * Exported because it is the documented length of the startup grace period, and a
180 * host driving frames itself has no other way to know when the diagnostics screen
181 * takes over.
182 */
183#define PICO9918_FRAME_STARTUP_FRAMES 900
184
185 /**
186 * \brief the host's per-call display parameters, as the scanline sees them
187 *
188 * Per-call rather than stored, because that is the shape the host already had: the
189 * VGA layer hands its parameter block to every scanline callback, so the values are
190 * in registers at the call. A setter would add a store per mode change and a load
191 * per scanline to buy nothing.
192 *
193 * WIDTHS MIRROR THE HOST'S OWN DECLARATIONS - see the same note on
194 * pico9918_frame_display_t. hVirtualPixels feeds the border-fill count and the
195 * half-border offset; vVirtualPixels reaches the splash geometry; the interlace pair
196 * selects the field mapping.
197 *
198 * GEOMETRY THE SCANLINE PATH REQUIRES. Not checked per line - this is the video-rate
199 * path and a host's geometry is fixed at a mode change, so the cost belongs where the
200 * mode is chosen. Break one and the failure is silent:
201 *
202 * hVirtualPixels >= TMS9918_PIXELS_X * 2 the two half-borders are
203 * (hVirtualPixels - 512) / 4 words each,
204 * computed unsigned, so a narrower line
205 * underflows into a fill of ~1G words
206 * hVirtualPixels a multiple of 4 the halves have to tile the line exactly;
207 * a remainder leaves the right edge unwritten
208 * and disagrees with the border path's own
209 * hVirtualPixels / 2 count
210 * pixels 4-byte aligned the fills and the palette expansion both
211 * address the buffer as uint32_t
212 */
213 typedef struct
214 {
215 uint16_t hVirtualPixels; /**< full scanline width, guard pixels excluded */
216 uint16_t vVirtualPixels; /**< virtual lines per field */
217 bool interlaced; /**< the host's mode is interlaced */
218 uint8_t interlacedFieldOrder; /**< 0 or 1: XOR'd with the field number */
220
221 /**
222 * \brief generate one display scanline: border fill or active render, the F18A
223 * scanline and blanking registers, the R19 line interrupt, the GPU trigger, the
224 * splash and the palette LUT maintenance
225 *
226 * y: the display line. For an interlaced mode bit 12 carries the field number
227 * and bits 11-0 the line within the field, which is the encoding the host's
228 * VGA layer already uses.
229 * params: the host's display parameters for this line (above).
230 * pixels: the host's scanline buffer, at least hVirtualPixels wide and 4-byte
231 * aligned - see the geometry note on pico9918_scanline_params_t.
232 *
233 * Returns TRUE when the BORDER path was taken. The host needs exactly this and
234 * nothing more: it draws its own overlays after this call, and one of them (the
235 * host's pending-display banner) must appear on border lines only. Returning
236 * the flag is what keeps the border test in one place - a host re-deriving it from
237 * the geometry would be a second copy of the rule, and in row-30 progressive mode
238 * (vBorder == 0) the top rows the banner sits on are ACTIVE, so a host that simply
239 * assumed "low y is border" would paint over the display.
240 *
241 * __time_critical_func: this is the per-scanline path, and it must stay in RAM.
242 * The attribute is on the DEFINITION (a Pico build compiles this module with
243 * copy_to_ram anyway, but the attribute is what pins it if that ever changes).
244 */
247 PICO9918_PIXEL_T* pixels);
248
249 /**
250 * \brief generate one OUTPUT line - the entry for a host that scans out a fixed frame
251 *
252 * `outputLine` runs 0 to the host's output height (480 for VGA) in every mode, so a host
253 * never sees vPixelScale, double rows, the CRT-scanlines dim or the overlay.
254 *
255 * Returns whether `pixels` changed; false means a host's converted copy still stands.
256 * `params->vVirtualPixels` is an output here. Progressive hosts only - an interlaced one
257 * drives pico9918_frame_scanline per field.
258 */
260 bool pico9918_frame_output_line(PICO9918_INST_ARG uint32_t outputLine,
261 pico9918_scanline_params_t* params, PICO9918_PIXEL_T* pixels);
262
263 /**
264 * \brief one pixel from the buffer above as 0x00RRGGBB
265 *
266 * A rendered pixel is the board's: BGR12, four bits a channel, red's nibble
267 * lowest. A host blitting to a 32-bit surface converts through here rather than
268 * unpacking the nibbles itself - OR in whatever alpha its format wants.
269 */
270 static inline uint32_t pico9918_pixel_rgb888(PICO9918_PIXEL_T pixel)
271 {
272 const uint32_t p = (uint32_t)pixel;
273 return ((p & 0x00f) * 0x11u) << 16 | (((p >> 4) & 0x00f) * 0x11u) << 8 | ((p >> 8) & 0x00f) * 0x11u;
274 }
275
276 /**
277 * \brief register the host's late-config-reload hook
278 *
279 * Fires from pico9918_frame_end, once, on the frame that first sees the display
280 * enabled - and only if that happened later than PICO9918_FRAME_STARTUP_FRAMES
281 * frames into the run, i.e. the user sat on the diagnostics screen long enough
282 * for it to have been forced on. Reloading the stored config is what puts the
283 * diagnostics bytes (and everything else) back to what the user actually saved.
284 *
285 * A function pointer rather than a tier-1 op because the host's implementation
286 * is a FLASH read: it cannot be a macro expanded into a library TU, and it is
287 * per-run rather than per-frame - it can fire at most once between console
288 * resets. NULL (the default) means the host has no stored config to reload and
289 * nothing is called.
290 *
291 * A host that reads flash here should know it is called from the per-frame
292 * path, so the read lands inside a frame and collides with XIP.
293 *
294 * Registered per instance in a multi-instance build - see pico9918.h for why the
295 * two builds take different shapes.
296 */
299 void* userdata);
300
301#ifdef __cplusplus
302}
303#endif
304
305#endif // _PICO9918_FRAME_H
pico9918-core - core interface
#define PICO9918_INST_ARG
declare the instance ahead of other parameters
Definition pico9918.h:71
#define PICO9918_INST_ONLY_ARG
declare the instance as the only parameter
Definition pico9918.h:72
#define PICO9918_DLLEXPORT
the linkage every public entry point carries - see LINKAGE MODES above
Definition pico9918.h:41
static uint32_t pico9918_pixel_rgb888(PICO9918_PIXEL_T pixel)
one pixel from the buffer above as 0x00RRGGBB
bool pico9918_frame_output_line(pico9918_t *tms9918, uint32_t outputLine, pico9918_scanline_params_t *params, PICO9918_PIXEL_T *pixels)
generate one OUTPUT line - the entry for a host that scans out a fixed frame
void pico9918_frame_porch(pico9918_t *tms9918)
vertical porch: blank the display and park the scanline counter at the F18A vsync value
bool pico9918_frame_scanline(pico9918_t *tms9918, uint16_t y, const pico9918_scanline_params_t *params, PICO9918_PIXEL_T *pixels)
generate one display scanline: border fill or active render, the F18A scanline and blanking registers...
void pico9918_frame_raise_end_of_frame_int(pico9918_t *tms9918)
raise this frame's end-of-frame interrupt: latch doneInt, set the SR1 vsync bit, trigger the GPU if R...
pico9918_frame_geometry_t pico9918_frame_end(pico9918_t *tms9918, float tempC, float frameRateHz, pico9918_frame_display_t *display)
true end of frame: advance the frame counter, fold in this frame's temperature reading,...
void pico9918_frame_end_of_scanline(pico9918_t *tms9918)
end-of-frame trigger line: if this frame has not raised its interrupt yet, account for the dropped fr...
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
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_frame_geometry_t pico9918_frame_geometry(pico9918_t *tms9918, pico9918_frame_display_t *display)
recompute the vertical display geometry from R0's double-rows bit and R49's row-30 bit,...
pico9918-core - Platform Abstraction
the host's mutable vertical display parameters, as the end-of-frame geometry sees them
int displayPixels
vertical active lines of the host's mode (in)
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 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, guard pixels excluded
uint8_t interlacedFieldOrder
0 or 1: XOR'd with the field number
bool interlaced
the host's mode is interlaced