@satinhq/kit 0.1.0-beta.1 → 0.1.0-beta.3

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 (129) hide show
  1. package/NOTICE +2 -2
  2. package/README.md +1 -1
  3. package/dist/acknowledgements/inventory.json +5 -5
  4. package/dist/actions/RowActions.d.ts +2 -2
  5. package/dist/actions/RowActions.js +2 -2
  6. package/dist/app/deployment.js +8 -2
  7. package/dist/app/installCheck.js +4 -3
  8. package/dist/app/launchWatchdog.js +6 -1
  9. package/dist/controls/civilDate.d.ts +1 -1
  10. package/dist/controls/civilDate.js +1 -1
  11. package/dist/docs/components/file-importer.md +1 -1
  12. package/dist/docs/components/paste-button.md +1 -1
  13. package/dist/docs/components/photos-picker.md +1 -1
  14. package/dist/docs/components/share-link.md +1 -1
  15. package/dist/docs/limitations.md +1 -1
  16. package/dist/docs/push.md +3 -3
  17. package/dist/docs/reference.md +1870 -0
  18. package/dist/glass/GlassMaterialRoot.d.ts +3 -1
  19. package/dist/glass/GlassMaterialRoot.js +2 -2
  20. package/dist/glass/MaterialEffectRoot.js +5 -1
  21. package/dist/glass/errors.d.ts +3 -1
  22. package/dist/glass/errors.js +49 -3
  23. package/dist/glass/fallback/canvasOwnership.d.ts +7 -0
  24. package/dist/glass/fallback/canvasOwnership.js +23 -0
  25. package/dist/glass/fallback/material.js +3 -2
  26. package/dist/glass/fallback/regions.d.ts +1 -0
  27. package/dist/glass/fallback/regions.js +9 -12
  28. package/dist/glass/fallback/runtime.d.ts +2 -0
  29. package/dist/glass/fallback/runtime.js +15 -4
  30. package/dist/glass/lens/controlLift.d.ts +1 -1
  31. package/dist/glass/lens/controlLift.js +1 -1
  32. package/dist/glass/lens/interactivePress.d.ts +2 -2
  33. package/dist/glass/lens/interactivePress.js +2 -2
  34. package/dist/glass/lens/menu-opening-native-fits.json +1 -1
  35. package/dist/glass/lens/menuContextFit.json +1 -1
  36. package/dist/glass/lens/menuDropFit.json +1 -1
  37. package/dist/glass/lens/menuField.d.ts +2 -2
  38. package/dist/glass/lens/menuField.js +3 -3
  39. package/dist/glass/lens/menuGlyphDropFit.json +1 -1
  40. package/dist/glass/lens/menuMorphFit.json +1 -1
  41. package/dist/glass/lens/menuMorphPhoneFit.json +1 -1
  42. package/dist/glass/lens/tabAccessoryMotion.d.ts +1 -1
  43. package/dist/glass/lens/tabSearchMember.d.ts +1 -1
  44. package/dist/glass/lens/tabSearchMember.js +1 -1
  45. package/dist/glass/menuPresenter.js +1 -1
  46. package/dist/glass/regions.d.ts +1 -1
  47. package/dist/glass/regions.js +1 -1
  48. package/dist/glass/shapeTrack.d.ts +1 -1
  49. package/dist/glass/submenuMotion.d.ts +1 -1
  50. package/dist/glass/submenuMotion.js +1 -1
  51. package/dist/glass/webgpu/client.d.ts +3 -0
  52. package/dist/glass/webgpu/client.js +57 -9
  53. package/dist/glass/webgpu/entry.worker.js +3 -6
  54. package/dist/glass/webgpu/errorCode.js +3 -2
  55. package/dist/glass/webgpu/frameClock.d.ts +6 -2
  56. package/dist/glass/webgpu/frameClock.js +9 -2
  57. package/dist/glass/webgpu/frameProbe.d.ts +1 -1
  58. package/dist/glass/webgpu/frameProbe.js +3 -3
  59. package/dist/glass/webgpu/layers.d.ts +5 -1
  60. package/dist/glass/webgpu/layers.js +61 -6
  61. package/dist/glass/webgpu/minimize.js +20 -2
  62. package/dist/glass/webgpu/production.js +13 -1
  63. package/dist/glass/webgpu/raster.d.ts +18 -0
  64. package/dist/glass/webgpu/raster.js +105 -15
  65. package/dist/glass/webgpu/rasterMaterial.d.ts +4 -0
  66. package/dist/glass/webgpu/rasterMaterial.js +17 -1
  67. package/dist/glass/webgpu/regionScene.d.ts +4 -1
  68. package/dist/glass/webgpu/regionScene.js +30 -1
  69. package/dist/glass/webgpu/renderer.js +23 -7
  70. package/dist/glass/webgpu/sourcePixels.d.ts +4 -4
  71. package/dist/glass/webgpu/sourcePixels.js +48 -8
  72. package/dist/keyboard/adapter.js +2 -2
  73. package/dist/keyboard/avoidance.d.ts +1 -1
  74. package/dist/keyboard/avoidance.js +1 -1
  75. package/dist/keyboard/motion.d.ts +1 -1
  76. package/dist/keyboard/motion.js +1 -1
  77. package/dist/list-form/ListForm.d.ts +2 -2
  78. package/dist/list-form/ListForm.js +1 -1
  79. package/dist/list-form/ListItem.js +1 -1
  80. package/dist/list-form/RowLabel.d.ts +1 -1
  81. package/dist/list-form/RowLabel.js +1 -1
  82. package/dist/list-form/Slider.js +1 -1
  83. package/dist/list-form/editMotion.js +1 -1
  84. package/dist/list-form/editing.d.ts +5 -5
  85. package/dist/list-form/editing.js +1 -1
  86. package/dist/motion/animationValue.d.ts +2 -2
  87. package/dist/motion/animationValue.js +9 -10
  88. package/dist/motion/matchedGeometry.d.ts +4 -4
  89. package/dist/motion/matchedGeometry.js +7 -8
  90. package/dist/motion/springTrack.d.ts +1 -2
  91. package/dist/motion/springTrack.js +10 -5
  92. package/dist/navigation/ScreenContainer.js +1 -1
  93. package/dist/navigation/chromeCurves.js +1 -1
  94. package/dist/navigation/types.d.ts +1 -1
  95. package/dist/navigation/zoomGeometry.js +1 -1
  96. package/dist/navigation/zoomMotion.d.ts +1 -1
  97. package/dist/navigation/zoomMotion.js +2 -2
  98. package/dist/navigation/zoomTransition.js +3 -4
  99. package/dist/presentation/Presentation.d.ts +2 -2
  100. package/dist/presentation/Presentation.js +1 -1
  101. package/dist/presentation/presentationSource.js +1 -1
  102. package/dist/public/controls.d.ts +4 -4
  103. package/dist/public/controls.js +7 -7
  104. package/dist/public/glass.d.ts +3 -4
  105. package/dist/public/glass.js +1 -2
  106. package/dist/public/navigationTransition.d.ts +1 -1
  107. package/dist/public/navigationTransition.js +1 -1
  108. package/dist/public/scroll.d.ts +1 -1
  109. package/dist/public/scroll.js +1 -1
  110. package/dist/public/toolbar.js +1 -1
  111. package/dist/root/SatinRoot.js +9 -5
  112. package/dist/safe-area/SafeAreaInset.js +3 -3
  113. package/dist/safe-area/modifiers.js +2 -2
  114. package/dist/safe-area/useSafeAreaMetrics.js +2 -2
  115. package/dist/scroll/ScrollSurface.d.ts +5 -5
  116. package/dist/scroll/ScrollSurface.js +1 -1
  117. package/dist/scroll/position.js +1 -1
  118. package/dist/scroll/screenGeometry.d.ts +1 -1
  119. package/dist/scroll/screenGeometry.js +1 -1
  120. package/dist/search/SearchDrawer.js +1 -1
  121. package/dist/search/searchMotion.d.ts +1 -1
  122. package/dist/search/searchMotion.js +1 -1
  123. package/dist/search/useSearchPresentation.js +1 -1
  124. package/dist/shapes/corners.js +1 -1
  125. package/dist/styles.css +55 -34
  126. package/dist/sw/renderServiceWorker.js +12 -3
  127. package/dist/text-input/TextEditor.js +7 -3
  128. package/package.json +4 -4
  129. package/dist/docs/swiftui-reference.md +0 -1895
