ntk 8.10.0 → 8.11.1

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.
@@ -1,5 +1,6 @@
1
1
  import LineBreaker from 'linebreak';
2
2
 
3
+ import { rasterizePolys } from '../rasterize.js';
3
4
  import { drawGlyphRuns } from './glyphs.js';
4
5
  import { embeddingLevels, normalizedLevels, reorderRuns } from './shape.js';
5
6
 
@@ -533,6 +534,53 @@ export class TextLayout {
533
534
  return this;
534
535
  }
535
536
 
537
+ /**
538
+ * How much of each pixel the glyphs cover, one byte a pixel, without
539
+ * drawing them anywhere — for text that is drawn where a 2d context is
540
+ * not: a GL surface's label atlas, a distance field (react-x11#673).
541
+ *
542
+ * The raster is the layout box in whole pixels with `pad` pixels round it
543
+ * (`width` × `height` bytes, row-major), and the layout's origin — the
544
+ * (x, y) of `draw(ctx, x, y)` — is at (pad, pad). Every glyph's outline is
545
+ * placed where the shaped runs put it, unrounded, and the whole layout is
546
+ * filled once, nonzero, so overlapping glyphs cover a pixel once: the
547
+ * outlines' own coverage, unhinted, which is what a raster that will be
548
+ * scaled wants. Nothing is uploaded and nothing is read back.
549
+ *
550
+ * @param {{ pad?: number }} [options]
551
+ * @returns {{ width: number, height: number, data: Uint8Array } | null}
552
+ */
553
+ coverage({ pad = 0 } = {}) {
554
+ const p = pad > 0 ? Math.ceil(pad) : 0;
555
+ const width = Math.ceil(this.width) + p * 2;
556
+ const height = Math.ceil(this.height) + p * 2;
557
+ if (!(width > 0 && height > 0)) return null;
558
+ const polys = [];
559
+ for (const line of this.lines) {
560
+ for (const r of line.runs) {
561
+ const { font, size, glyphs } = r.run;
562
+ let cursor = p + line.x + r.x;
563
+ const baseline = p + line.baseline;
564
+ for (const g of glyphs) {
565
+ const gx = cursor + g.dx;
566
+ const gy = baseline - g.dy;
567
+ cursor += g.ax;
568
+ for (const poly of font.outline(g.id, size)) {
569
+ const placed = new Float64Array(poly.length);
570
+ for (let i = 0; i < poly.length; i += 2) {
571
+ placed[i] = poly[i] + gx;
572
+ placed[i + 1] = poly[i + 1] + gy;
573
+ }
574
+ polys.push(placed);
575
+ }
576
+ }
577
+ }
578
+ }
579
+ const data = new Uint8Array(width * height);
580
+ if (polys.length > 0) rasterizePolys(polys, width, height, 'nonzero', { out: data });
581
+ return { width, height, data };
582
+ }
583
+
536
584
  // ---- caret positioning / hit testing ----------------------------------
537
585
  //
538
586
  // Both methods speak logical **code-point** indices (what you get from
package/lib/window.js CHANGED
@@ -109,6 +109,32 @@ const REFRESH_QUANTILE = 0.25;
109
109
  const STALL_TIMEOUT = 2000;
110
110
  const STALL_TIMEOUT_FIRST = 250;
111
111
 
