ntk 8.23.1 → 8.23.2

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.
@@ -805,6 +805,10 @@ class RenderingContext2d {
805
805
  this.fillMaskDrawable = null;
806
806
  this.clipMask = null;
807
807
  this.clipMaskDrawable = null;
808
+ // a clip mask a restore let go of, for the next path clip (`_rebuildClipMask`)
809
+ this._keptClipMask = null;
810
+ // where the clip mask may hold coverage, [x, y, w, h]; null for nowhere
811
+ this._clipMaskInk = null;
808
812
  this._textStyle = null;
809
813
  this._lastFontString = null;
810
814
  this.textAlign = "start";
@@ -1224,6 +1228,8 @@ class RenderingContext2d {
1224
1228
  this.clipMaskDrawable.destroy();
1225
1229
  this.clipMask = this.clipMaskDrawable = null;
1226
1230
  }
1231
+ // one kept is the size the surface was
1232
+ this._dropKeptClipMask();
1227
1233
  // clip paths are device-space and survive the resize; rebuild the mask
1228
1234
  if (this._clips && this._clips.length) this._rebuildClipMask();
1229
1235
  }
@@ -2361,6 +2367,16 @@ class RenderingContext2d {
2361
2367
 
2362
2368
  _ensureClipMask() {
2363
2369
  if (this.clipMask) return;
2370
+ const kept = this._keptClipMask;
2371
+ if (kept) {
2372
+ // the one a restore let go of: what it holds is cleared where it was
2373
+ // written, by `_materializeClipMask`
2374
+ this._keptClipMask = null;
2375
+ this.clipMask = kept.mask;
2376
+ this.clipMaskDrawable = kept.drawable;
2377
+ this._clipMaskInk = kept.ink;
2378
+ return;
2379
+ }
2364
2380
  this.clipMaskDrawable = new Pixmap(this.window.app, {
2365
2381
  depth: 8,
2366
2382
  width: this.width,
@@ -2370,6 +2386,8 @@ class RenderingContext2d {
2370
2386
  drawable: this.clipMaskDrawable,
2371
2387
  format: this.Render.a8,
2372
2388
  });
2389
+ // a fresh pixmap's contents are undefined, all of them
2390
+ this._clipMaskInk = [0, 0, this.width, this.height];
2373
2391
  }
2374
2392
 
2375
2393
  /**
@@ -4900,12 +4918,16 @@ class RenderingContext2d {
4900
4918
  _materializeClipMask() {
4901
4919
  const R = this.Render;
4902
4920
  this._ensureClipMask();
4903
- R.FillRectangles(
4904
- R.PictOp.Src,
4905
- this.clipMask.id,
4906
- [0, 0, 0, 0],
4907
- [0, 0, this.width, this.height],
4908
- );
4921
+ // Cleared where it may hold coverage, not all over: the mask is the size
4922
+ // of the surface, and a clip a rounded row of a list puts on a window
4923
+ // cleared the whole window's worth of it for each row of each frame.
4924
+ // What it holds is never outside its first shape's box, since what is
4925
+ // intersected with it only takes coverage away (`_clipMaskInk`).
4926
+ const ink = this._clipMaskInk;
4927
+ if (ink && ink[2] > 0 && ink[3] > 0) {
4928
+ R.FillRectangles(R.PictOp.Src, this.clipMask.id, [0, 0, 0, 0], ink);
4929
+ }
4930
+ this._clipMaskInk = null;
4909
4931
  // region entries are the picture's clip, not the mask's business
4910
4932
  const shapes = this._clips.filter((entry) => !entry.region);
4911
4933
  if (!shapes.length) {
@@ -4915,6 +4937,7 @@ class RenderingContext2d {
4915
4937
  [0, 0, 0, 1],
4916
4938
  [0, 0, this.width, this.height],
4917
4939
  );
4940
+ this._clipMaskInk = [0, 0, this.width, this.height];
4918
4941
  return;
4919
4942
  }
4920
4943
  const first = shapes[0];
@@ -4931,9 +4954,20 @@ class RenderingContext2d {
4931
4954
  [0, 0, 0, 1],
4932
4955
  [x, y, right - x, bottom - y],
4933
4956
  );
4957
+ this._clipMaskInk = [x, y, right - x, bottom - y];
4934
4958
  }
4935
4959
  } else {
4936
4960
  this._applyPolyClip(first, R.PictOp.Src);
4961
+ const bb = this._polysBBox(first.polys);
4962
+ if (bb && bb.w > 0 && bb.h > 0) {
4963
+ const x = Math.max(0, Math.floor(bb.x));
4964
+ const y = Math.max(0, Math.floor(bb.y));
4965
+ const right = Math.min(this.width, Math.ceil(bb.x + bb.w));
4966
+ const bottom = Math.min(this.height, Math.ceil(bb.y + bb.h));
4967
+ if (right > x && bottom > y) {
4968
+ this._clipMaskInk = [x, y, right - x, bottom - y];
4969
+ }
4970
+ }
4937
4971
  }
4938
4972
  for (let i = 1; i < shapes.length; i++) this._intersectClip(shapes[i]);
4939
4973
  }
@@ -5084,9 +5118,18 @@ class RenderingContext2d {
5084
5118
  if (!rasterizer) return false;
5085
5119
  const { shapes, edges } = fillableShapes(entry.polys);
5086
5120
  if (!shapes.length) return false;
5087
- // routed by the clip's own extent, as a fill is (`_rawBox`), and
5088
- // rasterized over the part of it on the surface
5089
- if (this._routeFor(this._polysExtent(entry.polys) ?? bb, edges) !== "local")
5121
+ // A clip's coverage is rasterized here up to the policy's byte bound,
5122
+ // whatever its edges, where a fill is routed by its cost (`_routeFor`).
5123
+ // The server route is AddTraps into the pixmap the coverage lands on,
5124
+ // and AddTraps adds into a picture that exists: glamor — Xwayland's,
5125
+ // and Xorg's on most GPUs — has no GPU path for it, and reads the
5126
+ // pixmap back, rasterizes in software and uploads it again, a GPU sync
5127
+ // for every clip. A rounded row clipped that way took 5.6 ms on a virgl
5128
+ // Xwayland, where its raster here and a PutImage took 0.14; pixman,
5129
+ // which adds the traps itself, takes 0.03 ms less for it.
5130
+ const extent = this._polysExtent(entry.polys) ?? bb;
5131
+ const area = Math.min(extent.w, this.width) * Math.min(extent.h, this.height);
5132
+ if (!(area > 0 && area <= this.window.app.rasterPolicy.maxBytes) || !edges)
5090
5133
  return false;
5091
5134
 
5092
5135
  const coverage = rasterizer.rasterize({
@@ -5148,17 +5191,33 @@ class RenderingContext2d {
5148
5191
  }
5149
5192
 
5150
5193
  _rebuildClipMask() {
5151
- // The stack changed shape (restore, resize): drop the mask rather than
5194
+ // The stack changed shape (restore, resize): let the mask go rather than
5152
5195
  // rebuild it eagerly. A stack that went back to rectangles never needs
5153
5196
  // one again, and one that still holds a poly rebuilds on first demand
5154
5197
  // through _requireClipMask — same work, paid only if something draws.
5198
+ // It is kept, not destroyed, for the next path clip to fill again: a
5199
+ // pixmap the size of the surface was made and freed for every rounded
5200
+ // clip, sixteen a frame for a list of eight rounded rows, where the fill
5201
+ // mask beside it is kept for the context's life.
5155
5202
  if (this.clipMask) {
5156
- this.clipMask.destroy();
5157
- this.clipMaskDrawable.destroy();
5203
+ this._dropKeptClipMask();
5204
+ this._keptClipMask = {
5205
+ mask: this.clipMask,
5206
+ drawable: this.clipMaskDrawable,
5207
+ ink: this._clipMaskInk,
5208
+ };
5158
5209
  this.clipMask = this.clipMaskDrawable = null;
5159
5210
  }
5160
5211
  }
5161
5212
 
5213
+ _dropKeptClipMask() {
5214
+ const kept = this._keptClipMask;
5215
+ if (!kept) return;
5216
+ this._keptClipMask = null;
5217
+ kept.mask.destroy();
5218
+ kept.drawable.destroy();
5219
+ }
5220
+
5162
5221
  isPointInPath(...args) {
5163
5222
  let path = null;
5164
5223
  let rest = args;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.23.1",
3
+ "version": "8.23.2",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",