@geastack/core 0.1.33 → 0.1.35

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.
@@ -30,6 +30,12 @@ const char *applicationFramePhaseName(ApplicationFramePhase phase)
30
30
  case ApplicationFramePhase::StyleRecompute: return "style_recompute";
31
31
  case ApplicationFramePhase::DocumentFrame: return "document_frame";
32
32
  case ApplicationFramePhase::RefreshMounted: return "refresh_mounted";
33
+ case ApplicationFramePhase::CycleCollection: return "cycle_collection";
34
+ case ApplicationFramePhase::CallbackMicrotasks: return "callback_microtasks";
35
+ case ApplicationFramePhase::WebSocketCallbacks: return "websocket_callbacks";
36
+ case ApplicationFramePhase::RtcCallbacks: return "rtc_callbacks";
37
+ case ApplicationFramePhase::VideoPresentation: return "video_presentation";
38
+ case ApplicationFramePhase::HttpRequests: return "http_requests";
33
39
  case ApplicationFramePhase::Idle: return "idle";
34
40
  }
35
41
  return "idle";
@@ -59,6 +65,12 @@ void applicationFramePerfStatsAdd(ApplicationFramePerfStats &stats, ApplicationF
59
65
  case ApplicationFramePhase::RefreshMounted:
60
66
  stats.refreshMountedUs += durationUs;
61
67
  break;
68
+ case ApplicationFramePhase::CycleCollection:
69
+ case ApplicationFramePhase::CallbackMicrotasks:
70
+ case ApplicationFramePhase::WebSocketCallbacks:
71
+ case ApplicationFramePhase::RtcCallbacks:
72
+ case ApplicationFramePhase::VideoPresentation:
73
+ case ApplicationFramePhase::HttpRequests:
62
74
  case ApplicationFramePhase::Idle:
63
75
  break;
64
76
  }
@@ -0,0 +1,49 @@
1
+ import type { AudioContext, AudioDestinationNode } from './index'
2
+ import type { MessagePort } from './workers'
3
+
4
+ export interface AudioContextOptions {
5
+ sampleRate?: number
6
+ latencyHint?: 'interactive' | 'balanced' | 'playback' | number
7
+ }
8
+
9
+ export interface AudioWorklet {
10
+ addModule(moduleURL: string | URL): Promise<void>
11
+ }
12
+
13
+ export interface AudioWorkletNodeOptions {
14
+ numberOfInputs?: number
15
+ numberOfOutputs?: number
16
+ channelCount?: number
17
+ outputChannelCount?: number[]
18
+ }
19
+
20
+ export interface AudioWorkletNode {
21
+ readonly port: MessagePort
22
+ onprocessorerror: ((event: { readonly message: string }) => void) | null
23
+ connect(destination: AudioDestinationNode): AudioDestinationNode
24
+ disconnect(): void
25
+ }
26
+
27
+ export interface AudioWorkletNodeConstructor {
28
+ new (context: AudioContext, name: string, options?: AudioWorkletNodeOptions): AudioWorkletNode
29
+ }
30
+
31
+ export interface MediaStreamAudioSourceNode {
32
+ connect(destination: AudioWorkletNode): AudioWorkletNode
33
+ disconnect(): void
34
+ }
35
+
36
+ declare global {
37
+ const AudioWorkletNode: AudioWorkletNodeConstructor
38
+ abstract class AudioWorkletProcessor {
39
+ readonly port: MessagePort
40
+ constructor()
41
+ abstract process(inputs: Float32Array[][], outputs: Float32Array[][],
42
+ parameters: Record<string, Float32Array>): boolean
43
+ }
44
+ function registerProcessor(name: string, processorCtor: new () => AudioWorkletProcessor): void
45
+ /** AudioWorkletGlobalScope clock, in the context's sample-rate domain. */
46
+ const sampleRate: number
47
+ const currentFrame: number
48
+ const currentTime: number
49
+ }
package/gea_app_entry.cpp CHANGED
@@ -6,6 +6,7 @@
6
6
  #include "gea_perf_config.h" // GEA_EMBEDDED_PERF master + per-subsystem perf flags
7
7
  #include "gea/embedded.h"
8
8
  #include "host/backends.h"
9
+ #include "host/worker.h"
9
10
  #include "ui/style.h"
10
11
  #include "ui/tree_internal.h" // Tree::mount for the post-mount clean repaint in init()
11
12
 
@@ -64,7 +65,23 @@ public:
64
65
  {
65
66
  if (active_) gea_cycle_collection_defer_begin();
66
67
  }
