pico9918-core 1.3.0
TMS9918A / F18A video display processor emulation in C99
Loading...
Searching...
No Matches
platform.h
Go to the documentation of this file.
1/**
2 * \file
3 * \brief pico9918-core - Platform Abstraction
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 * Dispatch header. Selects the platform implementation and declares the
12 * platform-independent parts of the contract.
13 *
14 * A host may pre-define any PICO9918_* macro before including the library to
15 * override a desktop default (a real lock, a different pixel type, ...).
16 * The Pico implementation is not overridable - it must emit exact codegen.
17 */
18
19#pragma once
20
21#include <stdint.h>
22
23#ifdef PICO_BUILD
25#else
27#endif
28
29
30/*
31 * PICO9918_INLINE / PICO9918_INLINE_HOT - the library's header-inline linkage.
32 *
33 * BOTH ARE `static inline`, and the static is load-bearing. A non-static C99
34 * `inline` in a header is an inline DEFINITION with no external definition anywhere
35 * in the library: at -O2 every call inlines and nothing is emitted, but at -O0 GCC
36 * emits calls to symbols that do not exist and the link fails. That failure is
37 * invisible on Pico, where the SDK's __force_inline forces every call to inline
38 * regardless. Static is what makes an unoptimized desktop build link, and it is
39 * correct on both platforms.
40 *
41 * TWO forms, because collapsing them into one changes shipping codegen:
42 *
43 * PICO9918_INLINE static inline. Left to the compiler.
44 * PICO9918_INLINE_HOT static inline + always_inline.
45 *
46 * THE SPLIT IS NOT A JUDGEMENT ABOUT HOTNESS. Read HOT as "always_inline is
47 * required here", not as "this one is hot" - and do not add it anywhere new, which
48 * would be a codegen change rather than a cleanup.
49 *
50 * WHERE IT IS LOAD-BEARING, measured rather than assumed:
51 *
52 * - the file-local helpers in pico9918.c are the F18A sprite and tile renderers.
53 * Dropping the attribute out-lines renderSprites and renderEcmTile, adds `bl`
54 * calls inside the per-scanline mode-ops stagers, and dissolves
55 * pico9918_output_sprites into its callers. NONE of that is visible to the asm
56 * gate in tools/capture-baselines.sh - no affected symbol is in its FUNCS list.
57 * - on the *Impl surface it matters to the status-read chain: without it
58 * pico9918_status_read_core and pico9918_status_read_reconcile_impl both grow
59 * the read IRQ handler, while pico9918_read_ahead_data_impl on the same
60 * handler's other arm makes no difference at all. It is NOT predictable from
61 * the call site: measure the one function before dropping an attribute.
62 *
63 * MSVC has no always_inline attribute (__forceinline is a different spelling in a
64 * different position) and the MSVC path is desktop-only, so HOT degrades to that
65 * spelling there - the same treatment the desktop header gives every __attribute__.
66 */
67#define PICO9918_INLINE static inline
68
69#if defined(_MSC_VER) && !defined(__clang__)
70#define PICO9918_INLINE_HOT static __forceinline
71#else
72#define PICO9918_INLINE_HOT static inline __attribute__((always_inline))
73#endif
74
75/*
76 * The rest of the compiler directives the library needs, in both spellings.
77 * Every one is a codegen hint with no bearing on semantics, so where MSVC has
78 * no equivalent the macro is simply empty - MSVC does no type-based alias
79 * analysis, and an alignment assumption it cannot be told is only a missed
80 * optimisation.
81 *
82 * PICO9918_ASSUME_ALIGNED yields the pointer itself there, so every call site
83 * casts the result rather than relying on the builtin's void*.
84 */
85#if defined(_MSC_VER) && !defined(__clang__)
86#define PICO9918_NOINLINE __declspec(noinline)
87#define PICO9918_MAY_ALIAS
88#define PICO9918_ASSUME_ALIGNED(ptr, n) (ptr)
89#define PICO9918_ALIGN(n) __declspec(align(n))
90#else
91#define PICO9918_NOINLINE __attribute__((noinline))
92#define PICO9918_MAY_ALIAS __attribute__((may_alias))
93#define PICO9918_ASSUME_ALIGNED(ptr, n) __builtin_assume_aligned((ptr), (n))
94#define PICO9918_ALIGN(n) __attribute__((aligned(n)))
95#endif
96
97/*
98 * A compile-time assertion a public header can carry. _Static_assert is C11, and MSVC
99 * does not provide it in C++ mode, so a header a C++ host includes cannot use the
100 * keyword directly.
101 */
102#ifdef __cplusplus
103#define PICO9918_STATIC_ASSERT(cond, msg) static_assert(cond, msg)
104#else
105#define PICO9918_STATIC_ASSERT(cond, msg) _Static_assert(cond, msg)
106#endif
107
108/*
109 * Host ops header: how a host supplies its own tier-1 ops.
110 *
111 * A host that owns state a tier-1 op must reach - the PICO9918's PIO read-ahead
112 * push needs `nextValue` and the read SM - names its ops header here, and the
113 * macros expand inside the library TU rather than becoming a call. The firmware
114 * passes -DPICO9918_HOST_OPS_HEADER=... from its root CMakeLists.
115 *
116 * Included AFTER the platform header so a host op can override a platform
117 * default, and before the ops defaults below so those stay the fallback.
118 *
119 * The macros here may reference library state (`tms9918`, TMS_REGISTER,
120 * TMS_STATUS) that is declared later, in pico9918_priv.h - that is fine and
121 * intentional: a macro body binds its names where it EXPANDS, and every
122 * expansion site is inside a TU that has already included Priv.h.
123 */
124#ifdef PICO9918_HOST_OPS_HEADER
125#include PICO9918_HOST_OPS_HEADER
126#endif
127
128/*
129 * Interlock: prove the host's ops header actually arrived.
130 *
131 * Two ways a build can silently lose its tier-1 ops, both demonstrated by review
132 * rather than imagined, and both of which compile and link cleanly:
133 *
134 * 1. The quoted #include above searches the INCLUDING file's directory first, so
135 * a file of the same name sitting next to this header outranks the -I path
136 * that was meant to supply it. A planted decoy defining the ops as no-ops
137 * produced a firmware with no critical section and no read-ahead push, and
138 * every tracked asm baseline still read exactly as expected.
139 * 2. If PICO9918_HOST_OPS_HEADER is simply not passed, "no ops header" is
140 * indistinguishable from "desktop", and the defaults below apply.
141 *
142 * Either way a Pico build loses the IRQ-disable window around the interrupt
143 * latch merge - a rare, hard-to-reproduce corruption with no diagnostic. So a
144 * real ops header must announce itself, and on a Pico build its absence is a
145 * hard error rather than a silent downgrade to no-ops.
146 */
147#if defined(PICO_BUILD) && !defined(PICO9918_HOST_OPS_SUPPLIED)
148#error \
149 "Pico build without host tier-1 ops: PICO9918_HOST_OPS_HEADER was not supplied, or the header that was included is not the host's (a same-named file beside this header shadows it). The ops header must #define PICO9918_HOST_OPS_SUPPLIED 1."
150#endif
151
152/*
153 * Tier-1 host op defaults: the critical section and the status-visible publish.
154 * Both are no-ops unless a host supplies them.
155 *
156 * ENTER/EXIT_CRITICAL guard the frame interrupt/status update against
157 * *same-core IRQ preemption* by the host's bus-interface handlers. The no-op
158 * mapping is correct only for a host where bus-access calls (WriteData /
159 * WriteAddr / ReadStatus / ...) and frame-advance calls are never concurrent. A
160 * multithreaded emulator must serialize those externally or define a real lock
161 * here.
162 *
163 * STATUS_VISIBLE marks the point at which a newly latched status becomes
164 * readable by the host CPU. A polling host needs nothing; a host that pre-loads
165 * a bus response (the PICO9918's PIO read-ahead) does its push here.
166 */
167#ifndef PICO9918_HOST_ENTER_CRITICAL
168#define PICO9918_HOST_ENTER_CRITICAL() ((void)0)
169#endif
170
171#ifndef PICO9918_HOST_EXIT_CRITICAL
172#define PICO9918_HOST_EXIT_CRITICAL() ((void)0)
173#endif
174
175#ifndef PICO9918_HOST_STATUS_VISIBLE
176#define PICO9918_HOST_STATUS_VISIBLE() ((void)0)
177#endif
178
179
180/*
181 * Optional per-scanline capture seams for a host test harness (the PICO9918's
182 * test/live). Undefined by default and compiled out entirely; a host that wants
183 * them defines them in its tier-1 ops header - which is why they are defaulted
184 * HERE, after that header is included, and not in either platform header, where
185 * the default would already have won by the time the host got a say. Never
186 * enable them in a build you intend to take timing readings from: the capture
187 * costs about a microsecond a line, which is what PICO9918_LINE_NOTE_TIME is there
188 * to let the harness record.
189 */
190#ifndef PICO9918_LINE_CAPTURE_WAIT
191#define PICO9918_LINE_CAPTURE_WAIT() ((void)0)
192#endif
193
194#ifndef PICO9918_LINE_CAPTURE
195#define PICO9918_LINE_CAPTURE(y, height, width, indices) ((void)0)
196#endif
197
198#ifndef PICO9918_LINE_NOTE_TIME
199#define PICO9918_LINE_NOTE_TIME(y, us) ((void)0)
200#endif
201
202
203/*
204 * PICO9918_HOST_TIME_US - the library's only wall-clock read.
205 *
206 * Defaulted here rather than guarded inside platform/desktop for two reasons.
207 * It is platform-NEUTRAL: the Pico and desktop platforms both supply a
208 * time_us_32(), so one default covers both and the override point is the same
209 * on either. And it is defaulted AFTER the platform headers, so a host that
210 * substitutes a clock does not have to know which platform it is displacing.
211 *
212 * On a Pico build this expands to the SDK's time_us_32() verbatim - no wrapper,
213 * no indirection, identical codegen to reading it directly.
214 *
215 * Two library surfaces read it, and BOTH must see the same clock:
216 *
217 * - the diagnostics panel's GPU% row, fed from overlay/diag.c, gpu/gpu.c and
218 * pico9918_frame.c, and
219 * - the F18A reset/snap TIMER REGISTERS (pico9918.c), which are device
220 * behaviour a host can read back, not diagnostics.
221 *
222 * That is why the op is whole-library and not overlay-local: a clock injected
223 * for the overlay alone would leave the timer registers on the wall clock, and
224 * a test surface covering them would be nondeterministic for a reason that
225 * looks like a harness bug.
226 *
227 * To substitute a clock, force-include a header defining this macro into every
228 * TU, the library's and the host's. test/golden/goldenClock.h is the worked
229 * example, wired in core/CMakeLists.txt.
230 */
231#ifndef PICO9918_HOST_TIME_US
232#define PICO9918_HOST_TIME_US() time_us_32()
233#endif
234
235
236/*
237 * PICO9918_RGB12_FROM_RGB333 - widen a 3-bit-per-channel value to RGB444.
238 *
239 * Bit replication `(v << 1) | (v >> 2)` maps 0..7 onto 0..15 with the endpoints
240 * exact (7 -> 15); a plain `v << 1` would cap full intensity at 14 and dim the
241 * whole palette. Used by V9938 palette-port writes so that RGB444 (0x0RGB)
242 * stays the library's single canonical colour format and the palette rebuild
243 * path needs no per-base variant.
244 */
245#define PICO9918_RGB333_CH(v) (((v) << 1) | ((v) >> 2))
246
247#define PICO9918_RGB12_FROM_RGB333(v) \
248 ((uint16_t)((PICO9918_RGB333_CH(((v) >> 6) & 0x07) << 8) | (PICO9918_RGB333_CH(((v) >> 3) & 0x07) << 4) | \
249 (PICO9918_RGB333_CH(((v)) & 0x07))))
250
251
252/*
253 * Palette LUT build class.
254 *
255 * The indexed scanline buffer is always a 256-byte stream consumed through a
256 * 256-entry LUT; only the *meaning* of a byte changes per mode. That makes this
257 * a three-way class resolved once per palette rebuild - never per pixel - and
258 * not a doubled/undoubled boolean.
259 *
260 * PICO9918_LUT_DOUBLED - every entry is the same pixel twice (all doubled modes)
261 * PICO9918_LUT_PAIRED - each byte is two adjacent 4-bit indexes (TEXT80 today;
262 * V9938 G5/G6 later)
263 * PICO9918_LUT_DIRECT - the byte is the colour itself, via a fixed conversion
264 * table rather than a palette (V9938 G7)
265 */
266typedef enum
267{
268 PICO9918_LUT_DOUBLED = 0,
269 PICO9918_LUT_PAIRED,
270 PICO9918_LUT_DIRECT
271} pico9918_lut_class_t;
pico9918-core - Platform Abstraction (RP2040 / RP2350)
pico9918-core - Platform Abstraction (portable C)