xbintsc 0.5.38 → 0.5.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.
Files changed (117) hide show
  1. package/dist/src/cli/help.d.ts +1 -1
  2. package/dist/src/codegen/generator/module.js +23 -7
  3. package/dist/src/codegen/generator/module.js.map +1 -1
  4. package/dist/src/driver/version.d.ts +1 -1
  5. package/dist/src/driver/version.js +1 -1
  6. package/dist/src/extensions/gui/html.js +9 -2
  7. package/dist/src/extensions/gui/html.js.map +1 -1
  8. package/dist/tests/codegen/closures.test.js +7 -0
  9. package/dist/tests/codegen/closures.test.js.map +1 -1
  10. package/dist/tests/e2e/compile-objects-classes.test.js +31 -0
  11. package/dist/tests/e2e/compile-objects-classes.test.js.map +1 -1
  12. package/dist/tests/e2e/gui-canvas-state.test.d.ts +9 -0
  13. package/dist/tests/e2e/gui-canvas-state.test.js +297 -0
  14. package/dist/tests/e2e/gui-canvas-state.test.js.map +1 -0
  15. package/dist/tests/e2e/gui-draw-perf.test.d.ts +22 -0
  16. package/dist/tests/e2e/gui-draw-perf.test.js +405 -0
  17. package/dist/tests/e2e/gui-draw-perf.test.js.map +1 -0
  18. package/dist/tests/e2e/gui-perf.test.d.ts +23 -0
  19. package/dist/tests/e2e/gui-perf.test.js +283 -0
  20. package/dist/tests/e2e/gui-perf.test.js.map +1 -0
  21. package/dist/tests/e2e/gui-script-import-order.test.js +9 -1
  22. package/dist/tests/e2e/gui-script-import-order.test.js.map +1 -1
  23. package/dist/tests/e2e/gui-webgl.test.js +4 -4
  24. package/dist/tests/e2e/gui-webgl.test.js.map +1 -1
  25. package/dist/tests/e2e/gui-webgl2-shadow-sampler.test.d.ts +8 -0
  26. package/dist/tests/e2e/gui-webgl2-shadow-sampler.test.js +336 -0
  27. package/dist/tests/e2e/gui-webgl2-shadow-sampler.test.js.map +1 -0
  28. package/dist/tests/extensions/gui.test.js +23 -0
  29. package/dist/tests/extensions/gui.test.js.map +1 -1
  30. package/doc/gui/api.md +28 -2
  31. package/doc/gui/canvas.md +110 -27
  32. package/doc/gui/known-issues.md +32 -10
  33. package/doc/gui/rendering.md +77 -0
  34. package/doc/gui/runtime.md +18 -3
  35. package/doc/gui/webgl.md +38 -19
  36. package/doc/gui.md +33 -1
  37. package/doc/zh-CN/gui/api.md +25 -2
  38. package/doc/zh-CN/gui/canvas.md +95 -26
  39. package/doc/zh-CN/gui/known-issues.md +21 -4
  40. package/doc/zh-CN/gui/rendering.md +59 -0
  41. package/doc/zh-CN/gui/runtime.md +13 -3
  42. package/doc/zh-CN/gui/webgl.md +27 -5
  43. package/doc/zh-CN/gui.md +25 -2
  44. package/package.json +7 -7
  45. package/runtime/ext_gui/browser.cpp +3 -1
  46. package/runtime/ext_gui/canvas.cpp +135 -89
  47. package/runtime/ext_gui/canvas.h +116 -0
  48. package/runtime/ext_gui/canvas_clip.cpp +143 -0
  49. package/runtime/ext_gui/canvas_composite.cpp +241 -0
  50. package/runtime/ext_gui/canvas_coverage.cpp +241 -0
  51. package/runtime/ext_gui/canvas_gradient.cpp +41 -4
  52. package/runtime/ext_gui/canvas_image_data.cpp +4 -0
  53. package/runtime/ext_gui/canvas_internal.h +34 -9
  54. package/runtime/ext_gui/canvas_raster.cpp +207 -178
  55. package/runtime/ext_gui/canvas_shadow.cpp +161 -0
  56. package/runtime/ext_gui/canvas_stroke.cpp +37 -9
  57. package/runtime/ext_gui/canvas_style.cpp +59 -0
  58. package/runtime/ext_gui/canvas_text.cpp +140 -0
  59. package/runtime/ext_gui/css.h +7 -0
  60. package/runtime/ext_gui/css_sheet.cpp +22 -0
  61. package/runtime/ext_gui/document.cpp +25 -1
  62. package/runtime/ext_gui/document.h +8 -0
  63. package/runtime/ext_gui/dom_api_canvas.cpp +16 -1
  64. package/runtime/ext_gui/dom_api_canvas_image.cpp +9 -0
  65. package/runtime/ext_gui/dom_api_canvas_state.cpp +217 -0
  66. package/runtime/ext_gui/dom_api_internal.h +36 -37
  67. package/runtime/ext_gui/dom_api_offscreen.cpp +213 -0
  68. package/runtime/ext_gui/dom_api_proto.cpp +18 -0
  69. package/runtime/ext_gui/dom_api_webgl.cpp +19 -9
  70. package/runtime/ext_gui/dom_api_webgl_internal.h +58 -0
  71. package/runtime/ext_gui/dom_api_webgl_source.cpp +18 -0
  72. package/runtime/ext_gui/gradient_values.cpp +12 -1
  73. package/runtime/ext_gui/gui.cpp +5 -0
  74. package/runtime/ext_gui/gui_canvas.cpp +4 -0
  75. package/runtime/ext_gui/gui_engine.h +30 -0
  76. package/runtime/ext_gui/gui_events.cpp +182 -58
  77. package/runtime/ext_gui/gui_media.cpp +3 -0
  78. package/runtime/ext_gui/image.cpp +69 -0
  79. package/runtime/ext_gui/image_bitmap_browser.cpp +11 -0
  80. package/runtime/ext_gui/layout.cpp +4 -0
  81. package/runtime/ext_gui/paint.cpp +19 -0
  82. package/runtime/ext_gui/paint_background.cpp +18 -1
  83. package/runtime/ext_gui/perf.cpp +171 -0
  84. package/runtime/ext_gui/perf.h +127 -0
  85. package/runtime/ext_gui/renderer.cpp +109 -41
  86. package/runtime/ext_gui/renderer.h +33 -9
  87. package/runtime/ext_gui/renderer_image.cpp +135 -65
  88. package/runtime/ext_gui/renderer_internal.h +15 -3
  89. package/runtime/ext_gui/renderer_text.cpp +24 -12
  90. package/runtime/ext_gui/style.cpp +101 -30
  91. package/runtime/ext_gui/style.h +6 -3
  92. package/runtime/ext_gui/style_print.cpp +7 -1
  93. package/runtime/ext_gui/text.cpp +23 -4
  94. package/runtime/ext_gui/text.h +29 -0
  95. package/runtime/ext_gui/text_cache.cpp +118 -0
  96. package/runtime/ext_gui/text_glyph_cache.cpp +100 -0
  97. package/runtime/ext_gui/webgl.h +48 -146
  98. package/runtime/ext_gui/webgl_blit.cpp +12 -1
  99. package/runtime/ext_gui/webgl_blit_depth.cpp +270 -0
  100. package/runtime/ext_gui/webgl_buffer.cpp +59 -1
  101. package/runtime/ext_gui/webgl_buffer_objects.cpp +106 -9
  102. package/runtime/ext_gui/webgl_clear.cpp +3 -0
  103. package/runtime/ext_gui/webgl_context.cpp +29 -3
  104. package/runtime/ext_gui/webgl_draw.cpp +75 -270
  105. package/runtime/ext_gui/webgl_draw_buffers.cpp +1 -0
  106. package/runtime/ext_gui/webgl_draw_setup.cpp +321 -0
  107. package/runtime/ext_gui/webgl_draw_state.h +130 -0
  108. package/runtime/ext_gui/webgl_glsl_scan.cpp +3 -8
  109. package/runtime/ext_gui/webgl_objects.h +148 -0
  110. package/runtime/ext_gui/webgl_pipeline.cpp +8 -8
  111. package/runtime/ext_gui/webgl_program_internal.h +35 -0
  112. package/runtime/ext_gui/webgl_shader.cpp +22 -30
  113. package/runtime/ext_gui/webgl_webgl2.cpp +36 -0
  114. package/runtime/ext_gui/window.cpp +56 -1
  115. package/src/codegen/generator/module.ts +22 -7
  116. package/src/driver/version.ts +1 -1
  117. package/src/extensions/gui/html.ts +9 -2
