xbintsc 0.5.41 → 0.5.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.
Files changed (43) hide show
  1. package/dist/src/cli/help.d.ts +1 -1
  2. package/dist/src/driver/version.d.ts +1 -1
  3. package/dist/src/driver/version.js +1 -1
  4. package/dist/tests/e2e/gui-data-url.test.d.ts +10 -0
  5. package/dist/tests/e2e/gui-data-url.test.js +127 -0
  6. package/dist/tests/e2e/gui-data-url.test.js.map +1 -0
  7. package/dist/tests/e2e/gui-effects.test.js +144 -4
  8. package/dist/tests/e2e/gui-effects.test.js.map +1 -1
  9. package/dist/tests/e2e/gui-paint.test.js +77 -0
  10. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  11. package/doc/gui/api.md +1 -1
  12. package/doc/gui/canvas.md +6 -2
  13. package/doc/gui/known-issues.md +18 -10
  14. package/doc/gui/media.md +11 -2
  15. package/doc/gui/rendering.md +48 -19
  16. package/doc/gui.md +12 -8
  17. package/doc/zh-CN/gui/api.md +1 -1
  18. package/doc/zh-CN/gui/canvas.md +6 -4
  19. package/doc/zh-CN/gui/known-issues.md +12 -8
  20. package/doc/zh-CN/gui/media.md +9 -2
  21. package/doc/zh-CN/gui/rendering.md +37 -15
  22. package/doc/zh-CN/gui.md +7 -6
  23. package/package.json +7 -7
  24. package/runtime/ext_gui/dom_api_canvas.cpp +42 -24
  25. package/runtime/ext_gui/gradient_values.cpp +44 -48
  26. package/runtime/ext_gui/gui_events.cpp +16 -13
  27. package/runtime/ext_gui/image.cpp +180 -25
  28. package/runtime/ext_gui/image.h +6 -4
  29. package/runtime/ext_gui/paint.cpp +69 -4
  30. package/runtime/ext_gui/paint.h +57 -1
  31. package/runtime/ext_gui/paint_background.cpp +197 -41
  32. package/runtime/ext_gui/paint_effects.cpp +2 -1
  33. package/runtime/ext_gui/paint_internal.h +5 -1
  34. package/runtime/ext_gui/renderer.cpp +81 -13
  35. package/runtime/ext_gui/renderer.h +19 -5
  36. package/runtime/ext_gui/renderer_gradient.cpp +156 -0
  37. package/runtime/ext_gui/renderer_image.cpp +6 -1
  38. package/runtime/ext_gui/renderer_internal.h +48 -0
  39. package/runtime/ext_gui/renderer_layer.cpp +552 -0
  40. package/runtime/ext_gui/style.h +29 -1
  41. package/runtime/ext_gui/style_print.cpp +10 -2
  42. package/runtime/ext_gui/style_values.cpp +10 -0
  43. package/src/driver/version.ts +1 -1
@@ -1,17 +1,22 @@
1
1
  /*
2
2
  * xbintsc GUI engine — background layer painting.
3
3
  *
4
- * The renderer has one rounded-rectangle pipeline and no gradient shader, so a
5
- * gradient is emitted as a run of solid bands: 18 strips for a linear gradient
6
- * and 32 concentric ellipses for a radial one. Radial ellipses use the shape
7
- * pipeline's transform slot (a unit circle scaled to the ellipse radii), which
8
- * keeps the distance-field antialiasing intact.
4
+ * `background-color` is one rounded rectangle and a `background-image` layer is
5
+ * a gradient or an image. A linear gradient is emitted as a run of solid bands
6
+ * (18 strips, 64 while repeating). A radial gradient is emitted as a single
7
+ * `PaintGradient` that the renderer rasterises per pixel
8
+ * (`renderer_gradient.cpp`): stacking translucent radial bands composites their
9
+ * alpha once per band, which turns a vignette whose stops fade to `transparent`
10
+ * into an opaque disc.
9
11
  */
10
12
 
11
13
  #include "paint_internal.h"
12
14
 
13
15
  #include <algorithm>
14
16
  #include <cmath>
17
+ #include <cstdio>
18
+ #include <memory>
19
+ #include <string>
15
20
  #include <vector>
16
21
 
