@geastack/core 0.1.39 → 0.1.41

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/runtime/host.ts CHANGED
@@ -30,7 +30,11 @@ import type {
30
30
  EmbeddedMemoryConfig,
31
31
  EmbeddedMemoryStats,
32
32
  HttpReply,
33
- AudioContext
33
+ IncomingMessage,
34
+ HttpModule,
35
+ HttpServer,
36
+ AudioContext,
37
+ WiFiController,
34
38
  } from '../index'
35
39
 
36
40
  // These 17 used to be declared HERE as well as in `../index`, byte for byte.
@@ -56,7 +60,7 @@ export type {
56
60
  CameraWhiteBalanceOptions,
57
61
  EmbeddedMemoryConfig,
58
62
  EmbeddedMemoryStats,
59
- HttpReply
63
+ HttpReply,
60
64
  } from '../index'
61
65
 
62
66
  declare const apps: {
@@ -86,32 +90,16 @@ declare const navigator: {
86
90
  getCurrentPosition(
87
91
  success: (position: GeolocationPosition) => void,
88
92
  error?: (error: GeolocationPositionError) => void,
89
- options?: PositionOptions
93
+ options?: PositionOptions,
90
94
  ): void
91
95
  watchPosition(
92
96
  success: (position: GeolocationPosition) => void,
93
97
  error?: (error: GeolocationPositionError) => void,
94
- options?: PositionOptions
98
+ options?: PositionOptions,
95
99
  ): number
96
100
  clearWatch(id: number): void
97
101
  }
98
- wifi: {
99
- enabled(): boolean
100
- setEnabled(enabled: boolean): void
101
- connected(): boolean
102
- rssi(): number
103
- ssid(): string
104
- ip(): string
105
- mac(): string
106
- configure(ssid: string, password: string): void
107
- waitForConnection(timeoutMs: number): boolean
108
- startScan(): void
109
- scanning(): boolean
110
- scanCount(): number
111
- scanSsidAt(index: number): string
112
- scanRssiAt(index: number): number
113
- scanSecuredAt(index: number): boolean
114
- }
102
+ wifi: WiFiController
115
103
  bluetooth: {
116
104
  keyboard: {
117
105
  tap(hidCode: number): void
@@ -209,6 +197,7 @@ declare const __gea_Camera: {
209
197
  draw(x: number, y: number, destWidth: number, destHeight: number): void
210
198
  capture(): number
211
199
  captureMirrored(): number
200
+ captureFrame(): string
212
201
  startRecording(path: string, fps: number): boolean
213
202
  stopRecording(): number
214
203
  isRecording(): boolean
@@ -239,9 +228,11 @@ declare const __gea_Audio: {
239
228
  setVolume(volume: number): void
240
229
  }
241
230
 
242
- type EmbeddedDisplayOrientation = 'portrait-primary' | 'portrait-secondary' | 'landscape-primary' | 'landscape-secondary'
231
+ type EmbeddedDisplayOrientation =
232
+ 'portrait-primary' | 'portrait-secondary' | 'landscape-primary' | 'landscape-secondary'
243
233
 
244
- type EmbeddedDisplayOrientationSupport = EmbeddedDisplayOrientation | 'portrait' | 'landscape' | 'all'
234
+ type EmbeddedDisplayOrientationSupport =
235
+ EmbeddedDisplayOrientation | 'portrait' | 'landscape' | 'all'
245
236
 
246
237
  type EmbeddedDisplayPixelFormat = 'rgb565' | 'rgb888' | 'rgb8888' | 'argb8888'
247
238
 
@@ -262,7 +253,9 @@ declare const __gea_Display: {
262
253
  getOrientation(): EmbeddedDisplayOrientation
263
254
  setOrientation(orientation: EmbeddedDisplayOrientation): void
264
255
  getSupportedOrientations(): EmbeddedDisplayOrientationSupport[]
265
- setSupportedOrientations(orientations: EmbeddedDisplayOrientationSupport | EmbeddedDisplayOrientationSupport[]): void
256
+ setSupportedOrientations(
257
+ orientations: EmbeddedDisplayOrientationSupport | EmbeddedDisplayOrientationSupport[],
258
+ ): void
266
259
  getAutoRotate(): boolean
267
260
  setAutoRotate(enabled: boolean): void
268
261
  setVSync(on: boolean): void
@@ -375,6 +368,7 @@ declare const __gea_Led: {
375
368
  detach(): void
376
369
  }
377
370
  declare const __gea_Clock: {
371
+ setEpochMs(timestamp: number): boolean
378
372
  // Milliseconds since the Unix epoch from the platform real-time clock
379
373
  // (gettimeofday). Reflects a host-set time (GEADEV SETTIME); unlike
380
374
  // Date.now() — monotonic on this ESP32 build — it tracks real wall-clock
@@ -384,8 +378,14 @@ declare const __gea_Clock: {
384
378
  declare const __gea_Profiler: {
385
379
  // Monotonic microseconds from the platform high-resolution timer.
386
380
  nowUs(): number
381
+ // Hardware cycle count on ESP32; nanoseconds on desktop hosts.
382
+ nowCycles(): number
383
+ }
384
+ declare const __gea_Haptics: {
385
+ vibrate(durationMs: number, strength: number): boolean
387
386
  }
388
387
  declare const __gea_Battery: {
388
+ charging(): boolean
389
389
  // Battery charge percentage (0-100) from the platform PMU.
390
390
  level(): number
391
391
  }
@@ -420,13 +420,13 @@ export const defaults = {}
420
420
  export const touch = {
421
421
  read(): TouchSample {
422
422
  return { touching: false, x: 0, y: 0 }
423
- }
423
+ },
424
424
  }
425
425
 
426
426
  export const Apps = {
427
427
  launch(appId: string): number {
428
428
  return apps.launch(appId)
429
- }
429
+ },
430
430
  }
431
431
 
432
432
  // Case-variant aliases for host accessors. Keep these lazy: direct top-level
@@ -442,7 +442,7 @@ export const BLE: typeof navigator.bluetooth = {
442
442
  },
443
443
  up(): void {
444
444
  navigator.bluetooth.keyboard.up()
445
- }
445
+ },
446
446
  },
447
447
  mouse: {
448
448
  move(dx: number, dy: number, buttons?: number, wheel?: number): void {
@@ -450,7 +450,7 @@ export const BLE: typeof navigator.bluetooth = {
450
450
  },
451
451
  click(button: number): void {
452
452
  navigator.bluetooth.mouse.click(button)
453
- }
453
+ },
454
454
  },
455
455
  // BLE-MIDI (MIDI over GATT). `enable()` registers the MIDI service +
456
456
  // advertises its UUID; `send()` transmits ONE already-framed BLE-MIDI
@@ -487,7 +487,7 @@ export const BLE: typeof navigator.bluetooth = {
487
487
  },
488
488
  disconnect(): void {
489
489
  navigator.bluetooth.midi.disconnect()
490
- }
490
+ },
491
491
  },
492
492
  // HID host role: this device is the central; a remote HID peripheral (a
493
493
  // keyboard/macro pad like the XPPen ACK05) is the input source. Raw input
@@ -531,7 +531,7 @@ export const BLE: typeof navigator.bluetooth = {
531
531
  },
532
532
  clearReports(): void {
533
533
  navigator.bluetooth.hidHost.clearReports()
534
- }
534
+ },
535
535
  },