112
+ /**
113
+ * The frame period Present makes up for a screen with no vertical blank to
114
+ * offer it, in µs: 60Hz, which xorg-server 21.1 and later compute as
115
+ * `1000000 / 60` and 1.20 and earlier wrote down as 16667.
116
+ *
117
+ * A server started with `-fakescreenfps` at another rate is not recognised.
118
+ * It was asked for that clock, and keeps it.
119
+ */
120
+ const FAKE_MSC_INTERVALS = [16666, 16667];
121
+
122
+ /**
123
+ * How much of the counter a made-up msc has to account for before a window
124
+ * believes it, in vblanks: about a quarter of a second.
125
+ *
126
+ * One completion that fits proves little. A display's counter starts after
127
+ * the server's clock does, so its `ust / msc` falls toward its own period as
128
+ * it runs, and a display a little faster than 60Hz passes through the
129
+ * made-up line on the way down. It stays within half a tick of the line for
130
+ * `interval / (interval - period)` vblanks, so a fit across this span rules
131
+ * out every display more than `interval / span` µs faster than the tick, which
132
+ * is anything past 64Hz. A display closer than that fits only while its
133
+ * counter crosses the line, and only a window whose first frames land in that
134
+ * stretch is fooled.
135
+ */
136
+ const SYNTHETIC_MSC_SPAN = 16;
137
+
112
138
  /**
113
139
  * MotionNotify detail saying "this is the only motion you get until you ask".
114
140
  *
@@ -476,7 +502,9 @@ export default class Window extends Drawable {
476
502
  this._frameSyncEnabled = args.frameSync !== false;
477
503
  // which signal ends a frame (see _clockOnPresent): 'auto' takes the
478
504
  // display's own vblank where the Present path can supply it and the fence
479
- // everywhere else, 'fence' pins the round-trip clock
505
+ // everywhere else, including a server whose Present has no display behind
506
+ // it (see _probeMsc); 'present' takes Present's clock even there; 'fence'
507
+ // pins the round-trip clock
480
508
  this._frameClockPref = args.frameClock ?? 'auto';
481
509
  // _NET_WM_SYNC_REQUEST (see enableSyncRequest): the counter the window
482
510
  // manager watches, the value it last asked for, and the watchdog that
@@ -520,6 +548,12 @@ export default class Window extends Drawable {
520
548
  this._refreshSamples = [];
521
549
  this._lastComplete = null;
522
550
  this._clockStalled = false;
551
+ // Whether the msc on those completions is a display's count or a number
552
+ // the server works out from its clock (see _probeMsc): while undecided,
553
+ // the fake intervals every completion so far fits and the msc of the
554
+ // first of them; then the verdict.
555
+ this._mscProbe = { intervals: FAKE_MSC_INTERVALS, since: null };
556
+ this._mscSynthetic = false;
523
557
  // `frameInterval` is a plain number, but whether the caller set it is what
524
558
  // decides its meaning under the vblank clock — an explicit value caps the
525
559
  // frame rate, an unasked-for default must not (see _armTimer). Assigning
@@ -1311,6 +1345,8 @@ export default class Window extends Drawable {
1311
1345
  * - a fence, everywhere else: a cheap request with a reply
1312
1346
  * (GetInputFocus) sent after each frame; X processes requests in order,
1313
1347
  * so its reply confirms the server consumed everything the frame drew.
1348
+ * That includes a server whose Present has no display behind it and
1349
+ * makes its vertical blanks up from a timer (see _probeMsc).
1314
1350
  *
1315
1351
  * Either way at most one is outstanding, which is what bounds the work in
1316
1352
  * flight: on a slow connection frames degrade to one per round-trip instead
@@ -1379,14 +1415,15 @@ export default class Window extends Drawable {
1379
1415
  /**
1380
1416
  * Which clock is ending this window's frames right now: 'present' when the
1381
1417
  * display is, 'fence' when the server round-trip is. Assignable with
1382
- * 'auto' (prefer the display) or 'fence' (never use it).
1418
+ * 'auto' (prefer the display), 'present' (prefer Present's clock even
1419
+ * where no display is behind it) or 'fence' (never use it).
1383
1420
  */
1384
1421
  get frameClock() {
1385
1422
  return this._clockOnPresent() ? 'present' : 'fence';
1386
1423
  }
1387
1424
 
1388
1425
  set frameClock(mode) {
1389
- this._frameClockPref = mode === 'present' ? 'auto' : mode;
1426
+ this._frameClockPref = mode;
1390
1427
  if (this._frameClockPref !== 'fence') this._selectPresentInput();
1391
1428
  }
1392
1429
 
