ntk 3.5.2 → 3.6.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 +4 -2
- package/lib/app.js +26 -1
- package/lib/glx.js +195 -0
- package/lib/index.js +6 -3
- package/lib/renderingcontext_2d.js +45 -12
- package/lib/renderingcontext_opengl.js +120 -30
- package/lib/window.js +31 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -132,10 +132,12 @@ Note that on many systems indirect GLX is disabled by default —
|
|
|
132
132
|
import { createClient } from 'ntk';
|
|
133
133
|
|
|
134
134
|
const app = await createClient();
|
|
135
|
-
|
|
135
|
+
// GLX drawables need a GLX-capable visual, chosen before the window exists
|
|
136
|
+
const glx = await app.chooseGLXConfig({ DEPTH_SIZE: 24 });
|
|
137
|
+
const wnd = app.createWindow({ width: 300, height: 300, visual: glx.visual, depth: glx.depth });
|
|
136
138
|
wnd.map();
|
|
137
139
|
|
|
138
|
-
const gl = wnd.getContext('opengl');
|
|
140
|
+
const gl = wnd.getContext('opengl', glx);
|
|
139
141
|
gl.ClearColor(0.3, 0.3, 0.3, 0.0);
|
|
140
142
|
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
141
143
|
gl.Begin(gl.TRIANGLES);
|
package/lib/app.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Clipboard from './clipboard.js';
|
|
2
2
|
import { CursorCache } from './cursor.js';
|
|
3
|
+
import { chooseGLXConfig } from './glx.js';
|
|
3
4
|
import Pixmap from './pixmap.js';
|
|
4
5
|
import FontManager from './text/fontmanager.js';
|
|
5
6
|
import Window from './window.js';
|
|
@@ -13,7 +14,7 @@ export default class App {
|
|
|
13
14
|
* @param {object} display node-x11 display
|
|
14
15
|
* @param {object} [options] environment hooks: { fontSource (see
|
|
15
16
|
* text/fontsource.js), glxVisual (visual id for getContext('opengl')
|
|
16
|
-
*
|
|
17
|
+
* instead of querying the server for one) }
|
|
17
18
|
*/
|
|
18
19
|
constructor(display, options = {}) {
|
|
19
20
|
this.display = display;
|
|
@@ -56,6 +57,30 @@ export default class App {
|
|
|
56
57
|
return new Window(this, args);
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Allocate a colormap for `visual` (X CreateColormap, alloc None — the
|
|
62
|
+
* only legal value for the TrueColor visuals we use). Windows created
|
|
63
|
+
* with an explicit `visual` get one of these automatically.
|
|
64
|
+
* @returns {number} the colormap id
|
|
65
|
+
*/
|
|
66
|
+
createColormap(visual, screen = 0) {
|
|
67
|
+
const mid = this.X.AllocID();
|
|
68
|
+
this.X.CreateColormap(mid, this.display.screen[screen].root, visual, 0);
|
|
69
|
+
return mid;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Pick a GLX-capable visual (docs/context-opengl.md). Resolves with
|
|
74
|
+
* `{ visual, depth, class, doubleBuffer, depthSize, fbconfig, screen }` —
|
|
75
|
+
* pass `visual`/`depth` to `createWindow` and the whole object to
|
|
76
|
+
* `wnd.getContext('opengl', config)`.
|
|
77
|
+
* @param {object} [spec] GLX attributes, e.g.
|
|
78
|
+
* `{ DEPTH_SIZE: 24, DOUBLEBUFFER: true, ALPHA_SIZE: 8 }`
|
|
79
|
+
*/
|
|
80
|
+
chooseGLXConfig(spec) {
|
|
81
|
+
return chooseGLXConfig(this, spec);
|
|
82
|
+
}
|
|
83
|
+
|
|
59
84
|
rootWindow() {
|
|
60
85
|
return new Window(this, { id: this.display.screen[0].root });
|
|
61
86
|
}
|
package/lib/glx.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// GLX visual/fbconfig discovery.
|
|
2
|
+
//
|
|
3
|
+
// A GLX drawable has to use a visual the GL context was created for, so the
|
|
4
|
+
// visual must be known *before* CreateWindow. This module asks the server
|
|
5
|
+
// (GetFBConfigs, falling back to the GLX 1.2 GetVisualConfigs) instead of
|
|
6
|
+
// shelling out to `glxinfo`, so it works headlessly and in CI.
|
|
7
|
+
|
|
8
|
+
// defaults applied on top of the caller's spec; `null` means "don't care"
|
|
9
|
+
const DEFAULT_SPEC = {
|
|
10
|
+
DOUBLEBUFFER: true,
|
|
11
|
+
DEPTH_SIZE: 16,
|
|
12
|
+
RED_SIZE: 8,
|
|
13
|
+
GREEN_SIZE: 8,
|
|
14
|
+
BLUE_SIZE: 8
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// GLX 1.2 GetVisualConfigs property names for the attributes we filter on
|
|
18
|
+
const legacyProps = {
|
|
19
|
+
DOUBLEBUFFER: 'doubleBufferMode',
|
|
20
|
+
STEREO: 'stereoMode',
|
|
21
|
+
BUFFER_SIZE: 'rgbBits',
|
|
22
|
+
DEPTH_SIZE: 'depthBits',
|
|
23
|
+
STENCIL_SIZE: 'stencilBits',
|
|
24
|
+
RED_SIZE: 'redBits',
|
|
25
|
+
GREEN_SIZE: 'greenBits',
|
|
26
|
+
BLUE_SIZE: 'blueBits',
|
|
27
|
+
ALPHA_SIZE: 'alphaBits',
|
|
28
|
+
AUX_BUFFERS: 'numAuxBuffers',
|
|
29
|
+
LEVEL: 'level'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// attributes compared as "at least this much"; the rest must match exactly
|
|
33
|
+
const minimums = new Set([
|
|
34
|
+
'BUFFER_SIZE',
|
|
35
|
+
'DEPTH_SIZE',
|
|
36
|
+
'STENCIL_SIZE',
|
|
37
|
+
'RED_SIZE',
|
|
38
|
+
'GREEN_SIZE',
|
|
39
|
+
'BLUE_SIZE',
|
|
40
|
+
'ALPHA_SIZE',
|
|
41
|
+
'AUX_BUFFERS',
|
|
42
|
+
'SAMPLE_BUFFERS',
|
|
43
|
+
'SAMPLES'
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
function toNumber(value) {
|
|
47
|
+
if (typeof value === 'boolean') return value ? 1 : 0;
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// the X depth and visual info for a visual id, or null if the screen has no
|
|
52
|
+
// such visual (fbconfigs with VISUAL_ID 0 are pbuffer/pixmap only)
|
|
53
|
+
function visualInfo(screen, visualId) {
|
|
54
|
+
if (!visualId) return null;
|
|
55
|
+
for (const depth in screen.depths) {
|
|
56
|
+
const visual = screen.depths[depth][visualId];
|
|
57
|
+
if (visual) return { depth: Number(depth), visual };
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function describe(spec) {
|
|
63
|
+
return (
|
|
64
|
+
Object.entries(spec)
|
|
65
|
+
.filter(([, v]) => v !== null && v !== undefined)
|
|
66
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
67
|
+
.join(' ') || '(no constraints)'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function fbConfigs(GLX, screen, spec) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
// ChooseFBConfig filters and sorts per the GLX 1.4 selection rules
|
|
74
|
+
GLX.ChooseFBConfig(screen, spec, (err, configs) => {
|
|
75
|
+
// a bad attribute name is the caller's bug, not a missing feature —
|
|
76
|
+
// everything else means "this server has no fbconfigs for us"
|
|
77
|
+
if (err instanceof TypeError) return reject(err);
|
|
78
|
+
resolve(err ? [] : configs);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function visualConfigs(GLX, screen) {
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
GLX.GetVisualConfigs(screen, (err, configs) => resolve(err ? [] : configs));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// GLX 1.2 fallback: filter GetVisualConfigs results by the same spec
|
|
90
|
+
function matchLegacy(configs, spec) {
|
|
91
|
+
return configs.filter((cfg) => {
|
|
92
|
+
if (!cfg.rgbMode) return false;
|
|
93
|
+
for (const key in spec) {
|
|
94
|
+
const want = toNumber(spec[key]);
|
|
95
|
+
if (want === null || want === undefined) continue;
|
|
96
|
+
const prop = legacyProps[key];
|
|
97
|
+
// attributes GetVisualConfigs doesn't report (RENDER_TYPE,
|
|
98
|
+
// DRAWABLE_TYPE, ...) can't be checked — RGBA windows are all this
|
|
99
|
+
// path can offer anyway
|
|
100
|
+
if (!prop) continue;
|
|
101
|
+
const have = cfg[prop] || 0;
|
|
102
|
+
if (minimums.has(key) ? have < want : have !== want) return false;
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Choose a GLX-capable visual on `app`'s display.
|
|
110
|
+
*
|
|
111
|
+
* @param {import('./app.js').default} app
|
|
112
|
+
* @param {object} [spec] GLX attribute constraints by name (see
|
|
113
|
+
* `GLX.glxAttrib`), e.g. `{ DEPTH_SIZE: 24, DOUBLEBUFFER: true }`;
|
|
114
|
+
* `null` means "don't care". `screen` picks the X screen (default 0),
|
|
115
|
+
* `visual` pins a specific visual id and skips the search.
|
|
116
|
+
* @returns {Promise<{visual: number, depth: number, class: number,
|
|
117
|
+
* doubleBuffer: boolean, depthSize: number, fbconfig: number|null,
|
|
118
|
+
* config: object}>}
|
|
119
|
+
*/
|
|
120
|
+
export async function chooseGLXConfig(app, spec = {}) {
|
|
121
|
+
const GLX = app.display.GLX;
|
|
122
|
+
if (!GLX) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
'GLX: extension not available on this display — indirect GLX is often disabled (X server flag +iglx / AllowIndirectGLX)'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { screen: screenNum = 0, preferVisual, ...wanted } = spec;
|
|
129
|
+
const want = { ...DEFAULT_SPEC, ...wanted };
|
|
130
|
+
const screen = app.display.screen[screenNum];
|
|
131
|
+
// a candidate list is searched for `preferVisual` first: a context must
|
|
132
|
+
// be created for the visual its drawable already has
|
|
133
|
+
const pick = (candidates) =>
|
|
134
|
+
(preferVisual && candidates.find((c) => c.visual === preferVisual)) || candidates[0];
|
|
135
|
+
|
|
136
|
+
if (want.visual) {
|
|
137
|
+
const info = visualInfo(screen, want.visual);
|
|
138
|
+
if (!info) throw new Error(`GLX: visual 0x${want.visual.toString(16)} is not on screen ${screenNum}`);
|
|
139
|
+
return {
|
|
140
|
+
visual: want.visual,
|
|
141
|
+
depth: info.depth,
|
|
142
|
+
class: info.visual.class,
|
|
143
|
+
doubleBuffer: !!toNumber(want.DOUBLEBUFFER),
|
|
144
|
+
depthSize: toNumber(want.DEPTH_SIZE) || 0,
|
|
145
|
+
screen: screenNum,
|
|
146
|
+
fbconfig: null,
|
|
147
|
+
config: {}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// GLX >= 1.3 fbconfigs first; they carry the attributes we care about
|
|
152
|
+
const fromFbConfigs = [];
|
|
153
|
+
for (const cfg of await fbConfigs(GLX, screenNum, want)) {
|
|
154
|
+
const info = visualInfo(screen, cfg.VISUAL_ID);
|
|
155
|
+
if (!info) continue; // no X visual: pbuffer/pixmap-only config
|
|
156
|
+
fromFbConfigs.push({
|
|
157
|
+
visual: cfg.VISUAL_ID,
|
|
158
|
+
depth: info.depth,
|
|
159
|
+
class: info.visual.class,
|
|
160
|
+
doubleBuffer: !!cfg.DOUBLEBUFFER,
|
|
161
|
+
depthSize: cfg.DEPTH_SIZE || 0,
|
|
162
|
+
screen: screenNum,
|
|
163
|
+
fbconfig: cfg.FBCONFIG_ID,
|
|
164
|
+
config: cfg
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const best = pick(fromFbConfigs);
|
|
168
|
+
if (best) return best;
|
|
169
|
+
|
|
170
|
+
// GLX 1.2 servers (and servers that report no usable fbconfig)
|
|
171
|
+
const legacy = await visualConfigs(GLX, screenNum);
|
|
172
|
+
const fromVisuals = [];
|
|
173
|
+
for (const cfg of matchLegacy(legacy, want)) {
|
|
174
|
+
const info = visualInfo(screen, cfg.visualID);
|
|
175
|
+
if (!info) continue;
|
|
176
|
+
fromVisuals.push({
|
|
177
|
+
visual: cfg.visualID,
|
|
178
|
+
depth: info.depth,
|
|
179
|
+
class: info.visual.class,
|
|
180
|
+
doubleBuffer: !!cfg.doubleBufferMode,
|
|
181
|
+
depthSize: cfg.depthBits || 0,
|
|
182
|
+
screen: screenNum,
|
|
183
|
+
fbconfig: null,
|
|
184
|
+
config: cfg
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const legacyBest = pick(fromVisuals);
|
|
188
|
+
if (legacyBest) return legacyBest;
|
|
189
|
+
|
|
190
|
+
throw new Error(
|
|
191
|
+
`GLX: no visual on screen ${screenNum} matches ${describe(want)} (server offered ${legacy.length} GLX visuals)`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export default chooseGLXConfig;
|
package/lib/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import SvgView from './widgets/svgview.js';
|
|
|
22
22
|
import MarkdownView from './widgets/markdownview.js';
|
|
23
23
|
import TexView, { configureTex, layoutTex, TexBox } from './widgets/tex.js';
|
|
24
24
|
import { tokenize as highlightCode } from './widgets/highlight.js';
|
|
25
|
+
import { cssColor, cssLength } from './widgets/css.js';
|
|
25
26
|
|
|
26
27
|
// rendering context modules register themselves on Drawable
|
|
27
28
|
import './renderingcontext_x11.js';
|
|
@@ -40,8 +41,8 @@ import './renderingcontext_opengl.js';
|
|
|
40
41
|
* @param {object} [options] passed through to x11.createClient
|
|
41
42
|
* (e.g. { display: ':1' }); ntk additionally understands
|
|
42
43
|
* { fontSource } — a pluggable system-font lookup (see docs/fonts.md) —
|
|
43
|
-
* { glxVisual } —
|
|
44
|
-
*
|
|
44
|
+
* { glxVisual } — a visual id for getContext('opengl') to use instead of
|
|
45
|
+
* querying the server for one — and
|
|
45
46
|
* { onXError } — called with X protocol errors no request callback
|
|
46
47
|
* claimed (default: console.warn)
|
|
47
48
|
* @param {function} [callback] optional node-style callback
|
|
@@ -126,7 +127,9 @@ export {
|
|
|
126
127
|
TexBox,
|
|
127
128
|
layoutTex,
|
|
128
129
|
configureTex,
|
|
129
|
-
highlightCode
|
|
130
|
+
highlightCode,
|
|
131
|
+
cssColor,
|
|
132
|
+
cssLength
|
|
130
133
|
};
|
|
131
134
|
// the yoga-layout instance ntk lays HtmlView out with — downstream layout
|
|
132
135
|
// consumers (e.g. the react-x11 renderer) must share it to avoid loading a
|
|
@@ -552,6 +552,34 @@ class RenderingContext2d {
|
|
|
552
552
|
* Core fill: rasterize device-space polys into the scratch a8 mask,
|
|
553
553
|
* scale by globalAlpha, intersect with the clip, composite the source.
|
|
554
554
|
*/
|
|
555
|
+
/**
|
|
556
|
+
* Device-space bounding box of flattened polys, with a pixel of slack for
|
|
557
|
+
* the antialiased edge, clamped to the surface. Null when nothing lands
|
|
558
|
+
* on it.
|
|
559
|
+
*/
|
|
560
|
+
_polysBBox(polys) {
|
|
561
|
+
let minX = Infinity;
|
|
562
|
+
let minY = Infinity;
|
|
563
|
+
let maxX = -Infinity;
|
|
564
|
+
let maxY = -Infinity;
|
|
565
|
+
for (const poly of polys) {
|
|
566
|
+
const pts = poly.pts;
|
|
567
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
568
|
+
if (pts[i] < minX) minX = pts[i];
|
|
569
|
+
if (pts[i] > maxX) maxX = pts[i];
|
|
570
|
+
if (pts[i + 1] < minY) minY = pts[i + 1];
|
|
571
|
+
if (pts[i + 1] > maxY) maxY = pts[i + 1];
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (maxX === -Infinity) return null;
|
|
575
|
+
const x = Math.max(0, Math.floor(minX) - 1);
|
|
576
|
+
const y = Math.max(0, Math.floor(minY) - 1);
|
|
577
|
+
const w = Math.min(this.width, Math.ceil(maxX) + 1) - x;
|
|
578
|
+
const h = Math.min(this.height, Math.ceil(maxY) + 1) - y;
|
|
579
|
+
if (w <= 0 || h <= 0) return null;
|
|
580
|
+
return { x, y, w, h };
|
|
581
|
+
}
|
|
582
|
+
|
|
555
583
|
_fillPolys(polys, rule, { src = null, op = null, alpha = null } = {}) {
|
|
556
584
|
if (!polys.length) return;
|
|
557
585
|
src = src ?? this._backgroundPicture;
|
|
@@ -559,26 +587,31 @@ class RenderingContext2d {
|
|
|
559
587
|
alpha = alpha ?? this.globalAlpha;
|
|
560
588
|
if (alpha <= 0) return;
|
|
561
589
|
|
|
590
|
+
// Everything below is bounded to the shape's bounding box rather than
|
|
591
|
+
// the whole surface. On the wire it makes no difference — a Composite
|
|
592
|
+
// request is the same size either way — but it is the difference
|
|
593
|
+
// between the server touching a 34x34 box and a 400x400 one per fill.
|
|
594
|
+
// Stale mask content outside the box is never composited, so clearing
|
|
595
|
+
// only the box is safe.
|
|
596
|
+
const b = this._polysBBox(polys);
|
|
597
|
+
if (!b) return;
|
|
598
|
+
const R = this.Render;
|
|
599
|
+
|
|
562
600
|
this._ensureFillMask();
|
|
563
|
-
|
|
601
|
+
R.FillRectangles(R.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], [b.x, b.y, b.w, b.h]);
|
|
564
602
|
this._rasterizePolys(this.fillMask, polys, rule);
|
|
565
603
|
|
|
566
604
|
if (alpha < 1) {
|
|
567
605
|
// In with a constant color scales the a8 coverage by that alpha
|
|
568
|
-
|
|
606
|
+
R.FillRectangles(R.PictOp.In, this.fillMask.id, [0, 0, 0, alpha], [b.x, b.y, b.w, b.h]);
|
|
569
607
|
}
|
|
570
608
|
if (this.clipMask) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
this.clipMask.id,
|
|
574
|
-
0,
|
|
575
|
-
this.fillMask.id,
|
|
576
|
-
0, 0, 0, 0, 0, 0,
|
|
577
|
-
this.width,
|
|
578
|
-
this.height
|
|
579
|
-
);
|
|
609
|
+
// clipMask is surface-aligned, so it is sampled at the same offset
|
|
610
|
+
R.Composite(R.PictOp.In, this.clipMask.id, 0, this.fillMask.id, b.x, b.y, 0, 0, b.x, b.y, b.w, b.h);
|
|
580
611
|
}
|
|
581
|
-
|
|
612
|
+
// src is either a 1x1 repeating solid (offset irrelevant) or a
|
|
613
|
+
// surface-aligned gradient, so it is sampled at the same offset too
|
|
614
|
+
R.Composite(op, src.id, this.fillMask.id, this.picture.id, b.x, b.y, b.x, b.y, b.x, b.y, b.w, b.h);
|
|
582
615
|
this._markDirty();
|
|
583
616
|
}
|
|
584
617
|
|
|
@@ -1,53 +1,143 @@
|
|
|
1
1
|
import Drawable from './drawable.js';
|
|
2
|
+
import { chooseGLXConfig } from './glx.js';
|
|
2
3
|
|
|
3
4
|
// Indirect GLX rendering context exposing an OpenGL 1.4-style api.
|
|
4
5
|
// Note: many modern X servers ship with indirect GLX disabled — see README.
|
|
6
|
+
//
|
|
7
|
+
// Setting up a context is asynchronous by protocol: the visual has to be
|
|
8
|
+
// discovered (GetFBConfigs) and, above all, GLX Render requests carry the
|
|
9
|
+
// *context tag* the server returns from MakeCurrent — not the context XID.
|
|
10
|
+
// The api stays synchronous by queueing GL calls until the tag arrives and
|
|
11
|
+
// replaying them in order; `await gl.ready` waits for the setup instead.
|
|
5
12
|
class RenderingContextOpenGL {
|
|
6
|
-
constructor(window,
|
|
7
|
-
// TODO: use glx api to query the best visual instead of shelling out
|
|
8
|
-
if (typeof visual === 'undefined') visual = window.app?.options?.glxVisual;
|
|
9
|
-
if (typeof visual === 'undefined') {
|
|
10
|
-
// lazy builtin lookup: browser bundles must not depend on child_process
|
|
11
|
-
const cp = globalThis.process?.getBuiltinModule?.('node:child_process');
|
|
12
|
-
if (!cp) {
|
|
13
|
-
throw new Error(
|
|
14
|
-
"getContext('opengl'): no visual id — pass one explicitly or via createClient({ glxVisual }) when glxinfo cannot be run"
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
visual = parseInt(cp.execSync('glxinfo -i -b').toString(), 10);
|
|
18
|
-
}
|
|
19
|
-
|
|
13
|
+
constructor(window, config) {
|
|
20
14
|
const X = window.X;
|
|
21
15
|
const GLX = window.display.GLX;
|
|
22
|
-
|
|
16
|
+
if (!GLX) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
"getContext('opengl'): the X server has no GLX extension — indirect GLX is often disabled (+iglx / AllowIndirectGLX)"
|
|
19
|
+
);
|
|
20
|
+
}
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
GLX
|
|
22
|
+
this.window = window;
|
|
23
|
+
this.X = X;
|
|
24
|
+
this.GLX = GLX;
|
|
25
|
+
/** the MakeCurrent context tag every Render request carries; 0 until ready */
|
|
26
|
+
this.contextTag = 0;
|
|
27
|
+
/** the GLX context XID */
|
|
28
|
+
this.contextId = 0;
|
|
29
|
+
/** the config chosen (or passed in), see app.chooseGLXConfig */
|
|
30
|
+
this.config = null;
|
|
31
|
+
this.visual = 0;
|
|
32
|
+
this.error = null;
|
|
27
33
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
// GL commands land here until the tag is known, then in the real
|
|
35
|
+
// pipeline. renderPipeline() issues no traffic, so a throwaway one is a
|
|
36
|
+
// safe source of the method names and GL constants.
|
|
37
|
+
let pipeline = null;
|
|
38
|
+
const queue = [];
|
|
39
|
+
const template = GLX.renderPipeline(0);
|
|
40
|
+
for (const key in template) {
|
|
41
|
+
if (typeof template[key] !== 'function') {
|
|
42
|
+
this[key] = template[key];
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
this[key] = (...args) => {
|
|
46
|
+
if (pipeline) return pipeline[key](...args);
|
|
47
|
+
queue.push([key, args]);
|
|
48
|
+
};
|
|
33
49
|
}
|
|
34
50
|
|
|
35
|
-
|
|
36
|
-
|
|
51
|
+
// SwapBuffers swaps the owning window
|
|
52
|
+
const swapBuffers = this.SwapBuffers;
|
|
53
|
+
this.SwapBuffers = () => swapBuffers(window.id);
|
|
54
|
+
|
|
55
|
+
const afterReady = (fn) => {
|
|
56
|
+
if (this.contextTag) return fn();
|
|
57
|
+
this.ready.then(fn, () => {});
|
|
58
|
+
};
|
|
37
59
|
|
|
38
60
|
this.CreateGLXPixmap = (pixmapId) => {
|
|
39
61
|
const glxpixmapId = X.AllocID();
|
|
40
|
-
GLX.CreateGLXPixmap(0, visual, pixmapId, glxpixmapId);
|
|
62
|
+
afterReady(() => GLX.CreateGLXPixmap(0, this.visual, pixmapId, glxpixmapId));
|
|
41
63
|
return glxpixmapId;
|
|
42
64
|
};
|
|
43
|
-
this.BindTexImage = (pixmap, buffer) => {
|
|
44
|
-
GLX.BindTexImage(
|
|
65
|
+
this.BindTexImage = (pixmap, buffer, attribs) => {
|
|
66
|
+
afterReady(() => GLX.BindTexImage(this.contextTag, pixmap, buffer, attribs));
|
|
67
|
+
};
|
|
68
|
+
this.ReleaseTexImage = (pixmap, buffer) => {
|
|
69
|
+
afterReady(() => GLX.ReleaseTexImage(this.contextTag, pixmap, buffer));
|
|
45
70
|
};
|
|
71
|
+
|
|
72
|
+
/** resolves with this context once it is current, rejects on failure */
|
|
73
|
+
this.ready = (async () => {
|
|
74
|
+
const cfg = await this._resolveConfig(config);
|
|
75
|
+
this.config = cfg;
|
|
76
|
+
this.visual = cfg.visual;
|
|
77
|
+
if (window.visual && window.visual !== cfg.visual) {
|
|
78
|
+
console.warn(
|
|
79
|
+
`ntk: getContext('opengl') is using visual 0x${cfg.visual.toString(16)} on a window created with visual 0x${window.visual.toString(16)} — create the window with the visual from app.chooseGLXConfig()`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ctx = X.AllocID();
|
|
84
|
+
this.contextId = ctx;
|
|
85
|
+
GLX.CreateContext(ctx, cfg.visual, cfg.screen ?? 0, 0, 0);
|
|
86
|
+
const tag = await new Promise((resolve, reject) => {
|
|
87
|
+
GLX.MakeCurrent(window.id, ctx, 0, (err, contextTag) => (err ? reject(err) : resolve(contextTag)));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
this.contextTag = tag;
|
|
91
|
+
pipeline = GLX.renderPipeline(tag);
|
|
92
|
+
for (const [key, args] of queue) pipeline[key](...args);
|
|
93
|
+
queue.length = 0;
|
|
94
|
+
return this;
|
|
95
|
+
})();
|
|
96
|
+
|
|
97
|
+
this.ready.catch((err) => {
|
|
98
|
+
this.error = err;
|
|
99
|
+
queue.length = 0;
|
|
100
|
+
console.warn(
|
|
101
|
+
`ntk: getContext('opengl') failed: ${err.message} (await gl.ready to handle this yourself)`
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// an explicit config object or visual id wins; otherwise ask the server
|
|
107
|
+
// for one, preferring a config for the visual the window already has
|
|
108
|
+
async _resolveConfig(config) {
|
|
109
|
+
if (config && typeof config === 'object') {
|
|
110
|
+
if (!config.visual)
|
|
111
|
+
throw new Error("getContext('opengl'): config has no visual — use app.chooseGLXConfig()");
|
|
112
|
+
return config;
|
|
113
|
+
}
|
|
114
|
+
const app = this.window.app;
|
|
115
|
+
const visual = config ?? app?.options?.glxVisual;
|
|
116
|
+
if (visual) return chooseGLXConfig(app, { visual });
|
|
117
|
+
const screen = app.display.screen[0];
|
|
118
|
+
return chooseGLXConfig(app, {
|
|
119
|
+
preferVisual: this.window.visual || screen.root_visual
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** release the context (the window keeps its drawable) */
|
|
124
|
+
destroy() {
|
|
125
|
+
if (!this.contextId) return;
|
|
126
|
+
const { GLX } = this;
|
|
127
|
+
const ctx = this.contextId;
|
|
128
|
+
const tag = this.contextTag;
|
|
129
|
+
this.contextId = 0;
|
|
130
|
+
this.contextTag = 0;
|
|
131
|
+
if (tag) GLX.MakeCurrent(0, 0, tag, () => {});
|
|
132
|
+
GLX.DestroyContext(ctx);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
[Symbol.dispose]() {
|
|
136
|
+
this.destroy();
|
|
46
137
|
}
|
|
47
138
|
}
|
|
48
139
|
|
|
49
140
|
// register context
|
|
50
|
-
Drawable.renderingContextFactory['opengl'] = (window,
|
|
51
|
-
new RenderingContextOpenGL(window, visual);
|
|
141
|
+
Drawable.renderingContextFactory['opengl'] = (window, config) => new RenderingContextOpenGL(window, config);
|
|
52
142
|
|
|
53
143
|
export default RenderingContextOpenGL;
|
package/lib/window.js
CHANGED
|
@@ -15,6 +15,8 @@ import * as xevents from './events_map.js';
|
|
|
15
15
|
// client-side double buffering (see docs/window.md) and predates this
|
|
16
16
|
// list — it is a different concept from the X backing-store attribute,
|
|
17
17
|
// which is therefore not forwarded to avoid a silent collision
|
|
18
|
+
// `visual`, `depth`, `windowClass` and `borderWidth` are CreateWindow header
|
|
19
|
+
// fields rather than attributes, and are handled separately below
|
|
18
20
|
const forwardedXAttributes = [
|
|
19
21
|
'backgroundPixmap',
|
|
20
22
|
'backgroundPixel',
|
|
@@ -113,10 +115,29 @@ export default class Window extends Drawable {
|
|
|
113
115
|
for (const name of forwardedXAttributes) {
|
|
114
116
|
if (args[name] !== undefined) values[name] = args[name];
|
|
115
117
|
}
|
|
116
|
-
|
|
118
|
+
// a window on a non-default visual (GLX, ARGB) also needs its own
|
|
119
|
+
// colormap and an explicit border pixel: inheriting either from a
|
|
120
|
+
// parent of a different depth is a BadMatch (X11 CreateWindow)
|
|
121
|
+
this.visual = args.visual ?? 0;
|
|
122
|
+
this.depth = args.depth ?? 0;
|
|
123
|
+
this.windowClass = args.windowClass ?? 0;
|
|
124
|
+
if (this.visual) {
|
|
125
|
+
if (values.colormap === undefined) {
|
|
126
|
+
this._ownedColormap = app.createColormap(this.visual);
|
|
127
|
+
values.colormap = this._ownedColormap;
|
|
128
|
+
}
|
|
129
|
+
if (values.borderPixel === undefined && values.borderPixmap === undefined) values.borderPixel = 0;
|
|
130
|
+
}
|
|
131
|
+
const borderWidth = args.borderWidth ?? 0;
|
|
132
|
+
X.CreateWindow(
|
|
133
|
+
this.id, parentId, this.x, this.y, this.width, this.height,
|
|
134
|
+
borderWidth, this.depth, this.windowClass, this.visual, values
|
|
135
|
+
);
|
|
117
136
|
} else {
|
|
118
137
|
this.id = args.id;
|
|
119
138
|
this.eventMask = 0;
|
|
139
|
+
this.visual = 0;
|
|
140
|
+
this.depth = 0;
|
|
120
141
|
// populate width and height
|
|
121
142
|
X.GetGeometry(this.id, (err, res) => {
|
|
122
143
|
if (!err) {
|
|
@@ -124,6 +145,7 @@ export default class Window extends Drawable {
|
|
|
124
145
|
this.height = res.height;
|
|
125
146
|
this.x = res.xPos;
|
|
126
147
|
this.y = res.yPos;
|
|
148
|
+
this.depth = res.depth;
|
|
127
149
|
}
|
|
128
150
|
this._readyPromiseResolve();
|
|
129
151
|
});
|
|
@@ -293,7 +315,9 @@ export default class Window extends Drawable {
|
|
|
293
315
|
parent: this,
|
|
294
316
|
width: newW,
|
|
295
317
|
height: newH,
|
|
296
|
-
depth:
|
|
318
|
+
// must match the window's depth: CopyArea between drawables of
|
|
319
|
+
// different depths is a BadMatch
|
|
320
|
+
depth: this.depth || this.display.screen[0].root_depth
|
|
297
321
|
});
|
|
298
322
|
if (!this._clearGc) {
|
|
299
323
|
// reusable across reallocs: a GC is valid for any drawable of the
|
|
@@ -974,6 +998,11 @@ export default class Window extends Drawable {
|
|
|
974
998
|
this._backing = null;
|
|
975
999
|
}
|
|
976
1000
|
safeRelease(this.X, () => this.X.DestroyWindow(this.id));
|
|
1001
|
+
if (this._ownedColormap) {
|
|
1002
|
+
const colormap = this._ownedColormap;
|
|
1003
|
+
this._ownedColormap = null;
|
|
1004
|
+
safeRelease(this.X, () => this.X.FreeColormap(colormap));
|
|
1005
|
+
}
|
|
977
1006
|
}
|
|
978
1007
|
|
|
979
1008
|
[Symbol.dispose]() {
|