536
536
  // Live connection registry across every concurrent BLE role. kind: 0 =
537
537
  // HID central peer (desktop), 1 = MIDI central peer (DAW), 2 = HID
@@ -546,7 +546,7 @@ export const BLE: typeof navigator.bluetooth = {
546
546
  },
547
547
  nameAt(index: number): string {
548
548
  return navigator.bluetooth.connections.nameAt(index)
549
- }
549
+ },
550
550
  },
551
551
  // Config service (custom GATT). A Web Bluetooth portal live-programs the
552
552
  // device: it reads the app's current config blob and, once paired with the
@@ -581,7 +581,7 @@ export const BLE: typeof navigator.bluetooth = {
581
581
  // highlight it live. No-op on the device when no portal is subscribed.
582
582
  pushActivity(index: number): void {
583
583
  navigator.bluetooth.config.pushActivity(index)
584
- }
584
+ },
585
585
  },
586
586
  enabled(): boolean {
587
587
  return navigator.bluetooth.enabled()
@@ -612,10 +612,29 @@ export const BLE: typeof navigator.bluetooth = {
612
612
  },
613
613
  stopAdvertising(): void {
614
614
  navigator.bluetooth.stopAdvertising()
615
- }
615
+ },
616
616
  }
617
617
  export const bluetooth = BLE