67
- ~FrameCycleCollectionDeferral() { if (active_) gea_cycle_collection_defer_end(); }
68
+ ~FrameCycleCollectionDeferral() {
69
+ #if defined(ESP_PLATFORM) && CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS
70
+ const auto started = nowUs();
71
+ #endif
72
+ if (active_) {
73
+ // Scope-exit collection runs after frame phases finish. Keep it
74
+ // visible to the watchdog instead of reporting a stalled frame as idle.
75
+ const auto previous = applicationFramePhaseRead();
76
+ applicationFramePhaseSet(ApplicationFramePhase::CycleCollection);
77
+ gea_cycle_collection_defer_end();
78
+ applicationFramePhaseSet(previous);
79
+ }
80
+ #if defined(ESP_PLATFORM) && CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS
81
+ const auto elapsed = nowUs() - started;
82
+ if (elapsed > 30000) ESP_LOGW("frame_phase", "cycle_collection duration_ms=%lld", (long long)(elapsed / 1000));
83
+ #endif
84
+ }
68
85
 
69
86
  private:
70
87
  bool active_;
@@ -209,7 +226,7 @@ void logFramePerfWindow(int64_t windowEndUs)
209
226
  // pays zero for phase instrumentation.
210
227
  std::int64_t phaseClockUs()
