react-x11 2.17.0 → 2.17.1
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/package.json +1 -1
- package/src/cocoa/app.js +9 -5
- package/src/cocoa/glarea.js +48 -10
- package/src/cocoa/window.js +21 -0
- package/src/glnodes.js +9 -6
package/package.json
CHANGED
package/src/cocoa/app.js
CHANGED
|
@@ -41,7 +41,7 @@ import { CocoaFontManager } from './fonts.js';
|
|
|
41
41
|
import { releaseImageUpload } from '../backend/context2d.js';
|
|
42
42
|
import { CocoaSurface } from './surface.js';
|
|
43
43
|
import { CocoaSymbols } from './symbols.js';
|
|
44
|
-
import { CocoaWindow } from './window.js';
|
|
44
|
+
import { CocoaWindow, FRAME_SLACK_MS } from './window.js';
|
|
45
45
|
import { decodeKey, modifierMask } from './keymap.js';
|
|
46
46
|
import { loadNative } from './native.js';
|
|
47
47
|
import { requestAppKit, threadedChannel } from './threaded.js';
|
|
@@ -55,9 +55,6 @@ import { requestAppKit, threadedChannel } from './threaded.js';
|
|
|
55
55
|
// bridge that does not report it: a 60Hz floor every Mac clears.
|
|
56
56
|
const RAF_INTERVAL_MS = 16;
|
|
57
57
|
const PUMP_INTERVAL_MS = 8;
|
|
58
|
-
// How early a pump tick may take a frame that is not quite due, in ms — the
|
|
59
|
-
// drift of a timer, not a fraction of the pump (`_frameDue`).
|
|
60
|
-
const FRAME_SLACK_MS = 1;
|
|
61
58
|
// Threaded mode's live-resize handshake (windowkit/appkit#53): how long
|
|
62
59
|
// AppKit may hold a resize tick for a frame painted at the new size, in ms.
|
|
63
60
|
// The edge moves when the delegate returns, so the budget is how long a
|
|
@@ -906,6 +903,13 @@ export class CocoaApp {
|
|
|
906
903
|
* tick alone still does is pump AppKit's events, which is what
|
|
907
904
|
* `pumpInterval` stays the cadence of. With no pump at all — threaded
|
|
908
905
|
* mode, where `_pumpInterval` is Infinity — every frame is one of these.
|
|
906
|
+
*
|
|
907
|
+
* The wait is rounded **up** to whole milliseconds, because `setTimeout`
|
|
908
|
+
* only counts those: a 120Hz panel's 8.333ms gate rounded to 8 fired
|
|
909
|
+
* before the clock would take the frame, which cost a second timer per
|
|
910
|
+
* frame to cover the last third of a millisecond. Rounding up costs the
|
|
911
|
+
* grid nothing — `_frameDue` anchors the next slot to the last one, not
|
|
912
|
+
* to when the frame ran — and lands on the first tick the clock accepts.
|
|
909
913
|
*/
|
|
910
914
|
_armFrameTimer(wait, now) {
|
|
911
915
|
if (!(wait > 0 && wait < this._pumpInterval)) return;
|
|
@@ -922,7 +926,7 @@ export class CocoaApp {
|
|
|
922
926
|
this._tickFrames();
|
|
923
927
|
this._presentAll();
|
|
924
928
|
},
|
|
925
|
-
Math.max(1, Math.
|
|
929
|
+
Math.max(1, Math.ceil(wait)),
|
|
926
930
|
);
|
|
927
931
|
}
|
|
928
932
|
|
package/src/cocoa/glarea.js
CHANGED
|
@@ -231,6 +231,16 @@ export class CocoaGLArea {
|
|
|
231
231
|
return this.parent.requestAnimationFrame(cb);
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* The owning window's clock, asked where a gate of `interval` ms lands
|
|
236
|
+
* on its grid (`CocoaWindow.nextFrameAt`) — the swap gate phase-locks to
|
|
237
|
+
* it so the two do not stack. A parent with no clock (a bare stub in a
|
|
238
|
+
* test) answers one interval from now, the old swap-relative wait.
|
|
239
|
+
*/
|
|
240
|
+
nextFrameAt(interval) {
|
|
241
|
+
return this.parent.nextFrameAt?.(interval) ?? performance.now() + interval;
|
|
242
|
+
}
|
|
243
|
+
|
|
234
244
|
getContext(kind, config) {
|
|
235
245
|
if (kind !== 'opengl' || this.destroyed) return null;
|
|
236
246
|
if (!this._context) {
|
|
@@ -262,7 +272,10 @@ function createGLAreaContext(runtime, area, config) {
|
|
|
262
272
|
let back = null;
|
|
263
273
|
let width = 0;
|
|
264
274
|
let height = 0;
|
|
265
|
-
|
|
275
|
+
// The moment the swap gate reopens, on the window clock's grid — not a
|
|
276
|
+
// boolean a timer flips, so a frame that arrives a fraction of a
|
|
277
|
+
// millisecond before its own timer still reads the true state.
|
|
278
|
+
let gateOpenAt = -Infinity;
|
|
266
279
|
let gateTimer = null;
|
|
267
280
|
let destroyed = false;
|
|
268
281
|
|
|
@@ -289,7 +302,17 @@ function createGLAreaContext(runtime, area, config) {
|
|
|
289
302
|
// one, so the honest gate is one display period per swap — the same
|
|
290
303
|
// timer-reopened gate the XQuartz CGL flavor uses (no backpressure
|
|
291
304
|
// exists on either).
|
|
292
|
-
|
|
305
|
+
//
|
|
306
|
+
// One period *per frame*, though, not per swap: timed from the swap the
|
|
307
|
+
// gate reopened at `slot + drawCost + period`, which overshoots the
|
|
308
|
+
// window clock's next slot however cheap the frame, and the clock then
|
|
309
|
+
// rounded it up to the slot after — two display periods, a 120Hz panel
|
|
310
|
+
// pinned at 60fps whatever the scene cost (issue #631). Anchored to the
|
|
311
|
+
// slot the frame was due at (`CocoaWindow.nextFrameAt`) the two gates
|
|
312
|
+
// are on one grid: the clock cannot take a frame the gate would refuse,
|
|
313
|
+
// because both are the same instant, and the gate still never lets two
|
|
314
|
+
// frames into one period.
|
|
315
|
+
ctx.canRender = (now = performance.now()) => !destroyed && now >= gateOpenAt;
|
|
293
316
|
|
|
294
317
|
ctx.makeCurrent = () => {
|
|
295
318
|
if (destroyed) return;
|
|
@@ -305,6 +328,26 @@ function createGLAreaContext(runtime, area, config) {
|
|
|
305
328
|
runtime.gl.bindFramebuffer(target, fb == null ? (back?.fbo ?? 0) : fb);
|
|
306
329
|
};
|
|
307
330
|
|
|
331
|
+
// A frame refused by the gate has nothing else to wake it, so the gate
|
|
332
|
+
// says when it opens. `setTimeout` counts whole milliseconds and a
|
|
333
|
+
// display period is rarely one (8.333 at 120Hz), so a timer that lands
|
|
334
|
+
// early re-arms rather than announcing a gate that is still shut.
|
|
335
|
+
const armGate = () => {
|
|
336
|
+
if (gateTimer || destroyed) return;
|
|
337
|
+
const wait = gateOpenAt - performance.now();
|
|
338
|
+
gateTimer = setTimeout(
|
|
339
|
+
() => {
|
|
340
|
+
gateTimer = null;
|
|
341
|
+
if (destroyed) return;
|
|
342
|
+
if (performance.now() < gateOpenAt) armGate();
|
|
343
|
+
else ctx.onFrameAvailable?.();
|
|
344
|
+
},
|
|
345
|
+
Math.max(1, Math.ceil(wait)),
|
|
346
|
+
);
|
|
347
|
+
// the timer must not hold the process open for an idle scene
|
|
348
|
+
gateTimer.unref?.();
|
|
349
|
+
};
|
|
350
|
+
|
|
308
351
|
ctx.SwapBuffers = () => {
|
|
309
352
|
if (destroyed || !back) return;
|
|
310
353
|
runtime.gl.flush();
|
|
@@ -312,14 +355,9 @@ function createGLAreaContext(runtime, area, config) {
|
|
|
312
355
|
const shown = back;
|
|
313
356
|
back = front;
|
|
314
357
|
front = shown;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
gateClosed = false;
|
|
319
|
-
ctx.onFrameAvailable?.();
|
|
320
|
-
}, runtime.frameInterval);
|
|
321
|
-
// the timer must not hold the process open for an idle scene
|
|
322
|
-
gateTimer.unref?.();
|
|
358
|
+
// one display period from the frame's slot, not from this moment
|
|
359
|
+
gateOpenAt = area.nextFrameAt(runtime.frameInterval);
|
|
360
|
+
armGate();
|
|
323
361
|
};
|
|
324
362
|
|
|
325
363
|
ctx._resized = () => {
|
package/src/cocoa/window.js
CHANGED
|
@@ -12,6 +12,11 @@ import { CocoaLayerPresenter } from './presenter.js';
|
|
|
12
12
|
import { CocoaPromotion } from './promotion.js';
|
|
13
13
|
|
|
14
14
|
let nextWindowId = 1;
|
|
15
|
+
// The slack a frame clock allows under its interval, in ms: a timer's drift
|
|
16
|
+
// and no more (`CocoaApp._frameDue`, `_frameWait`, `nextFrameAt`). It lives
|
|
17
|
+
// here because the window is the clock — `_rafLast` and `_frameInterval`
|
|
18
|
+
// are its fields — and the app imports it from this side.
|
|
19
|
+
export const FRAME_SLACK_MS = 1;
|
|
15
20
|
// How long a worker's flip may hold its window's next frame (`_armFence`).
|
|
16
21
|
// The release is reported once the replacing frame has committed, a
|
|
17
22
|
// fraction of a millisecond later, and a window must not freeze on a report
|
|
@@ -910,6 +915,22 @@ export class CocoaWindow {
|
|
|
910
915
|
return this.app._requestFrame(cb, this);
|
|
911
916
|
}
|
|
912
917
|
|
|
918
|
+
/**
|
|
919
|
+
* The earliest moment this window's clock will hand out another frame,
|
|
920
|
+
* for a gate of `interval` ms — the display's period by default, which
|
|
921
|
+
* is the clock's own.
|
|
922
|
+
*
|
|
923
|
+
* It is the grid `_frameDue` decides on, not the wall clock: the anchor
|
|
924
|
+
* is the slot the running frame was *due* at, so the answer does not
|
|
925
|
+
* move with how long that frame took or how late its timer fired. A
|
|
926
|
+
* second gate that wants to compose with this clock rather than be
|
|
927
|
+
* rounded up by it has to land on this grid — which is what a
|
|
928
|
+
* `<glarea>`'s swap gate reads it for (src/cocoa/glarea.js).
|
|
929
|
+
*/
|
|
930
|
+
nextFrameAt(interval = this._frameInterval) {
|
|
931
|
+
return this._rafLast + interval - FRAME_SLACK_MS;
|
|
932
|
+
}
|
|
933
|
+
|
|
913
934
|
/**
|
|
914
935
|
* Push the backing surface at the WindowServer, if anything drew — and
|
|
915
936
|
* tell the window node what it cost. The flip itself is cheap, and the
|
package/src/glnodes.js
CHANGED
|
@@ -376,18 +376,21 @@ export class GlAreaNode extends Node {
|
|
|
376
376
|
typeof this.window.requestAnimationFrame === 'function'
|
|
377
377
|
? (cb) => this.window.requestAnimationFrame(cb)
|
|
378
378
|
: (cb) => setImmediate(cb);
|
|
379
|
-
|
|
379
|
+
// the frame clock's own timestamp, not a fresh reading: a gate that
|
|
380
|
+
// composes with that clock is answered from the same moment it is
|
|
381
|
+
// (`canRender` on the Cocoa surface, issue #631)
|
|
382
|
+
schedule((now) => {
|
|
380
383
|
this._frameScheduled = false;
|
|
381
|
-
this._drawFrame();
|
|
384
|
+
this._drawFrame(now);
|
|
382
385
|
});
|
|
383
386
|
}
|
|
384
387
|
|
|
385
|
-
_drawFrame() {
|
|
388
|
+
_drawFrame(now) {
|
|
386
389
|
const pacer = this._pacer;
|
|
387
390
|
pacer.began();
|
|
388
391
|
let drawn = false;
|
|
389
392
|
try {
|
|
390
|
-
drawn = this._drawFrameNow();
|
|
393
|
+
drawn = this._drawFrameNow(now);
|
|
391
394
|
} finally {
|
|
392
395
|
pacer.ended(undefined, drawn);
|
|
393
396
|
}
|
|
@@ -397,14 +400,14 @@ export class GlAreaNode extends Node {
|
|
|
397
400
|
}
|
|
398
401
|
|
|
399
402
|
/** The frame itself; true when it drew. */
|
|
400
|
-
_drawFrameNow() {
|
|
403
|
+
_drawFrameNow(now) {
|
|
401
404
|
const gl = this.gl;
|
|
402
405
|
if (!gl || this.destroyed) return false;
|
|
403
406
|
const direct = gl.backend === 'direct';
|
|
404
407
|
// On the direct backend every buffer may still be held by the display,
|
|
405
408
|
// and drawing into one before it comes back would paint what is on
|
|
406
409
|
// screen. `onFrameAvailable` asks for this frame again when one frees.
|
|
407
|
-
if (direct && gl.canRender && !gl.canRender()) {
|
|
410
|
+
if (direct && gl.canRender && !gl.canRender(now)) {
|
|
408
411
|
this._frameRefused = true;
|
|
409
412
|
return false;
|
|
410
413
|
}
|