618
618
  export const WiFi: typeof navigator.wifi = {
619
+ accessPointMac(): string {
620
+ return navigator.wifi.accessPointMac()
621
+ },
622
+
623
+ startCaptivePortal(): boolean {
624
+ return navigator.wifi.startCaptivePortal()
625
+ },
626
+
627
+ stopCaptivePortal(): void {
628
+ navigator.wifi.stopCaptivePortal()
629
+ },
630
+
631
+ startAccessPoint(ssid: string, password = ''): boolean {
632
+ return navigator.wifi.startAccessPoint(ssid, password)
633
+ },
634
+
635
+ stopAccessPoint(): void {
636
+ navigator.wifi.stopAccessPoint()
637
+ },
619
638
  enabled(): boolean {
620
639
  return navigator.wifi.enabled()
621
640
  },
@@ -660,39 +679,33 @@ export const WiFi: typeof navigator.wifi = {
660
679
  },
661
680
  scanSecuredAt(index: number): boolean {
662
681
  return navigator.wifi.scanSecuredAt(index)
663
- }
682
+ },
664
683
  }
665
684
  export const wifi = WiFi
666
685
 
667
686
  // Device-hosted HTTP server. On hardware, geatsc lowers `http.createServer(...)`
668
687
  // to the native esp_http_server facade (packages/core/host/http.cpp); this JS
669
- // stub is the web/simulator fallback — browsers can't open a listening socket,
670
- // so it accepts the handler and reports a no-op server (listen() returns false).
671
- export interface HttpIncomingMessage {
672
- method: string
673
- url: string
674
- path: string
675
- query: string
676
- }
688
+ // fallback reports unsupported use because browsers cannot open a listening socket.
689
+ export type HttpIncomingMessage = IncomingMessage
677
690
  export interface HttpServerInstance {
678
691
  listen(port: number): boolean
679
692
  close(): void
680
693
  id(): number
681
694
  }
