react-x11 2.18.3 → 2.18.5

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/src/win32/app.js CHANGED
@@ -16,6 +16,8 @@ import { setScreensForTests } from '../screens.js';
16
16
  import { createBezels } from './bezels.js';
17
17
  import { Win32InputMethod } from './ime.js';
18
18
  import { decodeKey, modifierMask } from './keymap.js';
19
+ import { refuseOverlay } from '../gloverlay.js';
20
+ import { Win32OverlayPane } from './overlay.js';
19
21
  import { Win32PaneHost } from './panehost.js';
20
22
  import { Win32PaneWindow } from './panewindow.js';
21
23
  import { Win32Surface } from './surface.js';
@@ -80,6 +82,8 @@ export class Win32App {
80
82
  // an in-app drop reads to keep `e.items` by reference (src/dnd.js).
81
83
  this._activeDrag = null;
82
84
  this._rafQueue = [];
85
+ /** Windows holding input for the next frame (`Win32Window._queueInput`). */
86
+ this._inputHeld = new Set();
83
87
  this._frameTimer = null;
84
88
  /** A frame has been asked for and not yet delivered — one outstanding
85
89
  * request at a time, whether the clock or the timer is answering it. */
@@ -118,6 +122,7 @@ export class Win32App {
118
122
  this.nativeBezels = null;
119
123
 
120
124
  this.fonts = new Win32FontManager(native);
125
+ this._installOverlay();
121
126
 
122
127
  const screens = native.listScreens?.() ?? [];
123
128
  const primary = screens[0] ?? {
@@ -275,10 +280,19 @@ export class Win32App {
275
280
  * A copy of the set is walked because `onDismiss` is what unmounts the
276
281
  * popup, and unmounting runs `ungrabPointer` — which deletes from the set
277
282
  * this loop would otherwise still be reading.
283
+ *
284
+ * `open` is the popups to judge — for a press, the ones that were open
285
+ * *before* it (`_route`). The press is delivered first, and a press that
286
+ * opens a menu — a `<Select>`, a menu button — opens it and takes its
287
+ * grab synchronously, in the same dispatch; judged afterwards, the new
288
+ * menu heard the press that opened it as a press outside itself and
289
+ * closed at once. Every `<Select>` did that. An X11 grab taken during a
290
+ * press does not see that press either.
278
291
  */
279
- _dismissOutsidePopups(pressed) {
292
+ _dismissOutsidePopups(pressed, open = null) {
280
293
  if (this._dismissOnOutside.size === 0) return;
281
- for (const wnd of [...this._dismissOnOutside]) {
294
+ for (const wnd of open ?? [...this._dismissOnOutside]) {
295
+ if (!this._dismissOnOutside.has(wnd)) continue;
282
296
  if (wnd === pressed) continue;
283
297
  if (wnd.destroyed) {
284
298
  this._dismissOnOutside.delete(wnd);
@@ -367,6 +381,29 @@ export class Win32App {
367
381
  return new Win32PaneHost(this, wnd);
368
382
  }
369
383
 
384
+ /**
385
+ * The pane a `<glarea>`'s children are drawn on (src/gloverlay.js): a
386
+ * composited layer above the window's swap chains, where the children's
387
+ * transparent pixels show the GL frame (src/win32/overlay.js). Having this
388
+ * at all is how the overlay knows it composites — and the X11 fallback it
389
+ * would otherwise reach for, a plain child window, is a GL surface here.
390
+ * Installed in the constructor where the bridge has layers.
391
+ */
392
+ _installOverlay() {
393
+ if (typeof this._native.layerCreate === 'function') {
394
+ this.createOverlayPane = (attributes) =>
395
+ new Win32OverlayPane(this, attributes);
396
+ return;
397
+ }
398
+ refuseOverlay(
399
+ this,
400
+ "this @windowkit/win32 predates layers, which are what a <glarea>'s " +
401
+ 'children are drawn on here, and a child window on this backend is ' +
402
+ 'a GL surface of its own. Update the bridge, or draw the overlay ' +
403
+ 'beside the surface',
404
+ );
405
+ }
406
+
370
407
  /**
371
408
  * The pane process's end of the frame channel: geometry and input in,
372
409
  * presents out. Feature-detected by the pane bootstrap
@@ -421,7 +458,18 @@ export class Win32App {
421
458
 
422
459
  _requestFrame(cb, wnd = null) {
423
460
  this._rafQueue.push({ cb, wnd });
424
- if (this._framePending || this._closed) return this._rafQueue.length;
461
+ this._armFrame();
462
+ return this._rafQueue.length;
463
+ }
464
+
465
+ /** A window is holding input for the next frame: there has to be one. */
466
+ _holdInput(wnd) {
467
+ this._inputHeld.add(wnd);
468
+ this._armFrame();
469
+ }
470
+
471
+ _armFrame() {
472
+ if (this._framePending || this._closed) return;
425
473
  this._framePending = true;
426
474
  // One tick, from the compositor if this Windows has a clock to wait on.
427
475
  // It answers false when it has not, and then — and only then — a timer.
@@ -434,7 +482,7 @@ export class Win32App {
434
482
  this._tickFrames();
435
483
  }, CLOCK_WATCHDOG_MS);
436
484
  this._frameTimer.unref?.();
437
- return this._rafQueue.length;
485
+ return;
438
486
  }
439
487
  this._frameTimer = setTimeout(() => {
440
488
  this._frameTimer = null;
@@ -442,10 +490,24 @@ export class Win32App {
442
490
  this._tickFrames();
443
491
  }, FRAME_INTERVAL_MS);
444
492
  this._frameTimer.unref?.();
445
- return this._rafQueue.length;
446
493
  }
447
494
 
448
495
  _tickFrames() {
496
+ // The input held for this frame goes first: what it changes is what the
497
+ // frame paints, and the frames its handlers ask for are this one —
498
+ // the callbacks are taken after it.
499
+ if (this._inputHeld.size !== 0) {
500
+ const held = [...this._inputHeld];
501
+ this._inputHeld.clear();
502
+ for (const wnd of held) {
503
+ if (wnd.destroyed) continue;
504
+ try {
505
+ wnd._flushInput();
506
+ } catch (err) {
507
+ if (process.env.NODE_ENV !== 'production') console.error(err);
508
+ }
509
+ }
510
+ }
449
511
  const due = this._rafQueue;
450
512
  this._rafQueue = [];
451
513
  const now = performance.now();
@@ -515,6 +577,11 @@ export class Win32App {
515
577
 
516
578
  const wnd = this._windows.get(event.id);
517
579
  if (!wnd) return;
580
+ // Whatever this is happened after the motion and the wheel the window is
581
+ // holding for its next frame, so they go first (`_queueInput`).
582
+ if (event.type !== 'mousemove' && event.type !== 'wheel') {
583
+ wnd._flushInput?.();
584
+ }
518
585
  switch (event.type) {
519
586
  case 'window-ready':
520
587
  wnd._onReady(event.a, event.b);
@@ -533,7 +600,7 @@ export class Win32App {
533
600
  break;
534
601
  }
535
602
  case 'mousemove':
536
- wnd.emit('mousemove', {
603
+ wnd._queueInput('mousemove', {
537
604
  x: event.a,
538
605
  y: event.b,
539
606
  ...rootOf(wnd, event),
@@ -545,7 +612,7 @@ export class Win32App {
545
612
  case 'wheel': {
546
613
  const deltaX = event.c;
547
614
  const deltaY = event.d;
548
- wnd.emit('wheel', {
615
+ wnd._queueInput('wheel', {
549
616
  name: 'wheel',
550
617
  x: event.a,
551
618
  y: event.b,
@@ -564,7 +631,13 @@ export class Win32App {
564
631
  break;
565
632
  }
566
633
  case 'mousedown':
567
- case 'mouseup':
634
+ case 'mouseup': {
635
+ // the menus this press may close: those open before it (see
636
+ // `_dismissOutsidePopups`)
637
+ const open =
638
+ event.type === 'mousedown' && this._dismissOnOutside.size !== 0
639
+ ? [...this._dismissOnOutside]
640
+ : null;
568
641
  // `keycode` is the button, numbered as X numbers them — the bridge
569
642
  // already speaks that vocabulary. It used to be hardcoded to 1, so
570
643
  // a right-click arrived as a left-click and no context menu ever
@@ -579,8 +652,9 @@ export class Win32App {
579
652
  // A press in one window is a press *outside* every open menu but the
580
653
  // one it landed in — which on X11 the grab would have delivered to
581
654
  // the menu instead of to this window.
582
- if (event.type === 'mousedown') this._dismissOutsidePopups(wnd);
655
+ if (open) this._dismissOutsidePopups(wnd, open);
583
656
  break;
657
+ }
584
658
  case 'keydown':
585
659
  case 'keyup': {
586
660
  // The bridge asked the active layout what the key types before the
@@ -187,13 +187,25 @@ class Win32Face {
187
187
  this.italic = style === 'italic' || style === 'oblique';
188
188
  }
189
189
 
190
+ /**
191
+ * Kept per size: a face's metrics never change, and the bridge answers
192
+ * each call by resolving the family through DirectWrite again. A trimmed
193
+ * `<text>` asks on every placement of its layout (`_trim`), which on a
194
+ * graph of widget cards was every body on every step of a drag.
195
+ */
190
196
  metrics(size) {
191
- return this._manager._native.fontMetrics(
192
- this.family,
193
- size,
194
- this.weight,
195
- this.italic,
196
- );
197
+ this._metrics ??= new Map();
198
+ let m = this._metrics.get(size);
199
+ if (m === undefined) {
200
+ m = this._manager._native.fontMetrics(
201
+ this.family,
202
+ size,
203
+ this.weight,
204
+ this.italic,
205
+ );
206
+ this._metrics.set(size, m);
207
+ }
208
+ return m;
197
209
  }
198
210
 
199
211
  /**
@@ -474,7 +486,14 @@ export class Win32FontManager {
474
486
  }
475
487
 
476
488
  match(family, options = {}) {
477
- const name = familyOf(this._native, family);
489
+ // the stack resolved once: a named family is a DirectWrite lookup, and
490
+ // every text layout asks
491
+ this._families ??= new Map();
492
+ let name = this._families.get(family);
493
+ if (name === undefined) {
494
+ name = familyOf(this._native, family);
495
+ this._families.set(family, name);
496
+ }
478
497
  const key = `${name}|${weightOf(options.weight)}|${options.style ?? 'normal'}`;
479
498
  let face = this._faces.get(key);
480
499
  if (!face) {
@@ -0,0 +1,244 @@
1
+ // The pane a `<glarea>`'s children are drawn on, on the Windows backend: a
2
+ // DirectComposition visual with a premultiplied surface of its own, above
3
+ // every swap chain in the window's tree (src/gloverlay.js; the bridge's
4
+ // src/layer.cc).
5
+ //
6
+ // Without it a pane was whatever `app.createWindow({ parent })` answers,
7
+ // which here is a GL surface (src/win32/glarea.js) — a WGL context and a
8
+ // swap chain per pane, and no 2D context at all. The children were laid out,
9
+ // claimed and painted nowhere, and a component that drew their cards under
10
+ // them had nothing left on screen where they were.
11
+ //
12
+ // Composited, like the Cocoa pane (src/cocoa/overlay.js): what the children
13
+ // leave transparent shows the GL frame, and a translucent fill, an
14
+ // antialiased edge or a shadow blends with it. Text on it is grayscale
15
+ // rather than ClearType, which Direct2D does on any surface with alpha
16
+ // (docs/windows.md §Text).
17
+ //
18
+ // Drawn like the window it lives in (src/win32/window.js): a DirectComposition
19
+ // surface hands out a context per `BeginDraw`, valid until `EndDraw`, so the
20
+ // overlay's passes come through `paintPasses` — one `BeginDraw` each — rather
21
+ // than through a context that is always there. `present` asks for the commit
22
+ // the window's own frame makes, so a pane and the window land together.
23
+ import { BackendContext2D } from '../backend/context2d.js';
24
+
25
+ export class Win32OverlayPane {
26
+ constructor(app, options) {
27
+ this.app = app;
28
+ this.parent = options.parent;
29
+ this._native = app._native;
30
+ this.destroyed = false;
31
+ this.rect = {
32
+ x: options.x ?? 0,
33
+ y: options.y ?? 0,
34
+ width: options.width ?? 1,
35
+ height: options.height ?? 1,
36
+ };
37
+ // the bridge's layer, made once the window has a composition tree to
38
+ // hang it in — the tree renders before the window is ready
39
+ this._layer = 0;
40
+ this._visible = true;
41
+ // non-zero only between a pass's BeginDraw and its EndDraw
42
+ this._surface = 0;
43
+ this._gen = 0;
44
+ this._ctx = null;
45
+ this._handlers = {};
46
+ this._ensureLayer();
47
+ }
48
+
49
+ get width() {
50
+ return this.rect.width;
51
+ }
52
+
53
+ get height() {
54
+ return this.rect.height;
55
+ }
56
+
57
+ on(name, fn) {
58
+ (this._handlers[name] ??= []).push(fn);
59
+ }
60
+
61
+ emit(name, ev) {
62
+ for (const fn of this._handlers[name] ?? []) fn(ev);
63
+ }
64
+
65
+ _ensureLayer() {
66
+ if (this._layer || this.destroyed) return this._layer;
67
+ if (!this.parent?._composed || this.parent.destroyed) return 0;
68
+ const { x, y, width, height } = this.rect;
69
+ this._layer = this._native.layerCreate(
70
+ this.parent.id,
71
+ Math.round(x),
72
+ Math.round(y),
73
+ Math.max(1, Math.round(width)),
74
+ Math.max(1, Math.round(height)),
75
+ );
76
+ if (this._layer && !this._visible) {
77
+ this._native.layerSetVisible(this._layer, false);
78
+ }
79
+ return this._layer;
80
+ }
81
+
82
+ /** Geometry in device px, the unit the overlay's rects are in. */
83
+ setState(rect) {
84
+ if (this.destroyed) return;
85
+ this.rect = rect;
86
+ if (!this._layer) return;
87
+ this._native.layerSetRect(
88
+ this._layer,
89
+ Math.round(rect.x),
90
+ Math.round(rect.y),
91
+ Math.max(1, Math.round(rect.width)),
92
+ Math.max(1, Math.round(rect.height)),
93
+ );
94
+ this._owe();
95
+ }
96
+
97
+ map() {
98
+ this._setVisible(true);
99
+ }
100
+
101
+ unmap() {
102
+ this._setVisible(false);
103
+ }
104
+
105
+ _setVisible(on) {
106
+ if (this._visible === on) return;
107
+ this._visible = on;
108
+ if (!this._layer || this.destroyed) return;
109
+ this._native.layerSetVisible(this._layer, on);
110
+ this._owe();
111
+ }
112
+
113
+ getContext() {
114
+ if (!this._ctx) {
115
+ this._ctx = new BackendContext2D(
116
+ this._native,
117
+ () => this._surface,
118
+ () => this._gen,
119
+ );
120
+ this._ctx._fonts = this.app.fonts;
121
+ }
122
+ return this._ctx;
123
+ }
124
+
125
+ /**
126
+ * The overlay's passes, each its own `BeginDraw` — `rect`s in window
127
+ * coordinates, each painted by `paint(ctx, rect)` with the context drawing
128
+ * in window coordinates too. A pass is grown to whole pixels first:
129
+ * everything inside a `BeginDraw` rect is undefined until it is painted,
130
+ * so the rect the painter clears and the rect the surface hands out have
131
+ * to be the same one.
132
+ */
133
+ paintPasses(passes, paint) {
134
+ if (this.destroyed) return;
135
+ if (!this._ensureLayer()) {
136
+ // Nothing to draw into yet. The overlay believes these passes landed,
137
+ // so it is told they did not: all of the pane is owed, on the next
138
+ // frame, when the window will have a tree to hang the layer in.
139
+ setImmediate(() => {
140
+ if (!this.destroyed) this.emit('draw', {});
141
+ });
142
+ return;
143
+ }
144
+ const ctx = this.getContext();
145
+ const ox = Math.round(this.rect.x);
146
+ const oy = Math.round(this.rect.y);
147
+ let drawn = false;
148
+ for (const pass of passes) {
149
+ const x0 = Math.max(Math.floor(pass.x), ox);
150
+ const y0 = Math.max(Math.floor(pass.y), oy);
151
+ const x1 = Math.min(
152
+ Math.ceil(pass.x + pass.width),
153
+ ox + Math.max(1, Math.round(this.rect.width)),
154
+ );
155
+ const y1 = Math.min(
156
+ Math.ceil(pass.y + pass.height),
157
+ oy + Math.max(1, Math.round(this.rect.height)),
158
+ );
159
+ if (x1 <= x0 || y1 <= y0) continue;
160
+ const handle = this._native.layerBeginDraw(
161
+ this._layer,
162
+ x0 - ox,
163
+ y0 - oy,
164
+ x1 - x0,
165
+ y1 - y0,
166
+ );
167
+ if (!handle) continue;
168
+ this._surface = handle;
169
+ this._gen++;
170
+ try {
171
+ // the surface draws in the layer's own coordinates; the painter
172
+ // draws in the window's
173
+ ctx.save();
174
+ try {
175
+ ctx.translate(-ox, -oy);
176
+ paint(ctx, { x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
177
+ } finally {
178
+ ctx.restore();
179
+ }
180
+ drawn = true;
181
+ } finally {
182
+ this._surface = 0;
183
+ this._native.layerEndDraw(this._layer);
184
+ }
185
+ }
186
+ if (drawn) this._owe();
187
+ }
188
+
189
+ /**
190
+ * Move the pixels of `rect` (pane coordinates, device px) by (dx, dy) —
191
+ * ntk `Window.scrollRegion`'s contract: the band that survives the shift
192
+ * inside `rect` moves, the rest of it is left as it was, and false means
193
+ * nothing moved, so the caller repaints instead (issue #644).
194
+ */
195
+ scrollRegion(rect, dx, dy) {
196
+ if (this.destroyed || !this._layer) return false;
197
+ if (!Number.isInteger(dx) || !Number.isInteger(dy)) return false;
198
+ const moved = this._native.layerScroll(
199
+ this._layer,
200
+ Math.round(rect.x),
201
+ Math.round(rect.y),
202
+ Math.round(rect.width),
203
+ Math.round(rect.height),
204
+ dx,
205
+ dy,
206
+ );
207
+ if (moved) this._owe();
208
+ return Boolean(moved);
209
+ }
210
+
211
+ /** What was painted or moved, onto the screen with the window's frame. */
212
+ present() {
213
+ this._owe();
214
+ }
215
+
216
+ /**
217
+ * A commit is owed. The window's frame makes one after the overlay has
218
+ * painted (`presentFrame`, src/win32/window.js), so a pane's pixels and
219
+ * the window's land together; a change made outside a frame — a pane
220
+ * hidden between two — is committed on a microtask instead, once.
221
+ */
222
+ _owe() {
223
+ const wnd = this.parent;
224
+ if (!wnd || wnd.destroyed) return;
225
+ if (wnd._owesCommit) return;
226
+ wnd._owesCommit = true;
227
+ queueMicrotask(() => {
228
+ if (!wnd._owesCommit) return;
229
+ wnd._owesCommit = false;
230
+ if (!wnd.destroyed) this._native.commit();
231
+ });
232
+ }
233
+
234
+ destroy() {
235
+ if (this.destroyed) return;
236
+ this.destroyed = true;
237
+ this._ctx = null;
238
+ if (this._layer) {
239
+ this._native.layerDestroy(this._layer);
240
+ this._layer = 0;
241
+ this._owe();
242
+ }
243
+ }
244
+ }
@@ -287,6 +287,12 @@ export class Win32PaneWindow {
287
287
  }
288
288
  }
289
289
 
290
+ /** The frame is the ordinary paint walk, so it paints through core's
291
+ * paint cache (src/nodes/window/flush.js), as the window does. */
292
+ get usesPaintCache() {
293
+ return true;
294
+ }
295
+
290
296
  /**
291
297
  * No scroll blit. The fast path moves a band of pixels *within the surface
292
298
  * being drawn into*, and this one is `STALE_FRAMES` frames behind — the
@@ -53,6 +53,11 @@ export class Win32Window {
53
53
  this._pendingFrames = [];
54
54
  this._owesFullPaint = false;
55
55
  this._dirty = false;
56
+ // a pane over a `<glarea>` here changed and its commit is this frame's
57
+ // (src/win32/overlay.js)
58
+ this._owesCommit = false;
59
+ // pointer motion and wheel held for the next frame (`_queueInput`)
60
+ this._pendingInput = null;
56
61
 
57
62
  // `overrideRedirect` is how a `<popup>` says what it is — a menu, a
58
63
  // select's list, a tooltip. On X11 it means "the window manager does not
@@ -104,6 +109,58 @@ export class Win32Window {
104
109
  for (const fn of this._handlers[name] ?? []) fn(ev);
105
110
  }
106
111
 
112
+ /**
113
+ * Hold a pointer motion or a wheel for the next frame, merged with any
114
+ * already held: ntk's coalescing on X11 (its lib/window.js,
115
+ * `_deliverEvent`), which the rest of the event path was written against
116
+ * — src/events.js paces motion on the strength of it.
117
+ *
118
+ * Windows merges WM_MOUSEMOVE itself, but only while a queue waits to be
119
+ * read, and the bridge reads its messages on a UI thread of its own and
120
+ * posts every one of them to the loop. So a mouse reporting a thousand
121
+ * times a second was a thousand events on the loop, and a handler slower
122
+ * than a millisecond fell behind the pointer — then replayed every
123
+ * position it had missed, a frame of work each, long after the pointer
124
+ * had stopped: a box selection over a large graph trailed the pointer by
125
+ * seconds. Held here, a burst is one event a frame: the newest motion,
126
+ * with the rest riding along in `ev.coalesced`, and the wheel's deltas
127
+ * added up at its last position.
128
+ */
129
+ _queueInput(name, ev) {
130
+ const pending = (this._pendingInput ??= new Map());
131
+ const prev = pending.get(name);
132
+ if (!prev) {
133
+ ev.coalesced = [ev];
134
+ pending.set(name, ev);
135
+ } else if (name === 'wheel') {
136
+ // scroll distance adds up — the last delta alone would throw away all
137
+ // but the final step of a fast scroll
138
+ prev.coalesced.push(ev);
139
+ prev.deltaX += ev.deltaX;
140
+ prev.deltaY += ev.deltaY;
141
+ prev.x = ev.x;
142
+ prev.y = ev.y;
143
+ prev.rootx = ev.rootx;
144
+ prev.rooty = ev.rooty;
145
+ prev.smooth = prev.smooth || ev.smooth;
146
+ } else {
147
+ ev.coalesced = prev.coalesced;
148
+ ev.coalesced.push(ev);
149
+ pending.set(name, ev);
150
+ }
151
+ this.app._holdInput(this);
152
+ }
153
+
154
+ /** Deliver what `_queueInput` held: at the top of the next frame, and
155
+ * before any other event of this window's, so a handler sees events in
156
+ * the order they happened — a drag its last move, then the release. */
157
+ _flushInput() {
158
+ const pending = this._pendingInput;
159
+ if (pending === null) return;
160
+ this._pendingInput = null;
161
+ for (const [name, ev] of pending) this.emit(name, ev);
162
+ }
163
+
107
164
  /**
108
165
  * The bridge says the HWND exists. Composition is set up here and not at
109
166
  * construction because there is no HWND to target until now — the command
@@ -168,6 +225,7 @@ export class Win32Window {
168
225
  destroy() {
169
226
  if (this.destroyed) return;
170
227
  this.destroyed = true;
228
+ this._pendingInput = null;
171
229
  this._native.destroyWindow?.(this.id);
172
230
  this.app._unregister(this);
173
231
  }
@@ -577,7 +635,13 @@ export class Win32Window {
577
635
  this._native.endDraw(this.id);
578
636
  }
579
637
  }
580
- if (painted) this._native.commit();
638
+ // …and the panes over a `<glarea>` in this window, painted before this
639
+ // (src/win32/overlay.js), in the same commit: a frame whose only change
640
+ // was theirs still owes one
641
+ if (painted || this._owesCommit) {
642
+ this._owesCommit = false;
643
+ this._native.commit();
644
+ }
581
645
  }
582
646
 
583
647
  /**
@@ -604,6 +668,12 @@ export class Win32Window {
604
668
  );
605
669
  }
606
670
 
671
+ /** The frame is the ordinary paint walk, a pass per damage rect, so it
672
+ * paints through core's paint cache (src/nodes/window/flush.js). */
673
+ get usesPaintCache() {
674
+ return true;
675
+ }
676
+
607
677
  /** DirectComposition does not hold a frame back the way an X server's fence
608
678
  * or a WindowServer's buffer does: Commit is asynchronous and the surface
609
679
  * retains its own pixels. So there is never a frame in flight to wait for. */