pico9918-core 1.3.0
TMS9918A / F18A video display processor emulation in C99
Loading...
Searching...
No Matches
pico9918_config.c
Go to the documentation of this file.
1/**
2 * \file
3 * \brief pico9918-core - Config semantics
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 * Portable half of the config block: the field descriptor table, the
12 * validation / defaults / migration driver, the pending mirror refresh, and
13 * the VDP-side apply. Host storage (flash), host detection (SCART, hardware
14 * version) and host-side apply effects (VGA scanlines) live in the host.
15 */
16
17#include "impl/pico9918_priv.h"
18
19#include <string.h>
20
21const pico9918_config_field_t pico9918_config_fields[] = {
22 {PICO9918_CONF_CRT_SCANLINES, 1, 0, PICO9918_PENDING_MIRROR_NONE, 0x1000},
23 {PICO9918_CONF_SCANLINE_SPRITES, 3, 0, PICO9918_PENDING_MIRROR_NONE, 0x1000},
24 {PICO9918_CONF_CLOCK_PRESET_ID, 2, 0, PICO9918_CONF_PENDING_CLOCK_PRESET, 0x1000},
25 {PICO9918_CONF_SCART_MODE, 1, 0, PICO9918_CONF_PENDING_SCART_MODE, 0x1200},
26 {PICO9918_CONF_VDP_DEVICE, 3, 0, PICO9918_PENDING_MIRROR_NONE, 0x1101},
27 {PICO9918_CONF_DISP_DRIVER_PREF, 2, 0, PICO9918_CONF_PENDING_DRIVER_PREF, 0x1200}, // 1.2.0
28 {PICO9918_CONF_VGA_MODE, 0, 0, PICO9918_CONF_PENDING_VGA_MODE, 0x1200}, // 0=480p60 (only)
29 {PICO9918_CONF_DIAG_REGISTERS, 1, 0, PICO9918_PENDING_MIRROR_NONE, 0x1000},
30 {PICO9918_CONF_DIAG_PERFORMANCE, 1, 0, PICO9918_PENDING_MIRROR_NONE, 0x1000},
31 {PICO9918_CONF_DIAG_PALETTE, 1, 0, PICO9918_PENDING_MIRROR_NONE, 0x1000},
32 {PICO9918_CONF_DIAG_ADDRESS, 1, 0, PICO9918_PENDING_MIRROR_NONE, 0x1000},
33 // WARNING: VR58/59 lets a host write any byte >= 8, so shipped units may carry residue in byte 15.
34 // migrateNewFields compares strictly, so the stamp must be the release that first CLAIMS the byte:
35 // an earlier one leaves a unit already on that version unswept, booting on residue.
36 {PICO9918_CONF_VDP_BASE, 1, PICO9918_BASE_TMS9918, PICO9918_PENDING_MIRROR_NONE, 0x1300},
37};
38
39const size_t pico9918_config_field_count = sizeof(pico9918_config_fields) / sizeof(pico9918_config_fields[0]);
40
42{
43 config[PICO9918_CONF_PENDING_STATE] = state;
44 for (size_t i = 0; i < pico9918_config_field_count; ++i)
45 {
46 if (pico9918_config_fields[i].pendingMirror == PICO9918_PENDING_MIRROR_NONE) continue;
47 config[pico9918_config_fields[i].pendingMirror] = config[pico9918_config_fields[i].offset];
48 }
49}
50
51void pico9918_config_pending_capture(const uint8_t config[PICO9918_CONFIG_BYTES], uint8_t* record)
52{
53 for (size_t i = 0; i < pico9918_config_field_count; ++i)
54 {
55 if (pico9918_config_fields[i].pendingMirror == PICO9918_PENDING_MIRROR_NONE) continue;
56 record[pico9918_config_fields[i].pendingMirror - PICO9918_CONF_PENDING_STATE] =
57 config[pico9918_config_fields[i].offset];
58 }
59}
60
61void pico9918_config_pending_restore(uint8_t config[PICO9918_CONFIG_BYTES], const uint8_t* record)
62{
63 for (size_t i = 0; i < pico9918_config_field_count; ++i)
64 {
65 if (pico9918_config_fields[i].pendingMirror == PICO9918_PENDING_MIRROR_NONE) continue;
66 config[pico9918_config_fields[i].offset] =
67 record[pico9918_config_fields[i].pendingMirror - PICO9918_CONF_PENDING_STATE];
68 }
69}
70
71PICO9918_INLINE_HOT uint16_t configStoredVersion(const uint8_t* config)
72{
73 return ((uint16_t)config[PICO9918_CONF_SW_VERSION] << 8) | config[PICO9918_CONF_SW_PATCH_VERSION];
74}
75
76/* This build's version in the packing the block stores, so one comparison serves both the
77 stored version and the field table's introducedIn. */
78#define CONFIG_RUNNING_VERSION (((uint16_t)PICO9918_BUILD_SW_VERSION << 8) | PICO9918_BUILD_SW_PATCH)
79
80/* Board revision major 2 and up is the PRO tier, which is the RP2350 - see the encoding
81 pico9918_config_validate() documents. */
82static uint8_t configPicoModel(uint8_t hwVersion)
83{
84 return hwVersion >= 0x20 ? PICO9918_MODEL_RP2350 : PICO9918_MODEL_RP2040;
85}
86
87static bool configOutOfRange(const uint8_t* config)
88{
89 for (size_t i = 0; i < pico9918_config_field_count; ++i)
90 {
91 if (config[pico9918_config_fields[i].offset] > pico9918_config_fields[i].max) return true;
92 }
93 return false;
94}
95
97{
98 memset(config, 0, PICO9918_CONFIG_BYTES);
99
100 for (size_t i = 0; i < pico9918_config_field_count; ++i)
101 {
102 config[pico9918_config_fields[i].offset] = pico9918_config_fields[i].defaultValue;
103 }
104
105 /* entry 0 stays zero; the rest carry the 0xf alpha the validator reads as "initialised" */
106 for (int i = 1; i < 16; ++i)
107 {
108 uint16_t rgb = 0xf000 | pico9918_default_palette(i);
109 config[PICO9918_CONF_PALETTE_IDX_0 + (i * 2)] = rgb >> 8;
110 config[PICO9918_CONF_PALETTE_IDX_0 + (i * 2) + 1] = rgb & 0xff;
111 }
112}
113
114
115// apply defaults only for fields introduced after storedVer
116static void migrateNewFields(uint8_t* config, uint16_t storedVer)
117{
118 for (size_t i = 0; i < pico9918_config_field_count; ++i)
119 {
120 if (pico9918_config_fields[i].introducedIn > storedVer)
121 {
122 config[pico9918_config_fields[i].offset] = pico9918_config_fields[i].defaultValue;
123 }
124 }
125}
126
127bool pico9918_config_validate(uint8_t config[PICO9918_CONFIG_BYTES], uint8_t hwVersion)
128{
129 const uint8_t picoModel = configPicoModel(hwVersion);
130 uint16_t storedVer = configStoredVersion(config);
131
132 if (config[PICO9918_CONF_PALETTE_IDX_0] != 0x00 ||
133 (config[PICO9918_CONF_PALETTE_IDX_0 + 2] & 0xf0) != 0xf0 || // not initialised
134 configOutOfRange(config))
135 {
137
138 storedVer = 0; // a defaulted block stamps and saves like a version change
139 }
140
141 config[PICO9918_CONF_PICO_MODEL] = picoModel;
142 config[PICO9918_CONF_HW_VERSION] = hwVersion;
143
144 // the host persists all 256 bytes; clear command bytes read back from storage
145 config[PICO9918_CONF_SAVE_FORCED] = 0;
146 config[PICO9918_CONF_PENDING_CANCEL] = 0;
147 config[PICO9918_CONF_PENDING_CONFIRM] = 0;
148 config[PICO9918_CONF_SAVE_TO_FLASH] = 0;
149
150 if (storedVer == CONFIG_RUNNING_VERSION) return false;
151
152 migrateNewFields(config, storedVer);
153
154 config[PICO9918_CONF_SW_VERSION] = PICO9918_BUILD_SW_VERSION;
155 config[PICO9918_CONF_SW_PATCH_VERSION] = PICO9918_BUILD_SW_PATCH;
156
157 /* forced, not pending-split: a migration is not a display change the user chose */
158 config[PICO9918_CONF_SAVE_FORCED] = 1;
159 return true;
160}
161
162void pico9918_config_prepare_save(uint8_t config[PICO9918_CONFIG_BYTES], uint8_t hwVersion)
163{
164 config[PICO9918_CONF_PICO_MODEL] = configPicoModel(hwVersion);
165 config[PICO9918_CONF_HW_VERSION] = hwVersion;
166 config[PICO9918_CONF_SW_VERSION] = PICO9918_BUILD_SW_VERSION;
167 config[PICO9918_CONF_SW_PATCH_VERSION] = PICO9918_BUILD_SW_PATCH;
168
169 /* a command is a request to the run that made it, never a stored setting */
170 config[PICO9918_CONF_SAVE_FORCED] = 0;
171 config[PICO9918_CONF_PENDING_CANCEL] = 0;
172 config[PICO9918_CONF_PENDING_CONFIRM] = 0;
173 config[PICO9918_CONF_SAVE_TO_FLASH] = 0;
174
175 /* the initialised marker: entry 0 always 0, the rest carrying alpha 0xf */
176 config[PICO9918_CONF_PALETTE_IDX_0] = 0;
177 config[PICO9918_CONF_PALETTE_IDX_0 + 1] = 0;
178 for (int i = 1; i < 16; ++i)
179 {
180 config[PICO9918_CONF_PALETTE_IDX_0 + (i * 2)] |= 0xf0;
181 }
182}
183
185{
186 return tms9918->config;
187}
188
189/* host config-applied hook - see the header for the contract, and pico9918.h for why only
190 the storage differs between the two builds */
191#if PICO9918_SINGLE_INSTANCE
192static struct
193{
194 pico9918_config_applied_fn fn;
195 void* userdata;
196} configApplied;
197#define CONFIG_APPLIED_CB configApplied
198#else
199#define CONFIG_APPLIED_CB tms9918->configApplied
200#endif
201
202void pico9918_config_set_applied_callback(PICO9918_INST_ARG pico9918_config_applied_fn cb, void* userdata)
203{
204 CONFIG_APPLIED_CB.fn = cb;
205 CONFIG_APPLIED_CB.userdata = userdata;
206}
207
208/* `tms9918` names the parameter in one build and the global instance in the other, so the
209 callback is handed its instance either way */
210static inline void configAppliedFire(PICO9918_INST_ONLY_ARG)
211{
212 if (CONFIG_APPLIED_CB.fn) CONFIG_APPLIED_CB.fn(tms9918, CONFIG_APPLIED_CB.userdata);
213}
214
216{
217 /* the overlays are the personality's: panels nothing will draw are not a summary */
218 tms9918->config[PICO9918_CONF_DIAG] =
219 PICO9918_HAS(tms9918, PICO9918_FEAT_OVERLAY) &&
220 (tms9918->config[PICO9918_CONF_DIAG_ADDRESS] || tms9918->config[PICO9918_CONF_DIAG_PALETTE] ||
221 tms9918->config[PICO9918_CONF_DIAG_PERFORMANCE] || tms9918->config[PICO9918_CONF_DIAG_REGISTERS]);
222
223 if (!PICO9918_HAS(tms9918, PICO9918_FEAT_CONFIG))
224 {
225 /* an F18A's registers, palette and render base are not a settings block's */
226 tms9918->configVdpDirty = false;
227 tms9918->vdpBase = PICO9918_BASE_TMS9918;
228 }
229 else
230 {
231 if (tms9918->configVdpDirty)
232 {
233 tms9918->configVdpDirty = false;
234
235 if (tms9918->config[PICO9918_CONF_CRT_SCANLINES])
236 TMS_REGISTER(tms9918, PICO9918_REG_ENHANCED2) |= PICO9918_R50_VSCANLINES;
237 else
238 TMS_REGISTER(tms9918, PICO9918_REG_ENHANCED2) &= (uint8_t)~PICO9918_R50_VSCANLINES;
239
240 TMS_REGISTER(tms9918, PICO9918_REG_MAX_SCAN_SPRITES) =
241 1 << (tms9918->config[PICO9918_CONF_SCANLINE_SPRITES] + 2);
242
243 for (int i = 0; i < 16; ++i)
244 {
245 uint16_t rgb = (tms9918->config[PICO9918_CONF_PALETTE_IDX_0 + (i * 2)] << 8) |
246 tms9918->config[PICO9918_CONF_PALETTE_IDX_0 + (i * 2) + 1];
247 tms9918->vram.map.pram[i] = __builtin_bswap16(rgb);
248 }
249 tms9918->palDirty = 1;
250 }
251
252 tms9918->vdpBase = (tms9918->config[PICO9918_CONF_VDP_BASE] == PICO9918_BASE_V9938)
255 }
256
257 /* last, so the host derives its effects from registers this call may just have seeded */
258 configAppliedFire(PICO9918_INST_ONLY);
259}
260
262{
263 tms9918->configDirty = true;
264 if (applyVdpEffects) tms9918->configVdpDirty = true;
265}
266
268{
269 if (applyVdpEffects) tms9918->configVdpDirty = true;
271
272 /* the boundary must not apply the same block again */
273 tms9918->configDirty = false;
274}
uint16_t pico9918_default_palette(int index)
a default palette entry, 0xargb
Definition pico9918.c:3624
#define PICO9918_R50_VSCANLINES
F18A only: dim every second raster line.
Definition pico9918.h:374
#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
@ PICO9918_REG_ENHANCED2
GPU triggers, per-position attributes, layer priority.
Definition pico9918.h:251
@ PICO9918_REG_MAX_SCAN_SPRITES
sprites drawn per scanline before the limit bites
Definition pico9918.h:241
#define PICO9918_INST_ONLY_ARG
declare the instance as the only parameter
Definition pico9918.h:84
void pico9918_config_pending_capture(const uint8_t config[PICO9918_CONFIG_BYTES], uint8_t *record)
copy the live tracked fields into a PICO9918_PENDING_RECORD_BYTES record
void pico9918_config_apply(pico9918_t *tms9918)
apply the config block's VDP-side effects: registers 50 and 30, the palette unpack,...
uint8_t * pico9918_config(pico9918_t *tms9918)
the instance's PICO9918_CONFIG_BYTES settings block
void pico9918_config_schedule_apply(pico9918_t *tms9918, bool applyVdpEffects)
ask for the block to be applied at the next end of frame
void pico9918_config_defaults(uint8_t config[PICO9918_CONFIG_BYTES])
write a complete, valid settings block: every field at its default
void pico9918_config_set_applied_callback(pico9918_t *tms9918, pico9918_config_applied_fn cb, void *userdata)
register the host's config-applied hook
bool pico9918_config_validate(uint8_t config[PICO9918_CONFIG_BYTES], uint8_t hwVersion)
validate a config block just read from host storage, and stamp its identity
void pico9918_config_refresh_pending_mirror(uint8_t config[PICO9918_CONFIG_BYTES], uint8_t state)
copy live tracked fields into the in-RAM pending mirror with the given state
void pico9918_config_prepare_save(uint8_t config[PICO9918_CONFIG_BYTES], uint8_t hwVersion)
stamp the identity and the initialised marker into a block about to be persisted
void pico9918_config_apply_now(pico9918_t *tms9918, bool applyVdpEffects)
apply the block now, and cancel any apply already owed
void pico9918_config_pending_restore(uint8_t config[PICO9918_CONFIG_BYTES], const uint8_t *record)
copy a pending record's field slots back over the live config
#define PICO9918_BASE_V9938
the V9938 base
#define PICO9918_PENDING_MIRROR_NONE
pendingMirror value for a field outside the confirmation flow
#define PICO9918_BASE_TMS9918
vdpBase values - the render base selected by PICO9918_CONF_VDP_BASE
#define PICO9918_CONFIG_BYTES
size of the config block, in bytes
pico9918-core - the private instance layout
one config field's descriptor
uint8_t offset
the field's config byte index
uint8_t defaultValue
what a reset or a migration writes
uint8_t pendingMirror
PICO9918_CONF_PENDING_* offset, or PICO9918_PENDING_MIRROR_NONE.