695
+ declare const __gea_http: HttpModule
696
+
682
697
  export const http = {
683
- createServer(_handler: (req: HttpIncomingMessage) => HttpReply): HttpServerInstance {
684
- return {
685
- listen(_port: number): boolean {
686
- return false
687
- },
688
- close(): void {},
689
- id(): number {
690
- return 0
691
- }
698
+ createServer(handler: (req: HttpIncomingMessage) => HttpReply): HttpServer {
699
+ if (typeof __gea_http !== 'undefined') {
700
+ return __gea_http.createServer(handler)
692
701
  }
702
+ throw new Error('HTTP servers are unavailable in this browser runtime')
703
+ },
704
+ close(handle: number): void {
705
+ if (typeof __gea_http !== 'undefined') __gea_http.close(handle)
693
706
  },
694
- close(_handle: number): void {}
695
707
  }
708
+
696
709
  export const Geolocation: typeof navigator.geolocation = {
697
710
  hasFix(): boolean {
698
711
  return navigator.geolocation.hasFix()
@@ -715,20 +728,20 @@ export const Geolocation: typeof navigator.geolocation = {
715
728
  getCurrentPosition(
716
729
  success: (position: GeolocationPosition) => void,
717
730
  error?: (error: GeolocationPositionError) => void,
718
- options?: PositionOptions
731
+ options?: PositionOptions,
719
732
  ): void {
720
733
  navigator.geolocation.getCurrentPosition(success, error, options)
721
734
  },
722
735
  watchPosition(
723
736
  success: (position: GeolocationPosition) => void,
724
737
  error?: (error: GeolocationPositionError) => void,
725
- options?: PositionOptions
738
+ options?: PositionOptions,
726
739
  ): number {
727
740
  return navigator.geolocation.watchPosition(success, error, options)
728
741
  },
729
742
  clearWatch(id: number): void {
730
743
  navigator.geolocation.clearWatch(id)
731
- }
744
+ },
732
745
  }
733
746
  export const geolocation = Geolocation
734
747
  // `__gea_Accelerometer` is only injected by the vite plugin when an app actually
@@ -739,7 +752,9 @@ export const geolocation = Geolocation
739
752
  // whether or not the global was injected, and apps that do use it still get the
740
753
  // real binding.
741
754
  export const Accelerometer =
742
- typeof __gea_Accelerometer !== 'undefined' ? __gea_Accelerometer : (undefined as unknown as typeof __gea_Accelerometer)
755
+ typeof __gea_Accelerometer !== 'undefined'
756
+ ? __gea_Accelerometer
757
+ : (undefined as unknown as typeof __gea_Accelerometer)
743
758
 
744
759
  // Remembers the most recent recording sink so stopRecording can report it on
745
760
  // the resolved CameraClip (the host only returns a duration).
@@ -797,13 +812,16 @@ export const Camera = {
797
812
  orientation: __gea_Camera.orientation,
798
813
  dispose(): void {
799
814
  image.dispose(id)
800
- }
815
+ },
801
816
  }
802
817
  return Promise.resolve(photo)
803
818
  },
804
819
  capturePhoto(options?: CameraCaptureOptions): Promise<CameraPhoto> {
805
820
  return this.capture(options)
806
821
  },
822
+ captureFrame(): string {
823
+ return __gea_Camera.captureFrame()
824
+ },
807
825
  startRecording(options: CameraRecordOptions): Promise<void> {
808
826
  cameraRecordingPath = options.path
809
827
  __gea_Camera.startRecording(options.path, options.fps ?? 0)
@@ -828,13 +846,26 @@ export const Camera = {
828
846
  __gea_Camera.setMirror(mirror)
829
847
  },
830
848
  setExposure(options: CameraExposureOptions): void {
831
- __gea_Camera.setExposure(options.mode ?? 'continuous', options.bias ?? 0, options.iso ?? 0, options.durationMs ?? 0)
849
+ __gea_Camera.setExposure(
850
+ options.mode ?? 'continuous',
851
+ options.bias ?? 0,
852
+ options.iso ?? 0,
853
+ options.durationMs ?? 0,
854
+ )
832
855
  },
833
856
  setWhiteBalance(options: CameraWhiteBalanceOptions): void {
834
- __gea_Camera.setWhiteBalance(options.mode ?? 'auto', options.temperature ?? 0, options.tint ?? 0)
857
+ __gea_Camera.setWhiteBalance(
858
+ options.mode ?? 'auto',
859
+ options.temperature ?? 0,
860
+ options.tint ?? 0,
861
+ )
835
862
  },
836
863
  setFocus(options: CameraFocusOptions): void {
837
- __gea_Camera.setFocus(options.mode ?? 'continuous', options.point?.x ?? 0, options.point?.y ?? 0)
864
+ __gea_Camera.setFocus(
865
+ options.mode ?? 'continuous',
866
+ options.point?.x ?? 0,
867
+ options.point?.y ?? 0,
868
+ )
838
869
  },
839
870
  setTorch(mode: 'off' | 'on' | 'auto', level?: number): void {
840
871
  __gea_Camera.setTorch(mode, level ?? 0)
@@ -849,11 +880,11 @@ export const Camera = {
849
880
  for (let i = 0; i < count; i++) {
850
881
  out.push({
851
882
  id: __gea_Camera.deviceIdAt(i),
852
- facing: __gea_Camera.deviceFacingAt(i) as CameraFacing
883
+ facing: __gea_Camera.deviceFacingAt(i) as CameraFacing,
853
884
  })
854
885
  }
855
886
  return out
856
- }
887
+ },
857
888
  }
858
889
 
859
890
  // Same guard as Accelerometer above: these embedded host globals are not injected
@@ -861,7 +892,9 @@ export const Camera = {
861
892
  // reference ReferenceErrors at module load and blanks the page. `typeof` keeps the
862
893
  // real binding on embedded targets where the plugin injects them.
863
894
  export const audioContext: AudioContext =
864
- typeof __gea_audioContext !== 'undefined' ? __gea_audioContext : (undefined as unknown as AudioContext)
895
+ typeof __gea_audioContext !== 'undefined'
896
+ ? __gea_audioContext
897
+ : (undefined as unknown as AudioContext)
865
898
  // Method wrapper, NOT the `typeof … ? … : undefined as …` guard the
866
899
  // property-bearing host objects above use. Two reasons. (1) The guard exists
867
900
  // only to stop a bare top-level reference from ReferenceError-ing at module
@@ -879,9 +912,12 @@ export const Audio = {
879
912
  },
880
913
  setVolume(volume: number): void {
881
914
  __gea_Audio.setVolume(volume)
882
- }
915
+ },
883
916
  }
