@geastack/core 0.1.39 → 0.1.40
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.
- package/events.cpp +2 -0
- package/gea_app_entry.cpp +2 -2
- package/gea_sources.mjs +4 -3
- package/include/audio.h +11 -1
- package/include/audio_oscillator.h +52 -0
- package/include/camera.h +21 -0
- package/include/captive_dns.h +71 -0
- package/include/display_present.h +66 -28
- package/include/events.h +31 -3
- package/include/gea/embedded.h +2 -0
- package/include/graphics/font.h +19 -0
- package/include/image.h +39 -1
- package/include/input.h +12 -1
- package/include/platform/atomic_file.h +72 -0
- package/include/platform/file_cache.h +6 -3
- package/include/power.h +2 -0
- package/include/services/frame_scheduler.h +6 -0
- package/include/wifi.h +60 -45
- package/index.d.ts +223 -44
- package/package.json +3 -3
- package/runtime/audio-worklet.ts +7 -11
- package/runtime/host.ts +132 -77
- package/runtime/images.ts +5 -0
- package/runtime-surface.ts +1 -0
- package/runtime.cpp +23 -5
- package/scripts/bitmap-font-source.mjs +155 -0
- package/scripts/build-gea-vite-geatsc.mjs +97 -6
- package/scripts/gea-embedded-asset-policy.mjs +38 -0
- package/scripts/gea-embedded-compat-transform.mjs +4 -2
- package/scripts/gea-vite-module-graph-plugin.mjs +64 -15
- package/scripts/generate-gea-embedded-assets.mjs +17 -3
- package/scripts/generate-gea-embedded-fonts.mjs +986 -670
package/events.cpp
CHANGED
package/gea_app_entry.cpp
CHANGED
|
@@ -268,7 +268,7 @@ void Application::frame(int timestampMs)
|
|
|
268
268
|
applicationFramePhaseSet(ApplicationFramePhase::DrainMicrotasks);
|
|
269
269
|
generated::drainMicrotasks();
|
|
270
270
|
applicationFramePhaseSet(ApplicationFramePhase::AnimationFrameCallbacks);
|
|
271
|
-
gea::host::runAnimationFrameCallbacks(
|
|
271
|
+
gea::host::runAnimationFrameCallbacks(timestampMs);
|
|
272
272
|
generated::drainMicrotasks();
|
|
273
273
|
applicationFramePhaseSet(ApplicationFramePhase::Idle);
|
|
274
274
|
#else
|
|
@@ -294,7 +294,7 @@ void Application::frame(int timestampMs)
|
|
|
294
294
|
|
|
295
295
|
startUs = phaseClockUs();
|
|
296
296
|
applicationFramePhaseSet(ApplicationFramePhase::AnimationFrameCallbacks);
|
|
297
|
-
gea::host::runAnimationFrameCallbacks(
|
|
297
|
+
gea::host::runAnimationFrameCallbacks(timestampMs);
|
|
298
298
|
applicationFramePhaseSet(ApplicationFramePhase::CallbackMicrotasks);
|
|
299
299
|
generated::drainMicrotasks();
|
|
300
300
|
recordFramePhase(ApplicationFramePhase::AnimationFrameCallbacks, phaseClockUs() - startUs);
|
package/gea_sources.mjs
CHANGED
|
@@ -59,7 +59,8 @@ export function includeFlags(env = process.env) {
|
|
|
59
59
|
join(elements, 'ui'),
|
|
60
60
|
geaos,
|
|
61
61
|
join(engine, 'vendor/stb'),
|
|
62
|
-
join(engine, 'vendor/AnimatedGIF')
|
|
62
|
+
join(engine, 'vendor/AnimatedGIF'),
|
|
63
|
+
...(env.GEA_COMPILER_RUNTIME ? [env.GEA_COMPILER_RUNTIME] : [])
|
|
63
64
|
].map((dir) => `-I${dir}`)
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -87,8 +88,8 @@ export function cxxSources(env = process.env) {
|
|
|
87
88
|
// @geastack/host — platform layer + services
|
|
88
89
|
...cpp(host, ['input', 'wifi', 'bluetooth', 'geolocation']),
|
|
89
90
|
...cpp(join(host, 'host'), [
|
|
90
|
-
'timers', 'worker', 'input', 'display', 'fetch', 'media', 'rtc', 'websocket', 'http', 'wifi',
|
|
91
|
-
'ble', 'apps', 'audio', 'audio_worklet', 'video', 'mjpeg', 'image', 'touch', 'imu', 'memory', 'camera', 'geolocation',
|
|
91
|
+
'timers', 'worker', 'input', 'device', 'display', 'fetch', 'media', 'rtc', 'websocket', 'http', 'wifi',
|
|
92
|
+
'ble', 'apps', 'audio', 'audio_worklet', 'audio_buffer_runtime', 'video', 'mjpeg', 'image', 'touch', 'imu', 'memory', 'camera', 'geolocation',
|
|
92
93
|
'tile_loader'
|
|
93
94
|
]),
|
|
94
95
|
// framework services (built on the platform abstraction — no target-specific deps)
|
package/include/audio.h
CHANGED
|
@@ -7,8 +7,18 @@
|
|
|
7
7
|
#include <string>
|
|
8
8
|
#include <functional>
|
|
9
9
|
|
|
10
|
+
// Physical capture/mixer clock shared by the board driver and native hosts.
|
|
11
|
+
// AudioContext and network PCM may use other rates through their resamplers.
|
|
12
|
+
#ifndef GEA_AUDIO_DEVICE_SAMPLE_RATE
|
|
13
|
+
#define GEA_AUDIO_DEVICE_SAMPLE_RATE 16000
|
|
14
|
+
#endif
|
|
15
|
+
|
|
10
16
|
namespace gea::platform::audio {
|
|
11
17
|
|
|
18
|
+
inline constexpr int deviceSampleRate = GEA_AUDIO_DEVICE_SAMPLE_RATE;
|
|
19
|
+
static_assert(deviceSampleRate >= 8000 && deviceSampleRate <= 48000,
|
|
20
|
+
"Audio device sample rate must be between 8000 and 48000 Hz");
|
|
21
|
+
|
|
12
22
|
enum class OscillatorType {
|
|
13
23
|
Sine = 0,
|
|
14
24
|
Square = 1,
|
|
@@ -74,7 +84,7 @@ public:
|
|
|
74
84
|
static bool playPcm(const std::int16_t *samples, std::size_t sample_count, int sample_rate, int channels);
|
|
75
85
|
static void stopPlayback();
|
|
76
86
|
static void flushPlayback();
|
|
77
|
-
// Continuous
|
|
87
|
+
// Continuous mono source at deviceSampleRate; zero means the platform could not attach it.
|
|
78
88
|
static std::uint64_t playPcmStream(std::function<std::size_t(std::int16_t*, std::size_t)> pull);
|
|
79
89
|
static void stopPcmStream(std::uint64_t stream);
|
|
80
90
|
static bool pcmStreamSettled(std::uint64_t stream);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#pragma once
|
|
3
|
+
|
|
4
|
+
#include <algorithm>
|
|
5
|
+
#include <optional>
|
|
6
|
+
|
|
7
|
+
namespace gea::platform::audio {
|
|
8
|
+
|
|
9
|
+
// Worklet render time and the physical output scheduler can have different
|
|
10
|
+
// origins. Translate an explicit context deadline at the host boundary.
|
|
11
|
+
inline double physicalAudioDeadline(double when, double contextNow,
|
|
12
|
+
double physicalNow) {
|
|
13
|
+
return when > 0 ? physicalNow + when - contextNow : when;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
struct FiniteToneTiming {
|
|
17
|
+
int durationMs;
|
|
18
|
+
int delayMs;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// The embedded oscillator backend schedules one finite tone when stop() is
|
|
22
|
+
// first called. It does not support replacing/cancelling an already queued
|
|
23
|
+
// individual voice. Further stops must never submit that voice a second time.
|
|
24
|
+
class FiniteOscillatorSchedule {
|
|
25
|
+
public:
|
|
26
|
+
void start(double when, double now) {
|
|
27
|
+
startTime_ = when > 0 ? when : now;
|
|
28
|
+
started_ = true;
|
|
29
|
+
submitted_ = false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
std::optional<FiniteToneTiming> stop(double when, double now) {
|
|
33
|
+
if (submitted_)
|
|
34
|
+
return std::nullopt;
|
|
35
|
+
submitted_ = true;
|
|
36
|
+
const double start = started_ ? startTime_ : now;
|
|
37
|
+
const double end = std::max(when > 0 ? when : now, start);
|
|
38
|
+
const int duration =
|
|
39
|
+
static_cast<int>(std::min((end - start) * 1000.0 + 0.5, 2500.0));
|
|
40
|
+
if (duration <= 0)
|
|
41
|
+
return std::nullopt;
|
|
42
|
+
return FiniteToneTiming{
|
|
43
|
+
duration, static_cast<int>(std::max(start - now, 0.0) * 1000.0 + 0.5)};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private:
|
|
47
|
+
double startTime_ = 0;
|
|
48
|
+
bool started_ = false;
|
|
49
|
+
bool submitted_ = false;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
} // namespace gea::platform::audio
|
package/include/camera.h
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#include "pixel.h"
|
|
4
4
|
|
|
5
5
|
#include <cstdint>
|
|
6
|
+
#include <atomic>
|
|
6
7
|
#include <string>
|
|
7
8
|
|
|
8
9
|
namespace gea::platform::camera {
|
|
@@ -20,6 +21,14 @@ struct DeviceInfo {
|
|
|
20
21
|
int sensorHeight = 0;
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
// Optional encoded-frame producer. The device owns capture/encoding on its
|
|
25
|
+
// worker; reading a frame never waits for a sensor or codec on the UI thread.
|
|
26
|
+
class CameraFrameProvider {
|
|
27
|
+
public:
|
|
28
|
+
virtual ~CameraFrameProvider() = default;
|
|
29
|
+
virtual std::string takeFrameDataUrl() = 0;
|
|
30
|
+
};
|
|
31
|
+
|
|
23
32
|
// Static facade — one active camera at a time on the device. The platform
|
|
24
33
|
// layer maintains the live preview buffer and exposes a small surface that
|
|
25
34
|
// the host bridge forwards into.
|
|
@@ -36,6 +45,12 @@ public:
|
|
|
36
45
|
static void close();
|
|
37
46
|
static bool isOpen();
|
|
38
47
|
|
|
48
|
+
static void setFrameProvider(CameraFrameProvider *provider) { frameProvider().store(provider); }
|
|
49
|
+
static std::string captureFrame() {
|
|
50
|
+
auto *provider = frameProvider().load();
|
|
51
|
+
return provider ? provider->takeFrameDataUrl() : std::string();
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
static int width();
|
|
40
55
|
static int height();
|
|
41
56
|
static int orientation();
|
|
@@ -102,6 +117,12 @@ public:
|
|
|
102
117
|
static void setWhiteBalance(const std::string &mode, double temperatureK, double tint);
|
|
103
118
|
static void setFocus(const std::string &mode, double pointX, double pointY);
|
|
104
119
|
static void setTorch(const std::string &mode, double level);
|
|
120
|
+
|
|
121
|
+
private:
|
|
122
|
+
static std::atomic<CameraFrameProvider *> &frameProvider() {
|
|
123
|
+
static std::atomic<CameraFrameProvider *> provider{nullptr};
|
|
124
|
+
return provider;
|
|
125
|
+
}
|
|
105
126
|
};
|
|
106
127
|
|
|
107
128
|
} // namespace gea::platform::camera
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#pragma once
|
|
3
|
+
|
|
4
|
+
#include <cstddef>
|
|
5
|
+
#include <cstdint>
|
|
6
|
+
#include <cstring>
|
|
7
|
+
|
|
8
|
+
namespace gea::framework::network {
|
|
9
|
+
|
|
10
|
+
// A bounded DNS responder for AP captive portals. Rebuild the question name
|
|
11
|
+
// to avoid retaining compression pointers into discarded additional records.
|
|
12
|
+
inline std::size_t captiveDnsReply(const std::uint8_t *query,
|
|
13
|
+
std::size_t length,
|
|
14
|
+
const std::uint8_t address[4],
|
|
15
|
+
std::uint8_t *reply, std::size_t capacity) {
|
|
16
|
+
if (length < 12 || length > 512 || capacity < 12 || (query[2] & 0xf8) ||
|
|
17
|
+
query[4] != 0 || query[5] != 1)
|
|
18
|
+
return 0;
|
|
19
|
+
std::memcpy(reply, query, 12);
|
|
20
|
+
reply[2] = 0x80 | (query[2] & 1);
|
|
21
|
+
reply[3] = 0x80;
|
|
22
|
+
std::memset(reply + 6, 0, 6);
|
|
23
|
+
std::size_t source = 12, target = 12, questionEnd = 0, jumps = 0,
|
|
24
|
+
nameBytes = 0;
|
|
25
|
+
for (;;) {
|
|
26
|
+
if (source >= length)
|
|
27
|
+
return 0;
|
|
28
|
+
const auto count = query[source++];
|
|
29
|
+
if ((count & 0xc0) == 0xc0) {
|
|
30
|
+
if (source >= length || ++jumps > 8)
|
|
31
|
+
return 0;
|
|
32
|
+
const auto offset = ((count & 0x3f) << 8) | query[source++];
|
|
33
|
+
if (!questionEnd)
|
|
34
|
+
questionEnd = source;
|
|
35
|
+
if (offset >= length)
|
|
36
|
+
return 0;
|
|
37
|
+
source = offset;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (count > 63 || source + count > length ||
|
|
41
|
+
target + count + 1 > capacity || (nameBytes += count + 1) > 255)
|
|
42
|
+
return 0;
|
|
43
|
+
reply[target++] = count;
|
|
44
|
+
if (!count)
|
|
45
|
+
break;
|
|
46
|
+
std::memcpy(reply + target, query + source, count);
|
|
47
|
+
target += count;
|
|
48
|
+
source += count;
|
|
49
|
+
}
|
|
50
|
+
if (!questionEnd)
|
|
51
|
+
questionEnd = source;
|
|
52
|
+
if (questionEnd + 4 > length || target + 4 > capacity)
|
|
53
|
+
return 0;
|
|
54
|
+
const auto type = (query[questionEnd] << 8) | query[questionEnd + 1];
|
|
55
|
+
const auto klass = (query[questionEnd + 2] << 8) | query[questionEnd + 3];
|
|
56
|
+
std::memcpy(reply + target, query + questionEnd, 4);
|
|
57
|
+
target += 4;
|
|
58
|
+
// AAAA and other record types receive an empty successful answer so clients
|
|
59
|
+
// can issue their IPv4 query; this portal only owns its AP IPv4 address.
|
|
60
|
+
if (klass != 1 || (type != 1 && type != 255))
|
|
61
|
+
return target;
|
|
62
|
+
if (target + 16 > capacity)
|
|
63
|
+
return 0;
|
|
64
|
+
constexpr std::uint8_t answer[] = {0xc0, 0x0c, 0, 1, 0, 1, 0, 0, 0, 30, 0, 4};
|
|
65
|
+
std::memcpy(reply + target, answer, sizeof(answer));
|
|
66
|
+
std::memcpy(reply + target + sizeof(answer), address, 4);
|
|
67
|
+
reply[7] = 1;
|
|
68
|
+
return target + 16;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
} // namespace gea::framework::network
|
|
@@ -97,6 +97,7 @@ struct HeapCapsFree {
|
|
|
97
97
|
static constexpr int kInlineCircleEntryCapacity = 32;
|
|
98
98
|
|
|
99
99
|
struct Command {
|
|
100
|
+
gea::framework::graphics::ImageReference imageReference;
|
|
100
101
|
using CircleEntry = gea::framework::graphics::CircleEntry;
|
|
101
102
|
|
|
102
103
|
gea::platform::display::DisplayPresentCommandType type =
|
|
@@ -149,6 +150,9 @@ struct Command {
|
|
|
149
150
|
|
|
150
151
|
struct Frame {
|
|
151
152
|
std::vector<Command> commands;
|
|
153
|
+
// Embedded canvas batches preserve bitmap drawing semantics: circle painter
|
|
154
|
+
// order and the established triangle edge rasterization remain observable.
|
|
155
|
+
bool bitmapSemantics = false;
|
|
152
156
|
|
|
153
157
|
void clear()
|
|
154
158
|
{
|
|
@@ -308,29 +312,44 @@ inline bool commandsEqual(const Command &a, const Command &b)
|
|
|
308
312
|
case Type::FillCircleRgb565:
|
|
309
313
|
case Type::StrokeCircleRgb565:
|
|
310
314
|
return a.x == b.x && a.y == b.y && a.radius == b.radius && a.color == b.color;
|
|
311
|
-
case Type::FillCirclesRgb565:
|
|
312
|
-
|
|
313
|
-
if (a.circlesCount
|
|
315
|
+
case Type::FillCirclesRgb565:
|
|
316
|
+
{
|
|
317
|
+
if (a.radius != b.radius || a.circlesCount != b.circlesCount)
|
|
318
|
+
return false;
|
|
319
|
+
if (a.circlesCount == 0)
|
|
320
|
+
return true;
|
|
314
321
|
const auto *ap = a.circleEntries();
|
|
315
322
|
const auto *bp = b.circleEntries();
|
|
316
|
-
if (!ap || !bp)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
323
|
+
if (!ap || !bp)
|
|
324
|
+
return ap == bp;
|
|
325
|
+
for (int i = 0; i < a.circlesCount; ++i)
|
|
326
|
+
if (ap[i].x != bp[i].x || ap[i].y != bp[i].y || ap[i].color != bp[i].color)
|
|
327
|
+
return false;
|
|
328
|
+
return true;
|
|
320
329
|
}
|
|
321
|
-
case Type::FillTrianglesRgb565:
|
|
322
|
-
|
|
323
|
-
if (a.trianglesCount
|
|
330
|
+
case Type::FillTrianglesRgb565:
|
|
331
|
+
{
|
|
332
|
+
if (a.trianglesCount != b.trianglesCount)
|
|
333
|
+
return false;
|
|
334
|
+
if (a.trianglesCount == 0)
|
|
335
|
+
return true;
|
|
324
336
|
const auto *ap = a.trianglesBuffer.get();
|
|
325
337
|
const auto *bp = b.trianglesBuffer.get();
|
|
326
|
-
if (!ap || !bp)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
338
|
+
if (!ap || !bp)
|
|
339
|
+
return ap == bp;
|
|
340
|
+
for (int i = 0; i < a.trianglesCount; ++i)
|
|
341
|
+
{
|
|
342
|
+
const auto &x = ap[i];
|
|
343
|
+
const auto &y = bp[i];
|
|
344
|
+
if (x.x0 != y.x0 || x.y0 != y.y0 || x.x1 != y.x1 || x.y1 != y.y1 || x.x2 != y.x2 ||
|
|
345
|
+
x.y2 != y.y2 || x.color != y.color || x.rowY0 != y.rowY0 || x.rowY1 != y.rowY1)
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
return true;
|
|
330
349
|
}
|
|
331
350
|
case Type::DrawImage:
|
|
332
351
|
return a.pixels == b.pixels && a.alphaPixels == b.alphaPixels && imagePixelsStable(a) &&
|
|
333
|
-
|
|
352
|
+
a.srcWidth == b.srcWidth && a.srcHeight == b.srcHeight && a.x == b.x && a.y == b.y;
|
|
334
353
|
case Type::DrawImageScaled:
|
|
335
354
|
case Type::DrawImageRotated90CW:
|
|
336
355
|
return a.pixels == b.pixels && a.alphaPixels == b.alphaPixels && imagePixelsStable(a) &&
|
|
@@ -347,8 +366,9 @@ inline bool commandsEqual(const Command &a, const Command &b)
|
|
|
347
366
|
return false;
|
|
348
367
|
}
|
|
349
368
|
|
|
350
|
-
inline bool extractFrame(const gea::platform::display::DisplayPresentCommand *commands, int commandCount, Frame &frame)
|
|
369
|
+
inline bool extractFrame(const gea::platform::display::DisplayPresentCommand *commands, int commandCount, Frame &frame, bool bitmapSemantics = false)
|
|
351
370
|
{
|
|
371
|
+
frame.bitmapSemantics = bitmapSemantics;
|
|
352
372
|
frame.clear();
|
|
353
373
|
if (!commands || commandCount <= 0) return false;
|
|
354
374
|
frame.commands.reserve(static_cast<std::size_t>(commandCount));
|
|
@@ -435,7 +455,7 @@ inline bool extractFrame(const gea::platform::display::DisplayPresentCommand *co
|
|
|
435
455
|
}
|
|
436
456
|
bool sortedByBucket = false;
|
|
437
457
|
constexpr int kCircleBucketSortMaxYRange = 2048;
|
|
438
|
-
if (count <= 65535) {
|
|
458
|
+
if (!bitmapSemantics && count <= 65535) {
|
|
439
459
|
int minY = static_cast<int>(src.ys[0]);
|
|
440
460
|
int maxY = minY;
|
|
441
461
|
for (int i = 1; i < count; i++) {
|
|
@@ -466,8 +486,9 @@ inline bool extractFrame(const gea::platform::display::DisplayPresentCommand *co
|
|
|
466
486
|
for (int i = 0; i < count; i++) {
|
|
467
487
|
buffer[i] = CircleEntry{src.ys[i], src.xs[i], src.colors[i]};
|
|
468
488
|
}
|
|
469
|
-
|
|
470
|
-
|
|
489
|
+
if (!bitmapSemantics)
|
|
490
|
+
std::sort(buffer, buffer + count,
|
|
491
|
+
[](const CircleEntry &a, const CircleEntry &b) { return a.y < b.y; });
|
|
471
492
|
}
|
|
472
493
|
if (!out.circlesInline) out.circlesBuffer.reset(buffer);
|
|
473
494
|
}
|
|
@@ -571,6 +592,8 @@ inline bool extractFrame(const gea::platform::display::DisplayPresentCommand *co
|
|
|
571
592
|
out.alpha = command.fillText.alpha;
|
|
572
593
|
break;
|
|
573
594
|
}
|
|
595
|
+
out.imageReference = gea::framework::graphics::ImageReference(
|
|
596
|
+
gea::framework::graphics::ImageStore::instance().idForPixels(out.pixels));
|
|
574
597
|
frame.commands.push_back(std::move(out));
|
|
575
598
|
}
|
|
576
599
|
return true;
|
|
@@ -696,7 +719,8 @@ inline void rasterCommandRows(gea::framework::graphics::Canvas &canvas,
|
|
|
696
719
|
int row,
|
|
697
720
|
int chunkRows,
|
|
698
721
|
int displayHeight,
|
|
699
|
-
const Command &command
|
|
722
|
+
const Command &command,
|
|
723
|
+
bool bitmapSemantics = false)
|
|
700
724
|
{
|
|
701
725
|
if (!buffer) return;
|
|
702
726
|
using Type = gea::platform::display::DisplayPresentCommandType;
|
|
@@ -765,6 +789,13 @@ inline void rasterCommandRows(gea::framework::graphics::Canvas &canvas,
|
|
|
765
789
|
canvas.strokeCircle(command.x - ox, command.y - oy, command.radius, command.color);
|
|
766
790
|
break;
|
|
767
791
|
case Type::FillCirclesRgb565: {
|
|
792
|
+
if (bitmapSemantics) {
|
|
793
|
+
const auto *circles = command.circleEntries();
|
|
794
|
+
for (int i = 0; circles && i < command.circlesCount; ++i)
|
|
795
|
+
canvas.fillCircle(int(circles[i].x) - ox, int(circles[i].y) - oy,
|
|
796
|
+
command.radius, circles[i].color);
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
768
799
|
// Hand the whole sorted batch to Canvas's optimized batch entrypoint.
|
|
769
800
|
// It inlines fillCircleNoDirty's hot path and uses [chunkY0, chunkY1]
|
|
770
801
|
// as a world-Y band to skip leading + break trailing in one loop.
|
|
@@ -777,21 +808,28 @@ inline void rasterCommandRows(gea::framework::graphics::Canvas &canvas,
|
|
|
777
808
|
break;
|
|
778
809
|
}
|
|
779
810
|
case Type::FillTrianglesRgb565: {
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
811
|
+
// Full-display opaque 3D batches retain their front-to-back occlusion
|
|
812
|
+
// optimization. Bitmap canvas batches and translucent geometry replay
|
|
813
|
+
// in producer order, rejecting triangles outside this strip first.
|
|
783
814
|
const gea::framework::graphics::TriangleEntry *tris = command.trianglesBuffer.get();
|
|
784
815
|
const int count = command.trianglesCount;
|
|
785
816
|
if (!tris) break;
|
|
786
|
-
if (command.alpha == 255) {
|
|
817
|
+
if (command.alpha == 255 && !bitmapSemantics) {
|
|
787
818
|
canvas.fillTrianglesOpaqueOccluded(tris, count, ox, oy);
|
|
788
819
|
} else {
|
|
820
|
+
// Retained canvas batches preserve producer order. Sparse numeric
|
|
821
|
+
// geometry needs no coverage bitset; opaque spans avoid per-pixel
|
|
822
|
+
// comparisons while sharing the public triangle edge definition.
|
|
789
823
|
const int chunkY1 = row + chunkRows - 1;
|
|
790
824
|
for (int i = 0; i < count; i++) {
|
|
791
825
|
const auto &t = tris[i];
|
|
792
826
|
if (t.rowY1 < row || t.rowY0 > chunkY1) continue;
|
|
793
|
-
|
|
794
|
-
|
|
827
|
+
if (command.alpha == 255)
|
|
828
|
+
canvas.fillTriangleOpaque(t.x0 - ox, t.y0 - oy, t.x1 - ox, t.y1 - oy,
|
|
829
|
+
t.x2 - ox, t.y2 - oy, t.color);
|
|
830
|
+
else
|
|
831
|
+
canvas.fillTriangle(t.x0 - ox, t.y0 - oy, t.x1 - ox, t.y1 - oy,
|
|
832
|
+
t.x2 - ox, t.y2 - oy, t.color);
|
|
795
833
|
}
|
|
796
834
|
}
|
|
797
835
|
break;
|
|
@@ -851,7 +889,7 @@ inline void rasterFrameRows(gea::framework::graphics::pixel::native_t *buffer,
|
|
|
851
889
|
gea::framework::graphics::Canvas canvas;
|
|
852
890
|
canvas.bindPixels(buffer, regionWidth, chunkRows);
|
|
853
891
|
for (const Command &command : frame.commands) {
|
|
854
|
-
rasterCommandRows(canvas, buffer, regionX0, regionWidth, row, chunkRows, displayHeight, command);
|
|
892
|
+
rasterCommandRows(canvas, buffer, regionX0, regionWidth, row, chunkRows, displayHeight, command, frame.bitmapSemantics);
|
|
855
893
|
}
|
|
856
894
|
}
|
|
857
895
|
|
|
@@ -879,7 +917,7 @@ inline void rasterFrameRowsStrided(gea::framework::graphics::pixel::native_t *bu
|
|
|
879
917
|
}
|
|
880
918
|
continue;
|
|
881
919
|
}
|
|
882
|
-
rasterCommandRows(canvas, buffer, regionX0, regionWidth, row, chunkRows, displayHeight, command);
|
|
920
|
+
rasterCommandRows(canvas, buffer, regionX0, regionWidth, row, chunkRows, displayHeight, command, frame.bitmapSemantics);
|
|
883
921
|
}
|
|
884
922
|
}
|
|
885
923
|
|
package/include/events.h
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
#include <cstdint>
|
|
6
6
|
#include <functional>
|
|
7
|
+
#include <memory>
|
|
7
8
|
|
|
8
9
|
namespace gea::framework::events {
|
|
9
10
|
|
|
@@ -15,7 +16,8 @@ enum class PointerEventType {
|
|
|
15
16
|
Input,
|
|
16
17
|
KeyDown,
|
|
17
18
|
Rotary,
|
|
18
|
-
Scroll
|
|
19
|
+
Scroll,
|
|
20
|
+
KeyUp
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
enum class EventPhase {
|
|
@@ -51,6 +53,32 @@ struct TouchPoint {
|
|
|
51
53
|
int pageY = 0;
|
|
52
54
|
};
|
|
53
55
|
|
|
56
|
+
// Native callback lowering may copy the event carrier. Its cancellation flags
|
|
57
|
+
// still belong to the same dispatched event, including copies retained later.
|
|
58
|
+
// Allocate shared storage only when a carrier is actually copied.
|
|
59
|
+
class EventControlFlag {
|
|
60
|
+
public:
|
|
61
|
+
EventControlFlag(bool value = false) : initial_(value) {}
|
|
62
|
+
EventControlFlag(const EventControlFlag &other) : shared_(other.share()) {}
|
|
63
|
+
EventControlFlag &operator=(const EventControlFlag &other) {
|
|
64
|
+
if (this != &other) shared_ = other.share();
|
|
65
|
+
return *this;
|
|
66
|
+
}
|
|
67
|
+
EventControlFlag &operator=(bool value) {
|
|
68
|
+
if (shared_) *shared_ = value;
|
|
69
|
+
else initial_ = value;
|
|
70
|
+
return *this;
|
|
71
|
+
}
|
|
72
|
+
operator bool() const { return shared_ ? *shared_ : initial_; }
|
|
73
|
+
private:
|
|
74
|
+
std::shared_ptr<bool> share() const {
|
|
75
|
+
if (!shared_) shared_ = std::make_shared<bool>(initial_);
|
|
76
|
+
return shared_;
|
|
77
|
+
}
|
|
78
|
+
bool initial_ = false;
|
|
79
|
+
mutable std::shared_ptr<bool> shared_;
|
|
80
|
+
};
|
|
81
|
+
|
|
54
82
|
struct PointerEvent {
|
|
55
83
|
PointerEventType type;
|
|
56
84
|
EventTarget target{};
|
|
@@ -73,8 +101,8 @@ struct PointerEvent {
|
|
|
73
101
|
bool primary = true;
|
|
74
102
|
bool bubbles = true;
|
|
75
103
|
bool cancelable = true;
|
|
76
|
-
|
|
77
|
-
|
|
104
|
+
EventControlFlag defaultPrevented;
|
|
105
|
+
EventControlFlag propagationStopped;
|
|
78
106
|
EventPhase eventPhase = EventPhase::None;
|
|
79
107
|
TouchPoint touches[1]{};
|
|
80
108
|
TouchPoint targetTouches[1]{};
|
package/include/gea/embedded.h
CHANGED
package/include/graphics/font.h
CHANGED
|
@@ -13,6 +13,7 @@ struct Glyph {
|
|
|
13
13
|
int advance = 0;
|
|
14
14
|
int bearingX = 0;
|
|
15
15
|
int bearingY = 0;
|
|
16
|
+
int advance16 = -1;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
class Font {
|
|
@@ -25,6 +26,12 @@ public:
|
|
|
25
26
|
virtual int ascender() const = 0;
|
|
26
27
|
virtual bool glyph(int codepoint, Glyph *out) const = 0;
|
|
27
28
|
virtual std::uint8_t coverage(const Glyph &glyph, int row, int col) const = 0;
|
|
29
|
+
virtual int advance(int codepoint, int nextCodepoint = 0) const
|
|
30
|
+
{
|
|
31
|
+
(void)nextCodepoint;
|
|
32
|
+
Glyph value{};
|
|
33
|
+
return glyph(codepoint, &value) ? value.advance : sizePx() / 2;
|
|
34
|
+
}
|
|
28
35
|
};
|
|
29
36
|
|
|
30
37
|
class BitmapFont : public Font {
|
|
@@ -63,6 +70,13 @@ private:
|
|
|
63
70
|
static const std::uint8_t glyphs[kGlyphCount][kHeight];
|
|
64
71
|
};
|
|
65
72
|
|
|
73
|
+
struct FontKerningPair
|
|
74
|
+
{
|
|
75
|
+
int left = 0;
|
|
76
|
+
int right = 0;
|
|
77
|
+
int adjustment16 = 0;
|
|
78
|
+
};
|
|
79
|
+
|
|
66
80
|
struct RasterizedFontData {
|
|
67
81
|
int id = 0;
|
|
68
82
|
int sizePx = 0;
|
|
@@ -80,6 +94,10 @@ struct RasterizedFontData {
|
|
|
80
94
|
// byte-aligned (stride = (atlasWidth + 3) / 4) and the leftmost pixel of a
|
|
81
95
|
// byte sits in the most significant bit pair.
|
|
82
96
|
int atlasBits = 8;
|
|
97
|
+
int kerningCount = 0;
|
|
98
|
+
const FontKerningPair *kerning = nullptr;
|
|
99
|
+
bool strictGlyphLookup = false;
|
|
100
|
+
int fallbackCodepoint = -1;
|
|
83
101
|
};
|
|
84
102
|
|
|
85
103
|
class RasterizedFont final : public Font {
|
|
@@ -93,6 +111,7 @@ public:
|
|
|
93
111
|
int ascender() const final;
|
|
94
112
|
bool glyph(int codepoint, Glyph *out) const final;
|
|
95
113
|
std::uint8_t coverage(const Glyph &glyph, int row, int col) const final;
|
|
114
|
+
int advance(int codepoint, int nextCodepoint = 0) const final;
|
|
96
115
|
const RasterizedFontData *data() const { return data_; }
|
|
97
116
|
|
|
98
117
|
private:
|
package/include/image.h
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#include "pixel.h"
|
|
4
4
|
|
|
5
5
|
#include <cstdint>
|
|
6
|
+
#include <utility>
|
|
6
7
|
|
|
7
8
|
#if GEA_EMBEDDED_SINGLE_THREAD_IMAGE_STORE
|
|
8
9
|
namespace gea::framework::graphics {
|
|
@@ -26,7 +27,10 @@ using ImageStoreMutex = std::mutex;
|
|
|
26
27
|
// tile-grid app (maps) keep ~4 screens of decoded tiles resident in PSRAM for
|
|
27
28
|
// instant pan-back, on top of its persistent SD cache. ~128 KiB/tile (RGB565
|
|
28
29
|
// 256x256) when full → ~12 MiB; the P4 has ample PSRAM.
|
|
29
|
-
|
|
30
|
+
#ifndef GEA_EMBEDDED_MAX_IMAGES
|
|
31
|
+
#define GEA_EMBEDDED_MAX_IMAGES 96
|
|
32
|
+
#endif
|
|
33
|
+
inline constexpr int kImageMax = GEA_EMBEDDED_MAX_IMAGES;
|
|
30
34
|
|
|
31
35
|
enum class ImageFit {
|
|
32
36
|
Fill = 0,
|
|
@@ -119,6 +123,10 @@ public:
|
|
|
119
123
|
const pixel::native_t *framePixels(int id, int frame) const;
|
|
120
124
|
const pixel::native_t *currentPixels(int id) const;
|
|
121
125
|
const std::uint8_t *currentAlpha(int id) const;
|
|
126
|
+
// Pins keep decoded pixels alive for nodes and retained render commands.
|
|
127
|
+
bool retain(int id);
|
|
128
|
+
void release(int id);
|
|
129
|
+
int idForPixels(const pixel::native_t *pixels) const;
|
|
122
130
|
void dispose(int id);
|
|
123
131
|
void disposeAll();
|
|
124
132
|
ImageFormat detectFormat(const std::uint8_t *data, int length) const;
|
|
@@ -150,6 +158,8 @@ private:
|
|
|
150
158
|
|
|
151
159
|
ImageSlot images_[kImageMax]{};
|
|
152
160
|
bool used_[kImageMax]{};
|
|
161
|
+
unsigned references_[kImageMax]{};
|
|
162
|
+
bool retired_[kImageMax]{};
|
|
153
163
|
// Guards slot ALLOCATION and DISPOSAL only (reservation-style): the async
|
|
154
164
|
// tile loader decodes on the second core while the frame task blits and
|
|
155
165
|
// evicts. A decode reserves its slot under the lock, then fills it
|
|
@@ -159,4 +169,32 @@ private:
|
|
|
159
169
|
mutable ImageStoreMutex slotMutex_;
|
|
160
170
|
};
|
|
161
171
|
|
|
172
|
+
// Copyable ownership for retained commands; raw pixel pointers alone do not
|
|
173
|
+
// keep a decoded slot alive.
|
|
174
|
+
class ImageReference {
|
|
175
|
+
public:
|
|
176
|
+
ImageReference() = default;
|
|
177
|
+
explicit ImageReference(int id) : id_(ImageStore::instance().retain(id) ? id : -1) {}
|
|
178
|
+
ImageReference(const ImageReference &other) : ImageReference(other.id_) {}
|
|
179
|
+
ImageReference(ImageReference &&other) noexcept : id_(other.id_) { other.id_ = -1; }
|
|
180
|
+
~ImageReference() { ImageStore::instance().release(id_); }
|
|
181
|
+
ImageReference &operator=(const ImageReference &other) {
|
|
182
|
+
if (this != &other) {
|
|
183
|
+
ImageReference next(other);
|
|
184
|
+
std::swap(id_, next.id_);
|
|
185
|
+
}
|
|
186
|
+
return *this;
|
|
187
|
+
}
|
|
188
|
+
ImageReference &operator=(ImageReference &&other) noexcept {
|
|
189
|
+
if (this != &other) {
|
|
190
|
+
ImageStore::instance().release(id_);
|
|
191
|
+
id_ = other.id_;
|
|
192
|
+
other.id_ = -1;
|
|
193
|
+
}
|
|
194
|
+
return *this;
|
|
195
|
+
}
|
|
196
|
+
private:
|
|
197
|
+
int id_ = -1;
|
|
198
|
+
};
|
|
199
|
+
|
|
162
200
|
} // namespace gea::framework::graphics
|
package/include/input.h
CHANGED
|
@@ -27,7 +27,18 @@ int consumeRotaryDelta();
|
|
|
27
27
|
// `keydown` event. keyCode 0 is ignored.
|
|
28
28
|
void queueKeyDown(int keyCode);
|
|
29
29
|
|
|
30
|
+
// Queue a release in the same ordered stream as key presses.
|
|
31
|
+
void queueKeyUp(int keyCode);
|
|
32
|
+
|
|
33
|
+
struct HardwareKeyEvent {
|
|
34
|
+
int keyCode;
|
|
35
|
+
bool pressed;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Consume the oldest press or release without changing their order.
|
|
39
|
+
bool consumeKeyEvent(HardwareKeyEvent &event);
|
|
40
|
+
|
|
30
41
|
// Reads and clears the oldest pending key press. Returns 0 when none pending.
|
|
31
42
|
int consumeKeyDown();
|
|
32
43
|
|
|
33
|
-
}
|
|
44
|
+
} // namespace gea::framework::input
|