211
228
  {
212
- #if GEA_FRAME_PHASE_TIMING
229
+ #if GEA_FRAME_PHASE_TIMING || CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS
213
230
  return nowUs();
214
231
  #else
215
232
  return 0;
@@ -222,6 +239,11 @@ std::int64_t phaseClockUs()
222
239
  // neither is set.
223
240
  void recordFramePhase(ApplicationFramePhase phase, int64_t durationUs)
224
241
  {
242
+ #if defined(ESP_PLATFORM) && CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS
243
+ if (durationUs > 30000)
244
+ ESP_LOGW("frame_phase", "%s duration_ms=%lld", applicationFramePhaseName(phase),
245
+ static_cast<long long>(durationUs / 1000));
246
+ #endif
225
247
  #if GEA_EMBEDDED_FRAME_SCHEDULER_PERF_LOG
226
248
  applicationFramePerfStatsAdd(phase, durationUs);
227
249
  #endif
@@ -273,11 +295,22 @@ void Application::frame(int timestampMs)
273
295
  startUs = phaseClockUs();
274
296
  applicationFramePhaseSet(ApplicationFramePhase::AnimationFrameCallbacks);
275
297
  gea::host::runAnimationFrameCallbacks(static_cast<double>(timestampMs));
298
+ applicationFramePhaseSet(ApplicationFramePhase::CallbackMicrotasks);
276
299
  generated::drainMicrotasks();
277
- // Offline builds have no network events to drain. Avoid locking empty queues every frame.
300
+ // The build already proves whether network services are reachable. Keep their
301
+ // callback pumps out of offline frame loops, while still servicing local workers.
278
302
  #if !defined(GEA_EMBEDDED_NETWORK_SERVICES_DISABLED) || !GEA_EMBEDDED_NETWORK_SERVICES_DISABLED
303
+ applicationFramePhaseSet(ApplicationFramePhase::WebSocketCallbacks);
279
304
  gea::host::websocket::runCallbacks();
305
+ applicationFramePhaseSet(ApplicationFramePhase::RtcCallbacks);
280
306
  gea::host::rtc::runCallbacks();
307
+ #else
308
+ gea::host::workers::Context::runMainPending();
309
+ #endif
310
+ applicationFramePhaseSet(ApplicationFramePhase::VideoPresentation);
311
+ gea::host::video::presentFrames();
312
+ #if !defined(GEA_EMBEDDED_NETWORK_SERVICES_DISABLED) || !GEA_EMBEDDED_NETWORK_SERVICES_DISABLED
313
+ applicationFramePhaseSet(ApplicationFramePhase::HttpRequests);
281
314
  gea::host::http::runRequests();
282
315
  #endif
283
316
  recordFramePhase(ApplicationFramePhase::AnimationFrameCallbacks, phaseClockUs() - startUs);
package/gea_sources.mjs CHANGED
@@ -87,8 +87,8 @@ export function cxxSources(env = process.env) {
87
87
  // @geastack/host — platform layer + services
88
88
  ...cpp(host, ['input', 'wifi', 'bluetooth', 'geolocation']),
89
89
  ...cpp(join(host, 'host'), [
90
- 'timers', 'input', 'display', 'fetch', 'media', 'rtc', 'websocket', 'http', 'wifi',
91
- 'ble', 'apps', 'audio', 'image', 'touch', 'imu', 'memory', 'camera', 'geolocation',
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',
92
92
  'tile_loader'
93
93
  ]),
94
94
  // framework services (built on the platform abstraction — no target-specific deps)
package/include/app.h CHANGED
@@ -31,6 +31,12 @@ enum class ApplicationFramePhase : std::uint8_t {
31
31
  StyleRecompute,
32
32
  DocumentFrame,
33
33
  RefreshMounted,
34
+ CycleCollection,
35
+ CallbackMicrotasks,
36
+ WebSocketCallbacks,
37
+ RtcCallbacks,
38
+ VideoPresentation,
39
+ HttpRequests,
34
40
  };
35
41
 
36
42
  struct ApplicationFramePerfStats {
package/include/audio.h CHANGED
@@ -5,6 +5,7 @@
5
5
  #include <cstddef>
6
6
  #include <cstdint>
7
7
  #include <string>
8
+ #include <functional>
8
9
 
9
10
  namespace gea::platform::audio {
10
11
 
@@ -72,6 +73,14 @@ public:
72
73
  static bool playFile(const std::string &path);
73
74
  static bool playPcm(const std::int16_t *samples, std::size_t sample_count, int sample_rate, int channels);
74
75
  static void stopPlayback();
76
+ static void flushPlayback();
77
+ // Continuous 16 kHz mono source; zero means the platform could not attach it.
78
+ static std::uint64_t playPcmStream(std::function<std::size_t(std::int16_t*, std::size_t)> pull);
79
+ static void stopPcmStream(std::uint64_t stream);
80
+ static bool pcmStreamSettled(std::uint64_t stream);
81
+ // Maximum mixer and device buffering delays, in seconds.
82
+ static double processingLatency();
83
+ static double outputLatency();
75
84
  };
76
85
 
77
86
  #ifdef GEA_AUDIO_DRIVER_INTERNAL
@@ -80,8 +89,10 @@ public:
80
89
  static bool open(int sample_rate, int channels, int bits_per_sample);
81
90
  static bool write(const std::int16_t *pcm, std::size_t sample_count, int timeout_ms);
82
91
  static void close();
92
+ static bool flush();
83
93
  static int volume();
84
94
  static void setVolume(int volume_percent);
95
+ static double outputLatency(int sample_rate);
85
96
  };
86
97
  #endif
87
98
 
@@ -0,0 +1,73 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ #pragma once
3
+
4
+ #include <algorithm>
5
+ #include <array>
6
+ #include <cstdint>
7
+ #include <functional>
8
+ #include <map>
9
+ #include <mutex>
10
+
11
+ namespace gea::platform::audio {
12
+
13
+ // The RTC decoder supplies 16 kHz mono PCM. Pulling from independent readers
14
+ // avoids copying every incoming packet into another playback queue.
15
+ class PcmStreamMixer {
16
+ public:
17
+ using Pull = std::function<std::size_t(std::int16_t*, std::size_t)>;
18
+ using Handle = std::uint64_t;
19
+ static constexpr std::size_t chunkFrames = 512;
20
+
21
+ Handle add(Pull pull) {
22
+ if (!pull) return 0;
23
+ std::lock_guard<std::mutex> lock(mutex_);
24
+ const auto id = ++next_;
25
+ sources_.emplace(id, Source{std::move(pull)});
26
+ return id;
27
+ }
28
+ void remove(Handle id) { std::lock_guard<std::mutex> lock(mutex_); sources_.erase(id); }
29
+ void clear() { std::lock_guard<std::mutex> lock(mutex_); sources_.clear(); }
30
+ bool active() const { std::lock_guard<std::mutex> lock(mutex_); return !sources_.empty(); }
31
+ bool settled(Handle id) const {
32
+ std::lock_guard<std::mutex> lock(mutex_);
33
+ const auto it = sources_.find(id);
34
+ return it == sources_.end() || (!it->second.inFlight && !it->second.failed);
35
+ }
36
+ // Called by the sole output consumer AFTER its driver write returns. A
37
+ // reader being empty only proves its last block was pulled, not submitted.
38
+ void didWrite(bool success) {
39
+ std::lock_guard<std::mutex> lock(mutex_);
40
+ for (auto& [id, source] : sources_) {
41
+ if (source.inFlight && !success) source.failed = true;
42
+ source.inFlight = false;
43
+ }
44
+ }
45
+
46
+ // Mix into existing stereo samples (e.g. oscillator output). Return the
47
+ // frames actually read: a short network packet must not acquire silence
48
+ // padding that lengthens speech and accumulates latency.
49
+ std::size_t mix(std::int16_t* stereo, std::size_t frames) {
50
+ frames = std::min(frames, chunkFrames);
51
+ std::array<std::int16_t, chunkFrames> mono;
52
+ std::size_t available = 0;
53
+ std::lock_guard<std::mutex> lock(mutex_);
54
+ for (auto& [id, source] : sources_) {
55
+ const auto count = std::min(frames, source.pull(mono.data(), frames));
56
+ source.inFlight = source.inFlight || count > 0;
57
+ available = std::max(available, count);
58
+ for (std::size_t i = 0; i < count; ++i) {
59
+ for (std::size_t channel = 0; channel < 2; ++channel) {
60
+ const auto sum = static_cast<int>(stereo[i * 2 + channel]) + mono[i];
61
+ stereo[i * 2 + channel] = static_cast<std::int16_t>(std::clamp(sum, -32768, 32767));
62
+ }
63
+ }
64
+ }
65
+ return available;
66
+ }
67
+ private:
68
+ mutable std::mutex mutex_;
69
+ struct Source { Pull pull; bool inFlight = false; bool failed = false; };
70
+ std::map<Handle, Source> sources_;
71
+ Handle next_ = 0;
72
+ };
73
+ }
package/include/canvas.h CHANGED
@@ -88,6 +88,7 @@ public:
88
88
  // LANDSCAPE is defined (papers3), so every other target is byte-for-byte
89
89
  // unaffected. Supports both packed grayscale and native RGB framebuffers.
90
90
  void bindPixelsRotatedLandscape(pixel::native_t *physPixels, int logicalWidth, int logicalHeight);
91
+ bool isLandscapeRotated() const { return landscapeRot_; }
91
92
  #endif
92
93
 
93
94
  pixel::native_t *pixels();
package/include/pixel.h CHANGED
@@ -658,17 +658,51 @@ inline std::uint16_t fromNative(native_t pixel)
658
658
  #endif
659
659
  }
660
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).
661
+ // Non-overlapping native pixel spans, including cropped rows whose source and
662
+ // destination have different vector alignment. Keep every load within the span:
663
+ // a scalar head precedes the first aligned-down load, and the scalar tail leaves
664
+ // room for the unaligned vector load's second aligned block.
665
+ inline void copyNative(native_t *dst, const native_t *src, int count)
666
+ {
667
+ if (count <= 0) return;
668
+ #if GEA_PIXEL_PIE_FILL16 && !GEA_PIXEL_FORMAT_IS_8888 && !GEA_PIXEL_FORMAT_IS_GRAY
669
+ if (count >= 32) {
670
+ for (int i = 0; i < 8; ++i) { *dst++ = *src++; --count; }
671
+ while ((reinterpret_cast<std::uintptr_t>(dst) & 15u) != 0) {
672
+ *dst++ = *src++;
673
+ --count;
674
+ }
675
+ const int vectors = (count - 8) >> 3;
676
+ if (vectors > 0) {
677
+ const native_t *cursor = src;
678
+ asm volatile(
679
+ "ee.ld.128.usar.ip q0, %[src], 16\n"
680
+ "loopnez %[vectors], 1f\n"
681
+ "ee.ld.128.usar.ip q1, %[src], 16\n"
682
+ "ee.src.q.qup q2, q0, q1\n"
683
+ "ee.vst.128.ip q2, %[dst], 16\n"
684
+ "ee.orq q0, q1, q1\n"
685
+ "1:\n"
686
+ : [src] "+&r"(cursor), [dst] "+&r"(dst)
687
+ : [vectors] "r"(vectors)
688
+ : "memory");
689
+ src += vectors * 8;
690
+ count -= vectors * 8;
691
+ }
692
+ }
693
+ #endif
694
+ std::memcpy(dst, src, static_cast<std::size_t>(count) * sizeof(native_t));
695
+ }
696
+
697
+ // Row copies across the RGB565-scratch <-> native-framebuffer boundary. RGB565
698
+ // uses the native span copy above; other framebuffer formats convert per pixel.
665
699
  inline void copyNativeToRgb565(std::uint16_t *dst, const native_t *src, int count)
666
700
  {
667
701
  if (count <= 0) return;
668
702
  #if GEA_PIXEL_FORMAT_IS_8888 || GEA_PIXEL_FORMAT_IS_GRAY
669
703
  for (int i = 0; i < count; i++) dst[i] = fromNative(src[i]);
670
704
  #else
671
- std::memcpy(dst, src, static_cast<std::size_t>(count) * sizeof(std::uint16_t));
705
+ copyNative(dst, src, count);
672
706
  #endif
673
707
  }
674
708
 
@@ -678,7 +712,7 @@ inline void copyRgb565ToNative(native_t *dst, const std::uint16_t *src, int coun
678
712
  #if GEA_PIXEL_FORMAT_IS_8888 || GEA_PIXEL_FORMAT_IS_GRAY
679
713
  for (int i = 0; i < count; i++) dst[i] = toNative(src[i]);
680
714
  #else
681
- std::memcpy(dst, src, static_cast<std::size_t>(count) * sizeof(std::uint16_t));
715
+ copyNative(dst, src, count);
682
716
  #endif
683
717
  }
684
718