pico9918-core 1.3.0
TMS9918A / F18A video display processor emulation in C99
Loading...
Searching...
No Matches
pico9918_frame.h File Reference

pico9918-core - frame module More...

#include "pico9918.h"
#include "impl/platform.h"
+ Include dependency graph for pico9918_frame.h:
+ This graph shows which files directly or indirectly include this file:

Go to the source code of this file.

Data Structures

struct  pico9918_frame_display_t
 the host's mutable vertical display parameters, as the end-of-frame geometry sees them More...
 
struct  pico9918_frame_geometry_t
 the vertical geometry the end of frame derives More...
 
struct  pico9918_scanline_params_t
 the host's per-call display parameters, as the scanline sees them More...
 

Macros

#define PICO9918_FRAME_STARTUP_FRAMES   900
 frames of startup grace before an un-enabled display is taken to mean "nothing is driving this VDP", at which point the splash gives way to the diagnostics screen
 

Functions

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_porch (pico9918_t *tms9918)
 vertical porch: blank the display and park the scanline counter at the F18A vsync value
 
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 R50 bit 5 asks for it, consume a pending config change, then merge PICO9918_SR0_INT into the SR0 latch
 
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 frame and raise the interrupt
 
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, and publish it as this module's vPixels / vBorder
 
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, latch the first display enable, refresh the diagnostics panel, raise a still-owed end-of-frame interrupt, and recompute the geometry
 
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, the R19 line interrupt, the GPU trigger, the splash and the palette LUT maintenance
 
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
 
static uint32_t pico9918_pixel_rgb888 (PICO9918_PIXEL_T pixel)
 one pixel from the buffer above as 0x00RRGGBB
 
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
 

Detailed Description

pico9918-core - frame module

Copyright (c) 2021 Troy Schrapel

This code is licensed under the MIT license

https://github.com/visrealm/pico9918-core

The frame module owns per-frame and per-scanline sequencing.

Definition in file pico9918_frame.h.

Macro Definition Documentation

◆ PICO9918_FRAME_STARTUP_FRAMES

#define PICO9918_FRAME_STARTUP_FRAMES   900

frames of startup grace before an un-enabled display is taken to mean "nothing is driving this VDP", at which point the splash gives way to the diagnostics screen

Both halves of that behaviour are inside this module: the end of frame tests it to decide whether the config needs reloading once the display finally comes up, and the scanline's border path tests it to force the PICO9918_CONF_DIAG* bytes on. They must use the SAME number - a reload that fires at a different frame than the forcing did would either restore settings that were never overridden or leave overridden ones in place.

Exported because it is the documented length of the startup grace period, and a host driving frames itself has no other way to know when the diagnostics screen takes over.

Definition at line 183 of file pico9918_frame.h.

Function Documentation

◆ pico9918_frame_update_interrupts()

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

tempStatus: flags raised by the scanline just rendered (PICO9918_SR0_INT / _5S / _COL plus, when 5S is newly set, the sprite number in the low 5 bits)

Runs on every active scanline and at the end-of-frame trigger line. The body runs inside PICO9918_HOST_ENTER/EXIT_CRITICAL because the host's bus-interface handlers mutate the same latch - see the threading contract in impl/platform.h.

merge newly raised status flags into the SR0 latch, publish it, and bring the /INT pin into agreement

See the header for the parameter contract.

The internal sequence is load-bearing at two points and must not be reordered:

  • the three-way merge below is the v1.2.0 semantics, not a simplification of it. Each branch differs in what it lets through and what it preserves;
  • the status publish (PICO9918_HOST_STATUS_VISIBLE) happens BEFORE the pin sync, so a host CPU that takes the interrupt cannot read a stale status.

Definition at line 102 of file pico9918_frame.c.

References pico9918_frame_sync_int_impl(), PICO9918_INST, PICO9918_INST_ONLY, pico9918_set_status_impl(), PICO9918_SR0_5S, PICO9918_SR0_COLLISION, and PICO9918_SR0_INT.

Referenced by pico9918_frame_raise_end_of_frame_int(), and pico9918_frame_scanline().

◆ pico9918_frame_porch()

void pico9918_frame_porch ( pico9918_t *  tms9918)

