ntk 8.11.1 → 8.12.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 +2 -1
- package/lib/window.js +137 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,7 +106,8 @@ wnd.requestAnimationFrame(frame);
|
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
See [docs/window.md](docs/window.md) for the knobs (`frameInterval`,
|
|
109
|
-
`frameSync`, `coalesceEvents`) and the raw uncoalesced
|
|
109
|
+
`frameSync`, `maxFramesInFlight`, `coalesceEvents`) and the raw uncoalesced
|
|
110
|
+
event stream.
|
|
110
111
|
|
|
111
112
|
## Resource management
|
|
112
113
|
|
package/lib/window.js
CHANGED
|
@@ -60,6 +60,43 @@ const BACKING_GRANULARITY = 128;
|
|
|
60
60
|
*/
|
|
61
61
|
const DEFAULT_FRAME_INTERVAL = 16;
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* How many frames the fence clock lets the server owe a window at once,
|
|
65
|
+
* unless the window asks for another number.
|
|
66
|
+
*
|
|
67
|
+
* One is a frame per round trip: the client draws, the server works through
|
|
68
|
+
* it, the reply comes back, and only then does the next frame start. Neither
|
|
69
|
+
* side works while the other does, so a frame costs the sum of the two.
|
|
70
|
+
* Where the reply is slow for reasons of the server's own, most of that sum
|
|
71
|
+
* is waiting. XQuartz answers a fence only once the frame has gone to the
|
|
72
|
+
* macOS window server: 13 ms at the median during a drag, against about
|
|
73
|
+
* 0.1 ms for an idle round trip, and the drag ran at 75-78 fps on a 120 Hz
|
|
74
|
+
* panel (issue #369). With two, the next frame is drawn during that wait,
|
|
75
|
+
* and the same drag runs at 107-118. A slow link gains the same way: a frame
|
|
76
|
+
* per round trip becomes two.
|
|
77
|
+
*
|
|
78
|
+
* Not more, because past two the wait is already overlapped, and each frame
|
|
79
|
+
* queued behind a busy server, or on a link too narrow for it, is a frame of
|
|
80
|
+
* latency. Where a blit goes out through Present it stays one (see
|
|
81
|
+
* _frameLimit).
|
|
82
|
+
*/
|
|
83
|
+
const DEFAULT_MAX_FRAMES_IN_FLIGHT = 2;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* `maxFramesInFlight` as given, or a RangeError saying what it takes.
|
|
87
|
+
*
|
|
88
|
+
* Zero is refused, not read as "no limit", which is what `frameInterval: 0`
|
|
89
|
+
* would lead a reader to expect: a window that may have no frame in flight
|
|
90
|
+
* never draws. The way to wait on the server not at all is `frameSync: false`.
|
|
91
|
+
*/
|
|
92
|
+
function framesInFlightLimit(n) {
|
|
93
|
+
if (Number.isInteger(n) && n >= 1) return n;
|
|
94
|
+
throw new RangeError(
|
|
95
|
+
`ntk: maxFramesInFlight is a number of frames, 1 or more — got ${String(n)}. ` +
|
|
96
|
+
'For no limit at all, create the window with frameSync: false.'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
63
100
|
/**
|
|
64
101
|
* The range a measured vblank period has to fall in to be believed, in ms.
|
|
65
102
|
*
|
|
@@ -432,6 +469,9 @@ export default class Window extends Drawable {
|
|
|
432
469
|
if (args.id === 0) {
|
|
433
470
|
throw new Error('ntk: 0 (None) is not a window id');
|
|
434
471
|
}
|
|
472
|
+
const maxFramesInFlight = framesInFlightLimit(
|
|
473
|
+
args.maxFramesInFlight ?? DEFAULT_MAX_FRAMES_IN_FLIGHT
|
|
474
|
+
);
|
|
435
475
|
if (args.id) {
|
|
436
476
|
const cached = Window._cacheFor(app).get(args.id);
|
|
437
477
|
// TODO: check event mask, if more events in args - update it
|
|
@@ -564,12 +604,15 @@ export default class Window extends Drawable {
|
|
|
564
604
|
// that probe answers adopts the result when it lands.
|
|
565
605
|
this._frameInterval = args.frameInterval ?? app.frameInterval ?? DEFAULT_FRAME_INTERVAL;
|
|
566
606
|
this._frameIntervalExplicit = args.frameInterval !== undefined;
|
|
607
|
+
// how many fences may be outstanding before the next frame waits for one
|
|
608
|
+
// (see _frameLimit)
|
|
609
|
+
this._maxFramesInFlight = maxFramesInFlight;
|
|
567
610
|
this.frameLatency = null;
|
|
568
611
|
this._frame = {
|
|
569
612
|
pending: new Map(), // coalescible event name -> merged event
|
|
570
613
|
rafCbs: [],
|
|
571
614
|
rafId: 0,
|
|
572
|
-
|
|
615
|
+
fences: 0, // fence round trips awaiting the server's reply
|
|
573
616
|
presentInFlight: false, // present awaiting the display's CompleteNotify
|
|
574
617
|
presentAt: 0, // when that present was sent, for frameLatency
|
|
575
618
|
stallTimer: null,
|
|
@@ -1348,9 +1391,13 @@ export default class Window extends Drawable {
|
|
|
1348
1391
|
* That includes a server whose Present has no display behind it and
|
|
1349
1392
|
* makes its vertical blanks up from a timer (see _probeMsc).
|
|
1350
1393
|
*
|
|
1351
|
-
* Either way
|
|
1352
|
-
* flight:
|
|
1353
|
-
*
|
|
1394
|
+
* Either way the frames outstanding are bounded, which is what bounds the
|
|
1395
|
+
* work in flight: one present on the display's clock, and on the fence
|
|
1396
|
+
* `maxFramesInFlight` fences, two unless the window asked otherwise (see
|
|
1397
|
+
* _frameLimit). On a slow connection frames degrade to that many per round
|
|
1398
|
+
* trip instead of queueing a trail of stale updates. Two rather than one so
|
|
1399
|
+
* that the next frame can be drawn while the server is still working
|
|
1400
|
+
* through the last, instead of each waiting out the other.
|
|
1354
1401
|
*
|
|
1355
1402
|
* A timer backs both up. Under the fence it is the rate limit — at most one
|
|
1356
1403
|
* frame per `frameInterval` ms, so a fast local server isn't asked to redraw
|
|
@@ -1371,18 +1418,63 @@ export default class Window extends Drawable {
|
|
|
1371
1418
|
*/
|
|
1372
1419
|
_scheduleFrame() {
|
|
1373
1420
|
const f = this._frame;
|
|
1374
|
-
if (f.scheduled ||
|
|
1421
|
+
if (f.scheduled || this._fencesFull() || f.presentInFlight || f.timer) return;
|
|
1375
1422
|
f.scheduled = true;
|
|
1376
1423
|
setImmediate(() => {
|
|
1377
1424
|
f.scheduled = false;
|
|
1378
1425
|
// gates may have been armed after this got scheduled (work queued
|
|
1379
1426
|
// from inside a running frame, e.g. a rAF loop re-registering) —
|
|
1380
1427
|
// the completion / fence reply / timer expiry will reschedule then
|
|
1381
|
-
if (
|
|
1428
|
+
if (this._fencesFull() || f.presentInFlight || f.timer) return;
|
|
1382
1429
|
this._runFrame();
|
|
1383
1430
|
});
|
|
1384
1431
|
}
|
|
1385
1432
|
|
|
1433
|
+
/**
|
|
1434
|
+
* How many frames may be waiting for their fence before the next one waits
|
|
1435
|
+
* too: `maxFramesInFlight`, except where a blit goes out through Present.
|
|
1436
|
+
*
|
|
1437
|
+
* A second frame is safe behind a CopyArea. The server runs requests in
|
|
1438
|
+
* order, so the copy has read the backing store before anything drawn after
|
|
1439
|
+
* it lands there, and the fence says no more than that the server read the
|
|
1440
|
+
* frame — which is all the next one needs. A present with Option.Copy is
|
|
1441
|
+
* different: it owns the backing store until the copy executes, at the next
|
|
1442
|
+
* vertical blank, and the fence reply says only that the server has read the
|
|
1443
|
+
* present. A frame drawn behind it early can put half of itself on the screen
|
|
1444
|
+
* (the hazard issue #223 describes), and a second frame in flight would
|
|
1445
|
+
* widen that window. So a window whose blits are presents keeps one, whether
|
|
1446
|
+
* the fence or the display is ending its frames — on the display's clock a
|
|
1447
|
+
* present outstanding is the gate anyway, and there is never more than one.
|
|
1448
|
+
*
|
|
1449
|
+
* A window with no backing store has no blit to protect. Its frames go by
|
|
1450
|
+
* the limit as a CopyArea window's do.
|
|
1451
|
+
*/
|
|
1452
|
+
_frameLimit() {
|
|
1453
|
+
return this._blitsThroughPresent() ? 1 : this._maxFramesInFlight;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/** Have the frames waiting for their fence reached the limit? */
|
|
1457
|
+
_fencesFull() {
|
|
1458
|
+
return this._frame.fences >= this._frameLimit();
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* How many frames the server may owe this window at once, on the fence
|
|
1463
|
+
* clock, before the next one waits for the reply to the oldest: 2 unless
|
|
1464
|
+
* set. `1` is a frame per round trip. Where blits go through Present, or
|
|
1465
|
+
* the display is ending frames, one is all there is, whatever this says —
|
|
1466
|
+
* see _frameLimit.
|
|
1467
|
+
*/
|
|
1468
|
+
get maxFramesInFlight() {
|
|
1469
|
+
return this._maxFramesInFlight;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
set maxFramesInFlight(n) {
|
|
1473
|
+
// A raised limit needs no wakeup: the gate was shut because fences are
|
|
1474
|
+
// outstanding, and the next reply runs what it held back.
|
|
1475
|
+
this._maxFramesInFlight = framesInFlightLimit(n);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1386
1478
|
/**
|
|
1387
1479
|
* Minimum ms between paced frames, and between blits.
|
|
1388
1480
|
*
|
|
@@ -1636,15 +1728,24 @@ export default class Window extends Drawable {
|
|
|
1636
1728
|
for (const [name, ev] of pending) this.emit(name, ev);
|
|
1637
1729
|
}
|
|
1638
1730
|
|
|
1731
|
+
/**
|
|
1732
|
+
* Fence the frame that just went out: one request with a reply, answered
|
|
1733
|
+
* once the server has read everything before it.
|
|
1734
|
+
*
|
|
1735
|
+
* Every frame gets its own, so `fences` is exactly the number of frames the
|
|
1736
|
+
* server has not confirmed. Nothing here refuses one past the limit — the
|
|
1737
|
+
* callers only send a frame while the gate is open, and a frame sent some
|
|
1738
|
+
* other way is still better counted than left unfenced, where the gate
|
|
1739
|
+
* would open with it unconfirmed.
|
|
1740
|
+
*/
|
|
1639
1741
|
_armFence() {
|
|
1640
1742
|
if (!this._frameSyncEnabled) return;
|
|
1641
1743
|
const f = this._frame;
|
|
1642
|
-
if (f.inFlight) return;
|
|
1643
1744
|
const start = performance.now();
|
|
1644
1745
|
safeRelease(this.X, () => {
|
|
1645
|
-
f.
|
|
1746
|
+
f.fences++;
|
|
1646
1747
|
this.X.GetInputFocus(() => {
|
|
1647
|
-
f.
|
|
1748
|
+
f.fences--;
|
|
1648
1749
|
this.frameLatency = performance.now() - start;
|
|
1649
1750
|
this._frameEnded();
|
|
1650
1751
|
});
|
|
@@ -2139,8 +2240,9 @@ export default class Window extends Drawable {
|
|
|
2139
2240
|
* DOM-style requestAnimationFrame: `cb(now)` runs on this window's next
|
|
2140
2241
|
* paced frame — one per vertical blank where the display is the clock (see
|
|
2141
2242
|
* _clockOnPresent), otherwise at most once per `frameInterval` ms — and
|
|
2142
|
-
* always with
|
|
2143
|
-
*
|
|
2243
|
+
* always with a bounded number of frames outstanding (one on the display's
|
|
2244
|
+
* clock, `maxFramesInFlight` on the fence), so animation loops adapt to the
|
|
2245
|
+
* output's rate and to connection latency instead of flooding either.
|
|
2144
2246
|
* Returns an id for cancelAnimationFrame().
|
|
2145
2247
|
*/
|
|
2146
2248
|
requestAnimationFrame(cb) {
|
|
@@ -2159,9 +2261,12 @@ export default class Window extends Drawable {
|
|
|
2159
2261
|
|
|
2160
2262
|
/**
|
|
2161
2263
|
* Whether a blit this window already owes is still waiting to go out —
|
|
2162
|
-
* because the
|
|
2163
|
-
*
|
|
2164
|
-
* minimum inter-blit interval.
|
|
2264
|
+
* because the frames already sent are unanswered (as many fences unreplied
|
|
2265
|
+
* as `maxFramesInFlight` allows, or a present not yet on the display), or
|
|
2266
|
+
* because a present is deferred behind the minimum inter-blit interval.
|
|
2267
|
+
*
|
|
2268
|
+
* So it reads false with a frame in flight when the limit allows another:
|
|
2269
|
+
* a blit drawn then goes out at once, which is the question this answers.
|
|
2165
2270
|
*
|
|
2166
2271
|
* The one bit of frame-clock state worth publishing, because it is the
|
|
2167
2272
|
* difference between the two ways a toolkit can answer a discrete input.
|
|
@@ -2188,16 +2293,17 @@ export default class Window extends Drawable {
|
|
|
2188
2293
|
* asked for both gets the unpaced behaviour those options ask for.
|
|
2189
2294
|
*/
|
|
2190
2295
|
frameInFlight() {
|
|
2191
|
-
return this.
|
|
2296
|
+
return this._fencesFull() || this._frame.presentInFlight || this._presentPending;
|
|
2192
2297
|
}
|
|
2193
2298
|
|
|
2194
2299
|
_present() {
|
|
2195
2300
|
if (!this._backing) return;
|
|
2196
2301
|
const f = this._frame;
|
|
2197
2302
|
const wait = this._presentWait();
|
|
2198
|
-
if (
|
|
2199
|
-
// the server hasn't confirmed the
|
|
2200
|
-
// shown
|
|
2303
|
+
if (this._fencesFull() || f.presentInFlight || wait > 0) {
|
|
2304
|
+
// the server hasn't confirmed enough of the frames already sent, the
|
|
2305
|
+
// display hasn't shown the last one, or the last blit was too recent —
|
|
2306
|
+
// defer, leaving a wakeup
|
|
2201
2307
|
this._deferPresent(wait);
|
|
2202
2308
|
return;
|
|
2203
2309
|
}
|
|
@@ -2252,7 +2358,7 @@ export default class Window extends Drawable {
|
|
|
2252
2358
|
if (!this._presentPending && !this._dirty) return; // someone blitted it
|
|
2253
2359
|
const again = this._presentWait();
|
|
2254
2360
|
if (again > 0) return this._deferPresent(again); // a paced frame re-stamped
|
|
2255
|
-
if (
|
|
2361
|
+
if (this._fencesFull() || f.presentInFlight) return; // the frame's end is closer
|
|
2256
2362
|
this._presentNow();
|
|
2257
2363
|
if (!f.presentInFlight) this._armFence();
|
|
2258
2364
|
}, wait);
|
|
@@ -2333,15 +2439,25 @@ export default class Window extends Drawable {
|
|
|
2333
2439
|
});
|
|
2334
2440
|
}
|
|
2335
2441
|
|
|
2442
|
+
/**
|
|
2443
|
+
* Would a blit go out through the Present extension right now, rather than
|
|
2444
|
+
* as CopyArea? Not before the extensions have answered, and not once the
|
|
2445
|
+
* window has left the path (see _presentAbandoned).
|
|
2446
|
+
*/
|
|
2447
|
+
_blitsThroughPresent() {
|
|
2448
|
+
return (
|
|
2449
|
+
!!this._presentExt && !!this._fixesExt && !!this._updateRegion && !this._presentAbandoned()
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2336
2453
|
/**
|
|
2337
2454
|
* The Present form of the blit. Returns false when it did not happen, so
|
|
2338
2455
|
* the caller falls back to CopyArea — which is also what runs before the
|
|
2339
2456
|
* extensions have answered.
|
|
2340
2457
|
*/
|
|
2341
2458
|
_presentWithExtension(rects) {
|
|
2459
|
+
if (!this._blitsThroughPresent()) return false;
|
|
2342
2460
|
const P = this._presentExt;
|
|
2343
|
-
if (!P || !this._fixesExt || !this._updateRegion) return false;
|
|
2344
|
-
if (this._presentAbandoned()) return false;
|
|
2345
2461
|
safeRelease(this.X, () => {
|
|
2346
2462
|
this._fixesExt.SetRegion(
|
|
2347
2463
|
this._updateRegion,
|