ntk 7.2.0 → 7.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/app.js +95 -0
- package/lib/gl.js +321 -0
- package/lib/glswapchain.js +408 -0
- package/lib/index.js +34 -4
- package/lib/path.js +28 -0
- package/lib/renderingcontext_2d.js +128 -6
- package/lib/renderingcontext_gles.js +312 -0
- package/lib/window.js +29 -1
- package/package.json +5 -2
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// Direct rendering context: OpenGL ES 2 on the GPU, frames delivered to the
|
|
2
|
+
// server as dma-buf descriptors over DRI3 + Present (lib/glswapchain.js).
|
|
3
|
+
//
|
|
4
|
+
// The API is WebGL-shaped and camelCase — `gl.createShader`, `gl.drawElements`
|
|
5
|
+
// — because that is what the addon's ES 2 binding exposes and what anyone
|
|
6
|
+
// writing shaders already knows. It is *not* the same API as the indirect GLX
|
|
7
|
+
// context, whose OpenGL 1.x commands are PascalCase and whose pipeline has no
|
|
8
|
+
// shaders at all; the two backends are honestly different rather than one
|
|
9
|
+
// pretending to be the other. Cross-backend code branches on `gl.backend`
|
|
10
|
+
// ('direct' or 'indirect') — see docs/context-gles.md.
|
|
11
|
+
//
|
|
12
|
+
// Setup is synchronous, which is the point: creating the GPU context and its
|
|
13
|
+
// buffers needs the DRM device and nothing from the X server, so `gl.*` works
|
|
14
|
+
// on the line after `getContext`. Only *presenting* needs the server, and
|
|
15
|
+
// `ready` is what reports whether that works — it resolves when the first
|
|
16
|
+
// buffer has been imported by the server, which is the moment the whole path
|
|
17
|
+
// is proven.
|
|
18
|
+
|
|
19
|
+
import Drawable from './drawable.js';
|
|
20
|
+
import { GLError, backendFor, glError, loadDriAddon } from './gl.js';
|
|
21
|
+
import { GLSwapchain } from './glswapchain.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One GPU context per app and pixel format, not one per surface.
|
|
25
|
+
*
|
|
26
|
+
* An EGL context is expensive (a device, a GBM device, a display, a config
|
|
27
|
+
* scan) and every surface in an app wants the same one; sharing it also means
|
|
28
|
+
* textures and programs are shared between surfaces, as they are between
|
|
29
|
+
* canvases in a browser tab. The cost is that exactly one surface is current
|
|
30
|
+
* at a time, which `_bind` takes care of.
|
|
31
|
+
*/
|
|
32
|
+
function sharedGpu(app, dri, { format, depthSize, devicePath }) {
|
|
33
|
+
const key = `${format}|${depthSize}|${devicePath ?? ''}`;
|
|
34
|
+
const cache = (app._glGpus ??= new Map());
|
|
35
|
+
const existing = cache.get(key);
|
|
36
|
+
if (existing) return existing;
|
|
37
|
+
let gpu;
|
|
38
|
+
try {
|
|
39
|
+
gpu = new dri.Gpu({ format, depthSize, ...(devicePath ? { devicePath } : {}) });
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw glError(
|
|
42
|
+
GLError.CONTEXT_FAILED,
|
|
43
|
+
`could not create a GPU context on ${devicePath ?? 'the default render node'}: ${err.message}`,
|
|
44
|
+
null,
|
|
45
|
+
err
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
cache.set(key, gpu);
|
|
49
|
+
return gpu;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class RenderingContextGLES {
|
|
53
|
+
constructor(window, config = {}) {
|
|
54
|
+
const app = window.app;
|
|
55
|
+
const caps = app._glCapsResolved;
|
|
56
|
+
if (!caps) {
|
|
57
|
+
throw glError(
|
|
58
|
+
GLError.CONTEXT_FAILED,
|
|
59
|
+
"getContext('gles') needs the direct-rendering probe to have answered, and it has not",
|
|
60
|
+
`createClient() runs the probe during the handshake when glPolicy could pick the
|
|
61
|
+
direct backend, so a context can be created synchronously afterwards. Under the
|
|
62
|
+
default policy ('indirect') it does not run, and asking for this context by name
|
|
63
|
+
does not make it retroactive. Either:
|
|
64
|
+
|
|
65
|
+
const app = await createClient({ glPolicy: 'auto' }); // probe at connect
|
|
66
|
+
await app.glCapabilities(); // or ask, once, later`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (!caps.direct) throw caps.reason;
|
|
70
|
+
|
|
71
|
+
const dri = loadDriAddon();
|
|
72
|
+
this.window = window;
|
|
73
|
+
this.app = app;
|
|
74
|
+
this.X = window.X;
|
|
75
|
+
this.dri = dri;
|
|
76
|
+
/** which backend this is, for code that runs on either */
|
|
77
|
+
this.backend = 'direct';
|
|
78
|
+
this.error = null;
|
|
79
|
+
|
|
80
|
+
// The buffer format has to match how the server will read the pixmap, and
|
|
81
|
+
// that is the window's depth: 24 is XRGB (opaque), 32 is ARGB (the
|
|
82
|
+
// compositor blends the alpha). A window created without an explicit depth
|
|
83
|
+
// has the root's.
|
|
84
|
+
const depth = window.depth || app.display.screen[0].root_depth || 24;
|
|
85
|
+
if (depth !== 24 && depth !== 32) {
|
|
86
|
+
throw glError(
|
|
87
|
+
GLError.CONTEXT_FAILED,
|
|
88
|
+
`direct rendering needs a 24- or 32-bit window, and this one is ${depth}-bit`,
|
|
89
|
+
'Create the window with depth 24 (opaque) or 32 (per-pixel alpha, via\napp.findArgbVisual()).'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
this.depth = depth;
|
|
93
|
+
|
|
94
|
+
const policy = app.glPolicy;
|
|
95
|
+
this.gpu = sharedGpu(app, dri, {
|
|
96
|
+
format: depth === 32 ? dri.FORMAT.ARGB8888 : dri.FORMAT.XRGB8888,
|
|
97
|
+
depthSize: config.depthSize ?? config.DEPTH_SIZE ?? 16,
|
|
98
|
+
devicePath: policy.devicePath ?? caps.device
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.swapchain = new GLSwapchain({
|
|
102
|
+
window,
|
|
103
|
+
gpu: this.gpu,
|
|
104
|
+
dri,
|
|
105
|
+
DRI3: caps.DRI3,
|
|
106
|
+
Present: caps.Present,
|
|
107
|
+
depth,
|
|
108
|
+
policy
|
|
109
|
+
});
|
|
110
|
+
window._setGenericEventSink(caps.Present.majorOpcode, this.swapchain);
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolves once a frame's buffer has been accepted by the server — the
|
|
114
|
+
* whole path proven, not just the parts on this side of the socket — and
|
|
115
|
+
* rejects with a coded error if it cannot be. Nothing needs to await it
|
|
116
|
+
* before drawing; it is how a caller decides to show a fallback instead.
|
|
117
|
+
*/
|
|
118
|
+
this.ready = new Promise((resolve, reject) => {
|
|
119
|
+
this.swapchain.onValidated = (err) => {
|
|
120
|
+
if (err) {
|
|
121
|
+
this.error = err;
|
|
122
|
+
reject(err);
|
|
123
|
+
} else resolve(this);
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
// a rejection nobody is listening for must not take the process down;
|
|
127
|
+
// `error` and the onError hook are the other ways to find out
|
|
128
|
+
this.ready.catch(() => {});
|
|
129
|
+
this.swapchain.onReady = () => this._onFrameAvailable();
|
|
130
|
+
this._frameWanted = null;
|
|
131
|
+
|
|
132
|
+
// GL entry points and constants, bound so that whichever surface this
|
|
133
|
+
// context owns is the current one when they run
|
|
134
|
+
this._installGL();
|
|
135
|
+
this.makeCurrent();
|
|
136
|
+
// settle `ready` now rather than on the first frame — see validate()
|
|
137
|
+
this.swapchain.validate();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Copy the addon's ES 2 namespace onto this context.
|
|
142
|
+
*
|
|
143
|
+
* Every function is wrapped with the currency check rather than documented
|
|
144
|
+
* as the caller's job: the shared context means another surface may have
|
|
145
|
+
* been current since the last call here, and a GL call against the wrong
|
|
146
|
+
* surface draws into the wrong window. The check is one comparison against
|
|
147
|
+
* a field — next to a native call, it does not register.
|
|
148
|
+
*/
|
|
149
|
+
_installGL() {
|
|
150
|
+
const table = this.dri.gl;
|
|
151
|
+
for (const key in table) {
|
|
152
|
+
const value = table[key];
|
|
153
|
+
if (typeof value !== 'function') {
|
|
154
|
+
this[key] = value; // GL constants
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
this[key] = (...args) => {
|
|
158
|
+
if (this.app._glCurrent !== this) this._bind();
|
|
159
|
+
return value(...args);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Make this context's surface current, sizing it to the window first.
|
|
166
|
+
*
|
|
167
|
+
* Call it at the top of a frame: that is where a resize can be honoured
|
|
168
|
+
* without throwing away a half-drawn one.
|
|
169
|
+
*/
|
|
170
|
+
makeCurrent() {
|
|
171
|
+
if (this.error || this._destroyed || this.window._destroyed) return this;
|
|
172
|
+
const width = this.window.width;
|
|
173
|
+
const height = this.window.height;
|
|
174
|
+
if (width !== this._width || height !== this._height) {
|
|
175
|
+
this._width = width;
|
|
176
|
+
this._height = height;
|
|
177
|
+
this._surface = null; // a new size is a new generation
|
|
178
|
+
}
|
|
179
|
+
this._bind();
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_bind() {
|
|
184
|
+
if (this.error || this._destroyed) return;
|
|
185
|
+
if (!this._surface) {
|
|
186
|
+
this._surface = this.swapchain.surfaceFor(this._width ?? this.window.width, this._height ?? this.window.height);
|
|
187
|
+
}
|
|
188
|
+
this.gpu.makeCurrent(this._surface);
|
|
189
|
+
this.app._glCurrent = this;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Is a frame worth drawing right now?
|
|
194
|
+
*
|
|
195
|
+
* False when every buffer is still with the server. Drawing anyway is not
|
|
196
|
+
* wrong, only wasted: the swap that followed would have nowhere to go.
|
|
197
|
+
* `onFrameAvailable` is the other half — it fires when this turns true.
|
|
198
|
+
*/
|
|
199
|
+
canRender() {
|
|
200
|
+
return this.swapchain.canRender();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Called when `canRender()` became true again after a swap was refused. */
|
|
204
|
+
set onFrameAvailable(fn) {
|
|
205
|
+
this._frameWanted = fn;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
get onFrameAvailable() {
|
|
209
|
+
return this._frameWanted;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
_onFrameAvailable() {
|
|
213
|
+
this._frameWanted?.();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Show the frame just drawn.
|
|
218
|
+
*
|
|
219
|
+
* Named as the indirect context names it, so a draw loop can end the same
|
|
220
|
+
* way on either backend; `swapBuffers` is the same call under the spelling
|
|
221
|
+
* the rest of this API uses. Returns false when the frame could not be
|
|
222
|
+
* shown yet — see `canRender`.
|
|
223
|
+
*/
|
|
224
|
+
SwapBuffers() {
|
|
225
|
+
if (this.error || this._destroyed || this.window._destroyed) return false;
|
|
226
|
+
const sent = this.swapchain.swap();
|
|
227
|
+
// A resize seen only now still gets picked up: the next frame binds a
|
|
228
|
+
// generation at the new size. Checked after the swap so the frame that was
|
|
229
|
+
// drawn at the old size is the one that goes out.
|
|
230
|
+
if (this.window.width !== this._width || this.window.height !== this._height) {
|
|
231
|
+
this._surface = null;
|
|
232
|
+
}
|
|
233
|
+
return sent;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
swapBuffers() {
|
|
237
|
+
return this.SwapBuffers();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** The GL renderer string, once there is a context — handy in bug reports. */
|
|
241
|
+
get renderer() {
|
|
242
|
+
try {
|
|
243
|
+
return this.dri.gl.getString(this.dri.GL.RENDERER);
|
|
244
|
+
} catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
destroy() {
|
|
250
|
+
if (this._destroyed) return;
|
|
251
|
+
this._destroyed = true;
|
|
252
|
+
this.swapchain.destroy();
|
|
253
|
+
this.window._setGenericEventSink(0, null);
|
|
254
|
+
if (this.app._glCurrent === this) {
|
|
255
|
+
this.app._glCurrent = null;
|
|
256
|
+
try {
|
|
257
|
+
this.gpu.makeCurrent(null);
|
|
258
|
+
} catch {
|
|
259
|
+
// the context is going away regardless
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
this._surface = null;
|
|
263
|
+
// the Gpu itself is shared and outlives this context (App#close frees it)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
[Symbol.dispose]() {
|
|
267
|
+
this.destroy();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
Drawable.renderingContextFactory['gles'] = (window, config) => new RenderingContextGLES(window, config);
|
|
272
|
+
|
|
273
|
+
// `getContext('opengl')` is the backend-neutral name: it is what the indirect
|
|
274
|
+
// context registered before there was a choice, and what code that does not
|
|
275
|
+
// care should keep asking for. The policy decides which one it gets, and the
|
|
276
|
+
// default policy is still the indirect one, so nothing changes under an app
|
|
277
|
+
// that has not opted in.
|
|
278
|
+
const indirectFactory = Drawable.renderingContextFactory['opengl'];
|
|
279
|
+
Drawable.renderingContextFactory['opengl'] = (window, config) => {
|
|
280
|
+
const app = window.app;
|
|
281
|
+
const backend = backendFor(app);
|
|
282
|
+
if (backend === 'direct') return new RenderingContextGLES(window, config);
|
|
283
|
+
if (backend === 'off') {
|
|
284
|
+
const caps = app._glCapsResolved;
|
|
285
|
+
throw (
|
|
286
|
+
caps?.reason ??
|
|
287
|
+
glError(GLError.DISABLED, "glPolicy is 'off', so getContext('opengl') has no backend to use")
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
// null: the policy could pick direct, but the probe has not answered — a
|
|
291
|
+
// policy raised after connecting. 'direct' must not quietly become the other
|
|
292
|
+
// backend, because the whole point of asking for it by name is that the draw
|
|
293
|
+
// code only speaks ES 2.
|
|
294
|
+
if (backend === null) {
|
|
295
|
+
if (app.glPolicy.mode === 'direct') {
|
|
296
|
+
throw glError(
|
|
297
|
+
GLError.CONTEXT_FAILED,
|
|
298
|
+
"glPolicy is 'direct' but the direct-rendering probe has not answered, so there is no context to give you",
|
|
299
|
+
'The probe runs inside createClient() when the policy is set there. A policy\n' +
|
|
300
|
+
'raised afterwards needs one `await app.glCapabilities()` first.'
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
console.warn(
|
|
304
|
+
"ntk: glPolicy is 'auto' but the direct-rendering probe has not answered yet, so " +
|
|
305
|
+
"getContext('opengl') is using indirect GLX. Pass glPolicy to createClient(), or " +
|
|
306
|
+
'await app.glCapabilities() before creating the context.'
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
return indirectFactory(window, config);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export default RenderingContextGLES;
|
package/lib/window.js
CHANGED
|
@@ -435,6 +435,10 @@ export default class Window extends Drawable {
|
|
|
435
435
|
this._updateRegion = 0;
|
|
436
436
|
this._presentSerial = 0;
|
|
437
437
|
this._presentEid = 0;
|
|
438
|
+
// A direct GL context's swap chain, when one is presenting on this window
|
|
439
|
+
// (see _setGenericEventSink)
|
|
440
|
+
this._geSink = null;
|
|
441
|
+
this._geSinkOpcode = 0;
|
|
438
442
|
// The vblank clock (see _onPresentComplete): the period learnt from
|
|
439
443
|
// completion events, the samples it is drawn from, and the latch the
|
|
440
444
|
// watchdog sets when completions stop arriving.
|
|
@@ -612,7 +616,13 @@ export default class Window extends Drawable {
|
|
|
612
616
|
// and a sub-type where a core event carries its code — the name table
|
|
613
617
|
// below cannot see them, and they are not events user code asked for
|
|
614
618
|
if (ev.type === 35) {
|
|
615
|
-
|
|
619
|
+
// A direct GL context presenting on this window selected these events
|
|
620
|
+
// itself and owns the pixmaps they name, so they are its to read — and
|
|
621
|
+
// it is the only user of Present there, a GL window having no backing
|
|
622
|
+
// store to blit.
|
|
623
|
+
if (this._geSink && ev.extension === this._geSinkOpcode) {
|
|
624
|
+
this._geSink.handleEvent(ev);
|
|
625
|
+
} else if (this._presentExt && ev.extension === this._presentExt.majorOpcode) {
|
|
616
626
|
this._handlePresentEvent(ev);
|
|
617
627
|
}
|
|
618
628
|
return;
|
|
@@ -1256,6 +1266,24 @@ export default class Window extends Drawable {
|
|
|
1256
1266
|
});
|
|
1257
1267
|
}
|
|
1258
1268
|
|
|
1269
|
+
/**
|
|
1270
|
+
* Route this window's Generic Events for one extension to `sink`, ahead of
|
|
1271
|
+
* the built-in Present handling.
|
|
1272
|
+
*
|
|
1273
|
+
* The direct GL context is the caller: its swap chain selects Present's
|
|
1274
|
+
* CompleteNotify and IdleNotify for itself, because it needs to know when a
|
|
1275
|
+
* *specific* buffer came back — a question the window's own present path,
|
|
1276
|
+
* which owns exactly one grow-only pixmap, never has to ask. Pass `null` to
|
|
1277
|
+
* unregister.
|
|
1278
|
+
*
|
|
1279
|
+
* @param {number} majorOpcode the extension whose events to route
|
|
1280
|
+
* @param {{handleEvent: (ev: object) => void}|null} sink
|
|
1281
|
+
*/
|
|
1282
|
+
_setGenericEventSink(majorOpcode, sink) {
|
|
1283
|
+
this._geSink = sink;
|
|
1284
|
+
this._geSinkOpcode = sink ? majorOpcode : 0;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1259
1287
|
/**
|
|
1260
1288
|
* Present's events, which are the ones the core event table cannot name:
|
|
1261
1289
|
* they arrive as X Generic Events (type 35), carrying an extension opcode
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ntk",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.3.1",
|
|
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",
|
|
@@ -48,9 +48,12 @@
|
|
|
48
48
|
"parse-color": "^1.0.0",
|
|
49
49
|
"pngjs": "^7.0.0",
|
|
50
50
|
"postcss": "^8.5.23",
|
|
51
|
-
"x11": "^3.
|
|
51
|
+
"x11": "^3.9.0",
|
|
52
52
|
"yoga-layout": "^3.2.1"
|
|
53
53
|
},
|
|
54
|
+
"optionalDependencies": {
|
|
55
|
+
"x11-dri": ">=0.2.0 <1"
|
|
56
|
+
},
|
|
54
57
|
"scripts": {
|
|
55
58
|
"test": "NTK_STRICT_COLORS=1 node --test",
|
|
56
59
|
"check-release-message": "node scripts/check-release-message.mjs"
|