@@ -30,6 +30,7 @@ export class WebGPUClient {
30
30
  }
31
31
  adapterDeadline;
32
32
  failed;
33
+ startingTransport = false;
33
34
  frames = new PageFrameHost();
34
35
  webgpu = false;
35
36
  currentAppearance = () => readDarkAppearance() ? 'dark' : 'light';
@@ -68,7 +69,26 @@ export class WebGPUClient {
68
69
  })), motionPreparationEnabled);
69
70
  glassTiming('client-start');
70
71
  try {
71
- await this.initialize(canvas, resolution, worker, webgpu, appearance, execution);
72
+ for (let attempt = 0;; attempt++) {
73
+ this.startingTransport = true;
74
+ try {
75
+ await this.initialize(canvas, resolution, worker, webgpu, appearance, execution);
76
+ break;
77
+ }
78
+ catch (error) {
79
+ const code = glassFailureCode('renderer initialization', error);
80
+ const retryable = this.startingTransport &&
81
+ ['worker-startup', 'readiness-timeout', 'device-request'].includes(code);
82
+ if (!retryable || attempt === 2)
83
+ throw error;
84
+ this.close();
85
+ this.failed = undefined;
86
+ await new Promise(resolve => setTimeout(resolve, 250 * 2 ** attempt));
87
+ }
88
+ finally {
89
+ this.startingTransport = false;
90
+ }
91
+ }
72
92
  }
