@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,869 @@
1
+ #pragma once
2
+
3
+ #include "canvas.h"
4
+ #include "display.h"
5
+
6
+ #include <algorithm>
7
+ #include <array>
8
+ #include <cstddef>
9
+ #include <cstdint>
10
+ #include <cstdlib>
11
+ #include <cstring>
12
+ #include <memory>
13
+ #include <new>
14
+
15
+ #include <string>
16
+ #include <utility>
17
+ #include <vector>
18
+
19
+ #if __has_include("esp_heap_caps.h")
20
+ #include "esp_heap_caps.h"
21
+ #else
22
+ #define GEA_DISPLAY_PRESENT_HAS_ESP_HEAP_CAPS 0
23
+ #define MALLOC_CAP_INTERNAL 0
24
+ #define MALLOC_CAP_8BIT 0
25
+ inline void *heap_caps_malloc(std::size_t size, int)
26
+ {
27
+ return std::malloc(size);
28
+ }
29
+ inline void heap_caps_free(void *ptr)
30
+ {
31
+ std::free(ptr);
32
+ }
33
+ #endif
34
+
35
+ namespace gea::framework::display_present {
36
+
37
+ struct Rect {
38
+ int x0 = 0;
39
+ int y0 = 0;
40
+ int x1 = -1;
41
+ int y1 = -1;
42
+ };
43
+
44
+ inline bool valid(const Rect &rect)
45
+ {
46
+ return rect.x0 <= rect.x1 && rect.y0 <= rect.y1;
47
+ }
48
+
49
+ inline int area(const Rect &rect)
50
+ {
51
+ if (!valid(rect)) return 0;
52
+ return (rect.x1 - rect.x0 + 1) * (rect.y1 - rect.y0 + 1);
53
+ }
54
+
55
+ inline Rect unite(const Rect &a, const Rect &b)
56
+ {
57
+ if (!valid(a)) return b;
58
+ if (!valid(b)) return a;
59
+ Rect out = a;
60
+ if (b.x0 < out.x0) out.x0 = b.x0;
61
+ if (b.y0 < out.y0) out.y0 = b.y0;
62
+ if (b.x1 > out.x1) out.x1 = b.x1;
63
+ if (b.y1 > out.y1) out.y1 = b.y1;
64
+ return out;
65
+ }
66
+
67
+ inline Rect fullScreen(int width, int height)
68
+ {
69
+ return {0, 0, width - 1, height - 1};
70
+ }
71
+
72
+ inline Rect clampAndAlign(Rect rect, int width, int height)
73
+ {
74
+ rect.x0 &= ~1;
75
+ rect.y0 &= ~1;
76
+ rect.x1 |= 1;
77
+ rect.y1 |= 1;
78
+ if (rect.x0 < 0) rect.x0 = 0;
79
+ if (rect.y0 < 0) rect.y0 = 0;
80
+ if (rect.x1 >= width) rect.x1 = width - 1;
81
+ if (rect.y1 >= height) rect.y1 = height - 1;
82
+ return rect;
83
+ }
84
+
85
+ // unique_ptr deleter that frees buffers allocated via heap_caps_malloc.
86
+ // We must NOT use the global operator delete (or std::default_delete) here:
87
+ // the project's overridden operator new (targets/esp32/services/heap.cpp)
88
+ // routes through EspHeapAllocator and prefers PSRAM; freeing a
89
+ // heap_caps_malloc(INTERNAL) buffer through that path corrupts the heap.
90
+ struct HeapCapsFree {
91
+ void operator()(void *p) const noexcept {
92
+ heap_caps_free(p);
93
+ }
94
+ };
95
+
96
+ static constexpr int kInlineCircleEntryCapacity = 32;
97
+
98
+ struct Command {
99
+ using CircleEntry = gea::framework::graphics::CircleEntry;
100
+
101
+ gea::platform::display::DisplayPresentCommandType type =
102
+ gea::platform::display::DisplayPresentCommandType::Clear;
103
+ gea::framework::graphics::pixel::native_t color = 0;
104
+ std::uint8_t alpha = 255;
105
+ const gea::framework::graphics::pixel::native_t *pixels = nullptr;
106
+ const std::uint8_t *alphaPixels = nullptr;
107
+ int srcWidth = 0;
108
+ int srcHeight = 0;
109
+ int x = 0;
110
+ int y = 0;
111
+ int w = 0;
112
+ int h = 0;
113
+ int x0 = 0;
114
+ int y0 = 0;
115
+ int x1 = 0;
116
+ int y1 = 0;
117
+ int x2 = 0;
118
+ int y2 = 0;
119
+ int radius = 0;
120
+ float scale = 1.0f;
121
+ int fontFamilyId = -1;
122
+ int fontSizePx = 16;
123
+ std::string text;
124
+ // FillCirclesRgb565: small batches stay inline; larger batches own an
125
+ // INTERNAL-RAM buffer of CircleEntry triples (sorted by y), allocated once
126
+ // in extractFrame. No PSRAM deinterleave copy — the canvas rasterizer reads
127
+ // directly from this storage.
128
+ std::array<CircleEntry, kInlineCircleEntryCapacity> inlineCircles{};
129
+ std::unique_ptr<CircleEntry[], HeapCapsFree> circlesBuffer;
130
+ int circlesCount = 0;
131
+ bool circlesInline = false;
132
+ // FillTrianglesRgb565: same ownership scheme as the circle batch. Entries
133
+ // stay in the producer's painter's (depth) order; the batch's overall row
134
+ // extent is cached in the Command's y0/y1 fields for whole-chunk skips.
135
+ std::unique_ptr<gea::framework::graphics::TriangleEntry[], HeapCapsFree> trianglesBuffer;
136
+ int trianglesCount = 0;
137
+
138
+ const CircleEntry *circleEntries() const
139
+ {
140
+ return circlesInline ? inlineCircles.data() : circlesBuffer.get();
141
+ }
142
+
143
+ bool hasCircleEntries() const
144
+ {
145
+ return circlesCount <= 0 || circleEntries() != nullptr;
146
+ }
147
+ };
148
+
149
+ struct Frame {
150
+ std::vector<Command> commands;
151
+
152
+ void clear()
153
+ {
154
+ commands.clear();
155
+ }
156
+ };
157
+
158
+ inline Rect rectFromBox(int x, int y, int w, int h)
159
+ {
160
+ if (w <= 0 || h <= 0) return {};
161
+ return {x, y, x + w - 1, y + h - 1};
162
+ }
163
+
164
+ inline Rect circleBounds(int x, int y, int radius)
165
+ {
166
+ return {x - radius, y - radius, x + radius, y + radius};
167
+ }
168
+
169
+ inline Rect triangleBounds(const Command &command)
170
+ {
171
+ return {
172
+ std::min({command.x0, command.x1, command.x2}),
173
+ std::min({command.y0, command.y1, command.y2}),
174
+ std::max({command.x0, command.x1, command.x2}),
175
+ std::max({command.y0, command.y1, command.y2}),
176
+ };
177
+ }
178
+
179
+ inline Rect textBounds(const Command &command)
180
+ {
181
+ const int glyphWidth = std::max(1, static_cast<int>(8.0f * command.scale + 0.5f));
182
+ const int glyphHeight = std::max(1, static_cast<int>(16.0f * command.scale + 0.5f));
183
+ return rectFromBox(command.x, command.y, static_cast<int>(command.text.size()) * glyphWidth, glyphHeight);
184
+ }
185
+
186
+ inline Rect commandBounds(const Command &command, int width, int height)
187
+ {
188
+ using Type = gea::platform::display::DisplayPresentCommandType;
189
+ switch (command.type) {
190
+ case Type::Clear:
191
+ return fullScreen(width, height);
192
+ case Type::FillRectRgb565:
193
+ case Type::StrokeRectRgb565:
194
+ return rectFromBox(command.x, command.y, command.w, command.h);
195
+ case Type::FillTriangleRgb565:
196
+ return triangleBounds(command);
197
+ case Type::FillCircleRgb565:
198
+ case Type::StrokeCircleRgb565:
199
+ return circleBounds(command.x, command.y, command.radius);
200
+ case Type::FillCirclesRgb565: {
201
+ Rect bounds{};
202
+ const auto *circles = command.circleEntries();
203
+ const int limit = command.circlesCount;
204
+ if (!circles) return bounds;
205
+ for (int i = 0; i < limit; i++) {
206
+ bounds = unite(bounds, circleBounds(circles[i].x, circles[i].y, command.radius));
207
+ }
208
+ return bounds;
209
+ }
210
+ case Type::FillTrianglesRgb565: {
211
+ Rect bounds{};
212
+ const gea::framework::graphics::TriangleEntry *tris = command.trianglesBuffer.get();
213
+ const int limit = command.trianglesCount;
214
+ if (!tris) return bounds;
215
+ for (int i = 0; i < limit; i++) {
216
+ const auto &t = tris[i];
217
+ const Rect triRect{
218
+ std::min({static_cast<int>(t.x0), static_cast<int>(t.x1), static_cast<int>(t.x2)}),
219
+ static_cast<int>(t.rowY0),
220
+ std::max({static_cast<int>(t.x0), static_cast<int>(t.x1), static_cast<int>(t.x2)}),
221
+ static_cast<int>(t.rowY1),
222
+ };
223
+ bounds = unite(bounds, triRect);
224
+ }
225
+ return bounds;
226
+ }
227
+ case Type::DrawImage:
228
+ return rectFromBox(command.x, command.y, command.srcWidth, command.srcHeight);
229
+ case Type::DrawImageScaled:
230
+ case Type::DrawImageRotated90CW:
231
+ return rectFromBox(command.x, command.y, command.w, command.h);
232
+ case Type::DrawImageTiledX:
233
+ return rectFromBox(command.x, command.y, command.w, command.srcHeight);
234
+ case Type::FillText:
235
+ return textBounds(command);
236
+ }
237
+ return {};
238
+ }
239
+
240
+ inline void addRect(Rect *rects, int *count, int capacity, Rect rect, int width, int height)
241
+ {
242
+ if (!rects || !count || capacity <= 0) return;
243
+ rect = clampAndAlign(rect, width, height);
244
+ if (!valid(rect)) return;
245
+
246
+ for (int i = 0; i < *count; i++) {
247
+ if (rects[i].x0 <= rect.x1 + 1 && rects[i].x1 + 1 >= rect.x0 &&
248
+ rects[i].y0 <= rect.y1 + 1 && rects[i].y1 + 1 >= rect.y0) {
249
+ rects[i] = unite(rects[i], rect);
250
+ return;
251
+ }
252
+ }
253
+
254
+ if (*count < capacity) {
255
+ rects[(*count)++] = rect;
256
+ return;
257
+ }
258
+
259
+ int best = 0;
260
+ int bestCost = 0x7fffffff;
261
+ for (int i = 0; i < *count; i++) {
262
+ const Rect merged = unite(rects[i], rect);
263
+ const int cost = area(merged) - area(rects[i]);
264
+ if (cost < bestCost) {
265
+ bestCost = cost;
266
+ best = i;
267
+ }
268
+ }
269
+ rects[best] = unite(rects[best], rect);
270
+ }
271
+
272
+ inline void addCommandBounds(const Command &command, Rect *rects, int *count, int capacity, int width, int height)
273
+ {
274
+ using Type = gea::platform::display::DisplayPresentCommandType;
275
+ if (command.type == Type::FillCirclesRgb565) {
276
+ const auto *circles = command.circleEntries();
277
+ const int limit = command.circlesCount;
278
+ if (!circles) return;
279
+ for (int i = 0; i < limit; i++) {
280
+ addRect(rects, count, capacity, circleBounds(circles[i].x, circles[i].y, command.radius), width, height);
281
+ }
282
+ return;
283
+ }
284
+ addRect(rects, count, capacity, commandBounds(command, width, height), width, height);
285
+ }
286
+
287
+ inline bool commandsEqual(const Command &a, const Command &b)
288
+ {
289
+ if (a.type != b.type || a.alpha != b.alpha) return false;
290
+ using Type = gea::platform::display::DisplayPresentCommandType;
291
+ switch (a.type) {
292
+ case Type::Clear:
293
+ return a.color == b.color;
294
+ case Type::FillRectRgb565:
295
+ case Type::StrokeRectRgb565:
296
+ return a.x == b.x && a.y == b.y && a.w == b.w && a.h == b.h && a.color == b.color;
297
+ case Type::FillTriangleRgb565:
298
+ return a.x0 == b.x0 && a.y0 == b.y0 && a.x1 == b.x1 && a.y1 == b.y1 &&
299
+ a.x2 == b.x2 && a.y2 == b.y2 && a.color == b.color;
300
+ case Type::FillCircleRgb565:
301
+ case Type::StrokeCircleRgb565:
302
+ return a.x == b.x && a.y == b.y && a.radius == b.radius && a.color == b.color;
303
+ case Type::FillCirclesRgb565: {
304
+ if (a.radius != b.radius || a.circlesCount != b.circlesCount) return false;
305
+ if (a.circlesCount == 0) return true;
306
+ const auto *ap = a.circleEntries();
307
+ const auto *bp = b.circleEntries();
308
+ if (!ap || !bp) return ap == bp;
309
+ return std::memcmp(ap, bp,
310
+ static_cast<std::size_t>(a.circlesCount) *
311
+ sizeof(gea::framework::graphics::CircleEntry)) == 0;
312
+ }
313
+ case Type::FillTrianglesRgb565: {
314
+ if (a.trianglesCount != b.trianglesCount) return false;
315
+ if (a.trianglesCount == 0) return true;
316
+ const auto *ap = a.trianglesBuffer.get();
317
+ const auto *bp = b.trianglesBuffer.get();
318
+ if (!ap || !bp) return ap == bp;
319
+ return std::memcmp(ap, bp,
320
+ static_cast<std::size_t>(a.trianglesCount) *
321
+ sizeof(gea::framework::graphics::TriangleEntry)) == 0;
322
+ }
323
+ case Type::DrawImage:
324
+ return a.pixels == b.pixels && a.alphaPixels == b.alphaPixels &&
325
+ a.srcWidth == b.srcWidth && a.srcHeight == b.srcHeight && a.x == b.x && a.y == b.y;
326
+ case Type::DrawImageScaled:
327
+ case Type::DrawImageRotated90CW:
328
+ return a.pixels == b.pixels && a.alphaPixels == b.alphaPixels &&
329
+ a.srcWidth == b.srcWidth && a.srcHeight == b.srcHeight &&
330
+ a.x == b.x && a.y == b.y && a.w == b.w && a.h == b.h;
331
+ case Type::DrawImageTiledX:
332
+ return a.pixels == b.pixels && a.alphaPixels == b.alphaPixels &&
333
+ a.srcWidth == b.srcWidth && a.srcHeight == b.srcHeight &&
334
+ a.x == b.x && a.y == b.y && a.w == b.w;
335
+ case Type::FillText:
336
+ return a.x == b.x && a.y == b.y && a.color == b.color && a.scale == b.scale &&
337
+ a.fontFamilyId == b.fontFamilyId && a.fontSizePx == b.fontSizePx && a.text == b.text;
338
+ }
339
+ return false;
340
+ }
341
+
342
+ inline bool extractFrame(const gea::platform::display::DisplayPresentCommand *commands, int commandCount, Frame &frame)
343
+ {
344
+ frame.clear();
345
+ if (!commands || commandCount <= 0) return false;
346
+ frame.commands.reserve(static_cast<std::size_t>(commandCount));
347
+ for (int i = 0; i < commandCount; i++) {
348
+ const auto &command = commands[i];
349
+ Command out;
350
+ out.type = command.type;
351
+ switch (command.type) {
352
+ case gea::platform::display::DisplayPresentCommandType::Clear:
353
+ out.color = command.clear.color;
354
+ break;
355
+ case gea::platform::display::DisplayPresentCommandType::FillRectRgb565:
356
+ out.x = command.fillRectRgb565.x;
357
+ out.y = command.fillRectRgb565.y;
358
+ out.w = command.fillRectRgb565.w;
359
+ out.h = command.fillRectRgb565.h;
360
+ out.color = command.fillRectRgb565.color;
361
+ out.alpha = command.fillRectRgb565.alpha;
362
+ break;
363
+ case gea::platform::display::DisplayPresentCommandType::StrokeRectRgb565:
364
+ out.x = command.strokeRectRgb565.x;
365
+ out.y = command.strokeRectRgb565.y;
366
+ out.w = command.strokeRectRgb565.w;
367
+ out.h = command.strokeRectRgb565.h;
368
+ out.color = command.strokeRectRgb565.color;
369
+ out.alpha = command.strokeRectRgb565.alpha;
370
+ break;
371
+ case gea::platform::display::DisplayPresentCommandType::FillTriangleRgb565:
372
+ out.x0 = command.fillTriangleRgb565.x0;
373
+ out.y0 = command.fillTriangleRgb565.y0;
374
+ out.x1 = command.fillTriangleRgb565.x1;
375
+ out.y1 = command.fillTriangleRgb565.y1;
376
+ out.x2 = command.fillTriangleRgb565.x2;
377
+ out.y2 = command.fillTriangleRgb565.y2;
378
+ out.color = command.fillTriangleRgb565.color;
379
+ out.alpha = command.fillTriangleRgb565.alpha;
380
+ break;
381
+ case gea::platform::display::DisplayPresentCommandType::FillCircleRgb565:
382
+ out.x = command.fillCircleRgb565.x;
383
+ out.y = command.fillCircleRgb565.y;
384
+ out.radius = command.fillCircleRgb565.radius;
385
+ out.color = command.fillCircleRgb565.color;
386
+ out.alpha = command.fillCircleRgb565.alpha;
387
+ break;
388
+ case gea::platform::display::DisplayPresentCommandType::StrokeCircleRgb565:
389
+ out.x = command.strokeCircleRgb565.x;
390
+ out.y = command.strokeCircleRgb565.y;
391
+ out.radius = command.strokeCircleRgb565.radius;
392
+ out.color = command.strokeCircleRgb565.color;
393
+ out.alpha = command.strokeCircleRgb565.alpha;
394
+ break;
395
+ case gea::platform::display::DisplayPresentCommandType::FillCirclesRgb565: {
396
+ const auto &src = command.fillCirclesRgb565;
397
+ if (!src.xs || !src.ys || !src.colors || src.count < 0 || src.radius <= 0) return false;
398
+ out.radius = src.radius;
399
+ out.alpha = src.alpha;
400
+ // Opt 4: small inline storage, otherwise one owned INTERNAL-RAM
401
+ // buffer, sorted in place.
402
+ //
403
+ // The previous design did TWO things this loop now collapses into
404
+ // one: (a) build an interleaved scratch array in INTERNAL RAM and
405
+ // sort it, then (b) scatter-copy the sorted triples into three
406
+ // PSRAM std::vector<uint16_t>'s on the Command. Both the second
407
+ // alloc and the PSRAM stores are gone — extractFrame allocates
408
+ // either an inline Command array or ONE CircleEntry[] in INTERNAL
409
+ // RAM, sorts it, and the canvas rasterizer reads directly from it.
410
+ //
411
+ // MALLOC_CAP_INTERNAL is required: the project's overridden
412
+ // operator new (targets/esp32/services/heap.cpp) prefers PSRAM,
413
+ // and empirically a new PSRAM allocation in this hot path blanks
414
+ // the CO5300 panel — even a 4-byte alloc.
415
+ const int count = src.count;
416
+ using gea::framework::graphics::CircleEntry;
417
+ if (count > 0) {
418
+ CircleEntry *buffer = nullptr;
419
+ if (count <= kInlineCircleEntryCapacity) {
420
+ out.circlesInline = true;
421
+ buffer = out.inlineCircles.data();
422
+ } else {
423
+ buffer = static_cast<CircleEntry *>(
424
+ heap_caps_malloc(sizeof(CircleEntry) * static_cast<std::size_t>(count),
425
+ MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
426
+ if (!buffer) return false;
427
+ }
428
+ bool sortedByBucket = false;
429
+ constexpr int kCircleBucketSortMaxYRange = 2048;
430
+ if (count <= 65535) {
431
+ int minY = static_cast<int>(src.ys[0]);
432
+ int maxY = minY;
433
+ for (int i = 1; i < count; i++) {
434
+ const int y = static_cast<int>(src.ys[i]);
435
+ if (y < minY) minY = y;
436
+ if (y > maxY) maxY = y;
437
+ }
438
+ const int yRange = maxY - minY + 1;
439
+ if (yRange > 0 && yRange <= kCircleBucketSortMaxYRange) {
440
+ std::uint16_t offsets[kCircleBucketSortMaxYRange] = {};
441
+ for (int i = 0; i < count; i++) {
442
+ offsets[static_cast<int>(src.ys[i]) - minY]++;
443
+ }
444
+ std::uint16_t running = 0;
445
+ for (int y = 0; y < yRange; y++) {
446
+ const std::uint16_t n = offsets[y];
447
+ offsets[y] = running;
448
+ running = static_cast<std::uint16_t>(running + n);
449
+ }
450
+ for (int i = 0; i < count; i++) {
451
+ const int bucket = static_cast<int>(src.ys[i]) - minY;
452
+ buffer[offsets[bucket]++] = CircleEntry{src.ys[i], src.xs[i], src.colors[i]};
453
+ }
454
+ sortedByBucket = true;
455
+ }
456
+ }
457
+ if (!sortedByBucket) {
458
+ for (int i = 0; i < count; i++) {
459
+ buffer[i] = CircleEntry{src.ys[i], src.xs[i], src.colors[i]};
460
+ }
461
+ std::sort(buffer, buffer + count,
462
+ [](const CircleEntry &a, const CircleEntry &b) { return a.y < b.y; });
463
+ }
464
+ if (!out.circlesInline) out.circlesBuffer.reset(buffer);
465
+ }
466
+ out.circlesCount = count;
467
+ break;
468
+ }
469
+ case gea::platform::display::DisplayPresentCommandType::FillTrianglesRgb565: {
470
+ // Same INTERNAL-RAM ownership rationale as the circle batch above.
471
+ // Entries arrive pre-packed and depth-ordered from the canvas layer;
472
+ // the copy also caches the batch-wide row extent in the Command's
473
+ // y0/y1 fields for whole-chunk skips in commandRowExtent.
474
+ const auto &src = command.fillTrianglesRgb565;
475
+ if (!src.entries || src.count < 0) return false;
476
+ out.alpha = src.alpha;
477
+ using gea::framework::graphics::TriangleEntry;
478
+ const int count = src.count;
479
+ int batchY0 = 0;
480
+ int batchY1 = -1;
481
+ if (count > 0) {
482
+ TriangleEntry *buffer = static_cast<TriangleEntry *>(
483
+ heap_caps_malloc(sizeof(TriangleEntry) * static_cast<std::size_t>(count),
484
+ MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
485
+ if (!buffer) return false;
486
+ std::memcpy(buffer, src.entries, sizeof(TriangleEntry) * static_cast<std::size_t>(count));
487
+ batchY0 = buffer[0].rowY0;
488
+ batchY1 = buffer[0].rowY1;
489
+ for (int i = 1; i < count; i++) {
490
+ if (buffer[i].rowY0 < batchY0) batchY0 = buffer[i].rowY0;
491
+ if (buffer[i].rowY1 > batchY1) batchY1 = buffer[i].rowY1;
492
+ }
493
+ out.trianglesBuffer.reset(buffer);
494
+ }
495
+ out.trianglesCount = count;
496
+ out.y0 = batchY0;
497
+ out.y1 = batchY1;
498
+ break;
499
+ }
500
+ case gea::platform::display::DisplayPresentCommandType::DrawImage:
501
+ if (!command.drawImage.pixels || command.drawImage.srcWidth <= 0 || command.drawImage.srcHeight <= 0) return false;
502
+ out.pixels = command.drawImage.pixels;
503
+ out.alphaPixels = command.drawImage.alphaPixels;
504
+ out.srcWidth = command.drawImage.srcWidth;
505
+ out.srcHeight = command.drawImage.srcHeight;
506
+ out.x = command.drawImage.x;
507
+ out.y = command.drawImage.y;
508
+ out.alpha = command.drawImage.alpha;
509
+ break;
510
+ case gea::platform::display::DisplayPresentCommandType::DrawImageScaled:
511
+ if (!command.drawImageScaled.pixels || command.drawImageScaled.srcWidth <= 0 ||
512
+ command.drawImageScaled.srcHeight <= 0 || command.drawImageScaled.w <= 0 ||
513
+ command.drawImageScaled.h <= 0)
514
+ return false;
515
+ out.pixels = command.drawImageScaled.pixels;
516
+ out.alphaPixels = command.drawImageScaled.alphaPixels;
517
+ out.srcWidth = command.drawImageScaled.srcWidth;
518
+ out.srcHeight = command.drawImageScaled.srcHeight;
519
+ out.x = command.drawImageScaled.x;
520
+ out.y = command.drawImageScaled.y;
521
+ out.w = command.drawImageScaled.w;
522
+ out.h = command.drawImageScaled.h;
523
+ out.alpha = command.drawImageScaled.alpha;
524
+ out.radius = command.drawImageScaled.radius;
525
+ break;
526
+ case gea::platform::display::DisplayPresentCommandType::DrawImageRotated90CW:
527
+ if (!command.drawImageRotated90CW.pixels || command.drawImageRotated90CW.srcWidth <= 0 ||
528
+ command.drawImageRotated90CW.srcHeight <= 0 || command.drawImageRotated90CW.w <= 0 ||
529
+ command.drawImageRotated90CW.h <= 0)
530
+ return false;
531
+ out.pixels = command.drawImageRotated90CW.pixels;
532
+ out.alphaPixels = command.drawImageRotated90CW.alphaPixels;
533
+ out.srcWidth = command.drawImageRotated90CW.srcWidth;
534
+ out.srcHeight = command.drawImageRotated90CW.srcHeight;
535
+ out.x = command.drawImageRotated90CW.x;
536
+ out.y = command.drawImageRotated90CW.y;
537
+ out.w = command.drawImageRotated90CW.w;
538
+ out.h = command.drawImageRotated90CW.h;
539
+ out.alpha = command.drawImageRotated90CW.alpha;
540
+ break;
541
+ case gea::platform::display::DisplayPresentCommandType::DrawImageTiledX:
542
+ if (!command.drawImageTiledX.pixels || command.drawImageTiledX.srcWidth <= 0 ||
543
+ command.drawImageTiledX.srcHeight <= 0 || command.drawImageTiledX.w <= 0)
544
+ return false;
545
+ out.pixels = command.drawImageTiledX.pixels;
546
+ out.alphaPixels = command.drawImageTiledX.alphaPixels;
547
+ out.srcWidth = command.drawImageTiledX.srcWidth;
548
+ out.srcHeight = command.drawImageTiledX.srcHeight;
549
+ out.x = command.drawImageTiledX.x;
550
+ out.y = command.drawImageTiledX.y;
551
+ out.w = command.drawImageTiledX.w;
552
+ out.alpha = command.drawImageTiledX.alpha;
553
+ break;
554
+ case gea::platform::display::DisplayPresentCommandType::FillText:
555
+ if (!command.fillText.text) return false;
556
+ out.text = command.fillText.text;
557
+ out.x = command.fillText.x;
558
+ out.y = command.fillText.y;
559
+ out.color = command.fillText.color;
560
+ out.scale = command.fillText.scale;
561
+ out.fontFamilyId = command.fillText.fontFamilyId;
562
+ out.fontSizePx = command.fillText.fontSizePx;
563
+ out.alpha = command.fillText.alpha;
564
+ break;
565
+ }
566
+ frame.commands.push_back(std::move(out));
567
+ }
568
+ return true;
569
+ }
570
+
571
+ inline bool frameHasOpaqueBase(const Frame &frame, int width, int height)
572
+ {
573
+ if (frame.commands.empty()) return false;
574
+ const Command &first = frame.commands.front();
575
+ using Type = gea::platform::display::DisplayPresentCommandType;
576
+ if (first.type == Type::Clear) return true;
577
+ if (first.type != Type::FillRectRgb565 || first.alpha != 255) return false;
578
+ const Rect bounds = rectFromBox(first.x, first.y, first.w, first.h);
579
+ return bounds.x0 <= 0 && bounds.y0 <= 0 && bounds.x1 >= width - 1 && bounds.y1 >= height - 1;
580
+ }
581
+
582
+ inline int dirtyRects(const Frame *previous,
583
+ const Frame &current,
584
+ Rect *rects,
585
+ int capacity,
586
+ int width,
587
+ int height)
588
+ {
589
+ if (!rects || capacity <= 0 || width <= 0 || height <= 0) return 0;
590
+ if (!frameHasOpaqueBase(current, width, height)) {
591
+ rects[0] = fullScreen(width, height);
592
+ return 1;
593
+ }
594
+ if (!previous || !frameHasOpaqueBase(*previous, width, height)) {
595
+ rects[0] = fullScreen(width, height);
596
+ return 1;
597
+ }
598
+
599
+ int count = 0;
600
+ const std::size_t commandCount = std::max(previous->commands.size(), current.commands.size());
601
+ for (std::size_t i = 0; i < commandCount; i++) {
602
+ const Command *oldCommand = i < previous->commands.size() ? &previous->commands[i] : nullptr;
603
+ const Command *newCommand = i < current.commands.size() ? &current.commands[i] : nullptr;
604
+ if (oldCommand && newCommand && commandsEqual(*oldCommand, *newCommand)) continue;
605
+ if (oldCommand) addCommandBounds(*oldCommand, rects, &count, capacity, width, height);
606
+ if (newCommand) addCommandBounds(*newCommand, rects, &count, capacity, width, height);
607
+ }
608
+
609
+ int totalArea = 0;
610
+ for (int i = 0; i < count; i++) totalArea += area(rects[i]);
611
+ if (totalArea >= width * height) {
612
+ rects[0] = fullScreen(width, height);
613
+ return 1;
614
+ }
615
+ return count;
616
+ }
617
+
618
+ // Cheap vertical extent of a command, for skipping whole flush chunks.
619
+ // Returns false for types without a cheap extent (Clear, FillText, the
620
+ // pre-banded circle batch) — those must be visited by every chunk, and
621
+ // outY0/outY1 are set to the full display range.
622
+ inline bool commandRowExtent(const Command &command, int displayHeight, int &outY0, int &outY1)
623
+ {
624
+ using Type = gea::platform::display::DisplayPresentCommandType;
625
+ switch (command.type) {
626
+ case Type::FillRectRgb565:
627
+ case Type::StrokeRectRgb565:
628
+ outY0 = command.y;
629
+ outY1 = command.y + command.h - 1;
630
+ return true;
631
+ case Type::FillTriangleRgb565: {
632
+ const int lo01 = command.y0 < command.y1 ? command.y0 : command.y1;
633
+ const int hi01 = command.y0 > command.y1 ? command.y0 : command.y1;
634
+ outY0 = lo01 < command.y2 ? lo01 : command.y2;
635
+ outY1 = hi01 > command.y2 ? hi01 : command.y2;
636
+ return true;
637
+ }
638
+ case Type::FillTrianglesRgb565:
639
+ // Batch-wide extent cached at extract time; per-entry rejects happen
640
+ // inside the raster loop.
641
+ outY0 = command.y0;
642
+ outY1 = command.y1;
643
+ return true;
644
+ case Type::FillCircleRgb565:
645
+ case Type::StrokeCircleRgb565:
646
+ outY0 = command.y - command.radius;
647
+ outY1 = command.y + command.radius;
648
+ return true;
649
+ case Type::DrawImage:
650
+ case Type::DrawImageTiledX:
651
+ outY0 = command.y;
652
+ outY1 = command.y + command.srcHeight - 1;
653
+ return true;
654
+ case Type::DrawImageScaled:
655
+ case Type::DrawImageRotated90CW:
656
+ outY0 = command.y;
657
+ outY1 = command.y + command.h - 1;
658
+ return true;
659
+ case Type::FillText:
660
+ if (command.fontFamilyId < 0) {
661
+ const Rect bounds = textBounds(command);
662
+ outY0 = bounds.y0;
663
+ outY1 = bounds.y1;
664
+ return true;
665
+ }
666
+ break;
667
+ default:
668
+ break;
669
+ }
670
+ outY0 = 0;
671
+ outY1 = displayHeight - 1;
672
+ return false;
673
+ }
674
+
675
+ inline void rasterCommandRows(gea::framework::graphics::Canvas &canvas,
676
+ gea::framework::graphics::pixel::native_t *buffer,
677
+ int regionX0,
678
+ int regionWidth,
679
+ int row,
680
+ int chunkRows,
681
+ int displayHeight,
682
+ const Command &command)
683
+ {
684
+ if (!buffer) return;
685
+ using Type = gea::platform::display::DisplayPresentCommandType;
686
+ // Row-extent reject: the frame's command list is replayed once per flush
687
+ // chunk, so a command whose vertical extent misses [row, row+chunkRows)
688
+ // must cost O(1) here. Without this, every 16-row chunk dispatched EVERY
689
+ // command into Canvas (bind + primitive-level clipping) — ~6.3k dispatches
690
+ // per frame for a 360-triangle scene, ~30ms/frame on esp32-s3. With it, a
691
+ // triangle spanning ~40 rows is visited by the ~3 chunks it touches.
692
+ // (Callers that replay many chunks should hoist commandRowExtent() out of
693
+ // their chunk loop and skip before calling — this in-function reject is
694
+ // the safety net for one-shot callers.)
695
+ {
696
+ const int chunkY1 = row + chunkRows - 1;
697
+ int cmdY0 = 0;
698
+ int cmdY1 = displayHeight - 1;
699
+ const bool bounded = commandRowExtent(command, displayHeight, cmdY0, cmdY1);
700
+ if (bounded && (cmdY1 < row || cmdY0 > chunkY1)) return;
701
+ }
702
+ if (command.type == Type::Clear) {
703
+ const int pixelCount = regionWidth * chunkRows;
704
+ if (command.color == 0) std::memset(buffer, 0, static_cast<std::size_t>(pixelCount) * sizeof(std::uint16_t));
705
+ else std::fill_n(buffer, pixelCount, command.color);
706
+ return;
707
+ }
708
+
709
+ canvas.setGlobalAlpha(command.alpha);
710
+ const int ox = regionX0;
711
+ const int oy = row;
712
+ switch (command.type) {
713
+ case Type::Clear:
714
+ break;
715
+ case Type::FillRectRgb565:
716
+ // Present replay fills a fresh flush chunk from scratch, so Canvas::fillRect's
717
+ // persistent-surface optimizations (per-pixel dst-compare to skip unchanged pixels +
718
+ // run-based markDirty) are pure overhead here — every pixel is new and the present
719
+ // already owns the dirty region. Opaque rects (the full-screen background + floor
720
+ // band that cover most of every chunk) take the flat span fill; only translucent
721
+ // rects need the per-pixel blend.
722
+ if (command.alpha == 255)
723
+ canvas.fillRectOpaque(command.x - ox, command.y - oy, command.w, command.h, command.color);
724
+ else
725
+ canvas.fillRect(command.x - ox, command.y - oy, command.w, command.h, command.color);
726
+ break;
727
+ case Type::StrokeRectRgb565:
728
+ canvas.strokeRect(command.x - ox, command.y - oy, command.w, command.h, command.color);
729
+ break;
730
+ case Type::FillTriangleRgb565:
731
+ // Same rationale as the opaque-rect branch above: replay chunks are
732
+ // fresh, so skip the per-pixel dst-compare + markDirty bookkeeping.
733
+ if (command.alpha == 255)
734
+ canvas.fillTriangleOpaque(command.x0 - ox, command.y0 - oy,
735
+ command.x1 - ox, command.y1 - oy,
736
+ command.x2 - ox, command.y2 - oy,
737
+ command.color);
738
+ else
739
+ canvas.fillTriangle(command.x0 - ox, command.y0 - oy,
740
+ command.x1 - ox, command.y1 - oy,
741
+ command.x2 - ox, command.y2 - oy,
742
+ command.color);
743
+ break;
744
+ case Type::FillCircleRgb565:
745
+ canvas.fillCircle(command.x - ox, command.y - oy, command.radius, command.color);
746
+ break;
747
+ case Type::StrokeCircleRgb565:
748
+ canvas.strokeCircle(command.x - ox, command.y - oy, command.radius, command.color);
749
+ break;
750
+ case Type::FillCirclesRgb565: {
751
+ // Hand the whole sorted batch to Canvas's optimized batch entrypoint.
752
+ // It inlines fillCircleNoDirty's hot path and uses [chunkY0, chunkY1]
753
+ // as a world-Y band to skip leading + break trailing in one loop.
754
+ const int chunkY0 = row;
755
+ const int chunkY1 = row + chunkRows - 1;
756
+ canvas.fillCirclesRgb565WorldYSorted(command.circleEntries(),
757
+ command.circlesCount,
758
+ command.radius,
759
+ chunkY0, chunkY1, ox, oy);
760
+ break;
761
+ }
762
+ case Type::FillTrianglesRgb565: {
763
+ // Depth-ordered batch (farthest-first). OPAQUE batches take the span-
764
+ // occlusion path (front-to-back, each pixel written once); it band-rejects
765
+ // per chunk internally. TRANSLUCENT must stay painter's back-to-front.
766
+ const gea::framework::graphics::TriangleEntry *tris = command.trianglesBuffer.get();
767
+ const int count = command.trianglesCount;
768
+ if (!tris) break;
769
+ if (command.alpha == 255) {
770
+ canvas.fillTrianglesOpaqueOccluded(tris, count, ox, oy);
771
+ } else {
772
+ const int chunkY1 = row + chunkRows - 1;
773
+ for (int i = 0; i < count; i++) {
774
+ const auto &t = tris[i];
775
+ if (t.rowY1 < row || t.rowY0 > chunkY1) continue;
776
+ canvas.fillTriangle(t.x0 - ox, t.y0 - oy, t.x1 - ox, t.y1 - oy,
777
+ t.x2 - ox, t.y2 - oy, t.color);
778
+ }
779
+ }
780
+ break;
781
+ }
782
+ case Type::DrawImage:
783
+ canvas.drawImage(command.pixels, command.alphaPixels, command.srcWidth, command.srcHeight,
784
+ command.x - ox, command.y - oy);
785
+ break;
786
+ case Type::DrawImageScaled:
787
+ // A corner radius masks the destination box. All four equal to
788
+ // min(w,h)/2 is a circle -- how an OPAQUE sprite gets a round mask with
789
+ // no alpha plane at all, which is both smaller in flash and cheaper to
790
+ // blit than carrying a mask channel.
791
+ if (command.radius > 0) {
792
+ canvas.drawImageRounded(command.pixels, command.alphaPixels, command.srcWidth, command.srcHeight,
793
+ command.x - ox, command.y - oy, command.w, command.h,
794
+ command.radius, command.radius, command.radius, command.radius);
795
+ break;
796
+ }
797
+ // 1:1 "scaled" draws (integer-zoom map tiles) take the unscaled blit:
798
+ // row memcpys instead of per-pixel nearest sampling (~2.5x faster).
799
+ if (command.w == command.srcWidth && command.h == command.srcHeight)
800
+ canvas.drawImage(command.pixels, command.alphaPixels, command.srcWidth, command.srcHeight,
801
+ command.x - ox, command.y - oy);
802
+ else
803
+ canvas.drawImage(command.pixels, command.alphaPixels, command.srcWidth, command.srcHeight,
804
+ command.x - ox, command.y - oy, command.w, command.h);
805
+ break;
806
+ case Type::DrawImageRotated90CW:
807
+ canvas.drawImageRotated90CW(command.pixels, command.alphaPixels, command.srcWidth, command.srcHeight,
808
+ command.x - ox, command.y - oy, command.w, command.h);
809
+ break;
810
+ case Type::DrawImageTiledX:
811
+ canvas.drawImageTiledX(command.pixels, command.alphaPixels, command.srcWidth, command.srcHeight,
812
+ command.x - ox, command.y - oy, command.w);
813
+ break;
814
+ case Type::FillText:
815
+ if (command.fontFamilyId >= 0)
816
+ canvas.drawTextFontFamily(command.text.c_str(), command.x - ox, command.y - oy, command.color,
817
+ command.fontFamilyId, command.fontSizePx);
818
+ else
819
+ canvas.drawText(command.text.c_str(), command.x - ox, command.y - oy, command.color, command.scale);
820
+ break;
821
+ }
822
+ (void)displayHeight;
823
+ }
824
+
825
+ inline void rasterFrameRows(gea::framework::graphics::pixel::native_t *buffer,
826
+ int regionX0,
827
+ int regionWidth,
828
+ int row,
829
+ int chunkRows,
830
+ int displayHeight,
831
+ const Frame &frame)
832
+ {
833
+ if (!buffer || regionWidth <= 0 || chunkRows <= 0) return;
834
+ gea::framework::graphics::Canvas canvas;
835
+ canvas.bindPixels(buffer, regionWidth, chunkRows);
836
+ for (const Command &command : frame.commands) {
837
+ rasterCommandRows(canvas, buffer, regionX0, regionWidth, row, chunkRows, displayHeight, command);
838
+ }
839
+ }
840
+
841
+ inline void rasterFrameRowsStrided(gea::framework::graphics::pixel::native_t *buffer,
842
+ int stridePixels,
843
+ int regionX0,
844
+ int regionWidth,
845
+ int row,
846
+ int chunkRows,
847
+ int displayHeight,
848
+ const Frame &frame)
849
+ {
850
+ if (!buffer || stridePixels < regionWidth || regionWidth <= 0 || chunkRows <= 0) return;
851
+ gea::framework::graphics::Canvas canvas;
852
+ canvas.bindPixels(buffer, regionWidth, chunkRows, stridePixels);
853
+ for (const Command &command : frame.commands) {
854
+ if (command.type == gea::platform::display::DisplayPresentCommandType::Clear) {
855
+ for (int y = 0; y < chunkRows; ++y) {
856
+ gea::framework::graphics::pixel::native_t *dst = buffer + static_cast<std::size_t>(y) * stridePixels;
857
+ if (command.color == 0) {
858
+ std::memset(dst, 0, static_cast<std::size_t>(regionWidth) * sizeof(gea::framework::graphics::pixel::native_t));
859
+ } else {
860
+ std::fill_n(dst, regionWidth, command.color);
861
+ }
862
+ }
863
+ continue;
864
+ }
865
+ rasterCommandRows(canvas, buffer, regionX0, regionWidth, row, chunkRows, displayHeight, command);
866
+ }
867
+ }
868
+
869
+ } // namespace gea::framework::display_present