ntk 6.1.0 → 6.3.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/lib/widgets/htmlview.js +4 -2
- package/lib/widgets/markdownview.js +4 -2
- package/lib/window.js +401 -26
- package/package.json +1 -1
package/lib/widgets/htmlview.js
CHANGED
|
@@ -105,8 +105,10 @@ export default class HtmlView {
|
|
|
105
105
|
if (this.window) {
|
|
106
106
|
this._ctx = this.window.getContext('2d');
|
|
107
107
|
this.window.on('expose', () => this.render());
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
// a move is a ConfigureNotify too; re-laying out a document per step
|
|
109
|
+
// of a window drag is exactly what ev.resized is for
|
|
110
|
+
this.window.on('resize', (ev) => {
|
|
111
|
+
if (ev.resized) this._layoutWidth = -1;
|
|
110
112
|
});
|
|
111
113
|
this.window.on('mousedown', (ev) => {
|
|
112
114
|
if (ev.keycode === 4 || ev.keycode === 5) {
|
|
@@ -74,8 +74,10 @@ export default class MarkdownView {
|
|
|
74
74
|
if (this.window) {
|
|
75
75
|
this._ctx = this.window.getContext('2d');
|
|
76
76
|
this.window.on('expose', () => this.render());
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
// a move is a ConfigureNotify too; re-laying out a document per step
|
|
78
|
+
// of a window drag is exactly what ev.resized is for
|
|
79
|
+
this.window.on('resize', (ev) => {
|
|
80
|
+
if (ev.resized) this._layoutWidth = -1; // invalidate
|
|
79
81
|
});
|
|
80
82
|
this.window.on('mousedown', (ev) => {
|
|
81
83
|
if (ev.keycode !== 1 || !this.onLink) return; // left button only
|
package/lib/window.js
CHANGED
|
@@ -355,6 +355,20 @@ export default class Window extends Drawable {
|
|
|
355
355
|
// (see docs/window.md "Frames, coalescing and slow connections")
|
|
356
356
|
this._coalesce = args.coalesceEvents !== false;
|
|
357
357
|
this._frameSyncEnabled = args.frameSync !== false;
|
|
358
|
+
// _NET_WM_SYNC_REQUEST (see enableSyncRequest): the counter the window
|
|
359
|
+
// manager watches, the value it last asked for, and the watchdog that
|
|
360
|
+
// guarantees an answer even when nothing repaints
|
|
361
|
+
this._syncCounter = null;
|
|
362
|
+
this._syncRequestAtom = 0;
|
|
363
|
+
this._syncPending = null;
|
|
364
|
+
this._syncWatchdog = null;
|
|
365
|
+
// Present-based blits (see enablePresent): the extension objects, the
|
|
366
|
+
// XFixes region reused as the update region, and the serial the server
|
|
367
|
+
// echoes back in its completion events
|
|
368
|
+
this._presentExt = null;
|
|
369
|
+
this._fixesExt = null;
|
|
370
|
+
this._updateRegion = 0;
|
|
371
|
+
this._presentSerial = 0;
|
|
358
372
|
this.frameInterval = args.frameInterval ?? 16;
|
|
359
373
|
this.frameLatency = null;
|
|
360
374
|
this._frame = {
|
|
@@ -364,7 +378,12 @@ export default class Window extends Drawable {
|
|
|
364
378
|
inFlight: false, // fence round-trip awaiting the server's reply
|
|
365
379
|
timer: null,
|
|
366
380
|
scheduled: false,
|
|
367
|
-
needsRedraw: false
|
|
381
|
+
needsRedraw: false,
|
|
382
|
+
// minimum inter-blit pacing: when the last blit went out, and the timer
|
|
383
|
+
// that guarantees a blit held back by the interval still lands.
|
|
384
|
+
// -Infinity so the first blit after construction is never delayed.
|
|
385
|
+
lastPresentAt: -Infinity,
|
|
386
|
+
presentTimer: null
|
|
368
387
|
};
|
|
369
388
|
|
|
370
389
|
if (!args.id) {
|
|
@@ -386,6 +405,10 @@ export default class Window extends Drawable {
|
|
|
386
405
|
this.height = args.height ?? 800;
|
|
387
406
|
this.x = args.x ?? 0;
|
|
388
407
|
this.y = args.y ?? 0;
|
|
408
|
+
// the baseline 'resize' compares against (see _tagResize): what the
|
|
409
|
+
// window was asked to be, so the window manager's first ConfigureNotify
|
|
410
|
+
// reports only what it actually overrode
|
|
411
|
+
this._deliveredGeom = { x: this.x, y: this.y, width: this.width, height: this.height };
|
|
389
412
|
const values = {
|
|
390
413
|
// NorthWest bit gravity: keep the old content anchored during
|
|
391
414
|
// resize instead of discarding it (less flicker between the resize
|
|
@@ -419,6 +442,8 @@ export default class Window extends Drawable {
|
|
|
419
442
|
this.eventMask = 0;
|
|
420
443
|
this.visual = 0;
|
|
421
444
|
this.depth = 0;
|
|
445
|
+
// an adopted window's geometry is not known until GetGeometry replies
|
|
446
|
+
this._deliveredGeom = null;
|
|
422
447
|
// populate width and height
|
|
423
448
|
X.GetGeometry(this.id, (err, res) => {
|
|
424
449
|
if (!err) {
|
|
@@ -427,6 +452,10 @@ export default class Window extends Drawable {
|
|
|
427
452
|
this.x = res.xPos;
|
|
428
453
|
this.y = res.yPos;
|
|
429
454
|
this.depth = res.depth;
|
|
455
|
+
// nothing was known about this window until now, so until this
|
|
456
|
+
// reply lands a 'resize' reports both moved and resized (see
|
|
457
|
+
// _tagResize) rather than guessing
|
|
458
|
+
this._deliveredGeom ??= { x: this.x, y: this.y, width: this.width, height: this.height };
|
|
430
459
|
}
|
|
431
460
|
this._readyPromiseResolve();
|
|
432
461
|
});
|
|
@@ -472,6 +501,17 @@ export default class Window extends Drawable {
|
|
|
472
501
|
if (args.alwaysOnTop) {
|
|
473
502
|
this.setAlwaysOnTop(true);
|
|
474
503
|
}
|
|
504
|
+
if (args.syncRequest && !args.id) {
|
|
505
|
+
// fire and forget: the window manager only reads the counter when it
|
|
506
|
+
// starts managing the window, and map() is the caller's next line at
|
|
507
|
+
// the earliest. `await wnd.enableSyncRequest()` when that is too close.
|
|
508
|
+
this.enableSyncRequest().catch((err) => this.app.options.onXError?.(err));
|
|
509
|
+
}
|
|
510
|
+
if (args.present && !args.id) {
|
|
511
|
+
// fire and forget: until the extensions answer, blits use CopyArea,
|
|
512
|
+
// which is what they would have done anyway
|
|
513
|
+
this.enablePresent().catch((err) => this.app.options.onXError?.(err));
|
|
514
|
+
}
|
|
475
515
|
|
|
476
516
|
X.event_consumers[this.id] = this;
|
|
477
517
|
this.on('event', (ev) => {
|
|
@@ -523,10 +563,14 @@ export default class Window extends Drawable {
|
|
|
523
563
|
// deliver buffered state events before a discrete one so handlers see
|
|
524
564
|
// them in the order they happened (a drag sees the move, then the up)
|
|
525
565
|
this._flushCoalesced();
|
|
566
|
+
if (eventName === 'resize') this._tagResize(ntkev);
|
|
526
567
|
this.emit(eventName, ntkev);
|
|
527
568
|
// a WM_DELETE_WINDOW ClientMessage is the window manager *asking*, and
|
|
528
569
|
// 'close' is that question in a form an application can answer
|
|
529
|
-
if (eventName === 'message')
|
|
570
|
+
if (eventName === 'message') {
|
|
571
|
+
this._emitCloseRequest(ntkev);
|
|
572
|
+
this._handleSyncRequest(ntkev);
|
|
573
|
+
}
|
|
530
574
|
// anything drawn during the handlers becomes visible in one blit
|
|
531
575
|
if (this._dirty) this._present();
|
|
532
576
|
});
|
|
@@ -628,8 +672,6 @@ export default class Window extends Drawable {
|
|
|
628
672
|
this._dirty = false;
|
|
629
673
|
this._backingValid = false;
|
|
630
674
|
this._presentScheduled = false;
|
|
631
|
-
this._backedW = this.width;
|
|
632
|
-
this._backedH = this.height;
|
|
633
675
|
|
|
634
676
|
this._presentGc = this.X.AllocID();
|
|
635
677
|
this.X.CreateGC(this._presentGc, this.id, { graphicsExposures: 0 });
|
|
@@ -642,10 +684,11 @@ export default class Window extends Drawable {
|
|
|
642
684
|
this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, () => {});
|
|
643
685
|
}
|
|
644
686
|
|
|
687
|
+
// a pure move is a ConfigureNotify too, and reallocating a backing
|
|
688
|
+
// pixmap per step of a window drag is the bug this flag exists to spare
|
|
689
|
+
// everyone (see _tagResize)
|
|
645
690
|
this.on('resize', (ev) => {
|
|
646
|
-
if (ev.
|
|
647
|
-
this._backedW = ev.width;
|
|
648
|
-
this._backedH = ev.height;
|
|
691
|
+
if (!ev.resized) return;
|
|
649
692
|
this._allocBacking(ev.width, ev.height);
|
|
650
693
|
this._backingValid = false;
|
|
651
694
|
this._requestRedraw();
|
|
@@ -802,8 +845,13 @@ export default class Window extends Drawable {
|
|
|
802
845
|
* - a timer: at most one paced frame per `frameInterval` ms, so a fast
|
|
803
846
|
* local server doesn't get redraws at input-device rate.
|
|
804
847
|
*
|
|
805
|
-
* Discrete events (mousedown, keydown, ...) bypass the timer for latency
|
|
806
|
-
*
|
|
848
|
+
* Discrete events (mousedown, keydown, ...) bypass the timer for latency:
|
|
849
|
+
* the first blit after a quiet moment goes out with the handler's own
|
|
850
|
+
* requests. Their blits are still bounded, by the fence and by a minimum
|
|
851
|
+
* inter-blit interval (also `frameInterval`) — see _present(). Without that
|
|
852
|
+
* interval a stream of discrete events blits at round-trip rate, which on a
|
|
853
|
+
* local server is several hundred per second, and under a compositor every
|
|
854
|
+
* blit is a recomposite.
|
|
807
855
|
*/
|
|
808
856
|
_scheduleFrame() {
|
|
809
857
|
const f = this._frame;
|
|
@@ -821,7 +869,15 @@ export default class Window extends Drawable {
|
|
|
821
869
|
|
|
822
870
|
_runFrame() {
|
|
823
871
|
const f = this._frame;
|
|
824
|
-
if (
|
|
872
|
+
if (
|
|
873
|
+
!f.pending.size &&
|
|
874
|
+
!f.rafCbs.length &&
|
|
875
|
+
!f.needsRedraw &&
|
|
876
|
+
!this._dirty &&
|
|
877
|
+
!this._presentPending &&
|
|
878
|
+
this._syncPending == null
|
|
879
|
+
)
|
|
880
|
+
return;
|
|
825
881
|
this._flushCoalesced();
|
|
826
882
|
if (f.needsRedraw) {
|
|
827
883
|
f.needsRedraw = false;
|
|
@@ -844,6 +900,9 @@ export default class Window extends Drawable {
|
|
|
844
900
|
for (const entry of cbs) entry.cb(now);
|
|
845
901
|
}
|
|
846
902
|
if (this._dirty || this._presentPending) this._presentNow();
|
|
903
|
+
// covers a window with no backing store, and a resize that changed
|
|
904
|
+
// nothing: _presentNow did not run, but the frame is still "handled"
|
|
905
|
+
this._ackSyncRequest();
|
|
847
906
|
this._armFence();
|
|
848
907
|
this._armTimer();
|
|
849
908
|
}
|
|
@@ -875,6 +934,46 @@ export default class Window extends Drawable {
|
|
|
875
934
|
this._scheduleFrame();
|
|
876
935
|
}
|
|
877
936
|
|
|
937
|
+
/**
|
|
938
|
+
* Say what a 'resize' actually changed.
|
|
939
|
+
*
|
|
940
|
+
* 'resize' is ConfigureNotify, which fires for pure moves and for
|
|
941
|
+
* reparents as much as for size changes — under an opaque-move window
|
|
942
|
+
* manager a window drag is one per pointer step. Every consumer used to
|
|
943
|
+
* rediscover that by keeping its own copy of the last size (this library
|
|
944
|
+
* included, in the backing-store listener), and one that didn't paid a
|
|
945
|
+
* full relayout per step of a drag.
|
|
946
|
+
*
|
|
947
|
+
* So the delivered event says which it was:
|
|
948
|
+
*
|
|
949
|
+
* - `ev.resized` — the size differs from the last delivered event's
|
|
950
|
+
* - `ev.moved` — the position does
|
|
951
|
+
* - `ev.previous` — that event's `{x, y, width, height}`, for a delta;
|
|
952
|
+
* null on an adopted window whose geometry is not known yet, where
|
|
953
|
+
* both flags read true because nothing can be ruled out
|
|
954
|
+
*
|
|
955
|
+
* Measured against the last *delivered* event, not the last raw one, so
|
|
956
|
+
* that coalescing cannot swallow a change: a frame that merges two moves
|
|
957
|
+
* and a resize reports both, where per-hop flags would report only the
|
|
958
|
+
* final hop. The merged raw events in `ev.coalesced` are not tagged for
|
|
959
|
+
* that reason — the flags are a property of the delivery.
|
|
960
|
+
*
|
|
961
|
+
* Position is compared as reported. A reparenting window manager sends
|
|
962
|
+
* real ConfigureNotify in frame coordinates and synthetic ones in root
|
|
963
|
+
* coordinates (ICCCM 4.2.3), so a frame's own offset can read as a move
|
|
964
|
+
* on the first event after the switch. That is the safe direction —
|
|
965
|
+
* spurious work, never missed work — and the alternative is a
|
|
966
|
+
* TranslateCoordinates round trip per event, which is the cost this flag
|
|
967
|
+
* exists to avoid.
|
|
968
|
+
*/
|
|
969
|
+
_tagResize(ev) {
|
|
970
|
+
const prev = this._deliveredGeom;
|
|
971
|
+
ev.previous = prev;
|
|
972
|
+
ev.moved = !prev || ev.x !== prev.x || ev.y !== prev.y;
|
|
973
|
+
ev.resized = !prev || ev.width !== prev.width || ev.height !== prev.height;
|
|
974
|
+
this._deliveredGeom = { x: ev.x, y: ev.y, width: ev.width, height: ev.height };
|
|
975
|
+
}
|
|
976
|
+
|
|
878
977
|
_flushCoalesced() {
|
|
879
978
|
const pending = this._frame.pending;
|
|
880
979
|
if (!pending.size) return;
|
|
@@ -884,6 +983,7 @@ export default class Window extends Drawable {
|
|
|
884
983
|
const ev = pending.get(name);
|
|
885
984
|
if (ev) {
|
|
886
985
|
pending.delete(name);
|
|
986
|
+
if (name === 'resize') this._tagResize(ev);
|
|
887
987
|
this.emit(name, ev);
|
|
888
988
|
}
|
|
889
989
|
}
|
|
@@ -901,9 +1001,10 @@ export default class Window extends Drawable {
|
|
|
901
1001
|
f.inFlight = false;
|
|
902
1002
|
this.frameLatency = performance.now() - start;
|
|
903
1003
|
if (this._presentPending) {
|
|
904
|
-
// a blit deferred while the fence was in flight: show it
|
|
905
|
-
|
|
906
|
-
|
|
1004
|
+
// a blit deferred while the fence was in flight: show it as soon as
|
|
1005
|
+
// the minimum inter-blit interval allows (immediately when the last
|
|
1006
|
+
// blit is already older than that, which is the common case)
|
|
1007
|
+
this._present();
|
|
907
1008
|
}
|
|
908
1009
|
if ((f.pending.size || f.rafCbs.length || f.needsRedraw) && !f.timer) this._scheduleFrame();
|
|
909
1010
|
});
|
|
@@ -927,6 +1028,36 @@ export default class Window extends Drawable {
|
|
|
927
1028
|
clearTimeout(f.timer);
|
|
928
1029
|
f.timer = null;
|
|
929
1030
|
}
|
|
1031
|
+
if (f.presentTimer) {
|
|
1032
|
+
clearTimeout(f.presentTimer);
|
|
1033
|
+
f.presentTimer = null;
|
|
1034
|
+
}
|
|
1035
|
+
if (this._syncWatchdog) {
|
|
1036
|
+
clearTimeout(this._syncWatchdog);
|
|
1037
|
+
this._syncWatchdog = null;
|
|
1038
|
+
}
|
|
1039
|
+
// Destroying the counter releases any Await the window manager is
|
|
1040
|
+
// blocked in, so a window that goes away mid-drag does not strand it.
|
|
1041
|
+
if (this._syncCounter && this._sync) {
|
|
1042
|
+
const counter = this._syncCounter;
|
|
1043
|
+
this._syncCounter = null;
|
|
1044
|
+
this._syncPending = null;
|
|
1045
|
+
safeRelease(this.X, () => {
|
|
1046
|
+
this._sync.DestroyCounter(counter);
|
|
1047
|
+
this.X.ReleaseID(counter);
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
if (this._updateRegion && this._fixesExt) {
|
|
1051
|
+
const region = this._updateRegion;
|
|
1052
|
+
this._updateRegion = 0;
|
|
1053
|
+
this._presentExt = null;
|
|
1054
|
+
const fixes = this._fixesExt;
|
|
1055
|
+
this._fixesExt = null;
|
|
1056
|
+
safeRelease(this.X, () => {
|
|
1057
|
+
fixes.DestroyRegion(region);
|
|
1058
|
+
this.X.ReleaseID(region);
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
930
1061
|
f.pending.clear();
|
|
931
1062
|
f.rafCbs = [];
|
|
932
1063
|
f.needsRedraw = false;
|
|
@@ -963,9 +1094,11 @@ export default class Window extends Drawable {
|
|
|
963
1094
|
* Drawing the response inside the event handler costs a frame less latency:
|
|
964
1095
|
* the blit goes out with the handler's own requests (see the `_present()`
|
|
965
1096
|
* at the end of the event dispatch) instead of waiting for the next paced
|
|
966
|
-
* frame
|
|
967
|
-
*
|
|
968
|
-
*
|
|
1097
|
+
* frame — provided the last blit is at least `frameInterval` old, which is
|
|
1098
|
+
* what a discrete input arriving out of the blue always is. Drawing it while
|
|
1099
|
+
* a frame is in flight costs a frame's *work* for nothing: the present is
|
|
1100
|
+
* deferred until the ack and coalesces with the one already pending, so only
|
|
1101
|
+
* the last paint is ever seen. So `false`
|
|
969
1102
|
* means "draw it now", `true` means "leave it to the frame clock" — which
|
|
970
1103
|
* is how a burst of discrete events (a spun wheel) paints its first notch
|
|
971
1104
|
* immediately and folds the rest into one catch-up frame.
|
|
@@ -980,15 +1113,129 @@ export default class Window extends Drawable {
|
|
|
980
1113
|
|
|
981
1114
|
_present() {
|
|
982
1115
|
if (!this._backing) return;
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1116
|
+
const wait = this._presentWait();
|
|
1117
|
+
if (this._frame.inFlight || wait > 0) {
|
|
1118
|
+
// the server hasn't confirmed the previous frame yet, or the last blit
|
|
1119
|
+
// was too recent — defer, and leave a wakeup behind
|
|
1120
|
+
this._deferPresent(wait);
|
|
986
1121
|
return;
|
|
987
1122
|
}
|
|
988
1123
|
this._presentNow();
|
|
989
1124
|
this._armFence();
|
|
990
1125
|
}
|
|
991
1126
|
|
|
1127
|
+
/**
|
|
1128
|
+
* How long until the next blit may go out; 0 when it may go now.
|
|
1129
|
+
*
|
|
1130
|
+
* The gate is `frameInterval`, the same knob that paces frames, so
|
|
1131
|
+
* `frameInterval: 0` keeps the old fence-only behaviour — a caller who asked
|
|
1132
|
+
* for no timer gate gets none here either.
|
|
1133
|
+
*/
|
|
1134
|
+
_presentWait() {
|
|
1135
|
+
const min = this.frameInterval;
|
|
1136
|
+
if (!(min > 0)) return 0;
|
|
1137
|
+
const since = performance.now() - this._frame.lastPresentAt;
|
|
1138
|
+
return since >= min ? 0 : min - since;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Hold a blit back, and guarantee it still happens.
|
|
1143
|
+
*
|
|
1144
|
+
* This is the only place `_presentPending` is set, because every deferral has
|
|
1145
|
+
* to leave a wakeup behind: the fence reply when the fence is what we are
|
|
1146
|
+
* waiting on, this timer when the interval is. Nothing else reschedules a
|
|
1147
|
+
* present — `_scheduleFrame()` bails while a frame timer is armed, and
|
|
1148
|
+
* neither reschedule condition (in `_armFence`'s reply or `_armTimer`'s
|
|
1149
|
+
* callback) looks at the present. Without the timer the last blit of a burst
|
|
1150
|
+
* would simply never go out, leaving stale pixels on screen.
|
|
1151
|
+
*/
|
|
1152
|
+
_deferPresent(wait) {
|
|
1153
|
+
this._presentPending = true;
|
|
1154
|
+
if (wait <= 0) return; // waiting on the fence: its reply blits this
|
|
1155
|
+
const f = this._frame;
|
|
1156
|
+
if (f.presentTimer) return; // already armed
|
|
1157
|
+
f.presentTimer = setTimeout(() => {
|
|
1158
|
+
f.presentTimer = null;
|
|
1159
|
+
if (!this._presentPending && !this._dirty) return; // someone blitted it
|
|
1160
|
+
const again = this._presentWait();
|
|
1161
|
+
if (again > 0) return this._deferPresent(again); // a paced frame re-stamped
|
|
1162
|
+
if (f.inFlight) return; // the fence ack is closer; it will blit
|
|
1163
|
+
this._presentNow();
|
|
1164
|
+
this._armFence();
|
|
1165
|
+
}, wait);
|
|
1166
|
+
if (typeof f.presentTimer.unref === 'function') f.presentTimer.unref();
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Blit with the Present extension instead of `CopyArea`.
|
|
1171
|
+
*
|
|
1172
|
+
* A frame's dirty rectangles become one `PresentPixmap` with an update
|
|
1173
|
+
* region, in place of one `CopyArea` per rectangle. Two things follow:
|
|
1174
|
+
* a frame is a fixed two requests however fragmented the damage is, and
|
|
1175
|
+
* `_blitList`'s bounding-box collapse becomes unnecessary — its whole
|
|
1176
|
+
* premise is "each rectangle costs a request", so with Present the exact
|
|
1177
|
+
* rectangles are sent and pixels outside them are never touched.
|
|
1178
|
+
*
|
|
1179
|
+
* Presents are also scheduled by the server against the display's refresh
|
|
1180
|
+
* rather than executed on arrival, so a burst cannot produce more updates
|
|
1181
|
+
* than the output can show; the server drops superseded frames itself.
|
|
1182
|
+
*
|
|
1183
|
+
* Worth being precise about what this does *not* do: under a compositing
|
|
1184
|
+
* manager the window is redirected, so this schedules the server's copy
|
|
1185
|
+
* into the redirect pixmap — the compositor still composites on its own
|
|
1186
|
+
* schedule. Aligning with *that* is what `_NET_WM_SYNC_REQUEST`'s extended
|
|
1187
|
+
* counters are for.
|
|
1188
|
+
*
|
|
1189
|
+
* Opt-in via `createWindow({ present: true })`, and inert unless both
|
|
1190
|
+
* Present and XFixes are available — blits fall back to `CopyArea`, which
|
|
1191
|
+
* stays correct at all times, so the two paths can even alternate.
|
|
1192
|
+
*
|
|
1193
|
+
* @returns {Promise<Window>}
|
|
1194
|
+
*/
|
|
1195
|
+
enablePresent() {
|
|
1196
|
+
if (this._presentExt || this._destroyed) return Promise.resolve(this);
|
|
1197
|
+
const X = this.X;
|
|
1198
|
+
const need = (name) =>
|
|
1199
|
+
new Promise((resolve) => X.require(name, (err, ext) => resolve(err ? null : ext)));
|
|
1200
|
+
return Promise.all([need('present'), need('fixes')]).then(([present, fixes]) => {
|
|
1201
|
+
if (!present || !fixes || this._destroyed) return this; // stay on CopyArea
|
|
1202
|
+
this._updateRegion = X.AllocID();
|
|
1203
|
+
safeRelease(X, () => fixes.CreateRegion(this._updateRegion, []));
|
|
1204
|
+
this._presentExt = present;
|
|
1205
|
+
this._fixesExt = fixes;
|
|
1206
|
+
return this;
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* The Present form of the blit. Returns false when it did not happen, so
|
|
1212
|
+
* the caller falls back to CopyArea — which is also what runs before the
|
|
1213
|
+
* extensions have answered.
|
|
1214
|
+
*/
|
|
1215
|
+
_presentWithExtension(rects) {
|
|
1216
|
+
const P = this._presentExt;
|
|
1217
|
+
if (!P || !this._fixesExt || !this._updateRegion) return false;
|
|
1218
|
+
safeRelease(this.X, () => {
|
|
1219
|
+
this._fixesExt.SetRegion(
|
|
1220
|
+
this._updateRegion,
|
|
1221
|
+
rects.map((r) => ({ x: r.x, y: r.y, width: r.w, height: r.h }))
|
|
1222
|
+
);
|
|
1223
|
+
this._presentExt.Pixmap(this.id, this._backing.id, {
|
|
1224
|
+
serial: ++this._presentSerial,
|
|
1225
|
+
update: this._updateRegion,
|
|
1226
|
+
// Option.Copy forces the copy path. Without it the server may *flip*
|
|
1227
|
+
// the pixmap to the screen and take ownership of it until an
|
|
1228
|
+
// IdleNotify — and ntk reuses one grow-only backing pixmap, so
|
|
1229
|
+
// drawing into it while the server owned it would paint the screen
|
|
1230
|
+
// directly. Removing this needs a swap chain, not just a smaller diff.
|
|
1231
|
+
options: P.Option.Copy
|
|
1232
|
+
// targetMsc stays 0: "the next vblank", which is what we want. An
|
|
1233
|
+
// explicit target only adds ways to fall behind.
|
|
1234
|
+
});
|
|
1235
|
+
});
|
|
1236
|
+
return true;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
992
1239
|
_presentNow() {
|
|
993
1240
|
this._presentPending = false;
|
|
994
1241
|
if (!this._backing) return;
|
|
@@ -1010,13 +1257,27 @@ export default class Window extends Drawable {
|
|
|
1010
1257
|
if (x1 > x0 && y1 > y0) clamped.push({ x: x0, y: y0, w: x1 - x0, h: y1 - y0 });
|
|
1011
1258
|
}
|
|
1012
1259
|
if (!clamped.length) return;
|
|
1013
|
-
|
|
1014
|
-
//
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1260
|
+
// Stamped here rather than at the call sites so it is a true inter-blit
|
|
1261
|
+
// interval across all three of them, and only when pixels really move — a
|
|
1262
|
+
// present that copies nothing must not push the next one out.
|
|
1263
|
+
this._frame.lastPresentAt = performance.now();
|
|
1264
|
+
// Present sends the exact rectangles in one request, so the bounding-box
|
|
1265
|
+
// collapse — which trades pixels for fewer requests — has nothing to buy
|
|
1266
|
+
if (!this._presentWithExtension(clamped)) {
|
|
1267
|
+
const rects = this._blitList(clamped);
|
|
1268
|
+
// a paced frame can fire after the connection started closing
|
|
1269
|
+
safeRelease(this.X, () => {
|
|
1270
|
+
for (const r of rects) {
|
|
1271
|
+
this.X.CopyArea(this._backing.id, this.id, this._presentGc, r.x, r.y, r.x, r.y, r.w, r.h);
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
// Queued behind whichever of the two put the pixels on their way, so the
|
|
1276
|
+
// window manager hears about the new size only once the server has drawn
|
|
1277
|
+
// it. Both paths must reach this: a window using Present *and*
|
|
1278
|
+
// _NET_WM_SYNC_REQUEST would otherwise never answer, and the resize would
|
|
1279
|
+
// stall.
|
|
1280
|
+
this._ackSyncRequest();
|
|
1020
1281
|
}
|
|
1021
1282
|
|
|
1022
1283
|
/**
|
|
@@ -1453,6 +1714,10 @@ export default class Window extends Drawable {
|
|
|
1453
1714
|
*
|
|
1454
1715
|
* `WM_DELETE_WINDOW` needs none of this by hand: listening for the
|
|
1455
1716
|
* `'close'` event adds the protocol and decodes the message for you.
|
|
1717
|
+
* `_NET_WM_SYNC_REQUEST` likewise has a real opt-in — `enableSyncRequest()`
|
|
1718
|
+
* / `createWindow({ syncRequest: true })`. Adding that atom here instead
|
|
1719
|
+
* advertises a protocol this window cannot answer, which is worse than not
|
|
1720
|
+
* advertising it: the counter the window manager looks for is missing.
|
|
1456
1721
|
*
|
|
1457
1722
|
* Replaces the whole list. `addProtocol`/`removeProtocol` are the
|
|
1458
1723
|
* accumulating forms, and the ones to reach for: the property is a *set*,
|
|
@@ -2486,6 +2751,116 @@ export default class Window extends Drawable {
|
|
|
2486
2751
|
* existed — keeps behaving exactly as it did, rather than suddenly
|
|
2487
2752
|
* acquiring a second handler that destroys it.
|
|
2488
2753
|
*/
|
|
2754
|
+
/**
|
|
2755
|
+
* Opt into `_NET_WM_SYNC_REQUEST` (EWMH §6.2): let the window manager pace
|
|
2756
|
+
* an interactive resize to how fast this window actually repaints.
|
|
2757
|
+
*
|
|
2758
|
+
* Without it a WM has no idea when a resize has been drawn, so it either
|
|
2759
|
+
* throws ConfigureNotify at the client as fast as the pointer moves — and
|
|
2760
|
+
* the window lags behind the frame the user is dragging — or guesses with a
|
|
2761
|
+
* timer. With it, the WM sends a serial before each resize and waits for
|
|
2762
|
+
* this window to echo it back once the new size is on screen.
|
|
2763
|
+
*
|
|
2764
|
+
* Also available as `createWindow({ syncRequest: true })`. This is the
|
|
2765
|
+
* awaitable form, for when `map()` follows immediately: the counter and the
|
|
2766
|
+
* `_NET_WM_SYNC_REQUEST_COUNTER` property have to exist before the window
|
|
2767
|
+
* leaves the withdrawn state, because that is when the WM reads them.
|
|
2768
|
+
*
|
|
2769
|
+
* Silently does nothing when the server has no SYNC extension — a WM that
|
|
2770
|
+
* finds no counter simply drives the resize the old way.
|
|
2771
|
+
*
|
|
2772
|
+
* Only basic (single-counter) synchronization is implemented. The extended
|
|
2773
|
+
* two-counter form buys frame-timing feedback rather than resize pacing, and
|
|
2774
|
+
* its odd/even parity rules freeze the window if they are ever got wrong; a
|
|
2775
|
+
* compositor that supports it falls back to basic mode on its own when the
|
|
2776
|
+
* property holds one counter.
|
|
2777
|
+
*
|
|
2778
|
+
* @returns {Promise<Window>}
|
|
2779
|
+
*/
|
|
2780
|
+
enableSyncRequest() {
|
|
2781
|
+
if (this._syncCounter || this._destroyed) return Promise.resolve(this);
|
|
2782
|
+
const X = this.X;
|
|
2783
|
+
return new Promise((resolve, reject) => {
|
|
2784
|
+
X.require('sync', (err, Sync) => {
|
|
2785
|
+
if (err || !Sync || this._destroyed) return resolve(this); // no SYNC: stay quiet
|
|
2786
|
+
this._sync = Sync;
|
|
2787
|
+
const counter = X.AllocID();
|
|
2788
|
+
safeRelease(X, () => Sync.CreateCounter(counter, 0));
|
|
2789
|
+
this._syncCounter = counter;
|
|
2790
|
+
// The dispatch path compares against both atoms, and node-x11 caches
|
|
2791
|
+
// interned atoms per connection, so this is one round trip and no more.
|
|
2792
|
+
this.atom('WM_PROTOCOLS').catch(() => {});
|
|
2793
|
+
this._withAtoms(['_NET_WM_SYNC_REQUEST_COUNTER', '_NET_WM_SYNC_REQUEST'], (atoms) => {
|
|
2794
|
+
this._syncRequestAtom = atoms._NET_WM_SYNC_REQUEST;
|
|
2795
|
+
safeRelease(X, () =>
|
|
2796
|
+
X.ChangeProperty(
|
|
2797
|
+
0,
|
|
2798
|
+
this.id,
|
|
2799
|
+
atoms._NET_WM_SYNC_REQUEST_COUNTER,
|
|
2800
|
+
X.atoms.CARDINAL,
|
|
2801
|
+
32,
|
|
2802
|
+
[counter]
|
|
2803
|
+
)
|
|
2804
|
+
);
|
|
2805
|
+
// property first, protocol second: a window manager that sees the
|
|
2806
|
+
// protocol advertised must always find the counter behind it
|
|
2807
|
+
this.addProtocol('_NET_WM_SYNC_REQUEST').then(() => resolve(this), reject);
|
|
2808
|
+
});
|
|
2809
|
+
});
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
/**
|
|
2814
|
+
* The window manager asking us to report when a resize has been painted.
|
|
2815
|
+
*
|
|
2816
|
+
* Only the value is recorded here — acknowledging now would claim a frame
|
|
2817
|
+
* that has not been drawn. `_ackSyncRequest` sends it once the pixels are on
|
|
2818
|
+
* their way. EWMH is explicit that only the *last* message is acknowledged,
|
|
2819
|
+
* so a newer request simply overwrites an older one.
|
|
2820
|
+
*/
|
|
2821
|
+
_handleSyncRequest(ev) {
|
|
2822
|
+
if (!this._syncCounter || ev.format !== 32) return;
|
|
2823
|
+
const X = this.X;
|
|
2824
|
+
if (!X.atoms.WM_PROTOCOLS || ev.message_type !== X.atoms.WM_PROTOCOLS) return;
|
|
2825
|
+
if (!this._syncRequestAtom || ev.data?.[0] !== this._syncRequestAtom) return;
|
|
2826
|
+
// data[2] is the low half of the 64-bit request number, data[3] the high
|
|
2827
|
+
this._syncPending = ev.data[3] * 0x100000000 + ev.data[2];
|
|
2828
|
+
// Not every request leads to a repaint — a move, or a resize to the size
|
|
2829
|
+
// we already are, leaves nothing dirty and no frame scheduled. The
|
|
2830
|
+
// watchdog turns a missed acknowledgement into a stutter instead of a
|
|
2831
|
+
// window manager that waits forever.
|
|
2832
|
+
this._armSyncWatchdog();
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
_armSyncWatchdog() {
|
|
2836
|
+
if (this._syncWatchdog || this._syncPending == null) return;
|
|
2837
|
+
this._syncWatchdog = setTimeout(() => {
|
|
2838
|
+
this._syncWatchdog = null;
|
|
2839
|
+
this._ackSyncRequest();
|
|
2840
|
+
}, Math.max(this.frameInterval, 16) * 2);
|
|
2841
|
+
if (typeof this._syncWatchdog.unref === 'function') this._syncWatchdog.unref();
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
/**
|
|
2845
|
+
* Answer the window manager's last sync request.
|
|
2846
|
+
*
|
|
2847
|
+
* Called after the requests that repaint have been queued, never before: X
|
|
2848
|
+
* runs a client's requests in order, so a SetCounter queued behind the
|
|
2849
|
+
* copies is executed behind them too, which is exactly the "having handled
|
|
2850
|
+
* all repainting" the spec asks for. (It rides the same output batch, which
|
|
2851
|
+
* is flushed before the event loop polls — no manual flush needed.)
|
|
2852
|
+
*/
|
|
2853
|
+
_ackSyncRequest() {
|
|
2854
|
+
const value = this._syncPending;
|
|
2855
|
+
if (value == null || !this._syncCounter || !this._sync) return;
|
|
2856
|
+
this._syncPending = null;
|
|
2857
|
+
if (this._syncWatchdog) {
|
|
2858
|
+
clearTimeout(this._syncWatchdog);
|
|
2859
|
+
this._syncWatchdog = null;
|
|
2860
|
+
}
|
|
2861
|
+
safeRelease(this.X, () => this._sync.SetCounter(this._syncCounter, value));
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2489
2864
|
_emitCloseRequest(ev) {
|
|
2490
2865
|
if (ev.format !== 32 || !this.listenerCount('close')) return;
|
|
2491
2866
|
// node-x11 caches interned atoms per connection, so these are the ids
|