@geastack/core 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +36 -0
  3. package/app_frame_perf.cpp +77 -0
  4. package/css.d.ts +1 -0
  5. package/events.cpp +67 -0
  6. package/gea_app_entry.cpp +317 -0
  7. package/gea_sources.mjs +126 -0
  8. package/gea_sources.sh +36 -0
  9. package/include/app.h +60 -0
  10. package/include/apps.h +46 -0
  11. package/include/audio.h +90 -0
  12. package/include/ble.h +212 -0
  13. package/include/bluetooth/hid_profile.h +92 -0
  14. package/include/camera.h +107 -0
  15. package/include/canvas.h +290 -0
  16. package/include/display.h +390 -0
  17. package/include/display_present.h +869 -0
  18. package/include/event.h +32 -0
  19. package/include/events.h +116 -0
  20. package/include/gea/embedded.h +13 -0
  21. package/include/gea/reactive_runtime.h +32 -0
  22. package/include/gea.h +14 -0
  23. package/include/gea_perf_config.h +96 -0
  24. package/include/geaos/abi.h +165 -0
  25. package/include/geolocation.h +126 -0
  26. package/include/gpio.h +61 -0
  27. package/include/graphics/font.h +122 -0
  28. package/include/i2c.h +20 -0
  29. package/include/image.h +156 -0
  30. package/include/imu.h +23 -0
  31. package/include/input.h +33 -0
  32. package/include/memory.h +52 -0
  33. package/include/pixel.h +907 -0
  34. package/include/platform/file_cache.h +18 -0
  35. package/include/power.h +11 -0
  36. package/include/runtime.h +83 -0
  37. package/include/services/app_runner.h +10 -0
  38. package/include/services/app_state.h +14 -0
  39. package/include/services/battery_service.h +12 -0
  40. package/include/services/bluetooth_service.h +13 -0
  41. package/include/services/diagnostics.h +76 -0
  42. package/include/services/diagnostics_platform.h +54 -0
  43. package/include/services/frame_scheduler.h +71 -0
  44. package/include/services/mirror.h +49 -0
  45. package/include/services/network_services.h +25 -0
  46. package/include/services/ota.h +32 -0
  47. package/include/services/runtime_log.h +11 -0
  48. package/include/services/storage_service.h +26 -0
  49. package/include/touch.h +38 -0
  50. package/include/ui/signal.h +149 -0
  51. package/include/wifi.h +67 -0
  52. package/index.d.ts +1940 -0
  53. package/jsx.d.ts +1 -0
  54. package/package.json +70 -0
  55. package/runtime/ble.ts +49 -0
  56. package/runtime/color.ts +21 -0
  57. package/runtime/compiler.ts +43 -0
  58. package/runtime/host.ts +1083 -0
  59. package/runtime/images.ts +129 -0
  60. package/runtime/primitives.ts +49 -0
  61. package/runtime-surface.ts +180 -0
  62. package/runtime.cpp +431 -0
  63. package/runtime.ts +34 -0
  64. package/scripts/build-gea-vite-geatsc.mjs +4514 -0
  65. package/scripts/dotenv-defines.mjs +98 -0
  66. package/scripts/gea-bundle-type-hints.mjs +546 -0
  67. package/scripts/gea-embedded-compat-staging.mjs +17 -0
  68. package/scripts/gea-embedded-compat-transform.mjs +427 -0
  69. package/scripts/gea-vite-module-graph-plugin.mjs +578 -0
  70. package/scripts/generate-gea-embedded-assets.mjs +296 -0
  71. package/scripts/generate-gea-embedded-fonts.mjs +1252 -0
  72. package/scripts/generate-installed-app-plan.mjs +55 -0
  73. package/scripts/native-object-cache.sh +244 -0
