ntk 8.8.1 → 8.8.2

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
@@ -5,12 +5,15 @@ import {
5
5
  DIRECT_SAMPLES,
6
6
  GLError,
7
7
  backendFor,
8
+ directStencilSize,
8
9
  glCapabilities,
9
10
  glError,
10
11
  nativeRefreshRate,
11
12
  requestedSamples,
13
+ requestedStencil,
12
14
  resolveGLPolicy,
13
- warnDirectSamples
15
+ warnDirectSamples,
16
+ warnDirectStencil
14
17
  } from './gl.js';
15
18
  import { chooseGLXConfig } from './glx.js';
16
19
  import Picture from './picture.js';
@@ -637,6 +640,14 @@ export default class App {
637
640
  * request that cannot be met also warns once per connection, because an
638
641
  * aliased edge is otherwise found months later in a screenshot.
639
642
  *
643
+ * `stencilSize` is answered the same way, and matters more: a stencil test
644
+ * against a framebuffer with no stencil buffer passes everywhere, so
645
+ * stencil-based drawing renders wrong instead of failing. `STENCIL_SIZE`
646
+ * becomes the CGL context's stencil size on macOS (8 when the spec names
647
+ * none, as the Cocoa backend's surfaces have); the dri3 flavor has nothing
648
+ * to ask EGL with, so it answers 0 and a request for more warns once
649
+ * (`directStencilSize` in lib/gl.js).
650
+ *
640
651
  * @param {object} [spec] GLX attributes, e.g. `{ DEPTH_SIZE: 24 }`
641
652
  */
642
653
  async chooseGLConfig(spec = {}) {
@@ -659,6 +670,12 @@ export default class App {
659
670
  // answer honestly below rather than dropping the attribute (issue #341)
660
671
  const wantSamples = requestedSamples(spec);
661
672
  if (wantSamples > DIRECT_SAMPLES) warnDirectSamples(this, caps.flavor, wantSamples);
673
+ // the same for stencil, where it matters more: a flavor that cannot ask
674
+ // for a stencil buffer answers 0, because a stencil test against no
675
+ // buffer passes everywhere and the draw code would never find out
676
+ const wantStencil = requestedStencil(spec);
677
+ const stencilSize = directStencilSize(caps.flavor, wantStencil);
678
+ if (wantStencil > stencilSize) warnDirectStencil(this, caps.flavor, wantStencil);
662
679
 
663
680
  return {
664
681
  backend: 'direct',
@@ -671,6 +688,9 @@ export default class App {
671
688
  class: 4, // TrueColor; the only class these buffers can be read as
672
689
  doubleBuffer: true, // a swap chain, always
673
690
  depthSize: spec.DEPTH_SIZE ?? 16,
691
+ // what the context will ask for (8 on appledri unless the spec says),
692
+ // carried here so the spec reaches getContext with the rest of it
693
+ stencilSize,
674
694
  // no flavor can allocate a multisampled window buffer yet; said out
675
695
  // loud so a caller can supersample instead of assuming MSAA
676
696
  samples: DIRECT_SAMPLES,
package/lib/fontconfig.js CHANGED
@@ -133,8 +133,67 @@ function fcMatchError(err) {
133
133
  return err;
134
134
  }
135
135
 
136
+ /**
137
+ * macOS's own face for a CSS generic family that fontconfig answers there
138
+ * with the wrong script's face, keyed by the generic.
139
+ *
140
+ * Only `sans-serif` is here, because it is the only generic fontconfig
141
+ * resolves to a CJK face on a Mac (see `platformFamilies`). `serif` and
142
+ * `monospace` land on PT Serif and Andale Mono, both of which set Latin,
143
+ * Cyrillic and Greek at their own widths.
144
+ */
145
+ const DARWIN_GENERICS = new Map([['sans-serif', 'Helvetica']]);
146
+
147
+ /**
148
+ * A family list with macOS's own face for a generic named ahead of it — on
149
+ * macOS only, and only for the generics in `DARWIN_GENERICS`. Everywhere
150
+ * else the list goes to fc-match exactly as it came.
151
+ *
152
+ * Homebrew's fontconfig (2.18) ships 48-guessfamily.conf, which tags a query
153
+ * naming a generic with that generic, and under it a `sans-serif` query
154
+ * ranks every face with "Sans" in its name ahead of all of 60-latin.conf's
155
+ * preferences — Verdana, Arial and Helvetica come in past the hundredth
156
+ * candidate. So `fc-match sans-serif` answers with whichever "…Sans…" face
157
+ * ranks first, and on a Mac that is Hiragino Sans: a Japanese face whose
158
+ * Cyrillic, Greek, `…` and `—` are full-width. "Мост" set in it is four
159
+ * em-wide cells, and CoreText sets it the same way — the advances are the
160
+ * font's own, so what is wrong is the face, not the shaping. Without that
161
+ * one file (XQuartz's fontconfig ships without it) the same query answers
162
+ * Verdana.
163
+ *
164
+ * Browsers never ask fontconfig on macOS, and all of them give `sans-serif`
165
+ * Helvetica. Naming it first does the same here, and the generic stays
166
+ * after it, so a codepoint Helvetica lacks still falls back through
167
+ * fontconfig's sans-serif list — CJK to Hiragino, as before.
168
+ *
169
+ * @param {string} family CSS-style family list, as FontManager hands it over
170
+ * @param {string} [platform] `process.platform`; a parameter so that a test
171
+ * can ask for either answer on any machine
172
+ * @returns {string} the family list to hand fc-match
173
+ */
174
+ export function platformFamilies(family, platform = globalThis.process?.platform) {
175
+ if (platform !== 'darwin') return family;
176
+ const names = String(family)
177
+ .split(',')
178
+ .map((name) => name.trim())
179
+ .filter(Boolean);
180
+ const lower = names.map((name) => name.replace(/^["']|["']$/g, '').toLowerCase());
181
+ const out = [];
182
+ let changed = false;
183
+ for (let i = 0; i < names.length; i++) {
184
+ const face = DARWIN_GENERICS.get(lower[i]);
185
+ // a list that already names the face ahead of the generic needs nothing
186
+ if (face && !lower.slice(0, i).includes(face.toLowerCase())) {
187
+ out.push(face);
188
+ changed = true;
189
+ }
190
+ out.push(names[i]);
191
+ }
192
+ return changed ? out.join(',') : family;
193
+ }
194
+
136
195
  function patternFor({ family, weight, style }) {
137
- let fc = family || 'sans-serif';
196
+ let fc = platformFamilies(family || 'sans-serif');
138
197
  const fcWeight = normalizeWeight(weight);
139
198
  if (fcWeight !== undefined) fc += `:weight=${fcWeight}`;
140
199
  if (style && style.includes('italic')) fc += ':slant=italic';
package/lib/gl.js CHANGED
@@ -535,6 +535,78 @@ export function warnDirectSamples(app, flavor, wanted) {
535
535
  );
536
536
  }
537
537
 
538
+ /**
539
+ * The stencil bits a CGL context asks for when the config names no size:
540
+ * eight, what the Cocoa backend's GL surfaces carry (DEPTH24_STENCIL8), so
541
+ * a window on the `appledri` flavor has the same buffers whichever of the
542
+ * two macOS paths draws it. A stencil buffer costs next to nothing, and the
543
+ * failure without one is silent: a stencil test with no buffer behind it
544
+ * passes everywhere, so a stencil-then-cover fill does not fail — it fills
545
+ * its whole cover quad.
546
+ */
547
+ export const DEFAULT_STENCIL_SIZE = 8;
548
+
549
+ /**
550
+ * The stencil size a spec or config asks for, read the way the depth size
551
+ * is: `stencilSize` (a config `chooseGLConfig` answered) before
552
+ * `STENCIL_SIZE` (GLX's vocabulary). `undefined` when it names neither,
553
+ * which means "the flavor's default" rather than 0 — an explicit 0 asks for
554
+ * no stencil buffer and is honoured as such. `null` is GLX's "don't care"
555
+ * and reads as unnamed too.
556
+ */
557
+ export function requestedStencil(spec = {}) {
558
+ const bits = spec.stencilSize ?? spec.STENCIL_SIZE;
559
+ if (bits === undefined || bits === null) return undefined;
560
+ return Number(bits) || 0;
561
+ }
562
+
563
+ /**
564
+ * The stencil bits a direct-backend window gets for a request, per flavor —
565
+ * the one place that decides it, as `DIRECT_SAMPLES` is for samples.
566
+ *
567
+ * - `appledri`: what was asked, `DEFAULT_STENCIL_SIZE` when nothing was. It
568
+ * travels to `kCGLPFAStencilSize` on the CGL pixel format, which CGL
569
+ * treats as a minimum.
570
+ * - `dri3`: 0, whatever was asked. x11-dri's `Gpu` builds its EGLConfig from
571
+ * the colour format and a depth size and takes no stencil size, so none is
572
+ * asked for — and EGL sorts configs smallest stencil first, so assume
573
+ * none. There is nothing here to ask with; what can be done is say so
574
+ * (`warnDirectStencil`). When the addon grows the option, this becomes the
575
+ * request.
576
+ */
577
+ export function directStencilSize(flavor, wanted) {
578
+ if (flavor === 'appledri') return wanted ?? DEFAULT_STENCIL_SIZE;
579
+ return 0;
580
+ }
581
+
582
+ /**
583
+ * Say — once per connection, on the console — that a stencil request cannot
584
+ * be met on this flavor.
585
+ *
586
+ * Unlike a missing sample buffer this is not a downgrade that merely looks
587
+ * worse: a stencil test against a framebuffer with no stencil buffer passes
588
+ * everywhere, so draw code built on it renders wrong instead of failing, and
589
+ * this warning and the `stencilSize` field are the only signals it gets.
590
+ */
591
+ export function warnDirectStencil(app, flavor, wanted) {
592
+ if (app._warnedDirectStencil) return;
593
+ app._warnedDirectStencil = true;
594
+ console.warn(
595
+ `ntk: this GL request asks for STENCIL_SIZE=${wanted}, and the direct backend` +
596
+ `${flavor ? ` (${flavor} flavor)` : ''} cannot give its window a stencil buffer, so it has ` +
597
+ 'stencilSize: 0 — and a stencil test with no buffer behind it passes everywhere.\n' +
598
+ '\n' +
599
+ ' The stencil size belongs to the EGLConfig x11-dri builds one layer below\n' +
600
+ ' ntk, and its Gpu takes no such option yet — there is nothing here to ask with.\n' +
601
+ '\n' +
602
+ ' Branch on config.stencilSize (or gl.stencilSize) rather than on having asked.\n' +
603
+ ' For a stencil buffer today: draw into a framebuffer of your own with a\n' +
604
+ ' DEPTH24_STENCIL8 renderbuffer attached, or use indirect GLX, where\n' +
605
+ " STENCIL_SIZE picks an fbconfig that has one (glPolicy: 'indirect').\n" +
606
+ ' See docs/context-gles.md#stencil.'
607
+ );
608
+ }
609
+
538
610
  /**
539
611
  * The backend `getContext('opengl')` should use right now, synchronously.
540
612
  *
package/lib/glx.js CHANGED
@@ -227,11 +227,12 @@ function matchLegacy(configs, spec) {
227
227
  * `null` means "don't care". `screen` picks the X screen (default 0),
228
228
  * `visual` pins a specific visual id and skips the search.
229
229
  * @returns {Promise<{visual: number, depth: number, class: number,
230
- * doubleBuffer: boolean, depthSize: number, samples: number|null,
231
- * fbconfig: number|null, config: object}>} `samples` is the colour
232
- * samples per pixel the chosen config has — 0 for no multisampling, and
233
- * `null` only in the pinned-`visual` case, where no fbconfig was looked
234
- * at to know.
230
+ * doubleBuffer: boolean, depthSize: number, stencilSize: number|null,
231
+ * samples: number|null, fbconfig: number|null, config: object}>}
232
+ * `samples` is the colour samples per pixel the chosen config has — 0 for
233
+ * no multisampling — and `stencilSize` its stencil bits; both are `null`
234
+ * only in the pinned-`visual` case, where no fbconfig was looked at to
235
+ * know.
235
236
  */
236
237
  export async function chooseGLXConfig(app, spec = {}) {
237
238
  const GLX = app.display.GLX;
@@ -268,6 +269,7 @@ export async function chooseGLXConfig(app, spec = {}) {
268
269
  depthSize: toNumber(want.DEPTH_SIZE) || 0,
269
270
  // the caller pinned the visual, so nothing was chosen and no fbconfig
270
271
  // was read: unknown, which is not the same claim as "none"
272
+ stencilSize: null,
271
273
  samples: null,
272
274
  screen: screenNum,
273
275
  fbconfig: null,
@@ -286,6 +288,7 @@ export async function chooseGLXConfig(app, spec = {}) {
286
288
  class: info.visual.class,
287
289
  doubleBuffer: !!cfg.DOUBLEBUFFER,
288
290
  depthSize: cfg.DEPTH_SIZE || 0,
291
+ stencilSize: cfg.STENCIL_SIZE || 0,
289
292
  samples: cfg.SAMPLES || 0,
290
293
  screen: screenNum,
291
294
  fbconfig: cfg.FBCONFIG_ID,
@@ -307,6 +310,7 @@ export async function chooseGLXConfig(app, spec = {}) {
307
310
  class: info.visual.class,
308
311
  doubleBuffer: !!cfg.doubleBufferMode,
309
312
  depthSize: cfg.depthBits || 0,
313
+ stencilSize: cfg.stencilBits || 0,
310
314
  samples: cfg.SAMPLES || 0,
311
315
  screen: screenNum,
312
316
  fbconfig: null,
@@ -47,9 +47,11 @@ import {
47
47
  DIRECT_SAMPLES,
48
48
  GLError,
49
49
  backendFor,
50
+ directStencilSize,
50
51
  glError,
51
52
  loadDriAddon,
52
53
  requestedSamples,
54
+ requestedStencil,
53
55
  warnDirectSamples
54
56
  } from './gl.js';
55
57
 
@@ -134,6 +136,13 @@ does not make it retroactive. Either:
134
136
  const wantSamples = Math.max(requestedSamples(config), Number(config.samples) || 0);
135
137
  if (wantSamples > DIRECT_SAMPLES) warnDirectSamples(app, 'appledri', wantSamples);
136
138
 
139
+ /**
140
+ * Stencil bits the context asked CGL for: the config's `stencilSize` /
141
+ * `STENCIL_SIZE`, and 8 when it names none (`directStencilSize`). CGL
142
+ * takes the size as a minimum, so the buffer has at least this many.
143
+ */
144
+ this.stencilSize = directStencilSize('appledri', requestedStencil(config));
145
+
137
146
  this._screen = config.screen ?? 0;
138
147
  this._clientId = caps.appleClientId ?? dri.apple.clientId();
139
148
 
@@ -145,7 +154,11 @@ does not make it retroactive. Either:
145
154
  // covered (docs/context-gles.md#macos).
146
155
  try {
147
156
  this.ctx = new dri.apple.Context({
148
- depthSize: config.depthSize ?? config.DEPTH_SIZE ?? 16
157
+ depthSize: config.depthSize ?? config.DEPTH_SIZE ?? 16,
158
+ // Without this the pixel format has no stencil buffer at all, and GL
159
+ // raises no error over it: a stencil test against a missing buffer
160
+ // passes everywhere, so a stencil-then-cover fill covers its whole quad.
161
+ stencilSize: this.stencilSize
149
162
  });
150
163
  } catch (err) {
151
164
  throw glError(
@@ -21,10 +21,13 @@ import {
21
21
  DIRECT_SAMPLES,
22
22
  GLError,
23
23
  backendFor,
24
+ directStencilSize,
24
25
  glError,
25
26
  loadDriAddon,
26
27
  requestedSamples,
27
- warnDirectSamples
28
+ requestedStencil,
29
+ warnDirectSamples,
30
+ warnDirectStencil
28
31
  } from './gl.js';
29
32
  import { GLSwapchain } from './glswapchain.js';
30
33
 
@@ -116,6 +119,17 @@ does not make it retroactive. Either:
116
119
  const wantSamples = Math.max(requestedSamples(config), Number(config.samples) || 0);
117
120
  if (wantSamples > DIRECT_SAMPLES) warnDirectSamples(app, caps.flavor, wantSamples);
118
121
 
122
+ /**
123
+ * Stencil bits the window's buffers have: 0 on this flavor, whatever the
124
+ * config asks for, because x11-dri's `Gpu` takes no stencil size to pass
125
+ * on (`directStencilSize`). A request for one is answered the way a
126
+ * sample count is — once, out loud — because a stencil test with no
127
+ * buffer behind it passes everywhere rather than failing.
128
+ */
129
+ const wantStencil = requestedStencil(config);
130
+ this.stencilSize = directStencilSize('dri3', wantStencil);
131
+ if (wantStencil > this.stencilSize) warnDirectStencil(app, 'dri3', wantStencil);
132
+
119
133
  const policy = app.glPolicy;
120
134
  this.gpu = sharedGpu(app, dri, {
121
135
  format: depth === 32 ? dri.FORMAT.ARGB8888 : dri.FORMAT.XRGB8888,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.8.1",
3
+ "version": "8.8.2",
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",