ntk 8.17.8 → 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/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
 
@@ -29,6 +29,7 @@ import Picture from "./picture.js";
29
29
  import { formatForDepth } from "./pictformat.js";
30
30
  import Pixmap from "./pixmap.js";
31
31
  import { REGION_DOCS, regionId } from "./region.js";
32
+ import { snapFixed16 } from "./precise.js";
32
33
  import { routeRaster } from "./rasterize.js";
33
34
  import {
34
35
  clipRingToRect,
@@ -2098,29 +2099,33 @@ class RenderingContext2d {
2098
2099
  });
2099
2100
  }
2100
2101
 
2101
- _rasterizePolys(
2102
- picture,
2103
- shapes,
2104
- rule,
2105
- dx = 0,
2106
- dy = 0,
2107
- w = this.width,
2108
- h = this.height,
2109
- ) {
2110
- if (!shapes.length) return;
2111
- // Trapezoids are 16.16 fixed point, and a coordinate past 32,767 throws
2112
- // out of the paint (lib/cliprect.js). A shape that reaches that far is
2113
- // cut to the picture first, a pixel to spare on every side: nothing
2114
- // outside the picture is ever composited. One that does not goes as it
2115
- // came, for the server to clip.
2102
+ /**
2103
+ * The shapes a `w`×`h` picture whose pixel (0, 0) is device (-dx, -dy)
2104
+ * is sent, in device space: `_rasterizePolys` trapezoidizes them for the
2105
+ * server, and the local route hands the same ones to the rasterizer, so
2106
+ * that both rasterize one geometry (lib/precise.js).
2107
+ *
2108
+ * Trapezoids are 16.16 fixed point, and a coordinate past 32,767 throws
2109
+ * out of the paint (lib/cliprect.js). They go on the wire in device space
2110
+ * with the picture's offset beside them, which the server adds, so a
2111
+ * shape has to be in reach both in device space and in the picture's.
2112
+ * One that is not is cut to the picture first, a pixel to spare on every
2113
+ * side: nothing outside the picture is ever composited. One in reach
2114
+ * goes as it came, for the server to clip.
2115
+ */
2116
+ _wireShapes(shapes, dx = 0, dy = 0, w = this.width, h = this.height) {
2116
2117
  const x0 = -dx - 1;
2117
2118
  const y0 = -dy - 1;
2118
2119
  const x1 = w - dx + 1;
2119
2120
  const y1 = h - dy + 1;
2121
+ const rx0 = Math.max(-WIRE_REACH, -WIRE_REACH - dx);
2122
+ const ry0 = Math.max(-WIRE_REACH, -WIRE_REACH - dy);
2123
+ const rx1 = Math.min(WIRE_REACH, WIRE_REACH - dx);
2124
+ const ry1 = Math.min(WIRE_REACH, WIRE_REACH - dy);
2120
2125
  let fitted = null;
2121
2126
  for (let i = 0; i < shapes.length; i++) {
2122
2127
  const shape = shapes[i];
2123
- if (withinRect(shape, -WIRE_REACH - dx, -WIRE_REACH - dy, WIRE_REACH - dx, WIRE_REACH - dy)) {
2128
+ if (withinRect(shape, rx0, ry0, rx1, ry1)) {
2124
2129
  if (fitted) fitted.push(shape);
2125
2130
  continue;
2126
2131
  }
@@ -2128,11 +2133,30 @@ class RenderingContext2d {
2128
2133
  const ring = clipRingToRect(shape, x0, y0, x1, y1);
2129
2134
  if (ring.length >= 6) fitted.push(ring);
2130
2135
  }
2131
- const traps = trapezoidize(fitted ?? shapes, dx, dy, [], rule);
2136
+ return fitted ?? shapes;
2137
+ }
2138
+
2139
+ _rasterizePolys(
2140
+ picture,
2141
+ shapes,
2142
+ rule,
2143
+ dx = 0,
2144
+ dy = 0,
2145
+ w = this.width,
2146
+ h = this.height,
2147
+ ) {
2148
+ if (!shapes.length) return;
2149
+ // In device space and on the 16.16 grid, with the offset into the
2150
+ // picture left to the request: these are the numbers PreciseRasterizer
2151
+ // rasterizes when the same mask is drawn here, so the two routes agree
2152
+ // to the byte. An offset folded into the coordinates instead rounds
2153
+ // differently from one added to them on the wire.
2154
+ const traps = trapezoidize(this._wireShapes(shapes, dx, dy, w, h), 0, 0, [], rule);
2155
+ for (let i = 0; i < traps.length; i++) traps[i] = snapFixed16(traps[i]);
2132
2156
  // stay under the server's maximum request size
2133
2157
  const chunk = 4000 * 6;
2134
2158
  for (let i = 0; i < traps.length; i += chunk) {
2135
- this.Render.AddTraps(picture.id, 0, 0, traps.slice(i, i + chunk));
2159
+ this.Render.AddTraps(picture.id, dx, dy, traps.slice(i, i + chunk));
2136
2160
  }
2137
2161
  }
2138
2162
 
@@ -2491,17 +2515,20 @@ class RenderingContext2d {
2491
2515
  box = box ? unionBox(box, shown) : shown;
2492
2516
  }
2493
2517
  if (!box) continue;
2518
+ // what the server would be sent for the surface-sized mask, so that
2519
+ // drawn here it comes out the same
2520
+ const wire = this._wireShapes(flat);
2494
2521
  painted =
2495
2522
  this._paintThroughMask(
2496
2523
  box,
2497
- { polys: flat, rule },
2524
+ { polys: wire, rule },
2498
2525
  {
2499
2526
  edges: drawn,
2500
2527
  route: this._routeFor(cluster, edges),
2501
2528
  src,
2502
2529
  op,
2503
2530
  alpha,
2504
- server: () => this._rasterizePolys(this.fillMask, flat, rule),
2531
+ server: () => this._rasterizePolys(this.fillMask, wire, rule),
2505
2532
  },
2506
2533
  ) || painted;
2507
2534
  }
@@ -2864,6 +2891,10 @@ class RenderingContext2d {
2864
2891
  }
2865
2892
  if (w < tris.length) tris.length = w;
2866
2893
  if (!tris.length) return;
2894
+ // On the 16.16 grid the wire carries, once, for every route below: the
2895
+ // server rasterizes these numbers, and so does PreciseRasterizer when
2896
+ // the stroke is drawn here (lib/precise.js)
2897
+ for (let i = 0; i < tris.length; i++) tris[i] = snapFixed16(tris[i]);
2867
2898
 
2868
2899
  const op = this._op();
2869
2900
  // round-cap/join disks overlap the stroke body; overlapping coverage
@@ -4556,7 +4587,8 @@ class RenderingContext2d {
4556
4587
  return false;
4557
4588
 
4558
4589
  const coverage = rasterizer.rasterize({
4559
- polys: shapes,
4590
+ // what the AddTraps fallback in _applyPolyClip would send
4591
+ polys: this._wireShapes(shapes, -bb.x, -bb.y, bb.w, bb.h),
4560
4592
  rule: entry.rule,
4561
4593
  dx: -bb.x,
4562
4594
  dy: -bb.y,