ntk 8.17.7 → 8.18.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/README.md CHANGED
@@ -144,6 +144,10 @@ parser, say — can import just that piece, and loads only what it needs:
144
144
  Each is the same module the root re-exports, so importing a name both ways
145
145
  gives the same function or class: nothing is loaded, or instantiated, twice.
146
146
 
147
+ `ntk/package.json` is exported too, for the tools that read a dependency's
148
+ version or metadata that way — bundlers, React Native's resolver, version
149
+ reporters. It loads nothing.
150
+
147
151
  ## 3d graphics
148
152
 
149
153
  Two backends, chosen by `glPolicy`
package/lib/fontconfig.js CHANGED
@@ -397,7 +397,7 @@ function censusBytes() {
397
397
  const deadline = performance.now() + PREWARM_WAIT_MS;
398
398
  let status = prewarmStatus(census.base);
399
399
  while (status === null && !census.exited && performance.now() < deadline) {
400
- Atomics.wait(nap, 0, 0, 1);
400
+ nap();
401
401
  status = prewarmStatus(census.base);
402
402
  }
403
403
  census.bytes = null;
@@ -818,8 +818,16 @@ function spawnToFiles(fcs, bests = []) {
818
818
  return entries;
819
819
  }
820
820
 
821
- // the synchronous path's nap while a prewarm finishes
822
- const nap = new Int32Array(new SharedArrayBuffer(4));
821
+ // the synchronous path's nap while a prewarm finishes: a millisecond's
822
+ // Atomics.wait on a cell made by the first nap. Not at load: a page that is
823
+ // not cross-origin isolated has no SharedArrayBuffer, so one made here was a
824
+ // ReferenceError in every browser bundle of ntk, which never naps — there is
825
+ // no prewarm without node (NOT_NODE)
826
+ let napCell;
827
+ function nap() {
828
+ napCell ??= new Int32Array(new SharedArrayBuffer(4));
829
+ Atomics.wait(napCell, 0, 0, 1);
830
+ }
823
831
 
824
832
  // How long the synchronous path waits on a prewarm before asking fc-match
825
833
  // itself. fc-match answers in tens of milliseconds, and a prewarm that is
@@ -836,7 +844,7 @@ function answerSync(fc, entry) {
836
844
  let left = readPrewarm(entry.base);
837
845
  // a child that is gone has left all it ever will
838
846
  while (left === null && !entry.exited && performance.now() < deadline) {
839
- Atomics.wait(nap, 0, 0, 1);
847
+ nap();
840
848
  left = readPrewarm(entry.base);
841
849
  }
842
850
  if (inflight.get(fc) === entry) inflight.delete(fc);
@@ -866,7 +874,7 @@ function firstOfPrewarm(spec, entry) {
866
874
  const deadline = performance.now() + PREWARM_WAIT_MS;
867
875
  let status = prewarmStatus(entry.base);
868
876
  while (status === null && !entry.exited && performance.now() < deadline) {
869
- Atomics.wait(nap, 0, 0, 1);
877
+ nap();
870
878
  status = prewarmStatus(entry.base);
871
879
  }
872
880
  // not answered, or not yet: the whole read is what decides, and reports
@@ -893,7 +901,7 @@ function bestOfPrewarm(spec, entry) {
893
901
  const deadline = performance.now() + PREWARM_WAIT_MS;
894
902
  let left = readPrewarm(entry.base);
895
903
  while (left === null && !entry.exited && performance.now() < deadline) {
896
- Atomics.wait(nap, 0, 0, 1);
904
+ nap();
897
905
  left = readPrewarm(entry.base);
898
906
  }
899
907
  if (left === null) return null;
package/lib/index.js CHANGED
@@ -47,6 +47,7 @@ import {
47
47
  defaultRasterizer,
48
48
  setDefaultRasterizer
49
49
  } from './rasterize.js';
50
+ import { PreciseRasterizer } from './precise.js';
50
51
  import { DEFAULT_MASK_POLICY } from './maskcluster.js';
51
52
  import {
52
53
  DEFAULT_SHADOW_POLICY,
@@ -284,6 +285,7 @@ export {
284
285
  setDefaultFontSource,
285
286
  CoverageAccumulator,
286
287
  ScanlineRasterizer,
288
+ PreciseRasterizer,
287
289
  defaultRasterizer,
288
290
  setDefaultRasterizer,
289
291
  DEFAULT_RASTER_POLICY,
package/lib/precise.js ADDED
@@ -0,0 +1,452 @@
1
+ // RENDER's Precise rasterization, done here: the a8 coverage an X server
2
+ // draws for a Triangles or AddTraps request, computed on this side of the
3
+ // wire from the same numbers.
4
+ //
5
+ // A mask is rasterized either here or by the server (routeRaster in
6
+ // rasterize.js). The analytic ScanlineRasterizer and the server's point
7
+ // sampling come out a few levels apart at every edge, so whenever the same
8
+ // pixels were drawn by different routes in different passes — a stroke cut
9
+ // to the runs inside a clip against the whole of it, a stroke drawn straight
10
+ // to the destination against the same stroke through the mask — a partial
11
+ // repaint and a full one disagreed (issue #462). With this as the local
12
+ // rasterizer they cannot: both routes are one function of the wire geometry,
13
+ // and the route is a question of cost alone.
14
+ //
15
+ // RENDER's default poly-mode is Precise, and the protocol defines it
16
+ // (renderproto, "Polygon Rasterization"): for an 8-bit alpha, a regular grid
17
+ // 17 samples wide and 15 high, centred in the pixel; each trapezoid or
18
+ // triangle is Add-combined into the mask. 17 × 15 = 255, so a pixel's alpha
19
+ // is its count of covered samples. pixman implements it, and pixman is what
20
+ // rasterizes Triangles, Trapezoids and AddTraps in fb (Xvfb, XQuartz) and in
21
+ // glamor (Xorg's modesetting driver, Xwayland). This is a port of pixman's
22
+ // code — pixman_edge_init and _step, pixman_sample_ceil_y and _floor_y,
23
+ // rasterize_edges_8, triangle_to_trapezoids — down to its rounding. pixman
24
+ // sits up to one 16.16 unit off the spec's sample positions, and its edge
25
+ // step leaves the error term alone when a step carries nothing; matching it
26
+ // is matching the servers, so both are kept. Measured byte-identical against
27
+ // XQuartz on 300 random drawings, 24.5 M pixels.
28
+ //
29
+ // One liberty, which changes no byte: pixman adds each trapezoid into the
30
+ // mask a sample row at a time with a saturating add. Every count it adds is
31
+ // non-negative, so the saturating sum is the plain sum clamped once. Each
32
+ // sample row's span goes into a difference array instead — four cells,
33
+ // however wide the span — and each pixel row is integrated and clamped at
34
+ // the end.
35
+ //
36
+ // The input is the wire's integers. `fixed16` is what a coordinate becomes
37
+ // in a request, and the 2d context sends coordinates already on that grid
38
+ // (`snapFixed16`), so that the conversion node-x11 applies has nothing left
39
+ // to round.
40
+
41
+ import { trapezoidize } from './trapezoid.js';
42
+
43
+ // the sample grid for an 8-bit alpha, as pixman-private.h derives it
44
+ const STEP_Y_SMALL = 4369; // 65536 / 15
45
+ const STEP_Y_BIG = 4370; // 65536 - 14 * STEP_Y_SMALL
46
+ const Y_FRAC_FIRST = 2185; // STEP_Y_BIG / 2
47
+ const Y_FRAC_LAST = 63351; // Y_FRAC_FIRST + 14 * STEP_Y_SMALL
48
+ const STEP_X_SMALL = 3855; // 65536 / 17
49
+ const X_FRAC_FIRST = 1928; // (65536 - 16 * STEP_X_SMALL) / 2
50
+ const N_X_FRAC = 17;
51
+
52
+ /**
53
+ * A coordinate as the 16.16 fixed-point int32 a request carries: truncated
54
+ * toward zero, as node-x11 converts. Every coordinate the 2d context sends
55
+ * is in reach of the int32 (lib/cliprect.js's WIRE_REACH).
56
+ */
57
+ export function fixed16(v) {
58
+ return Math.trunc(v * 65536) | 0;
59
+ }
60
+
61
+ /**
62
+ * The nearest coordinate toward zero that 16.16 holds exactly. Sending this
63
+ * rather than `v` makes the request's integer `fixed16(v)` however the
64
+ * client library converts — node-x11's `parseInt(v * 65536)` reads a tiny
65
+ * product's exponent notation, so 4e-11 became 4 where it should be 0.
66
+ */
67
+ export function snapFixed16(v) {
68
+ return Math.trunc(v * 65536) / 65536;
69
+ }
70
+
71
+ // C's integer division, which truncates toward zero, exact for |a| < 2^53.
72
+ // A float quotient can land on the wrong side of an integer, so the
73
+ // remainder settles it.
74
+ function idiv(a, b) {
75
+ let q = Math.trunc(a / b);
76
+ const r = a - q * b;
77
+ if (a >= 0) {
78
+ if (r < 0) q--;
79
+ else if (r >= b) q++;
80
+ } else if (r > 0) q++;
81
+ else if (r <= -b) q--;
82
+ return q;
83
+ }
84
+
85
+ // pixman's DIV: division rounding toward -infinity, for b > 0
86
+ function floorDiv(a, b) {
87
+ return a < 0 ? idiv(a - b + 1, b) : idiv(a, b);
88
+ }
89
+
90
+ /** pixman_sample_ceil_y: the first sample row at or below y */
91
+ function sampleCeilY(y) {
92
+ let f = y & 0xffff;
93
+ let i = y - f;
94
+ f = floorDiv(f - Y_FRAC_FIRST + (STEP_Y_SMALL - 1), STEP_Y_SMALL) * STEP_Y_SMALL + Y_FRAC_FIRST;
95
+ if (f > Y_FRAC_LAST) {
96
+ if (i >> 16 === 0x7fff) {
97
+ f = 0xffff;
98
+ } else {
99
+ f = Y_FRAC_FIRST;
100
+ i += 65536;
101
+ }
102
+ }
103
+ return (i | f) | 0;
104
+ }
105
+
106
+ /** pixman_sample_floor_y: the last sample row strictly above y */
107
+ function sampleFloorY(y) {
108
+ let f = y & 0xffff;
109
+ let i = y - f;
110
+ f = floorDiv(f - 1 - Y_FRAC_FIRST, STEP_Y_SMALL) * STEP_Y_SMALL + Y_FRAC_FIRST;
111
+ if (f < Y_FRAC_FIRST) {
112
+ if (i >> 16 === -32768) {
113
+ f = 0;
114
+ } else {
115
+ f = Y_FRAC_LAST;
116
+ i -= 65536;
117
+ }
118
+ }
119
+ return (i | f) | 0;
120
+ }
121
+
122
+ // pixman_edge_t, as eight int32 fields
123
+ const X = 0;
124
+ const E = 1;
125
+ const DY = 2;
126
+ const SIGNDX = 3;
127
+ const STEPX_SMALL = 4;
128
+ const DX_SMALL = 5;
129
+ const STEPX_BIG = 6;
130
+ const DX_BIG = 7;
131
+ const left = new Int32Array(8);
132
+ const right = new Int32Array(8);
133
+
134
+ /**
135
+ * pixman_edge_init followed by its pixman_edge_step to `yStart`: the edge
136
+ * from (xTop, yTop) down to (xBot, yBot), placed on its first sample row.
137
+ */
138
+ function edgeInit(edge, yStart, xTop, yTop, xBot, yBot) {
139
+ const dx = (xBot - xTop) | 0;
140
+ const dy = (yBot - yTop) | 0;
141
+ let x = xTop;
142
+ let e = 0;
143
+ let stepx = 0;
144
+ let signdx = 0;
145
+ let rem = 0;
146
+ if (dy) {
147
+ if (dx >= 0) {
148
+ signdx = 1;
149
+ stepx = idiv(dx, dy);
150
+ rem = dx % dy;
151
+ e = -dy;
152
+ } else {
153
+ signdx = -1;
154
+ stepx = -idiv(-dx, dy);
155
+ rem = -dx % dy;
156
+ }
157
+ // _pixman_edge_multi_init for the small and the big step; n is at most
158
+ // 4370, so every product here is exact in a double
159
+ let ne = STEP_Y_SMALL * rem;
160
+ let sx = (STEP_Y_SMALL * stepx) | 0;
161
+ if (ne > 0) {
162
+ const nx = idiv(ne, dy) | 0;
163
+ ne -= nx * dy;
164
+ sx = (sx + nx * signdx) | 0;
165
+ }
166
+ edge[STEPX_SMALL] = sx;
167
+ edge[DX_SMALL] = ne;
168
+ ne = STEP_Y_BIG * rem;
169
+ sx = (STEP_Y_BIG * stepx) | 0;
170
+ if (ne > 0) {
171
+ const nx = idiv(ne, dy) | 0;
172
+ ne -= nx * dy;
173
+ sx = (sx + nx * signdx) | 0;
174
+ }
175
+ edge[STEPX_BIG] = sx;
176
+ edge[DX_BIG] = ne;
177
+ }
178
+
179
+ // pixman_edge_step(e, yStart - yTop). Its products are 64-bit in C and
180
+ // pass 2^53 for a tall edge entering the grid far below its top, so past
181
+ // that they are done in BigInt.
182
+ const n = (yStart - yTop) | 0;
183
+ const step = n * stepx;
184
+ x = Number.isSafeInteger(step)
185
+ ? (x + step) | 0
186
+ : Number(BigInt.asIntN(32, BigInt(x) + BigInt(n) * BigInt(stepx)));
187
+ const carry = n * rem;
188
+ if (Number.isSafeInteger(carry) && Number.isSafeInteger(e + carry + dy)) {
189
+ const ne = e + carry;
190
+ if (n >= 0) {
191
+ if (ne > 0) {
192
+ const nx = idiv(ne + dy - 1, dy) | 0;
193
+ e = (ne - nx * dy) | 0;
194
+ x = (x + nx * signdx) | 0;
195
+ }
196
+ } else if (ne <= -dy) {
197
+ const nx = idiv(-ne, dy) | 0;
198
+ e = (ne + nx * dy) | 0;
199
+ x = (x - nx * signdx) | 0;
200
+ }
201
+ } else {
202
+ const ne = BigInt(e) + BigInt(n) * BigInt(rem);
203
+ const bdy = BigInt(dy);
204
+ if (n >= 0) {
205
+ if (ne > 0n) {
206
+ const nx = BigInt.asIntN(32, (ne + bdy - 1n) / bdy);
207
+ e = Number(BigInt.asIntN(32, ne - nx * bdy));
208
+ x = (x + Number(nx) * signdx) | 0;
209
+ }
210
+ } else if (ne <= -bdy) {
211
+ const nx = BigInt.asIntN(32, -ne / bdy);
212
+ e = Number(BigInt.asIntN(32, ne + nx * bdy));
213
+ x = (x - Number(nx) * signdx) | 0;
214
+ }
215
+ }
216
+ edge[X] = x;
217
+ edge[E] = e;
218
+ edge[DY] = dy;
219
+ edge[SIGNDX] = signdx;
220
+ }
221
+
222
+ /**
223
+ * rasterize_edges_8 between `left` and `right`, from sample row `t` to `b`
224
+ * inclusive, into the difference array `acc` (rows `stride` apart).
225
+ */
226
+ function addSpans(acc, stride, width, t, b) {
227
+ let y = t;
228
+ let row = (y >> 16) * stride;
229
+ let lx = left[X];
230
+ let le = left[E];
231
+ const ldy = left[DY];
232
+ const lsign = left[SIGNDX];
233
+ const lsxs = left[STEPX_SMALL];
234
+ const ldxs = left[DX_SMALL];
235
+ const lsxb = left[STEPX_BIG];
236
+ const ldxb = left[DX_BIG];
237
+ let rx = right[X];
238
+ let re = right[E];
239
+ const rdy = right[DY];
240
+ const rsign = right[SIGNDX];
241
+ const rsxs = right[STEPX_SMALL];
242
+ const rdxs = right[DX_SMALL];
243
+ const rsxb = right[STEPX_BIG];
244
+ const rdxb = right[DX_BIG];
245
+ // pixman clips the span to the image: from 0, and to the last pixel of
246
+ // the row taken whole. Both are what an unclipped span would add there.
247
+ const lastX = (width << 16) - 1;
248
+ for (;;) {
249
+ const l = lx < 0 ? 0 : lx;
250
+ const r = rx >> 16 >= width ? lastX : rx;
251
+ 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
+ const li = l >> 16;
256
+ 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;
263
+ }
264
+ if (y === b) return;
265
+ if ((y & 0xffff) !== Y_FRAC_LAST) {
266
+ lx = (lx + lsxs) | 0;
267
+ le = (le + ldxs) | 0;
268
+ if (le > 0) {
269
+ le = (le - ldy) | 0;
270
+ lx = (lx + lsign) | 0;
271
+ }
272
+ rx = (rx + rsxs) | 0;
273
+ re = (re + rdxs) | 0;
274
+ if (re > 0) {
275
+ re = (re - rdy) | 0;
276
+ rx = (rx + rsign) | 0;
277
+ }
278
+ y = (y + STEP_Y_SMALL) | 0;
279
+ } else {
280
+ lx = (lx + lsxb) | 0;
281
+ le = (le + ldxb) | 0;
282
+ if (le > 0) {
283
+ le = (le - ldy) | 0;
284
+ lx = (lx + lsign) | 0;
285
+ }
286
+ rx = (rx + rsxb) | 0;
287
+ re = (re + rdxb) | 0;
288
+ if (re > 0) {
289
+ re = (re - rdy) | 0;
290
+ rx = (rx + rsign) | 0;
291
+ }
292
+ y = (y + STEP_Y_BIG) | 0;
293
+ row += stride;
294
+ }
295
+ }
296
+ }
297
+
298
+ /** the first sample row of a band from `top`, inside the grid */
299
+ function firstRow(top) {
300
+ return sampleCeilY(top < 0 ? 0 : top);
301
+ }
302
+
303
+ /** the last sample row of a band to `bottom`, inside a grid `height` high */
304
+ function lastRow(bottom, height) {
305
+ return sampleFloorY(bottom >> 16 >= height ? (height << 16) - 1 : bottom);
306
+ }
307
+
308
+ /**
309
+ * pixman_rasterize_trapezoid: a pixman_trapezoid_t — top and bottom, each
310
+ * side a line through two points — already offset into the grid
311
+ */
312
+ function addTrapezoid(acc, stride, width, height, top, bottom, l1x, l1y, l2x, l2y, r1x, r1y, r2x, r2y) {
313
+ if (l1y === l2y || r1y === r2y || !(bottom > top)) return;
314
+ const t = firstRow(top);
315
+ const b = lastRow(bottom, height);
316
+ if (b < t) return;
317
+ if (l1y <= l2y) edgeInit(left, t, l1x, l1y, l2x, l2y);
318
+ else edgeInit(left, t, l2x, l2y, l1x, l1y);
319
+ if (r1y <= r2y) edgeInit(right, t, r1x, r1y, r2x, r2y);
320
+ else edgeInit(right, t, r2x, r2y, r1x, r1y);
321
+ addSpans(acc, stride, width, t, b);
322
+ }
323
+
324
+ // whether b is clockwise of a about ref in y-down space, as pixman asks it
325
+ function clockwise(refX, refY, ax, ay, bx, by) {
326
+ const adx = ax - refX;
327
+ const ady = ay - refY;
328
+ const bdx = bx - refX;
329
+ const bdy = by - refY;
330
+ const p = bdy * adx;
331
+ const q = ady * bdx;
332
+ if (Number.isSafeInteger(p) && Number.isSafeInteger(q)) return p - q < 0;
333
+ return BigInt(bdy) * BigInt(adx) - BigInt(ady) * BigInt(bdx) < 0n;
334
+ }
335
+
336
+ /**
337
+ * Add a Triangles request's triangles — float device coordinates, six per
338
+ * triangle — to the grid, each split into two trapezoids as
339
+ * triangle_to_trapezoids does.
340
+ */
341
+ function addTriangles(acc, stride, width, height, tris, dx, dy) {
342
+ const xo = dx * 65536;
343
+ const yo = dy * 65536;
344
+ for (let i = 0; i + 5 < tris.length; i += 6) {
345
+ let tx = (fixed16(tris[i]) + xo) | 0;
346
+ let ty = (fixed16(tris[i + 1]) + yo) | 0;
347
+ let lx = (fixed16(tris[i + 2]) + xo) | 0;
348
+ let ly = (fixed16(tris[i + 3]) + yo) | 0;
349
+ let rx = (fixed16(tris[i + 4]) + xo) | 0;
350
+ let ry = (fixed16(tris[i + 5]) + yo) | 0;
351
+ let s;
352
+ // the topmost point first, ties to the left; then left before right
353
+ if (ty === ly ? tx > lx : ty > ly) {
354
+ s = lx; lx = tx; tx = s;
355
+ s = ly; ly = ty; ty = s;
356
+ }
357
+ if (ty === ry ? tx > rx : ty > ry) {
358
+ s = rx; rx = tx; tx = s;
359
+ s = ry; ry = ty; ty = s;
360
+ }
361
+ if (clockwise(tx, ty, rx, ry, lx, ly)) {
362
+ s = rx; rx = lx; lx = s;
363
+ s = ry; ry = ly; ly = s;
364
+ }
365
+ // down from the top to the higher of the other two, then on to the
366
+ // lower, with the side that turned there continuing along the third edge
367
+ const mid = ry < ly ? ry : ly;
368
+ addTrapezoid(acc, stride, width, height, ty, mid, tx, ty, lx, ly, tx, ty, rx, ry);
369
+ if (ry < ly) addTrapezoid(acc, stride, width, height, ry, ly, tx, ty, lx, ly, rx, ry, lx, ly);
370
+ else addTrapezoid(acc, stride, width, height, ly, ry, lx, ly, rx, ry, tx, ty, rx, ry);
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Add an AddTraps request's trapezoids — float device coordinates, six per
376
+ * trapezoid: top left, right, y, then bottom left, right, y — to the grid,
377
+ * as pixman_add_traps does.
378
+ */
379
+ function addTraps(acc, stride, width, height, traps, dx, dy) {
380
+ const xo = dx * 65536;
381
+ const yo = dy * 65536;
382
+ for (let i = 0; i + 5 < traps.length; i += 6) {
383
+ const topY = (fixed16(traps[i + 2]) + yo) | 0;
384
+ const botY = (fixed16(traps[i + 5]) + yo) | 0;
385
+ const t = firstRow(topY);
386
+ const b = lastRow(botY, height);
387
+ if (b < t) continue;
388
+ const tl = (fixed16(traps[i]) + xo) | 0;
389
+ const tr = (fixed16(traps[i + 1]) + xo) | 0;
390
+ const bl = (fixed16(traps[i + 3]) + xo) | 0;
391
+ const br = (fixed16(traps[i + 4]) + xo) | 0;
392
+ edgeInit(left, t, tl, topY, bl, botY);
393
+ edgeInit(right, t, tr, topY, br, botY);
394
+ addSpans(acc, stride, width, t, b);
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Each row's running sum, clamped: the saturating adds, all at once. The
400
+ * cells are zeroed as they are read, so the accumulator is ready for the
401
+ * next mask without a pass of its own.
402
+ */
403
+ function integrate(acc, stride, width, height) {
404
+ const out = new Uint8Array(width * height);
405
+ for (let y = 0; y < height; y++) {
406
+ let sum = 0;
407
+ const from = y * stride;
408
+ const to = y * width;
409
+ for (let x = 0; x < width; x++) {
410
+ sum += acc[from + x];
411
+ acc[from + x] = 0;
412
+ out[to + x] = sum > 255 ? 255 : sum;
413
+ }
414
+ // the slack cells past the row, which a span ending at its last pixel
415
+ // writes and nothing reads
416
+ acc[from + width] = 0;
417
+ acc[from + width + 1] = 0;
418
+ }
419
+ return out;
420
+ }
421
+
422
+ // One accumulator for every mask, grown to the largest asked for: masks are
423
+ // rasterized one at a time, and allocating a fresh one each time was a
424
+ // tenth of the cost of a wall of small icons, in the garbage collector.
425
+ let scratch = new Int32Array(0);
426
+
427
+ function accumulator(cells) {
428
+ if (scratch.length < cells) scratch = new Int32Array(Math.max(cells, scratch.length * 2));
429
+ return scratch;
430
+ }
431
+
432
+ /**
433
+ * A Rasterizer (docs/context-2d.md) whose masks are the ones the X server
434
+ * draws from the same geometry: `triangles` exactly as the 2d context sends
435
+ * them with Triangles, `polys` trapezoidized exactly as it sends them with
436
+ * AddTraps. A drawing gets the same bytes whichever side rasterizes it.
437
+ *
438
+ * `dx`/`dy` are whole pixels, which is all the 2d context passes; anything
439
+ * else is declined, and goes to the server.
440
+ */
441
+ export class PreciseRasterizer {
442
+ rasterize({ polys, triangles, width, height, rule, dx = 0, dy = 0 }) {
443
+ if (!Number.isInteger(dx) || !Number.isInteger(dy)) return null;
444
+ // one cell of slack: a span ending in the last column clears the cell
445
+ // past it
446
+ const stride = width + 2;
447
+ const acc = accumulator(stride * height);
448
+ if (triangles) addTriangles(acc, stride, width, height, triangles, dx, dy);
449
+ else addTraps(acc, stride, width, height, trapezoidize(polys, 0, 0, [], rule), dx, dy);
450
+ return integrate(acc, stride, width, height);
451
+ }
452
+ }
package/lib/rasterize.js CHANGED
@@ -1,11 +1,14 @@
1
1
  // Pure-JS coverage rasterization: outlines and polygons in, 8-bit alpha out.
2
2
  //
3
- // Two consumers, one algorithm:
4
- //
5
3
  // rasterizePath() glyph outlines -> a8 bitmap for XRender AddGlyphs
6
- // rasterizePolys() device-space path/stroke geometry -> a8 coverage mask,
7
- // uploaded by the 2d context instead of asking the server
8
- // to rasterize trapezoids (see routeRaster below)
4
+ // rasterizePolys() device-space polygons -> an a8 coverage mask
5
+ //
6
+ // The 2d context's masks are rasterized locally or by the server
7
+ // (routeRaster below), locally by default with PreciseRasterizer
8
+ // (lib/precise.js), which samples as the server does so that the route
9
+ // never shows. The algorithm here is the analytic one: ScanlineRasterizer,
10
+ // a Rasterizer that can still be plugged in, and what glyph bitmaps use,
11
+ // since a glyph has no server route to agree with.
9
12
  //
10
13
  // The algorithm is signed-area accumulation, the one font-rs and
11
14
  // stb_truetype v2 use. Each edge deposits the exact area it sweeps through
@@ -15,6 +18,8 @@
15
18
  // the result is closer to a reference rasterizer than 4x4 supersampling was,
16
19
  // at a fraction of the cost (~25 microseconds for a 20px icon against ~600).
17
20
 
21
+ import { PreciseRasterizer } from './precise.js';
22
+
18
23
  /**
19
24
  * Antialiased coverage accumulator over a width x height pixel grid.
20
25
  *
@@ -421,7 +426,12 @@ function curveSteps(...coords) {
421
426
  // A rasterizer that only handles, say, non-zero fills can return null
422
427
  // for everything else and stay correct.
423
428
 
424
- /** the in-repo rasterizer: CoverageAccumulator, no dependencies */
429
+ /**
430
+ * The analytic rasterizer: CoverageAccumulator, exact area coverage. Its
431
+ * edges are a few levels away from the server's point sampling, so a drawing
432
+ * it rasterizes and one the server does can disagree where they meet — the
433
+ * reason the default is PreciseRasterizer (lib/precise.js).
434
+ */
425
435
  export class ScanlineRasterizer {
426
436
  rasterize({ polys, triangles, width, height, rule, dx = 0, dy = 0 }) {
427
437
  const acc = new CoverageAccumulator(width, height);
@@ -433,9 +443,12 @@ export class ScanlineRasterizer {
433
443
 
434
444
  let _defaultRasterizer = null;
435
445
 
436
- /** the process-wide default Rasterizer */
446
+ /**
447
+ * The process-wide default Rasterizer: PreciseRasterizer, whose masks are
448
+ * byte-identical to the server's for the same geometry (lib/precise.js).
449
+ */
437
450
  export function defaultRasterizer() {
438
- if (!_defaultRasterizer) _defaultRasterizer = new ScanlineRasterizer();
451
+ if (!_defaultRasterizer) _defaultRasterizer = new PreciseRasterizer();
439
452
  return _defaultRasterizer;
440
453
  }
441
454