73
93
  catch (error) {
74
94
  reportGlassError(glassFailureCode('renderer initialization', error), error, 'renderer initialization');
@@ -153,7 +173,7 @@ export class WebGPUClient {
153
173
  return;
154
174
  }
155
175
  if (glassError) {
156
- reportGlassError(glassError.code, glassError.message, glassError.stage, glassError.reason);
176
+ reportGlassError(glassError.code, glassError.message, glassError.stage, glassError.reason, this.startingTransport ? { recovering: true } : undefined);
157
177
  return;
158
178
  }
159
179
  const request = this.pending.get(id);
@@ -174,12 +194,18 @@ export class WebGPUClient {
174
194
  };
175
195
  }
176
196
  else {
177
- const transport = new Worker(new URL('./entry.worker.js', import.meta.url), {
178
- type: 'module',
179
- name: 'satin-glass-webgpu',
180
- });
197
+ let transport;
198
+ try {
199
+ transport = new Worker(new URL('./entry.worker.js', import.meta.url), {
200
+ type: 'module',
201
+ name: 'satin-glass-webgpu',
202
+ });
203
+ }
204
+ catch (error) {
205
+ throw new Error(`Worker startup: ${String(error)}`, { cause: error });
206
+ }
181
207
  transport.onmessage = receive;
182
- transport.onerror = event => this.failTransport(new Error(event.message || 'Glass worker failed'), 'worker startup/runtime');
208
+ transport.onerror = event => this.failTransport(new Error(`Worker startup/runtime: ${event.message || 'Glass worker failed'}`), 'worker startup/runtime');
183
209
  transport.onmessageerror = () => this.failTransport(Error('Glass worker message could not be deserialized'), 'worker transport');
184
210
  this.transport = {
185
211
  postMessage: (data, transfer = []) => transport.postMessage(data, transfer),
@@ -205,7 +231,7 @@ export class WebGPUClient {
205
231
  }
206
232
  catch (error) {
207
233
  this.failed = error instanceof Error ? error : Error(String(error));
208
- reportGlassError(glassFailureCode('WebGPU startup', this.failed), this.failed, 'WebGPU startup');
234
+ // init owns the bounded startup retry and terminal report.
209
235
  this.transport?.terminate();
210
236
  for (const pending of this.pending.values())
211
237
  pending.reject(this.failed);
@@ -216,6 +242,7 @@ export class WebGPUClient {
216
242
  clearTimeout(timeout);
217
243
  }
218
244
  }
245
+ this.startingTransport = false;
219
246
  const offscreen = this.local ? canvas : canvas.transferControlToOffscreen();
220
247
  await this.request({
221
248
  type: 'init',
@@ -233,7 +260,8 @@ export class WebGPUClient {
233
260
  failTransport(error, stage) {
234
261
  this.adapterDeadline?.cancel();
235
262
  this.failed = error;
236
- reportGlassError(glassFailureCode(stage, error), error, stage);
263
+ if (!this.startingTransport)
264
+ reportGlassError(glassFailureCode(stage, error), error, stage);
237
265
  for (const pending of this.pending.values())
238
266
  pending.reject(error);
239
267
  this.pending.clear();
@@ -324,6 +352,26 @@ export class WebGPUClient {
324
352
  this.bindLifecycle();
325
353
  this.visibility();
326
354
  }
355
+ /** Dispose a failed transport before constructing its replacement. */
356
+ close() {
357
+ this.frames.cancel();
358
+ this.adapterDeadline?.cancel();
359
+ this.adapterDeadline = undefined;
360
+ document.removeEventListener('visibilitychange', this.visibility, true);
361
+ window.removeEventListener('pagehide', this.visibility, true);
362
+ window.removeEventListener('pageshow', this.visibility);
363
+ window.removeEventListener('focus', this.focus);
364
+ document.removeEventListener('visibilitychange', this.adapterVisibility, true);
365
+ window.removeEventListener('pagehide', this.adapterVisibility, true);
366
+ window.removeEventListener('pageshow', this.adapterVisibility);
367
+ window.removeEventListener('focus', this.adapterVisibility);
368
+ this.transport?.terminate();
369
+ this.transport = undefined;
370
+ this.local = undefined;
371
+ for (const pending of this.pending.values())
372
+ pending.reject(Error('Glass transport retired'));
373
+ this.pending.clear();
374
+ }
327
375
  park() {
328
376
  this.frames.cancel();
329
377
  document.removeEventListener('visibilitychange', this.visibility, true);
@@ -1,16 +1,13 @@
1
- import { WorkerFrameClock } from './frameClock.js';
1
+ import { workerAnimationFrameClock } from './frameClock.js';
2
2
  import { glassFailureCode } from './errorCode.js';
3
3
  import { createWebGPURenderer } from './renderer.js';
4
4
  import { glassTiming, setGlassTimingSink } from './profile.js';
5
5
  setGlassTimingSink(event => globalThis.postMessage({ glassProfile: event }));
6
6
  glassTiming('worker-start');
7
- const clock = new WorkerFrameClock(message => globalThis.postMessage(message));
7
+ const clock = workerAnimationFrameClock;
8
8
  const renderer = createWebGPURenderer(message => globalThis.postMessage(message), clock);
9
9
  globalThis.onmessage = event => {
10
- if (event.data.request?.type === 'page-frame')
11
- clock.receive(event.data.request);
12
- else
13
- renderer.receive(event);
10
+ renderer.receive(event);
14
11
  };
15
12
  globalThis.addEventListener('error', event => globalThis.postMessage({
16
13
  glassError: {
@@ -1,6 +1,9 @@
1
1
  /** Classification retains the raw message; it never chooses a replacement. */
2
2
  export function glassFailureCode(stage, cause) {
3
3
  const message = cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause), text = stage + ' ' + message;
4
+ // Preserve the failed transport owner when a worker reports its own device loss.
5
+ if (/worker.*(?:startup|runtime|transport|rejection)/i.test(text))
6
+ return 'worker-startup';
4
7
  if (/device\.lost|device.?lost/i.test(text))
5
8
  return 'device-lost';
6
9
  if (/out.?of.?memory/i.test(text))
@@ -19,8 +22,6 @@ export function glassFailureCode(stage, cause) {
19
22
  return 'context-configure';
20
23
  if (/readiness|2\.5 seconds|2500 ms/i.test(text))
21
24
  return 'readiness-timeout';
22
- if (/worker.*(?:startup|runtime|transport|rejection)/i.test(text))
23
- return 'worker-startup';
24
25
  if (/unsupported|invalid glass|Unknown.*(?:layer|region)|exceeds.*supported/i.test(text))
25
26
  return 'unsupported-configuration';
26
27
  return 'presentation';
@@ -1,18 +1,22 @@
1
1
  import type { Envelope } from '../worker/protocol.js';
2
2
  /** Epoch milliseconds from the document's animation frame, shared with WAAPI. */
3
3
  export interface FrameClock {
4
+ readonly kind?: 'page-raf' | 'worker-raf';
4
5
  request(callback: (at: number) => void): number;
5
6
  cancel(token: number): void;
6
7
  }
7
8
  export declare const documentFrameClock: FrameClock;
9
+ /** A retained worker timeline can sample without a page callback or message round trip.
10
+ * Both realms use epoch milliseconds, so the same WAAPI track remains the time authority. */
11
+ export declare const workerAnimationFrameClock: FrameClock;
8
12
  type FrameRequest = {
9
13
  glassFrame: {
10
14
  token: number;
11
15
  cancel?: boolean;
12
16
  };
13
17
  };
14
- /** The experimental worker renderer asks the page for one frame at a time.
15
- * Worker rAF can run faster than the page and is not a presentation clock. */
18
+ /** Page-linked worker clock retained for diagnostic comparisons. Its round trip
19
+ * also inherits page stalls; the independent worker renderer does not use it. */
16
20
  export declare class WorkerFrameClock implements FrameClock {
17
21
  private sequence;
18
22
  private pending?;
@@ -1,9 +1,16 @@
1
1
  export const documentFrameClock = {
2
+ kind: 'page-raf',
2
3
  request: callback => requestAnimationFrame(timestamp => callback(performance.timeOrigin + timestamp)),
3
4
  cancel: token => cancelAnimationFrame(token),
4
5
  };
5
- /** The experimental worker renderer asks the page for one frame at a time.
6
- * Worker rAF can run faster than the page and is not a presentation clock. */
6
+ /** A retained worker timeline can sample without a page callback or message round trip.
7
+ * Both realms use epoch milliseconds, so the same WAAPI track remains the time authority. */
8
+ export const workerAnimationFrameClock = {
9
+ ...documentFrameClock,
10
+ kind: 'worker-raf',
11
+ };
12
+ /** Page-linked worker clock retained for diagnostic comparisons. Its round trip
13
+ * also inherits page stalls; the independent worker renderer does not use it. */
7
14
  export class WorkerFrameClock {
8
15
  sequence = 0;
9
16
  pending;
@@ -44,7 +44,7 @@ export declare class FrameProbe {
44
44
  private post;
45
45
  constructor(post: (summary: FrameSummary[]) => void);
46
46
  get timestampQuery(): boolean;
47
- useDevice(device: GPUDevice | undefined): void;
47
+ useDevice(device: GPUDevice | undefined, gpuTiming?: boolean): void;
48
48
  /** Wraps one frame's encoding and submission. Returns the frame's sample: its completion and
49
49
  * GPU times are filled in when they arrive. */
50
50
  frame(kind: FrameKind, interval: number | null, restart: boolean, encode: (e: GPUCommandEncoder) => void, canSubmit?: () => boolean): FrameProbeSample | undefined;
@@ -78,12 +78,12 @@ export class FrameProbe {
78
78
  this.post = post;
79
79
  }
80
80
  get timestampQuery() {
81
- return !!this.device?.features.has('timestamp-query');
81
+ return !!this.timer;
82
82
  }
83
- useDevice(device) {
83
+ useDevice(device, gpuTiming = true) {
84
84
  this.device = device;
85
85
  this.timer = undefined;
86
- if (device?.features.has('timestamp-query'))
86
+ if (gpuTiming && device?.features.has('timestamp-query'))
87
87
  try {
88
88
  this.timer = new GPUFrameTimer(device);
89
89
  }
@@ -22,7 +22,9 @@ export declare class LayerDevice {
22
22
  private stopped;
23
23
  private adapterPending;
24
24
  private recovering;
25
- private recoveryBlocked;
25
+ private recoveryBlockedUntil;
26
+ private recoveryTimer?;
27
+ private recoveryFailures;
26
28
  private forcedNullAdapters;
27
29
  private lastLoss?;
28
30
  readonly stats: {
@@ -43,6 +45,8 @@ export declare class LayerDevice {
43
45
  stop(): void;
44
46
  /** Foreground signals wake the single acquisition already in flight. */
45
47
  retryAdapter(): Promise<GPUDevice> | undefined;
48
+ private scheduleRecovery;
49
+ private waitUntilVisible;
46
50
  forceNullAdaptersForTest(attempts: number): void;
47
51
  private requestAdapter;
48
52
  private acquire;
@@ -34,7 +34,9 @@ export class LayerDevice {
34
34
  stopped = false;
35
35
  adapterPending = false;
36
36
  recovering = false;
37
- recoveryBlocked = false;
37
+ recoveryBlockedUntil = 0;
38
+ recoveryTimer;
39
+ recoveryFailures = 0;
38
40
  forcedNullAdapters = 0;
39
41
  lastLoss;
40
42
  stats = {
@@ -57,8 +59,10 @@ export class LayerDevice {
57
59
  async ready() {
58
60
  if (this.device)
59
61
  return this.device;
60
- if (this.recoveryBlocked)
61
- throw Error('Glass device repeatedly lost during recovery');
62
+ if (Date.now() < this.recoveryBlockedUntil) {
63
+ this.scheduleRecovery();
64
+ throw Error('Glass device recovery is cooling down');
65
+ }
62
66
  if (this.starting)
63
67
  return this.starting;
64
68
  this.starting = this.acquire();
@@ -79,8 +83,13 @@ export class LayerDevice {
79
83
  }
80
84
  }
81
85
  failedDevice?.destroy();
82
- if (!this.stopped)
86
+ if (!this.stopped) {
83
87
  this.reportError(this.recovering ? 'device recreation' : 'device acquisition', error);
88
+ if (this.recovering) {
89
+ this.recoveryFailures++;
90
+ this.scheduleRecovery();
91
+ }
92
+ }
84
93
  throw error;
85
94
  }
86
95
  finally {
@@ -91,21 +100,53 @@ export class LayerDevice {
91
100
  setAdapterVisible(visible) {
92
101
  this.adapterVisible = visible;
93
102
  this.retryWake?.();
103
+ if (!visible) {
104
+ clearTimeout(this.recoveryTimer);
105
+ this.recoveryTimer = undefined;
106
+ }
107
+ else if (this.recovering && !this.starting && !this.device)
108
+ this.scheduleRecovery();
94
109
  }
95
110
  stop() {
96
111
  this.stopped = true;
112
+ clearTimeout(this.recoveryTimer);
97
113
  this.retryWake?.();
98
114
  }
99
115
  /** Foreground signals wake the single acquisition already in flight. */
100
116
  retryAdapter() {
101
- if (this.device || this.recoveryBlocked)
117
+ if (this.device || this.stopped || !this.adapterVisible)
118
+ return;
119
+ if (Date.now() < this.recoveryBlockedUntil) {
120
+ this.scheduleRecovery();
102
121
  return;
122
+ }
103
123
  if (this.starting) {
104
124
  this.retryWake?.();
105
125
  return;
106
126
  }
107
127
  return this.ready();
108
128
  }
129
+ scheduleRecovery() {
130
+ if (this.stopped || !this.adapterVisible || this.recoveryTimer)
131
+ return;
132
+ const delay = Math.max(this.recoveryBlockedUntil - Date.now(), Math.min(1000 * 2 ** this.recoveryFailures, 30_000));
133
+ this.recoveryTimer = setTimeout(() => {
134
+ this.recoveryTimer = undefined;
135
+ void this.retryAdapter()?.catch(() => {
136
+ /* ready records and schedules the next attempt. */
137
+ });
138
+ }, delay);
139
+ }
140
+ async waitUntilVisible() {
141
+ while (!this.adapterVisible && !this.stopped) {
142
+ await new Promise(resolve => {
143
+ this.retryWake = resolve;
144
+ });
145
+ this.retryWake = undefined;
146
+ }
147
+ if (this.stopped)
148
+ throw Error('Glass renderer stopped during adapter acquisition');
149
+ }
109
150
  forceNullAdaptersForTest(attempts) {
110
151
  if (!Number.isInteger(attempts) || attempts < 0 || attempts > 100)
111
152
  throw Error('Null-adapter attempts must be an integer between 0 and 100');
@@ -167,6 +208,15 @@ export class LayerDevice {
167
208
  })).catch(error => {
168
209
  throw Error(`requestDevice rejected: ${String(error)}`);
169
210
  });
211
+ // requestDevice may resolve after a quick re-background. Do not rebuild or
212
+ // announce readiness against a suspended presentation generation.
213
+ try {
214
+ await this.waitUntilVisible();
215
+ }
216
+ catch (error) {
217
+ device.destroy();
218
+ throw error;
219
+ }
170
220
  this.device = device;
171
221
  this.faults.length = 0;
172
222
  registerGPUReporter(device, (stage, error) => {
@@ -207,11 +257,13 @@ export class LayerDevice {
207
257
  }
208
258
  if (invalidationError) {
209
259
  this.reportError('device recreation', invalidationError);
260
+ this.scheduleRecovery();
210
261
  return;
211
262
  }
212
263
  // WebKit can report destroyed for an unexpected loss as well.
213
264
  if (this.lossTimes.length > 3) {
214
- this.recoveryBlocked = true;
265
+ this.recoveryBlockedUntil = this.lossTimes[0] + 60_000;
266
+ this.scheduleRecovery();
215
267
  this.reportError('device recreation', 'Glass device repeatedly lost during recovery');
216
268
  return;
217
269
  }
@@ -227,6 +279,9 @@ export class LayerDevice {
227
279
  for (const listener of this.listeners)
228
280
  listener(device);
229
281
  this.stats.restores++;
282
+ this.recoveryFailures = 0;
283
+ clearTimeout(this.recoveryTimer);
284
+ this.recoveryTimer = undefined;
230
285
  this.recovering = false;
231
286
  if (lagTraceEnabled())
232
287
  glassTiming('device-restored');
@@ -374,8 +374,26 @@ export class WorkerMinimize {
374
374
  },
375
375
  };
376
376
  }
377
- if (atRest(f))
378
- return spec;
377
+ if (atRest(f)) {
378
+ // A retargeted track includes its pre-onset frames. They must use the current
379
+ // slot too; returning the captured spec preserves the old expanded width.
380
+ return {
381
+ ...spec,
382
+ frameWidth: geometry.dockWidth,
383
+ frameHeight: geometry.dockHeight,
384
+ shape: {
385
+ ...spec.shape,
386
+ first: {
387
+ ...spec.shape.first,
388
+ centerX: geometry.dockWidth / 2,
389
+ centerY: geometry.dockHeight / 2,
390
+ halfWidth: geometry.dockWidth / 2,
391
+ halfHeight: geometry.dockHeight / 2,
392
+ radius: geometry.dockRadius,
393
+ },
394
+ },
395
+ };
396
+ }
379
397
  const g = this.options.geometry, p = minimizePlacement(f.progress, g.dockWidth, g.dockHeight, g.tabBarMinimizedOffsetX), height = g.dockHeight + (48 - g.dockHeight) * f.heightProgress, y = 7 * f.heightProgress, pill = spec.tabSelection?.capsule;
380
398
  const bar = {
381
399
  centerX: p.x + p.width / 2,
@@ -304,6 +304,12 @@ export class ProductionGlassRuntime {
304
304
  static async create(options) {
305
305
  if (this.shared) {
306
306
  const runtime = await this.shared;
307
+ if (runtime.client.status().failed) {
308
+ runtime.teardown();
309
+ runtime.client.close();
310
+ this.shared = undefined;
311
+ return this.create(options);
312
+ }
307
313
  runtime.rebind(options);
308
314
  await runtime.prepare();
309
315
  return runtime;
@@ -317,7 +323,9 @@ export class ProductionGlassRuntime {
317
323
  catch (error) {
318
324
  // A rejected create never reaches the React owner's cleanup. Retire its
319
325
  // listeners and captures here before a release app mounts CSS material.
320
- runtime.dispose(options);
326
+ runtime.teardown();
327
+ runtime.client.close();
328
+ this.shared = undefined;
321
329
  throw error;
322
330
  }
323
331
  })();
@@ -377,6 +385,10 @@ export class ProductionGlassRuntime {
377
385
  element.style.cssText =
378
386
  'position:absolute;inset:0;overflow:visible;clip-path:none;transform:none';
379
387
  this.canvas.style.cssText = `position:absolute;left:0;top:0;width:${geometry.width}px;height:${geometry.height}px;pointer-events:none`;
388
+ if (!this.client.documentOwned) {
389
+ this.canvas.style.objectFit = 'contain';
390
+ this.canvas.style.objectPosition = 'left bottom';
391
+ }
380
392
  this.canvas.dataset.satinWebgpu = 'production';
381
393
  // Chrome canvases start as one pixel; each preparation places them on the band their surfaces
382
394
  // reach. A remount (geometry change, rebind) keeps the band the worker is drawing for until the
@@ -37,6 +37,19 @@ export declare class WebGPURaster {
37
37
  private overlayPipeline;
38
38
  private presentPipeline;
39
39
  private format;
40
+ private presentationRoot?;
41
+ private presentationOrigin?;
42
+ private presentationMembers;
43
+ private presentationSequence?;
44
+ private collectingPresentation;
45
+ /** Member samples encode their materials first. The root then presents the whole frame once. */
46
+ withSharedPresentation(draw: () => void): void;
47
+ /** Retain independent material sampling, but store overlapping fixed chrome once. */
48
+ joinPresentation(root: WebGPURaster, origin: {
49
+ x: number;
50
+ y: number;
51
+ }): void;
52
+ leavePresentation(): void;
40
53
  private lost;
41
54
  private suspended;
42
55
  private covered;
@@ -172,8 +185,10 @@ export declare class WebGPURaster {
172
185
  * region presenter's layer already is its sample rectangle and keeps layer space. */
173
186
  private localResult;
174
187
  /** Device-pixel offset of this layer's canvas within its layer. */
188
+ private presentationViewport;
175
189
  private canvasOffset;
176
190
  private canvasSize;
191
+ private resizeCanvas;
177
192
  /** Size the shared layer canvas to a band of its layer (the element is placed there by
178
193
  * the page): a lift overlay presents a strip, not a second full-screen canvas. */
179
194
  setCanvasViewport(viewport: readonly [number, number, number, number] | undefined): void;
@@ -201,6 +216,9 @@ export declare class WebGPURaster {
201
216
  * Returns false, drawing nothing, when the canvas already shows exactly this (see
202
217
  * `presentedLayers`): the same results at the same places, or nothing at all. */
203
218
  static presentLayer(encoder: GPUCommandEncoder, rasters: readonly WebGPURaster[]): boolean;
219
+ private presentationOffset;
220
+ private groupPresentUniform;
221
+ private groupPresentScissor;
204
222
  private presentDraw;
205
223
  /** Scroll-attached glass is part of the scrolling content, and the navigation bar's scroll-edge
206
224
  * effect is painted over that content (CSS, a layer above the scroll container). Its own