react-x11 2.52.0 → 2.54.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.
Files changed (115) hide show
  1. package/README.md +10 -10
  2. package/docs/README.md +160 -314
  3. package/docs/accessibility.md +176 -229
  4. package/docs/appearance.md +77 -176
  5. package/docs/click-to-component.md +18 -51
  6. package/docs/clipboard.md +40 -59
  7. package/docs/components.md +518 -1036
  8. package/docs/dbus.md +61 -133
  9. package/docs/debugging.md +152 -245
  10. package/docs/desktop-calendar.md +51 -88
  11. package/docs/desktop.md +222 -347
  12. package/docs/devtools.md +50 -167
  13. package/docs/drag-and-drop.md +126 -204
  14. package/docs/ecosystem/animation.md +91 -125
  15. package/docs/ecosystem/data-fetching.md +38 -53
  16. package/docs/ecosystem/dev-tooling.md +159 -202
  17. package/docs/ecosystem/forms.md +61 -97
  18. package/docs/ecosystem/headless.md +64 -89
  19. package/docs/ecosystem/i18n.md +18 -21
  20. package/docs/ecosystem/icons.md +93 -126
  21. package/docs/ecosystem/layout.md +42 -52
  22. package/docs/ecosystem/routing.md +18 -29
  23. package/docs/ecosystem/state.md +71 -88
  24. package/docs/ecosystem/testing.md +131 -157
  25. package/docs/ecosystem/theming.md +119 -165
  26. package/docs/ecosystem.md +145 -179
  27. package/docs/elements.md +1096 -1774
  28. package/docs/embedding.md +83 -132
  29. package/docs/events.md +412 -695
  30. package/docs/extending.md +784 -1326
  31. package/docs/eyedropper.md +63 -122
  32. package/docs/filedialog.md +71 -107
  33. package/docs/frame.md +195 -339
  34. package/docs/gl.md +65 -62
  35. package/docs/globalmenu.md +63 -146
  36. package/docs/macos.md +531 -3234
  37. package/docs/notifications.md +33 -47
  38. package/docs/packaging.md +166 -309
  39. package/docs/permissions.md +9 -14
  40. package/docs/react-features.md +81 -106
  41. package/docs/remote.md +32 -76
  42. package/docs/scale.md +72 -186
  43. package/docs/styling.md +329 -547
  44. package/docs/system.md +200 -280
  45. package/docs/testing.md +63 -83
  46. package/docs/typescript.md +6 -48
  47. package/docs/uri-schemes.md +67 -107
  48. package/docs/wayland-backend.md +200 -459
  49. package/docs/windows-integrations.md +75 -131
  50. package/docs/windows.md +343 -1367
  51. package/package.json +3 -2
  52. package/src/Reconciler.js +2 -2
  53. package/src/a11y.js +1 -1
  54. package/src/backend/context2d.js +3 -3
  55. package/src/bootstrap.js +1 -1
  56. package/src/cocoa/app.js +6 -6
  57. package/src/cocoa/bezels.js +1 -1
  58. package/src/cocoa/fonts.js +1 -1
  59. package/src/cocoa/globalmenu.js +1 -1
  60. package/src/cocoa/keymap.js +1 -1
  61. package/src/cocoa/panewindow.js +1 -1
  62. package/src/cocoa/player.js +1 -1
  63. package/src/cocoa/presenter.js +2 -2
  64. package/src/cocoa/promotion.js +1 -1
  65. package/src/cocoa/relaunch.js +1 -1
  66. package/src/cocoa/threaded.js +1 -1
  67. package/src/cocoa/video.js +1 -1
  68. package/src/cocoa/window.js +4 -4
  69. package/src/components/Menu.js +7 -2
  70. package/src/components/Switch.js +1 -1
  71. package/src/components/native.js +1 -1
  72. package/src/dnd.js +2 -2
  73. package/src/glnodes.js +1 -1
  74. package/src/grid.js +1 -1
  75. package/src/imagedecode-worker.js +32 -0
  76. package/src/imagedecode.js +140 -2
  77. package/src/layouts.js +1 -1
  78. package/src/nodes/animation.js +1 -1
  79. package/src/nodes/invalidate.js +1 -1
  80. package/src/nodes/layouthost.js +1 -1
  81. package/src/nodes/video.js +19 -8
  82. package/src/nodes/window/droptarget.js +1 -1
  83. package/src/ntk.d.ts +12 -0
  84. package/src/pacing.js +1 -1
  85. package/src/refresh/index.js +2 -1
  86. package/src/types/video.d.ts +5 -2
  87. package/src/videobackend.js +28 -0
  88. package/src/videoframes.js +29 -16
  89. package/src/wayland/app.js +27 -0
  90. package/src/wayland/video.js +376 -0
  91. package/src/win32/a11y.js +1 -1
  92. package/src/win32/app.js +2 -2
  93. package/src/win32/fonts.js +1 -1
  94. package/src/win32/glarea.js +4 -4
  95. package/src/win32/overlay.js +1 -1
  96. package/src/win32/panewindow.js +3 -3
  97. package/src/win32/shell.js +1 -1
  98. package/src/win32/window.js +2 -2
  99. package/src/windowid.js +1 -1
  100. package/src/xvideo.js +189 -0
  101. package/docs/architecture/accessibility.md +0 -470
  102. package/docs/architecture/animation.md +0 -707
  103. package/docs/architecture/container-queries.md +0 -525
  104. package/docs/architecture/custom-layout.md +0 -334
  105. package/docs/architecture/drag-and-drop.md +0 -941
  106. package/docs/architecture/element-layer-contents.md +0 -622
  107. package/docs/architecture/frame-pacing.md +0 -255
  108. package/docs/architecture/grid-layout.md +0 -358
  109. package/docs/architecture/protocol-efficiency.md +0 -357
  110. package/docs/architecture/video.md +0 -1149
  111. package/docs/architecture/windowed-regions.md +0 -854
  112. package/docs/glx.md +0 -137
  113. package/docs/wayland.md +0 -545
  114. package/docs/windows-embedding.md +0 -271
  115. package/docs/windows-gl.md +0 -221
