ntk 8.3.1 → 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 +7 -5
- package/lib/appledri.js +177 -0
- 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/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
|
@@ -603,11 +603,11 @@ export default class App {
|
|
|
603
603
|
* `createWindow` and the whole object to `wnd.getContext('opengl', config)`.
|
|
604
604
|
*
|
|
605
605
|
* The spec is GLX's attribute vocabulary either way, because a caller
|
|
606
|
-
* should not have to write the request twice: `DEPTH_SIZE` becomes the
|
|
607
|
-
* depth-buffer size
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
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.
|
|
611
611
|
*
|
|
612
612
|
* @param {object} [spec] GLX attributes, e.g. `{ DEPTH_SIZE: 24 }`
|
|
613
613
|
*/
|
|
@@ -629,6 +629,8 @@ export default class App {
|
|
|
629
629
|
}
|
|
630
630
|
return {
|
|
631
631
|
backend: 'direct',
|
|
632
|
+
// which direct pipeline this connection runs: 'dri3' or 'appledri'
|
|
633
|
+
flavor: caps.flavor,
|
|
632
634
|
// depth 24 on the root visual needs no colormap of its own, which is
|
|
633
635
|
// why an opaque GL window here is an ordinary window
|
|
634
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/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/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",
|