@geastack/core 0.1.41 → 0.1.43

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/README.md CHANGED
@@ -13,8 +13,8 @@ npm install @geastack/core
13
13
  Apps import the types and the runtime modules:
14
14
 
15
15
  ```tsx
16
- import type { Style } from '@geastack/core'
17
- import { View, Text } from '@geastack/engine'
16
+ import type { Style } from "@geastack/core";
17
+ import { View, Text } from "@geastack/engine";
18
18
  ```
19
19
 
20
20
  Everything else in the package is consumed by tooling, not by app code. The
@@ -37,15 +37,15 @@ before this hook runs. Field initializers and constructors initialize state;
37
37
  code that needs a rendered canvas belongs in the hook.
38
38
 
39
39
  ```tsx
40
- import { ReactiveComponent } from '@geastack/core'
40
+ import { ReactiveComponent } from "@geastack/core";
41
41
 
42
42
  class Counter extends ReactiveComponent {
43
- count = 0
43
+ count = 0;
44
44
  onAfterRender(): void {
45
- this.count = 1
45
+ this.count = 1;
46
46
  }
47
47
  template(): JSX.Element {
48
- return <div>{this.count}</div>
48
+ return <div>{this.count}</div>;
49
49
  }
50
50
  }
51
51
  ```
@@ -63,15 +63,42 @@ changes need to preserve the shared component and native-type contracts.
63
63
  WebSocket constructor argument for connection headers on ESP32 native hosts:
64
64
 
65
65
  ```ts
66
- const socket = new WebSocket('wss://api.openai.com/v1/live/sessions', '', {
67
- headers: [{ name: 'Authorization', value: `Bearer ${process.env.OPENAI_API_KEY}` }],
68
- })
66
+ const socket = new WebSocket("wss://api.openai.com/v1/live/sessions", "", {
67
+ headers: [
68
+ { name: "Authorization", value: `Bearer ${process.env.OPENAI_API_KEY}` },
69
+ ],
70
+ });
69
71
  ```
70
72
 
71
73
  Use these paired versions or newer when a service requires bearer authentication
72
74
  in the WebSocket handshake. Header names and values are validated before the
73
75
  connection starts. This native option is unavailable in browser WebSockets.
74
76
 