17
22
  namespace xtgui {
@@ -19,28 +24,112 @@ namespace paint_detail {
19
24
 
20
25
  namespace {
21
26
 
22
- Color gradientColorAt(const BackgroundLayer &layer, float t) {
23
- const std::vector<BackgroundGradientStop> &stops = layer.stops;
27
+ /** Blend two stops in premultiplied alpha, as CSS specifies, and convert the
28
+ * result back to the straight alpha both pipelines blend. Without it a
29
+ * `transparent` stop (rgba(0,0,0,0)) drags the ramp towards black, so a
30
+ * `transparent -> white` gradient fades through grey. */
31
+ Color mixGradientStops(const Color &from, const Color &to, float amount) {
32
+ float alpha = from.a + (to.a - from.a) * amount;
33
+ Color out;
34
+ out.a = alpha;
35
+ if (alpha > 0.00001f) {
36
+ out.r = (from.r * from.a + (to.r * to.a - from.r * from.a) * amount) / alpha;
37
+ out.g = (from.g * from.a + (to.g * to.a - from.g * from.a) * amount) / alpha;
38
+ out.b = (from.b * from.a + (to.b * to.a - from.b * from.a) * amount) / alpha;
39
+ } else {
40
+ out.r = from.r;
41
+ out.g = from.g;
42
+ out.b = from.b;
43
+ }
44
+ out.valid = true;
45
+ return out;
46
+ }
47
+
48
+ } // namespace
49
+
50
+ Color gradientColorAt(const std::vector<GradientStop> &stops, bool repeating, float period,
51
+ float t) {
24
52
  if (stops.empty()) return Color();
53
+ /* A repeating gradient restarts its stop pattern at the last stop's offset. */
54
+ if (repeating) {
55
+ if (period <= 0.00001f) return stops.back().color;
56
+ t -= std::floor(t / period) * period;
57
+ }
25
58
  if (t <= stops.front().offset) return stops.front().color;
26
59
  if (t >= stops.back().offset) return stops.back().color;
27
60
  for (size_t i = 1; i < stops.size(); i++) {
28
61
  if (t > stops[i].offset) continue;
29
62
  float span = stops[i].offset - stops[i - 1].offset;
30
63
  float u = span > 0.00001f ? (t - stops[i - 1].offset) / span : 0.0f;
31
- Color a = stops[i - 1].color;
32
- Color b = stops[i].color;
33
- Color out;
34
- out.r = a.r + (b.r - a.r) * u;
35
- out.g = a.g + (b.g - a.g) * u;
36
- out.b = a.b + (b.b - a.b) * u;
37
- out.a = a.a + (b.a - a.a) * u;
38
- out.valid = true;
39
- return out;
64
+ return mixGradientStops(stops[i - 1].color, stops[i].color, u);
40
65
  }
41
66
  return stops.back().color;
42
67
  }
43
68
 
69
+ namespace {
70
+
71
+ /** Resolve the declaration's percentage/length offsets against the gradient
72
+ * line, then distribute the omitted (`Unit::Auto`) ones between their
73
+ * neighbours. The last explicit stop is preserved: for a normal gradient it
74
+ * holds that colour, and for a repeating one it defines the period. */
75
+ std::vector<GradientStop> resolveStops(const BackgroundLayer &layer, float line_length,
76
+ const XtStyle &style, float viewportWidth,
77
+ float viewportHeight) {
78
+ std::vector<GradientStop> stops;
79
+ for (const BackgroundGradientStop &stop : layer.stops) {
80
+ GradientStop resolved;
81
+ resolved.color = stop.color;
82
+ resolved.offset = -1.0f;
83
+ if (stop.offset.unit == Unit::Percent) {
84
+ resolved.offset = stop.offset.value / 100.0f;
85
+ } else if (stop.offset.unit != Unit::Auto && stop.offset.unit != Unit::Invalid &&
86
+ stop.offset.unit != Unit::None) {
87
+ float pixels = resolveLength(stop.offset, 0.0f, style.font_size, viewportWidth,
88
+ viewportHeight);
89
+ resolved.offset = line_length > 0.00001f ? pixels / line_length : 0.0f;
90
+ }
91
+ stops.push_back(resolved);
92
+ }
93
+ if (stops.empty()) return stops;
94
+
95
+ /* Explicit offsets are non-decreasing, as in CSS. */
96
+ float last = -1.0f;
97
+ for (GradientStop &stop : stops) {
98
+ if (stop.offset < 0.0f) continue;
99
+ if (stop.offset < last) stop.offset = last;
100
+ last = stop.offset;
101
+ }
102
+
103
+ /* Fill implicit offsets by distributing them between their neighbours. */
104
+ size_t i = 0;
105
+ while (i < stops.size()) {
106
+ if (stops[i].offset >= 0.0f) {
107
+ i++;
108
+ continue;
109
+ }
110
+ size_t j = i;
111
+ while (j < stops.size() && stops[j].offset < 0.0f) j++;
112
+ float before = (i == 0) ? 0.0f : stops[i - 1].offset;
113
+ float after = (j >= stops.size()) ? 1.0f : stops[j].offset;
114
+ size_t count = j - i;
115
+ for (size_t k = 0; k < count; k++) {
116
+ float offset = 0.0f;
117
+ if (i == 0 && j >= stops.size()) {
118
+ offset = count > 1 ? (float)k / (float)(count - 1) : 0.0f;
119
+ } else if (i == 0) {
120
+ offset = after * (float)k / (float)count;
121
+ } else if (j >= stops.size()) {
122
+ offset = before + (1.0f - before) * (float)(k + 1) / (float)count;
123
+ } else {
124
+ offset = before + (after - before) * (float)(k + 1) / (float)(count + 1);
125
+ }
126
+ stops[i + k].offset = offset;
127
+ }
128
+ i = j;
129
+ }
130
+ return stops;
131
+ }
132
+
44
133
  /** Intersect `clip` with the box's border box (backgrounds never paint outside
45
134
  * it, even when the box is not an overflow container). */
46
135
  PaintClip boxClip(const PaintClip &clip, const LayoutBox &box) {
@@ -75,17 +164,25 @@ PaintClip boxClip(const PaintClip &clip, const LayoutBox &box) {
75
164
  return out;
76
165
  }
77
166
 
78
- void paintLinear(const LayoutBox &box, const BackgroundLayer &layer, const FilterChain &chain,
79
- const PaintClip &clip, DisplayList &out) {
167
+ void paintLinear(const LayoutBox &box, const XtStyle &style, const BackgroundLayer &layer,
168
+ const FilterChain &chain, const PaintClip &clip, float viewportWidth,
169
+ float viewportHeight, DisplayList &out) {
80
170
  if (layer.stops.size() < 2 || box.width <= 0.0f || box.height <= 0.0f) return;
81
- const int strips = 18;
171
+ /* Repeating patterns need more samples than one gradient ramp: a 10%
172
+ * stripe is only two bands at the old 18-strip resolution. */
173
+ const int strips = layer.repeating ? 64 : 18;
82
174
  bool horizontal = (layer.angle > 80.0f && layer.angle < 100.0f) ||
83
175
  (layer.angle > 260.0f && layer.angle < 280.0f);
84
176
  bool reverse = (layer.angle > 260.0f && layer.angle < 280.0f) || layer.angle < 10.0f ||
85
177
  layer.angle > 350.0f;
178
+ const float line_length = horizontal ? box.width : box.height;
179
+ std::vector<GradientStop> stops = resolveStops(layer, line_length, style, viewportWidth,
180
+ viewportHeight);
181
+ if (stops.size() < 2) return;
182
+ const float period = stops.back().offset;
86
183
  for (int i = 0; i < strips; i++) {
87
184
  float t = ((float)i + 0.5f) / (float)strips;
88
- Color color = gradientColorAt(layer, reverse ? 1.0f - t : t);
185
+ Color color = gradientColorAt(stops, layer.repeating, period, reverse ? 1.0f - t : t);
89
186
  color = filterColor(chain, color);
90
187
  if (color.a <= 0.0f) continue;
91
188
  if (horizontal) {
@@ -98,6 +195,18 @@ void paintLinear(const LayoutBox &box, const BackgroundLayer &layer, const Filte
98
195
  }
99
196
  }
100
197
 
198
+ /** Radii of a keyword-sized ellipse: it keeps the box's aspect ratio and is
199
+ * scaled until it meets the corner at `(px, py)` from the centre, so the 100%
200
+ * stop lands on that corner as it does in CSS. Taking the side distances instead
201
+ * left the corners of a `farthest-corner` gradient unpainted. */
202
+ void cornerEllipseRadii(float width, float height, float px, float py, float *rx, float *ry) {
203
+ float x = 2.0f * px / width;
204
+ float y = 2.0f * py / height;
205
+ float scale = std::sqrt(x * x + y * y);
206
+ *rx = scale * width * 0.5f;
207
+ *ry = scale * height * 0.5f;
208
+ }
209
+
101
210
  /** Default radii for `closest-side` / `farthest-side` / corner keywords. */
102
211
  void radialRadii(const LayoutBox &box, const BackgroundLayer &layer, const XtStyle &style,
103
212
  float viewportWidth, float viewportHeight, float *cx, float *cy, float *rx,
@@ -151,31 +260,45 @@ void radialRadii(const LayoutBox &box, const BackgroundLayer &layer, const XtSty
151
260
  *rx = *ry = radius;
152
261
  } else {
153
262
  switch (layer.size) {
154
- case RadialSize::ClosestCorner:
155
263
  case RadialSize::ClosestSide:
156
264
  *rx = std::min(left, right);
157
265
  *ry = std::min(top, bottom);
158
266
  break;
159
267
  case RadialSize::FarthestSide:
160
- case RadialSize::FarthestCorner:
161
- default:
162
268
  *rx = std::max(left, right);
163
269
  *ry = std::max(top, bottom);
164
270
  break;
271
+ case RadialSize::ClosestCorner:
272
+ cornerEllipseRadii(box.width, box.height, std::min(left, right), std::min(top, bottom), rx,
273
+ ry);
274
+ break;
275
+ case RadialSize::FarthestCorner:
276
+ default:
277
+ cornerEllipseRadii(box.width, box.height, std::max(left, right), std::max(top, bottom), rx,
278
+ ry);
279
+ break;
165
280
  }
166
281
  }
167
282
  *cx = centerX;
168
283
  *cy = centerY;
169
284
  }
170
285
 
171
- /** One filled ellipse: a unit-radius rounded rect mapped by a local scale. */
172
- void addEllipse(DisplayList &out, float cx, float cy, float rx, float ry, const Color &color,
173
- const PaintClip &clip, const Matrix2D &world) {
174
- if (rx <= 0.0f || ry <= 0.0f || color.a <= 0.0f) return;
175
- Matrix2D local = Matrix2D::translate(cx, cy)
176
- .multiply(Matrix2D::scale(rx, ry))
177
- .multiply(Matrix2D::translate(-cx, -cy));
178
- addRect(out, cx - 1.0f, cy - 1.0f, 2.0f, 2.0f, 1.0f, color, clip, world.multiply(local));
286
+ /** Stable identity of a rasterised gradient: the ramp plus its geometry. The
287
+ * renderer keys its texture cache by this string, so an unchanged declaration
288
+ * rasterises once. */
289
+ std::string gradientKey(const PaintGradient &gradient) {
290
+ char buffer[192];
291
+ std::string key;
292
+ for (const GradientStop &stop : gradient.stops) {
293
+ std::snprintf(buffer, sizeof(buffer), "%.4f/%.5f,%.5f,%.5f,%.5f;", stop.offset, stop.color.r,
294
+ stop.color.g, stop.color.b, stop.color.a);
295
+ key += buffer;
296
+ }
297
+ std::snprintf(buffer, sizeof(buffer), "%.3f,%.3f,%.3f,%.3f;%d;%.5f", gradient.center_x,
298
+ gradient.center_y, gradient.radius_x, gradient.radius_y,
299
+ gradient.repeating ? 1 : 0, gradient.period);
300
+ key += buffer;
301
+ return key;
179
302
  }
180
303
 
181
304
  void paintRadial(const LayoutBox &box, const XtStyle &style, const BackgroundLayer &layer,
@@ -188,16 +311,49 @@ void paintRadial(const LayoutBox &box, const XtStyle &style, const BackgroundLay
188
311
  float ry = 0.0f;
189
312
  radialRadii(box, layer, style, viewportWidth, viewportHeight, &cx, &cy, &rx, &ry);
190
313
  if (rx <= 0.0f || ry <= 0.0f) return;
191
- const int bands = 32;
192
- for (int i = bands; i >= 1; i--) {
193
- float t = (float)i / (float)bands;
194
- Color color = filterColor(chain, gradientColorAt(layer, t));
195
- if (color.a <= 0.0f) continue;
196
- addEllipse(out, cx, cy, rx * t, ry * t, color, clip, box.world);
314
+ std::vector<GradientStop> stops = resolveStops(layer, std::max(rx, ry), style, viewportWidth,
315
+ viewportHeight);
316
+ if (stops.size() < 2) return;
317
+ /* The colour functions fold into the stops (a per-stop application is exact
318
+ * for the linear ones); `brightness()` and `opacity()` stay out, because the
319
+ * image tint carries them exactly and leaving them out keeps the ramp — and
320
+ * therefore the texture cache key — stable while a fade animates. */
321
+ FilterChain stopChain = chain;
322
+ stopChain.opacity = 1.0f;
323
+ for (size_t i = stopChain.ops.size(); i-- > 0;) {
324
+ FilterKind kind = stopChain.ops[i].kind;
325
+ if (kind == FilterKind::Brightness || kind == FilterKind::Opacity) {
326
+ stopChain.ops.erase(stopChain.ops.begin() + (ptrdiff_t)i);
327
+ }
197
328
  }
198
- /* Exact centre colour (the innermost band samples just off zero). */
199
- Color center = filterColor(chain, gradientColorAt(layer, 0.0f));
200
- addEllipse(out, cx, cy, 0.6f, 0.6f, center, clip, box.world);
329
+ auto gradient = std::make_shared<PaintGradient>();
330
+ gradient->stops.reserve(stops.size());
331
+ bool visible = false;
332
+ for (size_t i = 0; i < stops.size(); i++) {
333
+ GradientStop resolved = stops[i];
334
+ resolved.color = filterColor(stopChain, resolved.color);
335
+ visible = visible || resolved.color.a > 0.0f;
336
+ gradient->stops.push_back(resolved);
337
+ }
338
+ if (!visible) return;
339
+ gradient->center_x = cx - box.x;
340
+ gradient->center_y = cy - box.y;
341
+ gradient->radius_x = rx;
342
+ gradient->radius_y = ry;
343
+ gradient->repeating = layer.repeating;
344
+ gradient->period = stops.back().offset;
345
+ gradient->key = gradientKey(*gradient);
346
+
347
+ /* One quad for the whole layer: the renderer resolves the ramp per pixel, so
348
+ * the layer composites exactly once instead of once per translucent band. */
349
+ PaintImage image;
350
+ image.x = box.x;
351
+ image.y = box.y;
352
+ image.width = box.width;
353
+ image.height = box.height;
354
+ image.transform = box.world;
355
+ image.gradient = gradient;
356
+ addImage(out, std::move(image), clip, chain);
201
357
  }
202
358
 
203
359
  void paintImage(const LayoutBox &box, const BackgroundLayer &layer, const FilterChain &chain,
@@ -233,7 +389,7 @@ void paintBackgrounds(DisplayList &out, const LayoutBox &box, const XtStyle &sty
233
389
  for (size_t i = style.background_layers.size(); i-- > 0;) {
234
390
  const BackgroundLayer &layer = style.background_layers[i];
235
391
  if (layer.kind == BackgroundKind::Linear) {
236
- paintLinear(box, layer, chain, layerClip, out);
392
+ paintLinear(box, style, layer, chain, layerClip, viewportWidth, viewportHeight, out);
237
393
  } else if (layer.kind == BackgroundKind::Radial) {
238
394
  paintRadial(box, style, layer, chain, layerClip, viewportWidth, viewportHeight, out);
239
395
  } else {
@@ -169,6 +169,7 @@ Color filterColor(const FilterChain &chain, Color color) {
169
169
  color.b = clamp01(color.b);
170
170
  color.a = clamp01(color.a);
171
171
  }
172
+ color.a = clamp01(color.a * chain.opacity);
172
173
  return color;
173
174
  }
174
175
 
@@ -190,7 +191,7 @@ Color imageTint(const FilterChain &chain) {
190
191
  tint.r = clamp01(tint.r);
191
192
  tint.g = clamp01(tint.g);
192
193
  tint.b = clamp01(tint.b);
193
- tint.a = clamp01(tint.a);
194
+ tint.a = clamp01(tint.a * chain.opacity);
194
195
  return tint;
195
196
  }
196
197
 
@@ -25,6 +25,10 @@ namespace paint_detail {
25
25
  struct FilterChain {
26
26
  std::vector<FilterOp> ops; // colour functions (and any unknown no-ops)
27
27
  float blur = 0.0f; // resolved px
28
+ /** Product of the CSS `opacity` values on the ancestor chain (including the
29
+ * element itself). It multiplies every painted colour's alpha, which is the
30
+ * per-shape approximation of group opacity. */
31
+ float opacity = 1.0f;
28
32
  struct Shadow {
29
33
  float dx = 0.0f;
30
34
  float dy = 0.0f;
@@ -33,7 +37,7 @@ struct FilterChain {
33
37
  };
34
38
  std::vector<Shadow> shadows;
35
39
 
36
- bool empty() const { return ops.empty() && blur <= 0.0f && shadows.empty(); }
40
+ bool empty() const { return ops.empty() && blur <= 0.0f && shadows.empty() && opacity >= 1.0f; }
37
41
  };
38
42
 
39
43
  /** Append `filters` (declared on a box with `style`) to `parent`. */
@@ -27,6 +27,8 @@ SDL_GPUGraphicsPipeline *g_pipeline = nullptr;
27
27
  SDL_GPUGraphicsPipeline *g_text_pipeline = nullptr;
28
28
  SDL_GPUGraphicsPipeline *g_image_pipeline = nullptr;
29
29
  SDL_GPUSampler *g_sampler = nullptr;
30
+ SDL_GPUDevice *g_device = nullptr;
31
+ SDL_GPUTextureFormat g_swapchain_format = SDL_GPU_TEXTUREFORMAT_INVALID;
30
32
  int g_renderer_failed = 0;
31
33
  bool g_text_ok = false;
32
34
  std::unordered_map<std::string, SDL_GPUTexture *> g_image_textures;
@@ -146,6 +148,8 @@ bool xt_gui_renderer_ensure(SDL_GPUDevice *device, SDL_Window *reference) {
146
148
  using namespace xtgui::renderer_detail;
147
149
  if (g_pipeline != nullptr) return true;
148
150
  if (g_renderer_failed) return false;
151
+ g_device = device;
152
+ g_swapchain_format = SDL_GetGPUSwapchainTextureFormat(device, reference);
149
153
  const ShaderSource fill_vs = selectShader(device, ShaderProgram::Fill, false);
150
154
  const ShaderSource fill_fs = selectShader(device, ShaderProgram::Fill, true);
151
155
  if (!fill_vs.valid() || !fill_fs.valid()) {
@@ -242,6 +246,7 @@ bool xt_gui_renderer_ensure(SDL_GPUDevice *device, SDL_Window *reference) {
242
246
 
243
247
  void xt_gui_renderer_destroy(SDL_GPUDevice *device) {
244
248
  using namespace xtgui::renderer_detail;
249
+ xtgui::renderer_detail::layerRendererDestroy(device);
245
250
  if (g_pipeline != nullptr) {
246
251
  SDL_ReleaseGPUGraphicsPipeline(device, g_pipeline);
247
252
  g_pipeline = nullptr;
@@ -275,6 +280,8 @@ void xt_gui_renderer_destroy(SDL_GPUDevice *device) {
275
280
  g_atlas_full_logged = false;
276
281
  g_text_ok = false;
277
282
  g_renderer_failed = 0;
283
+ g_device = nullptr;
284
+ g_swapchain_format = SDL_GPU_TEXTUREFORMAT_INVALID;
278
285
  }
279
286
 
280
287
  void xt_gui_geometry_destroy(SDL_GPUDevice *device, XtGuiGeometry *geometry) {
@@ -286,6 +293,13 @@ void xt_gui_geometry_destroy(SDL_GPUDevice *device, XtGuiGeometry *geometry) {
286
293
  SDL_ReleaseGPUTransferBuffer(device, geometry->staging);
287
294
  geometry->staging = nullptr;
288
295
  }
296
+ for (SDL_GPUTexture *texture : geometry->scene_textures) {
297
+ if (texture != nullptr) SDL_ReleaseGPUTexture(device, texture);
298
+ }
299
+ geometry->scene_textures.clear();
300
+ geometry->scene_width = 0;
301
+ geometry->scene_height = 0;
302
+ geometry->scene_format = SDL_GPU_TEXTUREFORMAT_INVALID;
289
303
  geometry->staging_capacity = 0;
290
304
  geometry->capacity_bytes = 0;
291
305
  geometry->vertex_count = 0;
@@ -314,6 +328,15 @@ void xt_gui_build_runs(XtGuiGeometry *shapes, XtGuiGeometry *texts, XtGuiGeometr
314
328
  shapes->runs.clear();
315
329
  uint32_t cursor[3] = {0, 0, 0};
316
330
  for (const xtgui::PaintOp &op : list.ops) {
331
+ if (op.kind == xtgui::PaintKind::LayerBegin ||
332
+ op.kind == xtgui::PaintKind::LayerEnd) {
333
+ XtGuiDrawRun marker;
334
+ marker.pipeline = op.kind == xtgui::PaintKind::LayerBegin ? XtGuiDrawRun::LayerBegin
335
+ : XtGuiDrawRun::LayerEnd;
336
+ marker.layer = op.kind == xtgui::PaintKind::LayerBegin ? op.index : 0;
337
+ shapes->runs.push_back(marker);
338
+ continue;
339
+ }
317
340
  int pipeline = (int)op.kind;
318
341
  XtGuiGeometry *geometry = pipeline == 0 ? shapes : (pipeline == 1 ? texts : images);
319
342
  if (op.index >= geometry->op_counts.size()) continue;
@@ -349,17 +372,13 @@ void xt_gui_build_runs(XtGuiGeometry *shapes, XtGuiGeometry *texts, XtGuiGeometr
349
372
  }
350
373
  }
351
374
 
352
- void xt_gui_renderer_draw(SDL_GPUCommandBuffer *cmd, SDL_GPURenderPass *pass,
353
- XtGuiGeometry *shapes, XtGuiGeometry *texts, XtGuiGeometry *images,
354
- float viewport_width, float viewport_height, float pixel_density) {
355
- using namespace xtgui::renderer_detail;
356
- if (viewport_width <= 0.0f || viewport_height <= 0.0f) return;
357
- if (pixel_density <= 0.0f) pixel_density = 1.0f;
358
- float viewport[2] = {viewport_width, viewport_height};
359
- SDL_PushGPUVertexUniformData(cmd, 0, viewport, sizeof(viewport));
375
+ namespace xtgui {
376
+ namespace renderer_detail {
360
377
 
361
- int target_width = (int)(viewport_width * pixel_density + 0.5f);
362
- int target_height = (int)(viewport_height * pixel_density + 0.5f);
378
+ void drawRuns(SDL_GPURenderPass *pass, const std::vector<XtGuiDrawRun> &runs, size_t begin,
379
+ size_t end, XtGuiGeometry *shapes, XtGuiGeometry *texts, XtGuiGeometry *images,
380
+ int target_width, int target_height, float pixel_density) {
381
+ if (pass == nullptr) return;
363
382
  /* A run list alternates pipelines (background, text, background, …), so the
364
383
  * same scissor rectangle and the same pipeline were re-bound for every run.
365
384
  * Track what is bound and only issue the state change when it actually
@@ -389,7 +408,11 @@ void xt_gui_renderer_draw(SDL_GPUCommandBuffer *cmd, SDL_GPURenderPass *pass,
389
408
  SDL_BindGPUFragmentSamplers(pass, 0, &texture_binding, 1);
390
409
  bound_texture = texture;
391
410
  };
392
- for (const XtGuiDrawRun &run : shapes->runs) {
411
+ for (size_t i = begin; i < end && i < runs.size(); i++) {
412
+ const XtGuiDrawRun &run = runs[i];
413
+ if (run.pipeline == XtGuiDrawRun::LayerBegin || run.pipeline == XtGuiDrawRun::LayerEnd)
414
+ continue;
415
+ if (run.count == 0) continue;
393
416
  SDL_Rect scissor;
394
417
  scissor.x = 0;
395
418
  scissor.y = 0;
@@ -426,7 +449,7 @@ void xt_gui_renderer_draw(SDL_GPUCommandBuffer *cmd, SDL_GPURenderPass *pass,
426
449
  bindTexture(g_atlas_texture);
427
450
  SDL_DrawGPUPrimitives(pass, run.count, 1, run.first, 0);
428
451
  xtgui::g_perf_frame.drawCalls += 1;
429
- } else {
452
+ } else if (run.pipeline == XtGuiDrawRun::Image) {
430
453
  if (g_image_pipeline == nullptr || g_sampler == nullptr || images->buffer == nullptr ||
431
454
  run.texture == nullptr)
432
455
  continue;
@@ -437,7 +460,7 @@ void xt_gui_renderer_draw(SDL_GPUCommandBuffer *cmd, SDL_GPURenderPass *pass,
437
460
  }
438
461
  }
439
462
 
440
- /* Restore the full-target scissor so the next frame starts clean. */
463
+ /* Restore the full-target scissor so the next pass starts clean. */
441
464
  SDL_Rect full;
442
465
  full.x = 0;
443
466
  full.y = 0;
@@ -445,3 +468,48 @@ void xt_gui_renderer_draw(SDL_GPUCommandBuffer *cmd, SDL_GPURenderPass *pass,
445
468
  full.h = target_height;
446
469
  SDL_SetGPUScissor(pass, &full);
447
470
  }
471
+
472
+ } // namespace renderer_detail
473
+ } // namespace xtgui
474
+
475
+ void xt_gui_renderer_draw(SDL_GPUCommandBuffer *cmd, SDL_GPUTexture *swapchain,
476
+ const float clear_color[4], XtGuiGeometry *shapes,
477
+ XtGuiGeometry *texts, XtGuiGeometry *images,
478
+ const xtgui::DisplayList &list, float viewport_width,
479
+ float viewport_height, float pixel_density) {
480
+ using namespace xtgui::renderer_detail;
481
+ if (cmd == nullptr || swapchain == nullptr) return;
482
+ if (viewport_width <= 0.0f || viewport_height <= 0.0f) return;
483
+ if (pixel_density <= 0.0f) pixel_density = 1.0f;
484
+ float viewport[2] = {viewport_width, viewport_height};
485
+ SDL_PushGPUVertexUniformData(cmd, 0, viewport, sizeof(viewport));
486
+
487
+ /* `mix-blend-mode` needs the backdrop as a texture; the blend pipeline is
488
+ * compiled at run time on the SPIR-V/MSL backends. Everywhere else (or if
489
+ * that compilation failed) the ordinary single pass still paints the
490
+ * unblended content rather than dropping it. */
491
+ if (!list.layers.empty() && layerRendererReady(g_device)) {
492
+ if (drawLayeredScene(cmd, swapchain, clear_color, shapes, texts, images, list, viewport_width,
493
+ viewport_height, pixel_density))
494
+ return;
495
+ }
496
+
497
+ SDL_GPUColorTargetInfo target;
498
+ memset(&target, 0, sizeof(target));
499
+ target.texture = swapchain;
500
+ target.clear_color.r = clear_color != nullptr ? clear_color[0] : 0.0f;
501
+ target.clear_color.g = clear_color != nullptr ? clear_color[1] : 0.0f;
502
+ target.clear_color.b = clear_color != nullptr ? clear_color[2] : 0.0f;
503
+ target.clear_color.a = clear_color != nullptr ? clear_color[3] : 1.0f;
504
+ target.load_op = SDL_GPU_LOADOP_CLEAR;
505
+ target.store_op = SDL_GPU_STOREOP_STORE;
506
+ SDL_GPURenderPass *pass = SDL_BeginGPURenderPass(cmd, &target, 1, nullptr);
507
+ if (pass == nullptr) return;
508
+ int target_width = (int)(viewport_width * pixel_density + 0.5f);
509
+ int target_height = (int)(viewport_height * pixel_density + 0.5f);
510
+ if (shapes != nullptr && texts != nullptr && images != nullptr) {
511
+ drawRuns(pass, shapes->runs, 0, shapes->runs.size(), shapes, texts, images, target_width,
512
+ target_height, pixel_density);
513
+ }
514
+ SDL_EndGPURenderPass(pass);
515
+ }
@@ -33,7 +33,9 @@ struct DisplayList;
33
33
  * `clip_width <= 0` means the whole viewport. For image runs, `texture` is the
34
34
  * bound texture. */
35
35
  struct XtGuiDrawRun {
36
- enum Pipeline { Shape = 0, Text = 1, Image = 2 };
36
+ /** `LayerBegin`/`LayerEnd` are zero-count markers around a
37
+ * `mix-blend-mode` group; `layer` indexes `DisplayList::layers`. */
38
+ enum Pipeline { Shape = 0, Text = 1, Image = 2, LayerBegin = 3, LayerEnd = 4 };
37
39
  int pipeline = Shape;
38
40
  uint32_t first = 0;
39
41
  uint32_t count = 0;
@@ -42,6 +44,7 @@ struct XtGuiDrawRun {
42
44
  float clip_width = 0;
43
45
  float clip_height = 0;
44
46
  SDL_GPUTexture *texture = nullptr;
47
+ uint32_t layer = 0;
45
48
  };
46
49
 
47
50
  /** Per-window GPU geometry (vertex buffer + draw range). */
@@ -63,6 +66,12 @@ struct XtGuiGeometry {
63
66
  /** Painter-order draw runs, built from the display list by
64
67
  * `xt_gui_build_runs` and consumed by `xt_gui_renderer_draw`. */
65
68
  std::vector<XtGuiDrawRun> runs;
69
+ /** Offscreen colour targets used by `mix-blend-mode` groups (two per nesting
70
+ * level). Reused across frames and tied to the swapchain size/format. */
71
+ std::vector<SDL_GPUTexture *> scene_textures;
72
+ uint32_t scene_width = 0;
73
+ uint32_t scene_height = 0;
74
+ SDL_GPUTextureFormat scene_format = SDL_GPU_TEXTUREFORMAT_INVALID;
66
75
  };
67
76
 
68
77
  /** One GPU command buffer shared by every upload a frame performs (shape, text,
@@ -91,10 +100,15 @@ void xt_gui_geometry_destroy(SDL_GPUDevice *device, XtGuiGeometry *geometry);
91
100
  * immediately. */
92
101
  void xt_gui_geometry_upload(SDL_GPUDevice *device, XtGuiGeometry *geometry,
93
102
  const xtgui::DisplayList &list, XtGuiUploadBatch *batch = nullptr);
94
- /** Draw `geometry` inside an active render pass. */
95
- void xt_gui_renderer_draw(SDL_GPUCommandBuffer *cmd, SDL_GPURenderPass *pass,
96
- XtGuiGeometry *shapes, XtGuiGeometry *texts, XtGuiGeometry *images,
97
- float viewport_width, float viewport_height, float pixel_density);
103
+ /** Draw the display list into `swapchain`, beginning and ending its own render
104
+ * passes. With `mix-blend-mode` groups the renderer draws them to offscreen
105
+ * textures and composites them with a runtime-compiled blend shader; without
106
+ * groups this is the original single clear-and-draw pass. */
107
+ void xt_gui_renderer_draw(SDL_GPUCommandBuffer *cmd, SDL_GPUTexture *swapchain,
108
+ const float clear_color[4], XtGuiGeometry *shapes,
109
+ XtGuiGeometry *texts, XtGuiGeometry *images,
110
+ const xtgui::DisplayList &list, float viewport_width,
111
+ float viewport_height, float pixel_density);
98
112
  /** Shape `list.texts`, ensure their glyphs are in the atlas, and upload the
99
113
  * text geometry. `scale` is the device pixel density (1.0 = logical pixels). */
100
114
  void xt_gui_text_upload(SDL_GPUDevice *device, XtGuiGeometry *geometry,