ntk 8.23.4 → 8.24.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/lib/app.js CHANGED
@@ -614,6 +614,21 @@ export default class App {
614
614
  return this._extension('composite');
615
615
  }
616
616
 
617
+ /**
618
+ * The XVideo extension, or `null` where the server has none: adaptors that
619
+ * convert a YCbCr frame — and scale it — on the GPU. `openVideoPort` in
620
+ * lib/xv.js is the way in (docs/xv.md); this is the bare extension for a
621
+ * caller that wants the requests themselves.
622
+ *
623
+ * Present is not usable: XQuartz and Xvfb both answer the extension and
624
+ * then "no adaptors present", which is why `openVideoPort` asks further.
625
+ *
626
+ * @returns {Promise<object|null>}
627
+ */
628
+ xv() {
629
+ return this._extension('xv');
630
+ }
631
+
617
632
  /**
618
633
  * The DAMAGE extension, or `null` where the server has none: what turns
619
634
  * "this drawable changed" into an event, so a compositor repaints the
@@ -1211,6 +1226,11 @@ export default class App {
1211
1226
  this._cursors.dispose();
1212
1227
  this._cursors = null;
1213
1228
  }
1229
+ // ungrabbed and their segments detached before the uploader's go
1230
+ // (lib/xv.js); each one leaves the set as it closes
1231
+ if (this._videoPorts) {
1232
+ for (const port of [...this._videoPorts]) port.close();
1233
+ }
1214
1234
  if (this._shm) {
1215
1235
  this._shm.dispose();
1216
1236
  this._shm = undefined;
package/lib/index.js CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  unpackIcons
31
31
  } from './imagedata.js';
32
32
  import { Surface } from './surface.js';
33
+ import { VIDEO_FOURCC, VideoPort, VideoTarget, openVideoPort } from './xv.js';
33
34
  import { Path2D, parseSvgPath } from './path.js';
34
35
  import Font from './text/font.js';
35
36
  import FontManager from './text/fontmanager.js';
@@ -261,6 +262,11 @@ export {
261
262
  // app.createRegion(rects) makes one, ctx.clipRegion(region) clips to it
262
263
  Region,
263
264
  Surface,
265
+ // XVideo (docs/xv.md): also available as `ntk/xv`
266
+ openVideoPort,
267
+ VideoPort,
268
+ VideoTarget,
269
+ VIDEO_FOURCC,
264
270
  // fill/stroke styles the 2d context hands back (docs/context-2d.md) —
265
271
  // exported for `instanceof`, not to be constructed directly
266
272
  CanvasGradient,
package/lib/xv.js ADDED
@@ -0,0 +1,604 @@
1
+ // XVideo: a YCbCr frame handed to the server, which converts it — and can
2
+ // scale it — on the GPU (docs/xv.md).
3
+ //
4
+ // A thin layer over node-x11's Xv requests (x11 >= 4.2 has the image half:
5
+ // QueryImageAttributes, PutImage, ShmPutImage, StopVideo), with what every
6
+ // caller would otherwise write again:
7
+ //
8
+ // - **a port**, chosen and grabbed once: the first image adaptor that draws
9
+ // at the depth asked for, and the first port on it no other client holds;
10
+ // - **the formats ntk can lay out**, by name — `'NV12'`, `'I420'`, `'YV12'`,
11
+ // `'UYVY'`, `'YUY2'`, `'AR24'` — and the layout the port wants each in at a
12
+ // size, `QueryImageAttributes` asked once per size and kept;
13
+ // - **the transport**: a frame goes through a shared-memory segment where
14
+ // MIT-SHM attaches, and down the socket as `PutImage` where it does not and
15
+ // the frame fits one request;
16
+ // - **a target**, because a pixmap is not one. Measured on Xwayland's glamor
17
+ // adaptor: `PutImage` into a depth-24 pixmap is `BadWindow`, into a depth-32
18
+ // one `BadMatch` — the textured adaptors take windows only. A window draws
19
+ // on screen and takes the pointer, which is what a video in a layout must
20
+ // not do, so `VideoTarget` is a child window that is **manually
21
+ // redirected** and placed outside its parent: Composite gives it a pixmap
22
+ // of its own that nothing shows, the adaptor draws into that, and RENDER
23
+ // reads it back out wherever the caller composites it, in paint order.
24
+ //
25
+ // And a clean decline: `openVideoPort` answers `null` where there is no
26
+ // XVideo, no image adaptor at the depth, no port free, or an x11 too old for
27
+ // the image requests — XQuartz and Xvfb advertise the extension and answer
28
+ // "no adaptors present", so a caller always has a path without this one.
29
+ //
30
+ // Ordering: every request here is a void request issued in line with the
31
+ // caller's drawing. A frame through shared memory uses only a segment that is
32
+ // already attached (the discipline of lib/shm-upload.js), so a put lands
33
+ // exactly where the caller made it, and a composite issued after it reads the
34
+ // new frame.
35
+
36
+ import Picture, { OWN_PICTURE } from './picture.js';
37
+ import { safeRelease } from './cleanup.js';
38
+
39
+ /** The formats ntk lays out, by the FOURCC each is known by. */
40
+ export const VIDEO_FOURCC = Object.freeze({
41
+ I420: 0x30323449,
42
+ YV12: 0x32315659,
43
+ NV12: 0x3231564e,
44
+ UYVY: 0x59565955,
45
+ YUY2: 0x32595559,
46
+ AR24: 0x34325241
47
+ });
48
+
49
+ const NAME_OF = new Map(Object.entries(VIDEO_FOURCC).map(([name, id]) => [id, name]));
50
+
51
+ // Segments a port keeps for frames in flight: one the server is reading, one
52
+ // being written, and one spare for a frame pushed before the first came back.
53
+ const MAX_SEGMENTS = 3;
54
+ const SEGMENT_QUANTUM = 64 * 1024;
55
+
56
+ const roundUp = (n) => Math.max(SEGMENT_QUANTUM, Math.ceil(n / SEGMENT_QUANTUM) * SEGMENT_QUANTUM);
57
+
58
+ /**
59
+ * The planes of a `format` frame at `width` x `height`, in the format's own
60
+ * order: the bytes of picture a row of each holds, and how many rows. Chroma
61
+ * subsampled by two is half the size, rounded up; a 4:2:2 packed row is a
62
+ * whole number of pixel pairs.
63
+ */
64
+ export function videoPlanes(format, width, height) {
65
+ const cw = Math.ceil(width / 2);
66
+ const ch = Math.ceil(height / 2);
67
+ switch (format) {
68
+ case 'I420':
69
+ case 'YV12':
70
+ return [
71
+ { rowBytes: width, rows: height },
72
+ { rowBytes: cw, rows: ch },
73
+ { rowBytes: cw, rows: ch }
74
+ ];
75
+ case 'NV12':
76
+ return [
77
+ { rowBytes: width, rows: height },
78
+ { rowBytes: cw * 2, rows: ch }
79
+ ];
80
+ case 'UYVY':
81
+ case 'YUY2':
82
+ return [{ rowBytes: cw * 4, rows: height }];
83
+ case 'AR24':
84
+ return [{ rowBytes: width * 4, rows: height }];
85
+ }
86
+ throw new TypeError(`ntk: Xv format ${JSON.stringify(format)} is not one ntk lays out`);
87
+ }
88
+
89
+ /**
90
+ * Copy a frame's planes into `out` at the port's layout: plane `i` at
91
+ * `layout.offsets[i]`, `layout.pitches[i]` bytes a row. `planes` are in the
92
+ * format's own order, `strides` their bytes per row (packed when left out).
93
+ * A plane whose rows already sit at the port's pitch goes in one copy.
94
+ */
95
+ export function packVideoFrame(format, width, height, planes, strides, layout, out) {
96
+ const geometry = videoPlanes(format, width, height);
97
+ for (let i = 0; i < geometry.length; i++) {
98
+ const { rowBytes, rows } = geometry[i];
99
+ const src = planes[i];
100
+ const stride = strides?.[i] ?? rowBytes;
101
+ const pitch = layout.pitches[i];
102
+ const at = layout.offsets[i];
103
+ if (stride === pitch) {
104
+ const length = pitch * (rows - 1) + rowBytes;
105
+ out.set(src.subarray(0, length), at);
106
+ continue;
107
+ }
108
+ for (let row = 0; row < rows; row++) {
109
+ const from = row * stride;
110
+ out.set(src.subarray(from, from + rowBytes), at + row * pitch);
111
+ }
112
+ }
113
+ return out;
114
+ }
115
+
116
+ const call = (fn, ...args) =>
117
+ new Promise((resolve, reject) => fn(...args, (err, value) => (err ? reject(err) : resolve(value))));
118
+
119
+ /**
120
+ * Open a port that draws frames at `depth` (24, the depth every textured
121
+ * adaptor measured offers), or `null` where this server has none to give.
122
+ *
123
+ * Grabs the port: Xv lets any client put through an ungrabbed one, but an
124
+ * attribute another client sets — the colour space — would then change our
125
+ * frames under us. Release it with `close()`; the server releases it with
126
+ * the connection.
127
+ *
128
+ * @param {import('./app.js').default} app
129
+ * @param {{ depth?: number }} [options]
130
+ * @returns {Promise<VideoPort|null>}
131
+ */
132
+ export async function openVideoPort(app, { depth = 24 } = {}) {
133
+ const Xv = await app.xv();
134
+ // x11 < 4.2 answers the extension with the query half only
135
+ if (!Xv || typeof Xv.PutImage !== 'function' || typeof Xv.QueryImageAttributes !== 'function') {
136
+ return null;
137
+ }
138
+ const screen = app.display.screen[0];
139
+ let adaptors;
140
+ try {
141
+ adaptors = await call(Xv.QueryAdaptors, screen.root);
142
+ } catch {
143
+ return null;
144
+ }
145
+ const IMAGE = Xv.Type.ImageMask | Xv.Type.InputMask;
146
+ for (const adaptor of adaptors) {
147
+ if ((adaptor.type & IMAGE) !== IMAGE) continue;
148
+ const drawsAt = adaptor.formats.find((f) => f.depth === depth);
149
+ if (!drawsAt) continue;
150
+ for (let port = adaptor.baseId; port < adaptor.baseId + adaptor.numPorts; port++) {
151
+ let status;
152
+ try {
153
+ status = await call(Xv.GrabPort, port, 0);
154
+ } catch {
155
+ continue;
156
+ }
157
+ if (status !== Xv.GrabPortStatus.Success) continue;
158
+ try {
159
+ const [formats, attributes, composite, pictFormat] = await Promise.all([
160
+ call(Xv.ListImageFormats, port),
161
+ call(Xv.QueryPortAttributes, port),
162
+ app.composite(),
163
+ app.pictFormatFor(drawsAt.visual, { depth })
164
+ ]);
165
+ const colorspace = attributes.find(
166
+ (a) => a.name === 'XV_COLORSPACE' && a.flags & Xv.AttributeFlag.Settable && a.min <= 0 && a.max >= 1
167
+ );
168
+ const colorspaceAtom = colorspace ? await call(app.X.InternAtom.bind(app.X), false, 'XV_COLORSPACE') : null;
169
+ await new Promise((resolve) => app.shm.resolve(resolve));
170
+ const opened = new VideoPort(app, Xv, {
171
+ id: port,
172
+ adaptor: adaptor.name,
173
+ depth,
174
+ visual: drawsAt.visual,
175
+ pictFormat,
176
+ formats: formats.map((f) => NAME_OF.get(f.id)).filter(Boolean),
177
+ colorspaceAtom,
178
+ composite
179
+ });
180
+ // the app closes what is still open with the connection
181
+ (app._videoPorts ??= new Set()).add(opened);
182
+ return opened;
183
+ } catch {
184
+ safeRelease(app.X, () => Xv.UngrabPort(port, 0));
185
+ return null;
186
+ }
187
+ }
188
+ }
189
+ return null;
190
+ }
191
+
192
+ /**
193
+ * One grabbed Xv port: what it takes, and `putImage` into a window — most
194
+ * usefully a `VideoTarget`'s (`createTarget`).
195
+ */
196
+ export class VideoPort {
197
+ constructor(app, Xv, { id, adaptor, depth, visual, pictFormat, formats, colorspaceAtom, composite }) {
198
+ this.app = app;
199
+ this.X = app.X;
200
+ this.Xv = Xv;
201
+ /** the port's XID */
202
+ this.id = id;
203
+ /** the adaptor's name, as the server gives it — `'glamor textured video'` */
204
+ this.adaptor = adaptor;
205
+ /** the depth of every drawable this port draws into */
206
+ this.depth = depth;
207
+ this.visual = visual;
208
+ this.pictFormat = pictFormat;
209
+ /** the formats the port takes that ntk can lay out, by name */
210
+ this.formats = Object.freeze(formats);
211
+ /**
212
+ * The colour matrices a frame can be put in. Every adaptor reads BT.601
213
+ * when told nothing; one with a settable `XV_COLORSPACE` — glamor's, and
214
+ * the Intel drivers' — reads BT.709 at 1. Every adaptor ntk knows reads
215
+ * video range, black at 16: a full-range frame is not one to put here.
216
+ */
217
+ this.colorSpaces = Object.freeze(colorspaceAtom ? ['bt601', 'bt709'] : ['bt601']);
218
+ this.closed = false;
219
+ this._composite = composite;
220
+ this._colorspaceAtom = colorspaceAtom;
221
+ // Unknown until set: a port's attributes are the server's and outlive
222
+ // the client that set them, so the last one to hold the port — this
223
+ // program's last run, as often as not — left it at whatever it used.
224
+ this._colorspace = null;
225
+ this._layouts = new Map();
226
+ this._pool = [];
227
+ this._segments = 0;
228
+ this._warming = false;
229
+ this._gc = 0;
230
+ this._colormap = 0;
231
+ this._drawables = new Set();
232
+ }
233
+
234
+ /** Whether `createTarget` can make a target here: it needs Composite. */
235
+ get targets() {
236
+ return Boolean(this._composite) && !this.closed;
237
+ }
238
+
239
+ /**
240
+ * The port's layout for a `format` frame at `width` x `height` —
241
+ * `{ width, height, dataSize, pitches, offsets }`, the size possibly
242
+ * rounded up — or `null` the first time it is asked, while the query is in
243
+ * flight. `prepare` is the same question awaited.
244
+ */
245
+ layout(format, width, height) {
246
+ const key = `${format} ${width}x${height}`;
247
+ const known = this._layouts.get(key);
248
+ if (known && typeof known.then !== 'function') return known;
249
+ if (!known) this._query(key, format, width, height).catch(() => {});
250
+ return null;
251
+ }
252
+
253
+ /** `layout`, awaited: resolves once the port has said. */
254
+ prepare(format, width, height) {
255
+ const key = `${format} ${width}x${height}`;
256
+ const known = this._layouts.get(key);
257
+ if (known) return Promise.resolve(known);
258
+ return this._query(key, format, width, height);
259
+ }
260
+
261
+ _query(key, format, width, height) {
262
+ const fourcc = VIDEO_FOURCC[format];
263
+ if (!fourcc || !this.formats.includes(format)) {
264
+ return Promise.reject(new Error(`ntk: Xv port ${this.id} takes no ${format}`));
265
+ }
266
+ const pending = call(this.Xv.QueryImageAttributes, this.id, fourcc, width, height).then(
267
+ (layout) => {
268
+ this._layouts.set(key, layout);
269
+ return layout;
270
+ },
271
+ (err) => {
272
+ this._layouts.delete(key);
273
+ throw err;
274
+ }
275
+ );
276
+ this._layouts.set(key, pending);
277
+ return pending;
278
+ }
279
+
280
+ /**
281
+ * Set the matrix the next frame is read with. False for one this port
282
+ * cannot be told — and BT.601 is what an adaptor with nothing to set reads.
283
+ */
284
+ setColorSpace(colorSpace) {
285
+ if (!this.colorSpaces.includes(colorSpace)) return false;
286
+ if (!this._colorspaceAtom) return true;
287
+ const value = colorSpace === 'bt709' ? 1 : 0;
288
+ if (value !== this._colorspace) {
289
+ this.Xv.SetPortAttribute(this.id, this._colorspaceAtom, value);
290
+ this._colorspace = value;
291
+ }
292
+ return true;
293
+ }
294
+
295
+ /**
296
+ * One frame into `drawable`, a window at the port's depth: converted, and
297
+ * scaled from `src` (frame pixels, the whole frame when left out) to `dst`
298
+ * (drawable pixels, the frame's size at 0,0 when left out).
299
+ *
300
+ * `planes` are the frame's, in the format's own order (`[y, u, v]` for
301
+ * I420, `[y, v, u]` for YV12, `[y, uv]` for NV12, one for the packed ones),
302
+ * with `strides` where a decoder pads its rows; they are copied before
303
+ * this returns, so the caller may reuse them at once.
304
+ *
305
+ * Answers whether the frame was sent. False — and nothing on the wire —
306
+ * for a format the port does not take, a colour space it cannot be told,
307
+ * a size whose layout is not known yet (the first ask starts the query),
308
+ * or a frame too large for one request on a connection with no shared
309
+ * memory. A caller that gets false draws the frame its own way.
310
+ *
311
+ * @param {number} drawable
312
+ * @param {object} frame
313
+ * @param {string} frame.format
314
+ * @param {number} frame.width
315
+ * @param {number} frame.height
316
+ * @param {ArrayLike<Uint8Array>} frame.planes
317
+ * @param {number[]} [frame.strides]
318
+ * @param {{x:number,y:number,width:number,height:number}} [frame.src]
319
+ * @param {{x:number,y:number,width:number,height:number}} [frame.dst]
320
+ * @param {'bt601'|'bt709'} [frame.colorSpace]
321
+ * @returns {boolean}
322
+ */
323
+ putImage(drawable, { format, width, height, planes, strides, src, dst, colorSpace = 'bt601' }) {
324
+ if (this.closed) return false;
325
+ const fourcc = VIDEO_FOURCC[format];
326
+ if (!fourcc || !this.formats.includes(format)) return false;
327
+ const layout = this.layout(format, width, height);
328
+ if (!layout) return false;
329
+ // AR24 is RGB: no matrix to read it with, and no attribute to send
330
+ if (format !== 'AR24' && !this.setColorSpace(colorSpace)) return false;
331
+ const s = src ?? { x: 0, y: 0, width, height };
332
+ const d = dst ?? { x: 0, y: 0, width, height };
333
+ const image = {
334
+ srcX: s.x,
335
+ srcY: s.y,
336
+ srcWidth: s.width,
337
+ srcHeight: s.height,
338
+ drwX: d.x,
339
+ drwY: d.y,
340
+ drwWidth: d.width,
341
+ drwHeight: d.height,
342
+ width: layout.width,
343
+ height: layout.height
344
+ };
345
+ const gc = this._gcFor(drawable);
346
+ const segment = this._acquire(layout.dataSize);
347
+ if (segment) {
348
+ packVideoFrame(format, width, height, planes, strides, layout, segment.buffer);
349
+ segment.commit(0, layout.dataSize);
350
+ this.Xv.ShmPutImage(this.id, drawable, gc, segment.shmseg, fourcc, {
351
+ ...image,
352
+ offset: 0,
353
+ sendEvent: true
354
+ });
355
+ // back in the pool only once the server has read it
356
+ segment.once('complete', () => this._release(segment));
357
+ } else {
358
+ // Down the socket, and node-x11 queues the payload by reference: a
359
+ // buffer of the frame's own, never the segment's.
360
+ const data = Buffer.alloc(layout.dataSize);
361
+ packVideoFrame(format, width, height, planes, strides, layout, data);
362
+ try {
363
+ this.Xv.PutImage(this.id, drawable, gc, fourcc, { ...image, data });
364
+ } catch {
365
+ // larger than one request can carry here: refused before sending
366
+ return false;
367
+ }
368
+ }
369
+ this._drawables.add(drawable);
370
+ return true;
371
+ }
372
+
373
+ /** Stop whatever this port is putting into `drawable` — before the
374
+ * drawable goes, so the server drops what it keeps for the pair. */
375
+ stop(drawable) {
376
+ if (!this._drawables.delete(drawable)) return;
377
+ safeRelease(this.X, () => this.Xv.StopVideo(this.id, drawable));
378
+ }
379
+
380
+ /** `drawable` went with its window, and the server dropped what it kept
381
+ * for the pair itself: nothing to send. */
382
+ forget(drawable) {
383
+ this._drawables.delete(drawable);
384
+ }
385
+
386
+ /**
387
+ * A `VideoTarget` under `parent` (an ntk Window or a window id) at
388
+ * `width` x `height`, or null where this server has no Composite.
389
+ */
390
+ createTarget(parent, { width, height }) {
391
+ if (!this.targets) return null;
392
+ return new VideoTarget(this, parent, { width, height });
393
+ }
394
+
395
+ /** Ungrab the port and let go of its segments. Idempotent. */
396
+ close() {
397
+ if (this.closed) return;
398
+ this.closed = true;
399
+ this.app._videoPorts?.delete(this);
400
+ const X = this.X;
401
+ safeRelease(X, () => {
402
+ for (const drawable of this._drawables) this.Xv.StopVideo(this.id, drawable);
403
+ this.Xv.UngrabPort(this.id, 0);
404
+ if (this._gc) X.FreeGC(this._gc);
405
+ if (this._colormap) X.FreeColormap(this._colormap);
406
+ });
407
+ this._drawables.clear();
408
+ const pool = this._pool;
409
+ this._pool = [];
410
+ for (const segment of pool) detach(segment);
411
+ }
412
+
413
+ /** The port's GC: made on the first drawable it draws into, valid for
414
+ * every drawable at its depth on the screen. */
415
+ _gcFor(drawable) {
416
+ if (!this._gc) {
417
+ this._gc = this.X.AllocID();
418
+ this.X.CreateGC(this._gc, drawable, { graphicsExposures: 0 });
419
+ }
420
+ return this._gc;
421
+ }
422
+
423
+ /** The colormap a window on the port's visual is created with. */
424
+ _colormapFor() {
425
+ const screen = this.app.display.screen[0];
426
+ if (this.visual === screen.root_visual) return screen.default_colormap;
427
+ if (!this._colormap) this._colormap = this.app.createColormap(this.visual);
428
+ return this._colormap;
429
+ }
430
+
431
+ /**
432
+ * A free attached segment of at least `bytes`, or null — warming one for
433
+ * next time when there is room. A segment too small for the frames now
434
+ * being put is let go on the way past.
435
+ */
436
+ _acquire(bytes) {
437
+ const shm = this.app.shm;
438
+ if (!shm.ready) return null;
439
+ for (let i = 0; i < this._pool.length; i++) {
440
+ const segment = this._pool[i];
441
+ if (segment.size >= bytes) return this._pool.splice(i, 1)[0];
442
+ }
443
+ for (let i = this._pool.length - 1; i >= 0; i--) {
444
+ detach(this._pool[i]);
445
+ this._segments--;
446
+ }
447
+ this._pool.length = 0;
448
+ if (!this._warming && this._segments < MAX_SEGMENTS) {
449
+ this._warming = true;
450
+ this._segments++;
451
+ shm.ext.createSegment(roundUp(bytes), (err, segment) => {
452
+ this._warming = false;
453
+ if (err || !segment) {
454
+ this._segments--;
455
+ return;
456
+ }
457
+ if (this.closed) detach(segment);
458
+ else this._pool.push(segment);
459
+ });
460
+ }
461
+ return null;
462
+ }
463
+
464
+ _release(segment) {
465
+ if (this.closed) detach(segment);
466
+ else this._pool.push(segment);
467
+ }
468
+ }
469
+
470
+ function detach(segment) {
471
+ try {
472
+ segment.detach();
473
+ } catch {
474
+ // the connection went first
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Where a port draws a frame that is not to be seen yet: a child window,
480
+ * manually redirected and placed outside its parent, so the server keeps
481
+ * its pixels in a pixmap nothing shows and the pointer never finds it.
482
+ * Shaped like an `Image` — `width`, `height`, `picture(app)` — so
483
+ * `ctx.drawImage(target, …)` composites the last frame put into it, and
484
+ * `copyTo(surface)` keeps one.
485
+ *
486
+ * Its pixels last as long as its parent is mapped: a window that is not
487
+ * viewable has no redirected pixmap, so put a frame and copy it out in the
488
+ * same breath, while the parent is on screen.
489
+ */
490
+ export class VideoTarget {
491
+ constructor(port, parent, { width, height }) {
492
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
493
+ throw new Error('VideoTarget: width and height must be positive integers');
494
+ }
495
+ const app = port.app;
496
+ const X = app.X;
497
+ this.port = port;
498
+ this.app = app;
499
+ this.width = width;
500
+ this.height = height;
501
+ this.id = X.AllocID();
502
+ const parentId = typeof parent === 'number' ? parent : parent.id;
503
+ // Outside the parent, up and to the left: a redirected window still
504
+ // takes the pointer where it is, and a child entirely outside its
505
+ // parent is where the pointer can never be. Its pixels are its own
506
+ // pixmap's either way (measured at -5000,-5000 on Xwayland).
507
+ X.CreateWindow(this.id, parentId, -width - 1, -height - 1, width, height, 0, port.depth, 1, port.visual, {
508
+ // a depth or visual other than the parent's needs both named
509
+ borderPixel: 0,
510
+ colormap: port._colormapFor(),
511
+ // none: the adaptor writes every pixel, and a cleared one is wasted work
512
+ backgroundPixmap: 0
513
+ });
514
+ const composite = port._composite;
515
+ composite.RedirectWindow(this.id, composite.Redirect.Manual);
516
+ X.MapWindow(this.id);
517
+ this._picture = new Picture(app, {
518
+ drawable: { id: this.id },
519
+ format: port.pictFormat ?? app.display.Render.rgb24
520
+ });
521
+ this._destroyed = false;
522
+ // An ntk Window going takes this one with it, and the server frees the
523
+ // window, its picture and the port's hold on it in the same breath:
524
+ // asking for any of them again afterwards would be three X errors. A
525
+ // parent given by id is the caller's to outlive.
526
+ this._parent = typeof parent === 'object' && typeof parent.once === 'function' ? parent : null;
527
+ this._onParentGone = () => this._gone();
528
+ this._parent?.once('_destroyed', this._onParentGone);
529
+ }
530
+
531
+ /** The window went with its parent: forget it all, send nothing. */
532
+ _gone() {
533
+ if (this._destroyed) return;
534
+ this._destroyed = true;
535
+ this.port.forget(this.id);
536
+ this._picture.forget();
537
+ }
538
+
539
+ /** Gone: destroyed, or taken with its parent window. */
540
+ get destroyed() {
541
+ return this._destroyed;
542
+ }
543
+
544
+ /**
545
+ * Whether a frame put now lands anywhere: false once the target is gone,
546
+ * and while an ntk parent is unmapped — a minimized window, one not yet
547
+ * mapped — when the child has no pixmap to hold it and a copy out of it
548
+ * would read whatever the server has there. A parent given by id is
549
+ * taken to be on screen.
550
+ */
551
+ get viewable() {
552
+ return !this._destroyed && (this._parent === null || this._parent._mapped === true);
553
+ }
554
+
555
+ /**
556
+ * A frame into the target, scaled to fill it: `port.putImage` with the
557
+ * destination named. False where the target is not `viewable`, or the
558
+ * port declined (see there).
559
+ */
560
+ put(frame) {
561
+ if (!this.viewable) return false;
562
+ return this.port.putImage(this.id, {
563
+ ...frame,
564
+ dst: { x: 0, y: 0, width: this.width, height: this.height }
565
+ });
566
+ }
567
+
568
+ /** The picture, read as stored, as `Surface.picture(app)` is. */
569
+ picture(app) {
570
+ return this[OWN_PICTURE](app)._settle();
571
+ }
572
+
573
+ [OWN_PICTURE](app) {
574
+ if (app && app !== this.app) {
575
+ throw new Error('VideoTarget: belongs to a different X connection');
576
+ }
577
+ return this._picture;
578
+ }
579
+
580
+ /**
581
+ * The last frame into `destination` — an ntk `Surface`, or anything with
582
+ * its picture — at `dx`, `dy`: one server-side copy, alpha opaque, in
583
+ * line with the put before it.
584
+ */
585
+ copyTo(destination, dx = 0, dy = 0) {
586
+ if (this._destroyed) return;
587
+ const R = this.app.display.Render;
588
+ const dst = destination[OWN_PICTURE]?.(this.app) ?? destination.picture(this.app);
589
+ R.Composite(R.PictOp.Src, this._picture.id, 0, dst.id, 0, 0, 0, 0, dx, dy, this.width, this.height);
590
+ }
591
+
592
+ destroy() {
593
+ if (this._destroyed) return;
594
+ this._destroyed = true;
595
+ this._parent?.off('_destroyed', this._onParentGone);
596
+ this.port.stop(this.id);
597
+ this._picture.destroy();
598
+ safeRelease(this.app.X, () => this.app.X.DestroyWindow(this.id));
599
+ }
600
+
601
+ [Symbol.dispose]() {
602
+ this.destroy();
603
+ }
604
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.23.4",
3
+ "version": "8.24.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",
@@ -28,6 +28,7 @@
28
28
  "./xembed": "./lib/xembed.js",
29
29
  "./color": "./lib/color.js",
30
30
  "./imagedata": "./lib/imagedata.js",
31
+ "./xv": "./lib/xv.js",
31
32
  "./path": "./lib/path.js",
32
33
  "./gl": "./lib/gl.js",
33
34
  "./shadow-math": "./lib/shadowmath.js",