77
+ ## File-backed native assets
78
+
79
+ Native apps can keep images and baked font atlases on a mounted volume instead
80
+ of in the executable. Opt in through the app manifest:
81
+
82
+ ```json
83
+ {
84
+ "gea": {
85
+ "embeddedAssets": {
86
+ "storage": { "type": "file", "directory": "/nand" }
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ The generators emit content-addressed `.bin` payloads and a `.assets.json`
93
+ deployment manifest alongside each generated C++ source. Copy each `local` file
94
+ to its `remote` path, for example with `gea devctl push`, before starting the app.
95
+ The target must register a mount provider; ESP32 targets also need external BSS
96
+ enabled. The runtime reads and validates the entire bundle once into permanent
97
+ RAM buffers. Render-time image and font lookups use those buffers, with no file
98
+ I/O. Missing, truncated or corrupt bundles are rejected until the next process
99
+ start. Retain older content-addressed files when firmware rollback is required.
100
+ Apps without this option continue embedding their assets in the executable.
101
+
75
102
  ## Related packages
76
103
 
77
104
  - `@geastack/engine` renders the element tree, `@geastack/host` supplies the
package/events.cpp CHANGED
@@ -35,6 +35,7 @@ int EventTarget::pressValue() const
35
35
 
36
36
  const char *PointerEvent::typeName() const
37
37
  {
38
+ if (!subscriberTypeName.empty()) return subscriberTypeName.c_str();
38
39
  switch (type) {
39
40
  case PointerEventType::TouchStart:
40
41
  return "touchstart";
@@ -46,12 +47,22 @@ const char *PointerEvent::typeName() const
46
47
  return "click";
47
48
  case PointerEventType::Input:
48
49
  return "input";
50
+ case PointerEventType::FocusIn: return "focusin";
51
+ case PointerEventType::FocusOut: return "focusout";
52
+ case PointerEventType::Change:
53
+ return "change";
54
+ case PointerEventType::PointerCancel:
55
+ return "pointercancel";
49
56
  case PointerEventType::KeyUp:
50
57
  return "keyup";
51
58
  case PointerEventType::KeyDown:
52
59
  return "keydown";
53
60
  case PointerEventType::Rotary:
54
61
  return "rotary";
62
+ case PointerEventType::WindowBlur:
63
+ return "blur";
64
+ case PointerEventType::WindowResize:
65
+ return "resize";
55
66
  case PointerEventType::Scroll:
56
67
  return "scroll";
57
68
  }
package/gea_app_entry.cpp CHANGED
@@ -31,6 +31,7 @@ void drainMicrotasks();
31
31
 
32
32
  extern "C" void gea_cycle_collection_defer_begin() __attribute__((weak));
33
33
  extern "C" void gea_cycle_collection_defer_end() __attribute__((weak));
34
+ extern "C" bool gea_embedded_assets_prepare() __attribute__((weak));
34
35
 
35
36
  namespace gea::framework::app {
36
37
 
@@ -91,6 +92,9 @@ private:
91
92
 
92
93
  void Application::init(int width, int height, double devicePixelRatio)
93
94
  {
95
+ // File-backed static symbols must be populated before generated code can
96
+ // construct an image directly from them. Embedded assets need no hook.
97
+ if (gea_embedded_assets_prepare) gea_embedded_assets_prepare();
94
98
  // Top-level construction allocates the entire native object graph. Keep the
95
99
  // generated collector at a host safepoint until initialization has unwound,
96
100
  // just as Application::frame does for animation callbacks.
package/gea_sources.mjs CHANGED
@@ -78,7 +78,7 @@ export function cxxSources(env = process.env) {
78
78
  'tree_state', 'node_lifecycle', 'display_invalidation', 'layout_snapshot',
79
79
  'absolute_leaf_refresh', 'viewport_region', 'dirty_regions', 'root_scroll_refresh',
80
80
  'node', 'view_element', 'text_element', 'image_element', 'style', 'css_atom',
81
- 'document', 'core', 'tree_nodes', 'tree_style', 'tree_scroll_mirror', 'tree_render',
81
+ 'document', 'selector', 'core', 'tree_nodes', 'tree_style', 'tree_scroll_mirror', 'tree_render',
82
82
  'view', 'text', 'image_node', 'layout', 'render', 'input', 'input_render',
83
83
  'virtual_keyboard', 'canvas_element', 'canvas_store', 'tree_events'
84
84
  ]),
@@ -87,6 +87,8 @@ export function cxxSources(env = process.env) {
87
87
  ...cpp(join(elements, 'ui'), ['camera_element', 'virtual_list']),
88
88
  // @geastack/host — platform layer + services
89
89
  ...cpp(host, ['input', 'wifi', 'bluetooth', 'geolocation']),
90
+ // Only native Linux targets opt into their existing libcurl dependency.
91
+ ...(env.GEA_DESKTOP_WEBSOCKET_CURL === '1' ? [join(host, 'host/websocket_curl.cpp')] : []),
90
92
  ...cpp(join(host, 'host'), [
91
93
  'timers', 'worker', 'input', 'device', 'display', 'fetch', 'media', 'rtc', 'websocket', 'http', 'wifi',
92
94
  'ble', 'apps', 'audio', 'audio_worklet', 'audio_buffer_runtime', 'video', 'mjpeg', 'image', 'touch', 'imu', 'memory', 'camera', 'geolocation',
@@ -105,10 +107,18 @@ export function cxxSources(env = process.env) {
105
107
  ]
106
108
  }
107
109
 
110
+ // Apple targets compile Objective-C++ separately with ARC/Foundation. Keep the
111
+ // framework transport provider at the same source authority as C/C++ inputs.
112
+ export function objcxxSources(env = process.env) {
113
+ const { host } = packageDirs(env)
114
+ return [join(host, 'host/websocket_apple.mm')]
115
+ }
116
+
108
117
  const queries = {
109
118
  'include-flags': includeFlags,
110
119
  'c-sources': cSources,
111
- 'cxx-sources': cxxSources
120
+ 'cxx-sources': cxxSources,
121
+ 'objcxx-sources': objcxxSources
112
122
  }
113
123
 
114
124
  const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
package/gea_sources.sh CHANGED
@@ -4,7 +4,7 @@
4
4
  # no bash, so `execute_process(COMMAND bash -c ...)` failed and no ESP32 target
5
5
  # could configure. Node is already a hard requirement of every path that gets
6
6
  # here (the CLI and geatsc are Node), so reading it from Node costs no new
7
- # dependency, and keeping these three function names means shell callers that
7
+ # dependency, and keeping the existing function names means shell callers that
8
8
  # `source` this file (simulator build-web.sh, core's own test) do not change.
9
9
  #
10
10
  # Caller must export GEA_CORE = the @geastack/core package dir, plus
@@ -34,3 +34,8 @@ gea_fw_c_sources() {
34
34
  gea_fw_cxx_sources() {
35
35
  __gea_sources_run cxx-sources
36
36
  }
37
+
38
+ # Apple-only framework sources (ARC/Foundation); ordinary C++ targets do not call this.
39
+ gea_fw_objcxx_sources() {
40
+ __gea_sources_run objcxx-sources
41
+ }
package/include/canvas.h CHANGED
@@ -213,6 +213,7 @@ public:
213
213
  void drawText(const char *text, int x, int y, pixel::native_t color, float scale);
214
214
  void drawTextFont(const char *text, int x, int y, pixel::native_t color, int font_id);
215
215
  void drawTextFontFamily(const char *text, int x, int y, pixel::native_t color, int family_id, int size_px);
216
+ void drawTextFontFamily(const char *text, int x, int y, pixel::native_t color, int family_id, int size_px,int weight,int stretch);
216
217
  // Rotated 90deg for panel-native landscape rendering: the anchor and the
217
218
  // text advance are in LANDSCAPE coordinates; every glyph pixel writes to
218
219
  // the portrait panel address (panel_x = ly, panel_y = height-1-lx). The
package/include/display.h CHANGED
@@ -356,6 +356,7 @@ public:
356
356
  static void drawText(const char *text, int x, int y, gea::framework::graphics::pixel::native_t color, float scale);
357
357
  static void drawTextFont(const char *text, int x, int y, gea::framework::graphics::pixel::native_t color, int font_id);
358
358
  static void drawTextFontFamily(const char *text, int x, int y, gea::framework::graphics::pixel::native_t color, int family_id, int size_px);
359
+ static void drawTextFontFamily(const char *text,int x,int y,gea::framework::graphics::pixel::native_t color,int family_id,int size_px,int weight,int stretch);
359
360
  static void setPixel(int x, int y, gea::framework::graphics::pixel::native_t color);
360
361
  static void fillRoundedRect(int x, int y, int w, int h, int tl, int tr, int br, int bl, gea::framework::graphics::pixel::native_t color);
361
362
  static void fillRoundedRectBoxesRgb565(const int16_t *xs, const int16_t *ys, int count,
@@ -0,0 +1,26 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ #pragma once
3
+ #include <cstddef>
4
+ #include <cstdlib>
5
+ #include <type_traits>
6
+ namespace gea { template <typename... Arms> class TaggedUnion; }
7
+ namespace gea::framework::events {
8
+ struct NativeListenerOptions { bool capture=false; bool once=false; };
9
+ inline NativeListenerOptions listenerOptions(bool capture) { return {capture,false}; }
10
+ template <typename Boolean> bool listenerOptionBoolean(const Boolean &value) {
11
+ if constexpr (std::is_same_v<Boolean,bool>) return value;
12
+ else { static_assert(std::is_same_v<std::remove_cvref_t<decltype(*value)>,bool>); return value.has_value() && *value; }
13
+ }
14
+ // Exact generated record fields remain native Optional<bool>; never Value/boxing.
15
+ template <typename Record> requires requires(const Record &value) { value->capture; value->once; }
16
+ NativeListenerOptions listenerOptions(const Record &value) {
17
+ if (!value) return {};
18
+ return {listenerOptionBoolean(value->capture),listenerOptionBoolean(value->once)};
19
+ }
20
+ template <std::size_t Index=0, typename... Arms>
21
+ NativeListenerOptions listenerOptions(const gea::TaggedUnion<Arms...> &value) {
22
+ if (value.template is<Index>()) return listenerOptions(value.template get<Index>());
23
+ if constexpr (Index+1<sizeof...(Arms)) return listenerOptions<Index+1>(value);
24
+ else std::abort();
25
+ }
26
+ }
package/include/events.h CHANGED
@@ -1,10 +1,17 @@
1
1
  #pragma once
2
2
 
3
3
  #include "event.h"
4
+ #include "native_identity.h"
4
5
 
5
6
  #include <cstdint>
6
7
  #include <functional>
7
8
  #include <memory>
9
+ #include <string>
10
+ #include <tuple>
11
+ #include <type_traits>
12
+ #include <utility>
13
+
14
+ namespace gea::detail { struct RefVisitor; template<class T> struct TraceEdges; }
8
15
 
9
16
  namespace gea::framework::events {
10
17
 
@@ -17,7 +24,13 @@ enum class PointerEventType {
17
24
  KeyDown,
18
25
  Rotary,
19
26
  Scroll,
20
- KeyUp
27
+ KeyUp,
28
+ PointerCancel,
29
+ WindowBlur,
30
+ WindowResize,
31
+ Change,
32
+ FocusIn,
33
+ FocusOut
21
34
  };
22
35
 
23
36
  enum class EventPhase {
@@ -29,10 +42,13 @@ enum class EventPhase {
29
42
 
30
43
  class EventTarget {
31
44
  public:
32
- explicit EventTarget(int nodeId = -1) : nodeId_(nodeId) {}
45
+ explicit EventTarget(int nodeId = -1) : nodeId_(nodeId), identity_(identity::node(nodeId)) {}
46
+ const void *identity() const { return identity_.get(); }
47
+ std::shared_ptr<const void> weakIdentityOwner() const { return identity_; }
48
+ const identity::Owner &identityOwner() const { return identity_; }
33
49
 
34
- int id() const { return nodeId_; }
35
- bool valid() const { return nodeId_ >= 0; }
50
+ int id() const { return valid() ? nodeId_ : -1; }
51
+ bool valid() const { return identity::current(nodeId_, identity_); }
36
52
  const char *getAttribute(const char *name) const;
37
53
  const char *dataset(const char *name) const;
38
54
  int pressId() const;
@@ -40,6 +56,7 @@ public:
40
56
 
41
57
  private:
42
58
  int nodeId_;
59
+ identity::Owner identity_;
43
60
  };
44
61
 
45
62
  struct TouchPoint {
@@ -86,6 +103,9 @@ struct PointerEvent {
86
103
  int targetId = -1;
87
104
  int currentTargetId = -1;
88
105
  int pointerId = 0;
106
+ // Unknown/non-pointer producers never impersonate a primary button edge.
107
+ int button = -1;
108
+ std::uint32_t buttons = 0;
89
109
  int x = 0;
90
110
  int y = 0;
91
111
  int clientX = 0;
@@ -97,12 +117,25 @@ struct PointerEvent {
97
117
  int pressId = -1;
98
118
  int pressValue = -1;
99
119
  int keyCode = 0;
120
+ // Native keyboard metadata. Non-keyboard producers retain explicit empty
121
+ // strings/false flags; no browser record or boxed event is involved.
122
+ std::string key;
123
+ std::string code;
124
+ bool altKey = false;
125
+ bool ctrlKey = false;
126
+ bool metaKey = false;
127
+ bool shiftKey = false;
128
+ bool repeat = false;
129
+ bool isComposing = false;
100
130
  int delta = 0;
101
131
  bool primary = true;
102
132
  bool bubbles = true;
103
133
  bool cancelable = true;
104
134
  EventControlFlag defaultPrevented;
105
135
  EventControlFlag propagationStopped;
136
+ identity::Occurrence occurrenceIdentity;
137
+ const void *identity() const { return occurrenceIdentity.identity(); }
138
+ std::shared_ptr<const void> weakIdentityOwner() const { return occurrenceIdentity.owner(); }
106
139
  EventPhase eventPhase = EventPhase::None;
107
140
  TouchPoint touches[1]{};
108
141
  TouchPoint targetTouches[1]{};
@@ -111,12 +144,83 @@ struct PointerEvent {
111
144
  int targetTouchesLength = 0;
112
145
  int changedTouchesLength = 0;
113
146
 
147
+ // Scoped subscriber spelling; physical phase/type and occurrence stay unchanged.
148
+ std::string subscriberTypeName;
114
149
  const char *typeName() const;
115
150
  void preventDefault();
116
151
  void stopPropagation();
117
152
  };
118
153
 
119
- using EventListener = std::function<void(PointerEvent &)>;
154
+ // Ordinary native callbacks keep the platform std::function's own small-object
155
+ // storage. Explicit ADL-traceable callbacks instead own a concrete copy: a
156
+ // shared erased model would report one physical capture once for every copy.
157
+ class EventListener {
158
+ struct Model {
159
+ virtual ~Model() = default;
160
+ virtual std::unique_ptr<Model> clone() const = 0;
161
+ virtual void invoke(PointerEvent&) const = 0;
162
+ virtual void trace(gea::detail::RefVisitor&) const = 0;
163
+ };
164
+ template<class F> struct Concrete final : Model {
165
+ mutable F callable;
166
+ explicit Concrete(F value) : callable(std::move(value)) {}
167
+ std::unique_ptr<Model> clone() const override { return std::make_unique<Concrete>(callable); }
168
+ void invoke(PointerEvent& event) const override { callable(event); }
169
+ void trace(gea::detail::RefVisitor& visitor) const override { geaTraceRefs(callable, visitor); }
170
+ };
171
+ std::function<void(PointerEvent&)> ordinary_;
172
+ std::unique_ptr<Model> traced_;
173
+ public:
174
+ EventListener() = default;
175
+ EventListener(std::nullptr_t) noexcept {}
176
+ template<class F> requires (!std::is_same_v<std::decay_t<F>,EventListener> &&
177
+ std::is_copy_constructible_v<std::decay_t<F>> &&
178
+ std::is_invocable_r_v<void,std::decay_t<F>&,PointerEvent&>)
179
+ EventListener(F&& callable) {
180
+ using Held = std::decay_t<F>;
181
+ if constexpr (requires(const Held& held, gea::detail::RefVisitor& visitor) { geaTraceRefs(held,visitor); })
182
+ traced_ = std::make_unique<Concrete<Held>>(std::forward<F>(callable));
183
+ else ordinary_ = std::forward<F>(callable);
184
+ }
185
+ EventListener(const EventListener& other)
186
+ : ordinary_(other.ordinary_), traced_(other.traced_ ? other.traced_->clone() : nullptr) {}
187
+ EventListener(EventListener&&) noexcept = default;
188
+ EventListener& operator=(const EventListener& other) { EventListener copy(other); swap(copy); return *this; }
189
+ EventListener& operator=(EventListener&&) noexcept = default;
190
+ EventListener& operator=(std::nullptr_t) noexcept { ordinary_=nullptr; traced_.reset(); return *this; }
191
+ void swap(EventListener& other) noexcept { ordinary_.swap(other.ordinary_); traced_.swap(other.traced_); }
192
+ explicit operator bool() const noexcept { return bool(traced_) || bool(ordinary_); }
193
+ void operator()(PointerEvent& event) const { if(traced_) traced_->invoke(event); else ordinary_(event); }
194
+ friend void geaTraceRefs(const EventListener& value, gea::detail::RefVisitor& visitor) {
195
+ if(value.traced_) value.traced_->trace(visitor);
196
+ }
197
+ };
198
+
199
+ // Invocation has no hidden captures. Each owned capture occurs exactly once,
200
+ // in this tuple, and nested adapters preserve that same ADL trace authority.
201
+ template<class Invoke,class... Captures>
202
+ struct ExplicitEventCallback {
203
+ static_assert(std::is_empty_v<Invoke>, "event invocation must be capture-free");
204
+ Invoke invoke;
205
+ std::tuple<Captures...> captures;
206
+ void operator()(PointerEvent& event) {
207
+ std::apply([&](auto&... values){invoke(values...,event);},captures);
208
+ }
209
+ friend void geaTraceRefs(const ExplicitEventCallback& value,gea::detail::RefVisitor& visitor) {
210
+ std::apply([&](const auto&... values){
211
+ auto trace=[&]<class T>(const T& capture){
212
+ if constexpr(requires { gea::detail::TraceEdges<T>::supported; })
213
+ gea::detail::TraceEdges<T>::visit(capture,visitor);
214
+ else if constexpr(requires { geaTraceRefs(capture,visitor); }) geaTraceRefs(capture,visitor);
215
+ };
216
+ (trace(values),...);
217
+ },value.captures);
218
+ }
219
+ };
220
+ template<class Invoke,class... Captures>
221
+ auto explicitEventCallback(Invoke invoke,Captures&&... captures) {
222
+ return ExplicitEventCallback<Invoke,std::decay_t<Captures>...>{std::move(invoke),{std::forward<Captures>(captures)...}};
223
+ }
120
224
  using EventListenerId = std::uint64_t;
121
225
  inline constexpr EventListenerId kInvalidEventListenerId = 0;
122
226
 
@@ -10,6 +10,7 @@
10
10
  // includes gea/embedded-host.h instead (no engine/reactive dependency).
11
11
  #include "gea/embedded-host.h"
12
12
  #include "ui/document.h"
13
+ #include "ui/child_node_list.h"
13
14
  #include "ui/signal.h"
14
15
  // Debug builds: attributes JSX listeners to nodes for DevTools.
15
16
  #include "ui/debugger_listeners.h"
@@ -2,6 +2,10 @@
2
2
 
3
3
  #include <cstdint>
4
4
 
5
+ #ifndef GEA_EMBEDDED_TTF_FREETYPE
6
+ #define GEA_EMBEDDED_TTF_FREETYPE 0
7
+ #endif
8
+
5
9
  namespace gea::framework::graphics {
6
10
 
7
11
  struct Glyph {
@@ -98,6 +102,12 @@ struct RasterizedFontData {
98
102
  const FontKerningPair *kerning = nullptr;
99
103
  bool strictGlyphLookup = false;
100
104
  int fallbackCodepoint = -1;
105
+ #if GEA_EMBEDDED_TTF_FREETYPE
106
+ // Runtime slot reuse must not reuse Canvas sprite/glyph-cache identity.
107
+ std::uint64_t generation = 0;
108
+ #else
109
+ static constexpr std::uint64_t generation = 0;
110
+ #endif
101
111
  };
102
112
 
103
113
  class RasterizedFont final : public Font {
@@ -122,6 +132,7 @@ class FontRegistry {
122
132
  public:
123
133
  static RasterizedFont rasterized(int fontId);
124
134
  static RasterizedFont rasterizedFamily(int familyId, int sizePx);
135
+ static RasterizedFont rasterizedFamily(int familyId, int sizePx, int weight, int stretch);
125
136
  static const BitmapFont8x16 &bitmap8x16();
126
137
  static int familyId(const char *family);
127
138
  static const char *familyName(int familyId);
@@ -0,0 +1,65 @@
1
+ #pragma once
2
+ #include <memory>
3
+ #include <mutex>
4
+ #include <unordered_map>
5
+
6
+ namespace gea::framework::identity {
7
+ struct Token {};
8
+ using Owner = std::shared_ptr<const Token>;
9
+ // A copied carrier retains the same occurrence, including copies made before
10
+ // the first identity observation. Fresh event carriers have distinct tokens.
11
+ class Occurrence {
12
+ public:
13
+ Occurrence() = default;
14
+ Occurrence(const Occurrence &other) : owner_(other.owner()) {}
15
+ Occurrence &operator=(const Occurrence &other) {
16
+ if (this != &other) owner_ = other.owner();
17
+ return *this;
18
+ }
19
+ Owner owner() const {
20
+ if (!owner_) owner_ = std::make_shared<Token>();
21
+ return owner_;
22
+ }
23
+ const void *identity() const { return owner().get(); }
24
+ private:
25
+ mutable Owner owner_;
26
+ };
27
+ // Tree ids are reused. Registry entries cover only the current allocation;
28
+ // the tree retains live-node tokens until removal/clear. Retained handles
29
+ // own retired tokens independently; weak collections never extend that life.
30
+ struct NodeRegistry {
31
+ std::mutex mutex;
32
+ std::unordered_map<int, Owner> nodes;
33
+ };
34
+ inline NodeRegistry &registry() { static NodeRegistry value; return value; }
35
+ inline Owner node(int id) {
36
+ if (id < 0) return {};
37
+ auto &r = registry();
38
+ std::lock_guard<std::mutex> lock(r.mutex);
39
+ const auto found = r.nodes.find(id);
40
+ return found == r.nodes.end() ? Owner{} : found->second;
41
+ }
42
+ inline bool current(int id, const Owner &owner) {
43
+ if (id < 0 || !owner) return false;
44
+ auto &r = registry();
45
+ std::lock_guard<std::mutex> lock(r.mutex);
46
+ const auto found = r.nodes.find(id);
47
+ return found != r.nodes.end() && found->second == owner;
48
+ }
49
+ inline void activate(int id) {
50
+ if (id < 0) return;
51
+ auto &r = registry();
52
+ std::lock_guard<std::mutex> lock(r.mutex);
53
+ r.nodes[id] = std::make_shared<Token>();
54
+ }
55
+ inline void retire(int id) {
56
+ auto &r = registry();
57
+ std::lock_guard<std::mutex> lock(r.mutex);
58
+ r.nodes.erase(id);
59
+ }
60
+ inline void clear() {
61
+ auto &r = registry();
62
+ std::lock_guard<std::mutex> lock(r.mutex);
63
+ r.nodes.clear();
64
+ }
65
+ } // namespace gea::framework::identity
@@ -0,0 +1,71 @@
1
+ #pragma once
2
+
3
+ #include "platform/file_cache.h"
4
+ #include <cstddef>
5
+ #include <cstdint>
6
+ #include <cstdio>
7
+ #include <cstring>
8
+
9
+ #ifdef ESP_PLATFORM
10
+ #include "esp_attr.h"
11
+ #if !CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY
12
+ #error "File-backed assets require external BSS storage on ESP32"
13
+ #endif
14
+ #define GEA_FILE_ASSET_BUFFER EXT_RAM_BSS_ATTR
15
+ #else
16
+ #define GEA_FILE_ASSET_BUFFER
17
+ #endif
18
+
19
+ namespace gea::platform::storage {
20
+ struct AssetSegment {
21
+ unsigned char *data;
22
+ std::size_t size;
23
+ };
24
+
25
+ // The generator names immutable bundles by content. Validate the complete
26
+ // payload before exposing any font/image, then retain it in RAM for rendering.
27
+ inline bool loadAssetBundle(const char *path, const AssetSegment *segments,
28
+ std::size_t count, std::size_t bytes,
29
+ std::uint32_t expectedCrc) {
30
+ if (!ensureMounted())
31
+ return false;
32
+ auto *file = std::fopen(path, "rb");
33
+ if (!file) {
34
+ std::fprintf(stderr, "Gea asset bundle missing: %s\n", path);
35
+ return false;
36
+ }
37
+ std::uint32_t crc = 0xffffffffU;
38
+ constexpr std::uint32_t table[] = {
39
+ 0x00000000U, 0x1db71064U, 0x3b6e20c8U, 0x26d930acU,
40
+ 0x76dc4190U, 0x6b6b51f4U, 0x4db26158U, 0x5005713cU,
41
+ 0xedb88320U, 0xf00f9344U, 0xd6d6a3e8U, 0xcb61b38cU,
42
+ 0x9b64c2b0U, 0x86d3d2d4U, 0xa00ae278U, 0xbdbdf21cU};
43
+ std::size_t read = 0;
44
+ bool ok = true;
45
+ for (std::size_t index = 0; index < count; ++index) {
46
+ const auto &part = segments[index];
47
+ if (part.size > bytes - read ||
48
+ std::fread(part.data, 1, part.size, file) != part.size) {
49
+ ok = false;
50
+ break;
51
+ }
52
+ read += part.size;
53
+ for (std::size_t i = 0; i < part.size; ++i) {
54
+ crc ^= part.data[i];
55
+ crc = (crc >> 4) ^ table[crc & 15];
56
+ crc = (crc >> 4) ^ table[crc & 15];
57
+ }
58
+ }
59
+ ok = ok && read == bytes && (crc ^ 0xffffffffU) == expectedCrc &&
60
+ std::fgetc(file) == EOF && !std::ferror(file);
61
+ std::fclose(file);
62
+ if (!ok) {
63
+ for (std::size_t index = 0; index < count; ++index)
64
+ std::memset(segments[index].data, 0, segments[index].size);
65
+ std::fprintf(stderr, "Gea asset bundle failed validation: %s\n", path);
66
+ } else
67
+ std::fprintf(stderr, "Gea asset bundle loaded: %s (%lu bytes)\n", path,
68
+ static_cast<unsigned long>(bytes));
69
+ return ok;
70
+ }
71
+ } // namespace gea::platform::storage