@@ -13,6 +13,7 @@
13
13
  #include <vector>
14
14
 
15
15
  #include "dom_api_webgl_rt.h"
16
+ #include "canvas.h"
16
17
  #include "image.h"
17
18
  #include "image_bitmap.h"
18
19
  #include "webgl.h"
@@ -100,6 +101,23 @@ bool webgl_read_source(xt_value source, int *width, int *height, std::vector<uin
100
101
  return true;
101
102
  }
102
103
  }
104
+ if (XT_IS_OBJECT(source)) {
105
+ xt_value canvas_id = xt_object_get_cstr(source, "__xt_gui_canvas");
106
+ if (XT_IS_NUMBER(canvas_id)) {
107
+ xtgui::Canvas2D *canvas = xtgui::xt_canvas_by_id((uint64_t)xt_to_number(canvas_id));
108
+ if (canvas == nullptr || canvas->image().width <= 0) return false;
109
+ const xtgui::Image &image = canvas->image();
110
+ *width = image.width;
111
+ *height = image.height;
112
+ if (premultiply) {
113
+ webgl_convert_pixels(image.pixels.data(), image.width * image.height, GL_RGBA, true,
114
+ rgba);
115
+ } else {
116
+ *rgba = image.pixels;
117
+ }
118
+ return true;
119
+ }
120
+ }
103
121
  XtGuiWindow *win = nullptr;
104
122
  xtgui::Node *node = resolve_node(source, &win);
105
123
  const xtgui::Image *image = nullptr;
@@ -392,7 +392,7 @@ bool parseRadialGradient(const std::string &body, BackgroundLayer *layer) {
392
392
  return true;
393
393
  }
394
394
 