@@ -1,6 +1,6 @@
1
1
  // <video>: frames an application decodes (`frames`, a VideoFrames sink), or a
2
2
  // file or URL the platform plays (`src`, where there is a player), fitted
3
- // into the node's box (docs/architecture/video.md §6–7).
3
+ // into the node's box (design/architecture/video.md §6–7).
4
4
  //
5
5
  // A drawn node like `<image>`: it joins yoga, it is in the paint order and it
6
6
  // is hit like anything else — which is the whole point. A video in a form
@@ -18,9 +18,10 @@
18
18
  // - **drawn** — everywhere else, and in every frame the presenter declines
19
19
  // the part (something is painted over it, it fades, a rounded card does
20
20
  // not hold it): the newest frame is copied into a surface of the node's
21
- // own at paint, natively where the backend can (`app.writeVideoFrame`) and
22
- // through `frameToRGBA` where it cannot, and composited at the picture's
23
- // rect like an `<image>` whose pixels changed. A push claims the picture.
21
+ // own at paint, natively where the backend can (`writeVideoFrame`,
22
+ // src/videobackend.js) and through `frameToRGBA` where it cannot, and
23
+ // composited at the picture's rect like an `<image>` whose pixels
24
+ // changed. A push claims the picture.
24
25
  //
25
26
  // `src` needs a platform player, which this backend has when
26
27
  // `useSupports('mediaPlayback')` says so — the app's `createPlayer`, on macOS
@@ -41,6 +42,7 @@ import {
41
42
  } from '../imagesource.js';
42
43
  import { canPlayMedia, NoMediaPlaybackError } from '../mediaplayback.js';
43
44
  import { Surface } from '../offscreen.js';
45
+ import { videoBackendOf } from '../videobackend.js';
44
46
  import { frameToRGBA, isVideoFrames } from '../videoframes.js';
45
47
  import { fitRect, objectFitOf } from './fit.js';
46
48
  import { intrinsicSize } from './layout.js';
@@ -592,10 +594,18 @@ export class VideoNode extends Node {
592
594
  }
593
595
 
