ntk 8.18.0 → 8.19.0

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/lib/app.js CHANGED
@@ -52,6 +52,39 @@ const DEFAULT_FRAME_INTERVAL = 16;
52
52
  const MIN_REFRESH_RATE = 20;
53
53
  const MAX_REFRESH_RATE = 1000;
54
54
 
55
+ /**
56
+ * How many of the solids drawings make for themselves a connection keeps
57
+ * (`App#transientSolid`). A fade folds each new alpha into every colour it
58
+ * draws, so it makes a few a frame; a frame faded at one alpha draws with
59
+ * fewer than this, and finds them all again on the next.
60
+ */
61
+ const TRANSIENT_SOLIDS = 256;
62
+
63
+ /**
64
+ * How many of the solids contexts paint their colour styles with a
65
+ * connection keeps (`App#styleSolid`): `fillStyle`, `strokeStyle`, a
66
+ * `TextLayout` span's colour. An app's palette is far fewer; a colour
67
+ * animated through a transition or a tween is a new one every frame.
68
+ */
69
+ const STYLE_SOLIDS = 1024;
70
+
71
+ /** How many CSS colour strings it finds them by before starting over. */
72
+ const STYLE_NAMES = 1024;
73
+
74
+ // a solid, and on a server too old for CreateSolidFill the 1x1 pixmap it
75
+ // repeats
76
+ function freeSolid(picture) {
77
+ picture.destroy();
78
+ picture._sourcePixmap?.destroy();
79
+ }
80
+
81
+ // the value `key` names, taken out of `map`
82
+ function take(map, key) {
83
+ const value = map.get(key);
84
+ if (value !== undefined) map.delete(key);
85
+ return value;
86
+ }
87
+
55
88
  /**
56
89
  * A RandR mode's vertical refresh rate in Hz, or 0 when it does not describe
57
90
  * one.
@@ -147,9 +180,21 @@ export default class App {
147
180
  // client costs no I/O — discovery waits for the first page to bind.
148
181
  sharedGlyphsFor(this);
149
182
  this._cursors = null;
183
+ // the solids a caller holds for good (solidPicture)
150
184
  this._solidPictures = new Map();
151
- // the same pictures by the CSS colour that asked for them (solidPictureOf)
185
+ // the solids contexts paint their colour styles with, least recently
186
+ // used first; a clock that ticks as they are made and moved up; and the
187
+ // same pictures by the CSS colour that asked for them (styleSolid)
188
+ this._styleSolids = new Map();
189
+ this._styleSolidLimit = STYLE_SOLIDS;
190
+ this._styleClock = 0;
152
191
  this._solidByName = new Map();
192
+ // style solids let go of in this job, freed once it has run
193
+ this._evictedSolids = [];
194
+ // the solids one drawing makes for itself, least recently used first
195
+ // (transientSolid)
196
+ this._transientSolids = new Map();
197
+ this._transientSolidLimit = TRANSIENT_SOLIDS;
153
198
  this._rasterizer = undefined;
154
199
  this._shm = undefined;
155
200
  this._extensionPromises = new Map();
@@ -959,14 +1004,18 @@ export default class App {
959
1004
  }
960
1005
 
961
1006
  /**
962
- * A repeating source Picture of one colour, for compositing. Components
963
- * are 0..1 floats, premultiplied by alpha.
1007
+ * A repeating source Picture of one colour, for compositing, held for as
1008
+ * long as the connection is open. Components are 0..1 floats,
1009
+ * premultiplied by alpha.
964
1010
  *
965
- * Cached per connection, not per context: colours a context uses are the
966
- * app's palette, and `Surface.render` builds a context per call — a
967
- * per-context cache made every render recreate its fill colours
968
- * server-side. Nothing evicts; a solid is a few dozen bytes on the server
969
- * and an app cycles through few colours. Freed in `close()`.
1011
+ * Cached per connection, not per context: `Surface.render` builds a
1012
+ * context per call, and a per-context cache made every render recreate
1013
+ * its colours server-side. Nothing evicts: whoever asked may hold the
1014
+ * picture as long as it likes — a caller what `createSolidPicture` handed
1015
+ * back, a context its glyph source — so this is for the few colours an
1016
+ * app holds. Freed in `close()`. A context's colour styles are
1017
+ * `styleSolid`'s, and the colours a drawing makes for itself
1018
+ * `transientSolid`'s: once a colour animates, neither is few.
970
1019
  *
971
1020
  * On RENDER >= 0.10 a solid is one CreateSolidFill request; an older
972
1021
  * server (or a hand-built App that skipped the version handshake in
@@ -976,7 +1025,151 @@ export default class App {
976
1025
  const key = `${r}|${g}|${b}|${a}`;
977
1026
  let p = this._solidPictures.get(key);
978
1027
  if (p) return p;
1028
+ // one a style paints with, or a drawing made for itself, is held from
1029
+ // here on: out of its LRU, rather than made a second time
1030
+ p =
1031
+ take(this._styleSolids, key) ??
1032
+ take(this._transientSolids, key) ??
1033
+ this._createSolid(key, r, g, b, a);
1034
+ // and no LRU's clock ages it now (_usedStyle)
1035
+ p._used = Infinity;
1036
+ this._solidPictures.set(key, p);
1037
+ return p;
1038
+ }
979
1039
 
1040
+ /**
1041
+ * The solid a context paints a colour style with — `fillStyle`,
1042
+ * `strokeStyle`, a `TextLayout` span's colour: a CSS colour string, or a
1043
+ * premultiplied `[r, g, b, a]`. A string that is not a colour throws, as
1044
+ * `fillStyle` always has.
1045
+ *
1046
+ * Found by the string, where there is one: a UI sets the same few strings
1047
+ * again on every paint — every box's background, every border — and the
1048
+ * way to a solid is a parse, a premultiply and a key made of four numbers
1049
+ * turned into text, where by the string it is one lookup.
1050
+ *
1051
+ * Not held for good, as `solidPicture`'s are: a colour animated through a
1052
+ * transition or a tween is a new solid every frame, so the `STYLE_SOLIDS`
1053
+ * most recently used are kept and the rest let go. One let go is marked
1054
+ * `_evicted` at once, so that nothing hands it out again, and freed when
1055
+ * the job that let it go has run. X runs a client's requests in order, so
1056
+ * every request sent with it in that job reaches the server first: within
1057
+ * a job, holding one is safe however many colours are asked for meanwhile
1058
+ * — and a drawing does ask, taking its style before the shadow it paints
1059
+ * first makes colours of its own. Past the job, whoever holds one keeps
1060
+ * the style too, and asks again on finding the mark, as a context does
1061
+ * for `fillStyle` and `strokeStyle` (its `_backgroundPicture`).
1062
+ */
1063
+ styleSolid(value) {
1064
+ if (typeof value !== 'string') return this._styleSolid(value);
1065
+ const p = this._solidByName.get(value);
1066
+ if (p !== undefined && !p._evicted) return this._usedStyle(p);
1067
+ const c = cssColor(value);
1068
+ if (!c) throw new Error(`Not a color: ${JSON.stringify(value)}`);
1069
+ const solid = this._styleSolid(c);
1070
+ // spellings are unbounded too: an app animating a colour writes a new
1071
+ // string every frame
1072
+ if (this._solidByName.size >= STYLE_NAMES) this._solidByName.clear();
1073
+ this._solidByName.set(value, solid);
1074
+ return solid;
1075
+ }
1076
+
1077
+ // styleSolid by the numbers: a premultiplied [r, g, b, a]
1078
+ _styleSolid([r, g, b, a]) {
1079
+ const key = `${r}|${g}|${b}|${a}`;
1080
+ const held = this._solidPictures.get(key);
1081
+ if (held) return held;
1082
+ const lru = this._styleSolids;
1083
+ let p = lru.get(key);
1084
+ if (p) return this._usedStyle(p);
1085
+ p = this._createSolid(key, r, g, b, a);
1086
+ p._used = ++this._styleClock;
1087
+ lru.set(key, p);
1088
+ for (const [oldest, picture] of lru) {
1089
+ if (lru.size <= this._styleSolidLimit || picture === p) break;
1090
+ lru.delete(oldest);
1091
+ this._evictStyle(picture);
1092
+ }
1093
+ return p;
1094
+ }
1095
+
1096
+ /**
1097
+ * A style solid asked for again: moved to the recent end of the LRU, but
1098
+ * only once it has aged into the older half. Moved on every ask, a
1099
+ * `fillStyle` set to a string it knows cost six times what the lookup
1100
+ * does; this way it stays a lookup and a comparison, and a colour asked
1101
+ * for every frame is still not the one let go.
1102
+ */
1103
+ _usedStyle(p) {
1104
+ if (this._styleClock - p._used > this._styleSolidLimit / 2) {
1105
+ const lru = this._styleSolids;
1106
+ lru.delete(p._key);
1107
+ lru.set(p._key, p);
1108
+ p._used = ++this._styleClock;
1109
+ }
1110
+ return p;
1111
+ }
1112
+
1113
+ // a style solid let go: marked, and freed once the job has run (styleSolid)
1114
+ _evictStyle(picture) {
1115
+ picture._evicted = true;
1116
+ const evicted = this._evictedSolids;
1117
+ evicted.push(picture);
1118
+ if (evicted.length === 1) queueMicrotask(() => this._freeEvictedSolids());
1119
+ }
1120
+
1121
+ _freeEvictedSolids() {
1122
+ const evicted = this._evictedSolids;
1123
+ this._evictedSolids = [];
1124
+ for (const picture of evicted) freeSolid(picture);
1125
+ }
1126
+
1127
+ /**
1128
+ * A solid for the requests of one drawing call, and nothing after them:
1129
+ * the colours a drawing makes for itself — a style with `globalAlpha`
1130
+ * folded into it, or the alpha alone in a mask slot. A fade makes new ones
1131
+ * nearly every frame; kept the way `solidPicture` keeps its own, every
1132
+ * fade left frames × colours of them on the server until the connection
1133
+ * closed. So these are kept apart: the `TRANSIENT_SOLIDS` most recently
1134
+ * used, the least recent freed as the next is made. A drawing faded at
1135
+ * one alpha frame after frame finds its few again each time.
1136
+ *
1137
+ * Freeing one is safe because X runs a client's requests in the order
1138
+ * they were sent, so the FreePicture reaches the server after every
1139
+ * request sent with the picture before it. What that cannot cover is a
1140
+ * request sent after — hence the contract: use the solid in the requests
1141
+ * of the call that asked for it, and drop it. Never keep one or hand it
1142
+ * out. Once evicted its id goes back to the pool, and the next solid made
1143
+ * can take it, in another colour.
1144
+ *
1145
+ * A colour the app keeps anyway, for a caller (`solidPicture`) or a style
1146
+ * (`styleSolid`), is answered from there: at full opacity, a rounded
1147
+ * box's corners paint with the style's own.
1148
+ */
1149
+ transientSolid(r, g, b, a) {
1150
+ const key = `${r}|${g}|${b}|${a}`;
1151
+ let p = this._solidPictures.get(key) ?? this._styleSolids.get(key);
1152
+ if (p) return p;
1153
+ const lru = this._transientSolids;
1154
+ p = lru.get(key);
1155
+ if (p) {
1156
+ // Map iteration order is insertion order — re-insert to mark recent
1157
+ lru.delete(key);
1158
+ lru.set(key, p);
1159
+ return p;
1160
+ }
1161
+ p = this._createSolid(key, r, g, b, a);
1162
+ lru.set(key, p);
1163
+ for (const [oldest, picture] of lru) {
1164
+ if (lru.size <= this._transientSolidLimit || picture === p) break;
1165
+ lru.delete(oldest);
1166
+ freeSolid(picture);
1167
+ }
1168
+ return p;
1169
+ }
1170
+
1171
+ _createSolid(key, r, g, b, a) {
1172
+ let p;
980
1173
  const Render = this.display.Render;
981
1174
  const [major, minor] = Render.version || [0, 0];
982
1175
  if (major > 0 || minor >= 10) {
@@ -991,27 +1184,13 @@ export default class App {
991
1184
  p = new Picture(this, { id: pid });
992
1185
  p._sourcePixmap = pixmap; // keep the 1x1 pixmap alive alongside the picture
993
1186
  }
994
- this._solidPictures.set(key, p);
995
- return p;
996
- }
997
-
998
- /**
999
- * The solid picture a CSS colour string paints with. A UI sets the same few
1000
- * strings again on every paint — every box's background, every border —
1001
- * and the way to `solidPicture` is a parse, a premultiply and a key made of
1002
- * four numbers turned into text; by the string it is one lookup. Throws, as
1003
- * `fillStyle` always has, for a string that is not a colour.
1004
- */
1005
- solidPictureOf(value) {
1006
- let p = this._solidByName.get(value);
1007
- if (p !== undefined) return p;
1008
- const c = cssColor(value);
1009
- if (!c) throw new Error(`Not a color: ${JSON.stringify(value)}`);
1010
- p = this.solidPicture(c[0], c[1], c[2], c[3]);
1011
- // spellings are unbounded where solids are not: an app animating a
1012
- // colour writes a new string every frame
1013
- if (this._solidByName.size >= 1024) this._solidByName.clear();
1014
- this._solidByName.set(value, p);
1187
+ // what it paints with, so a drawing can fold `globalAlpha` into it: glyphs
1188
+ // composite straight from their source, with no mask slot to carry one
1189
+ p._rgba = Object.freeze([r, g, b, a]);
1190
+ // and what the style LRU needs of it (styleSolid)
1191
+ p._key = key;
1192
+ p._evicted = false;
1193
+ p._used = 0;
1015
1194
  return p;
1016
1195
  }
1017
1196
 
@@ -1028,13 +1207,16 @@ export default class App {
1028
1207
  // shadow coverage is cached per connection for the same reason solids
1029
1208
  // are: the contexts that drew it are long gone (see lib/shadow.js)
1030
1209
  dropShadowSurfaces(this);
1031
- // shared by every context that ever asked (see solidPicture), so their
1032
- // lifetime is the connection's — context teardown must not free them
1033
- for (const picture of this._solidPictures.values()) {
1034
- picture.destroy();
1035
- picture._sourcePixmap?.destroy();
1036
- }
1210
+ // shared by every context that ever asked (see solidPicture), so they are
1211
+ // the app's to free, never a context's teardown; whatever is left goes
1212
+ // with the connection, the style solids this job let go of included
1213
+ for (const picture of this._solidPictures.values()) freeSolid(picture);
1214
+ for (const picture of this._styleSolids.values()) freeSolid(picture);
1215
+ for (const picture of this._transientSolids.values()) freeSolid(picture);
1216
+ this._freeEvictedSolids();
1037
1217
  this._solidPictures.clear();
1218
+ this._styleSolids.clear();
1219
+ this._transientSolids.clear();
1038
1220
  this._solidByName.clear();
1039
1221
  // GPU contexts are shared by every direct GL surface on this connection
1040
1222
  // (see renderingcontext_gles.js), so they outlive individual contexts and
package/lib/precise.js CHANGED
@@ -144,22 +144,27 @@ function edgeInit(edge, yStart, xTop, yTop, xBot, yBot) {
144
144
  let signdx = 0;
145
145
  let rem = 0;
146
146
  if (dy) {
147
+ // Plain float divisions truncate exactly here. A quotient of integers
148
+ // whose dividend is below 2^45 sits at least 1/dy from any integer it
149
+ // is not, and a double holds it to within (dividend / dy) · 2^-52, far
150
+ // closer than that. The step from the line's top below, whose dividend
151
+ // can pass 2^53, keeps idiv.
147
152
  if (dx >= 0) {
148
153
  signdx = 1;
149
- stepx = idiv(dx, dy);
150
- rem = dx % dy;
154
+ stepx = Math.trunc(dx / dy);
155
+ rem = dx - stepx * dy;
151
156
  e = -dy;
152
157
  } else {
153
158
  signdx = -1;
154
- stepx = -idiv(-dx, dy);
155
- rem = -dx % dy;
159
+ stepx = -Math.trunc(-dx / dy);
160
+ rem = -dx + stepx * dy;
156
161
  }
157
162
  // _pixman_edge_multi_init for the small and the big step; n is at most
158
163
  // 4370, so every product here is exact in a double
159
164
  let ne = STEP_Y_SMALL * rem;
160
165
  let sx = (STEP_Y_SMALL * stepx) | 0;
161
166
  if (ne > 0) {
162
- const nx = idiv(ne, dy) | 0;
167
+ const nx = Math.trunc(ne / dy);
163
168
  ne -= nx * dy;
164
169
  sx = (sx + nx * signdx) | 0;
165
170
  }
@@ -168,7 +173,7 @@ function edgeInit(edge, yStart, xTop, yTop, xBot, yBot) {
168
173
  ne = STEP_Y_BIG * rem;
169
174
  sx = (STEP_Y_BIG * stepx) | 0;
170
175
  if (ne > 0) {
171
- const nx = idiv(ne, dy) | 0;
176
+ const nx = Math.trunc(ne / dy);
172
177
  ne -= nx * dy;
173
178
  sx = (sx + nx * signdx) | 0;
174
179
  }
@@ -219,9 +224,20 @@ function edgeInit(edge, yStart, xTop, yTop, xBot, yBot) {
219
224
  edge[SIGNDX] = signdx;
220
225
  }
221
226
 
227
+ // RENDER_SAMPLES_X for every fraction of a pixel: how many of a pixel's 17
228
+ // sample columns lie left of an edge that crosses it there
229
+ const SAMPLES_X = new Uint8Array(65536);
230
+ for (let f = 0; f < 65536; f++) SAMPLES_X[f] = ((f + X_FRAC_FIRST) / STEP_X_SMALL) | 0;
231
+
222
232
  /**
223
233
  * rasterize_edges_8 between `left` and `right`, from sample row `t` to `b`
224
234
  * inclusive, into the difference array `acc` (rows `stride` apart).
235
+ *
236
+ * A span adds 17 - ls to the pixel it starts in, 17 to each pixel after, rs
237
+ * to the pixel it ends in and nothing past it: the difference of two steps,
238
+ * each two cells of `acc`. While an edge stays in one pixel from sample row
239
+ * to sample row, as a steep one does for all fifteen, its two cells are
240
+ * summed here and written once.
225
241
  */
226
242
  function addSpans(acc, stride, width, t, b) {
227
243
  let y = t;
@@ -245,54 +261,92 @@ function addSpans(acc, stride, width, t, b) {
245
261
  // pixman clips the span to the image: from 0, and to the last pixel of
246
262
  // the row taken whole. Both are what an unclipped span would add there.
247
263
  const lastX = (width << 16) - 1;
264
+ // each edge's step not yet written: the pixel it is in (-1 for none), and
265
+ // what it adds to that cell and to the next
266
+ let lPixel = -1;
267
+ let lAt = 0;
268
+ let lPast = 0;
269
+ let rPixel = -1;
270
+ let rAt = 0;
271
+ let rPast = 0;
272
+ // an edge's step carries one more unit of x when its error term passes
273
+ // zero: computed, not branched on, since a slope's carries come in no
274
+ // pattern a branch predictor can learn
275
+ let c = 0;
248
276
  for (;;) {
249
277
  const l = lx < 0 ? 0 : lx;
250
278
  const r = rx >> 16 >= width ? lastX : rx;
251
279
  if (r > l) {
252
- // the span adds 17 - ls to the pixel it starts in, 17 to each pixel
253
- // after, rs to the pixel it ends in and nothing past it: the
254
- // difference of two steps, each two cells of `acc`
255
280
  const li = l >> 16;
281
+ const ls = SAMPLES_X[l & 0xffff];
282
+ if (li !== lPixel) {
283
+ if (lPixel >= 0) {
284
+ acc[row + lPixel] += lAt;
285
+ acc[row + lPixel + 1] += lPast;
286
+ }
287
+ lPixel = li;
288
+ lAt = 0;
289
+ lPast = 0;
290
+ }
291
+ lAt += N_X_FRAC - ls;
292
+ lPast += ls;
256
293
  const ri = r >> 16;
257
- const ls = (((l & 0xffff) + X_FRAC_FIRST) / STEP_X_SMALL) | 0;
258
- const rs = (((r & 0xffff) + X_FRAC_FIRST) / STEP_X_SMALL) | 0;
259
- acc[row + li] += N_X_FRAC - ls;
260
- acc[row + li + 1] += ls;
261
- acc[row + ri] -= N_X_FRAC - rs;
262
- acc[row + ri + 1] -= rs;
294
+ const rs = SAMPLES_X[r & 0xffff];
295
+ if (ri !== rPixel) {
296
+ if (rPixel >= 0) {
297
+ acc[row + rPixel] -= rAt;
298
+ acc[row + rPixel + 1] -= rPast;
299
+ }
300
+ rPixel = ri;
301
+ rAt = 0;
302
+ rPast = 0;
303
+ }
304
+ rAt += N_X_FRAC - rs;
305
+ rPast += rs;
263
306
  }
264
- if (y === b) return;
307
+ if (y === b) break;
265
308
  if ((y & 0xffff) !== Y_FRAC_LAST) {
266
- lx = (lx + lsxs) | 0;
267
309
  le = (le + ldxs) | 0;
268
- if (le > 0) {
269
- le = (le - ldy) | 0;
270
- lx = (lx + lsign) | 0;
271
- }
272
- rx = (rx + rsxs) | 0;
310
+ c = -le >>> 31;
311
+ le = (le - c * ldy) | 0;
312
+ lx = (lx + lsxs + c * lsign) | 0;
273
313
  re = (re + rdxs) | 0;
274
- if (re > 0) {
275
- re = (re - rdy) | 0;
276
- rx = (rx + rsign) | 0;
277
- }
314
+ c = -re >>> 31;
315
+ re = (re - c * rdy) | 0;
316
+ rx = (rx + rsxs + c * rsign) | 0;
278
317
  y = (y + STEP_Y_SMALL) | 0;
279
318
  } else {
280
- lx = (lx + lsxb) | 0;
281
319
  le = (le + ldxb) | 0;
282
- if (le > 0) {
283
- le = (le - ldy) | 0;
284
- lx = (lx + lsign) | 0;
285
- }
286
- rx = (rx + rsxb) | 0;
320
+ c = -le >>> 31;
321
+ le = (le - c * ldy) | 0;
322
+ lx = (lx + lsxb + c * lsign) | 0;
287
323
  re = (re + rdxb) | 0;
288
- if (re > 0) {
289
- re = (re - rdy) | 0;
290
- rx = (rx + rsign) | 0;
291
- }
324
+ c = -re >>> 31;
325
+ re = (re - c * rdy) | 0;
326
+ rx = (rx + rsxb + c * rsign) | 0;
292
327
  y = (y + STEP_Y_BIG) | 0;
328
+ // the pixel row is done: write what it left pending
329
+ if (lPixel >= 0) {
330
+ acc[row + lPixel] += lAt;
331
+ acc[row + lPixel + 1] += lPast;
332
+ lPixel = -1;
333
+ }
334
+ if (rPixel >= 0) {
335
+ acc[row + rPixel] -= rAt;
336
+ acc[row + rPixel + 1] -= rPast;
337
+ rPixel = -1;
338
+ }
293
339
  row += stride;
294
340
  }
295
341
  }
342
+ if (lPixel >= 0) {
343
+ acc[row + lPixel] += lAt;
344
+ acc[row + lPixel + 1] += lPast;
345
+ }
346
+ if (rPixel >= 0) {
347
+ acc[row + rPixel] -= rAt;
348
+ acc[row + rPixel + 1] -= rPast;
349
+ }
296
350
  }
297
351
 
298
352
  /** the first sample row of a band from `top`, inside the grid */