395
- /** Parse one layer, appending it when it is a supported gradient. */
395
+ /** Parse one layer, appending it when it is a supported gradient or image. */
396
396
  bool parseLayer(const std::string &part, BackgroundLayer *layer) {
397
397
  std::string body;
398
398
  if (findCall(part, "linear-gradient", &body) ||
@@ -403,6 +403,17 @@ bool parseLayer(const std::string &part, BackgroundLayer *layer) {
403
403
  findCall(part, "repeating-radial-gradient", &body)) {
404
404
  return parseRadialGradient(body, layer);
405
405
  }
406
+ if (findCall(part, "url", &body)) {
407
+ std::string source = xt_css_trim(body);
408
+ if (source.size() >= 2 && (source.front() == '"' || source.front() == '\'') &&
409
+ source.back() == source.front()) {
410
+ source = source.substr(1, source.size() - 2);
411
+ }
412
+ if (source.empty()) return false;
413
+ layer->kind = BackgroundKind::Image;
414
+ layer->image_src = source;
415
+ return true;
416
+ }
406
417
  return false;
407
418
  }
408
419
 
@@ -194,6 +194,7 @@ void xt_gui_quit_window(XtGuiWindow *win) {
194
194
  win->document_object = XT_UNDEFINED;
195
195
  win->document.reset();
196
196
  win->html.clear();
197
+ win->display_list.clear();
197
198
  if (g_device != NULL) {
198
199
  xt_gui_geometry_destroy(g_device, &win->geometry);
199
200
  xt_gui_geometry_destroy(g_device, &win->text_geometry);
@@ -253,6 +254,10 @@ static XtGuiWindow *create_window_record(const char *title, int width, int heigh
253
254
  record->window = window;
254
255
  record->open = 1;
255
256
  record->ready = 0;
257
+ record->present_dirty = 1;
258
+ record->expose_pending = 0;
259
+ record->present_deferred = 0;
260
+ record->sync_dirty = 1;
256
261
  record->width = width;
257
262
  record->height = height;
258
263
  record->html.clear();
@@ -15,6 +15,7 @@
15
15
  #include "document.h"
16
16
  #include "gui_engine.h"
17
17
  #include "gui_internal.h"
18
+ #include "perf.h"
18
19
 
19
20
  namespace {
20
21
 
@@ -25,6 +26,7 @@ bool isCanvasElement(const xtgui::Node *node) {
25
26
  /** Canvas children are fallback content, so a canvas element ends the walk. */
26
27
  void collectCanvases(const xtgui::Node *node, std::vector<const xtgui::Node *> *out) {
27
28
  if (node == nullptr) return;
29
+ xtgui::g_perf_frame.syncNodes += 1;
28
30
  if (isCanvasElement(node)) {
29
31
  out->push_back(node);
30
32
  return;
@@ -137,6 +139,8 @@ void xt_gui_canvas_shutdown(XtGuiWindow *win) {
137
139
  xt_gui_webgl_shutdown(win);
138
140
  for (auto &entry : win->canvases) releaseCanvasTexture(entry.second.get());
139
141
  win->canvases.clear();
142
+ for (auto &entry : win->offscreen_canvases) releaseCanvasTexture(entry.second.get());
143
+ win->offscreen_canvases.clear();
140
144
  /* `pending_canvases` survives `loadHTML`: a module that asked for a context
141
145
  * before the page existed still needs its bitmap when the parsed element
142
146
  * binds. They are released with the window record. */
@@ -92,6 +92,7 @@ xt_value xt_gui_audio_create(int32_t argc, xt_value *argv);
92
92
  #include "canvas.h"
93
93
  #include "document.h"
94
94
  #include "media.h"
95
+ #include "paint.h"
95
96
  #include "renderer.h"
96
97
  #include "webgl.h"
97
98
 
@@ -159,6 +160,10 @@ struct XtGuiWindow {
159
160
  XtGuiGeometry geometry;
160
161
  XtGuiGeometry text_geometry;
161
162
  XtGuiGeometry image_geometry;
163
+ /** The frame's display list. Kept on the window so its payload vectors keep
164
+ * their capacity across frames instead of being reallocated from zero on
165
+ * every repaint. */
166
+ xtgui::DisplayList display_list;
162
167
  float background[4];
163
168
  int width;
164
169
  int height;
@@ -188,6 +193,28 @@ struct XtGuiWindow {
188
193
  std::vector<XtGuiWindowListener> window_listeners;
189
194
  /** Set by a DOM mutation; consumed by the frame loop before repainting. */
190
195
  int struct_dirty = 0;
196
+ /** Set when something may have changed on screen (a mutation, a scroll, an
197
+ * animation frame, a new video/canvas frame, an expose event, …). The frame
198
+ * loop skips acquiring a swapchain and recording a render pass while it is
199
+ * clear, so a static page does no GPU work at all; `XT_GUI_ALWAYS_PRESENT=1`
200
+ * disables the gate. */
201
+ int present_dirty = 1;
202
+ /** Set when the window system asked for a redraw (map/expose/restore) since
203
+ * the last frame. Several such events coalesce into one present, so the frame
204
+ * loop charges at most one `exposes` counter per frame — that way
205
+ * `presents - exposes - deferred` is the work the page caused, and a present
206
+ * can be attributed to the compositor rather than to the document (a window
207
+ * server delivers its first `Expose` whenever it maps the window, which no
208
+ * test can schedule). */
209
+ int expose_pending = 0;
210
+ /** Set when a frame could not get a swapchain image and left its present
211
+ * pending (see `xt_gui_render_window`). The present that finally happens
212
+ * carries that earlier frame's work, so it is charged to `deferred`. */
213
+ int present_deferred = 0;
214
+ /** Set when the media/canvas tables must be reconciled with the document
215
+ * without a restyle (a replaced document, for example): the frame loop then
216
+ * runs the DOM walk once even though no style changed. */
217
+ int sync_dirty = 0;
191
218
  /** Callbacks queued for the next frame, plus the id counter. */
192
219
  std::vector<XtGuiAnimationFrame> animation_frames;
193
220
  /** One decoder per `<video>`/`<audio>` element, keyed by the element node. The
@@ -206,6 +233,9 @@ struct XtGuiWindow {
206
233
  * initialises). Keyed by canvas id; moved into `canvases` when the pending
207
234
  * handle binds to the parsed element. */
208
235
  std::unordered_map<uint64_t, std::unique_ptr<xtgui::Canvas2D>> pending_canvases;
236
+ /** `new OffscreenCanvas(w, h)` bitmaps, keyed by their canvas id. They are
237
+ * not part of the DOM; they exist until the window closes. */
238
+ std::unordered_map<uint64_t, std::unique_ptr<xtgui::Canvas2D>> offscreen_canvases;
209
239
  /** The batch currently running: its callbacks have been swapped out of
210
240
  * `animation_frames`, and a collection triggered by one of them must still
211
241
  * see the rest. Kept on the record (not a local) so the GC root provider can
@@ -7,6 +7,7 @@
7
7
  #include "listener_options.h"
8
8
 
9
9
  #include "paint.h"
10
+ #include "perf.h"
10
11
 
11
12
  #include <algorithm>
12
13
  #include <stdio.h>
@@ -69,6 +70,7 @@ void xt_gui_flush_dom(XtGuiWindow *win) {
69
70
  xt_gui_window_viewport(win, &width, &height);
70
71
  win->document->restyle(width, height);
71
72
  win->geometry.dirty = 1;
73
+ win->present_dirty = 1;
72
74
  }
73
75
 
74
76
  /* -- page scroll ---------------------------------------------------------- */
@@ -115,6 +117,7 @@ void xt_gui_scroll_to(XtGuiWindow *win, float x, float y) {
115
117
  win->scroll_x = x;
116
118
  win->scroll_y = y;
117
119
  win->geometry.dirty = 1;
120
+ win->present_dirty = 1;
118
121
  xt_gui_emit(win, "scroll");
119
122
  }
120
123
 
@@ -145,14 +148,21 @@ std::string xt_gui_hit_test(XtGuiWindow *win, float x, float y) {
145
148
  return node == nullptr ? std::string() : xtgui::xt_dom_describe(node);
146
149
  }
147
150
 
148
- /** Update the hovered element from the pointer position; marks paint dirty. */
149
- static void xt_gui_update_hover(XtGuiWindow *win, float x, float y) {
151
+ /** Update the hovered element from an already resolved target node; marks paint
152
+ * dirty when the hover state (and therefore the styles) changed. */
153
+ static void xt_gui_update_hover(XtGuiWindow *win, const xtgui::Node *node) {
150
154
  if (win->document == nullptr) return;
151
- if (win->document->setHover(xt_gui_hit_node(win, x, y))) win->geometry.dirty = 1;
155
+ if (win->document->setHover(node)) {
156
+ win->geometry.dirty = 1;
157
+ win->present_dirty = 1;
158
+ }
152
159
  }
153
160
 
161
+ /** Pointer payload. `node` is the hit-test result the caller already resolved,
162
+ * so one event does not walk the layout tree three times. */
154
163
  static xt_value xt_gui_pointer_payload(const char *type, XtGuiWindow *win, float x, float y,
155
- int button, int clicks) {
164
+ int button, int clicks, const xtgui::Node *node) {
165
+ (void)win;
156
166
  xt_value payload = xt_object_new();
157
167
  xt_object_set(payload, xt_string_from_cstr("type"), xt_string_from_cstr(type));
158
168
  xt_object_set(payload, xt_string_from_cstr("x"), xt_number(x));
@@ -161,9 +171,12 @@ static xt_value xt_gui_pointer_payload(const char *type, XtGuiWindow *win, float
161
171
  xt_object_set(payload, xt_string_from_cstr("clientY"), xt_number(y));
162
172
  xt_object_set(payload, xt_string_from_cstr("button"), xt_number(button));
163
173
  xt_object_set(payload, xt_string_from_cstr("clicks"), xt_number(clicks));
164
- std::string target = xt_gui_hit_test(win, x, y);
165
- if (!target.empty()) {
166
- xt_object_set(payload, xt_string_from_cstr("target"), xt_string_from_cstr(target.c_str()));
174
+ if (node != nullptr) {
175
+ std::string target = xtgui::xt_dom_describe(node);
176
+ if (!target.empty()) {
177
+ xt_object_set(payload, xt_string_from_cstr("target"),
178
+ xt_string_from_cstr(target.c_str()));
179
+ }
167
180
  }
168
181
  return payload;
169
182
  }
@@ -171,22 +184,25 @@ static xt_value xt_gui_pointer_payload(const char *type, XtGuiWindow *win, float
171
184
  void xt_gui_dispatch_pointer(XtGuiWindow *win, const char *type, float x, float y, int button,
172
185
  int clicks) {
173
186
  if (win == NULL || !win->open) return;
174
- xt_gui_update_hover(win, x, y);
175
187
  const xtgui::Node *node = xt_gui_hit_node(win, x, y);
188
+ xt_gui_update_hover(win, node);
176
189
  if (strcmp(type, "mousedown") == 0 && win->document != nullptr) {
177
- if (win->document->setFocus(node)) win->geometry.dirty = 1;
190
+ if (win->document->setFocus(node)) {
191
+ win->geometry.dirty = 1;
192
+ win->present_dirty = 1;
193
+ }
178
194
  }
179
- xt_value payload = xt_gui_pointer_payload(type, win, x, y, button, clicks);
195
+ xt_value payload = xt_gui_pointer_payload(type, win, x, y, button, clicks, node);
180
196
  if (node != nullptr) xt_gui_emit_dom_event(win, type, node, payload);
181
197
  xt_gui_emit_payload(win, type, payload);
182
198
  }
183
199
 
184
200
  void xt_gui_dispatch_wheel(XtGuiWindow *win, float x, float y, float delta_x, float delta_y) {
185
201
  if (win == NULL || !win->open) return;
186
- xt_value payload = xt_gui_pointer_payload("wheel", win, x, y, -1, 0);
202
+ const xtgui::Node *node = xt_gui_hit_node(win, x, y);
203
+ xt_value payload = xt_gui_pointer_payload("wheel", win, x, y, -1, 0, node);
187
204
  xt_object_set(payload, xt_string_from_cstr("deltaX"), xt_number(delta_x));
188
205
  xt_object_set(payload, xt_string_from_cstr("deltaY"), xt_number(delta_y));
189
- const xtgui::Node *node = xt_gui_hit_node(win, x, y);
190
206
  if (node != nullptr) xt_gui_emit_dom_event(win, "wheel", node, payload);
191
207
  xt_gui_emit_payload(win, "wheel", payload);
192
208
  /* Default action: scroll the page (content that fits does not move). */
@@ -234,12 +250,26 @@ void handle_event(const SDL_Event *event) {
234
250
  win->height = event->window.data2;
235
251
  if (win->document != NULL) win->document->restyle((float)win->width, (float)win->height);
236
252
  win->geometry.dirty = 1;
253
+ win->present_dirty = 1;
237
254
  /* Web 'resize', so a document can re-measure on a window change in every
238
255
  * host (a browser already fires one). */
239
256
  xt_gui_emit(win, "resize");
240
257
  xt_gui_media_lists_notify(win);
241
258
  return;
242
259
  }
260
+ /* A window the compositor revealed (uncovered, restored, shown) must be
261
+ * redrawn even though nothing in the document changed: the frame loop only
262
+ * presents on demand. The request is charged to `exposes` (below) so
263
+ * `stats()` can separate it from a present the page caused. */
264
+ if (event->type == SDL_EVENT_WINDOW_EXPOSED || event->type == SDL_EVENT_WINDOW_SHOWN ||
265
+ event->type == SDL_EVENT_WINDOW_RESTORED) {
266
+ XtGuiWindow *win = xt_gui_find_by_window_id(event->window.windowID);
267
+ if (win != NULL) {
268
+ win->present_dirty = 1;
269
+ win->expose_pending = 1;
270
+ }
271
+ return;
272
+ }
243
273
 
244
274
  if (event->type == SDL_EVENT_MOUSE_MOTION) {
245
275
  XtGuiWindow *win = xt_gui_find_by_window_id(event->motion.windowID);
@@ -309,6 +339,13 @@ void xt_gui_run_animation_frames(XtGuiWindow *win, double timestamp_ms) {
309
339
 
310
340
  void xt_gui_render_window(XtGuiWindow *win) {
311
341
  if (win == NULL || !win->open || win->window == NULL || g_device == NULL) return;
342
+ xtgui::xt_perf_frame_begin();
343
+ /* The events were dispatched before the frame began. A redraw the window
344
+ * system asked for is charged to `exposes` once this frame actually presents
345
+ * (several expose events coalesce into one present); the flag is consumed
346
+ * here because only this frame may claim it. */
347
+ bool window_requested = win->expose_pending != 0;
348
+ win->expose_pending = 0;
312
349
  float viewport_width = 0;
313
350
  float viewport_height = 0;
314
351
  xt_gui_window_viewport(win, &viewport_width, &viewport_height);
@@ -318,77 +355,164 @@ void xt_gui_render_window(XtGuiWindow *win) {
318
355
  double delta = win->last_frame_ms > 0.0 ? now - win->last_frame_ms : 0.0;
319
356
  win->last_frame_ms = now;
320
357
  /* Animation-frame callbacks run before layout, so any DOM mutation they make
321
- * is picked up by this same frame. */
358
+ * is picked up by this same frame. A page that asked for a frame expects it
359
+ * to be presented even when its callback changed nothing visible. */
360
+ if (!win->animation_frames.empty()) win->present_dirty = 1;
322
361
  xt_gui_run_animation_frames(win, now);
323
362
  /* A callback may have closed the window (directly or through the
324
363
  * auto-close guard): `xt_gui_quit_window` released the document and the GPU
325
364
  * geometry, so there is nothing left to lay out or paint. */
326
- if (!win->open) return;
327
- /* Reconcile decoders with the live DOM first, so this frame's layout sees the
328
- * current sources and intrinsic sizes, then advance playback and fire the
329
- * DOM media events. */
330
- xt_gui_media_sync(win);
365
+ if (!win->open) {
366
+ xtgui::xt_perf_frame_end();
367
+ return;
368
+ }
369
+ /* Reconcile the decoder/canvas tables with the live DOM only when the
370
+ * document changed (or on the first frame): walking the whole tree every
371
+ * frame was a per-frame cost proportional to the element count even when a
372
+ * page had no `<video>` or `<canvas>` at all. */
373
+ bool dom_changed = win->struct_dirty != 0 || win->sync_dirty != 0 || !win->ready ||
374
+ (win->document != nullptr && win->document->dirty());
375
+ if (dom_changed) {
376
+ xt_gui_media_sync(win);
377
+ xt_gui_canvas_sync(win);
378
+ win->sync_dirty = 0;
379
+ }
380
+ /* Playback advances every frame (it raises the media DOM events), but only a
381
+ * decoder that produced a frame asks to present. */
331
382
  xt_gui_media_tick(win, now);
332
- /* Publish canvas bitmaps to the document (and create contexts for a canvas
333
- * that has none) before this frame's layout. */
334
- xt_gui_canvas_sync(win);
335
- if (win->document != nullptr && win->document->advance(delta)) win->geometry.dirty = 1;
383
+ if (win->document != nullptr && win->document->advance(delta)) {
384
+ win->geometry.dirty = 1;
385
+ win->present_dirty = 1;
386
+ xtgui::g_perf_frame.transitionFrames += 1;
387
+ }
336
388
 
337
389
  /* A DOM mutation from a handler invalidates the tree: restyle/relayout
338
390
  * before (re)building the geometry. */
339
- xt_gui_flush_dom(win);
391
+ {
392
+ xtgui::PerfScope scope(&xtgui::g_perf_frame_ms.restyle);
393
+ xt_gui_flush_dom(win);
394
+ }
340
395
 
341
396
  /* Rebuild the geometry only when the document or viewport changed. */
342
397
  bool can_paint = xt_gui_renderer_ensure(g_device, win->window);
398
+ /* One command buffer for every upload this frame — geometry, glyph atlas,
399
+ * image textures, video frames and canvas bitmaps — so K animated canvases
400
+ * cost one submit, not K. */
401
+ XtGuiUploadBatch uploads;
402
+ xt_gui_upload_begin(&uploads, g_device);
343
403
  if (can_paint && win->geometry.dirty && win->document != nullptr) {
344
- xtgui::DisplayList list;
345
- xtgui::xt_paint_build(win->document->layout().root(), list, viewport_width, viewport_height);
346
- xt_paint_translate(list, -win->scroll_x, -win->scroll_y);
347
- xt_gui_geometry_upload(g_device, &win->geometry, list);
348
- float density = SDL_GetWindowPixelDensity(win->window);
349
- float text_scale = density > 0.0f ? density : 1.0f;
350
- xt_gui_text_upload(g_device, &win->text_geometry, list, text_scale);
351
- xt_gui_image_upload(g_device, &win->image_geometry, list, text_scale);
352
- xt_gui_build_runs(&win->geometry, &win->text_geometry, &win->image_geometry, list);
404
+ {
405
+ xtgui::PerfScope scope(&xtgui::g_perf_frame_ms.paint);
406
+ /* Reuse the window's list: `clear()` keeps the payload vectors' capacity,
407
+ * so a repaint does not reallocate every array from zero. */
408
+ xtgui::DisplayList &list = win->display_list;
409
+ list.clear();
410
+ xtgui::xt_paint_build(win->document->layout().root(), list, viewport_width, viewport_height);
411
+ xt_paint_translate(list, -win->scroll_x, -win->scroll_y);
412
+ xtgui::g_perf_frame.paints += 1;
413
+ xtgui::g_perf_frame.paintOps += (uint64_t)list.ops.size();
414
+ {
415
+ xtgui::PerfScope upload_scope(&xtgui::g_perf_frame_ms.upload);
416
+ xt_gui_geometry_upload(g_device, &win->geometry, list, &uploads);
417
+ float density = SDL_GetWindowPixelDensity(win->window);
418
+ float text_scale = density > 0.0f ? density : 1.0f;
419
+ xt_gui_text_upload(g_device, &win->text_geometry, list, text_scale, &uploads);
420
+ xt_gui_image_upload(g_device, &win->image_geometry, list, text_scale, &uploads);
421
+ xt_gui_build_runs(&win->geometry, &win->text_geometry, &win->image_geometry, list);
422
+ }
423
+ xtgui::g_perf_frame.drawRuns += (uint64_t)win->geometry.runs.size();
424
+ }
353
425
  win->geometry.dirty = 0;
354
426
  }
355
427
 
356
428
  /* Upload any new video frame before the render command buffer is recorded,
357
429
  * so the draw samples the frame that just arrived. */
358
- xt_gui_media_upload_frames(g_device);
430
+ bool uploaded = xt_gui_media_upload_frames(g_device, &uploads);
359
431
  /* Canvases are CPU-drawn: push any new bitmap without a relayout. */
360
- xt_gui_canvas_upload_frames(g_device);
432
+ if (xt_gui_canvas_upload_frames(g_device, &uploads)) uploaded = true;
433
+ xt_gui_upload_end(&uploads);
361
434
  /* WebGL contexts record into their own command buffers; submit them first so
362
435
  * the page's render pass samples this frame's drawing-buffer contents. */
363
436
  xt_gui_webgl_submit_all();
364
- SDL_GPUCommandBuffer *cmd = SDL_AcquireGPUCommandBuffer(g_device);
365
- if (cmd == NULL) return;
366
- SDL_GPUTexture *swapchain = NULL;
367
- SDL_AcquireGPUSwapchainTexture(cmd, win->window, &swapchain, NULL, NULL);
437
+ if (uploaded || !win->webgl_canvases.empty()) win->present_dirty = 1;
438
+
439
+ /* Present only when something happened. A static document otherwise burned a
440
+ * swapchain acquire and a full-scene draw every frame. `XT_GUI_ALWAYS_PRESENT`
441
+ * forces the old behaviour for debugging a missed invalidation. */
442
+ static int always_present = -1;
443
+ if (always_present < 0) {
444
+ const char *value = SDL_getenv("XT_GUI_ALWAYS_PRESENT");
445
+ always_present = (value != nullptr && value[0] != '\0' && strcmp(value, "0") != 0) ? 1 : 0;
446
+ }
447
+ if (win->ready && win->present_dirty == 0 && always_present == 0) {
448
+ xtgui::g_perf_frame.skippedFrames += 1;
449
+ xtgui::xt_perf_frame_end();
450
+ return;
451
+ }
452
+ win->present_dirty = 0;
453
+
368
454
  bool firstFrame = false;
369
- if (swapchain != NULL) {
370
- SDL_GPUColorTargetInfo target;
371
- memset(&target, 0, sizeof(target));
372
- target.texture = swapchain;
373
- target.clear_color.r = win->background[0];
374
- target.clear_color.g = win->background[1];
375
- target.clear_color.b = win->background[2];
376
- target.clear_color.a = win->background[3];
377
- target.load_op = SDL_GPU_LOADOP_CLEAR;
378
- target.store_op = SDL_GPU_STOREOP_STORE;
379
- SDL_GPURenderPass *pass = SDL_BeginGPURenderPass(cmd, &target, 1, NULL);
380
- if (can_paint) {
381
- float density = SDL_GetWindowPixelDensity(win->window);
382
- if (density <= 0.0f) density = 1.0f;
383
- xt_gui_renderer_draw(cmd, pass, &win->geometry, &win->text_geometry, &win->image_geometry,
384
- viewport_width, viewport_height, density);
455
+ {
456
+ /* Scoped: the timing must be recorded before `xt_perf_frame_end` copies
457
+ * the frame's phase timings. */
458
+ xtgui::PerfScope draw_scope(&xtgui::g_perf_frame_ms.draw);
459
+ SDL_GPUCommandBuffer *cmd = SDL_AcquireGPUCommandBuffer(g_device);
460
+ if (cmd != NULL) {
461
+ SDL_GPUTexture *swapchain = NULL;
462
+ SDL_AcquireGPUSwapchainTexture(cmd, win->window, &swapchain, NULL, NULL);
463
+ if (swapchain != NULL) {
464
+ /* Attribute the present: a redraw the window system asked for, one a
465
+ * starved frame had to defer, or this frame's own work. Both foreign
466
+ * kinds carry work that was *not* done in this frame. */
467
+ if (window_requested) {
468
+ xtgui::g_perf_frame.exposes += 1;
469
+ } else if (win->present_deferred != 0) {
470
+ xtgui::g_perf_frame.deferred += 1;
471
+ }
472
+ win->present_deferred = 0;
473
+ xtgui::g_perf_frame.presents += 1;
474
+ SDL_GPUColorTargetInfo target;
475
+ memset(&target, 0, sizeof(target));
476
+ target.texture = swapchain;
477
+ target.clear_color.r = win->background[0];
478
+ target.clear_color.g = win->background[1];
479
+ target.clear_color.b = win->background[2];
480
+ target.clear_color.a = win->background[3];
481
+ target.load_op = SDL_GPU_LOADOP_CLEAR;
482
+ target.store_op = SDL_GPU_STOREOP_STORE;
483
+ SDL_GPURenderPass *pass = SDL_BeginGPURenderPass(cmd, &target, 1, NULL);
484
+ if (can_paint) {
485
+ float density = SDL_GetWindowPixelDensity(win->window);
486
+ if (density <= 0.0f) density = 1.0f;
487
+ xt_gui_renderer_draw(cmd, pass, &win->geometry, &win->text_geometry,
488
+ &win->image_geometry, viewport_width, viewport_height, density);
489
+ }
490
+ SDL_EndGPURenderPass(pass);
491
+ firstFrame = !win->ready;
492
+ win->ready = 1;
493
+ } else {
494
+ /* No swapchain image this frame: SDL's non-blocking acquire skips a
495
+ * frame (rather than blocking) when the GPU still holds the image, and
496
+ * a minimized or hidden window has none at all. Dropping the frame here
497
+ * would leave the window showing the previous frame until something
498
+ * else dirtied the page, so keep the invalidation pending and present
499
+ * it on the next frame — the paint, the uploads and the run list are
500
+ * already built and retained, so the retry only re-records the draws. */
501
+ win->present_dirty = 1;
502
+ win->present_deferred = 1;
503
+ xtgui::g_perf_frame.starvedFrames += 1;
504
+ }
505
+ SDL_SubmitGPUCommandBuffer(cmd);
506
+ } else {
507
+ /* No command buffer from the device: same treatment as a starved frame —
508
+ * keep the invalidation so the page is shown once the device recovers. */
509
+ win->present_dirty = 1;
510
+ win->present_deferred = 1;
511
+ xtgui::g_perf_frame.starvedFrames += 1;
385
512
  }
386
- SDL_EndGPURenderPass(pass);
387
- firstFrame = !win->ready;
388
- win->ready = 1;
389
513
  }
390
- SDL_SubmitGPUCommandBuffer(cmd);
391
514
  /* The first presented frame marks the window ready. Emitting after the
392
515
  * submit lets a `ready` handler safely close/destroy the window. */
393
516
  if (firstFrame) xt_gui_emit(win, "ready");
517
+ xtgui::xt_perf_frame_end();
394
518
  }
@@ -19,6 +19,8 @@
19
19
 
20
20
  #include "gui_internal.h"
21
21
 
22
+ #include "perf.h"
23
+
22
24
  #include <cstring>
23
25
  #include <string>
24
26
  #include <unordered_set>
@@ -113,6 +115,7 @@ bool booleanAttr(const xtgui::Node *node, const char *name) {
113
115
 
114
116
  void collectMedia(const xtgui::Node *node, std::vector<const xtgui::Node *> *out) {
115
117
  if (node == nullptr) return;
118
+ xtgui::g_perf_frame.syncNodes += 1;
116
119
  if (isMediaElement(node)) {
117
120
  out->push_back(node);
118
121
  return; // a media element's children are `<source>`/`<track>`, not media
@@ -64,9 +64,71 @@ std::string normalizePath(const std::string &input) {
64
64
  return out;
65
65
  }
66
66
 
67
+ /** Decode a `data:` URL's base64 payload (the only encoding the engine emits
68
+ * and the common one a page assigns). */
69
+ bool decodeDataUrl(const std::string &url, std::vector<unsigned char> *out) {
70
+ const size_t comma = url.find(',');
71
+ if (comma == std::string::npos) return false;
72
+ const std::string header = url.substr(0, comma);
73
+ if (header.find(";base64") == std::string::npos) return false;
74
+ static const signed char kDecode[128] = {
75
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
76
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
77
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
78
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
79
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
80
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
81
+ -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
82
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1};
83
+ out->clear();
84
+ out->reserve((url.size() - comma) * 3 / 4 + 4);
85
+ uint32_t accumulator = 0;
86
+ int bits = 0;
87
+ for (size_t i = comma + 1; i < url.size(); i++) {
88
+ const unsigned char c = (unsigned char)url[i];
89
+ if (c == '=' || c == '\n' || c == '\r' || c == ' ') continue;
90
+ if (c >= 128) return false;
91
+ const int value = kDecode[c];
92
+ if (value < 0) return false;
93
+ accumulator = (accumulator << 6) | (uint32_t)value;
94
+ bits += 6;
95
+ if (bits >= 8) {
96
+ bits -= 8;
97
+ out->push_back((unsigned char)((accumulator >> bits) & 0xFF));
98
+ }
99
+ }
100
+ return !out->empty();
101
+ }
102
+
67
103
  } // namespace
68
104
 
69
105
  const Image *xt_image_load(const std::string &raw_path, int target_width, int target_height) {
106
+ /* A `data:` URL carries its bytes inline (a canvas used as a CSS
107
+ * `background-image` reaches this path through `toDataURL()`). */
108
+ if (raw_path.rfind("data:", 0) == 0) {
109
+ auto &cache = imageCache();
110
+ auto found = cache.find(raw_path);
111
+ if (found != cache.end()) {
112
+ return found->second.width > 0 ? &found->second : nullptr;
113
+ }
114
+ Image image;
115
+ std::vector<unsigned char> bytes;
116
+ if (decodeDataUrl(raw_path, &bytes)) {
117
+ int width = 0;
118
+ int height = 0;
119
+ int channels = 0;
120
+ unsigned char *data = stbi_load_from_memory(bytes.data(), (int)bytes.size(), &width, &height,
121
+ &channels, 4);
122
+ if (data != nullptr) {
123
+ image.width = width;
124
+ image.height = height;
125
+ image.pixels.assign(data, data + (size_t)width * (size_t)height * 4);
126
+ stbi_image_free(data);
127
+ }
128
+ }
129
+ auto inserted = cache.emplace(raw_path, std::move(image));
130
+ return inserted.first->second.width > 0 ? &inserted.first->second : nullptr;
131
+ }
70
132
  std::string path = normalizePath(raw_path);
71
133
  bool svg = xt_svg_is_svg(path);
72
134
  /* Raster formats always decode at their intrinsic resolution, so their cache
@@ -102,6 +164,13 @@ const Image *xt_image_load(const std::string &raw_path, int target_width, int ta
102
164
  }
103
165
 
104
166
  bool xt_image_size(const std::string &raw_path, int *width, int *height) {
167
+ if (raw_path.rfind("data:", 0) == 0) {
168
+ const Image *image = xt_image_load(raw_path);
169
+ if (image == nullptr) return false;
170
+ if (width != nullptr) *width = image->width;
171
+ if (height != nullptr) *height = image->height;
172
+ return true;
173
+ }
105
174
  std::string path = normalizePath(raw_path);
106
175
  if (xt_svg_is_svg(path)) return xt_svg_size(path, width, height);
107
176
  auto &cache = sizeCache();
@@ -15,6 +15,7 @@
15
15
  #include <vector>
16
16
 
17
17
  #include "image_bitmap.h"
18
+ #include "canvas.h"
18
19
  #include "webgl.h"
19
20
 
20
21
  namespace {
@@ -47,6 +48,16 @@ bool source_pixels(xt_value source, xtgui::Image *out) {
47
48
  return true;
48
49
  }
49
50
 
51
+ if (XT_IS_OBJECT(source)) {
52
+ xt_value canvas_id = xt_object_get_cstr(source, "__xt_gui_canvas");
53
+ if (XT_IS_NUMBER(canvas_id)) {
54
+ xtgui::Canvas2D *canvas = xtgui::xt_canvas_by_id((uint64_t)xt_to_number(canvas_id));
55
+ if (canvas == nullptr || canvas->image().width <= 0) return false;
56
+ *out = canvas->image();
57
+ return true;
58
+ }
59
+ }
60
+
50
61
  XtGuiWindow *win = nullptr;
51
62
  xtgui::Node *node = domapi::resolve_node(source, &win);
52
63
  if (node == nullptr || !node->isElement()) return false;
@@ -20,6 +20,7 @@
20
20
 
21
21
  #include "image.h"
22
22
  #include "inline_svg.h"
23
+ #include "perf.h"
23
24
  #include "style_internal.h"
24
25
 
25
26
  namespace xtgui {
@@ -172,6 +173,8 @@ std::string dumpBox(const LayoutBox *box, int depth) {
172
173
  void LayoutTree::compute(const Node *root, const std::unordered_map<const Node *, XtStyle> &styles,
173
174
  float viewportWidth, float viewportHeight, const MediaSizeMap *media,
174
175
  const CanvasInfoMap *canvases) {
176
+ PerfScope perf_scope(&g_perf_frame_ms.layout);
177
+ g_perf_frame.layouts += 1;
175
178
  root_.reset();
176
179
  index_.clear();
177
180
  root_style_ = xt_style_initial();
@@ -214,6 +217,7 @@ void LayoutTree::compute(const Node *root, const std::unordered_map<const Node *
214
217
  const LayoutBox *box = stack.back();
215
218
  stack.pop_back();
216
219
  if (box->node != nullptr) index_[box->node] = box;
220
+ g_perf_frame.layoutBoxes += 1;
217
221
  for (const std::unique_ptr<LayoutBox> &child : box->children) stack.push_back(child.get());
218
222
  }
219
223
  }