ntk 8.4.0 → 8.4.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.
- package/lib/app.js +17 -6
- package/lib/gl.js +16 -0
- package/lib/window.js +131 -0
- package/package.json +1 -1
package/lib/app.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { connectionGone } from './cleanup.js';
|
|
2
2
|
import Clipboard from './clipboard.js';
|
|
3
3
|
import { CursorCache } from './cursor.js';
|
|
4
|
-
import { GLError, backendFor, glCapabilities, glError, resolveGLPolicy } from './gl.js';
|
|
4
|
+
import { GLError, backendFor, glCapabilities, glError, nativeRefreshRate, resolveGLPolicy } from './gl.js';
|
|
5
5
|
import { chooseGLXConfig } from './glx.js';
|
|
6
6
|
import Picture from './picture.js';
|
|
7
7
|
import Pixmap from './pixmap.js';
|
|
@@ -254,6 +254,19 @@ export default class App {
|
|
|
254
254
|
_probeRefreshRate() {
|
|
255
255
|
this._refreshProbe = null; // in progress; only ever started once
|
|
256
256
|
const X = this.X;
|
|
257
|
+
// RandR's answer, or the native layer's where RandR has no usable one.
|
|
258
|
+
// An implausible rate is treated as no answer, not clamped: XQuartz
|
|
259
|
+
// synthesizes real timing for its canned mode list but fills the current
|
|
260
|
+
// desktop-sized mode with dot_clock = width * height — exactly 1 Hz — so
|
|
261
|
+
// the number that describes what is actually driving the panel has to
|
|
262
|
+
// come from the OS (x11-dri >= 0.6.0, apple.refreshRate()).
|
|
263
|
+
const plausible = (rate) => rate >= MIN_REFRESH_RATE && rate <= MAX_REFRESH_RATE;
|
|
264
|
+
const settle = (rate) => {
|
|
265
|
+
if (!plausible(rate)) rate = nativeRefreshRate();
|
|
266
|
+
if (!rate || !plausible(rate)) return;
|
|
267
|
+
this._refreshRate = rate;
|
|
268
|
+
this._adoptFrameInterval(1000 / rate);
|
|
269
|
+
};
|
|
257
270
|
// A connection on its way out is one more "no rate to be had". The probe
|
|
258
271
|
// is started lazily by the first window built on this connection, which
|
|
259
272
|
// may be one adopted from an event still arriving as it closes — and
|
|
@@ -261,10 +274,10 @@ export default class App {
|
|
|
261
274
|
// event dispatch has no caller to catch it (issue #321).
|
|
262
275
|
if (connectionGone(X)) return;
|
|
263
276
|
X.require('randr', (err, R) => {
|
|
264
|
-
if (err || !R) return;
|
|
277
|
+
if (err || !R) return settle(0);
|
|
265
278
|
const root = this.display.screen[0].root;
|
|
266
279
|
R.GetScreenResourcesCurrent(root, (resourcesError, resources) => {
|
|
267
|
-
if (resourcesError || !resources?.crtcs?.length) return;
|
|
280
|
+
if (resourcesError || !resources?.crtcs?.length) return settle(0);
|
|
268
281
|
const modes = new Map(resources.modeinfos.map((mode) => [mode.id, mode]));
|
|
269
282
|
let pending = resources.crtcs.length;
|
|
270
283
|
let best = 0;
|
|
@@ -273,9 +286,7 @@ export default class App {
|
|
|
273
286
|
// a crtc with no mode is one that is switched off
|
|
274
287
|
if (!crtcError && info) best = Math.max(best, modeRate(modes.get(info.mode)));
|
|
275
288
|
if (--pending) return;
|
|
276
|
-
|
|
277
|
-
this._refreshRate = best;
|
|
278
|
-
this._adoptFrameInterval(1000 / best);
|
|
289
|
+
settle(best);
|
|
279
290
|
});
|
|
280
291
|
}
|
|
281
292
|
});
|
package/lib/gl.js
CHANGED
|
@@ -171,6 +171,22 @@ export function setDriAddon(module) {
|
|
|
171
171
|
addon = module;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* The display's refresh rate asked of the native layer, in Hz, for servers
|
|
176
|
+
* whose RandR carries no usable timing — XQuartz reports the current
|
|
177
|
+
* desktop-sized mode with dot_clock = width * height, exactly 1 Hz.
|
|
178
|
+
* `null` everywhere there is no answer: not darwin, no addon (or one
|
|
179
|
+
* predating 0.6.0), or a session with no display to ask.
|
|
180
|
+
*/
|
|
181
|
+
export function nativeRefreshRate() {
|
|
182
|
+
if (globalThis.process?.platform !== 'darwin') return null;
|
|
183
|
+
try {
|
|
184
|
+
return loadDriAddon()?.apple?.refreshRate?.() ?? null;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
174
190
|
const INSTALL_HINT = `Direct rendering needs the optional native addon:
|
|
175
191
|
|
|
176
192
|
npm install x11-dri
|
package/lib/window.js
CHANGED
|
@@ -109,6 +109,16 @@ const REFRESH_QUANTILE = 0.25;
|
|
|
109
109
|
const STALL_TIMEOUT = 2000;
|
|
110
110
|
const STALL_TIMEOUT_FIRST = 250;
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* MotionNotify detail saying "this is the only motion you get until you ask".
|
|
114
|
+
*
|
|
115
|
+
* The core protocol calls it NotifyHint and puts it in the event's detail
|
|
116
|
+
* byte; node-x11 parses a MotionNotify's detail into `keycode`, the field
|
|
117
|
+
* that carries a button number on ButtonPress — so this is compared against
|
|
118
|
+
* `ev.keycode`, not `ev.detail`, which motion events do not have.
|
|
119
|
+
*/
|
|
120
|
+
const MOTION_NOTIFY_HINT = 1;
|
|
121
|
+
|
|
112
122
|
function rectArea(r) {
|
|
113
123
|
return Math.max(0, r.w) * Math.max(0, r.h);
|
|
114
124
|
}
|
|
@@ -493,6 +503,15 @@ export default class Window extends Drawable {
|
|
|
493
503
|
this._geSinkOpcode = 0;
|
|
494
504
|
// XI2 state, once a window has selected it (see selectXI2)
|
|
495
505
|
this._xi2 = null;
|
|
506
|
+
// PointerMotionHint bookkeeping (see setMouseHintOnly and
|
|
507
|
+
// _rearmMotionHint): whether a frame owes the server the QueryPointer
|
|
508
|
+
// that re-arms the hint, whether one is already in flight, the timestamp
|
|
509
|
+
// of the hint that asked for it, and the last position delivered — a
|
|
510
|
+
// reply that repeats it is not an event.
|
|
511
|
+
this._hintRearm = false;
|
|
512
|
+
this._hintPollPending = false;
|
|
513
|
+
this._hintTime = 0;
|
|
514
|
+
this._hintLast = null;
|
|
496
515
|
// The vblank clock (see _onPresentComplete): the period learnt from
|
|
497
516
|
// completion events, the samples it is drawn from, and the latch the
|
|
498
517
|
// watchdog sets when completions stop arriving.
|
|
@@ -762,6 +781,21 @@ export default class Window extends Drawable {
|
|
|
762
781
|
if (key.codepoint !== undefined) ev.codepoint = key.codepoint;
|
|
763
782
|
}
|
|
764
783
|
}
|
|
784
|
+
// the server saying "the pointer moved, and I will say no more about it
|
|
785
|
+
// until you ask": the event still carries the position it was generated
|
|
786
|
+
// at, so it is delivered like any other move, but the conversation has
|
|
787
|
+
// to be picked back up or this is the last one (see _rearmMotionHint)
|
|
788
|
+
if (eventName === 'mousemove' && ev.keycode === MOTION_NOTIFY_HINT) {
|
|
789
|
+
this._hintTime = ev.time;
|
|
790
|
+
this._hintLast = { x: ev.x, y: ev.y };
|
|
791
|
+
this._hintRearm = true;
|
|
792
|
+
this._deliverEvent(eventName, ntkev);
|
|
793
|
+
// the frame that just took the move sends the poll — one per frame,
|
|
794
|
+
// which is the rate a frame's worth of motion is reduced to anyway.
|
|
795
|
+
// An uncoalesced window has no frame to hang it on, so it goes now.
|
|
796
|
+
if (!this._coalesce) this._rearmMotionHint();
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
765
799
|
// a wheel is a button in the core protocol; say so in the units a
|
|
766
800
|
// consumer wants. Suppressed once XI2 is delivering the same scroll as
|
|
767
801
|
// valuators, which is the whole reason to select it (see selectXI2).
|
|
@@ -1393,6 +1427,9 @@ export default class Window extends Drawable {
|
|
|
1393
1427
|
)
|
|
1394
1428
|
return;
|
|
1395
1429
|
this._flushCoalesced();
|
|
1430
|
+
// after the flush: the frame's motion has been delivered, so what the
|
|
1431
|
+
// server is asked for now is what happened since
|
|
1432
|
+
if (this._hintRearm) this._rearmMotionHint();
|
|
1396
1433
|
if (f.needsRedraw) {
|
|
1397
1434
|
f.needsRedraw = false;
|
|
1398
1435
|
const ev = {
|
|
@@ -1987,6 +2024,9 @@ export default class Window extends Drawable {
|
|
|
1987
2024
|
f.rafCbs = [];
|
|
1988
2025
|
f.needsRedraw = false;
|
|
1989
2026
|
this._presentPending = false;
|
|
2027
|
+
// a window with no frames left has nothing to send the poll from
|
|
2028
|
+
this._hintRearm = false;
|
|
2029
|
+
this._hintLast = null;
|
|
1990
2030
|
}
|
|
1991
2031
|
|
|
1992
2032
|
/**
|
|
@@ -3252,11 +3292,45 @@ export default class Window extends Drawable {
|
|
|
3252
3292
|
return this;
|
|
3253
3293
|
}
|
|
3254
3294
|
|
|
3295
|
+
/**
|
|
3296
|
+
* Trade motion events for round trips: ask the server to report the pointer
|
|
3297
|
+
* moving *once*, and then wait to be asked where it went.
|
|
3298
|
+
*
|
|
3299
|
+
* PointerMotionHint is a two-party protocol. With it selected the server
|
|
3300
|
+
* may send a single MotionNotify carrying detail NotifyHint and then say
|
|
3301
|
+
* nothing more about the pointer until the client asks — so a window that
|
|
3302
|
+
* only sets the bit hears about one move and then silence (issue #319).
|
|
3303
|
+
* ntk holds up the other end: a hinted move is delivered with the position
|
|
3304
|
+
* it carries, and the frame it lands in sends the QueryPointer that lets
|
|
3305
|
+
* the server speak again (see _rearmMotionHint).
|
|
3306
|
+
*
|
|
3307
|
+
* What that buys, and what it costs, are the same thing. Motion stops being
|
|
3308
|
+
* paced by the input device and starts being paced by the connection: one
|
|
3309
|
+
* event per round trip instead of one per hardware sample, which is a large
|
|
3310
|
+
* saving on a link where bytes are scarce — X over ssh, a tunnel, a slow
|
|
3311
|
+
* network — and no saving at all on a local socket, where the poll costs
|
|
3312
|
+
* more bytes than the events it replaced. It also means a handler acts on a
|
|
3313
|
+
* position that is up to one round trip old. Reach for it when bandwidth is
|
|
3314
|
+
* the problem; leave it alone when latency is.
|
|
3315
|
+
*
|
|
3316
|
+
* Two things it is not:
|
|
3317
|
+
*
|
|
3318
|
+
* - not a substitute for selecting motion. The hint modifies
|
|
3319
|
+
* PointerMotion, it does not imply it — a window with no `mousemove`
|
|
3320
|
+
* listener and no PointerMotion in its mask receives nothing either way.
|
|
3321
|
+
* - not an XI2 control. A window that called `selectXI2(['Motion'])` is
|
|
3322
|
+
* off the core motion stream entirely (see lib/xi2.js), and the core
|
|
3323
|
+
* hint has nothing left to thin.
|
|
3324
|
+
*/
|
|
3255
3325
|
setMouseHintOnly(isOn) {
|
|
3256
3326
|
if (isOn && !(this.eventMask & x11.eventMask.PointerMotionHint)) {
|
|
3257
3327
|
this.eventMask |= x11.eventMask.PointerMotionHint;
|
|
3258
3328
|
} else if (!isOn && this.eventMask & x11.eventMask.PointerMotionHint) {
|
|
3259
3329
|
this.eventMask &= ~x11.eventMask.PointerMotionHint;
|
|
3330
|
+
// the server will report motion on its own again, so no frame owes it a
|
|
3331
|
+
// question. A poll already in flight is left alone: its answer is a
|
|
3332
|
+
// real position, and it is deduped against the last one like any other.
|
|
3333
|
+
this._hintRearm = false;
|
|
3260
3334
|
} else {
|
|
3261
3335
|
return this;
|
|
3262
3336
|
}
|
|
@@ -3265,6 +3339,63 @@ export default class Window extends Drawable {
|
|
|
3265
3339
|
return this;
|
|
3266
3340
|
}
|
|
3267
3341
|
|
|
3342
|
+
/**
|
|
3343
|
+
* Answer a NotifyHint: ask where the pointer is, which is what re-arms the
|
|
3344
|
+
* server, and deliver the answer if it moved somewhere no event reported.
|
|
3345
|
+
*
|
|
3346
|
+
* The reply is not redundant with the hint that asked for it. Between the
|
|
3347
|
+
* hint being generated and the server processing this request the pointer
|
|
3348
|
+
* may have moved again, and that motion produces no event — it is exactly
|
|
3349
|
+
* what the hint suppressed. Dropped, the last position of a gesture that
|
|
3350
|
+
* ends mid-flight is lost and the window's idea of where the pointer is
|
|
3351
|
+
* stays wrong until it moves again. So the poll's position is delivered as
|
|
3352
|
+
* a motion event when it differs from the last one delivered, and nothing
|
|
3353
|
+
* is emitted when the pointer sat still.
|
|
3354
|
+
*
|
|
3355
|
+
* One poll at a time, and one per frame: a burst of motion cannot queue a
|
|
3356
|
+
* round trip each, and the flag stays raised if a poll is already out, so
|
|
3357
|
+
* the next frame picks it up.
|
|
3358
|
+
*/
|
|
3359
|
+
_rearmMotionHint() {
|
|
3360
|
+
if (this._hintPollPending) return;
|
|
3361
|
+
this._hintRearm = false;
|
|
3362
|
+
if (this._destroyed || connectionGone(this.X)) return;
|
|
3363
|
+
this._hintPollPending = true;
|
|
3364
|
+
const time = this._hintTime;
|
|
3365
|
+
this.X.QueryPointer(this.id, (err, pointer) => {
|
|
3366
|
+
this._hintPollPending = false;
|
|
3367
|
+
if (err || this._destroyed || connectionGone(this.X)) return;
|
|
3368
|
+
// the pointer is on another screen: `child` and the window-relative
|
|
3369
|
+
// coordinates are zero by protocol, not a position (core QueryPointer)
|
|
3370
|
+
if (!pointer.sameScreen) return;
|
|
3371
|
+
const last = this._hintLast;
|
|
3372
|
+
if (last && last.x === pointer.childX && last.y === pointer.childY) return;
|
|
3373
|
+
this._hintLast = { x: pointer.childX, y: pointer.childY };
|
|
3374
|
+
this._deliverEvent('mousemove', {
|
|
3375
|
+
type: 6,
|
|
3376
|
+
name: 'MotionNotify',
|
|
3377
|
+
// the poll has no timestamp of its own — QueryPointer's reply carries
|
|
3378
|
+
// no time — so the event is stamped with the hint that prompted it
|
|
3379
|
+
time,
|
|
3380
|
+
keycode: 0,
|
|
3381
|
+
root: pointer.root,
|
|
3382
|
+
wid: this.id,
|
|
3383
|
+
child: pointer.child,
|
|
3384
|
+
rootx: pointer.rootX,
|
|
3385
|
+
rooty: pointer.rootY,
|
|
3386
|
+
x: pointer.childX,
|
|
3387
|
+
y: pointer.childY,
|
|
3388
|
+
buttons: pointer.keyMask,
|
|
3389
|
+
sameScreen: pointer.sameScreen,
|
|
3390
|
+
// built from a reply rather than read off the wire, the way a paced
|
|
3391
|
+
// window's redraw events are
|
|
3392
|
+
synthetic: true,
|
|
3393
|
+
window: this,
|
|
3394
|
+
target: this
|
|
3395
|
+
});
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3268
3399
|
queryPointer(callback) {
|
|
3269
3400
|
this.X.QueryPointer(this.id, callback);
|
|
3270
3401
|
return this;
|