@@ -0,0 +1,907 @@
1
+ #pragma once
2
+
3
+ #include <algorithm>
4
+ #include <cstddef>
5
+ #include <cstdint>
6
+ #include <cstring>
7
+
8
+ // ESP32-S3 PIE (Processor Instruction Extensions): 128-bit vector stores for
9
+ // the RGB565 span-fill hot path. Guarded on the IDF target so every other
10
+ // target (wasm, apple, geaos, host tools) compiles the portable path.
11
+ #if defined(__XTENSA__) && __has_include("sdkconfig.h")
12
+ #include "sdkconfig.h"
13
+ #endif
14
+ #if defined(CONFIG_IDF_TARGET_ESP32S3)
15
+ #define GEA_PIXEL_PIE_FILL16 1
16
+ #else
17
+ #define GEA_PIXEL_PIE_FILL16 0
18
+ #endif
19
+
20
+ #ifndef GEA_EMBEDDED_PIXEL_PANEL_ENDIAN
21
+ #define GEA_EMBEDDED_PIXEL_PANEL_ENDIAN 0
22
+ #endif
23
+
24
+ // Per-target native framebuffer pixel format, fixed at COMPILE TIME. A target
25
+ // has exactly one format, so this is a build directive — never a runtime branch.
26
+ // esp32 / geaos panels are RGB565 (16-bit); iOS is RGBA8888 (32-bit, full
27
+ // colour); Android is ARGB8888 to match Bitmap.Config.ARGB_8888. Each target compiles to one native pixel type + native pixel ops via
28
+ // the `native_t` typedef + `#if` below — no per-pixel colour conversion.
29
+ #define GEA_PIXEL_RGB565 0
30
+ #define GEA_PIXEL_RGBA8888 1
31
+ #define GEA_PIXEL_ARGB8888 2
32
+ // GRAY4: 16-level grayscale, ONE nibble (4 bits) per pixel. For panels that
33
+ // display exactly 16 gray levels (e.g. the M5Paper IT8951 in GL16 mode). The
34
+ // pixel VALUE type is a byte holding 0..15, so every colour/blend helper is a
35
+ // plain scalar op; the 4-bit STORAGE packing (2 pixels per byte) lives entirely
36
+ // in the Canvas framebuffer accessors. This is what lets a grayscale target
37
+ // render straight into the panel's own format — no RGB565 buffer, no per-frame
38
+ // repack, quarter-size framebuffer. Additive + compile-time gated: RGB565/8888
39
+ // targets never compile any GRAY4 branch, so their output is unchanged.
40
+ #define GEA_PIXEL_GRAY4 3
41
+ // GRAY2: 4-level grayscale, TWO bits per pixel (4 px/byte). Same idea as GRAY4,
42
+ // one step further down: for panels that can only show four levels anyway (the
43
+ // SSD1677's dual RAM planes give exactly black / dark / light / white) on parts
44
+ // with no PSRAM. A 528x792 framebuffer costs 102 KB at 2bpp against 204 KB at
45
+ // 4bpp — the difference between fitting and not fitting in an ESP32-C3's ~380 KB
46
+ // of SRAM. Like GRAY4 the pixel VALUE type is a byte (0..3); only the
47
+ // framebuffer STORAGE is packed.
48
+ #define GEA_PIXEL_GRAY2 4
49
+ #ifndef GEA_EMBEDDED_PIXEL_FORMAT
50
+ #define GEA_EMBEDDED_PIXEL_FORMAT GEA_PIXEL_RGB565
51
+ #endif
52
+ #define GEA_PIXEL_FORMAT_IS_8888 \
53
+ (GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_RGBA8888 || GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_ARGB8888)
54
+ #define GEA_PIXEL_FORMAT_IS_GRAY4 (GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_GRAY4)
55
+ #define GEA_PIXEL_FORMAT_IS_GRAY2 (GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_GRAY2)
56
+ // "A grayscale-level format" — the pixel value is a small integer level in a
57
+ // single byte rather than packed colour channels. Everything that treats GRAY4
58
+ // as "not an RGB565 pack and not an 8888" applies verbatim to GRAY2.
59
+ #define GEA_PIXEL_FORMAT_IS_GRAY (GEA_PIXEL_FORMAT_IS_GRAY4 || GEA_PIXEL_FORMAT_IS_GRAY2)
60
+ // The grayscale formats store the framebuffer in the panel's own sub-byte
61
+ // packing, so it goes straight to the controller with no per-frame repack. The
62
+ // Canvas span/pixel primitives handle the read-modify-write; the pixel VALUE
63
+ // type stays a full byte.
64
+ //
65
+ // GEA_PIXEL_STORAGE_PACKED_BITS is the ONE knob: bits of framebuffer storage per
66
+ // pixel when the format is sub-byte packed (4 for GRAY4 = 2 px/byte, 2 for GRAY2
67
+ // = 4 px/byte), and 0 for every byte-addressed format. Packed code is written
68
+ // against this constant rather than against a per-depth macro, so a new depth is
69
+ // a value here plus the value helpers below — not a second copy of every
70
+ // framebuffer primitive.
71
+ #if GEA_PIXEL_FORMAT_IS_GRAY4
72
+ #define GEA_PIXEL_STORAGE_PACKED_BITS 4
73
+ #elif GEA_PIXEL_FORMAT_IS_GRAY2
74
+ #define GEA_PIXEL_STORAGE_PACKED_BITS 2
75
+ #else
76
+ #define GEA_PIXEL_STORAGE_PACKED_BITS 0
77
+ #endif
78
+ #define GEA_PIXEL_STORAGE_PACKED (GEA_PIXEL_STORAGE_PACKED_BITS != 0)
79
+
80
+ namespace gea::framework::graphics::pixel {
81
+
82
+ enum class Format : std::uint8_t {
83
+ Rgb565,
84
+ Rgb888,
85
+ Rgba8888,
86
+ Argb8888
87
+ };
88
+
89
+ constexpr int bitsPerPixel(Format format)
90
+ {
91
+ switch (format) {
92
+ case Format::Rgb565:
93
+ return 16;
94
+ case Format::Rgb888:
95
+ return 24;
96
+ case Format::Rgba8888:
97
+ case Format::Argb8888:
98
+ return 32;
99
+ }
100
+ return 16;
101
+ }
102
+
103
+ constexpr int bytesPerPixel(Format format)
104
+ {
105
+ return bitsPerPixel(format) / 8;
106
+ }
107
+
108
+ constexpr const char *name(Format format)
109
+ {
110
+ switch (format) {
111
+ case Format::Rgb565:
112
+ return "rgb565";
113
+ case Format::Rgb888:
114
+ return "rgb888";
115
+ case Format::Rgba8888:
116
+ return "rgb8888";
117
+ case Format::Argb8888:
118
+ return "argb8888";
119
+ }
120
+ return "rgb565";
121
+ }
122
+
123
+ inline Format parseFormat(const char *value, Format fallback = Format::Rgb565)
124
+ {
125
+ if (!value) return fallback;
126
+ const char *expected = name(Format::Rgb565);
127
+ const char *cursor = value;
128
+ while (*cursor && *expected && *cursor == *expected) {
129
+ ++cursor;
130
+ ++expected;
131
+ }
132
+ if (!*cursor && !*expected) return Format::Rgb565;
133
+ expected = name(Format::Rgb888);
134
+ cursor = value;
135
+ while (*cursor && *expected && *cursor == *expected) {
136
+ ++cursor;
137
+ ++expected;
138
+ }
139
+ if (!*cursor && !*expected) return Format::Rgb888;
140
+ expected = name(Format::Rgba8888);
141
+ cursor = value;
142
+ while (*cursor && *expected && *cursor == *expected) {
143
+ ++cursor;
144
+ ++expected;
145
+ }
146
+ if (!*cursor && !*expected) return Format::Rgba8888;
147
+ expected = name(Format::Argb8888);
148
+ cursor = value;
149
+ while (*cursor && *expected && *cursor == *expected) {
150
+ ++cursor;
151
+ ++expected;
152
+ }
153
+ if (!*cursor && !*expected) return Format::Argb8888;
154
+ return fallback;
155
+ }
156
+
157
+ constexpr std::uint16_t byteSwap16(std::uint16_t value)
158
+ {
159
+ return static_cast<std::uint16_t>((value << 8) | (value >> 8));
160
+ }
161
+
162
+ constexpr std::uint16_t fromRgb565(std::uint16_t rgb565)
163
+ {
164
+ #if GEA_EMBEDDED_PIXEL_PANEL_ENDIAN
165
+ return byteSwap16(rgb565);
166
+ #else
167
+ return rgb565;
168
+ #endif
169
+ }
170
+
171
+ constexpr std::uint16_t toRgb565(std::uint16_t pixel)
172
+ {
173
+ #if GEA_EMBEDDED_PIXEL_PANEL_ENDIAN
174
+ return byteSwap16(pixel);
175
+ #else
176
+ return pixel;
177
+ #endif
178
+ }
179
+
180
+ constexpr std::uint16_t rgb565FromRgb888(int r, int g, int b)
181
+ {
182
+ return static_cast<std::uint16_t>(((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3));
183
+ }
184
+
185
+ constexpr std::uint16_t fromRgb888(int r, int g, int b)
186
+ {
187
+ return fromRgb565(rgb565FromRgb888(r, g, b));
188
+ }
189
+
190
+ inline void unpackRgb565(std::uint16_t pixel, int *r, int *g, int *b)
191
+ {
192
+ std::uint16_t rgb565 = toRgb565(pixel);
193
+ *r = (rgb565 >> 11) & 0x1F;
194
+ *g = (rgb565 >> 5) & 0x3F;
195
+ *b = rgb565 & 0x1F;
196
+ }
197
+
198
+ constexpr int expand5To8(int value)
199
+ {
200
+ return (value * 255 + 15) / 31;
201
+ }
202
+
203
+ constexpr int expand6To8(int value)
204
+ {
205
+ return (value * 255 + 31) / 63;
206
+ }
207
+
208
+ constexpr int quantize8To5(int value)
209
+ {
210
+ return (value * 31 + 127) / 255;
211
+ }
212
+
213
+ constexpr int quantize8To6(int value)
214
+ {
215
+ return (value * 63 + 127) / 255;
216
+ }
217
+
218
+ constexpr std::uint16_t packRgb565Components(int r, int g, int b)
219
+ {
220
+ return fromRgb565(static_cast<std::uint16_t>((r << 11) | (g << 5) | b));
221
+ }
222
+
223
+ // ---- Grayscale-level value helpers ------------------------------------------
224
+ // A grayscale pixel VALUE is a byte holding a small level — 0..15 for GRAY4,
225
+ // 0..3 for GRAY2, in both cases 0 = black and max = white. These convert to/from
226
+ // the framework's RGB565 authoring model and blend two gray values. Sub-byte
227
+ // PACKING (2 or 4 px/byte in the framebuffer) is separate and lives in Canvas —
228
+ // see GEA_PIXEL_STORAGE_PACKED_BITS.
229
+ //
230
+ // Luma is depth-independent, so both depths share it.
231
+ constexpr int grayLuma8(int r, int g, int b)
232
+ {
233
+ // Rec.601 luma on 8-bit channels: (77 R + 150 G + 29 B) / 256.
234
+ return (77 * r + 150 * g + 29 * b) >> 8;
235
+ }
236
+ constexpr std::uint8_t gray4FromRgb888(int r, int g, int b)
237
+ {
238
+ // 8-bit luma (0..255) -> 4-bit level (0..15), rounded.
239
+ return static_cast<std::uint8_t>((grayLuma8(r, g, b) * 15 + 127) / 255);
240
+ }
241
+ constexpr std::uint8_t gray4FromRgb565(std::uint16_t pixel)
242
+ {
243
+ const std::uint16_t rgb = fromRgb565(pixel); // undo any panel byte-swap
244
+ const int r5 = (rgb >> 11) & 0x1F, g6 = (rgb >> 5) & 0x3F, b5 = rgb & 0x1F;
245
+ return gray4FromRgb888((r5 * 255 + 15) / 31, (g6 * 255 + 31) / 63, (b5 * 255 + 15) / 31);
246
+ }
247
+ constexpr int gray4Expand8(std::uint8_t v) { return (v & 0x0F) * 255 / 15; } // 0..15 -> 0..255
248
+ constexpr std::uint16_t gray4ToRgb565(std::uint8_t v)
249
+ {
250
+ const int y = gray4Expand8(v);
251
+ return packRgb565Components(quantize8To5(y), quantize8To6(y), quantize8To5(y));
252
+ }
253
+ inline std::uint8_t gray4Blend(std::uint8_t fg, std::uint8_t bg, int alpha)
254
+ {
255
+ // Native 4-bit blend: the values ARE 4-bit levels, so expanding to 8-bit,
256
+ // mixing with two /255 divides and requantizing with a third bought nothing
257
+ // but cycles on a per-pixel path (text AA / translucent fills — Xtensa has
258
+ // no integer divide). alpha 0..255 → 0..16, one fused multiply-accumulate
259
+ // in the 4-bit domain: exact at the endpoints, within half a level in the
260
+ // mid-tones the atlas already quantized.
261
+ const int a = (alpha + 8) >> 4; // 0..16
262
+ return static_cast<std::uint8_t>(((fg & 0x0F) * a + (bg & 0x0F) * (16 - a) + 8) >> 4);
263
+ }
264
+
265
+ // GRAY2 (4-level grayscale, 2 bits/pixel). Same shape as the GRAY4 helpers, one
266
+ // depth down: 0 = black, 3 = white.
267
+ constexpr std::uint8_t gray2FromRgb888(int r, int g, int b)
268
+ {
269
+ // 8-bit luma (0..255) -> 2-bit level (0..3), rounded.
270
+ return static_cast<std::uint8_t>((grayLuma8(r, g, b) * 3 + 127) / 255);
271
+ }
272
+ constexpr std::uint8_t gray2FromRgb565(std::uint16_t pixel)
273
+ {
274
+ const std::uint16_t rgb = fromRgb565(pixel); // undo any panel byte-swap
275
+ const int r5 = (rgb >> 11) & 0x1F, g6 = (rgb >> 5) & 0x3F, b5 = rgb & 0x1F;
276
+ return gray2FromRgb888((r5 * 255 + 15) / 31, (g6 * 255 + 31) / 63, (b5 * 255 + 15) / 31);
277
+ }
278
+ constexpr int gray2Expand8(std::uint8_t v) { return (v & 0x03) * 255 / 3; } // 0..3 -> 0..255
279
+ constexpr std::uint16_t gray2ToRgb565(std::uint8_t v)
280
+ {
281
+ const int y = gray2Expand8(v);
282
+ return packRgb565Components(quantize8To5(y), quantize8To6(y), quantize8To5(y));
283
+ }
284
+ inline std::uint8_t gray2Blend(std::uint8_t fg, std::uint8_t bg, int alpha)
285
+ {
286
+ // Mirror of gray4Blend, and for the same reason: the RISC-V core on an
287
+ // ESP32-C3 has no fast divide either (RV32IMC's DIV is multi-cycle and the
288
+ // -march we build with may not even include M), so the blend must stay
289
+ // divide-free. alpha 0..255 → 0..16 and ONE fused multiply-accumulate, kept
290
+ // at 16 weight steps rather than 4: the extra steps cost nothing (same two
291
+ // multiplies, same shift) and keep the rounding of an AA edge from snapping
292
+ // to the endpoints, while a=0/a=16 remain exactly bg/fg. Result is 0..3
293
+ // because 3*16 + 8 >> 4 == 3.
294
+ const int a = (alpha + 8) >> 4; // 0..16
295
+ return static_cast<std::uint8_t>(((fg & 0x03) * a + (bg & 0x03) * (16 - a) + 8) >> 4);
296
+ }
297
+
298
+ namespace detail {
299
+ // Compile-time lookup tables for the 5/6-bit<->8-bit conversions, exact copies of
300
+ // expand5To8/expand6To8/quantize8To5/quantize8To6. The Xtensa LX7 has no integer
301
+ // divide, so the original blend cost ~12 software __divsi3 per pixel (4x/31, 2x/63,
302
+ // 3x/255 mix, 3x/255 quantize) — the dominant per-pixel cost of EVERY translucent
303
+ // fill/gradient/text pixel. These tables move all of that to compile time.
304
+ struct Lut5To8 { std::uint8_t v[32]; constexpr Lut5To8() : v{} { for (int i = 0; i < 32; i++) v[i] = static_cast<std::uint8_t>((i * 255 + 15) / 31); } };
305
+ struct Lut6To8 { std::uint8_t v[64]; constexpr Lut6To8() : v{} { for (int i = 0; i < 64; i++) v[i] = static_cast<std::uint8_t>((i * 255 + 31) / 63); } };
306
+ struct Lut8To5 { std::uint8_t v[256]; constexpr Lut8To5() : v{} { for (int i = 0; i < 256; i++) v[i] = static_cast<std::uint8_t>((i * 31 + 127) / 255); } };
307
+ struct Lut8To6 { std::uint8_t v[256]; constexpr Lut8To6() : v{} { for (int i = 0; i < 256; i++) v[i] = static_cast<std::uint8_t>((i * 63 + 127) / 255); } };
308
+ inline constexpr Lut5To8 kExp5{};
309
+ inline constexpr Lut6To8 kExp6{};
310
+ inline constexpr Lut8To5 kQuant5{};
311
+ inline constexpr Lut8To6 kQuant6{};
312
+ } // namespace detail
313
+
314
+ // ---- RGBA8888 (32-bit) helpers ---------------------------------------------
315
+ // Used by the iOS target, which renders into a full-colour RGBA8888 framebuffer
316
+ // instead of RGB565. Byte layout in memory is R,G,B,A (matches a little-endian
317
+ // uint32 of (A<<24)|(B<<16)|(G<<8)|R and an iOS CGImage configured RGBA8 little).
318
+ // esp32/geaos never touch these — they stay on the RGB565 path above.
319
+
320
+ constexpr std::uint32_t packRgba8888(int r, int g, int b, int a = 255)
321
+ {
322
+ return static_cast<std::uint32_t>((r & 0xFF) | ((g & 0xFF) << 8) | ((b & 0xFF) << 16) |
323
+ ((a & 0xFF) << 24));
324
+ }
325
+
326
+ constexpr std::uint32_t packArgb8888(int r, int g, int b, int a = 255)
327
+ {
328
+ return static_cast<std::uint32_t>(((a & 0xFF) << 24) | ((r & 0xFF) << 16) |
329
+ ((g & 0xFF) << 8) | (b & 0xFF));
330
+ }
331
+
332
+ // Expand an RGB565 (panel-endian-aware) pixel to full RGBA8888. The 5/6-bit
333
+ // channels are expanded to 8-bit; this is lossless within 565's gamut but lets
334
+ // subsequent 8-bit blends (gradients, alpha, AA) accumulate without re-banding.
335
+ inline std::uint32_t rgba8888FromRgb565(std::uint16_t pixel)
336
+ {
337
+ int r, g, b;
338
+ unpackRgb565(pixel, &r, &g, &b);
339
+ return packRgba8888(detail::kExp5.v[r], detail::kExp6.v[g], detail::kExp5.v[b], 255);
340
+ }
341
+
342
+ inline std::uint32_t argb8888FromRgb565(std::uint16_t pixel)
343
+ {
344
+ int r, g, b;
345
+ unpackRgb565(pixel, &r, &g, &b);
346
+ return packArgb8888(detail::kExp5.v[r], detail::kExp6.v[g], detail::kExp5.v[b], 255);
347
+ }
348
+
349
+ constexpr std::uint32_t rgba8888FromRgb888(int r, int g, int b, int a = 255)
350
+ {
351
+ return packRgba8888(r, g, b, a);
352
+ }
353
+
354
+ inline void unpackRgba8888(std::uint32_t pixel, int *r, int *g, int *b, int *a)
355
+ {
356
+ *r = static_cast<int>(pixel & 0xFF);
357
+ *g = static_cast<int>((pixel >> 8) & 0xFF);
358
+ *b = static_cast<int>((pixel >> 16) & 0xFF);
359
+ *a = static_cast<int>((pixel >> 24) & 0xFF);
360
+ }
361
+
362
+ inline void unpackArgb8888(std::uint32_t pixel, int *r, int *g, int *b, int *a)
363
+ {
364
+ *a = static_cast<int>((pixel >> 24) & 0xFF);
365
+ *r = static_cast<int>((pixel >> 16) & 0xFF);
366
+ *g = static_cast<int>((pixel >> 8) & 0xFF);
367
+ *b = static_cast<int>(pixel & 0xFF);
368
+ }
369
+
370
+ // Alpha-composite fg over bg in full 8-bit per channel (no 565 re-quantization),
371
+ // so gradients / translucent fills / anti-aliased edges stay smooth on iOS.
372
+ inline std::uint32_t blend8888(std::uint32_t fg, std::uint32_t bg, int alpha)
373
+ {
374
+ if (alpha >= 255) return fg | 0xFF000000u;
375
+ if (alpha <= 0) return bg;
376
+ const int inv = 255 - alpha;
377
+ const int fr = static_cast<int>(fg & 0xFF);
378
+ const int fgc = static_cast<int>((fg >> 8) & 0xFF);
379
+ const int fb = static_cast<int>((fg >> 16) & 0xFF);
380
+ const int br = static_cast<int>(bg & 0xFF);
381
+ const int bgc = static_cast<int>((bg >> 8) & 0xFF);
382
+ const int bb = static_cast<int>((bg >> 16) & 0xFF);
383
+ const int r = ((fr * alpha + br * inv) + 128) * 257 >> 16;
384
+ const int g = ((fgc * alpha + bgc * inv) + 128) * 257 >> 16;
385
+ const int b = ((fb * alpha + bb * inv) + 128) * 257 >> 16;
386
+ return packRgba8888(r, g, b, 255);
387
+ }
388
+
389
+ inline std::uint32_t blendArgb8888(std::uint32_t fg, std::uint32_t bg, int alpha)
390
+ {
391
+ if (alpha >= 255) return fg | 0xFF000000u;
392
+ if (alpha <= 0) return bg;
393
+ const int inv = 255 - alpha;
394
+ const int fr = static_cast<int>((fg >> 16) & 0xFF);
395
+ const int fgc = static_cast<int>((fg >> 8) & 0xFF);
396
+ const int fb = static_cast<int>(fg & 0xFF);
397
+ const int br = static_cast<int>((bg >> 16) & 0xFF);
398
+ const int bgc = static_cast<int>((bg >> 8) & 0xFF);
399
+ const int bb = static_cast<int>(bg & 0xFF);
400
+ const int r = ((fr * alpha + br * inv) + 128) * 257 >> 16;
401
+ const int g = ((fgc * alpha + bgc * inv) + 128) * 257 >> 16;
402
+ const int b = ((fb * alpha + bb * inv) + 128) * 257 >> 16;
403
+ return packArgb8888(r, g, b, 255);
404
+ }
405
+
406
+ inline std::uint16_t blend(std::uint16_t fg, std::uint16_t bg, int alpha)
407
+ {
408
+ int fr, fg6, fb;
409
+ int br, bg6, bb;
410
+ unpackRgb565(fg, &fr, &fg6, &fb);
411
+ unpackRgb565(bg, &br, &bg6, &bb);
412
+ const int inverse = 255 - alpha;
413
+ // 5/6->8 expand via LUT, /255 via multiply-shift (((v+128)*257)>>16 == round(v/255)
414
+ // for v in [0,65025]), 8->5/6 quantize via LUT. Bit-identical to the divide-based
415
+ // version but with zero per-pixel software divides.
416
+ const int vr = detail::kExp5.v[fr] * alpha + detail::kExp5.v[br] * inverse;
417
+ const int vg = detail::kExp6.v[fg6] * alpha + detail::kExp6.v[bg6] * inverse;
418
+ const int vb = detail::kExp5.v[fb] * alpha + detail::kExp5.v[bb] * inverse;
419
+ const int r8 = ((vr + 128) * 257) >> 16;
420
+ const int g8 = ((vg + 128) * 257) >> 16;
421
+ const int b8 = ((vb + 128) * 257) >> 16;
422
+ return packRgb565Components(
423
+ detail::kQuant5.v[r8],
424
+ detail::kQuant6.v[g8],
425
+ detail::kQuant5.v[b8]);
426
+ }
427
+
428
+ // ---- Compile-time native pixel type ----------------------------------------
429
+ // `native_t` is THIS target's framebuffer/storage pixel — RGB565 (16-bit) or
430
+ // RGBA8888 (32-bit) per GEA_EMBEDDED_PIXEL_FORMAT. Canvas and the renderer
431
+ // operate solely on native_t; there is no runtime format branch and no
432
+ // per-pixel colour conversion. The framework colour model is RGB565
433
+ // (std::uint16_t); `toNative` converts a colour to the native pixel ONCE per
434
+ // draw call (identity on RGB565 targets, so it compiles away).
435
+ #if GEA_PIXEL_FORMAT_IS_8888
436
+ using native_t = std::uint32_t;
437
+ #elif GEA_PIXEL_FORMAT_IS_GRAY
438
+ // A grayscale pixel value is a single byte holding the gray level (0..15 GRAY4,
439
+ // 0..3 GRAY2). Storage in the framebuffer is sub-byte packed (2 or 4 px/byte),
440
+ // but the value carried through style, display commands and blends is this
441
+ // scalar byte.
442
+ using native_t = std::uint8_t;
443
+ #else
444
+ using native_t = std::uint16_t;
445
+ #endif
446
+
447
+ struct NativeColor {
448
+ native_t value;
449
+
450
+ constexpr NativeColor(native_t native = native_t{}) : value(native) {}
451
+ constexpr operator native_t() const { return value; }
452
+ };
453
+
454
+ inline native_t toNative(std::uint16_t rgb565)
455
+ {
456
+ #if GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_RGBA8888
457
+ return rgba8888FromRgb565(rgb565);
458
+ #elif GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_ARGB8888
459
+ return argb8888FromRgb565(rgb565);
460
+ #elif GEA_PIXEL_FORMAT_IS_GRAY4
461
+ return gray4FromRgb565(rgb565);
462
+ #elif GEA_PIXEL_FORMAT_IS_GRAY2
463
+ return gray2FromRgb565(rgb565);
464
+ #else
465
+ return rgb565;
466
+ #endif
467
+ }
468
+
469
+ constexpr std::uint32_t rgba8888ToArgb8888(std::uint32_t rgba)
470
+ {
471
+ return (rgba & 0xFF000000u) | ((rgba & 0x000000FFu) << 16) |
472
+ (rgba & 0x0000FF00u) | ((rgba & 0x00FF0000u) >> 16);
473
+ }
474
+
475
+ // ---- The ONE colour conversion: authoring RGBA8888 -> this board's pixel -----
476
+ // gea apps author colours in RGBA8888 (bytes R,G,B,A — `packRgba8888`). This is
477
+ // the single place a colour becomes a pixel, and it is `constexpr`: a colour that
478
+ // is known at compile time (CSS literal folded by geatsc, a constant) is lowered
479
+ // to the board's native pixel AT BUILD TIME (esp32 -> RGB565, iOS -> RGBA8888) and
480
+ // costs nothing at runtime. A colour computed at runtime (the imperative
481
+ // `fillStyle = …` path) runs the same function once at the set-site. It never
482
+ // appears in the render path: style/IR/Canvas all carry `native_t`, born native.
483
+ constexpr native_t nativeFromRgba8888(std::uint32_t rgba)
484
+ {
485
+ #if GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_RGBA8888
486
+ return rgba;
487
+ #elif GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_ARGB8888
488
+ return rgba8888ToArgb8888(rgba);
489
+ #elif GEA_PIXEL_FORMAT_IS_GRAY4
490
+ return gray4FromRgb888(static_cast<int>(rgba & 0xFFu),
491
+ static_cast<int>((rgba >> 8) & 0xFFu),
492
+ static_cast<int>((rgba >> 16) & 0xFFu));
493
+ #elif GEA_PIXEL_FORMAT_IS_GRAY2
494
+ return gray2FromRgb888(static_cast<int>(rgba & 0xFFu),
495
+ static_cast<int>((rgba >> 8) & 0xFFu),
496
+ static_cast<int>((rgba >> 16) & 0xFFu));
497
+ #else
498
+ return fromRgb888(static_cast<int>(rgba & 0xFFu),
499
+ static_cast<int>((rgba >> 8) & 0xFFu),
500
+ static_cast<int>((rgba >> 16) & 0xFFu));
501
+ #endif
502
+ }
503
+
504
+ // Author a colour from 8-bit channels (alpha defaults opaque). Same build-time /
505
+ // runtime folding behaviour as nativeFromRgba8888.
506
+ constexpr native_t nativeColor(int r, int g, int b, int a = 255)
507
+ {
508
+ return nativeFromRgba8888(packRgba8888(r, g, b, a));
509
+ }
510
+
511
+ constexpr NativeColor nativeColorValue(int r, int g, int b, int a = 255)
512
+ {
513
+ return NativeColor(nativeColor(r, g, b, a));
514
+ }
515
+
516
+ constexpr NativeColor nativeColorFromRgba8888(std::uint32_t rgba)
517
+ {
518
+ return NativeColor(nativeFromRgba8888(rgba));
519
+ }
520
+
521
+ // Author a colour from a STANDARD 0xRRGGBBAA value (red in the HIGH byte — the
522
+ // natural order an app writes, e.g. 0xff8800ff for orange / `rgb(r,g,b)` /
523
+ // `rgba(r,g,b,a)`). This is the canonical numeric colour the canvas API accepts;
524
+ // geatsc lowers a runtime canvas-colour number through here. (packRgba8888 /
525
+ // nativeFromRgba8888 use the in-memory R,G,B,A byte order, i.e. 0xAABBGGRR as a
526
+ // hex constant — an internal representation, not what apps type.)
527
+ constexpr native_t nativeFromRrggbbaa(std::uint32_t rrggbbaa)
528
+ {
529
+ return nativeColor(static_cast<int>((rrggbbaa >> 24) & 0xFFu),
530
+ static_cast<int>((rrggbbaa >> 16) & 0xFFu),
531
+ static_cast<int>((rrggbbaa >> 8) & 0xFFu),
532
+ static_cast<int>(rrggbbaa & 0xFFu));
533
+ }
534
+
535
+ constexpr NativeColor nativeColorFromRrggbbaa(std::uint32_t rrggbbaa)
536
+ {
537
+ return NativeColor(nativeFromRrggbbaa(rrggbbaa));
538
+ }
539
+
540
+ // PRE-panel-swap authoring form, for the style pipeline that applies the panel
541
+ // byte-swap downstream (StyleValues::pixelFromStyleValue). On 16-bit panels this
542
+ // is the RAW (unswapped) RGB565 — the swap happens once on write; on full-colour
543
+ // boards there is no panel concept, so it equals the final native pixel.
544
+ constexpr native_t nativeStyleValueFromRgba8888(std::uint32_t rgba)
545
+ {
546
+ #if GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_RGBA8888
547
+ return rgba;
548
+ #elif GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_ARGB8888
549
+ return rgba8888ToArgb8888(rgba);
550
+ #elif GEA_PIXEL_FORMAT_IS_GRAY4
551
+ // GRAY4 has no panel byte-swap, so — like the full-colour boards — the style
552
+ // value IS the final native pixel (a 0..15 gray level). pixelFromStyleValue is
553
+ // therefore identity for GRAY4 (it must NOT re-run gray4FromRgb565, or the gray
554
+ // value gets re-read as an RGB565 and white collapses to black).
555
+ return gray4FromRgb888(static_cast<int>(rgba & 0xFFu),
556
+ static_cast<int>((rgba >> 8) & 0xFFu),
557
+ static_cast<int>((rgba >> 16) & 0xFFu));
558
+ #elif GEA_PIXEL_FORMAT_IS_GRAY2
559
+ // Same for GRAY2: no panel byte-swap, so the style value IS the final native
560
+ // pixel (a 0..3 gray level) and pixelFromStyleValue stays identity.
561
+ return gray2FromRgb888(static_cast<int>(rgba & 0xFFu),
562
+ static_cast<int>((rgba >> 8) & 0xFFu),
563
+ static_cast<int>((rgba >> 16) & 0xFFu));
564
+ #else
565
+ return rgb565FromRgb888(static_cast<int>(rgba & 0xFFu),
566
+ static_cast<int>((rgba >> 8) & 0xFFu),
567
+ static_cast<int>((rgba >> 16) & 0xFFu));
568
+ #endif
569
+ }
570
+
571
+ constexpr native_t nativeStyleValue(int r, int g, int b, int a = 255)
572
+ {
573
+ return nativeStyleValueFromRgba8888(packRgba8888(r, g, b, a));
574
+ }
575
+
576
+ inline native_t blendNative(native_t fg, native_t bg, int alpha)
577
+ {
578
+ #if GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_RGBA8888
579
+ return blend8888(fg, bg, alpha);
580
+ #elif GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_ARGB8888
581
+ return blendArgb8888(fg, bg, alpha);
582
+ #elif GEA_PIXEL_FORMAT_IS_GRAY4
583
+ return gray4Blend(fg, bg, alpha);
584
+ #elif GEA_PIXEL_FORMAT_IS_GRAY2
585
+ return gray2Blend(fg, bg, alpha);
586
+ #else
587
+ return blend(fg, bg, alpha);
588
+ #endif
589
+ }
590
+
591
+ // Unpack a native pixel to full 8-bit channels. On RGB565 targets the 5/6-bit
592
+ // channels are expanded to 8-bit (alpha is always opaque). Used by paths that
593
+ // must do channel math on the source — e.g. bilinear image scaling.
594
+ inline void unpackNative8(native_t pixel, int *r, int *g, int *b, int *a)
595
+ {
596
+ #if GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_RGBA8888
597
+ unpackRgba8888(pixel, r, g, b, a);
598
+ #elif GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_ARGB8888
599
+ unpackArgb8888(pixel, r, g, b, a);
600
+ #else
601
+ #if GEA_PIXEL_FORMAT_IS_GRAY4
602
+ const int y = gray4Expand8(pixel);
603
+ *r = y; *g = y; *b = y; *a = 255;
604
+ #elif GEA_PIXEL_FORMAT_IS_GRAY2
605
+ const int y = gray2Expand8(pixel);
606
+ *r = y; *g = y; *b = y; *a = 255;
607
+ #else
608
+ int r5 = 0, g6 = 0, b5 = 0;
609
+ unpackRgb565(pixel, &r5, &g6, &b5);
610
+ *r = detail::kExp5.v[r5];
611
+ *g = detail::kExp6.v[g6];
612
+ *b = detail::kExp5.v[b5];
613
+ *a = 255;
614
+ #endif
615
+ #endif
616
+ }
617
+
618
+ // Pack full 8-bit channels into a native pixel (quantizes to 5/6-bit on RGB565).
619
+ inline native_t packNative8(int r, int g, int b, int a = 255)
620
+ {
621
+ #if GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_RGBA8888
622
+ return packRgba8888(r, g, b, a);
623
+ #elif GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_ARGB8888
624
+ return packArgb8888(r, g, b, a);
625
+ #else
626
+ (void)a;
627
+ #if GEA_PIXEL_FORMAT_IS_GRAY4
628
+ return gray4FromRgb888(r & 0xFF, g & 0xFF, b & 0xFF);
629
+ #elif GEA_PIXEL_FORMAT_IS_GRAY2
630
+ return gray2FromRgb888(r & 0xFF, g & 0xFF, b & 0xFF);
631
+ #else
632
+ return packRgb565Components(detail::kQuant5.v[r & 0xFF], detail::kQuant6.v[g & 0xFF], detail::kQuant5.v[b & 0xFF]);
633
+ #endif
634
+ #endif
635
+ }
636
+
637
+ // Downconvert a native framebuffer pixel back to an RGB565 colour. Identity on
638
+ // RGB565 targets (compiles away). Used by the few RGB565-internal scratch
639
+ // subsystems (filter-blur layers, recolor) that read framebuffer pixels into a
640
+ // 16-bit working buffer; on RGBA8888 targets those subsystems are inactive but
641
+ // must still type-check.
642
+ inline std::uint16_t fromNative(native_t pixel)
643
+ {
644
+ #if GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_RGBA8888
645
+ int r = 0, g = 0, b = 0, a = 0;
646
+ unpackRgba8888(pixel, &r, &g, &b, &a);
647
+ return packRgb565Components(detail::kQuant5.v[r & 0xFF], detail::kQuant6.v[g & 0xFF], detail::kQuant5.v[b & 0xFF]);
648
+ #elif GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_ARGB8888
649
+ int r = 0, g = 0, b = 0, a = 0;
650
+ unpackArgb8888(pixel, &r, &g, &b, &a);
651
+ return packRgb565Components(detail::kQuant5.v[r & 0xFF], detail::kQuant6.v[g & 0xFF], detail::kQuant5.v[b & 0xFF]);
652
+ #elif GEA_PIXEL_FORMAT_IS_GRAY4
653
+ return gray4ToRgb565(pixel);
654
+ #elif GEA_PIXEL_FORMAT_IS_GRAY2
655
+ return gray2ToRgb565(pixel);
656
+ #else
657
+ return pixel;
658
+ #endif
659
+ }
660
+
661
+ // Row copies across the RGB565-scratch <-> native-framebuffer boundary. Both
662
+ // compile to a plain std::memcpy on RGB565 targets (native_t == std::uint16_t),
663
+ // byte-identical to the original framebuffer code; on RGBA8888 they convert
664
+ // per pixel (inactive path).
665
+ inline void copyNativeToRgb565(std::uint16_t *dst, const native_t *src, int count)
666
+ {
667
+ if (count <= 0) return;
668
+ #if GEA_PIXEL_FORMAT_IS_8888 || GEA_PIXEL_FORMAT_IS_GRAY
669
+ for (int i = 0; i < count; i++) dst[i] = fromNative(src[i]);
670
+ #else
671
+ std::memcpy(dst, src, static_cast<std::size_t>(count) * sizeof(std::uint16_t));
672
+ #endif
673
+ }
674
+
675
+ inline void copyRgb565ToNative(native_t *dst, const std::uint16_t *src, int count)
676
+ {
677
+ if (count <= 0) return;
678
+ #if GEA_PIXEL_FORMAT_IS_8888 || GEA_PIXEL_FORMAT_IS_GRAY
679
+ for (int i = 0; i < count; i++) dst[i] = toNative(src[i]);
680
+ #else
681
+ std::memcpy(dst, src, static_cast<std::size_t>(count) * sizeof(std::uint16_t));
682
+ #endif
683
+ }
684
+
685
+ // Fill `count` native pixels with a native colour. Keeps the RGB565 fast path
686
+ // (memset for black, uint32-pair fill) on 16-bit targets.
687
+ #if GEA_PIXEL_PIE_FILL16 && !GEA_PIXEL_FORMAT_IS_8888
688
+ // PIE span fill: 8 RGB565 pixels per EE.VST.128.IP through a zero-overhead
689
+ // loop, vs 2 per store on the portable 32-bit-pair path. The q registers are
690
+ // not compiler-allocated (q0 is free to clobber), and the IDF FreeRTOS
691
+ // xtensa port lazily context-switches PIE state like the FPU, so this is
692
+ // task-safe. Scalar head to reach 16-byte alignment (EE.VST.128 requires
693
+ // it), vector body, scalar tail.
694
+ inline void fillNativePie(native_t *dst, int count, native_t color)
695
+ {
696
+ while (count > 0 && (reinterpret_cast<std::uintptr_t>(dst) & 0xFu) != 0) {
697
+ *dst++ = color;
698
+ count--;
699
+ }
700
+ const int vec = count >> 3;
701
+ if (vec > 0) {
702
+ alignas(16) native_t pattern[8] = {color, color, color, color, color, color, color, color};
703
+ const native_t *pat = pattern;
704
+ asm volatile(
705
+ "ee.vld.128.ip q0, %[pat], 0\n"
706
+ "loopnez %[vec], 1f\n"
707
+ "ee.vst.128.ip q0, %[dst], 16\n"
708
+ "1:\n"
709
+ : [dst] "+r"(dst), [pat] "+r"(pat)
710
+ : [vec] "r"(vec)
711
+ : "memory");
712
+ count -= vec << 3;
713
+ }
714
+ while (count-- > 0) *dst++ = color;
715
+ }
716
+ #endif
717
+
718
+ inline void fillNative(native_t *dst, int count, native_t color)
719
+ {
720
+ if (count <= 0) return;
721
+ #if GEA_PIXEL_FORMAT_IS_8888
722
+ std::fill_n(dst, count, color);
723
+ #elif GEA_PIXEL_FORMAT_IS_GRAY
724
+ // native_t is one byte, so an UNPACKED gray-value run is a plain memset.
725
+ // (The sub-byte-PACKED framebuffer is filled by Canvas, not here.)
726
+ std::memset(dst, color, static_cast<std::size_t>(count));
727
+ #else
728
+ #if GEA_PIXEL_PIE_FILL16
729
+ // Worth the head/tail bookkeeping from two vector stores up.
730
+ if (count >= 16) {
731
+ fillNativePie(dst, count, color);
732
+ return;
733
+ }
734
+ #endif
735
+ if (color == 0) {
736
+ std::memset(dst, 0, static_cast<std::size_t>(count) * sizeof(native_t));
737
+ return;
738
+ }
739
+ if ((reinterpret_cast<std::uintptr_t>(dst) & 0x2u) != 0) {
740
+ *dst++ = color;
741
+ count--;
742
+ }
743
+ const std::uint32_t pair = static_cast<std::uint32_t>(color) | (static_cast<std::uint32_t>(color) << 16);
744
+ const int pairCount = count / 2;
745
+ std::fill_n(reinterpret_cast<std::uint32_t *>(dst), pairCount, pair);
746
+ if ((count & 1) != 0) dst[pairCount * 2] = color;
747
+ #endif
748
+ }
749
+
750
+ // ---- Sub-byte packed STORAGE primitives -------------------------------------
751
+ // The framebuffer packs 8/Bits pixels per byte, MSB first — the order both the
752
+ // IT8951 (4bpp) and the SSD1677 (2bpp) read their RAM in. For Bits = 4 that is
753
+ // byte[b] = (px[2b] << 4) | px[2b+1]; for Bits = 2 it is
754
+ // byte[b] = (px[4b] << 6) | (px[4b+1] << 4) | (px[4b+2] << 2) | px[4b+3].
755
+ // `row` points at the first packed byte of a physical row; `x` is a pixel index
756
+ // within that row — always >= 0, since every Canvas caller has already clipped to
757
+ // the framebuffer (the / and % below truncate toward zero, so a negative index
758
+ // would not address the byte a floor-division would).
759
+ //
760
+ // These are pure byte-buffer ops (no format guard) parameterised on the bit
761
+ // depth, so a host test can exercise every depth in one binary; the Canvas
762
+ // framebuffer accessors instantiate exactly one of them, via the `packed` alias
763
+ // below, under GEA_PIXEL_STORAGE_PACKED. Bits is a compile-time constant, so
764
+ // every / and % here folds to a shift or a mask — the 4bpp code generated is the
765
+ // same as the hand-written nibble version it replaces.
766
+ template <int Bits>
767
+ struct PackedPixels {
768
+ static_assert(Bits == 2 || Bits == 4, "packed pixel storage supports 2 or 4 bits per pixel");
769
+
770
+ static constexpr int kBits = Bits;
771
+ static constexpr int kPxPerByte = 8 / Bits;
772
+ static constexpr std::uint8_t kMask = static_cast<std::uint8_t>((1u << Bits) - 1u);
773
+
774
+ static constexpr int byteIndex(int x) { return x / kPxPerByte; }
775
+ // MSB-first: pixel 0 of a byte occupies the TOP `Bits` bits.
776
+ static constexpr int shiftFor(int x) { return 8 - Bits - (x % kPxPerByte) * Bits; }
777
+ // Bytes needed to hold a row of `widthPx` pixels (partial trailing byte counts).
778
+ static constexpr int rowBytes(int widthPx) { return (widthPx + kPxPerByte - 1) / kPxPerByte; }
779
+ // A byte holding kPxPerByte copies of `v` — the value a solid span memsets with.
780
+ static constexpr std::uint8_t replicate(std::uint8_t v)
781
+ {
782
+ std::uint8_t out = 0;
783
+ for (int i = 0; i < kPxPerByte; i++) out = static_cast<std::uint8_t>((out << Bits) | (v & kMask));
784
+ return out;
785
+ }
786
+ // Blend in the native value domain — see gray4Blend / gray2Blend.
787
+ static std::uint8_t blendValue(std::uint8_t fg, std::uint8_t bg, int alpha)
788
+ {
789
+ if constexpr (Bits == 4) return gray4Blend(fg, bg, alpha);
790
+ else return gray2Blend(fg, bg, alpha);
791
+ }
792
+
793
+ static std::uint8_t get(const std::uint8_t *row, int x)
794
+ {
795
+ return static_cast<std::uint8_t>((row[byteIndex(x)] >> shiftFor(x)) & kMask);
796
+ }
797
+
798
+ static void set(std::uint8_t *row, int x, std::uint8_t v)
799
+ {
800
+ std::uint8_t &byte = row[byteIndex(x)];
801
+ const int shift = shiftFor(x);
802
+ byte = static_cast<std::uint8_t>((byte & ~(kMask << shift)) | ((v & kMask) << shift));
803
+ }
804
+
805
+ // Fill a run of `count` pixels starting at pixel x0 with gray value v.
806
+ static void fillSpan(std::uint8_t *row, int x0, int count, std::uint8_t v)
807
+ {
808
+ if (count <= 0) return;
809
+ v &= kMask;
810
+ int x = x0;
811
+ int n = count;
812
+ // Leading pixels sharing a byte with pixels BEFORE the span: RMW so their
813
+ // neighbours already in that byte survive.
814
+ while (n > 0 && (x % kPxPerByte) != 0) { set(row, x, v); x++; n--; }
815
+ // Aligned middle: whole bytes hold kPxPerByte pixels of the same value.
816
+ const std::uint8_t rep = replicate(v);
817
+ const int bytes = n / kPxPerByte;
818
+ if (bytes > 0) std::memset(row + byteIndex(x), rep, static_cast<std::size_t>(bytes));
819
+ x += bytes * kPxPerByte;
820
+ n -= bytes * kPxPerByte;
821
+ // Trailing partial byte.
822
+ while (n > 0) { set(row, x, v); x++; n--; }
823
+ }
824
+
825
+ // Write `count` UNPACKED gray values (one level per byte) into packed pixels.
826
+ static void writeUnpacked(std::uint8_t *row, int x0, const std::uint8_t *src, int count)
827
+ {
828
+ for (int i = 0; i < count; i++) set(row, x0 + i, src[i]);
829
+ }
830
+
831
+ // Read `count` packed pixels into an UNPACKED buffer (one value per byte).
832
+ static void readUnpacked(const std::uint8_t *row, int x0, std::uint8_t *dst, int count)
833
+ {
834
+ for (int i = 0; i < count; i++) dst[i] = get(row, x0 + i);
835
+ }
836
+
837
+ // Blend one pixel: dst = blendValue(fg, dst, alpha).
838
+ static void blendPixel(std::uint8_t *row, int x, std::uint8_t fg, int alpha)
839
+ {
840
+ if (alpha <= 0) return;
841
+ if (alpha >= 255) { set(row, x, fg); return; }
842
+ set(row, x, blendValue(fg, get(row, x), alpha));
843
+ }
844
+
845
+ // Copy `count` pixels between two packed rows (e.g. scrollRect). Falls back to
846
+ // per-pixel; both endpoints may be unaligned. Overlap-safe, including when
847
+ // dstRow and srcRow are the SAME row.
848
+ static void copyPacked(std::uint8_t *dstRow, int dstX, const std::uint8_t *srcRow, int srcX, int count)
849
+ {
850
+ if (count <= 0) return;
851
+
852
+ // Same phase within the byte ⇒ the aligned middle is a byte-wise memmove.
853
+ if ((dstX % kPxPerByte) == (srcX % kPxPerByte)) {
854
+ // Split into `lead` pixels up to the first dst byte boundary, `mid`
855
+ // whole bytes, then the trailing remainder.
856
+ int lead = 0;
857
+ while (lead < count && ((dstX + lead) % kPxPerByte) != 0) lead++;
858
+ const int mid = (count - lead) / kPxPerByte;
859
+ const int tail = lead + mid * kPxPerByte;
860
+
861
+ // ORDER MATTERS when the rows alias and the ranges overlap. That case
862
+ // is live, not theoretical: Canvas::scrollRect passes dstRow ==
863
+ // srcRow for a horizontal pan (dy == 0, dx != 0), reached from
864
+ // tree_render.cpp's Display::scrollRect(vx, vy, vw, vh, panDx, 0).
865
+ //
866
+ // memmove is itself overlap-safe, but the partial-byte fixups around
867
+ // it are not — they read whole pixels out of bytes the memmove may
868
+ // already have rewritten, and they write bytes the memmove may still
869
+ // need to read. Doing lead-then-mid-then-tail unconditionally (as
870
+ // this did before) corrupts a rightward overlapping move on both
871
+ // counts.
872
+ //
873
+ // The fix is just to work from the far end of the move direction
874
+ // inward. It is sound because same-phase forces |dstX - srcX| to be a
875
+ // whole number of bytes, so the partial work on the far side is
876
+ // always at least one byte clear of the memmove's range. Keeping the
877
+ // memmove means scrolling does not lose its fast path.
878
+ if (dstX > srcX) {
879
+ for (int i = count - 1; i >= tail; i--) set(dstRow, dstX + i, get(srcRow, srcX + i));
880
+ if (mid > 0)
881
+ std::memmove(dstRow + byteIndex(dstX + lead), srcRow + byteIndex(srcX + lead),
882
+ static_cast<std::size_t>(mid));
883
+ for (int i = lead - 1; i >= 0; i--) set(dstRow, dstX + i, get(srcRow, srcX + i));
884
+ } else {
885
+ for (int i = 0; i < lead; i++) set(dstRow, dstX + i, get(srcRow, srcX + i));
886
+ if (mid > 0)
887
+ std::memmove(dstRow + byteIndex(dstX + lead), srcRow + byteIndex(srcX + lead),
888
+ static_cast<std::size_t>(mid));
889
+ for (int i = tail; i < count; i++) set(dstRow, dstX + i, get(srcRow, srcX + i));
890
+ }
891
+ return;
892
+ }
893
+ // Mismatched phase: shift pixel by pixel. Iterate in a safe direction.
894
+ if (dstX <= srcX)
895
+ for (int i = 0; i < count; i++) set(dstRow, dstX + i, get(srcRow, srcX + i));
896
+ else
897
+ for (int i = count - 1; i >= 0; i--) set(dstRow, dstX + i, get(srcRow, srcX + i));
898
+ }
899
+ };
900
+
901
+ #if GEA_PIXEL_STORAGE_PACKED
902
+ // THIS target's packed-storage op set. Canvas writes `pixel::packed::set(...)`
903
+ // and gets the right depth for the board it is being compiled for.
904
+ using packed = PackedPixels<GEA_PIXEL_STORAGE_PACKED_BITS>;
905
+ #endif
906
+
907
+ } // namespace gea::framework::graphics::pixel