@@ -1400,21 +1437,35 @@ export default class Window extends Drawable {
1400
1437
  * completion that never came drops the window back to the fence until one
1401
1438
  * does, so a clock that stops cannot stop the window with it.
1402
1439
  *
1403
- * `_presentFlipped` is the one-way door — a server that has flipped a copy
1404
- * present is off this path for good, and its blits no longer produce the
1405
- * completions the clock is made of.
1440
+ * `_presentAbandoned` is the one-way door — a window whose server turned
1441
+ * out unable to do what the path is for is off it from then on, and its
1442
+ * blits no longer produce the completions the clock is made of.
1406
1443
  */
1407
1444
  _clockOnPresent() {
1408
1445
  return (
1409
1446
  this._frameClockPref !== 'fence' &&
1410
1447
  this._frameSyncEnabled &&
1411
1448
  !this._clockStalled &&
1412
- !this._presentFlipped &&
1449
+ !this._presentAbandoned() &&
1413
1450
  !!this._presentExt &&
1414
1451
  !!this._presentEid
1415
1452
  );
1416
1453
  }
1417
1454
 
1455
+ /**
1456
+ * Has this window stopped presenting through the extension, the blit along
1457
+ * with the clock, because of what its server turned out to be?
1458
+ *
1459
+ * Two things close the door. A server that flipped a present sent with
1460
+ * Option.Copy owns the pixmap it was handed (see _handlePresentEvent). A
1461
+ * server whose msc is made up has no display behind the extension, and
1462
+ * only holds each copy back for a timer (see _probeMsc) — that one stays
1463
+ * open to a caller who asked for Present's clock by name.
1464
+ */
1465
+ _presentAbandoned() {
1466
+ return !!this._presentFlipped || (this._mscSynthetic && this._frameClockPref !== 'present');
1467
+ }
1468
+
1418
1469
  _runFrame() {
1419
1470
  const f = this._frame;
1420
1471
  if (
@@ -1823,6 +1874,7 @@ export default class Window extends Drawable {
1823
1874
  f.presentInFlight = false;
1824
1875
  this.frameLatency = now - f.presentAt;
1825
1876
  this._sampleRefresh(ev, now);
1877
+ this._probeMsc(ev);
1826
1878
  this._frameEnded();
1827
1879
  }
1828
1880
 
@@ -1890,6 +1942,60 @@ export default class Window extends Drawable {
1890
1942
  this.refreshInterval = sorted[Math.floor(REFRESH_QUANTILE * (sorted.length - 1))];
1891
1943
  }
1892
1944
 
1945
+ /**
1946
+ * Is a display counting these frames, or is the server making the count up?
1947
+ *
1948
+ * Present waits for vertical blanks, and a screen whose driver has none to
1949
+ * offer gets fake ones: xserver's present_fake.c computes the msc from its
1950
+ * clock, `(ust + interval / 2) / interval`, and completes a present when a
1951
+ * timer set for the next tick fires. That is XQuartz, Xvfb, Xephyr, Xnest,
1952
+ * the VNC servers and Xorg on a driver without Present support. The
1953
+ * vblank clock buys nothing there and costs throughput. It paces to a 60Hz
1954
+ * timer rather than to the panel, which on XQuartz is the macOS
1955
+ * compositor's business. And a present arriving past the middle of a
1956
+ * period already counts as the next tick, and is aimed at the one after
1957
+ * that, so a frame that takes more than half a period to turn round waits
1958
+ * two (issue #366).
1959
+ *
1960
+ * The blit goes with the clock, as it does for a server that flips: the
1961
+ * copy waits for the same timer, so a window on the fence would draw
1962
+ * faster yet still update at most 60 times a second, and a copy could
1963
+ * read a frame half drawn, because the fence reply only says the server
1964
+ * has read the present, not run it. CopyArea runs in request order.
1965
+ *
1966
+ * So the question is whether `msc` is `ust` rounded to the nearest tick.
1967
+ * A display's counter cannot keep to that for long (SYNTHETIC_MSC_SPAN),
1968
+ * and a single completion off the line settles it the other way. Either
1969
+ * verdict is final: whether Present has a driver behind it is settled per
1970
+ * screen, when the server starts.
1971
+ */
1972
+ _probeMsc(ev) {
1973
+ const probe = this._mscProbe;
1974
+ if (!probe) return;
1975
+ const intervals = probe.intervals.filter((interval) => {
1976
+ // msc === floor((ust + half) / interval), in integers
1977
+ const half = Math.floor(interval / 2);
1978
+ const offset = ev.ust - ev.msc * interval;
1979
+ return offset >= -half && offset < interval - half;
1980
+ });
1981
+ if (!intervals.length) {
1982
+ this._mscProbe = null; // a count, so a display
1983
+ return;
1984
+ }
1985
+ probe.intervals = intervals;
1986
+ probe.since ??= ev.msc;
1987
+ if (ev.msc - probe.since < SYNTHETIC_MSC_SPAN) return;
1988
+ this._mscProbe = null;
1989
+ this._mscSynthetic = true;
1990
+ // What was measured was a timer: no display period to report, and no
1991
+ // display to have dropped frames on. A window kept on this clock by
1992
+ // `frameClock: 'present'` learns the timer's period again from here.
1993
+ this.refreshInterval = null;
1994
+ this._refreshSamples = [];
1995
+ this._lastComplete = null;
1996
+ this.droppedFrames = 0;
1997
+ }
1998
+
1893
1999
  /**
1894
2000
  * Guarantee that a frame ends even if its completion does not arrive.
1895
2001
  *
@@ -2235,7 +2341,7 @@ export default class Window extends Drawable {
2235
2341
  _presentWithExtension(rects) {
2236
2342
  const P = this._presentExt;
2237
2343
  if (!P || !this._fixesExt || !this._updateRegion) return false;
2238
- if (this._presentFlipped) return false; // see _handlePresentEvent
2344
+ if (this._presentAbandoned()) return false;
2239
2345
  safeRelease(this.X, () => {
2240
2346
  this._fixesExt.SetRegion(
2241
2347
  this._updateRegion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.10.0",
3
+ "version": "8.11.1",
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",