ntk 6.0.1 → 6.1.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
@@ -4,6 +4,7 @@ import { chooseGLXConfig } from './glx.js';
4
4
  import Picture from './picture.js';
5
5
  import Pixmap from './pixmap.js';
6
6
  import { DEFAULT_RASTER_POLICY, defaultRasterizer } from './rasterize.js';
7
+ import { ShmUploader } from './shm-upload.js';
7
8
  import FontManager from './text/fontmanager.js';
8
9
  import Window from './window.js';
9
10
 
@@ -45,6 +46,7 @@ export default class App {
45
46
  this._cursors = null;
46
47
  this._solidPictures = new Map();
47
48
  this._rasterizer = undefined;
49
+ this._shm = undefined;
48
50
  // node-x11 emits X errors it cannot route to a request callback as
49
51
  // 'error' on the client — from inside its packet parser. With no
50
52
  // listener that emit throws and the parser never re-arms, silently
@@ -81,6 +83,22 @@ export default class App {
81
83
  this._rasterizer = value;
82
84
  }
83
85
 
86
+ /**
87
+ * The MIT-SHM upload helper: large PutImage/GetImage traffic (images,
88
+ * putImageData, getImageData) travels through shared memory on a local
89
+ * connection instead of the socket, and falls back to core requests
90
+ * everywhere else. Resolved lazily; availability follows node-x11's `shm`
91
+ * connection option (unset = on where possible, `false`/`'off'` = disabled).
92
+ * See lib/shm-upload.js and docs/context-2d.md.
93
+ */
94
+ get shm() {
95
+ if (this._shm === undefined) {
96
+ this._shm = new ShmUploader(this);
97
+ this._shm.resolve(() => {}); // probe availability in the background
98
+ }
99
+ return this._shm;
100
+ }
101
+
84
102
  /** thresholds for that decision, merged over DEFAULT_RASTER_POLICY */
85
103
  get rasterPolicy() {
86
104
  return this.options.rasterPolicy
@@ -212,6 +230,10 @@ export default class App {
212
230
  this._cursors.dispose();
213
231
  this._cursors = null;
214
232
  }
233
+ if (this._shm) {
234
+ this._shm.dispose();
235
+ this._shm = undefined;
236
+ }
215
237
  // shared by every context that ever asked (see solidPicture), so their
216
238
  // lifetime is the connection's — context teardown must not free them
217
239
  for (const picture of this._solidPictures.values()) {
package/lib/image.js CHANGED
@@ -59,13 +59,17 @@ export class Image {
59
59
  gc = app._imageUploadGC = X.AllocID();
60
60
  X.CreateGC(gc, pixmap.id);
61
61
  }
62
- // stay under the server's maximum request size by uploading row bands
63
- const stride = this.width * 4;
64
- const maxBytes = ((app.display.max_request_length ?? 65535) - 8) * 4;
65
- const rowsPerBand = Math.max(1, Math.floor(maxBytes / stride));
66
- for (let y = 0; y < this.height; y += rowsPerBand) {
67
- const rows = Math.min(rowsPerBand, this.height - y);
68
- X.PutImage(2, pixmap.id, gc, this.width, rows, 0, y, 0, 32, bgra.subarray(y * stride, (y + rows) * stride));
62
+ const bytes = Buffer.isBuffer(bgra) ? bgra : Buffer.from(bgra.buffer, bgra.byteOffset, bgra.byteLength);
63
+ // Shared memory for a large image; otherwise upload row bands that stay
64
+ // under the server's maximum request size.
65
+ if (!app.shm.putImage(pixmap.id, gc, { width: this.width, height: this.height, depth: 32, data: bytes })) {
66
+ const stride = this.width * 4;
67
+ const maxBytes = ((app.display.max_request_length ?? 65535) - 8) * 4;
68
+ const rowsPerBand = Math.max(1, Math.floor(maxBytes / stride));
69
+ for (let y = 0; y < this.height; y += rowsPerBand) {
70
+ const rows = Math.min(rowsPerBand, this.height - y);
71
+ X.PutImage(2, pixmap.id, gc, this.width, rows, 0, y, 0, 32, bytes.subarray(y * stride, (y + rows) * stride));
72
+ }
69
73
  }
70
74
 
71
75
  const picture = new Picture(app, { drawable: pixmap, format: Render.rgba32 });
@@ -1942,24 +1942,36 @@ class RenderingContext2d {
1942
1942
 
1943
1943
  const layout = this._layout;
1944
1944
  const bytes = fromStraightRgba(rgba, layout, sw, sh);
1945
- // stay under the server's maximum request size by uploading row bands
1946
- const stride = sw * 4;
1947
- const maxBytes = ((this.display.max_request_length ?? 65535) - 8) * 4;
1948
- const rowsPerBand = Math.max(1, Math.floor(maxBytes / stride));
1949
- for (let row = 0; row < sh; row += rowsPerBand) {
1950
- const rows = Math.min(rowsPerBand, sh - row);
1951
- this.X.PutImage(
1952
- 2, // ZPixmap
1953
- this._target.id,
1954
- this._gc,
1955
- sw,
1956
- rows,
1957
- x + sx,
1958
- y + sy + row,
1959
- 0,
1960
- layout.depth,
1961
- bytes.subarray(row * stride, (row + rows) * stride),
1962
- );
1945
+ // Shared memory for a large blit; otherwise upload row bands that stay
1946
+ // under the server's maximum request size.
1947
+ if (
1948
+ !this.window.app.shm.putImage(this._target.id, this._gc, {
1949
+ width: sw,
1950
+ height: sh,
1951
+ depth: layout.depth,
1952
+ dstX: x + sx,
1953
+ dstY: y + sy,
1954
+ data: bytes,
1955
+ })
1956
+ ) {
1957
+ const stride = sw * 4;
1958
+ const maxBytes = ((this.display.max_request_length ?? 65535) - 8) * 4;
1959
+ const rowsPerBand = Math.max(1, Math.floor(maxBytes / stride));
1960
+ for (let row = 0; row < sh; row += rowsPerBand) {
1961
+ const rows = Math.min(rowsPerBand, sh - row);
1962
+ this.X.PutImage(
1963
+ 2, // ZPixmap
1964
+ this._target.id,
1965
+ this._gc,
1966
+ sw,
1967
+ rows,
1968
+ x + sx,
1969
+ y + sy + row,
1970
+ 0,
1971
+ layout.depth,
1972
+ bytes.subarray(row * stride, (row + rows) * stride),
1973
+ );
1974
+ }
1963
1975
  }
1964
1976
  this._markDirty();
1965
1977
  }
@@ -2007,20 +2019,31 @@ class RenderingContext2d {
2007
2019
  */
2008
2020
  readPixels(x, y, w, h) {
2009
2021
  const layout = this._layout;
2022
+ const target = this._target.id;
2023
+ const X = this.X;
2010
2024
  return new Promise((resolve, reject) => {
2011
- this.X.GetImage(
2012
- 2,
2013
- this._target.id,
2014
- x,
2015
- y,
2016
- w,
2017
- h,
2018
- 0xffffffff,
2019
- (err, img) => {
2025
+ const coreGet = () =>
2026
+ X.GetImage(2, target, x, y, w, h, 0xffffffff, (err, img) => {
2020
2027
  if (err) return reject(err);
2021
2028
  resolve({ width: w, height: h, data: img.data, layout, ...layout });
2022
- },
2023
- );
2029
+ });
2030
+
2031
+ // A readback is where shared memory helps most: a core GetImage sends the
2032
+ // whole image back over the socket and can block the server for tens of
2033
+ // milliseconds. Route large ones through a segment; fall back on any miss.
2034
+ const bytesPerPixel = layout.bitsPerPixel ? layout.bitsPerPixel >> 3 : 4;
2035
+ const shm = this.window.app.shm;
2036
+ if (shm.wantsReadback(w * h * bytesPerPixel)) {
2037
+ shm.getImage(target, x, y, w, h, layout.depth, (err, buf, rep) => {
2038
+ if (err) return coreGet();
2039
+ // buf is the segment's own memory, reused after this callback returns
2040
+ // — copy out synchronously before it is handed to the next reader
2041
+ const data = Buffer.from(buf.subarray(0, rep.size));
2042
+ resolve({ width: w, height: h, data, layout, ...layout });
2043
+ });
2044
+ } else {
2045
+ coreGet();
2046
+ }
2024
2047
  });
2025
2048
  }
2026
2049
 
@@ -0,0 +1,166 @@
1
+ // Optional MIT-SHM fast path for bulk pixel transfers.
2
+ //
3
+ // Large PutImage/GetImage traffic on a local connection is much cheaper through
4
+ // shared memory than down the socket — roughly 2x on uploads and many times
5
+ // faster on readbacks (which otherwise pay for a whole image-sized reply). This
6
+ // wraps node-x11's provider-backed MIT-SHM API (X.require('shm')) into a small,
7
+ // ordering-safe helper the 2d context and Image use, and falls back to core
8
+ // PutImage/GetImage wherever SHM will not attach (remote display, old server,
9
+ // no /dev/shm) or the transfer is too small to bother.
10
+ //
11
+ // Ordering discipline: an SHM upload is only ever issued through a segment that
12
+ // is *already attached* — so it slots into the request stream synchronously,
13
+ // exactly where a core PutImage would have gone, and never reorders drawing.
14
+ // Attaching a segment costs a round trip, so the first upload of a given size
15
+ // falls back to core and warms a segment in the background; the next one is a
16
+ // synchronous shared-memory write. A segment is returned to the pool only after
17
+ // the server signals (ShmCompletion) it has finished reading, so a reused
18
+ // buffer is never overwritten mid-read.
19
+
20
+ // Below this many bytes the socket is as fast as shared memory (measured: a
21
+ // PutImage and a tmpfs write are both syscall-bound at a few KB; the win starts
22
+ // around 64 KB and grows with size). Coverage masks (a8, 1 byte/pixel) stay
23
+ // under it at every size the rasterizer produces, which is why the 2d fill mask
24
+ // keeps using core PutImage.
25
+ const MIN_UPLOAD_BYTES = 64 * 1024;
26
+ // A readback pays the win sooner: a core GetImage reply carries the pixels back
27
+ // over the socket and, on some drivers, blocks the server for tens of
28
+ // milliseconds, so route more of them through shared memory.
29
+ const MIN_READBACK_BYTES = 16 * 1024;
30
+ // Round segment sizes up so a handful of buckets serve many nearby sizes and
31
+ // the pool actually hits.
32
+ const SIZE_QUANTUM = 64 * 1024;
33
+
34
+ function roundUp(n) {
35
+ return Math.max(SIZE_QUANTUM, Math.ceil(n / SIZE_QUANTUM) * SIZE_QUANTUM);
36
+ }
37
+
38
+ export class ShmUploader {
39
+ /** @param {import('./app.js').default} app */
40
+ constructor(app) {
41
+ this.app = app;
42
+ this.X = app.X;
43
+ this.ext = null; // node-x11 shm extension once required; false if unavailable
44
+ this.ready = false; // usable() confirmed a segment attaches
45
+ this._pool = []; // free, already-read segments (not in flight)
46
+ this._warming = false;
47
+ this._resolving = null;
48
+ }
49
+
50
+ /**
51
+ * Resolve availability once (requires the extension and probes usable()).
52
+ * `cb(ready)`. Cheap and idempotent; safe to call before every draw.
53
+ */
54
+ resolve(cb) {
55
+ if (this.ready || this.ext === false) return cb(this.ready);
56
+ if (this._resolving) {
57
+ this._resolving.push(cb);
58
+ return;
59
+ }
60
+ this._resolving = [cb];
61
+ this.X.require('shm', (err, ext) => {
62
+ // The provider-backed API (usable/createSegment) needs x11 >= 3.7; an
63
+ // older x11 still resolves the extension but without it, so treat that
64
+ // as "no shared memory" rather than throwing.
65
+ if (err || !ext || typeof ext.usable !== 'function') return this._resolved(false, false);
66
+ ext.usable((e, ok) => this._resolved(ok ? ext : false, !!ok));
67
+ });
68
+ }
69
+
70
+ _resolved(ext, ready) {
71
+ this.ext = ext;
72
+ this.ready = ready;
73
+ const cbs = this._resolving || [];
74
+ this._resolving = null;
75
+ for (const cb of cbs) cb(ready);
76
+ }
77
+
78
+ _acquire(bytes) {
79
+ for (let i = 0; i < this._pool.length; ++i) {
80
+ if (this._pool[i].size >= bytes) return this._pool.splice(i, 1)[0];
81
+ }
82
+ return null;
83
+ }
84
+
85
+ _warm(bytes) {
86
+ if (this._warming || !this.ready) return;
87
+ this._warming = true;
88
+ this.ext.createSegment(roundUp(bytes), (err, seg) => {
89
+ this._warming = false;
90
+ if (!err && seg) this._pool.push(seg);
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Upload `data` (server-layout pixels, `stride*height` bytes at offset 0) to
96
+ * `drawable` at `dstX,dstY` as a `depth` image `width*height`. Returns true
97
+ * when shared memory handled it (the caller must then issue nothing else),
98
+ * or false to signal "do it with core PutImage" — including every case where
99
+ * SHM is unavailable, too small, or momentarily out of free segments (a
100
+ * segment is warmed in the background for next time).
101
+ */
102
+ putImage(drawable, gc, { width, height, depth, dstX = 0, dstY = 0, data }) {
103
+ const bytes = data.length;
104
+ if (!this.ready || bytes < MIN_UPLOAD_BYTES) return false;
105
+ const seg = this._acquire(bytes);
106
+ if (!seg) {
107
+ this._warm(bytes);
108
+ return false;
109
+ }
110
+ data.copy ? data.copy(seg.buffer, 0, 0, bytes) : seg.buffer.set(data.subarray(0, bytes));
111
+ seg.putImage(drawable, gc, { width, height, depth, dstX, dstY, sendEvent: true });
112
+ // reuse only once the server is done reading it
113
+ seg.once('complete', () => this._pool.push(seg));
114
+ return true;
115
+ }
116
+
117
+ /**
118
+ * Whether a readback of `bytes` should use shared memory (usable and large
119
+ * enough). The caller does the async segment dance through `getImage` below.
120
+ */
121
+ wantsReadback(bytes) {
122
+ return this.ready && bytes >= MIN_READBACK_BYTES;
123
+ }
124
+
125
+ /**
126
+ * Read `width*height` pixels of `depth` from `drawable` at `x,y` through a
127
+ * shared segment. `cb(err, buffer)` — `buffer` is a view of the segment's
128
+ * bytes valid until the next `getImage`; copy out before then. Falls back is
129
+ * the caller's job: only call this when `wantsReadback` was true.
130
+ */
131
+ getImage(drawable, x, y, width, height, depth, cb) {
132
+ const bytes = width * height * ((depth + 7) >> 3 === 3 ? 4 : (depth + 7) >> 3);
133
+ const use = (seg) => {
134
+ seg.getImage(drawable, x, y, width, height, 0xffffffff, undefined, 0, (err, rep) => {
135
+ if (err) {
136
+ this._pool.push(seg);
137
+ return cb(err);
138
+ }
139
+ // hand back the buffer, then reclaim the segment after the caller has
140
+ // synchronously copied out (they must, per the doc above)
141
+ cb(null, seg.buffer, rep);
142
+ this._pool.push(seg);
143
+ });
144
+ };
145
+ const seg = this._acquire(bytes);
146
+ if (seg) return use(seg);
147
+ this.ext.createSegment(roundUp(bytes), (err, s) => {
148
+ if (err || !s) return cb(err || new Error('shm: no segment'));
149
+ use(s);
150
+ });
151
+ }
152
+
153
+ /** Detach every pooled segment (called from App#close). */
154
+ dispose() {
155
+ const pool = this._pool;
156
+ this._pool = [];
157
+ this.ready = false;
158
+ for (const seg of pool) {
159
+ try {
160
+ seg.detach();
161
+ } catch {
162
+ /* connection may already be gone */
163
+ }
164
+ }
165
+ }
166
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "6.0.1",
3
+ "version": "6.1.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",
@@ -48,7 +48,7 @@
48
48
  "parse-color": "^1.0.0",
49
49
  "pngjs": "^7.0.0",
50
50
  "postcss": "^8.5.23",
51
- "x11": "^3.6.0",
51
+ "x11": "^3.7.0",
52
52
  "yoga-layout": "^3.2.1"
53
53
  },
54
54
  "scripts": {