884
- export const Display = typeof __gea_Display !== 'undefined' ? __gea_Display : (undefined as unknown as typeof __gea_Display)
917
+ export const Display =
918
+ typeof __gea_Display !== 'undefined'
919
+ ? __gea_Display
920
+ : (undefined as unknown as typeof __gea_Display)
885
921
  export const Memory = {
886
922
  internalFree(): number {
887
923
  return __gea_Memory.internalFree()
@@ -958,7 +994,7 @@ export const Memory = {
958
994
  allocationSramBytes: __gea_Memory.allocationSramBytes(),
959
995
  allocationPsramBytes: __gea_Memory.allocationPsramBytes(),
960
996
  allocationSramPeakBytes: __gea_Memory.allocationSramPeakBytes(),
961
- allocationPsramPeakBytes: __gea_Memory.allocationPsramPeakBytes()
997
+ allocationPsramPeakBytes: __gea_Memory.allocationPsramPeakBytes(),
962
998
  }
963
999
  },
964
1000
  config(): EmbeddedMemoryConfig {
@@ -972,14 +1008,14 @@ export const Memory = {
972
1008
  displayFlushBufferMaxBytes: __gea_Memory.displayFlushBufferMaxBytes(),
973
1009
  displayFlushRows: __gea_Memory.displayFlushRows(),
974
1010
  displayFlushDepth: __gea_Memory.displayFlushDepth(),
975
- displayFlushBufferBytes: __gea_Memory.displayFlushBufferBytes()
1011
+ displayFlushBufferBytes: __gea_Memory.displayFlushBufferBytes(),
976
1012
  }
977
- }
1013
+ },
978
1014
  }
979
1015
  export const Input = {
980
1016
  consumeBackButton(): boolean {
981
1017
  return __gea_Input.consumeBackButton()
982
- }
1018
+ },
983
1019
  }
984
1020
  export const Gpio = {
985
1021
  configureOutput(pin: number): boolean {
@@ -993,7 +1029,7 @@ export const Gpio = {
993
1029
  },
994
1030
  read(pin: number): boolean {
995
1031
  return __gea_Gpio.read(pin)
996
- }
1032
+ },
997
1033
  }
998
1034
  export const Led = {
999
1035
  set(pin: number, r: number, g: number, b: number): boolean {
@@ -1013,12 +1049,16 @@ export const Led = {
1013
1049
  },
1014
1050
  detach(): void {
1015
1051
  __gea_Led.detach()
1016
- }
1052
+ },
1017
1053
  }
1018
1054
  export const Clock = {
1055
+ setEpochMs(timestamp: number): boolean {
1056
+ return __gea_Clock.setEpochMs(timestamp)
1057
+ },
1058
+
1019
1059
  epochMs(): number {
1020
1060
  return __gea_Clock.epochMs()
1021
- }
1061
+ },
1022
1062
  }