vertical porch: blank the display and park the scanline counter at the F18A vsync value

Called once per frame, at the start of the vertical porch. Touches only VDP state, so it needs nothing from the host.

vertical porch: blank the display and park the scanline counter at the F18A vsync value

Definition at line 131 of file pico9918_frame.c.

References PICO9918_SR_RASTER_LINE.

◆ pico9918_frame_raise_end_of_frame_int()

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 R50 bit 5 asks for it, consume a pending config change, then merge PICO9918_SR0_INT into the SR0 latch

Unconditional - the caller decides whether the frame still owes an interrupt by testing pico9918_frame_done_int_impl(). Called at the trigger line (from pico9918_frame_end_of_scanline) and again as a fallback at true end of frame, for a frame whose trigger line was never reached.

Exported as part of the frame contract: a host driving frames itself needs the fallback, though every caller in this tree is inside the module.

raise this frame's end-of-frame interrupt: latch doneInt, set the SR1 vsync bit, trigger the GPU if R50 bit 5 asks for it, consume a pending config change, then merge PICO9918_SR0_INT into the SR0 latch

The configDirty consumption is a CHECK-THEN-CLEAR on a non-volatile flag written from the other core (the register path sets it on core 1, the host's config load on core 0) and consumed here. Deliberately left alone: the worst case is a config apply deferred by one frame.

Definition at line 146 of file pico9918_frame.c.

References pico9918_config_apply(), pico9918_diag_config_updated(), pico9918_frame_update_interrupts(), PICO9918_INST, PICO9918_INST_ONLY, PICO9918_R50_GPU_VSYNC, PICO9918_REG_ENHANCED2, PICO9918_SR0_INT, PICO9918_SR1_BLANK, and PICO9918_SR_IDENT.

Referenced by pico9918_frame_end(), and pico9918_frame_end_of_scanline().

◆ pico9918_frame_end_of_scanline()

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 frame and raise the interrupt

Called once per frame, from the trigger scanline the host was told to fire on (the first line past the display region). Does nothing on a frame whose interrupt was already raised.

The VGA callback that reaches this passes the display line; it is not a parameter here because the body never used it.

end-of-frame trigger line: if this frame has not raised its interrupt yet, account for the dropped frame and raise the interrupt

It takes no display line: the body never reads one.

Definition at line 168 of file pico9918_frame.c.

References pico9918_frame_raise_end_of_frame_int(), PICO9918_INST_ONLY, and PICO9918_SR0_INT.

◆ pico9918_frame_geometry()

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, and publish it as this module's vPixels / vBorder

display: the host's mutable vertical parameters, read and - on a progressive build only - written. Under interlace (yScale 1) vPixelScale and vVirtualPixels are NOT touched: the host set them up and owns them.

Returns the derived geometry, including the scanline the host must arm its end-of-frame trigger on. The host applies both: any rewritten display fields and the trigger line are host plumbing (a VGA parameter block and a trigger register here), so the library computes and returns rather than reaching out.

Called once per frame from pico9918_frame_end, and exported so the golden frame surface can drive it directly - it is the surface's candidate path.

recompute the vertical display geometry from R0's double-rows bit and R49's row-30 bit, and publish it as this module's vPixels / vBorder

Three things about this expression are deliberately NOT tidied:

  • the yScale-conditional structure. vPixelScale and vVirtualPixels are rewritten only when yScale > 1, and the vPixels doubling is gated on the SAME condition. Under interlace the host owns the first two and vPixels must NOT double - the two fields already supply the second set of lines;
  • the shift-by-bool algebra (yScale - (bool)doubleRows, << (bool)doubleRows). The golden frame surface's reference deliberately decomposes this into explicit cases so the two do not share the algebra; rewriting it here to look like the reference would destroy that independence;
  • the SIGNED intermediate for the border. SCART NTSC in row-30 mode makes it negative and the narrowing to uint32_t is the shipping behaviour, pinned at geom-scart-ntsc-row30. A known defect, ruled won't-fix: the host's unsigned border test then sends all 220 lines down the border path. Do not "fix" it here.

yScale is derived from interlaced rather than from a build-time DISPLAY_YSCALE. A non-SCART build's DISPLAY_YSCALE is always 2 with interlaced false, so the interlace-derived form covers both cases exactly and the library needs no build-time host macro.

Definition at line 204 of file pico9918_frame.c.

References PICO9918_R49_ROW30, PICO9918_REG_ENHANCED1, TMS_R0_DOUBLE_ROWS, pico9918_frame_geometry_t::triggerScanline, pico9918_frame_geometry_t::vBorder, and pico9918_frame_geometry_t::vPixels.

Referenced by pico9918_frame_end().

◆ pico9918_frame_end()

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, latch the first display enable, refresh the diagnostics panel, raise a still-owed end-of-frame interrupt, and recompute the geometry

tempC: this frame's core temperature in degrees C. The host owns the sensor; the averaging cadence and the SR13 publish are the library's. frameRateHz: the host's display timing, needed by the diagnostics panel. Read only when the panel is enabled, but passed unconditionally - it is a register-resident float on the host side, so a conditional read would cost the host a branch to save nothing. display: as pico9918_frame_geometry, which this calls last.

Returns that geometry, so the host applies the display fields and arms the trigger line exactly once per frame.

The late config reload reaches the host through the tier-2 hook below, NOT through a flag tested on return: it must land at its original point in this sequence, ahead of the diagnostics refresh and the interrupt fallback, both of which read config bytes the reload rewrites.

true end of frame: advance the frame counter, fold in this frame's temperature reading, latch the first display enable, refresh the diagnostics panel, raise a still-owed end-of-frame interrupt, and recompute the geometry

It takes no frame number: the cadence and the thresholds all run off this module's own frame counter, which - unlike the VGA layer's frame number - resets on console reset.

The frame count is re-READ at each use rather than cached in a local. That is not a style choice: the host's tier-1 critical section deliberately does not mask the reset GPIO IRQ, which zeroes the counter, so a console reset landing mid-function is observable and a cached copy would hide it.

Definition at line 241 of file pico9918_frame.c.

References pico9918_diag_set_frame_rate(), pico9918_diag_set_temperature(), pico9918_diag_update(), pico9918_frame_geometry(), pico9918_frame_raise_end_of_frame_int(), PICO9918_FRAME_STARTUP_FRAMES, PICO9918_INST, PICO9918_INST_ONLY, pico9918_splash_allow_hide(), PICO9918_SR_GPU, PICO9918_SR_TEMPERATURE, TMS_R1_DISP_ACTIVE, and pico9918_frame_display_t::vVirtualPixels.

◆ pico9918_frame_scanline()

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, the R19 line interrupt, the GPU trigger, the splash and the palette LUT maintenance

y: the display line. For an interlaced mode bit 12 carries the field number and bits 11-0 the line within the field, which is the encoding the host's VGA layer already uses. params: the host's display parameters for this line (above). pixels: the host's scanline buffer, at least hVirtualPixels wide and 4-byte aligned - see the geometry note on pico9918_scanline_params_t.

Returns TRUE when the BORDER path was taken. The host needs exactly this and nothing more: it draws its own overlays after this call, and one of them (the host's pending-display banner) must appear on border lines only. Returning the flag is what keeps the border test in one place - a host re-deriving it from the geometry would be a second copy of the rule, and in row-30 progressive mode (vBorder == 0) the top rows the banner sits on are ACTIVE, so a host that simply assumed "low y is border" would paint over the display.

__time_critical_func: this is the per-scanline path, and it must stay in RAM. The attribute is on the DEFINITION (a Pico build compiles this module with copy_to_ram anyway, but the attribute is what pins it if that ever changes).

generate one display scanline: border fill or active render, the F18A scanline and blanking registers, the R19 line interrupt, the GPU trigger, the splash and the palette LUT maintenance

The per-scanline path - the function every gate in this project exists to protect.

The host's pending-display banner is deliberately not here. It is host code - host flash state, a host trigger byte, centring against the host's own buffer width - and its only ordering constraints are pixel ones: after the border fill and the splash, before the diagnostics overlay. The host's overlay tail already sits there, and what runs between touches no pixels, so the framebuffer is bit-identical either way.

The return value is the border flag, and the host needs it to place that banner. "y is small" is not the same test: in row-30 progressive mode vBorder is 0, so the rows the banner occupies are active ones and a host that guessed would paint over the display.

FIVE THINGS HERE MUST NOT BE TIDIED:

  • the bg store BEFORE the border WAIT. The previous line's right-border fill may still be reading the source word when this line overwrites it. Benign - the value is the same on all but the frame a background register changes - and "fixing" it by waiting first adds a per-scanline stall the goldens cannot see;
  • the row-30 border test. The TRAP: below is a recorded, user-ruled won't-fix;
  • the SCART-NTSC negative-border underflow. vBorder is unsigned and row-30 on that timing makes it 4294967286, so this test sends all 220 lines down the border path and renders none. Pinned deliberately by the golden frame surface at geom-scart-ntsc-row30. Do not fix it here;
  • the frame count re-READ at each use rather than cached, for the reason pico9918_frame_end above states: the reset GPIO IRQ is not masked and zeroes it;
  • the y -= vBorder in BOTH arms. It looks like it belongs after the branch, but the border arm's own body reads the unadjusted y (the bottom-border scanline register, the palette-regenerate trigger and the splash all do), so hoisting it would change all three.

Definition at line 326 of file pico9918_frame.c.

References pico9918_diag_render(), pico9918_diag_update_render_time(), PICO9918_FRAME_STARTUP_FRAMES, pico9918_frame_update_interrupts(), PICO9918_INST, PICO9918_INST_ONLY, PICO9918_R24_TILE1_PS, PICO9918_R50_GPU_HSYNC, PICO9918_REG_ENHANCED2, PICO9918_REG_HORZ_INT_LINE, PICO9918_REG_PALETTE_SELECT, pico9918_scan_line(), pico9918_splash_render(), PICO9918_SR1_BLANK, PICO9918_SR1_HF, PICO9918_SR_IDENT, PICO9918_SR_RASTER_LINE, and TMS9918_PIXELS_X.

Referenced by pico9918_frame_output_line().

◆ pico9918_frame_output_line()

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

outputLine runs 0 to the host's output height (480 for VGA) in every mode, so a host never sees vPixelScale, double rows, the CRT-scanlines dim or the overlay.

Returns whether pixels changed; false means a host's converted copy still stands. params->vVirtualPixels is an output here. Progressive hosts only - an interlaced one drives pico9918_frame_scanline per field.

generate one OUTPUT line - the entry for a host that scans out a fixed frame

A repeat re-reads the host's buffer rather than re-rendering.

Definition at line 494 of file pico9918_frame.c.

References pico9918_scanline_params_t::hVirtualPixels, PICO9918_CHIP_F18A, pico9918_diag_render(), pico9918_frame_scanline(), PICO9918_INST, PICO9918_INST_ONLY, and pico9918_scanline_params_t::vVirtualPixels.

◆ pico9918_pixel_rgb888()

static uint32_t pico9918_pixel_rgb888 ( PICO9918_PIXEL_T  pixel)
inlinestatic

one pixel from the buffer above as 0x00RRGGBB

A rendered pixel is the board's: BGR12, four bits a channel, red's nibble lowest. A host blitting to a 32-bit surface converts through here rather than unpacking the nibbles itself - OR in whatever alpha its format wants.

Definition at line 270 of file pico9918_frame.h.

◆ pico9918_frame_set_config_reload_callback()

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

Fires from pico9918_frame_end, once, on the frame that first sees the display enabled - and only if that happened later than PICO9918_FRAME_STARTUP_FRAMES frames into the run, i.e. the user sat on the diagnostics screen long enough for it to have been forced on. Reloading the stored config is what puts the diagnostics bytes (and everything else) back to what the user actually saved.

A function pointer rather than a tier-1 op because the host's implementation is a FLASH read: it cannot be a macro expanded into a library TU, and it is per-run rather than per-frame - it can fire at most once between console resets. NULL (the default) means the host has no stored config to reload and nothing is called.

A host that reads flash here should know it is called from the per-frame path, so the read lands inside a frame and collides with XIP.

Registered per instance in a multi-instance build - see pico9918.h for why the two builds take different shapes.