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#else
90#define PICO9918_NOINLINE __attribute__((noinline))
91#define PICO9918_MAY_ALIAS __attribute__((may_alias))
92#define PICO9918_ASSUME_ALIGNED(ptr, n) __builtin_assume_aligned((ptr), (n))
93#endif
94
95/*
96 * A compile-time assertion a public header can carry. _Static_assert is C11, and MSVC
97 * does not provide it in C++ mode, so a header a C++ host includes cannot use the
98 * keyword directly.
99 */
100#ifdef __cplusplus
101#define PICO9918_STATIC_ASSERT(cond, msg) static_assert(cond, msg)
102#else
103#define PICO9918_STATIC_ASSERT(cond, msg) _Static_assert(cond, msg)
104#endif
105
106/*
107 * Host ops header: how a host supplies its own tier-1 ops.
108 *
109 * A host that owns state a tier-1 op must reach - the PICO9918's PIO read-ahead
110 * push needs `nextValue` and the read SM - names its ops header here, and the
111 * macros expand inside the library TU rather than becoming a call. The firmware
112 * passes -DPICO9918_HOST_OPS_HEADER=... from its root CMakeLists.
113 *
114 * Included AFTER the platform header so a host op can override a platform
115 * default, and before the ops defaults below so those stay the fallback.
116 *
117 * The macros here may reference library state (`tms9918`, TMS_REGISTER,
118 * TMS_STATUS) that is declared later, in pico9918_priv.h - that is fine and
119 * intentional: a macro body binds its names where it EXPANDS, and every
120 * expansion site is inside a TU that has already included Priv.h.
121 */
122#ifdef PICO9918_HOST_OPS_HEADER
123#include PICO9918_HOST_OPS_HEADER
124#endif
125
126/*
127 * Interlock: prove the host's ops header actually arrived.
128 *
129 * Two ways a build can silently lose its tier-1 ops, both demonstrated by review
130 * rather than imagined, and both of which compile and link cleanly:
131 *
132 * 1. The quoted #include above searches the INCLUDING file's directory first, so
133 * a file of the same name sitting next to this header outranks the -I path
134 * that was meant to supply it. A planted decoy defining the ops as no-ops
135 * produced a firmware with no critical section and no read-ahead push, and
136 * every tracked asm baseline still read exactly as expected.
137 * 2. If PICO9918_HOST_OPS_HEADER is simply not passed, "no ops header" is
138 * indistinguishable from "desktop", and the defaults below apply.
139 *
140 * Either way a Pico build loses the IRQ-disable window around the interrupt
141 * latch merge - a rare, hard-to-reproduce corruption with no diagnostic. So a
142 * real ops header must announce itself, and on a Pico build its absence is a
143 * hard error rather than a silent downgrade to no-ops.
144 */
145#if defined(PICO_BUILD) && !defined(PICO9918_HOST_OPS_SUPPLIED)
146#error \
147 "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."
148#endif
149
150/*
151 * Tier-1 host op defaults: the critical section and the status-visible publish.
152 * Both are no-ops unless a host supplies them.
153 *
154 * ENTER/EXIT_CRITICAL guard the frame interrupt/status update against
155 * *same-core IRQ preemption* by the host's bus-interface handlers. The no-op
156 * mapping is correct only for a host where bus-access calls (WriteData /
157 * WriteAddr / ReadStatus / ...) and frame-advance calls are never concurrent. A
158 * multithreaded emulator must serialize those externally or define a real lock
159 * here.
160 *
161 * STATUS_VISIBLE marks the point at which a newly latched status becomes
162 * readable by the host CPU. A polling host needs nothing; a host that pre-loads
163 * a bus response (the PICO9918's PIO read-ahead) does its push here.
164 */
165#ifndef PICO9918_HOST_ENTER_CRITICAL
166#define PICO9918_HOST_ENTER_CRITICAL() ((void)0)
167#endif
168
169#ifndef PICO9918_HOST_EXIT_CRITICAL
170#define PICO9918_HOST_EXIT_CRITICAL() ((void)0)
171#endif
172
173#ifndef PICO9918_HOST_STATUS_VISIBLE
174#define PICO9918_HOST_STATUS_VISIBLE() ((void)0)
175#endif
176
177
178/*
179 * Optional per-scanline capture seams for a host test harness (the PICO9918's
180 * test/live). Undefined by default and compiled out entirely; a host that wants
181 * them defines them in its tier-1 ops header - which is why they are defaulted
182 * HERE, after that header is included, and not in either platform header, where
183 * the default would already have won by the time the host got a say. Never
184 * enable them in a build you intend to take timing readings from: the capture
185 * costs about a microsecond a line, which is what PICO9918_LINE_NOTE_TIME is there
186 * to let the harness record.
187 */
188#ifndef PICO9918_LINE_CAPTURE_WAIT
189#define PICO9918_LINE_CAPTURE_WAIT() ((void)0)
190#endif
191
192#ifndef PICO9918_LINE_CAPTURE
193#define PICO9918_LINE_CAPTURE(y, height, width, indices) ((void)0)
194#endif
195
196#ifndef PICO9918_LINE_NOTE_TIME
197#define PICO9918_LINE_NOTE_TIME(y, us) ((void)0)
198#endif
199
200
201/*
202 * PICO9918_HOST_TIME_US - the library's only wall-clock read.
203 *
204 * Defaulted here rather than guarded inside platform/desktop for two reasons.
205 * It is platform-NEUTRAL: the Pico and desktop platforms both supply a
206 * time_us_32(), so one default covers both and the override point is the same
207 * on either. And it is defaulted AFTER the platform headers, so a host that
208 * substitutes a clock does not have to know which platform it is displacing.
209 *
210 * On a Pico build this expands to the SDK's time_us_32() verbatim - no wrapper,
211 * no indirection, identical codegen to reading it directly.
212 *
213 * Two library surfaces read it, and BOTH must see the same clock:
214 *
215 * - the diagnostics panel's GPU% row, fed from overlay/diag.c, gpu/gpu.c and
216 * pico9918_frame.c, and
217 * - the F18A reset/snap TIMER REGISTERS (pico9918.c), which are device
218 * behaviour a host can read back, not diagnostics.
219 *
220 * That is why the op is whole-library and not overlay-local: a clock injected
221 * for the overlay alone would leave the timer registers on the wall clock, and
222 * a test surface covering them would be nondeterministic for a reason that
223 * looks like a harness bug. See test/golden/goldenClock.h.
224 */
225#ifndef PICO9918_HOST_TIME_US
226#define PICO9918_HOST_TIME_US() time_us_32()
227#endif
228
229
230/*
231 * PICO9918_RGB12_FROM_RGB333 - widen a 3-bit-per-channel value to RGB444.
232 *
233 * Bit replication `(v << 1) | (v >> 2)` maps 0..7 onto 0..15 with the endpoints
234 * exact (7 -> 15); a plain `v << 1` would cap full intensity at 14 and dim the
235 * whole palette. Used by V9938 palette-port writes so that RGB444 (0x0RGB)
236 * stays the library's single canonical colour format and the palette rebuild
237 * path needs no per-base variant.
238 */
239#define PICO9918_RGB333_CH(v) (((v) << 1) | ((v) >> 2))
240
241#define PICO9918_RGB12_FROM_RGB333(v) \
242 ((uint16_t)((PICO9918_RGB333_CH(((v) >> 6) & 0x07) << 8) | (PICO9918_RGB333_CH(((v) >> 3) & 0x07) << 4) | \
243 (PICO9918_RGB333_CH(((v)) & 0x07))))
244
245
246/*
247 * Palette LUT build class.
248 *
249 * The indexed scanline buffer is always a 256-byte stream consumed through a
250 * 256-entry LUT; only the *meaning* of a byte changes per mode. That makes this
251 * a three-way class resolved once per palette rebuild - never per pixel - and
252 * not a doubled/undoubled boolean.
253 *
254 * PICO9918_LUT_DOUBLED - every entry is the same pixel twice (all doubled modes)
255 * PICO9918_LUT_PAIRED - each byte is two adjacent 4-bit indexes (TEXT80 today;
256 * V9938 G5/G6 later)
257 * PICO9918_LUT_DIRECT - the byte is the colour itself, via a fixed conversion
258 * table rather than a palette (V9938 G7)
259 */
260typedef enum
261{
262 PICO9918_LUT_DOUBLED = 0,
263 PICO9918_LUT_PAIRED,
264 PICO9918_LUT_DIRECT
265} pico9918_lut_class_t;
pico9918-core - Platform Abstraction (RP2040 / RP2350)
pico9918-core - Platform Abstraction (portable C)