ntk 8.3.0 → 8.4.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 +10 -3
- package/lib/app.js +14 -5
- package/lib/appledri.js +177 -0
- package/lib/cleanup.js +23 -7
- package/lib/gl.js +150 -29
- package/lib/index.js +5 -3
- package/lib/renderingcontext_cgl.js +439 -0
- package/lib/renderingcontext_gles.js +7 -0
- package/lib/window.js +103 -25
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -122,9 +122,16 @@ Server-side resources support `using` / `await using` (Node 24+):
|
|
|
122
122
|
|
|
123
123
|
## 3d graphics
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
Two backends, chosen by `glPolicy`
|
|
126
|
+
([docs/context-gles.md](docs/context-gles.md)):
|
|
127
|
+
|
|
128
|
+
- **direct** (opt-in) — shader GL on the real GPU with no pixels on the
|
|
129
|
+
socket: OpenGL ES 2 over DRI3 + Present on Linux, CGL over the Apple-DRI
|
|
130
|
+
extension on macOS/XQuartz. Needs the optional `x11-dri` addon.
|
|
131
|
+
- **indirect GLX** (default) — most of the OpenGL 1.4 api, serialized into
|
|
132
|
+
the X connection. Note that on many systems indirect GLX is disabled by
|
|
133
|
+
default —
|
|
134
|
+
[you'll need to enable it for gl to work](https://github.com/sidorares/node-x11/issues/117#issuecomment-214762185).
|
|
128
135
|
|
|
129
136
|
```js
|
|
130
137
|
import { createClient } from 'ntk';
|
package/lib/app.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { connectionGone } from './cleanup.js';
|
|
1
2
|
import Clipboard from './clipboard.js';
|
|
2
3
|
import { CursorCache } from './cursor.js';
|
|
3
4
|
import { GLError, backendFor, glCapabilities, glError, resolveGLPolicy } from './gl.js';
|
|
@@ -253,6 +254,12 @@ export default class App {
|
|
|
253
254
|
_probeRefreshRate() {
|
|
254
255
|
this._refreshProbe = null; // in progress; only ever started once
|
|
255
256
|
const X = this.X;
|
|
257
|
+
// A connection on its way out is one more "no rate to be had". The probe
|
|
258
|
+
// is started lazily by the first window built on this connection, which
|
|
259
|
+
// may be one adopted from an event still arriving as it closes — and
|
|
260
|
+
// node-x11 throws synchronously at a request from then on, where the
|
|
261
|
+
// event dispatch has no caller to catch it (issue #321).
|
|
262
|
+
if (connectionGone(X)) return;
|
|
256
263
|
X.require('randr', (err, R) => {
|
|
257
264
|
if (err || !R) return;
|
|
258
265
|
const root = this.display.screen[0].root;
|
|
@@ -596,11 +603,11 @@ export default class App {
|
|
|
596
603
|
* `createWindow` and the whole object to `wnd.getContext('opengl', config)`.
|
|
597
604
|
*
|
|
598
605
|
* The spec is GLX's attribute vocabulary either way, because a caller
|
|
599
|
-
* should not have to write the request twice: `DEPTH_SIZE` becomes the
|
|
600
|
-
* depth-buffer size
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
606
|
+
* should not have to write the request twice: `DEPTH_SIZE` becomes the
|
|
607
|
+
* depth-buffer size of the direct backend's context (EGL on Linux, CGL on
|
|
608
|
+
* macOS), and `ALPHA_SIZE` picks an ARGB visual there rather than an
|
|
609
|
+
* fbconfig with an alpha channel. Direct needs no round trip to answer —
|
|
610
|
+
* there are no fbconfigs in it, only a window the GPU draws for.
|
|
604
611
|
*
|
|
605
612
|
* @param {object} [spec] GLX attributes, e.g. `{ DEPTH_SIZE: 24 }`
|
|
606
613
|
*/
|
|
@@ -622,6 +629,8 @@ export default class App {
|
|
|
622
629
|
}
|
|
623
630
|
return {
|
|
624
631
|
backend: 'direct',
|
|
632
|
+
// which direct pipeline this connection runs: 'dri3' or 'appledri'
|
|
633
|
+
flavor: caps.flavor,
|
|
625
634
|
// depth 24 on the root visual needs no colormap of its own, which is
|
|
626
635
|
// why an opaque GL window here is an ordinary window
|
|
627
636
|
visual: argb?.visual ?? screen.root_visual,
|
package/lib/appledri.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Apple-DRI: XQuartz's direct-rendering extension — the macOS counterpart of
|
|
2
|
+
// DRI3. Where DRI3 passes dma-buf descriptors from client to server,
|
|
3
|
+
// Apple-DRI runs the other way: the *server* owns a WindowServer surface for
|
|
4
|
+
// the drawable and exports it to the client's WindowServer connection,
|
|
5
|
+
// identified by (client_id, key[2]). What the client does with the key —
|
|
6
|
+
// import it and attach an OpenGL context through the Xplugin/CGL system
|
|
7
|
+
// libraries — is native-code territory: the x11-dri addon's `apple`
|
|
8
|
+
// namespace (lib/renderingcontext_cgl.js is the consumer).
|
|
9
|
+
//
|
|
10
|
+
// The protocol half lives here rather than in node-x11 because node-x11 does
|
|
11
|
+
// not ship it yet; the binding is written in node-x11's lib/ext style so it
|
|
12
|
+
// can move there verbatim when it does (docs/context-gles.md#macos).
|
|
13
|
+
//
|
|
14
|
+
// Protocol source (there is no shipped header — the extension is defined in
|
|
15
|
+
// the XQuartz server tree):
|
|
16
|
+
// https://github.com/XQuartz/xorg-server/blob/master/hw/xquartz/xpr/appledristr.h
|
|
17
|
+
// https://github.com/XQuartz/xorg-server/blob/master/hw/xquartz/xpr/appledri.h
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
#define X_AppleDRIQueryVersion 0
|
|
21
|
+
#define X_AppleDRIQueryDirectRenderingCapable 1
|
|
22
|
+
#define X_AppleDRICreateSurface 2
|
|
23
|
+
#define X_AppleDRIDestroySurface 3
|
|
24
|
+
(4..8: AuthConnection and the shm pixmap requests — the accelerated path
|
|
25
|
+
needs none of them, so they are not bound)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Sub-codes of the AppleDRISurfaceNotify event's `kind`. */
|
|
29
|
+
export const NotifyKind = {
|
|
30
|
+
/** the window moved or resized under the surface: `ctx.update()` */
|
|
31
|
+
Changed: 0,
|
|
32
|
+
/** the surface is gone (window unmapped, frame recreated): CreateSurface + attach anew */
|
|
33
|
+
Destroyed: 1
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Bind the Apple-DRI extension on a node-x11 display.
|
|
38
|
+
*
|
|
39
|
+
* Resolves the extension object — with the requests, `events`, `NotifyKind`
|
|
40
|
+
* and `errors` below on it — or `null` where the server has no Apple-DRI,
|
|
41
|
+
* which is every server that is not XQuartz. Asked once per connection and
|
|
42
|
+
* cached; the event and error parsers are registered on first resolution.
|
|
43
|
+
*
|
|
44
|
+
* The requests are callback-style, like every node-x11 extension:
|
|
45
|
+
*
|
|
46
|
+
* - `QueryVersion(cb)` -> `{ major, minor, patch }`
|
|
47
|
+
* - `QueryDirectRenderingCapable(screen, cb)` -> `boolean`
|
|
48
|
+
* - `CreateSurface(screen, drawable, clientId, cb)` -> `{ key: [k0, k1], uid }`
|
|
49
|
+
* - `DestroySurface(screen, drawable)` (void)
|
|
50
|
+
*
|
|
51
|
+
* `CreateSurface` makes the server create (or reference) a WindowServer
|
|
52
|
+
* surface for the drawable and export it to the process whose WindowServer
|
|
53
|
+
* id is `clientId` (x11-dri: `dri.apple.clientId()`). `key` is what
|
|
54
|
+
* `AppleContext.attach()` consumes; `uid` is the server-side surface id that
|
|
55
|
+
* SurfaceNotify events carry as `arg` — route by uid, not by window id.
|
|
56
|
+
*
|
|
57
|
+
* The `AppleDRISurfaceNotify` event is *classic* (type `firstEvent + 3`),
|
|
58
|
+
* not a GenericEvent, and is sent unsolicited to the client that created the
|
|
59
|
+
* surface — there is no event mask to select. It reaches node-x11's
|
|
60
|
+
* client-level `'event'` stream only (no `wid` field for per-window
|
|
61
|
+
* routing), parsed as `{ name: 'AppleDRISurfaceNotify', kind, time, arg }`.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} display node-x11 display
|
|
64
|
+
* @returns {Promise<object|null>}
|
|
65
|
+
*/
|
|
66
|
+
export function requireAppleDRI(display) {
|
|
67
|
+
const X = display.client;
|
|
68
|
+
if (X._appleDRIPromise) return X._appleDRIPromise;
|
|
69
|
+
X._appleDRIPromise = new Promise((resolve) => {
|
|
70
|
+
X.QueryExtension('Apple-DRI', (err, ext) => {
|
|
71
|
+
if (err || !ext.present) return resolve(null);
|
|
72
|
+
|
|
73
|
+
// -> { major, minor, patch }
|
|
74
|
+
ext.QueryVersion = (cb) => {
|
|
75
|
+
X.seq_num++;
|
|
76
|
+
const b = Buffer.alloc(4);
|
|
77
|
+
b.writeUInt8(ext.majorOpcode, 0);
|
|
78
|
+
b.writeUInt8(0, 1);
|
|
79
|
+
b.writeUInt16LE(1, 2);
|
|
80
|
+
X.pack_stream.put(b);
|
|
81
|
+
X.replies[X.seq_num] = [
|
|
82
|
+
(buf) => ({
|
|
83
|
+
major: buf.readUInt16LE(0),
|
|
84
|
+
minor: buf.readUInt16LE(2),
|
|
85
|
+
patch: buf.readUInt32LE(4)
|
|
86
|
+
}),
|
|
87
|
+
cb
|
|
88
|
+
];
|
|
89
|
+
X.pack_stream.submit(true);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// -> boolean
|
|
93
|
+
ext.QueryDirectRenderingCapable = (screen, cb) => {
|
|
94
|
+
X.seq_num++;
|
|
95
|
+
const b = Buffer.alloc(8);
|
|
96
|
+
b.writeUInt8(ext.majorOpcode, 0);
|
|
97
|
+
b.writeUInt8(1, 1);
|
|
98
|
+
b.writeUInt16LE(2, 2);
|
|
99
|
+
b.writeUInt32LE(screen >>> 0, 4);
|
|
100
|
+
X.pack_stream.put(b);
|
|
101
|
+
X.replies[X.seq_num] = [(buf) => buf.readUInt8(0) !== 0, cb];
|
|
102
|
+
X.pack_stream.submit(true);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// -> { key: [key0, key1], uid }
|
|
106
|
+
ext.CreateSurface = (screen, drawable, clientId, cb) => {
|
|
107
|
+
X.seq_num++;
|
|
108
|
+
const b = Buffer.alloc(16);
|
|
109
|
+
b.writeUInt8(ext.majorOpcode, 0);
|
|
110
|
+
b.writeUInt8(2, 1);
|
|
111
|
+
b.writeUInt16LE(4, 2);
|
|
112
|
+
b.writeUInt32LE(screen >>> 0, 4);
|
|
113
|
+
b.writeUInt32LE(drawable >>> 0, 8);
|
|
114
|
+
b.writeUInt32LE(clientId >>> 0, 12);
|
|
115
|
+
X.pack_stream.put(b);
|
|
116
|
+
X.replies[X.seq_num] = [
|
|
117
|
+
(buf) => ({
|
|
118
|
+
key: [buf.readUInt32LE(0), buf.readUInt32LE(4)],
|
|
119
|
+
uid: buf.readUInt32LE(8)
|
|
120
|
+
}),
|
|
121
|
+
cb
|
|
122
|
+
];
|
|
123
|
+
X.pack_stream.submit(true);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
ext.DestroySurface = (screen, drawable) => {
|
|
127
|
+
X.seq_num++;
|
|
128
|
+
const b = Buffer.alloc(12);
|
|
129
|
+
b.writeUInt8(ext.majorOpcode, 0);
|
|
130
|
+
b.writeUInt8(3, 1);
|
|
131
|
+
b.writeUInt16LE(3, 2);
|
|
132
|
+
b.writeUInt32LE(screen >>> 0, 4);
|
|
133
|
+
b.writeUInt32LE(drawable >>> 0, 8);
|
|
134
|
+
X.pack_stream.put(b);
|
|
135
|
+
X.pack_stream.submit(false);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
ext.events = {
|
|
139
|
+
AppleDRISurfaceNotify: 3 // 0..2 are obsolete
|
|
140
|
+
};
|
|
141
|
+
ext.NotifyKind = NotifyKind;
|
|
142
|
+
|
|
143
|
+
X.eventParsers[ext.firstEvent + ext.events.AppleDRISurfaceNotify] = (
|
|
144
|
+
type,
|
|
145
|
+
seq,
|
|
146
|
+
extra,
|
|
147
|
+
code,
|
|
148
|
+
raw
|
|
149
|
+
) => ({
|
|
150
|
+
type,
|
|
151
|
+
seq,
|
|
152
|
+
name: 'AppleDRISurfaceNotify',
|
|
153
|
+
kind: code, // NotifyKind
|
|
154
|
+
time: extra,
|
|
155
|
+
arg: raw.readUInt32LE(4) // the surface uid, NOT a window id
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
ext.errors = {
|
|
159
|
+
ClientNotLocal: 0,
|
|
160
|
+
OperationNotSupported: 1
|
|
161
|
+
};
|
|
162
|
+
// node-x11 names an extension error it cannot decode after a core one;
|
|
163
|
+
// give both of Apple-DRI's a message that says what to do instead
|
|
164
|
+
X.errorParsers[ext.firstError + ext.errors.ClientNotLocal] = (error) => {
|
|
165
|
+
error.message =
|
|
166
|
+
'Apple-DRI: the client is not local — surfaces can only be exported to a process on the same machine as the X server';
|
|
167
|
+
};
|
|
168
|
+
X.errorParsers[ext.firstError + ext.errors.OperationNotSupported] = (error) => {
|
|
169
|
+
error.message =
|
|
170
|
+
'Apple-DRI: operation not supported — the server refused the request for this drawable (a root or already-destroyed window, or a server running without the Xplugin backend)';
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
resolve(ext);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
return X._appleDRIPromise;
|
|
177
|
+
}
|
package/lib/cleanup.js
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Whether a request issued now would throw rather than reach the server.
|
|
3
|
+
* node-x11 throws *synchronously* ("client is in closing state") once
|
|
4
|
+
* `close()` has begun, and a destroyed or ended stream is the same state one
|
|
5
|
+
* step further along.
|
|
6
|
+
*
|
|
7
|
+
* The check matters wherever a request is issued from somewhere with no
|
|
8
|
+
* caller to catch: a FinalizationRegistry callback, a paced frame's timer, or
|
|
9
|
+
* the X event dispatch itself — events already in the read buffer keep being
|
|
10
|
+
* delivered after `close()` (issue #321).
|
|
11
|
+
*/
|
|
12
|
+
export function connectionGone(X) {
|
|
13
|
+
return !!(X._closing || !X.stream || X.stream.destroyed || X.stream.writableEnded);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Issue requests — typically releasing a server-side resource — tolerating a
|
|
17
|
+
// connection that is closing or already gone. The X server frees all of a
|
|
18
|
+
// client's resources on disconnect, so there is nothing left to do and
|
|
19
|
+
// nothing worth throwing about. This matters wherever the requests have no
|
|
20
|
+
// caller around them to catch: FinalizationRegistry callbacks, which run
|
|
21
|
+
// after app.close() if wrappers get garbage collected late, a paced frame's
|
|
22
|
+
// timer, and the X event dispatch (issue #321).
|
|
7
23
|
export function safeRelease(X, fn) {
|
|
8
|
-
if (X
|
|
24
|
+
if (connectionGone(X)) return;
|
|
9
25
|
try {
|
|
10
26
|
fn();
|
|
11
27
|
} catch {
|
package/lib/gl.js
CHANGED
|
@@ -7,11 +7,17 @@
|
|
|
7
7
|
// into the X connection. Reaches any server that allows indirect contexts,
|
|
8
8
|
// including over a network, and is a fixed-function OpenGL 1.x pipeline
|
|
9
9
|
// with no shaders, because that is what the GLX protocol encodes.
|
|
10
|
-
// - **direct**
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
10
|
+
// - **direct** — the GPU draws the frame and no pixels cross the socket;
|
|
11
|
+
// real shaders; local connections only; needs the optional `x11-dri`
|
|
12
|
+
// addon. It comes in two *flavors*, one per platform, behind the same
|
|
13
|
+
// context contract:
|
|
14
|
+
// - `dri3` (Linux, lib/renderingcontext_gles.js): OpenGL ES 2 on a DRM
|
|
15
|
+
// render node, finished buffers handed to the server as dma-buf
|
|
16
|
+
// descriptors over DRI3 + Present.
|
|
17
|
+
// - `appledri` (macOS/XQuartz, lib/renderingcontext_cgl.js): the server
|
|
18
|
+
// exports the window's WindowServer surface over the Apple-DRI
|
|
19
|
+
// extension, and a CGL context draws straight into it — desktop GL
|
|
20
|
+
// with ES2 compatibility, so the same shaders compile.
|
|
15
21
|
//
|
|
16
22
|
// Which one runs is `glPolicy`, and the default is `indirect` — the backend
|
|
17
23
|
// that has always run. See docs/context-gles.md.
|
|
@@ -20,6 +26,7 @@
|
|
|
20
26
|
// caller asked for, and what that resolves to. Neither context imports the
|
|
21
27
|
// other, and the direct one is only ever loaded when the answer is `direct`.
|
|
22
28
|
|
|
29
|
+
import { requireAppleDRI } from './appledri.js';
|
|
23
30
|
import { nodeRequire } from './builtin.js';
|
|
24
31
|
|
|
25
32
|
/**
|
|
@@ -29,18 +36,28 @@ import { nodeRequire } from './builtin.js';
|
|
|
29
36
|
* - `GL_DISABLED` — `glPolicy: 'off'`; nothing tried.
|
|
30
37
|
* - `GL_NO_ADDON` — the optional `x11-dri` addon is not installed or would
|
|
31
38
|
* not load. `npm install x11-dri`.
|
|
32
|
-
* - `GL_NO_DRIVER` — the addon loaded but the
|
|
33
|
-
* missing
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
39
|
+
* - `GL_NO_DRIVER` — the addon loaded but the platform libraries it needs
|
|
40
|
+
* are missing: `libgbm`/`libEGL`/`libGLESv2` on Linux,
|
|
41
|
+
* libXplugin/OpenGL.framework on macOS — or the platform has no direct
|
|
42
|
+
* path at all.
|
|
43
|
+
* - `GL_NO_DEVICE` — no readable DRM render node (`/dev/dri/renderD*`);
|
|
44
|
+
* Linux only, macOS needs no device node.
|
|
45
|
+
* - `GL_REMOTE_DISPLAY` — a TCP or forwarded display; both flavors are
|
|
46
|
+
* local-only by construction.
|
|
37
47
|
* - `GL_NO_FD_PASSING` — the display is local, but this JavaScript runtime
|
|
38
48
|
* cannot send a descriptor over the socket. Bun is the case in the field;
|
|
39
|
-
* x11 does it through a Node internal Bun does not implement.
|
|
40
|
-
*
|
|
49
|
+
* x11 does it through a Node internal Bun does not implement. Linux only —
|
|
50
|
+
* Apple-DRI passes no descriptors.
|
|
51
|
+
* - `GL_NO_DRI3` — the server has no DRI3/Present (Xvfb, Xephyr, XQuartz —
|
|
52
|
+
* though XQuartz has its own path, see `GL_NO_APPLEDRI`).
|
|
53
|
+
* - `GL_NO_APPLEDRI` — macOS, and the server has no Apple-DRI extension:
|
|
54
|
+
* not XQuartz, or an XQuartz running without its Xplugin backend.
|
|
55
|
+
* - `GL_NO_WINDOWSERVER` — macOS, but this process has no WindowServer
|
|
56
|
+
* session to import surfaces into — an SSH session. Run from the
|
|
57
|
+
* logged-in GUI session.
|
|
41
58
|
* - `GL_IMPORT_FAILED` — the server refused the buffer; usually client and
|
|
42
59
|
* server on different DRM devices.
|
|
43
|
-
* - `GL_CONTEXT_FAILED` —
|
|
60
|
+
* - `GL_CONTEXT_FAILED` — GPU context setup failed for some other reason.
|
|
44
61
|
*/
|
|
45
62
|
export const GLError = {
|
|
46
63
|
DISABLED: 'GL_DISABLED',
|
|
@@ -50,6 +67,8 @@ export const GLError = {
|
|
|
50
67
|
REMOTE_DISPLAY: 'GL_REMOTE_DISPLAY',
|
|
51
68
|
NO_FD_PASSING: 'GL_NO_FD_PASSING',
|
|
52
69
|
NO_DRI3: 'GL_NO_DRI3',
|
|
70
|
+
NO_APPLEDRI: 'GL_NO_APPLEDRI',
|
|
71
|
+
NO_WINDOWSERVER: 'GL_NO_WINDOWSERVER',
|
|
53
72
|
IMPORT_FAILED: 'GL_IMPORT_FAILED',
|
|
54
73
|
CONTEXT_FAILED: 'GL_CONTEXT_FAILED'
|
|
55
74
|
};
|
|
@@ -161,11 +180,18 @@ tools are needed; anything else compiles with node-gyp and a C toolchain. ntk
|
|
|
161
180
|
does not depend on it — without it, GL runs through indirect GLX.`;
|
|
162
181
|
|
|
163
182
|
/**
|
|
164
|
-
* What the *client side* can do, before any server is asked: the addon
|
|
165
|
-
*
|
|
183
|
+
* What the *client side* can do, before any server is asked: the addon and
|
|
184
|
+
* the platform libraries under it — plus, on Linux, a render node to draw
|
|
185
|
+
* on. Never throws.
|
|
186
|
+
*
|
|
187
|
+
* `flavor` on an ok answer says which direct pipeline this machine runs:
|
|
188
|
+
* `'dri3'` (Linux — GBM/EGL, dma-buf to the server) or `'appledri'`
|
|
189
|
+
* (macOS — Apple-DRI surface export, CGL). macOS needs no device scan; the
|
|
190
|
+
* window's surface is the render target, so `device` is `null` there.
|
|
166
191
|
*
|
|
167
192
|
* @returns {{ok: boolean, code?: string, message?: string, hint?: string,
|
|
168
|
-
* device?: string, devices?: string[],
|
|
193
|
+
* flavor?: 'dri3'|'appledri', device?: string|null, devices?: string[],
|
|
194
|
+
* probe?: object}}
|
|
169
195
|
*/
|
|
170
196
|
export function probeDirect(policy = DEFAULT_GL_POLICY) {
|
|
171
197
|
const dri = loadDriAddon();
|
|
@@ -185,6 +211,28 @@ export function probeDirect(policy = DEFAULT_GL_POLICY) {
|
|
|
185
211
|
return { ok: false, code: GLError.NO_DRIVER, message: `x11-dri probe() failed: ${err.message}` };
|
|
186
212
|
}
|
|
187
213
|
|
|
214
|
+
if (probe.platform === 'darwin') {
|
|
215
|
+
// The macOS path draws through Apple-DRI + CGL, not GBM/EGL — probe()
|
|
216
|
+
// reports it as `appledri`: `true`, or the string saying why not. An
|
|
217
|
+
// addon from before 0.5.0 has no such key at all.
|
|
218
|
+
if (probe.appledri !== true) {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
code: GLError.NO_DRIVER,
|
|
222
|
+
message: `the libraries direct rendering needs on macOS are unavailable (appledri: ${
|
|
223
|
+
probe.appledri ?? 'not reported — this x11-dri predates Apple-DRI support'
|
|
224
|
+
})`,
|
|
225
|
+
hint:
|
|
226
|
+
probe.appledri === undefined
|
|
227
|
+
? 'Upgrade the addon: npm install x11-dri@latest (Apple-DRI support arrived in 0.5.0).'
|
|
228
|
+
: 'The macOS path needs XQuartz installed (it dlopen()s libXplugin from /opt/X11)\nand the system OpenGL framework — https://www.xquartz.org.',
|
|
229
|
+
probe
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
// no render nodes on macOS: the window's own surface is the target
|
|
233
|
+
return { ok: true, flavor: 'appledri', device: null, devices: [], probe };
|
|
234
|
+
}
|
|
235
|
+
|
|
188
236
|
// probe() reports each capability as `true` or as the string saying why not
|
|
189
237
|
const missing = ['gbm', 'egl', 'gles'].filter((key) => probe[key] !== true);
|
|
190
238
|
if (missing.length) {
|
|
@@ -195,7 +243,7 @@ export function probeDirect(policy = DEFAULT_GL_POLICY) {
|
|
|
195
243
|
message: `the GPU libraries direct rendering needs are unavailable (${detail})`,
|
|
196
244
|
hint:
|
|
197
245
|
probe.platform && probe.platform !== 'linux'
|
|
198
|
-
? `Direct rendering
|
|
246
|
+
? `Direct rendering needs Linux (DRI3/dma-buf) or macOS (Apple-DRI/XQuartz) — ${probe.platform} has neither path.`
|
|
199
247
|
: 'Install Mesa (libgbm1, libegl1, libgles2 on Debian/Ubuntu). They are dlopen()ed at\nrun time, so no rebuild of x11-dri is needed once they are there.',
|
|
200
248
|
probe
|
|
201
249
|
};
|
|
@@ -219,7 +267,7 @@ export function probeDirect(policy = DEFAULT_GL_POLICY) {
|
|
|
219
267
|
devices
|
|
220
268
|
};
|
|
221
269
|
}
|
|
222
|
-
return { ok: true, device, devices, probe };
|
|
270
|
+
return { ok: true, flavor: 'dri3', device, devices, probe };
|
|
223
271
|
}
|
|
224
272
|
|
|
225
273
|
// ---------------------------------------------------------------------------
|
|
@@ -253,13 +301,20 @@ const requireExt = (X, name) =>
|
|
|
253
301
|
/**
|
|
254
302
|
* Everything about `app` that decides the backend, answered once and cached.
|
|
255
303
|
*
|
|
256
|
-
* The
|
|
304
|
+
* The extension queries are the only round trips, and they only happen
|
|
257
305
|
* under a policy that could use direct — `createClient` warms this during the
|
|
258
306
|
* connect handshake for exactly that reason, so `getContext` can decide
|
|
259
307
|
* synchronously afterwards.
|
|
260
308
|
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
309
|
+
* `flavor` names the direct pipeline where `direct` is true: `'dri3'`
|
|
310
|
+
* carries `DRI3` and `Present`, `'appledri'` carries `AppleDRI` (the
|
|
311
|
+
* lib/appledri.js extension object) and `appleClientId` (this process's
|
|
312
|
+
* WindowServer id, which CreateSurface exports surfaces to).
|
|
313
|
+
*
|
|
314
|
+
* @returns {Promise<{direct: boolean, indirect: boolean,
|
|
315
|
+
* flavor: 'dri3'|'appledri'|null, device: string|null, reason: Error|null,
|
|
316
|
+
* DRI3: object|null, Present: object|null, AppleDRI: object|null,
|
|
317
|
+
* appleClientId: number|null}>}
|
|
263
318
|
*/
|
|
264
319
|
export function glCapabilities(app) {
|
|
265
320
|
if (app._glCaps) return app._glCaps;
|
|
@@ -269,10 +324,13 @@ export function glCapabilities(app) {
|
|
|
269
324
|
const fail = (code, message, hint) => ({
|
|
270
325
|
direct: false,
|
|
271
326
|
indirect,
|
|
327
|
+
flavor: null,
|
|
272
328
|
device: null,
|
|
273
329
|
reason: glError(code, message, hint),
|
|
274
330
|
DRI3: null,
|
|
275
|
-
Present: null
|
|
331
|
+
Present: null,
|
|
332
|
+
AppleDRI: null,
|
|
333
|
+
appleClientId: null
|
|
276
334
|
});
|
|
277
335
|
|
|
278
336
|
if (policy.mode === 'off') {
|
|
@@ -281,11 +339,67 @@ export function glCapabilities(app) {
|
|
|
281
339
|
if (!app.display.isLocalSocket) {
|
|
282
340
|
return fail(
|
|
283
341
|
GLError.REMOTE_DISPLAY,
|
|
284
|
-
'this X connection is not a local socket, and
|
|
285
|
-
'
|
|
286
|
-
'
|
|
342
|
+
'this X connection is not a local socket, and direct rendering is same-machine by construction (DRI3 passes a descriptor down the socket; Apple-DRI exports a surface to a local process)',
|
|
343
|
+
'Over a network, indirect GLX is the backend that can work at all — leave\n' +
|
|
344
|
+
'glPolicy at its default.'
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const client = probeDirect(policy);
|
|
349
|
+
if (!client.ok) return fail(client.code, client.message, client.hint);
|
|
350
|
+
|
|
351
|
+
if (client.flavor === 'appledri') {
|
|
352
|
+
// No descriptor ever crosses this socket — the fd-passing checks the
|
|
353
|
+
// dri3 flavor needs below do not apply, which is also what lets this
|
|
354
|
+
// path work under runtimes that cannot send one.
|
|
355
|
+
const dri = loadDriAddon();
|
|
356
|
+
let appleClientId;
|
|
357
|
+
try {
|
|
358
|
+
// the WindowServer handshake happens on first call; a session with
|
|
359
|
+
// no WindowServer (SSH into the machine) is where it throws
|
|
360
|
+
appleClientId = dri.apple.clientId();
|
|
361
|
+
} catch (err) {
|
|
362
|
+
return fail(
|
|
363
|
+
GLError.NO_WINDOWSERVER,
|
|
364
|
+
`this process has no WindowServer session to import surfaces into (${err.message})`,
|
|
365
|
+
'Apple-DRI hands the window surface to the WindowServer connection of this\n' +
|
|
366
|
+
'process, which an SSH session does not have. Run the app from the logged-in\n' +
|
|
367
|
+
'GUI session; over SSH, indirect GLX is the backend that can work.'
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
const AppleDRI = await requireAppleDRI(app.display);
|
|
371
|
+
if (!AppleDRI) {
|
|
372
|
+
return fail(
|
|
373
|
+
GLError.NO_APPLEDRI,
|
|
374
|
+
`${displayName()} does not have the Apple-DRI extension, so it cannot export a window surface to render into`,
|
|
375
|
+
'Apple-DRI is XQuartz\'s direct-rendering extension — is this display an\n' +
|
|
376
|
+
'XQuartz server? Indirect GLX is the backend that can work on any other.'
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
const capable = await new Promise((resolve) =>
|
|
380
|
+
AppleDRI.QueryDirectRenderingCapable(0, (err, answer) => resolve(err ? false : answer))
|
|
287
381
|
);
|
|
382
|
+
if (!capable) {
|
|
383
|
+
return fail(
|
|
384
|
+
GLError.NO_APPLEDRI,
|
|
385
|
+
`${displayName()} has Apple-DRI but reports it not direct-rendering capable`,
|
|
386
|
+
'XQuartz answers this false when its Xplugin backend is not driving a real\n' +
|
|
387
|
+
'display. Indirect GLX is the backend that can work on such a server.'
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
direct: true,
|
|
392
|
+
indirect,
|
|
393
|
+
flavor: 'appledri',
|
|
394
|
+
device: null,
|
|
395
|
+
reason: null,
|
|
396
|
+
DRI3: null,
|
|
397
|
+
Present: null,
|
|
398
|
+
AppleDRI,
|
|
399
|
+
appleClientId
|
|
400
|
+
};
|
|
288
401
|
}
|
|
402
|
+
|
|
289
403
|
if (!canPassDescriptors(app.display)) {
|
|
290
404
|
// The socket is local; what is missing is the ability to send a
|
|
291
405
|
// descriptor along it. x11 does that through Node's internal
|
|
@@ -302,9 +416,6 @@ export function glCapabilities(app) {
|
|
|
302
416
|
);
|
|
303
417
|
}
|
|
304
418
|
|
|
305
|
-
const client = probeDirect(policy);
|
|
306
|
-
if (!client.ok) return fail(client.code, client.message, client.hint);
|
|
307
|
-
|
|
308
419
|
const X = app.X;
|
|
309
420
|
const [DRI3, Present] = await Promise.all([requireExt(X, 'dri3'), requireExt(X, 'present')]);
|
|
310
421
|
if (!DRI3 || !Present) {
|
|
@@ -325,7 +436,17 @@ export function glCapabilities(app) {
|
|
|
325
436
|
);
|
|
326
437
|
}
|
|
327
438
|
|
|
328
|
-
return {
|
|
439
|
+
return {
|
|
440
|
+
direct: true,
|
|
441
|
+
indirect,
|
|
442
|
+
flavor: 'dri3',
|
|
443
|
+
device: client.device,
|
|
444
|
+
reason: null,
|
|
445
|
+
DRI3,
|
|
446
|
+
Present,
|
|
447
|
+
AppleDRI: null,
|
|
448
|
+
appleClientId: null
|
|
449
|
+
};
|
|
329
450
|
})();
|
|
330
451
|
return app._glCaps;
|
|
331
452
|
}
|
package/lib/index.js
CHANGED
|
@@ -55,13 +55,15 @@ import { TextLayout } from './text/layout.js';
|
|
|
55
55
|
import SvgView from './widgets/svgview.js';
|
|
56
56
|
import { cssColor, cssColorStraight, premultiply } from './color.js';
|
|
57
57
|
|
|
58
|
-
// rendering context modules register themselves on Drawable. The direct
|
|
59
|
-
//
|
|
60
|
-
// registered, so that the backend-neutral name can dispatch on glPolicy
|
|
58
|
+
// rendering context modules register themselves on Drawable. The direct ones
|
|
59
|
+
// come last on purpose: each wraps the 'opengl' factory the one before it
|
|
60
|
+
// registered, so that the backend-neutral name can dispatch on glPolicy —
|
|
61
|
+
// indirect underneath, then the dri3 flavor, then the appledri one on top.
|
|
61
62
|
import './renderingcontext_x11.js';
|
|
62
63
|
import { CanvasGradient, CanvasPattern } from './renderingcontext_2d.js';
|
|
63
64
|
import './renderingcontext_opengl.js';
|
|
64
65
|
import './renderingcontext_gles.js';
|
|
66
|
+
import './renderingcontext_cgl.js';
|
|
65
67
|
|
|
66
68
|
// One socket write per frame instead of one per request (x11 >= 3.6). A frame
|
|
67
69
|
// is emitted in one synchronous run of _runFrame() and ends with the frame
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
// Direct rendering context, macOS/XQuartz flavor: OpenGL on the GPU through
|
|
2
|
+
// CGL, drawing straight into the window's WindowServer surface, which the
|
|
3
|
+
// server exports over the Apple-DRI extension (lib/appledri.js).
|
|
4
|
+
//
|
|
5
|
+
// The mirror image of the DRI3 flavor (lib/renderingcontext_gles.js): DRI3
|
|
6
|
+
// is client-allocates-and-pushes — GBM buffers travel to the server as
|
|
7
|
+
// dma-buf descriptors and are shown with Present — where Apple-DRI is
|
|
8
|
+
// server-exports-and-client-attaches:
|
|
9
|
+
//
|
|
10
|
+
// this process X server (XQuartz)
|
|
11
|
+
// ------------ ------------------
|
|
12
|
+
// apple.clientId() --- AppleDRICreateSurface(win, cid) ---> exports the
|
|
13
|
+
// <-------------- key[2] ---------------- window's surface
|
|
14
|
+
// ctx.attach(key) (import surface + bind CGL context)
|
|
15
|
+
// gl draws straight into the window's backing store
|
|
16
|
+
// ctx.flush() (CGLFlushDrawable — WindowServer composites)
|
|
17
|
+
// <--- AppleDRISurfaceNotify ------------ moved/resized/
|
|
18
|
+
// destroyed
|
|
19
|
+
//
|
|
20
|
+
// After attach, no pixels and no per-frame requests cross the X socket at
|
|
21
|
+
// all. The GL the context speaks is the same WebGL-shaped camelCase table
|
|
22
|
+
// the DRI3 flavor installs — the addon serves both from one `gl` object, and
|
|
23
|
+
// on Apple's GL (4.1 core on Metal, ES2-compatible) the same GLSL ES 1.00
|
|
24
|
+
// shaders compile unchanged — so draw code written against `gl.backend ===
|
|
25
|
+
// 'direct'` runs on either. See docs/context-gles.md#macos.
|
|
26
|
+
//
|
|
27
|
+
// Two structural differences from the GLES flavor, both consequences of the
|
|
28
|
+
// server owning the buffer:
|
|
29
|
+
//
|
|
30
|
+
// - **The surface exists only while the physical (Quartz) window does**, so
|
|
31
|
+
// setup is not synchronous: the CGL context is created in the
|
|
32
|
+
// constructor (and `gl.*` works immediately — an unattached context
|
|
33
|
+
// compiles shaders and renders to FBOs), but the attach waits for
|
|
34
|
+
// MapNotify, and `SurfaceNotify(destroyed)` (unmap, frame recreated)
|
|
35
|
+
// means export-and-attach again. `ready`/`canRender()` report it, and
|
|
36
|
+
// the existing `onFrameAvailable` contract absorbs the wait.
|
|
37
|
+
// - **No backpressure from the server**: `flush()` always succeeds
|
|
38
|
+
// immediately, so a loop that draws whenever it can would spin at 100%
|
|
39
|
+
// CPU. Rather than a second pacing model, the swap itself closes the
|
|
40
|
+
// `canRender()` gate for one display period and a timer reopens it —
|
|
41
|
+
// the same gate IdleNotify drives on Linux, so a draw loop written for
|
|
42
|
+
// one flavor paces correctly on the other.
|
|
43
|
+
|
|
44
|
+
import { connectionGone } from './cleanup.js';
|
|
45
|
+
import Drawable from './drawable.js';
|
|
46
|
+
import { GLError, backendFor, glError, loadDriAddon } from './gl.js';
|
|
47
|
+
|
|
48
|
+
/** pacing fallback where the display's rate is not known (see App#frameInterval) */
|
|
49
|
+
const FALLBACK_FRAME_INTERVAL = 1000 / 60;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The app-level uid -> context routes for AppleDRISurfaceNotify.
|
|
53
|
+
*
|
|
54
|
+
* The event is classic (not a GenericEvent, so `_setGenericEventSink` never
|
|
55
|
+
* sees it), unsolicited (no mask to select), and names its surface by the
|
|
56
|
+
* **uid** from the CreateSurface reply rather than by window id — so the
|
|
57
|
+
* per-window `event_consumers` dispatch cannot route it either. One
|
|
58
|
+
* client-level listener per connection, keyed by uid, is the whole scheme.
|
|
59
|
+
*/
|
|
60
|
+
function appleSurfaceRoutes(app) {
|
|
61
|
+
if (app._appleSurfaces) return app._appleSurfaces;
|
|
62
|
+
const routes = new Map();
|
|
63
|
+
app._appleSurfaces = routes;
|
|
64
|
+
app.X.on('event', (ev) => {
|
|
65
|
+
if (ev.name !== 'AppleDRISurfaceNotify') return;
|
|
66
|
+
routes.get(ev.arg)?._surfaceNotify(ev);
|
|
67
|
+
});
|
|
68
|
+
return routes;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class RenderingContextCGL {
|
|
72
|
+
constructor(window, config = {}) {
|
|
73
|
+
const app = window.app;
|
|
74
|
+
const caps = app._glCapsResolved;
|
|
75
|
+
if (!caps) {
|
|
76
|
+
throw glError(
|
|
77
|
+
GLError.CONTEXT_FAILED,
|
|
78
|
+
"getContext('cgl') needs the direct-rendering probe to have answered, and it has not",
|
|
79
|
+
`createClient() runs the probe during the handshake when glPolicy could pick the
|
|
80
|
+
direct backend, so a context can be created synchronously afterwards. Under the
|
|
81
|
+
default policy ('indirect') it does not run, and asking for this context by name
|
|
82
|
+
does not make it retroactive. Either:
|
|
83
|
+
|
|
84
|
+
const app = await createClient({ glPolicy: 'auto' }); // probe at connect
|
|
85
|
+
await app.glCapabilities(); // or ask, once, later`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (!caps.direct) throw caps.reason;
|
|
89
|
+
if (caps.flavor !== 'appledri') {
|
|
90
|
+
throw glError(
|
|
91
|
+
GLError.CONTEXT_FAILED,
|
|
92
|
+
`this connection's direct backend is ${caps.flavor}, not Apple-DRI/CGL`,
|
|
93
|
+
"getContext('opengl') is the backend-neutral name and picks the right flavor;\n'cgl' asks for this one by name and only exists on macOS/XQuartz."
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const dri = loadDriAddon();
|
|
98
|
+
this.window = window;
|
|
99
|
+
this.app = app;
|
|
100
|
+
this.X = window.X;
|
|
101
|
+
this.dri = dri;
|
|
102
|
+
this.AppleDRI = caps.AppleDRI;
|
|
103
|
+
/** which backend this is, for code that runs on either */
|
|
104
|
+
this.backend = 'direct';
|
|
105
|
+
/** which direct pipeline, for the curious ('dri3' on Linux) */
|
|
106
|
+
this.flavor = 'appledri';
|
|
107
|
+
this.error = null;
|
|
108
|
+
|
|
109
|
+
const depth = window.depth || app.display.screen[0].root_depth || 24;
|
|
110
|
+
if (depth !== 24 && depth !== 32) {
|
|
111
|
+
throw glError(
|
|
112
|
+
GLError.CONTEXT_FAILED,
|
|
113
|
+
`direct rendering needs a 24- or 32-bit window, and this one is ${depth}-bit`,
|
|
114
|
+
'Create the window with depth 24 (opaque) or 32 (per-pixel alpha, via\napp.findArgbVisual()).'
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
this.depth = depth;
|
|
118
|
+
this._screen = config.screen ?? 0;
|
|
119
|
+
this._clientId = caps.appleClientId ?? dri.apple.clientId();
|
|
120
|
+
|
|
121
|
+
// One CGL context per context object, not a shared one per app: a CGL
|
|
122
|
+
// context can be attached to exactly one surface, and contexts are cheap
|
|
123
|
+
// where EGL ones are not. The visible consequence is that GL resources
|
|
124
|
+
// (programs, textures) are per-window on this flavor where the GLES one
|
|
125
|
+
// shares them — key caches by the `gl` object identity and both are
|
|
126
|
+
// covered (docs/context-gles.md#macos).
|
|
127
|
+
try {
|
|
128
|
+
this.ctx = new dri.apple.Context({
|
|
129
|
+
depthSize: config.depthSize ?? config.DEPTH_SIZE ?? 16
|
|
130
|
+
});
|
|
131
|
+
} catch (err) {
|
|
132
|
+
throw glError(
|
|
133
|
+
GLError.CONTEXT_FAILED,
|
|
134
|
+
`could not create a CGL context: ${err.message}`,
|
|
135
|
+
null,
|
|
136
|
+
err
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this._routes = appleSurfaceRoutes(app);
|
|
141
|
+
this._uid = undefined;
|
|
142
|
+
this._attached = false;
|
|
143
|
+
this._pendingSurface = false;
|
|
144
|
+
this._throttled = false;
|
|
145
|
+
this._timer = null;
|
|
146
|
+
this._frameWanted = null;
|
|
147
|
+
this._destroyed = false;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolves once the window's surface has been exported and attached —
|
|
151
|
+
* the whole path proven — and rejects with a coded error if it cannot
|
|
152
|
+
* be. Settles only after the window is mapped: the physical Quartz
|
|
153
|
+
* window has to exist before the server has a surface to export, so
|
|
154
|
+
* `map()` first, then `await gl.ready`.
|
|
155
|
+
*/
|
|
156
|
+
this.ready = new Promise((resolve, reject) => {
|
|
157
|
+
this._settleReady = (err) => {
|
|
158
|
+
this._settleReady = null;
|
|
159
|
+
if (err) reject(err);
|
|
160
|
+
else resolve(this);
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
// a rejection nobody is listening for must not take the process down;
|
|
164
|
+
// `error` is the other way to find out
|
|
165
|
+
this.ready.catch(() => {});
|
|
166
|
+
|
|
167
|
+
// GL entry points and constants, bound so that this context is the
|
|
168
|
+
// current one when they run
|
|
169
|
+
this._installGL();
|
|
170
|
+
this.makeCurrent();
|
|
171
|
+
|
|
172
|
+
// The attach dance: surface after MapNotify, again after a Destroyed
|
|
173
|
+
// notify once the window is viewable again. `on('map')` also selects
|
|
174
|
+
// StructureNotify where the window does not have it yet (adopted ids).
|
|
175
|
+
this._onMap = () => {
|
|
176
|
+
if (!this._attached && !this._pendingSurface) this._createSurface();
|
|
177
|
+
};
|
|
178
|
+
window.on('map', this._onMap);
|
|
179
|
+
if (window._mapped) this._createSurface();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Copy the addon's GL namespace onto this context.
|
|
184
|
+
*
|
|
185
|
+
* Same shape as the GLES flavor: every function is wrapped with a currency
|
|
186
|
+
* check, because another context may have made itself current since the
|
|
187
|
+
* last call here — `app._glCurrent` is one slot shared by every direct
|
|
188
|
+
* context on the connection, whichever flavor.
|
|
189
|
+
*/
|
|
190
|
+
_installGL() {
|
|
191
|
+
const table = this.dri.gl;
|
|
192
|
+
for (const key in table) {
|
|
193
|
+
const value = table[key];
|
|
194
|
+
if (typeof value !== 'function') {
|
|
195
|
+
this[key] = value; // GL constants
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
this[key] = (...args) => {
|
|
199
|
+
if (this.app._glCurrent !== this) this._bind();
|
|
200
|
+
return value(...args);
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Ask the server to export the window's surface, then attach to it.
|
|
207
|
+
*
|
|
208
|
+
* Called from MapNotify (the physical window now exists) and from the
|
|
209
|
+
* SurfaceNotify(destroyed) recovery path. Attaching again on the same CGL
|
|
210
|
+
* context replaces whatever surface it held.
|
|
211
|
+
*/
|
|
212
|
+
_createSurface() {
|
|
213
|
+
if (this._destroyed || this.error || this.window._destroyed || connectionGone(this.X)) return;
|
|
214
|
+
this._pendingSurface = true;
|
|
215
|
+
this.AppleDRI.CreateSurface(this._screen, this.window.id, this._clientId, (err, surf) => {
|
|
216
|
+
this._pendingSurface = false;
|
|
217
|
+
if (this._destroyed || this.window._destroyed) return;
|
|
218
|
+
if (err) {
|
|
219
|
+
return this._fail(
|
|
220
|
+
glError(
|
|
221
|
+
GLError.CONTEXT_FAILED,
|
|
222
|
+
`Apple-DRI could not export a surface for this window: ${err.message}`,
|
|
223
|
+
null,
|
|
224
|
+
err
|
|
225
|
+
)
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (this._uid !== undefined) this._routes.delete(this._uid);
|
|
229
|
+
this._uid = surf.uid;
|
|
230
|
+
this._routes.set(surf.uid, this);
|
|
231
|
+
try {
|
|
232
|
+
this.ctx.attach(surf.key);
|
|
233
|
+
} catch (attachErr) {
|
|
234
|
+
return this._fail(
|
|
235
|
+
glError(
|
|
236
|
+
GLError.CONTEXT_FAILED,
|
|
237
|
+
`could not attach the CGL context to the exported surface: ${attachErr.message}`,
|
|
238
|
+
null,
|
|
239
|
+
attachErr
|
|
240
|
+
)
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
this.app._glCurrent = this; // attach leaves the context current
|
|
244
|
+
this._attached = true;
|
|
245
|
+
this._settleReady?.(null);
|
|
246
|
+
this._onFrameAvailable();
|
|
247
|
+
});
|
|
248
|
+
this.X.flush?.();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** the app-level route target for this context's SurfaceNotify events */
|
|
252
|
+
_surfaceNotify(ev) {
|
|
253
|
+
if (this._destroyed || this.error) return;
|
|
254
|
+
if (ev.kind === this.AppleDRI.NotifyKind.Changed) {
|
|
255
|
+
// moved or resized under the surface: refresh the context's idea of it
|
|
256
|
+
if (this._attached) {
|
|
257
|
+
try {
|
|
258
|
+
this.ctx.update();
|
|
259
|
+
} catch {
|
|
260
|
+
// a surface torn down between the event and now; the Destroyed
|
|
261
|
+
// notify that follows is what handles it
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
// Destroyed: the Quartz window went away — unmapped, or its frame was
|
|
267
|
+
// recreated. The context survives; the surface has to be exported and
|
|
268
|
+
// attached again once there is a window to export.
|
|
269
|
+
this._attached = false;
|
|
270
|
+
if (this._uid !== undefined) {
|
|
271
|
+
this._routes.delete(this._uid);
|
|
272
|
+
this._uid = undefined;
|
|
273
|
+
}
|
|
274
|
+
if (this.window._mapped && !this._pendingSurface) this._createSurface();
|
|
275
|
+
// not mapped: the 'map' listener picks it up when it is again
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Make this context current, and pick up a resize.
|
|
280
|
+
*
|
|
281
|
+
* Call it at the top of a frame, as on the GLES flavor. A size change
|
|
282
|
+
* needs no new buffers here — the surface is the window's backing store
|
|
283
|
+
* and tracks it — but the context has to be told to re-read the geometry.
|
|
284
|
+
*/
|
|
285
|
+
makeCurrent() {
|
|
286
|
+
if (this.error || this._destroyed || this.window._destroyed) return this;
|
|
287
|
+
this._bind();
|
|
288
|
+
const width = this.window.width;
|
|
289
|
+
const height = this.window.height;
|
|
290
|
+
if (width !== this._width || height !== this._height) {
|
|
291
|
+
this._width = width;
|
|
292
|
+
this._height = height;
|
|
293
|
+
if (this._attached) {
|
|
294
|
+
try {
|
|
295
|
+
this.ctx.update();
|
|
296
|
+
} catch {
|
|
297
|
+
// surface gone; the Destroyed notify re-attaches
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return this;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
_bind() {
|
|
305
|
+
if (this.error || this._destroyed) return;
|
|
306
|
+
this.ctx.makeCurrent();
|
|
307
|
+
this.app._glCurrent = this;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Is a frame worth drawing right now?
|
|
312
|
+
*
|
|
313
|
+
* False until the surface is attached (nowhere to draw to), and false for
|
|
314
|
+
* one display period after each swap (the pacing gate). `onFrameAvailable`
|
|
315
|
+
* fires when either turns true.
|
|
316
|
+
*/
|
|
317
|
+
canRender() {
|
|
318
|
+
if (this.error || this._destroyed) return false;
|
|
319
|
+
return this._attached && !this._throttled;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Called when `canRender()` became true again — attach settled, or the pacing gate reopened. */
|
|
323
|
+
set onFrameAvailable(fn) {
|
|
324
|
+
this._frameWanted = fn;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
get onFrameAvailable() {
|
|
328
|
+
return this._frameWanted;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
_onFrameAvailable() {
|
|
332
|
+
this._frameWanted?.();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Show the frame just drawn (CGLFlushDrawable — the WindowServer
|
|
337
|
+
* composites the surface; no X request is involved).
|
|
338
|
+
*
|
|
339
|
+
* Returns false when there is no attached surface yet or the pacing gate
|
|
340
|
+
* is closed — the same contract as the GLES flavor's "every buffer is with
|
|
341
|
+
* the server", and `onFrameAvailable` reopens it the same way.
|
|
342
|
+
*/
|
|
343
|
+
SwapBuffers() {
|
|
344
|
+
if (this.error || this._destroyed || this.window._destroyed) return false;
|
|
345
|
+
if (!this._attached || this._throttled) return false;
|
|
346
|
+
try {
|
|
347
|
+
this.ctx.flush();
|
|
348
|
+
} catch (err) {
|
|
349
|
+
this._fail(glError(GLError.CONTEXT_FAILED, `CGL flush failed: ${err.message}`, null, err));
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
// The pacing gate: flush() never blocks and the server sends no
|
|
353
|
+
// done-with-it event, so an unthrottled loop would spin. Close the gate
|
|
354
|
+
// for most of a display period; the timer reopens it and fires
|
|
355
|
+
// onFrameAvailable, which is exactly what IdleNotify does on Linux.
|
|
356
|
+
// Most of one rather than a full one because callers paced by
|
|
357
|
+
// requestAnimationFrame already wait a display period of their own, and
|
|
358
|
+
// a full gate in series with it would drop below display rate; at 3/4
|
|
359
|
+
// the frame clock stays the pacer and this gate only stops the spin.
|
|
360
|
+
// (ctx.setSwapInterval(1) — a blocking vsync'd flush — remains available
|
|
361
|
+
// on the raw context for callers who want real vsync.)
|
|
362
|
+
this._throttled = true;
|
|
363
|
+
this._timer = setTimeout(() => {
|
|
364
|
+
this._timer = null;
|
|
365
|
+
this._throttled = false;
|
|
366
|
+
this._onFrameAvailable();
|
|
367
|
+
}, (this.app.frameInterval ?? FALLBACK_FRAME_INTERVAL) * 0.75);
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
swapBuffers() {
|
|
372
|
+
return this.SwapBuffers();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** The GL renderer string — "Apple M1 Pro" and the like, handy in bug reports. */
|
|
376
|
+
get renderer() {
|
|
377
|
+
try {
|
|
378
|
+
return this.dri.gl.getString(this.dri.GL.RENDERER);
|
|
379
|
+
} catch {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
_fail(err) {
|
|
385
|
+
if (this.error) return;
|
|
386
|
+
this.error = err;
|
|
387
|
+
this._settleReady?.(err);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
destroy() {
|
|
391
|
+
if (this._destroyed) return;
|
|
392
|
+
this._destroyed = true;
|
|
393
|
+
if (this._timer) {
|
|
394
|
+
clearTimeout(this._timer);
|
|
395
|
+
this._timer = null;
|
|
396
|
+
}
|
|
397
|
+
this.window.removeListener('map', this._onMap);
|
|
398
|
+
if (this._uid !== undefined) {
|
|
399
|
+
this._routes.delete(this._uid);
|
|
400
|
+
this._uid = undefined;
|
|
401
|
+
}
|
|
402
|
+
// release the server's reference to the surface; with the window (or the
|
|
403
|
+
// connection) already gone the server has done it for us
|
|
404
|
+
if (!this.window._destroyed && !connectionGone(this.X)) {
|
|
405
|
+
try {
|
|
406
|
+
this.AppleDRI.DestroySurface(this._screen, this.window.id);
|
|
407
|
+
} catch {
|
|
408
|
+
// the connection is closing; the server frees everything with it
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (this.app._glCurrent === this) this.app._glCurrent = null;
|
|
412
|
+
try {
|
|
413
|
+
this.ctx.destroy();
|
|
414
|
+
} catch {
|
|
415
|
+
// already torn down
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
[Symbol.dispose]() {
|
|
420
|
+
this.destroy();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
Drawable.renderingContextFactory['cgl'] = (window, config) => new RenderingContextCGL(window, config);
|
|
425
|
+
|
|
426
|
+
// The 'opengl' dispatcher was registered by renderingcontext_gles.js (which
|
|
427
|
+
// index.js imports first); this wrap slots the Apple flavor in ahead of it.
|
|
428
|
+
// The chain: appledri flavor -> this context; everything else -> the GLES
|
|
429
|
+
// module's dispatch, which handles dri3, indirect, off and not-yet-probed.
|
|
430
|
+
const dispatchBelow = Drawable.renderingContextFactory['opengl'];
|
|
431
|
+
Drawable.renderingContextFactory['opengl'] = (window, config) => {
|
|
432
|
+
const app = window.app;
|
|
433
|
+
if (backendFor(app) === 'direct' && app._glCapsResolved?.flavor === 'appledri') {
|
|
434
|
+
return new RenderingContextCGL(window, config);
|
|
435
|
+
}
|
|
436
|
+
return dispatchBelow(window, config);
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
export default RenderingContextCGL;
|
|
@@ -67,6 +67,13 @@ does not make it retroactive. Either:
|
|
|
67
67
|
);
|
|
68
68
|
}
|
|
69
69
|
if (!caps.direct) throw caps.reason;
|
|
70
|
+
if (caps.flavor && caps.flavor !== 'dri3') {
|
|
71
|
+
throw glError(
|
|
72
|
+
GLError.CONTEXT_FAILED,
|
|
73
|
+
`this connection's direct backend is ${caps.flavor}, not DRI3/GLES`,
|
|
74
|
+
"getContext('opengl') is the backend-neutral name and picks the right flavor;\n'gles' asks for the Linux one by name. On macOS/XQuartz the flavor is 'cgl'\n(lib/renderingcontext_cgl.js), same contract and the same gl surface."
|
|
75
|
+
);
|
|
76
|
+
}
|
|
70
77
|
|
|
71
78
|
const dri = loadDriAddon();
|
|
72
79
|
this.window = window;
|
package/lib/window.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import x11 from 'x11';
|
|
2
2
|
|
|
3
3
|
import { builtin } from './builtin.js';
|
|
4
|
-
import { safeRelease } from './cleanup.js';
|
|
4
|
+
import { connectionGone, safeRelease } from './cleanup.js';
|
|
5
5
|
import Drawable from './drawable.js';
|
|
6
6
|
import { packIcons, unpackIcons } from './imagedata.js';
|
|
7
7
|
import Pixmap from './pixmap.js';
|
|
@@ -606,22 +606,34 @@ export default class Window extends Drawable {
|
|
|
606
606
|
// client chose the visual for (issue #295). They go out in the same
|
|
607
607
|
// batch, so the pair costs one round trip, not two, and `ready` is the
|
|
608
608
|
// wait for both.
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
609
|
+
//
|
|
610
|
+
// Nothing is asked of a connection that is going away: node-x11 throws
|
|
611
|
+
// synchronously at a request once close() has begun, and adoption
|
|
612
|
+
// happens from places with no caller to catch — a routed child-event,
|
|
613
|
+
// XEmbed, the shared glyph cache all build windows from inside the X
|
|
614
|
+
// event dispatch (issue #321). There is nothing to learn there either,
|
|
615
|
+
// so `ready` settles with the geometry unknown, the same outcome as a
|
|
616
|
+
// window destroyed before its reply landed (see the getter).
|
|
617
|
+
this._readyPending = connectionGone(X) ? 0 : 2;
|
|
618
|
+
if (this._readyPending) {
|
|
619
|
+
X.GetGeometry(this.id, (err, res) => {
|
|
620
|
+
// A window that was destroyed between the id reaching us and this
|
|
621
|
+
// request reaching the server answers BadWindow, and there is
|
|
622
|
+
// nothing to record. `ready` still resolves: adopting a window that
|
|
623
|
+
// has since gone is ordinary for a window manager, and a wait that
|
|
624
|
+
// never ends is worse than one that ends with width undefined.
|
|
625
|
+
this._readyPending--;
|
|
626
|
+
if (err) this._settleReady();
|
|
627
|
+
else this._applyGeometry(unpackGeometry(res));
|
|
628
|
+
});
|
|
629
|
+
X.GetWindowAttributes(this.id, (err, attrs) => {
|
|
630
|
+
this._readyPending--;
|
|
631
|
+
if (!err) this.visualId = attrs.visual;
|
|
632
|
+
this._settleReady();
|
|
633
|
+
});
|
|
634
|
+
} else {
|
|
623
635
|
this._settleReady();
|
|
624
|
-
}
|
|
636
|
+
}
|
|
625
637
|
}
|
|
626
638
|
|
|
627
639
|
Window._cacheFor(app).set(this.id, this);
|
|
@@ -779,6 +791,12 @@ export default class Window extends Drawable {
|
|
|
779
791
|
this.on('child-event', (ev) => {
|
|
780
792
|
const eventName = xevents.eventName[ev.type];
|
|
781
793
|
if (!eventName) return;
|
|
794
|
+
// Events buffered before `close()` keep being dispatched after it, and
|
|
795
|
+
// a window this client already saw destroyed has no substructure left
|
|
796
|
+
// to report. Neither is worth building a child for — and the questions
|
|
797
|
+
// the constructor would ask on the way out are what used to throw into
|
|
798
|
+
// the dispatcher (issue #321).
|
|
799
|
+
if (this._destroyed || connectionGone(X)) return;
|
|
782
800
|
const child = new Window(app, { id: ev.wid });
|
|
783
801
|
const ntkev = { ...ev, parent: this, window: child, target: child };
|
|
784
802
|
// wait until we know that we track correct x,y,w,h values
|
|
@@ -788,6 +806,9 @@ export default class Window extends Drawable {
|
|
|
788
806
|
});
|
|
789
807
|
|
|
790
808
|
this.on('newListener', (name) => {
|
|
809
|
+
// one of ntk's own handlers going on, not a caller asking for an
|
|
810
|
+
// event: nothing to arm and nothing to select — see _listenInternal
|
|
811
|
+
if (this._internalListen) return;
|
|
791
812
|
// Listening for 'close' is the opt-in. WM_DELETE_WINDOW only reaches a
|
|
792
813
|
// client that advertised it in WM_PROTOCOLS — a window manager kills
|
|
793
814
|
// anyone else outright — and having to know that, on top of decoding a
|
|
@@ -814,6 +835,13 @@ export default class Window extends Drawable {
|
|
|
814
835
|
const eventMask = xevents.mask[name];
|
|
815
836
|
if (!eventMask) return;
|
|
816
837
|
if ((eventMask & this.eventMask) === 0) {
|
|
838
|
+
// A connection on its way out can select nothing, and node-x11 throws
|
|
839
|
+
// synchronously at a request from then on. This hook runs from
|
|
840
|
+
// `.on()` — including the calls made while adopting a window from
|
|
841
|
+
// inside the X event dispatch, where there is no caller to catch it
|
|
842
|
+
// (issue #321). Before the mask is touched, so it goes on saying what
|
|
843
|
+
// this connection holds.
|
|
844
|
+
if (connectionGone(X)) return;
|
|
817
845
|
this.eventMask |= eventMask;
|
|
818
846
|
// the selection can legitimately fail — SubstructureRedirect is
|
|
819
847
|
// one-client-only, so `on('map_request')` is how you find out
|
|
@@ -821,14 +849,29 @@ export default class Window extends Drawable {
|
|
|
821
849
|
// error hook rather than dropping it; selectInput() is the
|
|
822
850
|
// explicit form that hands the error straight back.
|
|
823
851
|
X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, (err) => {
|
|
824
|
-
if (err)
|
|
852
|
+
if (!err) return;
|
|
853
|
+
// the request failed as a whole, so the server kept the mask it
|
|
854
|
+
// had: drop the bits again rather than leave `eventMask` claiming
|
|
855
|
+
// a selection this connection does not hold. selectInput() trusts
|
|
856
|
+
// it to decide whether a write can be skipped.
|
|
857
|
+
this.eventMask &= ~eventMask;
|
|
858
|
+
this.app.options.onXError?.(err);
|
|
825
859
|
});
|
|
826
860
|
}
|
|
827
861
|
});
|
|
828
862
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
863
|
+
// ntk's own bookkeeping listens for the same events an app does, and the
|
|
864
|
+
// hook above reads a listener as a caller expressing interest and selects
|
|
865
|
+
// for it. For a window ntk created that is free — StructureNotify is in
|
|
866
|
+
// the CreateWindow value list already — but for an adopted one the
|
|
867
|
+
// tracked mask starts at zero and the write is absolute, so it would
|
|
868
|
+
// replace whatever this connection had selected on a window another
|
|
869
|
+
// client owns (issue #322). Subscribe past the hook: adopting a window
|
|
870
|
+
// costs no requests and changes no server-side state, and a caller that
|
|
871
|
+
// wants these events still asks for them, with on('map') or selectInput.
|
|
872
|
+
this._listenInternal('map', () => (this._mapped = true));
|
|
873
|
+
this._listenInternal('unmap', () => (this._mapped = false));
|
|
874
|
+
this._listenInternal('destroy', () => {
|
|
832
875
|
this._destroyed = true;
|
|
833
876
|
this._forget();
|
|
834
877
|
this._teardownFrame(false); // the server has already taken the window
|
|
@@ -842,6 +885,24 @@ export default class Window extends Drawable {
|
|
|
842
885
|
}
|
|
843
886
|
}
|
|
844
887
|
|
|
888
|
+
/**
|
|
889
|
+
* Attach one of ntk's own handlers without the `newListener` hook reading
|
|
890
|
+
* it as a caller asking for the event. The hook's job is to turn expressed
|
|
891
|
+
* interest into a server-side selection; ntk's internal bookkeeping
|
|
892
|
+
* expresses none — it only wants the events that arrive anyway — and on an
|
|
893
|
+
* adopted window a selection nobody asked for overwrites another
|
|
894
|
+
* subsystem's (issue #322). The flag is read synchronously by the hook,
|
|
895
|
+
* which EventEmitter emits before the listener is stored.
|
|
896
|
+
*/
|
|
897
|
+
_listenInternal(name, fn) {
|
|
898
|
+
this._internalListen = true;
|
|
899
|
+
try {
|
|
900
|
+
this.on(name, fn);
|
|
901
|
+
} finally {
|
|
902
|
+
this._internalListen = false;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
845
906
|
/**
|
|
846
907
|
* Resolves with this window once its geometry is known — immediately for a
|
|
847
908
|
* window ntk created (it already knows what it asked for), when the
|
|
@@ -1098,7 +1159,11 @@ export default class Window extends Drawable {
|
|
|
1098
1159
|
|
|
1099
1160
|
_handleExpose(ev) {
|
|
1100
1161
|
if (this._backingValid) {
|
|
1101
|
-
|
|
1162
|
+
// an expose can arrive after the connection started closing, and this
|
|
1163
|
+
// runs inside the event dispatch where a throw has no caller (#321)
|
|
1164
|
+
safeRelease(this.X, () => {
|
|
1165
|
+
this.X.CopyArea(this._backing.id, this.id, this._presentGc, ev.x, ev.y, ev.x, ev.y, ev.width, ev.height);
|
|
1166
|
+
});
|
|
1102
1167
|
} else {
|
|
1103
1168
|
this._requestRedraw();
|
|
1104
1169
|
}
|
|
@@ -3517,13 +3582,26 @@ export default class Window extends Drawable {
|
|
|
3517
3582
|
* either makes you the window manager or rejects with BadAccess because
|
|
3518
3583
|
* something else already is. The mask is OR-ed into whatever handlers
|
|
3519
3584
|
* have already asked for.
|
|
3585
|
+
*
|
|
3586
|
+
* A mask this connection already holds is a request that would change
|
|
3587
|
+
* nothing, so it resolves without one — `eventMask` is what this
|
|
3588
|
+
* connection selected and nothing else adds to it behind the caller's
|
|
3589
|
+
* back. It resolves rather than returning undefined because the promise is
|
|
3590
|
+
* the whole interface: an awaited no-op still has to mean "you have it".
|
|
3520
3591
|
*/
|
|
3521
3592
|
selectInput(mask) {
|
|
3522
|
-
this.eventMask
|
|
3593
|
+
const added = mask & ~this.eventMask;
|
|
3594
|
+
if (added === 0) return Promise.resolve(this);
|
|
3595
|
+
this.eventMask |= added;
|
|
3523
3596
|
return new Promise((resolve, reject) => {
|
|
3524
|
-
this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, (err) =>
|
|
3525
|
-
|
|
3526
|
-
|
|
3597
|
+
this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, (err) => {
|
|
3598
|
+
if (!err) return resolve(this);
|
|
3599
|
+
// a refused write left the server's mask as it was — give back the
|
|
3600
|
+
// bits, or the next call would find them held and skip a request
|
|
3601
|
+
// that has not happened yet
|
|
3602
|
+
this.eventMask &= ~added;
|
|
3603
|
+
reject(err);
|
|
3604
|
+
});
|
|
3527
3605
|
});
|
|
3528
3606
|
}
|
|
3529
3607
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ntk",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.4.0",
|
|
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",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"x11": "^4.0.1"
|
|
48
48
|
},
|
|
49
49
|
"optionalDependencies": {
|
|
50
|
-
"x11-dri": ">=0.
|
|
50
|
+
"x11-dri": ">=0.5.0 <1"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"test": "NTK_STRICT_COLORS=1 node --test",
|