1023
1063
  // Same method-wrapper reasoning as `Audio` above: the host global is only
1024
1064
  // touched when the method runs, and the conditional form has no plannable
@@ -1028,12 +1068,27 @@ export const Profiler = {
1028
1068
  nowUs(): number {
1029
1069
  if (typeof __gea_Profiler === 'undefined') return Date.now() * 1000
1030
1070
  return __gea_Profiler.nowUs()
1031
- }
1071
+ },
1072
+
1073
+ nowCycles(): number {
1074
+ // Desktop native hosts also expose nanoseconds instead of CPU cycles.
1075
+ if (typeof __gea_Profiler === 'undefined') return Date.now() * 1000000
1076
+ return __gea_Profiler.nowCycles()
1077
+ },
1078
+ }
1079
+ export const Haptics = {
1080
+ vibrate(durationMs: number, strength = 100): boolean {
1081
+ return __gea_Haptics.vibrate(durationMs, strength)
1082
+ },
1032
1083
  }
1033
1084
  export const Battery = {
1085
+ charging(): boolean {
1086
+ return __gea_Battery.charging()
1087
+ },
1088
+
1034
1089
  level(): number {
1035
1090
  return __gea_Battery.level()
1036
- }
1091
+ },
1037
1092
  }
1038
1093
  export const Notify = {
1039
1094
  text(): string {
@@ -1041,12 +1096,12 @@ export const Notify = {
1041
1096
  },
1042
1097
  seq(): number {
1043
1098
  return __gea_Notify.seq()
1044
- }
1099
+ },
1045
1100
  }
1046
1101
  export const DeviceControl = {
1047
1102
  exec(command: string): string {
1048
1103
  return __gea_DeviceControl.exec(command)
1049
- }
1104
+ },
1050
1105
  }
1051
1106
  // Method wrapper for the same two reasons as `Audio`/`Profiler` above: a
1052
1107
  // reference inside a method body cannot ReferenceError at module load, and the
@@ -1079,5 +1134,5 @@ export const imageHost = {
1079
1134
  },
1080
1135
  dispose(id: number): void {
1081
1136
  image.dispose(id)
1082
- }
1137
+ },
1083
1138
  }
