react-x11 2.15.2 → 2.16.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/README.md +37 -0
- package/package.json +4 -3
- package/src/Reconciler.js +85 -22
- package/src/anchor.js +60 -18
- package/src/application.js +25 -1
- package/src/capabilities.js +349 -0
- package/src/cocoa/app.js +28 -9
- package/src/cocoa/context2d.js +139 -6
- package/src/cocoa/fonts.js +78 -0
- package/src/cocoa/presenter.js +17 -0
- package/src/cocoa/promotion.js +20 -0
- package/src/cocoa/relaunch.js +8 -3
- package/src/cocoa/symbols.js +64 -0
- package/src/cocoa/threaded.js +24 -4
- package/src/cocoa/window.js +362 -139
- package/src/components/ProgressBar.js +1 -1
- package/src/components/Slider.js +72 -39
- package/src/components/anchor.js +7 -2
- package/src/components/index.js +1 -0
- package/src/components/theme.js +32 -28
- package/src/dbusmenuexport.js +243 -0
- package/src/desktopcapabilityhooks.js +160 -0
- package/src/filedialoghooks.js +3 -5
- package/src/frame/childmain.js +8 -20
- package/src/frame/env.js +2 -10
- package/src/globalmenu.js +3 -205
- package/src/icontheme.js +240 -0
- package/src/imagesource.js +98 -3
- package/src/index.d.ts +1 -0
- package/src/index.js +11 -2
- package/src/launcher.js +235 -32
- package/src/launcherhooks.js +47 -28
- package/src/node.d.ts +7 -0
- package/src/nodes/animation.js +17 -47
- package/src/nodes/cascade.js +17 -2
- package/src/nodes/image.js +65 -2
- package/src/nodes/kinds.js +12 -0
- package/src/nodes/layout.js +5 -1
- package/src/nodes/node.js +17 -3
- package/src/nodes/paint.js +117 -0
- package/src/nodes/scope.js +259 -0
- package/src/nodes/scrollable.js +53 -6
- package/src/nodes/text.js +2 -0
- package/src/nodes/textarea.js +1 -1
- package/src/nodes/textinput.js +1 -1
- package/src/nodes/window/anchoring.js +45 -18
- package/src/nodes/window/flush.js +6 -5
- package/src/nodes/window/popup.js +10 -0
- package/src/nodes/window/size.js +40 -2
- package/src/nodes/window/window.js +41 -14
- package/src/registry.js +2 -1
- package/src/settings.js +332 -0
- package/src/statusnotifier.js +752 -0
- package/src/styles.js +212 -8
- package/src/symbols.js +200 -0
- package/src/testing/mock-app.js +10 -0
- package/src/trayhooks.js +193 -29
- package/src/types/capabilities.d.ts +139 -0
- package/src/types/components.d.ts +33 -0
- package/src/types/elements.d.ts +57 -6
- package/src/types/launcher.d.ts +50 -4
- package/src/types/style.d.ts +57 -0
- package/src/types/system.d.ts +104 -0
- package/src/types/tray.d.ts +64 -6
package/src/cocoa/window.js
CHANGED
|
@@ -12,11 +12,38 @@ import { CocoaLayerPresenter } from './presenter.js';
|
|
|
12
12
|
import { CocoaPromotion } from './promotion.js';
|
|
13
13
|
|
|
14
14
|
let nextWindowId = 1;
|
|
15
|
-
// How long a worker's flip may
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
15
|
+
// How long a worker's flip may hold its window's next frame (`_armFence`).
|
|
16
|
+
// The release is reported once the replacing frame has committed, a
|
|
17
|
+
// fraction of a millisecond later, and a window must not freeze on a report
|
|
18
|
+
// that never came.
|
|
19
19
|
const FENCE_TIMEOUT_MS = 100;
|
|
20
|
+
// How many buffers a window's swapchain may hold, the one on glass included.
|
|
21
|
+
// A buffer leaves glass and the WindowServer lets go of it a refresh or so
|
|
22
|
+
// later — a median of about 9ms on a 120Hz panel, and under 40 at the 99th
|
|
23
|
+
// percentile on a loaded machine — so a frame on the next refresh often
|
|
24
|
+
// finds the buffer its flip replaced still held, and takes the one before
|
|
25
|
+
// (`_takeBack`). Three, a `CAMetalLayer`'s depth, is what an animation
|
|
26
|
+
// mostly needs; an input answered between two of its frames puts two flips
|
|
27
|
+
// inside one refresh, and four kept every write of a clicked 120Hz animation
|
|
28
|
+
// off a held buffer (docs/macos.md, "Measured: the swapchain and the
|
|
29
|
+
// WindowServer's hold").
|
|
30
|
+
const MAX_BUFFERS = 4;
|
|
31
|
+
// Past this many rects, what a buffer missed is copied whole: a buffer left
|
|
32
|
+
// out for a while owes a rect list per frame, and one memcpy of the window
|
|
33
|
+
// is cheaper than keeping them.
|
|
34
|
+
const OWED_RECTS_MAX = 64;
|
|
35
|
+
// How long a window goes without a frame before a chain that grew past two
|
|
36
|
+
// buffers gives the rest back (`_trimChain`).
|
|
37
|
+
const TRIM_AFTER_MS = 1000;
|
|
38
|
+
|
|
39
|
+
/** What a buffer owes once `damage` has been presented without it: the
|
|
40
|
+
* rects it missed, or 'full'. */
|
|
41
|
+
function owe(owed, damage) {
|
|
42
|
+
if (owed === 'full' || damage === 'full') return 'full';
|
|
43
|
+
if (!owed) return damage;
|
|
44
|
+
const rects = owed.concat(damage);
|
|
45
|
+
return rects.length > OWED_RECTS_MAX ? 'full' : rects;
|
|
46
|
+
}
|
|
20
47
|
|
|
21
48
|
export class CocoaWindow {
|
|
22
49
|
constructor(app, attributes = {}) {
|
|
@@ -44,28 +71,35 @@ export class CocoaWindow {
|
|
|
44
71
|
this._presentedAt = -Infinity;
|
|
45
72
|
|
|
46
73
|
const s = this.scale;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
this.width = snap(attributes.width, 640);
|
|
54
|
-
this.height = snap(attributes.height, 480);
|
|
74
|
+
const born = this.snapSize(
|
|
75
|
+
attributes.width ?? 640,
|
|
76
|
+
attributes.height ?? 480,
|
|
77
|
+
);
|
|
78
|
+
this.width = born.width;
|
|
79
|
+
this.height = born.height;
|
|
55
80
|
this.title = attributes.title ?? '';
|
|
56
81
|
this._popup = attributes.overrideRedirect === true;
|
|
82
|
+
// A popup that takes the keyboard (`<popup grabKeyboard>`): AppKit's
|
|
83
|
+
// popup panel can never become key, so this one is a borderless window
|
|
84
|
+
// that can, at the popup level, and it activates the app when it shows
|
|
85
|
+
// — how NSPopover's own window behaves. It is decided here because the
|
|
86
|
+
// kind of window is: a later change of the prop does not remake it.
|
|
87
|
+
this._keyPopup = this._popup && attributes.grabKeyboard === true;
|
|
57
88
|
|
|
58
89
|
const options = {
|
|
59
90
|
width: this.width / s,
|
|
60
91
|
height: this.height / s,
|
|
61
92
|
title: this.title,
|
|
62
|
-
kind: this.
|
|
63
|
-
? '
|
|
64
|
-
:
|
|
65
|
-
? '
|
|
66
|
-
:
|
|
93
|
+
kind: this._keyPopup
|
|
94
|
+
? 'borderless'
|
|
95
|
+
: this._popup
|
|
96
|
+
? 'popup'
|
|
97
|
+
: attributes.decorations === false
|
|
98
|
+
? 'borderless'
|
|
99
|
+
: 'normal',
|
|
67
100
|
resizable: attributes.resizable !== false,
|
|
68
101
|
};
|
|
102
|
+
if (this._keyPopup) options.level = 'popup';
|
|
69
103
|
if (typeof attributes.x === 'number' && typeof attributes.y === 'number') {
|
|
70
104
|
options.x = attributes.x / s;
|
|
71
105
|
options.y = attributes.y / s;
|
|
@@ -104,13 +138,11 @@ export class CocoaWindow {
|
|
|
104
138
|
// (`_frameSize`)
|
|
105
139
|
this._points = null;
|
|
106
140
|
// Threaded mode's swapchain fence (`frameInFlight`): the IOSurface id
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// catch-up copy the back buffer is owed once it is back.
|
|
110
|
-
this._onGlass = null;
|
|
141
|
+
// the last flip is taking off glass, until the bridge says the flip has
|
|
142
|
+
// been applied.
|
|
111
143
|
this._awaiting = null;
|
|
112
|
-
this._catchUp = null;
|
|
113
144
|
this._fenceTimer = null;
|
|
145
|
+
this._trimTimer = null;
|
|
114
146
|
this._shadowTimer = null;
|
|
115
147
|
// AppKit's live resize, between the bridge's `window-live-resize` begin
|
|
116
148
|
// and end (`CocoaApp._routeLiveResize`): a tick of it lays out with the
|
|
@@ -241,10 +273,32 @@ export class CocoaWindow {
|
|
|
241
273
|
this._refreshFrameInterval();
|
|
242
274
|
}
|
|
243
275
|
|
|
276
|
+
/**
|
|
277
|
+
* The size this window really takes for a request of `width` x `height`
|
|
278
|
+
* device pixels — **whole POINTS**, because that is the grid AppKit puts a
|
|
279
|
+
* window on. An odd device-pixel request at scale 2 comes back one short
|
|
280
|
+
* in the resize echo, which churns the backing surface — each swap an
|
|
281
|
+
* uninitialized canvas only the next damage rect repaints — and which the
|
|
282
|
+
* renderer reads as somebody else having set the size, ending an
|
|
283
|
+
* `'auto'` window's authority over its own for good (#586). So the
|
|
284
|
+
* renderer asks this what it is going to get and records that instead
|
|
285
|
+
* (`WindowNode._snapSize`).
|
|
286
|
+
*
|
|
287
|
+
* **Up**, not to the nearest: a window a device pixel taller than its
|
|
288
|
+
* content shows all of it, and one a device pixel shorter clips it.
|
|
289
|
+
*/
|
|
290
|
+
snapSize(width, height) {
|
|
291
|
+
const s = this.scale;
|
|
292
|
+
const up = (v) =>
|
|
293
|
+
Math.max(1, Math.ceil(Math.max(1, Math.round(v)) / s) * s);
|
|
294
|
+
return { width: up(width), height: up(height) };
|
|
295
|
+
}
|
|
296
|
+
|
|
244
297
|
resize(width, height) {
|
|
245
298
|
const s = this.scale;
|
|
246
|
-
|
|
247
|
-
this.
|
|
299
|
+
const size = this.snapSize(width, height);
|
|
300
|
+
this.width = size.width;
|
|
301
|
+
this.height = size.height;
|
|
248
302
|
this._native.setWindowFrame(
|
|
249
303
|
this._h,
|
|
250
304
|
null,
|
|
@@ -274,9 +328,10 @@ export class CocoaWindow {
|
|
|
274
328
|
// so a window that was hidden behind one, unmapped and mapped again
|
|
275
329
|
// does not wait on an event that may already have been delivered.
|
|
276
330
|
this._occluded = false;
|
|
277
|
-
// A popup must not take the keyboard from its owner
|
|
278
|
-
// map is the app coming up and
|
|
279
|
-
|
|
331
|
+
// A popup must not take the keyboard from its owner, unless taking it
|
|
332
|
+
// is what it is for; a toplevel's first map is the app coming up and
|
|
333
|
+
// takes it.
|
|
334
|
+
this._native.showWindow(this._h, !this._popup || this._keyPopup);
|
|
280
335
|
this._refreshOrigin();
|
|
281
336
|
this._refreshFrameInterval();
|
|
282
337
|
}
|
|
@@ -434,28 +489,36 @@ export class CocoaWindow {
|
|
|
434
489
|
// --- drawing -------------------------------------------------------------
|
|
435
490
|
|
|
436
491
|
/**
|
|
437
|
-
* The backing store is
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
* a window-sized upload. Falls back to the single plain surface
|
|
445
|
-
* IOSurface creation fails.
|
|
492
|
+
* The backing store is an IOSurface swapchain: painters draw into the
|
|
493
|
+
* back buffer's CG bitmap, and presenting is `layer.contents = iosurface`
|
|
494
|
+
* — zero-copy, where the plain-surface path paid a window-sized CGImage
|
|
495
|
+
* copy per dirty frame (12ms at 900x700@2x — the presenter bench's whole
|
|
496
|
+
* surface-vs-layers gap on bounded damage). A buffer that has been on
|
|
497
|
+
* glass is behind by the frames presented since, so the frame that takes
|
|
498
|
+
* it copies what it missed across first — a damage-sized memcpy
|
|
499
|
+
* replacing a window-sized upload. Falls back to the single plain surface
|
|
500
|
+
* where IOSurface creation fails.
|
|
501
|
+
*
|
|
502
|
+
* Which buffer that is, and when, is the WindowServer's to say (#602). It
|
|
503
|
+
* composites out of the buffer it was handed, and lets go of one a
|
|
504
|
+
* refresh or so after a flip replaced it — on a worker, not before the
|
|
505
|
+
* flip has even been applied. A write into a buffer it still holds is a
|
|
506
|
+
* write into what it may be compositing, and a composite caught mid-write
|
|
507
|
+
* is half a frame on glass. So no buffer is chosen at the flip: the next
|
|
508
|
+
* frame takes one at its first draw, from those the WindowServer has let
|
|
509
|
+
* go of (`_takeBack`).
|
|
446
510
|
*
|
|
447
|
-
* A new size retires the
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
511
|
+
* A new size retires the chain, released on the spot (`_releaseBacking`):
|
|
512
|
+
* left to the handles' finalizers, a forty-tick drag of the two-buffer
|
|
513
|
+
* chain this used to be held 800MB until a collection happened to run —
|
|
514
|
+
* the `rss +80MB` docs/macos.md measured. The layer keeps its own
|
|
515
|
+
* reference to whichever IOSurface it is still showing, so the free is
|
|
516
|
+
* safe while that frame is on glass. A new chain starts with the one
|
|
517
|
+
* buffer its first frame paints: a live-resize tick paints its size once,
|
|
518
|
+
* and the next tick's size needs a chain of its own. A chain that grew
|
|
519
|
+
* past two gives the rest back once the window stops (`_armTrim`).
|
|
454
520
|
*/
|
|
455
521
|
_ensureSurface() {
|
|
456
|
-
// a paint that did not wait for the fence: the back buffer gets its
|
|
457
|
-
// catch-up now, so what it paints lands over the frame before it
|
|
458
|
-
this._settleBack();
|
|
459
522
|
const w = this.width;
|
|
460
523
|
const h = this.height;
|
|
461
524
|
if (
|
|
@@ -466,13 +529,12 @@ export class CocoaWindow {
|
|
|
466
529
|
const hadSurface = Boolean(this._surface);
|
|
467
530
|
this._releaseBacking();
|
|
468
531
|
try {
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
this._chain = {
|
|
472
|
-
this._native.surfaceLock(
|
|
473
|
-
this._native.ctxClearRect(
|
|
474
|
-
this.
|
|
475
|
-
this._surface = a.handle;
|
|
532
|
+
const back = this._newBuffer(w, h);
|
|
533
|
+
back.owed = null;
|
|
534
|
+
this._chain = { buffers: [back], front: null, back };
|
|
535
|
+
this._native.surfaceLock(back.handle);
|
|
536
|
+
this._native.ctxClearRect(back.handle, 0, 0, w, h);
|
|
537
|
+
this._surface = back.handle;
|
|
476
538
|
} catch {
|
|
477
539
|
this._surface = this._native.createSurface(w, h, this.scale);
|
|
478
540
|
this._native.ctxClearRect(this._surface, 0, 0, w, h);
|
|
@@ -491,33 +553,193 @@ export class CocoaWindow {
|
|
|
491
553
|
// frames that all ran on the mouse release — the freeze after a
|
|
492
554
|
// resize, measured at seconds on a large tree.
|
|
493
555
|
if (hadSurface) this._freshSurface = true;
|
|
556
|
+
} else if (this._chain && !this._chain.back) {
|
|
557
|
+
this._takeBack();
|
|
494
558
|
}
|
|
495
559
|
return this._surface;
|
|
496
560
|
}
|
|
497
561
|
|
|
562
|
+
/** A buffer for the chain, owing everything: nothing has been drawn in it. */
|
|
563
|
+
_newBuffer(w, h) {
|
|
564
|
+
const { handle, iosurfaceId } = this._native.createSurfaceIOSurface(
|
|
565
|
+
w,
|
|
566
|
+
h,
|
|
567
|
+
this.scale,
|
|
568
|
+
);
|
|
569
|
+
return {
|
|
570
|
+
handle,
|
|
571
|
+
iosurfaceId,
|
|
572
|
+
// what it missed while another buffer was on glass: rects, or 'full'
|
|
573
|
+
owed: 'full',
|
|
574
|
+
// when a flip last took it off glass
|
|
575
|
+
offGlassAt: -Infinity,
|
|
576
|
+
// false from a worker's flip that took it off glass until the bridge
|
|
577
|
+
// reports that flip applied (`_surfaceReleased`)
|
|
578
|
+
released: true,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
498
582
|
/**
|
|
499
|
-
*
|
|
500
|
-
* the
|
|
501
|
-
*
|
|
502
|
-
*
|
|
583
|
+
* The buffer the frame about to be drawn goes into, taken at its first
|
|
584
|
+
* draw rather than at the flip before it — as late as the frame allows,
|
|
585
|
+
* so the WindowServer has had as long as it can to let go.
|
|
586
|
+
*
|
|
587
|
+
* Of the buffers not on glass, one it has let go of: `surfaceIsInUse`
|
|
588
|
+
* false, and on a worker the flip that retired it applied. Of those, the
|
|
589
|
+
* one on glass most recently, which missed the fewest frames. When every
|
|
590
|
+
* one is still held — the buffer the last flip replaced usually is, and a
|
|
591
|
+
* burst of flips inside one refresh holds more — a new buffer, while the
|
|
592
|
+
* chain has room for one. Past that, the one off glass longest: the
|
|
593
|
+
* likeliest to have been let go of by now, and the one a two-buffer chain
|
|
594
|
+
* always drew into.
|
|
595
|
+
*
|
|
596
|
+
* Then what it missed, copied across from the frame on glass.
|
|
597
|
+
*/
|
|
598
|
+
_takeBack() {
|
|
599
|
+
const chain = this._chain;
|
|
600
|
+
let free = null;
|
|
601
|
+
let oldest = null;
|
|
602
|
+
for (const buffer of chain.buffers) {
|
|
603
|
+
if (buffer === chain.front) continue;
|
|
604
|
+
if (
|
|
605
|
+
!oldest ||
|
|
606
|
+
(buffer.released && !oldest.released) ||
|
|
607
|
+
(buffer.released === oldest.released &&
|
|
608
|
+
buffer.offGlassAt < oldest.offGlassAt)
|
|
609
|
+
) {
|
|
610
|
+
oldest = buffer;
|
|
611
|
+
}
|
|
612
|
+
if (!buffer.released || this._held(buffer)) continue;
|
|
613
|
+
if (!free || buffer.offGlassAt > free.offGlassAt) free = buffer;
|
|
614
|
+
}
|
|
615
|
+
let back = free;
|
|
616
|
+
if (!back && chain.buffers.length < MAX_BUFFERS) {
|
|
617
|
+
try {
|
|
618
|
+
back = this._newBuffer(
|
|
619
|
+
this._surfaceSize.width,
|
|
620
|
+
this._surfaceSize.height,
|
|
621
|
+
);
|
|
622
|
+
chain.buffers.push(back);
|
|
623
|
+
if (chain.buffers.length > 2) this._armTrim();
|
|
624
|
+
} catch {
|
|
625
|
+
// no memory for another: the chain draws with what it has
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// …and a chain of one, whose next buffer could not be made, draws where
|
|
629
|
+
// it shows, as the plain surface does
|
|
630
|
+
back ??= oldest ?? chain.front;
|
|
631
|
+
chain.back = back;
|
|
632
|
+
this._surface = back.handle;
|
|
633
|
+
// a different native surface owns the graphics state now — the context
|
|
634
|
+
// re-syncs its sticky state off the generation
|
|
635
|
+
this._surfaceGen++;
|
|
636
|
+
this._native.surfaceLock(back.handle);
|
|
637
|
+
if (back.owed && back !== chain.front) {
|
|
638
|
+
this._native.copySurfaceRegion(
|
|
639
|
+
chain.front.handle,
|
|
640
|
+
back.handle,
|
|
641
|
+
back.owed === 'full'
|
|
642
|
+
? null
|
|
643
|
+
: back.owed.flatMap((r) => [
|
|
644
|
+
Math.floor(r.x),
|
|
645
|
+
Math.floor(r.y),
|
|
646
|
+
Math.ceil(r.width) + 1,
|
|
647
|
+
Math.ceil(r.height) + 1,
|
|
648
|
+
]),
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
back.owed = null;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* A chain that grew past two buffers gives the rest back once the window
|
|
656
|
+
* has gone `TRIM_AFTER_MS` without a frame. The extra buffers are what an
|
|
657
|
+
* animation needs, a window-sized IOSurface each, and a window that has
|
|
658
|
+
* stopped keeps the two a still window always had: the one on glass, and
|
|
659
|
+
* the one on glass before it — which owes the least, so the next frame
|
|
660
|
+
* takes it and makes nothing. A frame drawn and not yet presented keeps
|
|
661
|
+
* its own buffer instead.
|
|
662
|
+
*/
|
|
663
|
+
_armTrim() {
|
|
664
|
+
if (this._trimTimer) return;
|
|
665
|
+
this._trimTimer = setTimeout(() => {
|
|
666
|
+
this._trimTimer = null;
|
|
667
|
+
if (!this.destroyed && !this._trimChain()) this._armTrim();
|
|
668
|
+
}, TRIM_AFTER_MS);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** The trim, at `now`: false when the window is still painting, and the
|
|
672
|
+
* trim has to look again later. */
|
|
673
|
+
_trimChain(now = performance.now()) {
|
|
674
|
+
const chain = this._chain;
|
|
675
|
+
if (!chain || chain.buffers.length <= 2) return true;
|
|
676
|
+
// a worker's flip not yet applied names a buffer the release will look for
|
|
677
|
+
if (now - this._presentedAt < TRIM_AFTER_MS || this._awaiting != null) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
let keep = chain.back;
|
|
681
|
+
if (!keep) {
|
|
682
|
+
for (const buffer of chain.buffers) {
|
|
683
|
+
if (buffer === chain.front) continue;
|
|
684
|
+
if (
|
|
685
|
+
!keep ||
|
|
686
|
+
(buffer.released && !keep.released) ||
|
|
687
|
+
(buffer.released === keep.released &&
|
|
688
|
+
buffer.offGlassAt > keep.offGlassAt)
|
|
689
|
+
) {
|
|
690
|
+
keep = buffer;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const release = this._native.releaseSurface;
|
|
695
|
+
const kept = [];
|
|
696
|
+
for (const buffer of chain.buffers) {
|
|
697
|
+
if (buffer === chain.front || buffer === keep) {
|
|
698
|
+
kept.push(buffer);
|
|
699
|
+
} else if (typeof release === 'function') {
|
|
700
|
+
release.call(this._native, buffer.handle);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
chain.buffers = kept;
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** Whether the WindowServer still holds `buffer`: `IOSurfaceIsInUse`. A
|
|
708
|
+
* bridge that cannot say (before 0.10) is answered no, which is the
|
|
709
|
+
* two-buffer chain this was. */
|
|
710
|
+
_held(buffer) {
|
|
711
|
+
const inUse = this._native.surfaceIsInUse;
|
|
712
|
+
return (
|
|
713
|
+
typeof inUse === 'function' &&
|
|
714
|
+
inUse.call(this._native, buffer.handle) === true
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Free the backing store now — the swapchain's buffers, or the plain
|
|
720
|
+
* surface the fallback holds — rather than when V8 collects the handles.
|
|
721
|
+
* Bridges before 0.4 have no `releaseSurface`; there the finalizer is
|
|
722
|
+
* still the only owner, and this is the drop it always was.
|
|
503
723
|
*/
|
|
504
724
|
_releaseBacking() {
|
|
505
725
|
const release = this._native.releaseSurface;
|
|
506
726
|
if (typeof release === 'function') {
|
|
507
727
|
if (this._chain) {
|
|
508
|
-
|
|
509
|
-
|
|
728
|
+
for (const buffer of this._chain.buffers) {
|
|
729
|
+
release.call(this._native, buffer.handle);
|
|
730
|
+
}
|
|
510
731
|
} else if (this._surface) {
|
|
511
732
|
release.call(this._native, this._surface);
|
|
512
733
|
}
|
|
513
734
|
}
|
|
514
735
|
this._chain = null;
|
|
515
736
|
this._surface = null;
|
|
516
|
-
// a new
|
|
737
|
+
// a new chain owes nothing to what the old one was showing
|
|
517
738
|
clearTimeout(this._fenceTimer);
|
|
518
739
|
this._fenceTimer = null;
|
|
519
740
|
this._awaiting = null;
|
|
520
|
-
this.
|
|
741
|
+
clearTimeout(this._trimTimer);
|
|
742
|
+
this._trimTimer = null;
|
|
521
743
|
}
|
|
522
744
|
|
|
523
745
|
/**
|
|
@@ -593,13 +815,21 @@ export class CocoaWindow {
|
|
|
593
815
|
* rect, and a delta that leaves no surviving band reports false so the
|
|
594
816
|
* caller falls back to the plain repaint. */
|
|
595
817
|
scrollRegion(rect, dx, dy) {
|
|
596
|
-
|
|
597
|
-
//
|
|
598
|
-
|
|
599
|
-
|
|
818
|
+
// Nothing painted yet, or a size the next paint makes a new chain for:
|
|
819
|
+
// there is no band to move.
|
|
820
|
+
if (
|
|
821
|
+
!this._surface ||
|
|
822
|
+
this._surfaceSize?.width !== this.width ||
|
|
823
|
+
this._surfaceSize?.height !== this.height
|
|
824
|
+
) {
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
600
827
|
if (!Number.isInteger(dx) || !Number.isInteger(dy)) return false;
|
|
828
|
+
// The band moves in the buffer this frame draws into — taken, and
|
|
829
|
+
// caught up to the frame on glass, before anything moves in it.
|
|
830
|
+
const surface = this._ensureSurface();
|
|
601
831
|
const moved = this._native.scrollSurface(
|
|
602
|
-
|
|
832
|
+
surface,
|
|
603
833
|
Math.round(rect.x),
|
|
604
834
|
Math.round(rect.y),
|
|
605
835
|
Math.round(rect.width),
|
|
@@ -609,11 +839,11 @@ export class CocoaWindow {
|
|
|
609
839
|
);
|
|
610
840
|
if (moved) {
|
|
611
841
|
this._dirty = true;
|
|
612
|
-
// The band moved inside the BACK buffer only. After the flip
|
|
613
|
-
// other buffer still holds the band where it was, and
|
|
614
|
-
//
|
|
842
|
+
// The band moved inside the BACK buffer only. After the flip every
|
|
843
|
+
// other buffer still holds the band where it was, and what a buffer
|
|
844
|
+
// is owed only covers what the flush painted — the strips the shift
|
|
615
845
|
// exposed — so the next frame would blit a band one frame stale.
|
|
616
|
-
// Record the shifted rect as painted, and the
|
|
846
|
+
// Record the shifted rect as painted, and the catch-up carries it.
|
|
617
847
|
this.noteFrameDamage([
|
|
618
848
|
{
|
|
619
849
|
x: Math.round(rect.x),
|
|
@@ -627,14 +857,14 @@ export class CocoaWindow {
|
|
|
627
857
|
}
|
|
628
858
|
|
|
629
859
|
/**
|
|
630
|
-
* Whether this window's last
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
860
|
+
* Whether this window's last flip has yet to reach the layer — the X11
|
|
861
|
+
* contract's fence (src/frames.js). The pump never needs it: a pump-mode
|
|
862
|
+
* flip is applied in the call. A worker's flip is a command the UI thread
|
|
863
|
+
* applies later, and until the bridge reports the buffer it replaced as
|
|
864
|
+
* released (`_surfaceReleased`), that buffer is still what is on glass,
|
|
865
|
+
* and a frame painted now would be one the UI thread has not caught up
|
|
866
|
+
* with. A new size is never in flight: it paints into a new chain that
|
|
867
|
+
* nothing is showing.
|
|
638
868
|
*
|
|
639
869
|
* And while a batch is being routed (`CocoaApp._routeBatch`), its frame
|
|
640
870
|
* is the one owed: it goes out when the batch is done, answering every
|
|
@@ -660,10 +890,13 @@ export class CocoaWindow {
|
|
|
660
890
|
* buffer the first flip had taken off glass microseconds before, while the
|
|
661
891
|
* WindowServer could still be compositing from it. A scroll's frame blits,
|
|
662
892
|
* then repaints what the shift got wrong, so a composite caught between the
|
|
663
|
-
* two
|
|
664
|
-
* frame
|
|
665
|
-
*
|
|
666
|
-
*
|
|
893
|
+
* two showed the blit alone — a held sticky header dragged up a pixel for a
|
|
894
|
+
* frame. The swapchain no longer draws into a buffer the WindowServer
|
|
895
|
+
* holds (`_takeBack`), but one refresh still shows one frame: the rest of a
|
|
896
|
+
* burst is work nobody sees, and each flip of it would hold a buffer. A
|
|
897
|
+
* resize paints into a chain it has just made, and a press or a key does
|
|
898
|
+
* not come in bursts inside a refresh — the one answered between two
|
|
899
|
+
* frames of an animation is what the chain's fourth buffer is for.
|
|
667
900
|
*
|
|
668
901
|
* Off with the interval: `frameInterval: 0` asks for no pacing at all.
|
|
669
902
|
*/
|
|
@@ -679,12 +912,10 @@ export class CocoaWindow {
|
|
|
679
912
|
|
|
680
913
|
/**
|
|
681
914
|
* Push the backing surface at the WindowServer, if anything drew — and
|
|
682
|
-
* tell the window node what it cost. The flip is cheap
|
|
683
|
-
* copy
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
* by the thread's time, so the present reports in (src/pacing.js,
|
|
687
|
-
* `WindowNode._notePresentCost`).
|
|
915
|
+
* tell the window node what it cost. The flip itself is cheap, and the
|
|
916
|
+
* catch-up copy is the next frame's (`_takeBack`), inside its flush; the
|
|
917
|
+
* present still reports in, since the frame pacer prices a frame by the
|
|
918
|
+
* thread's time (src/pacing.js, `WindowNode._notePresentCost`).
|
|
688
919
|
*/
|
|
689
920
|
present() {
|
|
690
921
|
const started = performance.now();
|
|
@@ -701,33 +932,39 @@ export class CocoaWindow {
|
|
|
701
932
|
if (this._holdPresent || !this._visible()) return false;
|
|
702
933
|
this._dirty = false;
|
|
703
934
|
this._presentedAt = performance.now();
|
|
704
|
-
|
|
705
|
-
|
|
935
|
+
const chain = this._chain;
|
|
936
|
+
if (chain) {
|
|
937
|
+
const shown = chain.back;
|
|
706
938
|
this._native.surfaceUnlock(shown.handle);
|
|
707
939
|
this._flip(() =>
|
|
708
940
|
this._native.setLayerContentsIOSurface(this._layer, shown.iosurfaceId),
|
|
709
941
|
);
|
|
710
|
-
this.
|
|
711
|
-
this
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
// context re-syncs its sticky state off the generation
|
|
715
|
-
this._surfaceGen++;
|
|
716
|
-
const catchUp = { from: shown.handle, damage: this._flushDamage };
|
|
942
|
+
// Every other buffer has missed this frame. Nothing is copied into
|
|
943
|
+
// any of them now: the buffer this flip takes off glass is the one
|
|
944
|
+
// the WindowServer is surest to be holding (`_takeBack`).
|
|
945
|
+
const damage = this._flushDamage ?? 'full';
|
|
717
946
|
this._flushDamage = null;
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
947
|
+
for (const buffer of chain.buffers) {
|
|
948
|
+
if (buffer !== shown) buffer.owed = owe(buffer.owed, damage);
|
|
949
|
+
}
|
|
950
|
+
const retired = chain.front;
|
|
951
|
+
chain.front = shown;
|
|
952
|
+
chain.back = null;
|
|
953
|
+
// Until the next frame takes a buffer, the surface is the frame on
|
|
954
|
+
// glass: a read gets the picture presented, and nothing here draws
|
|
955
|
+
// into it — every draw asks `_ensureSurface` first.
|
|
956
|
+
this._surface = shown.handle;
|
|
957
|
+
// A new chain's first flip replaces a buffer of the old one, or
|
|
958
|
+
// nothing, and a buffer flipped again takes nothing off glass.
|
|
959
|
+
if (retired && retired !== shown) {
|
|
960
|
+
retired.offGlassAt = this._presentedAt;
|
|
961
|
+
// On a worker the flip has not happened yet: the next frame waits
|
|
962
|
+
// for the bridge to say it has (`frameInFlight`).
|
|
963
|
+
if (this.app._threaded) {
|
|
964
|
+
retired.released = false;
|
|
965
|
+
this._awaiting = retired.iosurfaceId;
|
|
966
|
+
this._armFence();
|
|
967
|
+
}
|
|
731
968
|
}
|
|
732
969
|
this._shadowAfterFlip();
|
|
733
970
|
return true;
|
|
@@ -774,49 +1011,35 @@ export class CocoaWindow {
|
|
|
774
1011
|
return { width: this.width / s, height: this.height / s };
|
|
775
1012
|
}
|
|
776
1013
|
|
|
777
|
-
/**
|
|
778
|
-
|
|
779
|
-
_settle({ from, damage }) {
|
|
780
|
-
this._native.surfaceLock(this._surface);
|
|
781
|
-
this._native.copySurfaceRegion(
|
|
782
|
-
from,
|
|
783
|
-
this._surface,
|
|
784
|
-
damage === 'full' || !damage
|
|
785
|
-
? null
|
|
786
|
-
: damage.flatMap((r) => [
|
|
787
|
-
Math.floor(r.x),
|
|
788
|
-
Math.floor(r.y),
|
|
789
|
-
Math.ceil(r.width) + 1,
|
|
790
|
-
Math.ceil(r.height) + 1,
|
|
791
|
-
]),
|
|
792
|
-
);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/** Stop waiting on the fence: the back buffer's catch-up, now. */
|
|
796
|
-
_settleBack() {
|
|
797
|
-
if (this._awaiting == null) return;
|
|
1014
|
+
/** Stop waiting on the fence. */
|
|
1015
|
+
_stopWaiting() {
|
|
798
1016
|
clearTimeout(this._fenceTimer);
|
|
799
1017
|
this._fenceTimer = null;
|
|
800
1018
|
this._awaiting = null;
|
|
801
|
-
const catchUp = this._catchUp;
|
|
802
|
-
this._catchUp = null;
|
|
803
|
-
if (catchUp && this._chain) this._settle(catchUp);
|
|
804
1019
|
}
|
|
805
1020
|
|
|
806
|
-
/** `surface-released` for `id`: true when
|
|
807
|
-
*
|
|
1021
|
+
/** `surface-released` for `id`: true when the buffer is this window's. It
|
|
1022
|
+
* may be drawn into again once the WindowServer lets go of it too. */
|
|
808
1023
|
_surfaceReleased(id) {
|
|
809
|
-
|
|
810
|
-
|
|
1024
|
+
const buffer = this._chain?.buffers.find((b) => b.iosurfaceId === id);
|
|
1025
|
+
if (!buffer) return false;
|
|
1026
|
+
buffer.released = true;
|
|
1027
|
+
if (this._awaiting === id) this._stopWaiting();
|
|
811
1028
|
return true;
|
|
812
1029
|
}
|
|
813
1030
|
|
|
1031
|
+
/**
|
|
1032
|
+
* A release that never comes stops holding the window's frames after a
|
|
1033
|
+
* moment. It does not make the buffer free: a flip the UI thread has not
|
|
1034
|
+
* applied still has that buffer on glass, and the next frame takes
|
|
1035
|
+
* another (`_takeBack`) until the release does come.
|
|
1036
|
+
*/
|
|
814
1037
|
_armFence() {
|
|
815
1038
|
clearTimeout(this._fenceTimer);
|
|
816
1039
|
this._fenceTimer = setTimeout(() => {
|
|
817
1040
|
this._fenceTimer = null;
|
|
818
1041
|
if (this._awaiting == null || this.destroyed) return;
|
|
819
|
-
this.
|
|
1042
|
+
this._stopWaiting();
|
|
820
1043
|
this.app._tickFrames();
|
|
821
1044
|
this.app._presentAll();
|
|
822
1045
|
}, FENCE_TIMEOUT_MS);
|