594
596
  _upload(surface, sink, frame) {
595
- // natively where the backend converts in its own code (the Cocoa
596
- // bridge's writeVideoSurface: a copy for BGRA, VideoToolbox for YCbCr,
597
- // colour-matched to what a layer shows)
598
- if (this.app?.writeVideoFrame?.(surface, sink, frame)) return;
597
+ // natively where the backend converts in its own code: the Cocoa
598
+ // bridge's writeVideoSurface (a copy for BGRA, VideoToolbox for YCbCr,
599
+ // colour-matched to what a layer shows), the Wayland context's shader,
600
+ // an Xv port on X11 (src/videobackend.js)
601
+ if (
602
+ videoBackendOf(this.app)?.writeVideoFrame?.(surface, sink, frame, this)
603
+ ) {
604
+ // a frame converted in JavaScript earlier — the first, on X11, before
605
+ // the window was mapped — left a buffer the native path does not need
606
+ this._rgba = null;
607
+ return;
608
+ }
599
609
  const w = sink.width;
600
610
  const h = sink.height;
601
611
  if (this._rgba?.width !== w || this._rgba?.height !== h) {
@@ -632,6 +642,7 @@ export class VideoNode extends Node {
632
642
  this._surface = null;
633
643
  this._rgba = null;
634
644
  this._drawn = -1;
645
+ if (surface) videoBackendOf(this.app)?.releaseVideoFrame?.(surface);
635
646
  try {
636
647
  surface?.destroy();
637
648
  } catch {
@@ -1,4 +1,4 @@
1
- // The window's half of XDND (docs/architecture/drag-and-drop.md): the drop
1
+ // The window's half of XDND (design/architecture/drag-and-drop.md): the drop
2
2
  // targets registered under it and the types they accept.
3
3
 
4
4
  import {
package/src/ntk.d.ts CHANGED
@@ -115,8 +115,20 @@ export interface DecodedImage {
115
115
  */
116
116
  export function decodeImageBytes(
117
117
  bytes: Uint8Array,
118
+ options?: DecodeImageOptions,
118
119
  ): DecodedImage | Promise<DecodedImage>;
119
120
 
121
+ /** How `decodeImageBytes` decodes. */
122
+ export interface DecodeImageOptions {
123
+ /**
124
+ * Decode on a worker thread where the runtime decodes in JavaScript (Node),
125
+ * for bytes long enough to pay for the trip: the answer is then a promise
126
+ * for a PNG and a JPEG too. Under Bun every decode is off the thread
127
+ * already.
128
+ */
129
+ offThread?: boolean;
130
+ }
131
+
120
132
  /** `code` values on a failed GL setup — see `<glarea onError>`. */
121
133
  export const GLXError: {
122
134
  NO_EXTENSION: 'GLX_NO_EXTENSION';
package/src/pacing.js CHANGED
@@ -49,7 +49,7 @@
49
49
  // Off by default: `'display'` is the built-in, and paints every frame the
50
50
  // clock gives, after React's own batching — a UI answers its input as
51
51
  // quickly as it can unless it says otherwise. `'adaptive'` is what a window
52
- // that streams asks for. docs/architecture/frame-pacing.md is the account.
52
+ // that streams asks for. design/architecture/frame-pacing.md is the account.
53
53
 
54
54
  export const DEFAULT_FRAME_RATE = 'display';
55
55
 
@@ -1,6 +1,7 @@
1
1
  // react-x11/refresh — the runtime half of state-preserving hot reload
2
2
  // (Fast Refresh). The loader half is ./loader.js; `react-x11/refresh/register`
3
- // wires both up for `node --import`. See docs/ecosystem/dev-tooling.md.
3
+ // wires both up for `node --import`. See docs/ecosystem/dev-tooling.md, and
4
+ // design/ecosystem/dev-tooling.md §"How it reloads" for the loader's rules.
4
5
  //
5
6
  // Import order is what this module exists to own:
6
7
  //
@@ -69,8 +69,11 @@ export class VideoFrames {
69
69
  readonly colorSpace: VideoColorSpace;
70
70
  readonly range: VideoRange;
71
71
  /** This display's formats, cheapest first: what a decoder that can choose
72
- * should emit (`ffmpeg -pix_fmt nv12`). NV12 leads where a frame goes on
73
- * a layer as it is — macOS — and BGRA wherever a 2D context draws it. */
72
+ * should emit (`ffmpeg -pix_fmt nv12`). NV12 leads wherever something
73
+ * native takes it as it is — a layer on macOS, a shader on Wayland, an Xv
74
+ * port on X11 — and BGRA where a frame is converted in JavaScript. On X11
75
+ * it is BGRA, and not kept, until the port has answered, a few round
76
+ * trips after the first sink is made. */
74
77
  readonly preferredFormats: readonly VideoFormat[];
75
78
  /** Frames pushed so far. */
76
79
  readonly version: number;
@@ -0,0 +1,28 @@
1
+ // Who writes a `<video>`'s frames natively on an app, if anyone — the seam
2
+ // `VideoFrames.preferredFormats` and the drawn presentation ask through
3
+ // (design/architecture/video.md §6.3).
4
+ //
5
+ // The app itself where its backend has the verbs: the Cocoa app over the
6
+ // bridge's video surfaces, the Wayland app over its own GL context. On X11
7
+ // the app is ntk's, which knows nothing of sinks, so the answer is the Xv
8
+ // writer react-x11 keeps for the connection (src/xvideo.js). A feature test,
9
+ // not a question about which backend this is — `typeof app.xv` is an ntk
10
+ // with XVideo discovery. Null where nothing will, and every frame is
11
+ // converted in JavaScript (`frameToRGBA`, src/videoframes.js).
12
+ import { xvVideoFor } from './xvideo.js';
13
+
14
+ /**
15
+ * `{ videoFormats(), writeVideoFrame(surface, sink, frame, node),
16
+ * releaseVideoFrame?(surface), prepare?(sink) }` for `app`, or null.
17
+ */
18
+ export function videoBackendOf(app) {
19
+ if (!app) return null;
20
+ if (
21
+ typeof app.writeVideoFrame === 'function' ||
22
+ typeof app.videoFormats === 'function'
23
+ ) {
24
+ return app;
25
+ }
26
+ if (typeof app.xv === 'function') return xvVideoFor(app);
27
+ return null;
28
+ }
@@ -1,5 +1,5 @@
1
1
  // `VideoFrames`: a sink an application pushes decoded video frames into,
2
- // and a `<video frames>` shows (docs/architecture/video.md §6.3).
2
+ // and a `<video frames>` shows (design/architecture/video.md §6.3).
3
3
  //
4
4
  // react-x11 decodes nothing. Where the bytes come from — an `ffmpeg` child
5
5
  // writing rawvideo to a pipe, a WASM decoder, an addon over libmpv — is the
@@ -33,12 +33,16 @@
33
33
  // a decoder that can choose (`ffmpeg -pix_fmt`); one that cannot still
34
34
  // works, with the conversion charged where it happens.
35
35
  //
36
- // Pure: no node, no backend. The app is asked for its formats
37
- // (`app.videoFormats()`, the Cocoa app's answer from the bridge) and nothing
38
- // else.
36
+ // No node and no backend: the one question asked of the display is its
37
+ // formats, through `videoBackendOf` (src/videobackend.js) — the Cocoa app's
38
+ // answer from the bridge, the Wayland app's own, and on X11 the Xv writer's,
39
+ // which a sink starts asking for when it is made.
40
+
41
+ import { videoBackendOf } from './videobackend.js';
39
42
 
40
43
  export const VIDEO_FORMATS = Object.freeze(['NV12', 'I420', 'BGRA']);
41
44
  const COLOR_SPACES = ['bt709', 'bt601', 'bt2020'];
45
+ const BGRA_ONLY = Object.freeze(['BGRA']);
42
46
  const RANGES = ['video', 'full'];
43
47
  // The largest side a frame may have: a surface's limit on every backend, and
44
48
  // a number past which a size is a bug rather than a video.
@@ -209,22 +213,30 @@ export class VideoFrames {
209
213
  this._frame = null;
210
214
  this._listeners = new Set();
211
215
  this._preferred = null;
216
+ // On X11 the formats are a port's, and asking for one is round trips:
217
+ // started here, so they are known by the time a decoder that waits for
218
+ // anything at all — a probe of the file, a spawn — reads them.
219
+ videoBackendOf(app)?.prepare?.(this);
212
220
  }
213
221
 
214
222
  /**
215
223
  * This display's formats, cheapest first: what a decoder that can choose
216
- * should emit. NV12 first where a frame can go on a layer as it is
217
- * (Cocoa); BGRA first wherever a 2D context draws it.
224
+ * should emit. YCbCr first wherever something native takes it as it is —
225
+ * a layer on Cocoa, a shader on Wayland, an Xv port on X11 — and BGRA
226
+ * first where a frame is converted in JavaScript on its way to a 2D
227
+ * context. On X11 the answer is BGRA until the port has answered, a few
228
+ * round trips after the first sink is made, and is not kept until then.
218
229
  */
219
230
  get preferredFormats() {
220
- if (this._preferred === null) {
221
- const asked = this.app?.videoFormats?.();
222
- this._preferred = Object.freeze(
223
- Array.isArray(asked) && asked.length !== 0
224
- ? asked.filter((f) => VIDEO_FORMATS.includes(f))
225
- : ['BGRA'],
226
- );
227
- }
231
+ if (this._preferred !== null) return this._preferred;
232
+ const asked = videoBackendOf(this.app)?.videoFormats?.();
233
+ // null: still being asked — answer for now, and ask again next time
234
+ if (asked === null) return BGRA_ONLY;
235
+ this._preferred = Object.freeze(
236
+ Array.isArray(asked) && asked.length !== 0
237
+ ? asked.filter((f) => VIDEO_FORMATS.includes(f))
238
+ : ['BGRA'],
239
+ );
228
240
  return this._preferred;
229
241
  }
230
242
 
@@ -281,8 +293,9 @@ export const isVideoFrames = (v) => v instanceof VideoFrames;
281
293
 
282
294
  // --- converting a frame where nothing native will ----------------------------
283
295
  //
284
- // The drawn path on a backend with no native write (X11, win32, Wayland, a
285
- // Cocoa bridge older than its video surfaces): the frame converted here to
296
+ // The drawn path where nothing native writes the frame (win32, an X server
297
+ // with no Xv adaptor, a Cocoa bridge older than its video surfaces, and on
298
+ // any backend a frame the native writer declines): the frame converted here to
286
299
  // the straight RGBA `putImageData` takes. Not free — two million pixels a
287
300
  // frame at 1080p — which is why `preferredFormats` says BGRA first there.
288
301
  // The matrix and the range are the frame's; no transfer function is
@@ -30,6 +30,7 @@ import { releaseDevice } from './device.js';
30
30
  import { WaylandSeat } from './seat.js';
31
31
  import { WaylandBackendWindow } from './backendwindow.js';
32
32
  import { WaylandSurface } from './surface.js';
33
+ import { releaseVideoFrame, writeVideoFrame } from './video.js';
33
34
  import { InputRouter } from './input.js';
34
35
  import { WaylandTouch } from './touch.js';
35
36
  import { WaylandTablet } from './tablet.js';
@@ -440,6 +441,32 @@ export class WaylandApp extends EventEmitter {
440
441
  return new WaylandSurface(this, options);
441
442
  }
442
443
 
444
+ /**
445
+ * The formats a `VideoFrames` sink is cheapest in here
446
+ * (`preferredFormats`, src/videoframes.js): all three, YCbCr first. Every
447
+ * one goes up as textures as it is and is converted by a shader
448
+ * (src/wayland/video.js), and NV12 or I420 is less than half of BGRA's
449
+ * bytes on the way.
450
+ */
451
+ videoFormats() {
452
+ return ['NV12', 'I420', 'BGRA'];
453
+ }
454
+
455
+ /**
456
+ * A `<video>`'s frame into `surface`, its drawn presentation, on the GPU:
457
+ * planes as textures, the matrix in a fragment shader. False where the
458
+ * frame cannot be — a size past the GPU's textures — and the node
459
+ * converts it in JavaScript instead (src/nodes/video.js).
460
+ */
461
+ writeVideoFrame(surface, sink, frame) {
462
+ return writeVideoFrame(this, surface, sink, frame);
463
+ }
464
+
465
+ /** The node let `surface` go: its plane textures go too. */
466
+ releaseVideoFrame(surface) {
467
+ releaseVideoFrame(this, surface);
468
+ }
469
+
443
470
  /**
444
471
  * What the machine can do, as a promise like the X11 and Cocoa answers
445
472
  * (src/glbackend.js chains on it): here the answer was settled when the
@@ -0,0 +1,376 @@
1
+ // `<video frames>` on Wayland: a frame's planes go up as textures as the
2
+ // decoder made them, and the colour conversion is a fragment shader drawing
3
+ // into the `<video>`'s own surface (design/architecture/video.md §5).
4
+ //
5
+ // The backend's 2D context is already a GLES rasterizer, so this is the GLX
6
+ // transport of §2.3 made native: no conversion in JavaScript for any format,
7
+ // and NV12 and I420 cross into the GPU at 1.5 bytes a pixel. Y is an R8
8
+ // texture; NV12's chroma one RG8 texture at half size, I420's two R8 ones;
9
+ // BGRA one RGBA8 texture read back to front. The shader applies the sink's
10
+ // matrix and range exactly as `frameToRGBA` does — no transfer function,
11
+ // because nothing else on this backend is colour-managed either — so a frame
12
+ // drawn here and one converted in JavaScript are the same picture, chroma
13
+ // apart: sampled bilinearly here, as a player does, and by nearest there.
14
+ //
15
+ // What it writes into is the surface the drawn presentation already keeps
16
+ // (src/nodes/video.js): a render target, which the window's context then
17
+ // composites at the picture's rect like any `drawImage`. Nothing lifts on
18
+ // Wayland — no subsurfaces, by the backend's own design — so this is the
19
+ // whole presentation.
20
+ //
21
+ // The GL is shared with every 2D context on the app (device.js): whatever
22
+ // the device's owner still has buffered is drawn before the target is
23
+ // written, the shader's vertex state lives in a vertex array of its own,
24
+ // and the device is released afterwards so the next context to draw puts
25
+ // its own state back. `UNPACK_ROW_LENGTH`, which a decoder's padded rows
26
+ // need, is set back to 0 for the uploads that assume it.
27
+
28
+ import { flushDevice, releaseDevice } from './device.js';
29
+
30
+ const VERT = `
31
+ attribute vec2 aPos;
32
+ attribute vec2 aUV;
33
+ varying vec2 vUV;
34
+ void main() {
35
+ vUV = aUV;
36
+ gl_Position = vec4(aPos, 0.0, 1.0);
37
+ }`;
38
+
39
+ const FRAG = `
40
+ #ifdef GL_FRAGMENT_PRECISION_HIGH
41
+ precision highp float;
42
+ #else
43
+ precision mediump float;
44
+ #endif
45
+ varying vec2 vUV;
46
+ uniform int uLayout;
47
+ uniform sampler2D uY;
48
+ uniform sampler2D uC;
49
+ uniform sampler2D uV;
50
+ uniform vec2 uChroma;
51
+ uniform mat3 uMatrix;
52
+ uniform vec3 uOffset;
53
+ void main() {
54
+ if (uLayout == 0) {
55
+ // BGRA bytes uploaded as RGBA: blue is in r. Opaque, whatever the
56
+ // fourth byte held.
57
+ gl_FragColor = vec4(texture2D(uY, vUV).bgr, 1.0);
58
+ return;
59
+ }
60
+ float y = texture2D(uY, vUV).r;
61
+ vec2 cuv = vUV * uChroma;
62
+ vec2 c = uLayout == 1
63
+ ? texture2D(uC, cuv).rg
64
+ : vec2(texture2D(uC, cuv).r, texture2D(uV, cuv).r);
65
+ vec3 rgb = uMatrix * (vec3(y, c) - uOffset);
66
+ gl_FragColor = vec4(clamp(rgb, 0.0, 1.0), 1.0);
67
+ }`;
68
+
69
+ const LAYOUT = { BGRA: 0, NV12: 1, I420: 2 };
70
+
71
+ // Kr and Kb, as src/videoframes.js has them
72
+ const MATRIX = {
73
+ bt709: [0.2126, 0.0722],
74
+ bt601: [0.299, 0.114],
75
+ bt2020: [0.2627, 0.0593],
76
+ };
77
+
78
+ const matrices = new Map();
79
+
80
+ /**
81
+ * The column-major mat3 and the offset that take normalized Y'CbCr to
82
+ * R'G'B' for a matrix and a range — the same numbers `frameToRGBA`'s tables
83
+ * are made of, in units of 1 rather than of 255.
84
+ */
85
+ export function videoMatrix(colorSpace, range) {
86
+ const key = `${colorSpace}/${range}`;
87
+ let m = matrices.get(key);
88
+ if (m) return m;
89
+ const [kr, kb] = MATRIX[colorSpace];
90
+ const kg = 1 - kr - kb;
91
+ const full = range === 'full';
92
+ const ys = full ? 1 : 255 / 219;
93
+ const cs = full ? 1 : 255 / 224;
94
+ m = {
95
+ matrix: Float32Array.of(
96
+ // the column Y' multiplies
97
+ ys,
98
+ ys,
99
+ ys,
100
+ // Cb
101
+ 0,
102
+ -cs * ((2 * kb * (1 - kb)) / kg),
103
+ cs * 2 * (1 - kb),
104
+ // Cr
105
+ cs * 2 * (1 - kr),
106
+ -cs * ((2 * kr * (1 - kr)) / kg),
107
+ 0,
108
+ ),
109
+ offset: Float32Array.of(full ? 0 : 16 / 255, 128 / 255, 128 / 255),
110
+ };
111
+ matrices.set(key, m);
112
+ return m;
113
+ }
114
+
115
+ /** The planes of a frame as textures: format, size and the bytes per texel
116
+ * `UNPACK_ROW_LENGTH` counts a decoder's stride in. */
117
+ function planesOf(gl, format, width, height) {
118
+ const cw = Math.ceil(width / 2);
119
+ const ch = Math.ceil(height / 2);
120
+ const r8 = { internal: gl.R8, format: gl.RED, texel: 1 };
121
+ switch (format) {
122
+ case 'NV12':
123
+ return [
124
+ { ...r8, width, height },
125
+ { internal: gl.RG8, format: gl.RG, texel: 2, width: cw, height: ch },
126
+ ];
127
+ case 'I420':
128
+ return [
129
+ { ...r8, width, height },
130
+ { ...r8, width: cw, height: ch },
131
+ { ...r8, width: cw, height: ch },
132
+ ];
133
+ case 'BGRA':
134
+ return [{ internal: gl.RGBA, format: gl.RGBA, texel: 4, width, height }];
135
+ }
136
+ return null;
137
+ }
138
+
139
+ /** One per GL device: the program, and the quad it draws. */
140
+ const programs = new WeakMap();
141
+
142
+ function programFor(gl) {
143
+ let p = programs.get(gl);
144
+ if (p) return p;
145
+ const vs = compile(gl, gl.VERTEX_SHADER, VERT, 'video vertex');
146
+ let fs;
147
+ try {
148
+ fs = compile(gl, gl.FRAGMENT_SHADER, FRAG, 'video fragment');
149
+ } catch (err) {
150
+ gl.deleteShader(vs);
151
+ throw err;
152
+ }
153
+ const program = gl.createProgram();
154
+ gl.attachShader(program, vs);
155
+ gl.attachShader(program, fs);
156
+ gl.linkProgram(program);
157
+ const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
158
+ const log = linked ? '' : gl.getProgramInfoLog(program);
159
+ gl.deleteShader(vs);
160
+ gl.deleteShader(fs);
161
+ if (!linked) {
162
+ gl.deleteProgram(program);
163
+ throw new Error(`video program failed to link: ${log}`);
164
+ }
165
+ const u = (name) => gl.getUniformLocation(program, name);
166
+ const aPos = gl.getAttribLocation(program, 'aPos');
167
+ const aUV = gl.getAttribLocation(program, 'aUV');
168
+ // A strip over the whole target. The 2D context draws device row 0 at the
169
+ // top of clip space, so the frame's first row — texture row 0 — goes
170
+ // there: v = 0 at clip y = +1.
171
+ const buffer = gl.createBuffer();
172
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
173
+ gl.bufferData(
174
+ gl.ARRAY_BUFFER,
175
+ Float32Array.of(-1, 1, 0, 0, -1, -1, 0, 1, 1, 1, 1, 0, 1, -1, 1, 1),
176
+ gl.STATIC_DRAW,
177
+ );
178
+ // its own vertex array, so the 2D contexts' attribute state — on the
179
+ // default one, re-pointed every batch — is never this program's
180
+ const vao = gl.createVertexArray();
181
+ gl.bindVertexArray(vao);
182
+ gl.enableVertexAttribArray(aPos);
183
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 16, 0);
184
+ gl.enableVertexAttribArray(aUV);
185
+ gl.vertexAttribPointer(aUV, 2, gl.FLOAT, false, 16, 8);
186
+ gl.bindVertexArray(null);
187
+ p = {
188
+ program,
189
+ buffer,
190
+ vao,
191
+ maxSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
192
+ loc: {
193
+ uLayout: u('uLayout'),
194
+ uY: u('uY'),
195
+ uC: u('uC'),
196
+ uV: u('uV'),
197
+ uChroma: u('uChroma'),
198
+ uMatrix: u('uMatrix'),
199
+ uOffset: u('uOffset'),
200
+ },
201
+ };
202
+ programs.set(gl, p);
203
+ return p;
204
+ }
205
+
206
+ /** Each surface's plane textures, kept across frames of one size. */
207
+ const uploads = new WeakMap();
208
+
209
+ function texturesFor(gl, surface, format, width, height) {
210
+ let kept = uploads.get(surface);
211
+ if (
212
+ kept &&
213
+ kept.format === format &&
214
+ kept.width === width &&
215
+ kept.height === height
216
+ ) {
217
+ return kept;
218
+ }
219
+ if (kept) deleteTextures(gl, kept);
220
+ const planes = planesOf(gl, format, width, height);
221
+ kept = {
222
+ format,
223
+ width,
224
+ height,
225
+ planes,
226
+ textures: planes.map(() => gl.createTexture()),
227
+ fresh: true,
228
+ };
229
+ uploads.set(surface, kept);
230
+ return kept;
231
+ }
232
+
233
+ function deleteTextures(gl, kept) {
234
+ for (const tex of kept.textures) gl.deleteTexture(tex);
235
+ kept.textures = [];
236
+ }
237
+
238
+ /**
239
+ * A plane's rows at a stride `UNPACK_ROW_LENGTH` cannot say — one that is
240
+ * not a whole number of texels — packed tight. Every decoder pads to whole
241
+ * pixels, so this is the case that should never come.
242
+ */
243
+ function tight(data, stride, rowBytes, rows) {
244
+ const out = new Uint8Array(rowBytes * rows);
245
+ for (let r = 0; r < rows; r++) {
246
+ out.set(data.subarray(r * stride, r * stride + rowBytes), r * rowBytes);
247
+ }
248
+ return out;
249
+ }
250
+
251
+ /**
252
+ * `frame` from `sink` into `surface` (a WaylandSurface at the frame's size).
253
+ * False — and the frame is converted in JavaScript instead — where the
254
+ * surface is not one of this backend's, the app has no GL, or the frame is
255
+ * larger than a texture can be.
256
+ */
257
+ export function writeVideoFrame(app, surface, sink, frame) {
258
+ const gl = app?.gl;
259
+ const target = surface?.target;
260
+ if (!gl || !target?.fbo || target.destroyed) return false;
261
+ if (!(sink.format in LAYOUT)) return false;
262
+ app.makeCurrent();
263
+ const p = programFor(gl);
264
+ if (sink.width > p.maxSize || sink.height > p.maxSize) return false;
265
+ // Anything a context still has buffered is drawn first: one of its draws
266
+ // may read this target, and must read the frame it was given (device.js).
267
+ flushDevice(gl);
268
+ const kept = texturesFor(gl, surface, sink.format, sink.width, sink.height);
269
+ try {
270
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
271
+ kept.planes.forEach((plane, i) => {
272
+ const rowBytes = plane.width * plane.texel;
273
+ const stride = frame.strides[i];
274
+ let data = frame.planes[i];
275
+ let rowLength = 0;
276
+ if (stride !== rowBytes) {
277
+ if (stride % plane.texel === 0) rowLength = stride / plane.texel;
278
+ else data = tight(data, stride, rowBytes, plane.height);
279
+ }
280
+ gl.pixelStorei(gl.UNPACK_ROW_LENGTH, rowLength);
281
+ gl.activeTexture(gl.TEXTURE0 + i);
282
+ gl.bindTexture(gl.TEXTURE_2D, kept.textures[i]);
283
+ if (kept.fresh) {
284
+ // Y and BGRA meet the target texel for texel; chroma is half size,
285
+ // and read between its samples as a player reads it
286
+ const filter = i === 0 ? gl.NEAREST : gl.LINEAR;
287
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
288
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
289
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
290
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
291
+ gl.texImage2D(
292
+ gl.TEXTURE_2D,
293
+ 0,
294
+ plane.internal,
295
+ plane.width,
296
+ plane.height,
297
+ 0,
298
+ plane.format,
299
+ gl.UNSIGNED_BYTE,
300
+ data,
301
+ );
302
+ } else {
303
+ gl.texSubImage2D(
304
+ gl.TEXTURE_2D,
305
+ 0,
306
+ 0,
307
+ 0,
308
+ plane.width,
309
+ plane.height,
310
+ plane.format,
311
+ gl.UNSIGNED_BYTE,
312
+ data,
313
+ );
314
+ }
315
+ });
316
+ kept.fresh = false;
317
+
318
+ target.bind();
319
+ gl.disable(gl.BLEND);
320
+ gl.disable(gl.SCISSOR_TEST);
321
+ gl.disable(gl.STENCIL_TEST);
322
+ gl.disable(gl.DEPTH_TEST);
323
+ gl.disable(gl.CULL_FACE);
324
+ gl.colorMask(true, true, true, true);
325
+ gl.useProgram(p.program);
326
+ const { loc } = p;
327
+ gl.uniform1i(loc.uLayout, LAYOUT[sink.format]);
328
+ gl.uniform1i(loc.uY, 0);
329
+ gl.uniform1i(loc.uC, 1);
330
+ gl.uniform1i(loc.uV, 2);
331
+ // full-size texture coordinates onto a chroma plane rounded up: an odd
332
+ // frame's last chroma column covers one picture column, not two
333
+ gl.uniform2f(
334
+ loc.uChroma,
335
+ sink.width / (2 * Math.ceil(sink.width / 2)),
336
+ sink.height / (2 * Math.ceil(sink.height / 2)),
337
+ );
338
+ const { matrix, offset } = videoMatrix(sink.colorSpace, sink.range);
339
+ gl.uniformMatrix3fv(loc.uMatrix, false, matrix);
340
+ gl.uniform3fv(loc.uOffset, offset);
341
+ gl.bindVertexArray(p.vao);
342
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
343
+ gl.bindVertexArray(null);
344
+ } finally {
345
+ gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 0);
346
+ gl.activeTexture(gl.TEXTURE0);
347
+ // the program, the blend and the framebuffer were the 2D contexts' to
348
+ // say: the next one to draw says them again
349
+ releaseDevice(gl);
350
+ app.rebindWindowTarget?.();
351
+ }
352
+ return true;
353
+ }
354
+
355
+ /** `surface` is let go: its plane textures with it. */
356
+ export function releaseVideoFrame(app, surface) {
357
+ const kept = uploads.get(surface);
358
+ if (!kept) return;
359
+ uploads.delete(surface);
360
+ const gl = app?.gl;
361
+ if (!gl) return;
362
+ app.makeCurrent();
363
+ deleteTextures(gl, kept);
364
+ }
365
+
366
+ function compile(gl, type, src, what) {
367
+ const sh = gl.createShader(type);
368
+ gl.shaderSource(sh, src);
369
+ gl.compileShader(sh);
370
+ if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
371
+ const log = gl.getShaderInfoLog(sh);
372
+ gl.deleteShader(sh);
373
+ throw new Error(`${what} shader failed to compile: ${log}`);
374
+ }
375
+ return sh;
376
+ }
package/src/win32/a11y.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // property reads — so answering from JS would pace the screen reader by the
8
8
  // application's busiest moment. The bridge therefore *pushes*, and
9
9
  // `windows/src/uia.cc` answers from its copy. The reasoning is
10
- // docs/windows.md §"Accessibility: UI Automation"; what matters here is the
10
+ // design/windows.md §"Accessibility: UI Automation"; what matters here is the
11
11
  // consequence: **this file's job is a diff, not an answer.**
12
12
  //
13
13
  // Everything it pushes comes from `src/a11y.js`, which is pure functions over
package/src/win32/app.js CHANGED
@@ -362,7 +362,7 @@ export class Win32App {
362
362
  // `embeddable` is "created, not shown; somebody else will place me", and
363
363
  // on this backend that cannot be a window: a composition target stops
364
364
  // presenting the moment its window becomes a child, so an HWND is not
365
- // something another process can take (docs/windows-embedding.md). What a
365
+ // something another process can take (design/windows-embedding.md). What a
366
366
  // host can take is the **buffer**, so an embeddable window here is the
367
367
  // same shared surface a `<Frame>` pane draws into — whether this process
368
368
  // was forked as a pane or is a guest somebody else started.
@@ -825,7 +825,7 @@ export class Win32App {
825
825
  break;
826
826
  }
827
827
  case 'dpichanged':
828
- // The renderer cannot re-scale a live window yet (docs/windows.md
828
+ // The renderer cannot re-scale a live window yet (design/windows.md
829
829
  // §Layout, open question 8), so this is recorded and not acted on.
830
830
  wnd.emit('statechange', { scale: event.a / 96 });
831
831
  break;