package/runtime/images.ts CHANGED
@@ -20,6 +20,7 @@ declare const image: {
20
20
  writeFile(path: string, bytes: Uint8Array): boolean
21
21
  // Read a whole file's raw bytes (e.g. a .pmtiles on microSD). Empty when the
22
22
  // file is missing or no storage is mounted.
23
+ removeFile(path: string): boolean
23
24
  readFile(path: string): Uint8Array
24
25
  // Read a .pmtiles archive flashed into the spare ota_1 partition. Empty when
25
26
  // none present (or off-device).
@@ -127,3 +128,7 @@ export function fetchText(url: string): string {
127
128
  export function imageFromId(id: number): GeaEmbeddedImage {
128
129
  return image.make(id)
129
130
  }
131
+
132
+ export function removeCacheFile(path: string): boolean {
133
+ return image.removeFile(path)
134
+ }
@@ -70,6 +70,7 @@ export type {
70
70
  AudioParam,
71
71
  AudioProps,
72
72
  BatteryController,
73
+ HapticsController,
73
74
  BluetoothConfig,
74
75
  BluetoothConnections,
75
76
  BluetoothController,
@@ -181,6 +182,7 @@ export type {
181
182
  VirtualListElement,
182
183
  WebSocketCloseEvent,
183
184
  WebSocketConstructor,
185
+ WebSocketOptions,
184
186
  WebSocketErrorEvent,
185
187
  WebSocketInstance,
186
188
  WebSocketMessageEvent,
package/runtime.cpp CHANGED
@@ -88,10 +88,11 @@ void dispatch_rotary_input()
88
88
  void dispatch_key_input()
89
89
  {
90
90
  auto &tree = gea::embedded::ui::Tree::instance();
91
- for (int keyCode = input::consumeKeyDown(); keyCode != 0; keyCode = input::consumeKeyDown()) {
91
+ input::HardwareKeyEvent key{};
92
+ while (input::consumeKeyEvent(key)) {
92
93
  events::PointerEvent event{};
93
- event.type = events::PointerEventType::KeyDown;
94
- event.keyCode = keyCode;
94
+ event.type = key.pressed ? events::PointerEventType::KeyDown : events::PointerEventType::KeyUp;
95
+ event.keyCode = key.keyCode;
95
96
  event.bubbles = true;
96
97
  event.cancelable = true;
97
98
 
@@ -105,7 +106,8 @@ void dispatch_key_input()
105
106
  }
106
107
  if (!stopped) {
107
108
  event.propagationStopped = false;
108
- gea::embedded::ui::dispatchDocumentKeyDown(event);
109
+ if (key.pressed) gea::embedded::ui::dispatchDocumentKeyDown(event);
110
+ else gea::embedded::ui::dispatchDocumentKeyUp(event);
109
111
  }
110
112
  }
111
113
  }
@@ -196,6 +198,9 @@ void dispatch_event(const events::Event &event, const RuntimeOptions &options)
196
198
  // A finger held still posts no moves, so gestureActiveWithin() lapses to
197
199
  // false after the window and the loop quiesces — no busy-render on a hold.
198
200
  for (;;) {
201
+ #if defined(ESP_PLATFORM) && defined(GEA_NATIVE_DEBUGGER) && GEA_NATIVE_DEBUGGER
202
+ if (services::FrameScheduler::debuggerFrameRate() > 0) break;
203
+ #endif
199
204
  const bool catchUp = services::FrameScheduler::takeCatchUpRequest();
200
205
  const bool activeDrag =
201
206
  GEA_EMBEDDED_CONTINUOUS_TOUCH_FRAMES &&
@@ -405,6 +410,15 @@ bool Runtime::boot(const RuntimeOptions &options)
405
410
  (void)options;
406
411
  services::RuntimeLog::appStarted(networkReady);
407
412
  #else
413
+ #if defined(GEA_EMBEDDED_BOOT_STAGING_RESERVE) && GEA_EMBEDDED_BOOT_STAGING_RESERVE
414
+ // Opt-in (an app sets GEA_EMBEDDED_BOOT_STAGING_RESERVE=1 in gea.defines).
415
+ // TS initialization can start flash workers and audio before the event loop
416
+ // is available to resize staging. Lend surplus DMA staging now, retaining
417
+ // the panel's minimum pipeline until application and runtime tasks exist.
418
+ gea::platform::display::Display::reserveInternal(
419
+ static_cast<std::size_t>(gea::platform::display::Display::flushBufferBytes()));
420
+ gea::platform::display::Display::applyPendingInternalReserve();
421
+ #endif
408
422
  if (!services::AppRunner::initApplication(options.width, options.height)) return false;
409
423
  services::HeapProbe::log("runtime:after_app_init");
410
424
  services::StackProbe::logCurrentTask("runtime:after_app_init");
@@ -417,7 +431,11 @@ bool Runtime::boot(const RuntimeOptions &options)
417
431
  services::HeapProbe::log("runtime:after_frame_start");
418
432
  services::StackProbe::logCurrentTask("runtime:after_frame_start");
419
433
  services::HeapTaskSummary::log("runtime:after_frame_start");
420
- #if GEA_EMBEDDED_WIFI_EARLY_CONNECT
434
+ #if defined(GEA_EMBEDDED_BOOT_STAGING_RESERVE) && GEA_EMBEDDED_BOOT_STAGING_RESERVE
435
+ // Initialization owns its allocations now. Grow staging into the remaining
436
+ // heap when the event loop resumes; allocation retains the DMA reserve.
437
+ gea::platform::display::Display::reserveInternal(0);
438
+ #elif GEA_EMBEDDED_WIFI_EARLY_CONNECT
421
439
  // The early-connect loan protects radio, app, and task initialization.
422
440
  // They now own their allocations; let the frame task grow staging into
423
441
  // the remaining heap instead of retaining the two-row boot floor forever.