ntk 8.11.0 → 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 +250 -28
- 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
|
*
|
|
@@ -109,6 +146,32 @@ const REFRESH_QUANTILE = 0.25;
|
|
|
109
146
|
const STALL_TIMEOUT = 2000;
|
|
110
147
|
const STALL_TIMEOUT_FIRST = 250;
|
|
111
148
|
|
|
149
|
+
/**
|
|
150
|
+
* The frame period Present makes up for a screen with no vertical blank to
|
|
151
|
+
* offer it, in µs: 60Hz, which xorg-server 21.1 and later compute as
|
|
152
|
+
* `1000000 / 60` and 1.20 and earlier wrote down as 16667.
|
|
153
|
+
*
|
|
154
|
+
* A server started with `-fakescreenfps` at another rate is not recognised.
|
|
155
|
+
* It was asked for that clock, and keeps it.
|
|
156
|
+
*/
|
|
157
|
+
const FAKE_MSC_INTERVALS = [16666, 16667];
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* How much of the counter a made-up msc has to account for before a window
|
|
161
|
+
* believes it, in vblanks: about a quarter of a second.
|
|
162
|
+
*
|
|
163
|
+
* One completion that fits proves little. A display's counter starts after
|
|
164
|
+
* the server's clock does, so its `ust / msc` falls toward its own period as
|
|
165
|
+
* it runs, and a display a little faster than 60Hz passes through the
|
|
166
|
+
* made-up line on the way down. It stays within half a tick of the line for
|
|
167
|
+
* `interval / (interval - period)` vblanks, so a fit across this span rules
|
|
168
|
+
* out every display more than `interval / span` µs faster than the tick, which
|
|
169
|
+
* is anything past 64Hz. A display closer than that fits only while its
|
|
170
|
+
* counter crosses the line, and only a window whose first frames land in that
|
|
171
|
+
* stretch is fooled.
|
|
172
|
+
*/
|
|
173
|
+
const SYNTHETIC_MSC_SPAN = 16;
|
|
174
|
+
|
|
112
175
|
/**
|
|
113
176
|
* MotionNotify detail saying "this is the only motion you get until you ask".
|
|
114
177
|
*
|
|
@@ -406,6 +469,9 @@ export default class Window extends Drawable {
|
|
|
406
469
|
if (args.id === 0) {
|
|
407
470
|
throw new Error('ntk: 0 (None) is not a window id');
|
|
408
471
|
}
|
|
472
|
+
const maxFramesInFlight = framesInFlightLimit(
|
|
473
|
+
args.maxFramesInFlight ?? DEFAULT_MAX_FRAMES_IN_FLIGHT
|
|
474
|
+
);
|
|
409
475
|
if (args.id) {
|
|
410
476
|
const cached = Window._cacheFor(app).get(args.id);
|
|
411
477
|
// TODO: check event mask, if more events in args - update it
|
|
@@ -476,7 +542,9 @@ export default class Window extends Drawable {
|
|
|
476
542
|
this._frameSyncEnabled = args.frameSync !== false;
|
|
477
543
|
// which signal ends a frame (see _clockOnPresent): 'auto' takes the
|
|
478
544
|
// display's own vblank where the Present path can supply it and the fence
|
|
479
|
-
// everywhere else,
|
|
545
|
+
// everywhere else, including a server whose Present has no display behind
|
|
546
|
+
// it (see _probeMsc); 'present' takes Present's clock even there; 'fence'
|
|
547
|
+
// pins the round-trip clock
|
|
480
548
|
this._frameClockPref = args.frameClock ?? 'auto';
|
|
481
549
|
// _NET_WM_SYNC_REQUEST (see enableSyncRequest): the counter the window
|
|
482
550
|
// manager watches, the value it last asked for, and the watchdog that
|
|
@@ -520,6 +588,12 @@ export default class Window extends Drawable {
|
|
|
520
588
|
this._refreshSamples = [];
|
|
521
589
|
this._lastComplete = null;
|
|
522
590
|
this._clockStalled = false;
|
|
591
|
+
// Whether the msc on those completions is a display's count or a number
|
|
592
|
+
// the server works out from its clock (see _probeMsc): while undecided,
|
|
593
|
+
// the fake intervals every completion so far fits and the msc of the
|
|
594
|
+
// first of them; then the verdict.
|
|
595
|
+
this._mscProbe = { intervals: FAKE_MSC_INTERVALS, since: null };
|
|
596
|
+
this._mscSynthetic = false;
|
|
523
597
|
// `frameInterval` is a plain number, but whether the caller set it is what
|
|
524
598
|
// decides its meaning under the vblank clock — an explicit value caps the
|
|
525
599
|
// frame rate, an unasked-for default must not (see _armTimer). Assigning
|
|
@@ -530,12 +604,15 @@ export default class Window extends Drawable {
|
|
|
530
604
|
// that probe answers adopts the result when it lands.
|
|
531
605
|
this._frameInterval = args.frameInterval ?? app.frameInterval ?? DEFAULT_FRAME_INTERVAL;
|
|
532
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;
|
|
533
610
|
this.frameLatency = null;
|
|
534
611
|
this._frame = {
|
|
535
612
|
pending: new Map(), // coalescible event name -> merged event
|
|
536
613
|
rafCbs: [],
|
|
537
614
|
rafId: 0,
|
|
538
|
-
|
|
615
|
+
fences: 0, // fence round trips awaiting the server's reply
|
|
539
616
|
presentInFlight: false, // present awaiting the display's CompleteNotify
|
|
540
617
|
presentAt: 0, // when that present was sent, for frameLatency
|
|
541
618
|
stallTimer: null,
|
|
@@ -1311,10 +1388,16 @@ export default class Window extends Drawable {
|
|
|
1311
1388
|
* - a fence, everywhere else: a cheap request with a reply
|
|
1312
1389
|
* (GetInputFocus) sent after each frame; X processes requests in order,
|
|
1313
1390
|
* so its reply confirms the server consumed everything the frame drew.
|
|
1391
|
+
* That includes a server whose Present has no display behind it and
|
|
1392
|
+
* makes its vertical blanks up from a timer (see _probeMsc).
|
|
1314
1393
|
*
|
|
1315
|
-
* Either way
|
|
1316
|
-
* flight:
|
|
1317
|
-
*
|
|
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.
|
|
1318
1401
|
*
|
|
1319
1402
|
* A timer backs both up. Under the fence it is the rate limit — at most one
|
|
1320
1403
|
* frame per `frameInterval` ms, so a fast local server isn't asked to redraw
|
|
@@ -1335,18 +1418,63 @@ export default class Window extends Drawable {
|
|
|
1335
1418
|
*/
|
|
1336
1419
|
_scheduleFrame() {
|
|
1337
1420
|
const f = this._frame;
|
|
1338
|
-
if (f.scheduled ||
|
|
1421
|
+
if (f.scheduled || this._fencesFull() || f.presentInFlight || f.timer) return;
|
|
1339
1422
|
f.scheduled = true;
|
|
1340
1423
|
setImmediate(() => {
|
|
1341
1424
|
f.scheduled = false;
|
|
1342
1425
|
// gates may have been armed after this got scheduled (work queued
|
|
1343
1426
|
// from inside a running frame, e.g. a rAF loop re-registering) —
|
|
1344
1427
|
// the completion / fence reply / timer expiry will reschedule then
|
|
1345
|
-
if (
|
|
1428
|
+
if (this._fencesFull() || f.presentInFlight || f.timer) return;
|
|
1346
1429
|
this._runFrame();
|
|
1347
1430
|
});
|
|
1348
1431
|
}
|
|
1349
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
|
+
|
|
1350
1478
|
/**
|
|
1351
1479
|
* Minimum ms between paced frames, and between blits.
|
|
1352
1480
|
*
|
|
@@ -1379,14 +1507,15 @@ export default class Window extends Drawable {
|
|
|
1379
1507
|
/**
|
|
1380
1508
|
* Which clock is ending this window's frames right now: 'present' when the
|
|
1381
1509
|
* display is, 'fence' when the server round-trip is. Assignable with
|
|
1382
|
-
* 'auto' (prefer the display)
|
|
1510
|
+
* 'auto' (prefer the display), 'present' (prefer Present's clock even
|
|
1511
|
+
* where no display is behind it) or 'fence' (never use it).
|
|
1383
1512
|
*/
|
|
1384
1513
|
get frameClock() {
|
|
1385
1514
|
return this._clockOnPresent() ? 'present' : 'fence';
|
|
1386
1515
|
}
|
|
1387
1516
|
|
|
1388
1517
|
set frameClock(mode) {
|
|
1389
|
-
this._frameClockPref = mode
|
|
1518
|
+
this._frameClockPref = mode;
|
|
1390
1519
|
if (this._frameClockPref !== 'fence') this._selectPresentInput();
|
|
1391
1520
|
}
|
|
1392
1521
|
|
|
@@ -1400,21 +1529,35 @@ export default class Window extends Drawable {
|
|
|
1400
1529
|
* completion that never came drops the window back to the fence until one
|
|
1401
1530
|
* does, so a clock that stops cannot stop the window with it.
|
|
1402
1531
|
*
|
|
1403
|
-
* `
|
|
1404
|
-
*
|
|
1405
|
-
* completions the clock is made of.
|
|
1532
|
+
* `_presentAbandoned` is the one-way door — a window whose server turned
|
|
1533
|
+
* out unable to do what the path is for is off it from then on, and its
|
|
1534
|
+
* blits no longer produce the completions the clock is made of.
|
|
1406
1535
|
*/
|
|
1407
1536
|
_clockOnPresent() {
|
|
1408
1537
|
return (
|
|
1409
1538
|
this._frameClockPref !== 'fence' &&
|
|
1410
1539
|
this._frameSyncEnabled &&
|
|
1411
1540
|
!this._clockStalled &&
|
|
1412
|
-
!this.
|
|
1541
|
+
!this._presentAbandoned() &&
|
|
1413
1542
|
!!this._presentExt &&
|
|
1414
1543
|
!!this._presentEid
|
|
1415
1544
|
);
|
|
1416
1545
|
}
|
|
1417
1546
|
|
|
1547
|
+
/**
|
|
1548
|
+
* Has this window stopped presenting through the extension, the blit along
|
|
1549
|
+
* with the clock, because of what its server turned out to be?
|
|
1550
|
+
*
|
|
1551
|
+
* Two things close the door. A server that flipped a present sent with
|
|
1552
|
+
* Option.Copy owns the pixmap it was handed (see _handlePresentEvent). A
|
|
1553
|
+
* server whose msc is made up has no display behind the extension, and
|
|
1554
|
+
* only holds each copy back for a timer (see _probeMsc) — that one stays
|
|
1555
|
+
* open to a caller who asked for Present's clock by name.
|
|
1556
|
+
*/
|
|
1557
|
+
_presentAbandoned() {
|
|
1558
|
+
return !!this._presentFlipped || (this._mscSynthetic && this._frameClockPref !== 'present');
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1418
1561
|
_runFrame() {
|
|
1419
1562
|
const f = this._frame;
|
|
1420
1563
|
if (
|
|
@@ -1585,15 +1728,24 @@ export default class Window extends Drawable {
|
|
|
1585
1728
|
for (const [name, ev] of pending) this.emit(name, ev);
|
|
1586
1729
|
}
|
|
1587
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
|
+
*/
|
|
1588
1741
|
_armFence() {
|
|
1589
1742
|
if (!this._frameSyncEnabled) return;
|
|
1590
1743
|
const f = this._frame;
|
|
1591
|
-
if (f.inFlight) return;
|
|
1592
1744
|
const start = performance.now();
|
|
1593
1745
|
safeRelease(this.X, () => {
|
|
1594
|
-
f.
|
|
1746
|
+
f.fences++;
|
|
1595
1747
|
this.X.GetInputFocus(() => {
|
|
1596
|
-
f.
|
|
1748
|
+
f.fences--;
|
|
1597
1749
|
this.frameLatency = performance.now() - start;
|
|
1598
1750
|
this._frameEnded();
|
|
1599
1751
|
});
|
|
@@ -1823,6 +1975,7 @@ export default class Window extends Drawable {
|
|
|
1823
1975
|
f.presentInFlight = false;
|
|
1824
1976
|
this.frameLatency = now - f.presentAt;
|
|
1825
1977
|
this._sampleRefresh(ev, now);
|
|
1978
|
+
this._probeMsc(ev);
|
|
1826
1979
|
this._frameEnded();
|
|
1827
1980
|
}
|
|
1828
1981
|
|
|
@@ -1890,6 +2043,60 @@ export default class Window extends Drawable {
|
|
|
1890
2043
|
this.refreshInterval = sorted[Math.floor(REFRESH_QUANTILE * (sorted.length - 1))];
|
|
1891
2044
|
}
|
|
1892
2045
|
|
|
2046
|
+
/**
|
|
2047
|
+
* Is a display counting these frames, or is the server making the count up?
|
|
2048
|
+
*
|
|
2049
|
+
* Present waits for vertical blanks, and a screen whose driver has none to
|
|
2050
|
+
* offer gets fake ones: xserver's present_fake.c computes the msc from its
|
|
2051
|
+
* clock, `(ust + interval / 2) / interval`, and completes a present when a
|
|
2052
|
+
* timer set for the next tick fires. That is XQuartz, Xvfb, Xephyr, Xnest,
|
|
2053
|
+
* the VNC servers and Xorg on a driver without Present support. The
|
|
2054
|
+
* vblank clock buys nothing there and costs throughput. It paces to a 60Hz
|
|
2055
|
+
* timer rather than to the panel, which on XQuartz is the macOS
|
|
2056
|
+
* compositor's business. And a present arriving past the middle of a
|
|
2057
|
+
* period already counts as the next tick, and is aimed at the one after
|
|
2058
|
+
* that, so a frame that takes more than half a period to turn round waits
|
|
2059
|
+
* two (issue #366).
|
|
2060
|
+
*
|
|
2061
|
+
* The blit goes with the clock, as it does for a server that flips: the
|
|
2062
|
+
* copy waits for the same timer, so a window on the fence would draw
|
|
2063
|
+
* faster yet still update at most 60 times a second, and a copy could
|
|
2064
|
+
* read a frame half drawn, because the fence reply only says the server
|
|
2065
|
+
* has read the present, not run it. CopyArea runs in request order.
|
|
2066
|
+
*
|
|
2067
|
+
* So the question is whether `msc` is `ust` rounded to the nearest tick.
|
|
2068
|
+
* A display's counter cannot keep to that for long (SYNTHETIC_MSC_SPAN),
|
|
2069
|
+
* and a single completion off the line settles it the other way. Either
|
|
2070
|
+
* verdict is final: whether Present has a driver behind it is settled per
|
|
2071
|
+
* screen, when the server starts.
|
|
2072
|
+
*/
|
|
2073
|
+
_probeMsc(ev) {
|
|
2074
|
+
const probe = this._mscProbe;
|
|
2075
|
+
if (!probe) return;
|
|
2076
|
+
const intervals = probe.intervals.filter((interval) => {
|
|
2077
|
+
// msc === floor((ust + half) / interval), in integers
|
|
2078
|
+
const half = Math.floor(interval / 2);
|
|
2079
|
+
const offset = ev.ust - ev.msc * interval;
|
|
2080
|
+
return offset >= -half && offset < interval - half;
|
|
2081
|
+
});
|
|
2082
|
+
if (!intervals.length) {
|
|
2083
|
+
this._mscProbe = null; // a count, so a display
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
probe.intervals = intervals;
|
|
2087
|
+
probe.since ??= ev.msc;
|
|
2088
|
+
if (ev.msc - probe.since < SYNTHETIC_MSC_SPAN) return;
|
|
2089
|
+
this._mscProbe = null;
|
|
2090
|
+
this._mscSynthetic = true;
|
|
2091
|
+
// What was measured was a timer: no display period to report, and no
|
|
2092
|
+
// display to have dropped frames on. A window kept on this clock by
|
|
2093
|
+
// `frameClock: 'present'` learns the timer's period again from here.
|
|
2094
|
+
this.refreshInterval = null;
|
|
2095
|
+
this._refreshSamples = [];
|
|
2096
|
+
this._lastComplete = null;
|
|
2097
|
+
this.droppedFrames = 0;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
1893
2100
|
/**
|
|
1894
2101
|
* Guarantee that a frame ends even if its completion does not arrive.
|
|
1895
2102
|
*
|
|
@@ -2033,8 +2240,9 @@ export default class Window extends Drawable {
|
|
|
2033
2240
|
* DOM-style requestAnimationFrame: `cb(now)` runs on this window's next
|
|
2034
2241
|
* paced frame — one per vertical blank where the display is the clock (see
|
|
2035
2242
|
* _clockOnPresent), otherwise at most once per `frameInterval` ms — and
|
|
2036
|
-
* always with
|
|
2037
|
-
*
|
|
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.
|
|
2038
2246
|
* Returns an id for cancelAnimationFrame().
|
|
2039
2247
|
*/
|
|
2040
2248
|
requestAnimationFrame(cb) {
|
|
@@ -2053,9 +2261,12 @@ export default class Window extends Drawable {
|
|
|
2053
2261
|
|
|
2054
2262
|
/**
|
|
2055
2263
|
* Whether a blit this window already owes is still waiting to go out —
|
|
2056
|
-
* because the
|
|
2057
|
-
*
|
|
2058
|
-
* 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.
|
|
2059
2270
|
*
|
|
2060
2271
|
* The one bit of frame-clock state worth publishing, because it is the
|
|
2061
2272
|
* difference between the two ways a toolkit can answer a discrete input.
|
|
@@ -2082,16 +2293,17 @@ export default class Window extends Drawable {
|
|
|
2082
2293
|
* asked for both gets the unpaced behaviour those options ask for.
|
|
2083
2294
|
*/
|
|
2084
2295
|
frameInFlight() {
|
|
2085
|
-
return this.
|
|
2296
|
+
return this._fencesFull() || this._frame.presentInFlight || this._presentPending;
|
|
2086
2297
|
}
|
|
2087
2298
|
|
|
2088
2299
|
_present() {
|
|
2089
2300
|
if (!this._backing) return;
|
|
2090
2301
|
const f = this._frame;
|
|
2091
2302
|
const wait = this._presentWait();
|
|
2092
|
-
if (
|
|
2093
|
-
// the server hasn't confirmed the
|
|
2094
|
-
// 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
|
|
2095
2307
|
this._deferPresent(wait);
|
|
2096
2308
|
return;
|
|
2097
2309
|
}
|
|
@@ -2146,7 +2358,7 @@ export default class Window extends Drawable {
|
|
|
2146
2358
|
if (!this._presentPending && !this._dirty) return; // someone blitted it
|
|
2147
2359
|
const again = this._presentWait();
|
|
2148
2360
|
if (again > 0) return this._deferPresent(again); // a paced frame re-stamped
|
|
2149
|
-
if (
|
|
2361
|
+
if (this._fencesFull() || f.presentInFlight) return; // the frame's end is closer
|
|
2150
2362
|
this._presentNow();
|
|
2151
2363
|
if (!f.presentInFlight) this._armFence();
|
|
2152
2364
|
}, wait);
|
|
@@ -2227,15 +2439,25 @@ export default class Window extends Drawable {
|
|
|
2227
2439
|
});
|
|
2228
2440
|
}
|
|
2229
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
|
+
|
|
2230
2453
|
/**
|
|
2231
2454
|
* The Present form of the blit. Returns false when it did not happen, so
|
|
2232
2455
|
* the caller falls back to CopyArea — which is also what runs before the
|
|
2233
2456
|
* extensions have answered.
|
|
2234
2457
|
*/
|
|
2235
2458
|
_presentWithExtension(rects) {
|
|
2459
|
+
if (!this._blitsThroughPresent()) return false;
|
|
2236
2460
|
const P = this._presentExt;
|
|
2237
|
-
if (!P || !this._fixesExt || !this._updateRegion) return false;
|
|
2238
|
-
if (this._presentFlipped) return false; // see _handlePresentEvent
|
|
2239
2461
|
safeRelease(this.X, () => {
|
|
2240
2462
|
this._fixesExt.SetRegion(
|
|
2241
2463
|
this._updateRegion,
|