@godot-scene-web/html 0.1.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/LICENSE +21 -0
- package/dist/extents-R8RIim2S.d.ts +1223 -0
- package/dist/extents-R8RIim2S.d.ts.map +1 -0
- package/dist/index.d.ts +346 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5330 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime-BkfFs_eO.js +6540 -0
- package/dist/runtime-BkfFs_eO.js.map +1 -0
- package/dist/runtime.d.ts +225 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +2 -0
- package/package.json +54 -0
- package/vendor/LICENSE-OpenSans +201 -0
- package/vendor/OpenSans_SemiBold.woff2 +0 -0
- package/vendor/VENDOR.md +51 -0
|
@@ -0,0 +1,1223 @@
|
|
|
1
|
+
import { GodotNode, GodotResource, GodotResourceRefValue, GodotVariant } from "@godot-scene-web/core";
|
|
2
|
+
import { InstanceBuffer, ParticleRenderConfig, sampleParticleGradient } from "@godot-scene-web/effects/particles";
|
|
3
|
+
import { uploadWebglTexture } from "@godot-scene-web/canvas-effects/webgl";
|
|
4
|
+
import { GodotBlendMode } from "@godot-scene-web/effects/shaders";
|
|
5
|
+
import { GodotSceneTreeDiagnostic } from "@godot-scene-web/layout";
|
|
6
|
+
import { GodotAnchorMap } from "@godot-scene-web/layout/anchors";
|
|
7
|
+
import { GodotResourceLoadStatus, GodotResourceStatus } from "@godot-scene-web/scene-graph";
|
|
8
|
+
|
|
9
|
+
//#region src/content-scale.d.ts
|
|
10
|
+
interface GodotContentScaleSize {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
type GodotContentScaleAspect = "ignore" | "keep" | "keep_width" | "keep_height" | "expand";
|
|
15
|
+
type GodotContentScaleTechnique = "container" | "transform";
|
|
16
|
+
interface GodotContentScale {
|
|
17
|
+
/** Godot `content_scale_aspect`. Only `"keep"` is implemented for now. */
|
|
18
|
+
aspect: GodotContentScaleAspect;
|
|
19
|
+
/**
|
|
20
|
+
* How the uniform scale is realized in the DOM:
|
|
21
|
+
* - `"container"` (default): CSS-only, every length in the stage subtree is
|
|
22
|
+
* expressed in container-query units. No JavaScript; identical in Vue and the
|
|
23
|
+
* static HTML document.
|
|
24
|
+
* - `"transform"`: the stage keeps its px layout and is scaled with
|
|
25
|
+
* `transform: scale(var(--godot-scale))`; the factor is set by JS on resize.
|
|
26
|
+
*/
|
|
27
|
+
technique?: GodotContentScaleTechnique;
|
|
28
|
+
/** Base/reference resolution (Godot `content_scale_size`). Defaults to the model viewport. */
|
|
29
|
+
baseSize?: GodotContentScaleSize;
|
|
30
|
+
/** Letterbox/pillarbox bar color painted by the frame around the centered stage. */
|
|
31
|
+
background?: string;
|
|
32
|
+
}
|
|
33
|
+
interface ResolvedContentScale {
|
|
34
|
+
aspect: GodotContentScaleAspect;
|
|
35
|
+
technique: GodotContentScaleTechnique;
|
|
36
|
+
base: GodotContentScaleSize;
|
|
37
|
+
background?: string;
|
|
38
|
+
}
|
|
39
|
+
declare function resolveContentScale(contentScale: GodotContentScale | undefined, viewport: GodotContentScaleSize): ResolvedContentScale | null;
|
|
40
|
+
declare function rewritePxLengths(text: string, base: GodotContentScaleSize): string;
|
|
41
|
+
declare function scaleStyleRecord(style: Record<string, string>, base: GodotContentScaleSize): void;
|
|
42
|
+
declare function contentScaleStageStyle(resolved: ResolvedContentScale): Record<string, string>;
|
|
43
|
+
declare function observeContentScale(frame: HTMLElement, base: GodotContentScaleSize): () => void;
|
|
44
|
+
declare function contentScaleScript(base: GodotContentScaleSize): string;
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/text-scale.d.ts
|
|
47
|
+
declare const TEXT_SCALE_VAR = "--godot-text-scale";
|
|
48
|
+
interface GodotTextScale {
|
|
49
|
+
exemptAutoFit?: boolean;
|
|
50
|
+
}
|
|
51
|
+
type GodotTextScaleOption = boolean | GodotTextScale;
|
|
52
|
+
interface ResolvedTextScale {
|
|
53
|
+
exemptAutoFit: boolean;
|
|
54
|
+
}
|
|
55
|
+
declare function resolveTextScale(option: GodotTextScaleOption | undefined): ResolvedTextScale | null;
|
|
56
|
+
declare function textScaleLength(px: number, enabled: boolean): string;
|
|
57
|
+
declare function setGodotTextScale(element: HTMLElement, value: number): void;
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/types.d.ts
|
|
60
|
+
/**
|
|
61
|
+
* A `<filter>` referenced by a self-layer's `filter: url(#id)` (an external
|
|
62
|
+
* texture's color-matrix tint, or a consumer-supplied shader-fallback effect).
|
|
63
|
+
* `markup` is the filter's inner SVG (e.g. a `feColorMatrix`, or a
|
|
64
|
+
* luminance→band→flood chain). Emitted once per render into a hidden
|
|
65
|
+
* `<svg><defs>` by every renderer.
|
|
66
|
+
*/
|
|
67
|
+
interface GodotHtmlTintFilter {
|
|
68
|
+
id: string;
|
|
69
|
+
markup: string;
|
|
70
|
+
}
|
|
71
|
+
interface GodotHtmlModel {
|
|
72
|
+
viewport: {
|
|
73
|
+
width: number;
|
|
74
|
+
height: number;
|
|
75
|
+
};
|
|
76
|
+
nodes: GodotHtmlNode[];
|
|
77
|
+
fontFaces: GodotHtmlFontFace[];
|
|
78
|
+
tintFilters: GodotHtmlTintFilter[];
|
|
79
|
+
diagnostics: GodotSceneTreeDiagnostic[];
|
|
80
|
+
resourceStatuses: GodotResourceStatus[];
|
|
81
|
+
css: string;
|
|
82
|
+
contentScale: ResolvedContentScale | null;
|
|
83
|
+
}
|
|
84
|
+
type GodotHtmlPositioning = "root" | "absolute" | "container-managed";
|
|
85
|
+
type GodotHtmlContainerLayout = "box" | "flow" | "grid" | "center" | "margin" | "panel" | "scroll" | "aspect-ratio";
|
|
86
|
+
interface GodotHtmlNode {
|
|
87
|
+
kind?: "hidden-placeholder";
|
|
88
|
+
path: string;
|
|
89
|
+
name: string;
|
|
90
|
+
type: string;
|
|
91
|
+
parentPath: string | null;
|
|
92
|
+
children: string[];
|
|
93
|
+
positioning: GodotHtmlPositioning;
|
|
94
|
+
containerLayout: GodotHtmlContainerLayout | null;
|
|
95
|
+
attributes: Record<string, string>;
|
|
96
|
+
className: string;
|
|
97
|
+
style: Record<string, string>;
|
|
98
|
+
selfAttributes: Record<string, string>;
|
|
99
|
+
selfStyle: Record<string, string>;
|
|
100
|
+
text: string | null;
|
|
101
|
+
html: string | null;
|
|
102
|
+
}
|
|
103
|
+
interface GodotResolvedResource {
|
|
104
|
+
status?: GodotResourceLoadStatus;
|
|
105
|
+
type?: string;
|
|
106
|
+
path?: string;
|
|
107
|
+
url?: string;
|
|
108
|
+
document?: GodotResource;
|
|
109
|
+
atlas?: GodotResolvedResource;
|
|
110
|
+
shader?: GodotResolvedResource;
|
|
111
|
+
region?: {
|
|
112
|
+
x: number;
|
|
113
|
+
y: number;
|
|
114
|
+
width: number;
|
|
115
|
+
height: number;
|
|
116
|
+
};
|
|
117
|
+
margin?: {
|
|
118
|
+
x: number;
|
|
119
|
+
y: number;
|
|
120
|
+
width: number;
|
|
121
|
+
height: number;
|
|
122
|
+
};
|
|
123
|
+
atlasCrop?: {
|
|
124
|
+
region: {
|
|
125
|
+
x: number;
|
|
126
|
+
y: number;
|
|
127
|
+
width: number;
|
|
128
|
+
height: number;
|
|
129
|
+
};
|
|
130
|
+
margin: {
|
|
131
|
+
x: number;
|
|
132
|
+
y: number;
|
|
133
|
+
width: number;
|
|
134
|
+
height: number;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
size?: {
|
|
138
|
+
width: number;
|
|
139
|
+
height: number;
|
|
140
|
+
};
|
|
141
|
+
fontFamily?: string;
|
|
142
|
+
fontUrl?: string;
|
|
143
|
+
fontStyle?: "normal" | "italic";
|
|
144
|
+
fontWeight?: number | string;
|
|
145
|
+
glyphSpacing?: number;
|
|
146
|
+
fontMsdf?: boolean;
|
|
147
|
+
message?: string;
|
|
148
|
+
}
|
|
149
|
+
interface GodotHtmlFontFace {
|
|
150
|
+
fontFamily: string;
|
|
151
|
+
url: string;
|
|
152
|
+
style: "normal" | "italic";
|
|
153
|
+
weight: string;
|
|
154
|
+
}
|
|
155
|
+
interface GodotTextAutoFitNominalMetrics {
|
|
156
|
+
contentWidthPx: number;
|
|
157
|
+
contentHeightPx: number;
|
|
158
|
+
lines?: {
|
|
159
|
+
widthPx?: number;
|
|
160
|
+
heightPx?: number;
|
|
161
|
+
}[];
|
|
162
|
+
metricSource?: string;
|
|
163
|
+
}
|
|
164
|
+
interface GodotTextAutoFitDirective {
|
|
165
|
+
minFontSizePx: number;
|
|
166
|
+
maxFontSizePx: number;
|
|
167
|
+
nominalFontSizePx?: number;
|
|
168
|
+
/**
|
|
169
|
+
* Paragraph geometry the host engine (Godot) measured at `nominalFontSizePx`.
|
|
170
|
+
* When present, auto-fit scales these by `candidate / nominalFontSizePx` to pick
|
|
171
|
+
* a size that matches the engine, instead of measuring the live DOM (which drifts
|
|
172
|
+
* before web fonts resolve). Absent => fall back to DOM measurement.
|
|
173
|
+
*/
|
|
174
|
+
nominalMetrics?: GodotTextAutoFitNominalMetrics;
|
|
175
|
+
fitWidth: boolean;
|
|
176
|
+
fitHeight: boolean;
|
|
177
|
+
wrapMode?: string;
|
|
178
|
+
textOverrunBehavior?: string;
|
|
179
|
+
}
|
|
180
|
+
interface GodotShaderLoadingFallback {
|
|
181
|
+
background: string;
|
|
182
|
+
clipPath?: string;
|
|
183
|
+
borderRadius?: string;
|
|
184
|
+
}
|
|
185
|
+
type GodotBbcodeTagDescriptor = {
|
|
186
|
+
kind: "color";
|
|
187
|
+
value: string;
|
|
188
|
+
} | {
|
|
189
|
+
kind: "style";
|
|
190
|
+
css: Record<string, string>;
|
|
191
|
+
} | {
|
|
192
|
+
kind: "effect";
|
|
193
|
+
perChar?: boolean;
|
|
194
|
+
perWord?: boolean;
|
|
195
|
+
className?: string;
|
|
196
|
+
};
|
|
197
|
+
interface GodotHtmlRenderOptions {
|
|
198
|
+
classPrefix?: string;
|
|
199
|
+
textAutoFitByPath?: Record<string, GodotTextAutoFitDirective>;
|
|
200
|
+
resolveResource?: (ref: GodotResourceRefValue, node: GodotNode) => unknown;
|
|
201
|
+
resolveResourcePath?: (path: string, node: GodotNode) => unknown;
|
|
202
|
+
resolveTheme?: (node: GodotNode, name: string) => GodotVariant | undefined;
|
|
203
|
+
bbcodeTags?: Record<string, GodotBbcodeTagDescriptor>;
|
|
204
|
+
contentScale?: GodotContentScale;
|
|
205
|
+
textScale?: GodotTextScaleOption;
|
|
206
|
+
tintFilterIdPrefix?: string;
|
|
207
|
+
anchorsByPath?: GodotAnchorMap;
|
|
208
|
+
enableWebglShaders?: boolean;
|
|
209
|
+
shaderLoadingFallbacksByPath?: Record<string, GodotShaderLoadingFallback>;
|
|
210
|
+
webglShaderNodesByPath?: Record<string, boolean>;
|
|
211
|
+
webglShaderIds?: string[];
|
|
212
|
+
hiddenRawShaderFallbacksByPath?: Record<string, boolean>;
|
|
213
|
+
hiddenRawShaderFallbackShaderIds?: string[];
|
|
214
|
+
hsvAdjustShaderIds?: string[];
|
|
215
|
+
shaderFallbackFiltersByPath?: Record<string, string>;
|
|
216
|
+
enableParticles?: boolean;
|
|
217
|
+
particleNodesByPath?: Record<string, boolean>;
|
|
218
|
+
particleIds?: string[];
|
|
219
|
+
godotRenderer?: "forward_plus" | "mobile" | "gl_compatibility";
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* What the effect runtimes know about the frame they just presented/handled, handed to
|
|
223
|
+
* `GodotHtmlRenderOptions.onBindingRendered` alongside the node and its canvas.
|
|
224
|
+
*
|
|
225
|
+
* ADDITIVE, and it stays that way because of how TypeScript reads a callback: a
|
|
226
|
+
* consumer that declares `(node, canvas) => …` still satisfies the option, so the
|
|
227
|
+
* two-argument callers that existed before this parameter are untouched.
|
|
228
|
+
*
|
|
229
|
+
* WHY THESE THREE. A consumer that COMPOSITES the canvas itself — rather than
|
|
230
|
+
* leaving it in the DOM where the runtime placed it — has to reproduce two things
|
|
231
|
+
* the runtime otherwise expresses through CSS, and refuse a third:
|
|
232
|
+
*
|
|
233
|
+
* - `blend` is the shader's Godot `render_mode`. The runtime writes it onto the
|
|
234
|
+
* node as a `mix-blend-mode` (see `blendToMixBlendMode`), which only means
|
|
235
|
+
* anything while the canvas is painted by the page's compositor; a consumer
|
|
236
|
+
* drawing the same pixels into its own surface needs the mode itself.
|
|
237
|
+
* - `usesScreenTexture` says the frame was shaded against a capture of what the
|
|
238
|
+
* page had already painted BEHIND the node. That capture is a DOM composite
|
|
239
|
+
* (see `enableScreenTextureCapture`), so it describes the page's stacking — not
|
|
240
|
+
* a consumer's own surface — and a consumer whose scene is somewhere else
|
|
241
|
+
* should treat the frame as content it cannot reproduce faithfully.
|
|
242
|
+
* - `usesScreenUv` is the weaker twin: the shader read SCREEN_UV, so its output
|
|
243
|
+
* depends on where the node sits in the viewport, and moving the pixels
|
|
244
|
+
* elsewhere moves what they mean.
|
|
245
|
+
*
|
|
246
|
+
* The PARTICLE runtime reports a constant (`"mix"`, neither screen flag): a
|
|
247
|
+
* particle system's own additive mode is resolved INSIDE its canvas, whose
|
|
248
|
+
* finished pixels are premultiplied and composite source-over like any other.
|
|
249
|
+
*
|
|
250
|
+
* …AND A FOURTH, `staticKey`, which is about identity rather than about how to
|
|
251
|
+
* composite one frame: see its own note below.
|
|
252
|
+
*/
|
|
253
|
+
interface GodotEffectRenderInfo {
|
|
254
|
+
/** The shader sampled SCREEN_TEXTURE. Always false for particles. */
|
|
255
|
+
usesScreenTexture: boolean;
|
|
256
|
+
/** The shader read SCREEN_UV. Always false for particles. */
|
|
257
|
+
usesScreenUv: boolean;
|
|
258
|
+
/** The shader's `render_mode` blend; always `"mix"` for particles. */
|
|
259
|
+
blend: GodotBlendMode;
|
|
260
|
+
/**
|
|
261
|
+
* The NAME OF THE FRAME this canvas now holds — the runtime's own static-frame
|
|
262
|
+
* key — or null when the frame is not content-addressed at all (live/animating
|
|
263
|
+
* mode, a screen-space shader, textures still decoding, a particle state the
|
|
264
|
+
* live loop has stepped, a blank that ends a burst).
|
|
265
|
+
*
|
|
266
|
+
* WHAT IT PROMISES: the frame is a PURE FUNCTION of this string. It is the same
|
|
267
|
+
* key the static-frame cache stores the bitmap under and the same one the image
|
|
268
|
+
* swap dedupes by, and it is what licenses those two to hand ONE binding's
|
|
269
|
+
* pixels to another. So two canvases reporting one key hold the same picture,
|
|
270
|
+
* and a host that uploads these canvases into its own renderer can upload ONE
|
|
271
|
+
* texture and point every twin at it — which on a hand of identical card glows
|
|
272
|
+
* is one upload instead of seven, of a surface that can be megabytes.
|
|
273
|
+
*
|
|
274
|
+
* A null key means "assume nothing": that frame may be unique to this binding,
|
|
275
|
+
* and a host must key it privately (its node id) as it always did.
|
|
276
|
+
*/
|
|
277
|
+
staticKey: string | null;
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region src/diagnostics.d.ts
|
|
281
|
+
type UnsupportedRenderKind = "shader" | "particle";
|
|
282
|
+
interface UnsupportedRenderInfo {
|
|
283
|
+
kind: UnsupportedRenderKind;
|
|
284
|
+
/** Shader identity (uid/path) or particle node path — enough to locate the offending node. */
|
|
285
|
+
id: string;
|
|
286
|
+
/** Short reason, e.g. "unsupported shader construct", "shader failed to compile", "shader source
|
|
287
|
+
* unresolved", "malformed particle spec". */
|
|
288
|
+
reason: string;
|
|
289
|
+
/** The underlying error, when the failure threw. */
|
|
290
|
+
error?: unknown;
|
|
291
|
+
}
|
|
292
|
+
type UnsupportedRenderReporter = (info: UnsupportedRenderInfo) => void;
|
|
293
|
+
declare function reportUnsupportedRender(info: UnsupportedRenderInfo, onUnsupported?: UnsupportedRenderReporter): void;
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region src/effects-loop-pacing.d.ts
|
|
296
|
+
/** How a capped effect loop arms its next tick (see `GodotHtmlRenderOptions.effectsLoopPacing`). */
|
|
297
|
+
type EffectsLoopPacing = "timer" | "raf";
|
|
298
|
+
/** Slop (seconds) around a cap boundary, `"timer"` pacing only (see the module doc). */
|
|
299
|
+
declare const PARK_SLOP_S = 0.004;
|
|
300
|
+
/** The wakeup scheduler of one capped effect loop (see `createEffectsLoopPacer`). */
|
|
301
|
+
interface EffectsLoopPacer {
|
|
302
|
+
/** Whether a boundary `remaining` seconds away counts as reached now (absorbs the park slop). */
|
|
303
|
+
isDue(remaining: number): boolean;
|
|
304
|
+
/** Whether a wakeup (rAF or park timer) is already in flight. */
|
|
305
|
+
isArmed(): boolean;
|
|
306
|
+
/** Arm the next tick `remaining` seconds from now. No-op while already armed. */
|
|
307
|
+
arm(remaining: number): void;
|
|
308
|
+
/** Drop a pending park (NOT a pending rAF) — for a cap change that invalidates its deadline. */
|
|
309
|
+
cancelPark(): void;
|
|
310
|
+
/** Drop every pending wakeup. */
|
|
311
|
+
cancel(): void;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Create the wakeup scheduler for one capped effect loop. `tick` is the loop body; it runs
|
|
315
|
+
* inside a rAF callback in both pacing modes.
|
|
316
|
+
*/
|
|
317
|
+
declare function createEffectsLoopPacer(tick: () => void, pacing: EffectsLoopPacing | undefined): EffectsLoopPacer;
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/surface-image-swap.d.ts
|
|
320
|
+
/** The DOM attribute stamped on the `<img>` that stands in for a frozen surface's canvas. */
|
|
321
|
+
declare const STATIC_SURFACE_IMAGE_ATTR = "data-godot-shader-image";
|
|
322
|
+
/** Default `content-key` gate: how many consecutive unchanged observations of a surface's content key
|
|
323
|
+
* are required before it is swapped. Small on purpose — the revert is the real safety net, and the
|
|
324
|
+
* clock ticks many times a second — so 3 costs a fraction of a second of latency and excludes a
|
|
325
|
+
* surface that is merely between two states. */
|
|
326
|
+
declare const STABLE_OBSERVATIONS_BEFORE_SWAP = 3;
|
|
327
|
+
/** Default `quiet-window` gate: a surface's own draws must hold still this long before it freezes. */
|
|
328
|
+
declare const DEFAULT_QUIET_WINDOW_MS = 1000;
|
|
329
|
+
/** Default watchdog cadence, for the gates that need one (see the module doc). */
|
|
330
|
+
declare const DEFAULT_SURFACE_WATCHDOG_MS = 3000;
|
|
331
|
+
/** Default encode pacing: surfaces per batch (see the module doc). */
|
|
332
|
+
declare const DEFAULT_ENCODE_SLICE = 4;
|
|
333
|
+
/** Default encode pacing: gap between batches, ms. */
|
|
334
|
+
declare const DEFAULT_ENCODE_INTERVAL_MS = 120;
|
|
335
|
+
/** Default cap on one unbroken run of encode deferrals, for EITHER reason (see the module doc). Long
|
|
336
|
+
* enough that an ordinary burst of host activity is ridden out whole, short enough that a host whose
|
|
337
|
+
* predicate is stuck ON degrades to "the fleet freezes slowly" rather than "the fleet never
|
|
338
|
+
* freezes". */
|
|
339
|
+
declare const DEFAULT_ENCODE_BUSY_MAX_DEFER_MS = 3000;
|
|
340
|
+
/** Default `encode.perTask`: `0` reads as "the whole slice", i.e. a window's entire budget drains
|
|
341
|
+
* back-to-back in one timer task — the behavior every existing consumer already has. A host that has
|
|
342
|
+
* MEASURED its readbacks in the tens or hundreds of ms should pin `1`; see `pumpEncodes`. */
|
|
343
|
+
declare const DEFAULT_ENCODE_PER_TASK = 0;
|
|
344
|
+
/** Default gap between two encode TASKS inside one pacing window (only reachable when
|
|
345
|
+
* `perTask < slice`). 16 ms ≈ one 60 Hz display frame, chosen so the host's own rAF gets to run
|
|
346
|
+
* BETWEEN two readbacks — which is what makes a frame-derived `busy` predicate fresh again instead
|
|
347
|
+
* of stale for the whole drain. */
|
|
348
|
+
declare const DEFAULT_ENCODE_TASK_GAP_MS = 16;
|
|
349
|
+
/** Default `encode.slowEncodeMs`: `0` = the adaptive backoff is OFF, so nothing changes for a host
|
|
350
|
+
* that has not asked for it. */
|
|
351
|
+
declare const DEFAULT_ENCODE_SLOW_MS = 0;
|
|
352
|
+
/** Default hold after a readback measured at or over `encode.slowEncodeMs`. */
|
|
353
|
+
declare const DEFAULT_ENCODE_SLOW_BACKOFF_MS = 1000;
|
|
354
|
+
/** Default `encode.maxDim`: `0` = the source canvas is read back at its full backing-store size,
|
|
355
|
+
* exactly as it always has been. */
|
|
356
|
+
declare const DEFAULT_ENCODE_MAX_DIM = 0;
|
|
357
|
+
/** Default `encode.parkedStillBytes`: `0` = parked stills are OFF and a revert revokes immediately,
|
|
358
|
+
* which is what every consumer has today. The trade is retained pixel memory for skipped readbacks
|
|
359
|
+
* (see the module doc's "PARKED STILLS"), so the budget is the host's to choose — 24 MB is the
|
|
360
|
+
* measured shape of one fleet's worth of quiet-window stills, not a number this module may assume. */
|
|
361
|
+
declare const DEFAULT_PARKED_STILL_BYTES = 0;
|
|
362
|
+
/** Default `encode.stillCacheBytes`: `0` = KEYED retention is off, so an entry whose last holder
|
|
363
|
+
* lets go is revoked exactly as it always has been. Same reasoning as `parkedStillBytes` — the
|
|
364
|
+
* budget is memory this module cannot know a device has — with one difference that makes it worth
|
|
365
|
+
* more per byte: a retained still is claimable by KEY, so its bytes serve every surface that ever
|
|
366
|
+
* reaches that frame rather than the one surface that parked it. */
|
|
367
|
+
declare const DEFAULT_STILL_CACHE_BYTES = 0;
|
|
368
|
+
/** What a capture hook answers INSTEAD of a canvas to report the failure that otherwise reads as a
|
|
369
|
+
* success: the capture completed and holds NO VISIBLE PIXELS for a frame the producer knows it drew
|
|
370
|
+
* (see the module doc's BLANK CAPTURES). A plain `null` stays what it always was — "I could not
|
|
371
|
+
* produce these pixels at all" — and the two are counted apart. */
|
|
372
|
+
declare const STATIC_CAPTURE_BLANK = "blank";
|
|
373
|
+
/** What `StaticImageSwapBinding.captureCanvas` resolves to: the frame as an ordinary 2D canvas,
|
|
374
|
+
* `STATIC_CAPTURE_BLANK`, or null. */
|
|
375
|
+
type StaticSurfaceCapture = HTMLCanvasElement | typeof STATIC_CAPTURE_BLANK | null;
|
|
376
|
+
/** Whatever the injected `setTimeout` seam returns; only ever handed back to the injected
|
|
377
|
+
* `clearTimeout`. Deliberately opaque so a host can inject any scheduler. */
|
|
378
|
+
type StaticSurfaceTimerHandle = unknown;
|
|
379
|
+
/** The default gate: the host names each painted frame with a content key, and the surface swaps
|
|
380
|
+
* once that key has been observed unchanged `observations` times (default
|
|
381
|
+
* `STABLE_OBSERVATIONS_BEFORE_SWAP`). */
|
|
382
|
+
interface StaticSurfaceContentKeyGate {
|
|
383
|
+
kind: "content-key";
|
|
384
|
+
observations?: number;
|
|
385
|
+
}
|
|
386
|
+
/** The keyless gate: a surface becomes eligible `quietMs` (default `DEFAULT_QUIET_WINDOW_MS`) after
|
|
387
|
+
* its LAST DRAW, and thaws the instant it draws again. See the module doc on why the clock is the
|
|
388
|
+
* per-draw signal and never `reconcile()`, and on why this gate needs the watchdog. */
|
|
389
|
+
interface StaticSurfaceQuietWindowGate {
|
|
390
|
+
kind: "quiet-window";
|
|
391
|
+
quietMs?: number;
|
|
392
|
+
/** The window for a surface whose LAST reported key was non-null (see the module doc's
|
|
393
|
+
* KEYED-OR-QUIET). Default: `quietMs`, i.e. this option is inert unless a host asks for it — a
|
|
394
|
+
* host that reports no keys, or that wants keyed surfaces held to the same window as keyless
|
|
395
|
+
* ones, is unaffected by its existence.
|
|
396
|
+
*
|
|
397
|
+
* `0` means "eligible the instant it paints", which is the setting for a host whose key is a
|
|
398
|
+
* complete description of the frame: there is nothing to wait FOR, because a second paint under
|
|
399
|
+
* the same key would not change a pixel and a paint under a different key reverts anyway. The
|
|
400
|
+
* cost of pinning it wrongly is stated at the module doc: a stale `<img>` no proxy can catch. */
|
|
401
|
+
keyedQuietMs?: number;
|
|
402
|
+
}
|
|
403
|
+
type StaticSurfaceGate = StaticSurfaceContentKeyGate | StaticSurfaceQuietWindowGate;
|
|
404
|
+
/** Batching for the (main-thread) encodes — see the module doc. */
|
|
405
|
+
interface StaticSurfaceEncodePacing {
|
|
406
|
+
/** Encodes kicked per batch. Default `DEFAULT_ENCODE_SLICE`. */
|
|
407
|
+
slice?: number;
|
|
408
|
+
/** Gap between batches, ms. Default `DEFAULT_ENCODE_INTERVAL_MS`. */
|
|
409
|
+
intervalMs?: number;
|
|
410
|
+
/** `"smallest-first"` (default) orders a batch by backing-store area; `"dom"` keeps the order the
|
|
411
|
+
* surfaces became eligible in. */
|
|
412
|
+
order?: "smallest-first" | "dom";
|
|
413
|
+
/** `true` defers even the head of a burst through the timer seam instead of encoding it inline on
|
|
414
|
+
* the caller's stack (see the module doc's "ENCODE PACING" section). Default `false` — the head
|
|
415
|
+
* stays inline, which is today's behavior and every existing consumer's default. */
|
|
416
|
+
deferHead?: boolean;
|
|
417
|
+
/** HOST BUSY SIGNAL: "is this a bad instant to spend ~30 ms on a GPU readback?". Consulted ONCE per
|
|
418
|
+
* drain pass (never per queued surface), and a pass that it defers re-arms at `intervalMs` rather
|
|
419
|
+
* than encoding — the head included, `deferHead` or not. The host owns this because only the host
|
|
420
|
+
* can see its own frame loop; absent, nothing about the pacing changes. A predicate that THROWS
|
|
421
|
+
* fails OPEN (the pass encodes): a host bug must not be able to stop the mechanism. */
|
|
422
|
+
busy?: () => boolean;
|
|
423
|
+
/** Cap on one unbroken run of deferrals, ms — ONE bound shared by `busy` and by the adaptive
|
|
424
|
+
* backoff below, so neither of them (nor the two together) can stop the fleet. Default
|
|
425
|
+
* `DEFAULT_ENCODE_BUSY_MAX_DEFER_MS`; `0` means NEVER DEFER, i.e. the whole deferral apparatus is
|
|
426
|
+
* switched off — `busy` is not consulted and `slowEncodeMs` is not consulted (the A/B lever for a
|
|
427
|
+
* host that wants both wired but disabled). Once the bound elapses one pass goes out against a
|
|
428
|
+
* still-busy host and the bound restarts, so deferring can only ever slow the fleet down — see the
|
|
429
|
+
* module doc. */
|
|
430
|
+
busyMaxDeferMs?: number;
|
|
431
|
+
/** Readbacks allowed in ONE task. Default: `slice` (today: the whole slice drains back-to-back).
|
|
432
|
+
* `1` is the setting for a host whose surfaces are GPU-resident and big: `toBlob` on such a canvas
|
|
433
|
+
* is a SYNCHRONOUS GPU→CPU readback, so N of them in one task is one unsplittable park of
|
|
434
|
+
* N × readback — measured downstream at 1,163 ms for four ~4821×2156 surfaces on a phone whose GPU
|
|
435
|
+
* was already saturated, against 6-13 ms each for the SAME surfaces once the load passed. `slice`
|
|
436
|
+
* still bounds THROUGHPUT per `intervalMs`; this bounds the BLOCK. Setting it below `slice` costs
|
|
437
|
+
* `taskGapMs` per extra task and buys back the ability to be interrupted. */
|
|
438
|
+
perTask?: number;
|
|
439
|
+
/** Gap between two encode tasks inside one window, ms. Default `DEFAULT_ENCODE_TASK_GAP_MS`.
|
|
440
|
+
* Unreachable (and therefore inert) while `perTask >= slice`. */
|
|
441
|
+
taskGapMs?: number;
|
|
442
|
+
/** ADAPTIVE BACKOFF, the module's own evidence about readback cost — a readback whose SYNCHRONOUS
|
|
443
|
+
* part took at least this long holds the next one for `slowBackoffMs`. On a CAPTURE-HOOK source
|
|
444
|
+
* (`StaticImageSwapBinding.captureCanvas`) there is no synchronous part to measure, so what is
|
|
445
|
+
* compared against this threshold is the capture's WALL time — the GPU readback's own latency,
|
|
446
|
+
* which is precisely the condition this lever exists to back off from. Either way a surface over
|
|
447
|
+
* the threshold books `staticImageSlowEncodes`. Default
|
|
448
|
+
* `DEFAULT_ENCODE_SLOW_MS` (0 = off). This exists because a host's `busy` predicate is usually
|
|
449
|
+
* derived from its frame loop, and a long readback SUPPRESSES the frames that signal is made of:
|
|
450
|
+
* the jam manufactures a stale "idle" reading exactly when the system is most overloaded (the
|
|
451
|
+
* module doc's stale-signal trap). The measured cost of the previous readback cannot be faked that
|
|
452
|
+
* way. Bounded by `busyMaxDeferMs` like every other deferral. */
|
|
453
|
+
slowEncodeMs?: number;
|
|
454
|
+
/** How long a slow readback holds the next one, ms. Default `DEFAULT_ENCODE_SLOW_BACKOFF_MS`. */
|
|
455
|
+
slowBackoffMs?: number;
|
|
456
|
+
/** READBACK CLAMP: longest edge (backing-store px) the encode may read. Over it, the surface is
|
|
457
|
+
* blitted into a scratch canvas at the clamped, ASPECT-PRESERVED size and that is what `toBlob`
|
|
458
|
+
* reads — a GPU-side downscale, so a 4821×2156 surface reads back 5.5× fewer pixels. Default
|
|
459
|
+
* `DEFAULT_ENCODE_MAX_DIM` (0 = no clamp).
|
|
460
|
+
* FIDELITY: the stand-in is presented at the canvas's CSS box with `object-fit: fill`, so a clamp
|
|
461
|
+
* is a resampling, not a re-layout — but it IS visible on a surface whose backing store was denser
|
|
462
|
+
* than its CSS box. Soft output (a vignette, fog, a glow) survives it; sharp output does not. Ship
|
|
463
|
+
* it behind a host A/B, never as a silent default.
|
|
464
|
+
* KEY SHARING: `entry.key` names the FRAME, not the encode size, and two surfaces on one key may
|
|
465
|
+
* already have different backing sizes — the first to reach the gate encodes and the other
|
|
466
|
+
* stretches it. The clamp does not change that contract, only the pixel size of the shared
|
|
467
|
+
* frame. */
|
|
468
|
+
maxDim?: number;
|
|
469
|
+
/** PARKED STILLS budget, bytes (see the module doc). A revert parks its encoded frame instead of
|
|
470
|
+
* revoking it, so a re-freeze of the same, unpainted surface costs no readback at all; this is the
|
|
471
|
+
* ceiling on the retained blob bytes that buys. Default `DEFAULT_PARKED_STILL_BYTES` (0 = off,
|
|
472
|
+
* today's immediate revoke). The pool is MODULE-wide (key dedup already is), evicts
|
|
473
|
+
* least-recently-parked first, and applies the budget of whichever policy last parked into it — a
|
|
474
|
+
* document running several swappers should give them the same number. */
|
|
475
|
+
parkedStillBytes?: number;
|
|
476
|
+
/** RETAINED STILLS budget, bytes (see the module doc's "RETAINED STILLS"). An entry under a real
|
|
477
|
+
* content key whose LAST holder lets go — by revert or by dispose — is kept, held by nobody, so
|
|
478
|
+
* the next surface to reach that key attaches for zero readback (`claimStaticStill`) and so
|
|
479
|
+
* `bakeStill` has somewhere to publish. Default `DEFAULT_STILL_CACHE_BYTES` (0 = off, i.e. the
|
|
480
|
+
* revoke every consumer has today).
|
|
481
|
+
* SHARES ONE POOL AND ONE EVICTION WALK with `parkedStillBytes`: the budget the pool is trimmed
|
|
482
|
+
* to is the SUM of the two, and either kind may evict the other, least-recently-pooled first.
|
|
483
|
+
* What stays separate is admission — this number alone decides whether a KEYED entry may be
|
|
484
|
+
* retained — so the two mechanisms can still be switched on and off independently. */
|
|
485
|
+
stillCacheBytes?: number;
|
|
486
|
+
/** Exempt the FIRST encode of each key from the deferral apparatus (`busy`, `slowEncodeMs`), not
|
|
487
|
+
* from the pacing (see the module doc's "PRIMING UNSEEN KEYS"). Default `false`.
|
|
488
|
+
* The judgement it encodes is about SURFACE SIZE: deferral was tuned against ~4821×2156 readbacks
|
|
489
|
+
* measured at ~290 ms on a loaded phone, and a fleet of ~320 px canvases collapsing to ~2 distinct
|
|
490
|
+
* keys is three orders off that — there, holding the one encode a key will ever need buys no park
|
|
491
|
+
* back and leaves the whole fleet in the composite for another window. A host with big surfaces
|
|
492
|
+
* must leave this off. */
|
|
493
|
+
primeUnseenKeys?: boolean;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* The host's policy for the surface image swap. `true` (or an absent option) is exactly
|
|
497
|
+
* `{ gate: { kind: "content-key" }, onInvalidate: "block" }` — the behavior that shipped first;
|
|
498
|
+
* `false` disables the mechanism entirely and the runtime takes the path it took before it existed.
|
|
499
|
+
*/
|
|
500
|
+
interface StaticSurfacePolicy {
|
|
501
|
+
/** How a surface earns its swap. Default `{ kind: "content-key" }`. */
|
|
502
|
+
gate?: StaticSurfaceGate;
|
|
503
|
+
/** What a post-swap invalidation (a content-key change, a failed encode/decode) does to the
|
|
504
|
+
* surface. Default `"block"` — never offer it again. `"retry"` reverts, resets the gate, and lets
|
|
505
|
+
* it re-earn (failed encodes are rescheduled on the encode cadence, forever). */
|
|
506
|
+
onInvalidate?: "block" | "retry";
|
|
507
|
+
/** Encode batching. */
|
|
508
|
+
encode?: StaticSurfaceEncodePacing;
|
|
509
|
+
/** HOST VETO, consulted inside the "should I swap this?" decision right beside the dormancy check.
|
|
510
|
+
* Return false to keep a surface on its canvas — for a host running its own occlusion /
|
|
511
|
+
* virtualizer pass over the same DOM, this is how it says "I already claimed this element",
|
|
512
|
+
* which is the only way two mechanisms can avoid both owning one element's `display`. */
|
|
513
|
+
canFreezeSurface?: (node: HTMLElement, canvas: HTMLCanvasElement) => boolean;
|
|
514
|
+
/** Standing verification cadence over the swapped set, ms (see the module doc). 0 disables it.
|
|
515
|
+
* Default: `DEFAULT_SURFACE_WATCHDOG_MS` for the `quiet-window` gate (which REQUIRES it), 0 for
|
|
516
|
+
* `content-key` (whose invariant makes it unnecessary, and which therefore keeps costing zero
|
|
517
|
+
* idle wakeups). */
|
|
518
|
+
watchdogMs?: number;
|
|
519
|
+
/** RESERVED FOR THE HOST RUNTIME — the code that owns the bindings, not the end consumer that
|
|
520
|
+
* configures it. Called once after a swap is undone, for ANY cause, with the revert already
|
|
521
|
+
* complete (entry released, `<img>` gone, canvas visibility restored), so a re-entrant
|
|
522
|
+
* `revertStaticImage` from inside it finds nothing to revert and is a no-op.
|
|
523
|
+
*
|
|
524
|
+
* WHY IT EXISTS. A revert normally uncovers a canvas that still holds the right frame — that is
|
|
525
|
+
* the mechanism's safety net, and it needs no notification. `claimStaticStill` breaks that
|
|
526
|
+
* assumption on purpose: a surface mounted straight from a cached still has NEVER painted, so
|
|
527
|
+
* under its `<img>` is a canvas with no pixels (and, in the runtime this was written for, no
|
|
528
|
+
* context and no backing store either). An AUTONOMOUS revert — the watchdog, a host
|
|
529
|
+
* `invalidateStaticSurfaces`, a re-size — would therefore uncover a blank surface with nothing
|
|
530
|
+
* scheduled to fix it. This is how that runtime hears about it in time to build the surface and
|
|
531
|
+
* draw, in the same task, before anything composites.
|
|
532
|
+
* A runtime that wraps a consumer's policy MUST run the consumer's handler too, if one was given;
|
|
533
|
+
* this module calls exactly the one function it is handed.
|
|
534
|
+
* A handler that throws is a host bug and is swallowed here: it must not be able to leave a
|
|
535
|
+
* half-reverted surface behind. */
|
|
536
|
+
onRevert?: (binding: StaticImageSwapBinding) => void;
|
|
537
|
+
/** Injectable monotonic clock, ms. Default `performance.now()` (falling back to `Date.now()`). */
|
|
538
|
+
now?: () => number;
|
|
539
|
+
/** Injectable timer seam. Both must be supplied together; absent ⇒ the globals, and where there is
|
|
540
|
+
* no timer host at all (SSR) every deferred path is simply inert. */
|
|
541
|
+
setTimeout?: (fn: () => void, ms: number) => StaticSurfaceTimerHandle;
|
|
542
|
+
clearTimeout?: (handle: StaticSurfaceTimerHandle) => void;
|
|
543
|
+
}
|
|
544
|
+
/** The public option shape: `true`/`false` keep their original meaning, an object supplies policy. */
|
|
545
|
+
type StaticSurfaceOption = boolean | StaticSurfacePolicy;
|
|
546
|
+
/** Why a live swap was undone. `staticImageRevertsByCause` splits the aggregate by these. */
|
|
547
|
+
type StaticImageRevertCause = /** The content key moved (the `content-key` gate's churn case). */"key-change" /** The surface stopped producing frozen output at all (the host reported a null key). */ | "not-frozen" /** The surface drew again (the `quiet-window` gate's thaw). */ | "draw" /** A host `invalidateStaticSurfaces` call. */ | "host-invalidate" /** The watchdog found a surface that was no longer legitimately frozen. */ | "watchdog"
|
|
548
|
+
/** A runtime-wide deliberate change: a re-size, a pixel-ratio pin, a mode flip, the kill switch,
|
|
549
|
+
* or a geometry move of the canvas itself. */
|
|
550
|
+
| "resize" /** A dormancy wake whose next repaint cannot be attributed (see `noteStaticSurfaceWake`). */ | "dormancy-wake" /** The `<img>` would not decode. */ | "decode-failure";
|
|
551
|
+
/** The counters this module bumps on the host's live stats object (`WebglShaderRuntimeStats`
|
|
552
|
+
* extends this). Build one with `createStaticImageSwapCounters()` so a later field addition does
|
|
553
|
+
* not break every construction site. */
|
|
554
|
+
interface StaticImageSwapCounters {
|
|
555
|
+
/** COUNTER. Surfaces whose `<img>` went live (canvas hidden). Monotonic; a surface that swaps,
|
|
556
|
+
* reverts and swaps again counts twice. */
|
|
557
|
+
staticImageSwaps: number;
|
|
558
|
+
/** COUNTER. Live swaps undone, for ANY reason — the aggregate of `staticImageRevertsByCause`,
|
|
559
|
+
* kept under its original name so existing dashboards keep working. Disposal does NOT count
|
|
560
|
+
* (the surface is gone, not reverted). */
|
|
561
|
+
staticImageReverts: number;
|
|
562
|
+
/** COUNTER, per cause (see `StaticImageRevertCause`). Sums to `staticImageReverts`. */
|
|
563
|
+
staticImageRevertsByCause: Record<StaticImageRevertCause, number>;
|
|
564
|
+
/** COUNTER. Frames encoded — ONE per distinct content key, however many surfaces share it.
|
|
565
|
+
* Counted when the blob is in hand. */
|
|
566
|
+
staticImageEncodes: number;
|
|
567
|
+
/** COUNTER. Encodes/decodes that failed or are unsupported (`toBlob` missing, a null blob, an
|
|
568
|
+
* `<img>` that would not decode). Every one of them leaves the surface on its canvas. */
|
|
569
|
+
staticImageFailures: number;
|
|
570
|
+
/** COUNTER. Drain passes the host's `encode.busy` predicate sent away — one per PASS, whatever the
|
|
571
|
+
* queue length. Zero unless a host supplies the predicate. NOTE, for anyone comparing across
|
|
572
|
+
* versions: a host that pins `encode.perTask` below its `slice` makes MORE passes for an identical
|
|
573
|
+
* workload, so this number rises with it. The diagnosis pair below is a ratio and is unaffected. */
|
|
574
|
+
staticImageBusyDeferrals: number;
|
|
575
|
+
/** COUNTER. Passes that encoded against a still-deferring signal because `busyMaxDeferMs` had
|
|
576
|
+
* elapsed. The diagnosis pair: `staticImageBusyForcedEncodes` ≈ `staticImageBusyDeferrals` means
|
|
577
|
+
* the host's predicate is stuck ON (every deferral ran the bound out), while forced ≪ deferrals is
|
|
578
|
+
* the signal working as intended — bursts ridden out, quiet windows drained normally. */
|
|
579
|
+
staticImageBusyForcedEncodes: number;
|
|
580
|
+
/** COUNTER, ms. Summed SYNCHRONOUS cost of every readback (`toBlob`'s own call, any `maxDim` clamp
|
|
581
|
+
* blit included) — the main-thread park this module is charged for, as a number rather than a
|
|
582
|
+
* trace. */
|
|
583
|
+
staticImageEncodeMs: number;
|
|
584
|
+
/** HIGH-WATER, ms. The single worst readback. The regression probe for "N readbacks in one task":
|
|
585
|
+
* under `encode.perTask: 1` this IS the longest task the mechanism can produce. */
|
|
586
|
+
staticImageEncodeMaxMs: number;
|
|
587
|
+
/** COUNTER. Readbacks measured at or over `encode.slowEncodeMs`, i.e. the ones that armed the
|
|
588
|
+
* adaptive backoff. Zero unless a host asks for it. */
|
|
589
|
+
staticImageSlowEncodes: number;
|
|
590
|
+
/** COUNTER. Passes the ADAPTIVE BACKOFF sent away. Sibling of `staticImageBusyDeferrals`: the two
|
|
591
|
+
* split the deferral total by who asked for it (the host's predicate, or this module's own
|
|
592
|
+
* measurement of the previous readback). */
|
|
593
|
+
staticImageBackoffDeferrals: number;
|
|
594
|
+
/** COUNTER. Encodes that read a downscaled scratch instead of the source canvas (`encode.maxDim`). */
|
|
595
|
+
staticImageClampedEncodes: number;
|
|
596
|
+
/** COUNTER. Frames produced through a binding's `captureCanvas` hook — one per ENCODE (so one per
|
|
597
|
+
* distinct content key, like `staticImageEncodes`), not one per surface. Zero on a runtime whose
|
|
598
|
+
* surfaces are all directly readable canvases; on a WebGPU runtime it should track
|
|
599
|
+
* `staticImageEncodes` exactly. */
|
|
600
|
+
staticImageCaptures: number;
|
|
601
|
+
/** COUNTER. Capture hooks that answered null, threw, or handed back a degenerate canvas. Each one
|
|
602
|
+
* ALSO lands in `staticImageFailures` (it goes through the same `fail()`), so the aggregate keeps
|
|
603
|
+
* its meaning; this is the split that says the failure was the READBACK rather than the codec. */
|
|
604
|
+
staticImageCaptureFailures: number;
|
|
605
|
+
/** COUNTER. Captures REFUSED because they held no visible pixels for a frame the producer knew it
|
|
606
|
+
* had drawn (`STATIC_CAPTURE_BLANK`, see the module doc's BLANK CAPTURES). A SUBSET of
|
|
607
|
+
* `staticImageCaptureFailures` — a capture that produced nothing is a capture failure — split out
|
|
608
|
+
* because it is the one failure that would otherwise have looked like a success: without it, a
|
|
609
|
+
* device that cannot produce still pixels reads as `staticImagesLive N/N` over N invisible
|
|
610
|
+
* surfaces.
|
|
611
|
+
*
|
|
612
|
+
* NON-ZERO MEANS ONE OF TWO THINGS, and they are told apart by whether the affected surfaces ever
|
|
613
|
+
* freeze again: a capture path that produces nothing on this device/launch mode (every capture
|
|
614
|
+
* blank, nothing swaps, and the surfaces stay on their canvases — the correct outcome), or a
|
|
615
|
+
* producer claiming coverage for a frame that really is invisible, which costs that surface its
|
|
616
|
+
* freeze and nothing else. Each blank is TERMINAL for its surface under either `onInvalidate`.
|
|
617
|
+
*
|
|
618
|
+
* ZERO IS NOT A CLEAN BILL OF HEALTH FOR A 2D-BACKED FLEET. Only a CAPTURE HOOK can book this;
|
|
619
|
+
* a surface read directly through `toBlob` has no equivalent check, on purpose and on evidence
|
|
620
|
+
* (the module doc's WHY THE DIRECT PATH IS NOT GUARDED). */
|
|
621
|
+
staticImageBlankCaptures: number;
|
|
622
|
+
/** COUNTER, ms. Summed WALL time of every capture hook. Deliberately NOT part of
|
|
623
|
+
* `staticImageEncodeMs`: that number means synchronous main-thread park, and a GPU `mapAsync`
|
|
624
|
+
* readback does not park the main thread (see the module doc's CAPTURE-HOOK section). */
|
|
625
|
+
staticImageCaptureMs: number;
|
|
626
|
+
/** HIGH-WATER, ms. The single slowest capture — the probe for a GPU that has stopped handing
|
|
627
|
+
* pixels over promptly, and the number `encode.slowEncodeMs` is compared against on this path. */
|
|
628
|
+
staticImageCaptureMaxMs: number;
|
|
629
|
+
/** COUNTER. Freezes served from a PARKED still — a re-attach that cost no readback at all because
|
|
630
|
+
* the canvas had not been painted or re-allocated since the entry was encoded (see the module
|
|
631
|
+
* doc). Zero unless a host sets `encode.parkedStillBytes`. */
|
|
632
|
+
staticImageReuseHits: number;
|
|
633
|
+
/** COUNTER. `claimStaticStill` calls that found an attachable entry for the key — one with a URL
|
|
634
|
+
* in hand, live or retained. THE measurement of the second-appearance shortcut: a hit is a
|
|
635
|
+
* surface that skipped the gate, the paint and the encode entirely. */
|
|
636
|
+
staticStillCacheHits: number;
|
|
637
|
+
/** COUNTER. `claimStaticStill` calls that found nothing attachable, so the caller must render the
|
|
638
|
+
* surface itself — no entry, a failed one, or an encode still IN FLIGHT (which has no URL yet;
|
|
639
|
+
* the caller cannot wait, so it renders, and the ordinary gate will swap it when it settles). A
|
|
640
|
+
* steady stream of these against a stable key population means the stills are not surviving —
|
|
641
|
+
* check `staticStillRetainedEntries` and `encode.stillCacheBytes` before blaming the keys. */
|
|
642
|
+
staticStillCacheMisses: number;
|
|
643
|
+
/** COUNTER. Claimed stills whose `<img>` actually went live (decoded and mounted). Sits below
|
|
644
|
+
* `staticStillCacheHits` by exactly the claims that were undone before their decode finished — a
|
|
645
|
+
* revert, a dispose, or a decode failure — which is the only way to tell "the key was there" from
|
|
646
|
+
* "the pixels reached the screen". */
|
|
647
|
+
staticStillMounts: number;
|
|
648
|
+
/** COUNTER. `bakeStill` calls that really enqueued an encode (a bake for a known key, a solo key,
|
|
649
|
+
* or an unreadable canvas is a no-op and books nothing). One per key, by construction. */
|
|
650
|
+
staticStillBakes: number;
|
|
651
|
+
/** GAUGE — entries sitting in the module-wide still pool RIGHT NOW, parked and retained together
|
|
652
|
+
* (see the module doc's "RETAINED STILLS"). Read through `staticStillPoolStats()`; like
|
|
653
|
+
* `staticImageUrlsLive` it is document-wide rather than per runtime, and a host refreshes it on
|
|
654
|
+
* each `stats()` read. */
|
|
655
|
+
staticStillRetainedEntries: number;
|
|
656
|
+
/** GAUGE — bytes those entries pin (each blob's own `size`, as recorded at publish). The number to
|
|
657
|
+
* compare against `parkedStillBytes + stillCacheBytes`: at the budget, the pool is evicting. */
|
|
658
|
+
staticStillRetainedBytes: number;
|
|
659
|
+
/** GAUGE — how many OBJECT URLS are alive right now, MODULE-wide (across every runtime in the
|
|
660
|
+
* document), refreshed on each `stats()` read. This is the leak probe: it must come back to 0
|
|
661
|
+
* after the last runtime is disposed. NOT a swap count — one URL can back many surfaces, and under
|
|
662
|
+
* `encode.parkedStillBytes` a URL held by NO surface still counts, because its bytes are still
|
|
663
|
+
* pinned. Dispose revokes those too, so the probe is unaffected. */
|
|
664
|
+
staticImageUrlsLive: number;
|
|
665
|
+
/** GAUGE — how many SURFACES are swapped right now (`<img>` up, canvas hidden) against THIS
|
|
666
|
+
* counters object, i.e. per runtime. Rises on swap, falls on revert AND on dispose, so it comes
|
|
667
|
+
* back to 0 at teardown. This is the "is the mechanism actually engaged?" measurement (`72/72`);
|
|
668
|
+
* `staticImageUrlsLive` cannot answer that, because one URL can back many surfaces. */
|
|
669
|
+
staticImagesLive: number;
|
|
670
|
+
}
|
|
671
|
+
/** A zeroed counters object (see `StaticImageSwapCounters`). */
|
|
672
|
+
declare function createStaticImageSwapCounters(): StaticImageSwapCounters;
|
|
673
|
+
/** Per-surface swap state. Present only while the mechanism is enabled — `null` is the kill switch,
|
|
674
|
+
* and the host's render path then takes exactly the code it took before this module existed. */
|
|
675
|
+
interface StaticImageState {
|
|
676
|
+
/** The content key of the frame the canvas currently holds (null until the first cacheable render,
|
|
677
|
+
* and typically null throughout for a keyless `quiet-window` surface). */
|
|
678
|
+
key: string | null;
|
|
679
|
+
/** Consecutive unchanged observations of `key` (the `content-key` gate). */
|
|
680
|
+
stable: number;
|
|
681
|
+
/** The stand-in element, once one exists (created at swap time, dropped on revert). */
|
|
682
|
+
img: HTMLImageElement | null;
|
|
683
|
+
/** The refcounted per-key object-URL entry this surface holds, or null. Non-null covers BOTH "the
|
|
684
|
+
* encode/decode is in flight" and "the `<img>` is live" — see `shown`. */
|
|
685
|
+
entry: StaticImageEntry | null;
|
|
686
|
+
/** The entry this surface's LAST freeze left parked (`encode.parkedStillBytes`), held by nobody and
|
|
687
|
+
* claimable only by this surface. Mutually exclusive with `entry`: a surface holds its still or
|
|
688
|
+
* parks it, never both. */
|
|
689
|
+
parked: StaticImageEntry | null;
|
|
690
|
+
/** The reuse fingerprint `parked` was stamped with — the paint count and backing-store size the
|
|
691
|
+
* entry was ENCODED at. `reclaimParkedStill` re-attaches only while the canvas still reports all
|
|
692
|
+
* three unchanged, which is the whole correctness argument for handing back old pixels. */
|
|
693
|
+
parkedDrawSeq: number;
|
|
694
|
+
parkedW: number;
|
|
695
|
+
parkedH: number;
|
|
696
|
+
/** The `<img>` is mounted and standing in for the canvas. */
|
|
697
|
+
shown: boolean;
|
|
698
|
+
/** The entry this surface currently holds was taken by `claimStaticStill` rather than earned
|
|
699
|
+
* through the gate — i.e. this canvas has never painted the frame the `<img>` is showing, and may
|
|
700
|
+
* never have painted at all. Cleared when the entry is let go. Read only to split
|
|
701
|
+
* `staticStillMounts` out of the ordinary swap count; the mechanics below treat a claimed surface
|
|
702
|
+
* exactly like any other, which is deliberate — its stand-in reverts, re-syncs and is refcounted
|
|
703
|
+
* by the same code. */
|
|
704
|
+
claimed: boolean;
|
|
705
|
+
/** Disqualified for the life of the binding (only reachable under `onInvalidate: "block"`). */
|
|
706
|
+
blocked: boolean;
|
|
707
|
+
/** The swapper this surface belongs to: its policy, its encode queue, its timers. Every free
|
|
708
|
+
* function in this module reads the policy from here, so one document can run several runtimes
|
|
709
|
+
* under different policies. */
|
|
710
|
+
swapper: SwapperContext;
|
|
711
|
+
/** The counters object this surface's TIMER-driven paths (sweep, watchdog, retry, dispose) bump.
|
|
712
|
+
* Seeded when the swapper attaches the binding and refreshed by every call that carries one, so
|
|
713
|
+
* a deferred revert lands on the same object the synchronous ones did. */
|
|
714
|
+
counters: StaticImageSwapCounters | null;
|
|
715
|
+
/** `now()` of the last reported paint into this canvas — the `quiet-window` gate's clock. */
|
|
716
|
+
lastDrawAt: number;
|
|
717
|
+
/** Monotonic count of reported paints; the watchdog compares it against `drawSeqAtFreeze`. */
|
|
718
|
+
drawSeq: number;
|
|
719
|
+
drawSeqAtFreeze: number;
|
|
720
|
+
/** Backing-store size at freeze time: a `width`/`height` write REALLOCATES (and clears) a canvas,
|
|
721
|
+
* which the watchdog reads as an unexplained repaint. */
|
|
722
|
+
frozenW: number;
|
|
723
|
+
frozenH: number;
|
|
724
|
+
/** The canvas's inline style at freeze time. The stand-in copies the box ONCE, so a later
|
|
725
|
+
* placement write would leave it at the old box — the watchdog re-syncs on a mismatch. */
|
|
726
|
+
boxCss: string;
|
|
727
|
+
/** Earliest `now()` at which a failed encode/decode may be retried (`onInvalidate: "retry"`). */
|
|
728
|
+
retryAfter: number;
|
|
729
|
+
/** THIS module hid the canvas (so it, and only it, may un-hide it). */
|
|
730
|
+
hidCanvas: boolean;
|
|
731
|
+
/** The canvas's `display` as the HOST left it, captured the moment this module first hid it and
|
|
732
|
+
* restored verbatim when it un-hides. */
|
|
733
|
+
hostDisplay: string;
|
|
734
|
+
}
|
|
735
|
+
/** The subset of a host runtime's node binding this module touches. Structural on purpose: it keeps
|
|
736
|
+
* the swap independently testable and keeps the host runtimes free of a back-import. */
|
|
737
|
+
interface StaticImageSwapBinding {
|
|
738
|
+
/** The host's node element — passed to `canFreezeSurface`, never otherwise read or written. */
|
|
739
|
+
node: HTMLElement;
|
|
740
|
+
canvas: HTMLCanvasElement;
|
|
741
|
+
/** Set whenever the canvas may not match `state.key` yet (a pending re-render, a realloc that
|
|
742
|
+
* cleared it). The gate refuses to encode from a dirty binding. */
|
|
743
|
+
dirty: boolean;
|
|
744
|
+
/** Parked by the host: observes nothing, and hides BOTH surfaces. */
|
|
745
|
+
dormant: boolean;
|
|
746
|
+
/** ASYNC ENCODE SOURCE, for a surface whose own canvas cannot be read back — a WebGPU one, whose
|
|
747
|
+
* every canvas-read path is blank headless and pathological on Android (see the module doc's
|
|
748
|
+
* CAPTURE-HOOK SOURCES). Returns a fresh 2D canvas holding the frame the surface is CURRENTLY
|
|
749
|
+
* showing, at its backing-store size, or null when it cannot be produced; this module encodes
|
|
750
|
+
* that canvas and then releases it. `STATIC_CAPTURE_BLANK` is the third answer: the capture
|
|
751
|
+
* completed and holds NOTHING VISIBLE, for a frame the producer knows it drew (see the module
|
|
752
|
+
* doc's BLANK CAPTURES). ABSENT ⇒ `canvas` is read directly, which is what every 2D-backed
|
|
753
|
+
* surface does and is exactly the path that shipped first. */
|
|
754
|
+
captureCanvas?: () => Promise<StaticSurfaceCapture>;
|
|
755
|
+
staticImage: StaticImageState | null;
|
|
756
|
+
}
|
|
757
|
+
/** One encoded frame, shared by every surface on that content key. */
|
|
758
|
+
interface StaticImageEntry {
|
|
759
|
+
key: string;
|
|
760
|
+
/** Surfaces holding this entry (swapped or waiting for the encode). At 0 the URL is revoked. */
|
|
761
|
+
refs: number;
|
|
762
|
+
url: string | null;
|
|
763
|
+
/** The encode failed / is unsupported and no one may retry this key. Only ever set under
|
|
764
|
+
* `onInvalidate: "block"` — a `retry` policy drops the entry instead, so the key stays open. */
|
|
765
|
+
failed: boolean;
|
|
766
|
+
/** Encoded size in bytes, captured from the blob at publish (0 until then). The still pool budgets
|
|
767
|
+
* on this: what a pooled entry costs is its retained pixels, not its pixel count. */
|
|
768
|
+
bytes: number;
|
|
769
|
+
/** Surfaces waiting for the in-flight encode. */
|
|
770
|
+
waiters: Set<StaticImageSwapBinding>;
|
|
771
|
+
/** BORN HELD BY NOBODY: this entry was created by `bakeStill` for a key with no waiting surface,
|
|
772
|
+
* so `refs: 0` is its normal state and not the "everyone let go" that the encode tail drops an
|
|
773
|
+
* entry for. The three places that read `refs <= 0` as abandonment — the queue drain, the capture
|
|
774
|
+
* tail and `publish` — consult this to tell the two apart. A published bake goes straight into
|
|
775
|
+
* the retained pool, where the flag stops mattering: from then on it behaves like any other
|
|
776
|
+
* unheld entry. */
|
|
777
|
+
bake: boolean;
|
|
778
|
+
}
|
|
779
|
+
interface ResolvedPolicy {
|
|
780
|
+
quietWindow: boolean;
|
|
781
|
+
observations: number;
|
|
782
|
+
quietMs: number;
|
|
783
|
+
/** The quiet window for a surface whose last reported key was non-null (`keyedQuietMs`, defaulted
|
|
784
|
+
* to `quietMs` here so every read site is one lookup and the inert case costs nothing). */
|
|
785
|
+
keyedQuietMs: number;
|
|
786
|
+
retry: boolean;
|
|
787
|
+
slice: number;
|
|
788
|
+
intervalMs: number;
|
|
789
|
+
smallestFirst: boolean;
|
|
790
|
+
deferHead: boolean;
|
|
791
|
+
busy: (() => boolean) | null;
|
|
792
|
+
busyMaxDeferMs: number;
|
|
793
|
+
perTask: number;
|
|
794
|
+
taskGapMs: number;
|
|
795
|
+
slowEncodeMs: number;
|
|
796
|
+
slowBackoffMs: number;
|
|
797
|
+
maxDim: number;
|
|
798
|
+
parkedStillBytes: number;
|
|
799
|
+
stillCacheBytes: number;
|
|
800
|
+
/** What the shared pool is trimmed to: the two budgets added, because the two kinds of unheld
|
|
801
|
+
* still are the same bytes (see the module doc). Precomputed so the eviction walk reads one
|
|
802
|
+
* number. */
|
|
803
|
+
poolBytes: number;
|
|
804
|
+
primeUnseenKeys: boolean;
|
|
805
|
+
watchdogMs: number;
|
|
806
|
+
canFreeze: ((node: HTMLElement, canvas: HTMLCanvasElement) => boolean) | null;
|
|
807
|
+
onRevert: ((binding: StaticImageSwapBinding) => void) | null;
|
|
808
|
+
now: () => number;
|
|
809
|
+
setT: ((fn: () => void, ms: number) => StaticSurfaceTimerHandle) | null;
|
|
810
|
+
clearT: (handle: StaticSurfaceTimerHandle) => void;
|
|
811
|
+
}
|
|
812
|
+
interface EncodeJob {
|
|
813
|
+
entry: StaticImageEntry;
|
|
814
|
+
canvas: HTMLCanvasElement;
|
|
815
|
+
/** The binding's async encode source, or null for the ordinary "read the canvas" path. Captured
|
|
816
|
+
* at ENQUEUE so the job stays self-contained: the queue outlives the call that made the surface
|
|
817
|
+
* eligible, and the pacing must not have to reach back into a binding to drain. */
|
|
818
|
+
capture: (() => Promise<StaticSurfaceCapture>) | null;
|
|
819
|
+
area: number;
|
|
820
|
+
counters: StaticImageSwapCounters;
|
|
821
|
+
/** `bakeStill`'s completion hook, or undefined for an ordinary surface-driven encode (whose
|
|
822
|
+
* completion IS its `<img>` going up). Called exactly once, wherever the job ends. */
|
|
823
|
+
settle?: (published: boolean) => void;
|
|
824
|
+
}
|
|
825
|
+
/** One policy + its bookkeeping. Shared by every surface the swapper attached. */
|
|
826
|
+
interface SwapperContext {
|
|
827
|
+
policy: ResolvedPolicy;
|
|
828
|
+
counters: StaticImageSwapCounters | null;
|
|
829
|
+
bindings: Set<StaticImageSwapBinding>;
|
|
830
|
+
queue: EncodeJob[];
|
|
831
|
+
/** Start of the current encode WINDOW, and how many encodes it has already kicked (see
|
|
832
|
+
* `pumpEncodes`). */
|
|
833
|
+
sliceAt: number;
|
|
834
|
+
sliceCount: number;
|
|
835
|
+
/** Readbacks the CURRENT task has already spent (`encode.perTask`). Reset only where a task
|
|
836
|
+
* boundary is actually observable — inside a timer callback — because the module is handed the
|
|
837
|
+
* thread many times per task: one `runSweep` calls `maybeSwap` for every eligible surface, and
|
|
838
|
+
* each of those reaches `pumpEncodes` on the SAME stack. A per-call counter would therefore bound
|
|
839
|
+
* nothing at all on the path that produced the measured 1,163 ms park. */
|
|
840
|
+
taskKicks: number;
|
|
841
|
+
/** `now()` the CURRENT unbroken run of deferrals started, or `NOT_DEFERRING`. The bound is measured
|
|
842
|
+
* from here and restarts on every forced pass, so it caps a RUN rather than the queue's total wait
|
|
843
|
+
* — and it is ONE run whichever reason is deferring (see `encodeDeferred`). */
|
|
844
|
+
busyDeferSince: number;
|
|
845
|
+
/** `now()` until which the adaptive slow-encode backoff holds encodes (0 = not backing off). */
|
|
846
|
+
backoffUntil: number;
|
|
847
|
+
/** The scratch canvas `encode.maxDim` downscales into, allocated on the first clamped encode and
|
|
848
|
+
* released at dispose. ONE per swapper: `toBlob` snapshots its source synchronously in Blink (the
|
|
849
|
+
* guarantee gsw's own perf harness already depends on — `scenarios/static-surfaces.ts`), so reuse
|
|
850
|
+
* cannot race a blob still being compressed. */
|
|
851
|
+
scratch: HTMLCanvasElement | null;
|
|
852
|
+
drainTimer: StaticSurfaceTimerHandle | null;
|
|
853
|
+
sweepTimer: StaticSurfaceTimerHandle | null;
|
|
854
|
+
/** Absolute `now()` the armed sweep fires at (so an arm never postpones an earlier one). */
|
|
855
|
+
sweepAt: number;
|
|
856
|
+
lastWatchdogAt: number;
|
|
857
|
+
disposed: boolean;
|
|
858
|
+
}
|
|
859
|
+
/** The host-facing handle: everything that needs to enumerate the swapper's surfaces. Per-surface
|
|
860
|
+
* signalling stays in the free functions below, which read the policy off the surface's state. */
|
|
861
|
+
//#endregion
|
|
862
|
+
//#region src/runtime-options.d.ts
|
|
863
|
+
/** Browser effect hosting controls, separate from HTML model projection. */
|
|
864
|
+
interface GodotHtmlRuntimeOptions extends Pick<GodotHtmlRenderOptions, "enableWebglShaders" | "enableParticles"> {
|
|
865
|
+
externalRuntimes?: boolean;
|
|
866
|
+
resolveShaderSource?: (path?: string, uid?: string) => Promise<string | undefined> | string | undefined;
|
|
867
|
+
particleMaxInstances?: number;
|
|
868
|
+
particleFps?: number;
|
|
869
|
+
particleRectCache?: boolean;
|
|
870
|
+
particleObserverSizing?: boolean;
|
|
871
|
+
particleDormant?: boolean;
|
|
872
|
+
particleTravelExtents?: boolean;
|
|
873
|
+
staticParticles?: boolean;
|
|
874
|
+
staticParticleOneShotExpiry?: boolean;
|
|
875
|
+
staticParticlePixelRatio?: number;
|
|
876
|
+
parkStaticParticleBlend?: boolean;
|
|
877
|
+
staticParticleImages?: StaticSurfaceOption;
|
|
878
|
+
staticParticleFreezeAtMount?: boolean;
|
|
879
|
+
shaderFps?: number;
|
|
880
|
+
effectsLoopPacing?: EffectsLoopPacing;
|
|
881
|
+
effectsRenderer?: "auto" | "webgl" | "webgpu";
|
|
882
|
+
effectsProfiling?: boolean;
|
|
883
|
+
renderScale?: number;
|
|
884
|
+
staticShaders?: boolean;
|
|
885
|
+
staticShaderTime?: number;
|
|
886
|
+
staticShaderPixelRatio?: number;
|
|
887
|
+
staticShaderImages?: StaticSurfaceOption;
|
|
888
|
+
maxTextureDimension?: number;
|
|
889
|
+
enableScreenTextureCapture?: boolean;
|
|
890
|
+
maxScreenCaptureDim?: number;
|
|
891
|
+
onUnsupported?: UnsupportedRenderReporter;
|
|
892
|
+
onBindingRendered?: (node: HTMLElement, canvas: HTMLCanvasElement, info: GodotEffectRenderInfo) => void;
|
|
893
|
+
}
|
|
894
|
+
/** Options accepted by a host that both emits a model and mounts its effects. */
|
|
895
|
+
type GodotHtmlMountOptions = GodotHtmlRenderOptions & GodotHtmlRuntimeOptions;
|
|
896
|
+
//#endregion
|
|
897
|
+
//#region src/webgl/shared-gl.d.ts
|
|
898
|
+
interface GpuInfo {
|
|
899
|
+
/** The unmasked GL renderer string, or "" when masked/unknown/unavailable. */
|
|
900
|
+
renderer: string;
|
|
901
|
+
/** True only when `renderer` POSITIVELY matches a software rasterizer — never on "" (unknown). */
|
|
902
|
+
software: boolean;
|
|
903
|
+
/** True when WebGL is unavailable at all (no context). */
|
|
904
|
+
unavailable: boolean;
|
|
905
|
+
}
|
|
906
|
+
declare function describeGpu(): GpuInfo;
|
|
907
|
+
declare function effectivePixelRatio(renderScale?: number): number;
|
|
908
|
+
declare const MAX_PINNED_BACKING_DIM = 2048;
|
|
909
|
+
/** Backing-store size (px) for a `cssW`×`cssH` surface at `ratio`, plus the ratio ACTUALLY applied —
|
|
910
|
+
* which differs from `ratio` only when `maxDim` bit, and is what a caller that draws geometry in
|
|
911
|
+
* CSS px × ratio (the particle runtime) must scale by so its sprites still land inside the canvas.
|
|
912
|
+
*
|
|
913
|
+
* With `maxDim` the result is scaled down ASPECT-PRESERVINGLY (the `downscaleForUpload` idiom) so
|
|
914
|
+
* its longest edge fits. Per-axis clamping is not an option: the shader runtime's `uvFit` derives
|
|
915
|
+
* contain/cover from the canvas aspect, so a squashed backing store would re-fit the texture
|
|
916
|
+
* wrongly. Without `maxDim` (or under it) this is exactly the `Math.max(1, Math.round(css * ratio))`
|
|
917
|
+
* both runtimes have always done, and `ratio` comes back untouched. */
|
|
918
|
+
declare function backingStoreSize(cssW: number, cssH: number, ratio: number, maxDim?: number): {
|
|
919
|
+
w: number;
|
|
920
|
+
h: number;
|
|
921
|
+
ratio: number;
|
|
922
|
+
};
|
|
923
|
+
/** The self-layer attribute that carries a surface's PER-BINDING backing-density multiplier — the
|
|
924
|
+
* factor by which the device pixels this surface really covers exceed its own CSS box.
|
|
925
|
+
*
|
|
926
|
+
* Both fx runtimes size a canvas from `clientWidth × (devicePixelRatio × renderScale)`, and
|
|
927
|
+
* `clientWidth` is blind to ancestor CSS transforms: a node under a `transform: scale(1.41)`
|
|
928
|
+
* ancestor lays out at its untransformed width, so its backing store is sized for 1/1.41 of the
|
|
929
|
+
* device pixels it is magnified onto and the surface is visibly soft. Nothing the runtime can read
|
|
930
|
+
* off its own element tells it that — the transform belongs to an ancestor the runtime does not
|
|
931
|
+
* own, and finding it would be a `getBoundingClientRect()` walk per surface per frame, i.e. exactly
|
|
932
|
+
* the forced layout both runtimes are built around avoiding.
|
|
933
|
+
*
|
|
934
|
+
* So the HOST states it. It already composed that transform to write it; this attribute is that
|
|
935
|
+
* number, handed down. It is a MULTIPLIER on the density term, not a replacement for it: the
|
|
936
|
+
* device ratio, `renderScale` and any frozen-mode pin all still apply, and this rides on top.
|
|
937
|
+
*
|
|
938
|
+
* IT IS AN AXIS SCALE, NOT A BOUNDING-BOX RATIO, and a host that confuses the two will over-allocate
|
|
939
|
+
* every rotated surface it owns. `getBoundingClientRect()` returns the axis-aligned bounding box of a
|
|
940
|
+
* transformed element, so a square rotated θ measures `|cos θ| + |sin θ|` wider than it is — √2 at
|
|
941
|
+
* 45° — while covering exactly as many device pixels as before. Rotation is rigid. The number this
|
|
942
|
+
* attribute wants is the transform's column norm (or the mean of the two under a non-uniform scale).
|
|
943
|
+
*
|
|
944
|
+
* Named `data-godot-shader-*` and read by the PARTICLE runtime as well, deliberately — one name
|
|
945
|
+
* for one question ("how magnified is this surface?") that both fx families ask identically, so a
|
|
946
|
+
* host stamps it the same way whichever runtime picks the node up. */
|
|
947
|
+
declare const SURFACE_PIXEL_RATIO_ATTR = "data-godot-shader-pixel-ratio";
|
|
948
|
+
/** Ceiling on the attribute above. The multiplier is the one density input that arrives as a STRING
|
|
949
|
+
* from outside the runtime — `renderScale` is clamped to ≤ 1 and the static pin is bounded by
|
|
950
|
+
* `MAX_PINNED_BACKING_DIM` — so a host mid-animation, or a host with a bug, could otherwise turn
|
|
951
|
+
* one attribute write into a quadratic backing-store allocation on a surface the runtime has no
|
|
952
|
+
* other reason to distrust. 4 is 16× the area, past any magnification a UI plausibly applies to a
|
|
953
|
+
* live effect, and a surface asking for more is far likelier to be wrong than under-resolved. */
|
|
954
|
+
declare const MAX_SURFACE_PIXEL_RATIO = 4;
|
|
955
|
+
/** Parse `SURFACE_PIXEL_RATIO_ATTR` into a density MULTIPLIER.
|
|
956
|
+
*
|
|
957
|
+
* Absent, empty, unparseable, non-finite or non-positive ⇒ exactly `1`, i.e. the density term is
|
|
958
|
+
* the product it has always been and the surface is sized byte-for-byte as it was before this
|
|
959
|
+
* attribute existed. That is the whole off-switch: a host that never writes the attribute cannot
|
|
960
|
+
* tell this feature is here.
|
|
961
|
+
*
|
|
962
|
+
* Values BELOW 1 are honoured (a surface minified by an ancestor really does cover fewer device
|
|
963
|
+
* pixels than its box). Above `MAX_SURFACE_PIXEL_RATIO` they are capped, not rejected — a too-large
|
|
964
|
+
* value is still evidence the surface is magnified, so clamping keeps most of the fix while
|
|
965
|
+
* refusing the allocation. */
|
|
966
|
+
declare function parseSurfacePixelRatio(attr: string | null | undefined): number;
|
|
967
|
+
//#endregion
|
|
968
|
+
//#region src/webgpu/device.d.ts
|
|
969
|
+
type WebgpuFallbackReason = "no-navigator-gpu" | "no-adapter" | "fallback-adapter" | "acquire-timeout" | "device-lost" | "context-refused" | "pipeline-error";
|
|
970
|
+
//#endregion
|
|
971
|
+
//#region src/particles/runtime.d.ts
|
|
972
|
+
/** OPT-IN per-frame cost attribution for ONE particle runtime (`effectsProfiling`, see `../types`),
|
|
973
|
+
* read from `stats().profile`. NULL when the option is off — see `ParticleRuntimeStats.profile`.
|
|
974
|
+
*
|
|
975
|
+
* WHY IT EXISTS. A live tick's wall clock alone says nothing about what to DO: "9 ms/frame" is the
|
|
976
|
+
* same number whether the CPU integrator is chewing through 2000 particles × 4 sub-steps, the
|
|
977
|
+
* instance buffer is being rebuilt per frame, or the GL→2D blit is fill-bound at
|
|
978
|
+
* devicePixelRatio 3. Those have opposite fixes (`particleFps`/`amount`/`staticParticles` vs
|
|
979
|
+
* `renderScale`), so the tick is split into the four buckets below and each carries its own WORK
|
|
980
|
+
* counter — a millisecond total is only interpretable next to the work that produced it.
|
|
981
|
+
*
|
|
982
|
+
* Counters are monotonic and never reset (the `ParticleRuntimeStats` contract): a benchmark
|
|
983
|
+
* snapshots the object, runs its window, and diffs. Times are `performance.now()` deltas in ms,
|
|
984
|
+
* summed — wall clock on the main thread, so an interrupted frame charges its interruption to
|
|
985
|
+
* whichever bucket was open. */
|
|
986
|
+
interface ParticleProfile {
|
|
987
|
+
/** LIVE ticks that simulated + drew at least one binding. The frozen (`staticParticles`) path
|
|
988
|
+
* books NONE: it draws once and parks the loop, so a frozen runtime reports `ticks: 0` and every
|
|
989
|
+
* other field 0 — which is the right report for a mode whose whole point is that it has no
|
|
990
|
+
* per-frame cost. A deferred tick (the FPS cap re-arming without work) books none either. */
|
|
991
|
+
ticks: number;
|
|
992
|
+
/** Bindings simulated + drawn, summed across those ticks. `bindings / ticks` is the live system
|
|
993
|
+
* count the frame really paid for — suspended, parked and unmounted bindings are skipped by the
|
|
994
|
+
* loop and never counted. */
|
|
995
|
+
bindings: number;
|
|
996
|
+
/** Fixed sub-steps `simulateParticles` executed. THE sim work unit, and deliberately not the tick
|
|
997
|
+
* count: the sim runs at `fixed_fps` (30 by default) regardless of display rate, so a 60Hz device
|
|
998
|
+
* runs ~0.5 steps per tick per binding and a stalled frame runs several. `simMs / simSteps` is
|
|
999
|
+
* therefore the only stable cost-per-unit, and a `simSteps` far from `bindings` is the proof that
|
|
1000
|
+
* the sim rate is decoupled from the display rate. */
|
|
1001
|
+
simSteps: number;
|
|
1002
|
+
/** Instances pushed into the GL instance buffer ≈ live particles actually drawn (a dead or
|
|
1003
|
+
* fully-transparent particle is skipped by the build loop). The denominator for both `buildMs`
|
|
1004
|
+
* and `glMs`, and the number to compare against `particleMaxInstances`. */
|
|
1005
|
+
instances: number;
|
|
1006
|
+
/** CPU simulation: `simulateParticles` (integrate + emit + curve sampling). The bucket the
|
|
1007
|
+
* mid-range-phone suspicion points at. */
|
|
1008
|
+
simMs: number;
|
|
1009
|
+
/** Instance-buffer BUILD: the per-particle push loop in `drawBinding` that turns simulation state
|
|
1010
|
+
* into the interleaved float array. Separate from `simMs` because it is a different fix — it
|
|
1011
|
+
* scales with LIVE particles, not with sub-steps. */
|
|
1012
|
+
buildMs: number;
|
|
1013
|
+
/** GL SUBMIT: `drawParticles` — uniform writes, the buffer upload and the instanced draw call.
|
|
1014
|
+
* SUBMIT ONLY: the GPU executes asynchronously, so this is main-thread issue cost, never GPU
|
|
1015
|
+
* time. A GPU-bound frame shows up as back-pressure in `blitMs` (the readback-shaped
|
|
1016
|
+
* `drawImage`), not here. */
|
|
1017
|
+
glMs: number;
|
|
1018
|
+
/** GL→2D BLIT: the binding canvas's `clearRect` plus the `drawImage` that copies the shared GL
|
|
1019
|
+
* canvas onto it. Pure fill cost, so it scales with BACKING-STORE AREA (`renderScale`,
|
|
1020
|
+
* `devicePixelRatio`, the sprite `pad`) and not with particle count — which is exactly the
|
|
1021
|
+
* distinction a "particles are slow" report cannot make without this split. */
|
|
1022
|
+
blitMs: number;
|
|
1023
|
+
}
|
|
1024
|
+
/** Live, monotonically-increasing counters for ONE particle runtime (see `ParticleRuntime.stats`).
|
|
1025
|
+
* Same contract as the shader runtime's `WebglShaderRuntimeStats`: plain `++` writes, never reset,
|
|
1026
|
+
* the SAME object returned on every `stats()` call (snapshot to diff). */
|
|
1027
|
+
interface ParticleRuntimeStats extends StaticImageSwapCounters {
|
|
1028
|
+
/** Actual instanced GL draws of a particle-system frame (`drawBinding` reaching `drawParticles`). */
|
|
1029
|
+
draws: number;
|
|
1030
|
+
/** Frozen-mode static-frame cache hits: the warm AND the instanced draw were both skipped and a
|
|
1031
|
+
* cached canvas blitted instead (see `./static-frame-cache`). The shader runtime's `cacheHits`
|
|
1032
|
+
* sibling. A fleet of N identical frozen systems should settle at 1 `draws` + (N-1) `cacheHits`. */
|
|
1033
|
+
cacheHits: number;
|
|
1034
|
+
/** `syncCanvasSize` calls that sized a binding at the PINNED static ratio
|
|
1035
|
+
* (`staticParticlePixelRatio`) instead of `devicePixelRatio × renderScale` — the shader runtime's
|
|
1036
|
+
* `pinnedCanvasSyncs` sibling, and the same purpose: a device probe reads it to confirm the pin
|
|
1037
|
+
* is really in force (0 = option unset, or frozen mode never entered). */
|
|
1038
|
+
pinnedCanvasSyncs: number;
|
|
1039
|
+
/** Self-layer box reads: `selfLayer.clientWidth`/`clientHeight`, i.e. the layout this runtime
|
|
1040
|
+
* forces. Booked by the create-time measure pass and by any `syncCanvasSize` that had neither a
|
|
1041
|
+
* ResizeObserver `contentRect` nor a cached box. With the cache on (`particleRectCache`, the
|
|
1042
|
+
* default) it settles at exactly one per binding CREATED and zero for everything after — fleet
|
|
1043
|
+
* re-sizes, texture-load re-pads and observer deliveries are all reflow-free — so a device probe
|
|
1044
|
+
* can confirm that live. It counts READS, not flushes: the create-time reads are batched into one
|
|
1045
|
+
* contiguous run, so N new bindings book N reads and cost ONE forced layout flush.
|
|
1046
|
+
*
|
|
1047
|
+
* With `particleObserverSizing` also on (the default) even that per-create read is gone and this
|
|
1048
|
+
* settles at **0**: a new binding's first box arrives from the shared ResizeObserver, off the main
|
|
1049
|
+
* path. Any read left standing is therefore a real signal — a wake whose observation never landed,
|
|
1050
|
+
* or the mount backstop firing on an engine that does not deliver 0x0 initial observations. */
|
|
1051
|
+
boxReads: number;
|
|
1052
|
+
/** COUNTER. Bindings PARKED (canvas hidden) because their subtree went suspended — a create that
|
|
1053
|
+
* was born parked included. Monotonic: a binding that parks, wakes and parks again counts twice.
|
|
1054
|
+
* 0 means the park never engaged (nothing suspended, or `particleDormant: false`). */
|
|
1055
|
+
dormantParks: number;
|
|
1056
|
+
/** COUNTER. Parked bindings woken again (the resume half, so a probe can tell "parked and stayed
|
|
1057
|
+
* parked" from "flapped"). */
|
|
1058
|
+
dormantWakes: number;
|
|
1059
|
+
/** COUNTER. Bindings the expiry sweep DISPOSED for real after ~`DORMANT_DISPOSE_SECONDS` parked
|
|
1060
|
+
* (canvas removed from the DOM, GL buffer released) rather than holding them forever. */
|
|
1061
|
+
dormantDisposes: number;
|
|
1062
|
+
/** GAUGE — bindings parked RIGHT NOW, sampled on each `stats()` read (the `staticImagesLive`
|
|
1063
|
+
* contract exactly, and re-derived from the binding set rather than trusted incrementally). This
|
|
1064
|
+
* is the "is the park actually engaged?" measurement a device probe reads — `28/33` — which the
|
|
1065
|
+
* monotonic counters above cannot answer. */
|
|
1066
|
+
dormantLive: number;
|
|
1067
|
+
/** Per-frame cost attribution (see `ParticleProfile`), or NULL when `effectsProfiling` is off —
|
|
1068
|
+
* which is the default, and the no-op handle always. NULL rather than a zeroed object ON PURPOSE:
|
|
1069
|
+
* a bench that read `simMs: 0` out of an un-instrumented runtime would report "the simulation is
|
|
1070
|
+
* free" when the truth is "nobody measured". The same object every `stats()` call, mutated in
|
|
1071
|
+
* place by the live tick. */
|
|
1072
|
+
profile: ParticleProfile | null;
|
|
1073
|
+
/** GAUGE — which renderer this runtime's bindings are drawing through RIGHT NOW, re-derived on
|
|
1074
|
+
* each `stats()` read (a runtime can change backend mid-life, in one direction: a WebGPU device
|
|
1075
|
+
* that is lost is rebuilt on WebGL).
|
|
1076
|
+
*
|
|
1077
|
+
* `"pending"` is a real state, not a transient to be waited out politely: with
|
|
1078
|
+
* `effectsRenderer: "auto"`/`"webgpu"` on a browser that HAS `navigator.gpu`, the device arrives
|
|
1079
|
+
* from a promise, and until it does the bindings exist, are sized and are mounted but have no
|
|
1080
|
+
* surface and draw nothing. `"none"` is the no-op handle (no WebGL2, or particles disabled). */
|
|
1081
|
+
renderer: "pending" | "webgpu" | "webgl" | "none";
|
|
1082
|
+
/** COUNTER. Times this runtime adopted WebGL after being asked for `"auto"`/`"webgpu"` — the
|
|
1083
|
+
* SYNCHRONOUS "this browser has no navigator.gpu" decline included, which is the common case and
|
|
1084
|
+
* the reason a plain WebGL page reports 1 here rather than 0. Stays 0 for `effectsRenderer:
|
|
1085
|
+
* "webgl"` (nothing was ever asked for) and for a runtime that adopted WebGPU and kept it. */
|
|
1086
|
+
webgpuFallbacks: number;
|
|
1087
|
+
/** The FIRST reason this runtime declined WebGPU (later ones cannot un-explain it), or null while
|
|
1088
|
+
* it never has. THE diagnostic for a silent fallback: `renderer: "webgl"` under
|
|
1089
|
+
* `effectsRenderer: "webgpu"` says something went wrong, and only this says what. */
|
|
1090
|
+
webgpuFallbackReason: WebgpuFallbackReason | null;
|
|
1091
|
+
/** COUNTER — `queue.submit` calls this runtime's WebGPU backend has made, sampled on read. ONE per
|
|
1092
|
+
* tick that drew anything, whatever the binding count: that batching is the measured win (S7), so
|
|
1093
|
+
* a probe that finds it climbing with N systems has found the win being given back. 0 on WebGL,
|
|
1094
|
+
* where the question is meaningless. */
|
|
1095
|
+
webgpuSubmits: number;
|
|
1096
|
+
/** GAUGE — `device.lost` resolutions seen by the page-wide device (see `../webgpu/device`). A lost
|
|
1097
|
+
* device stops producing frames, so a non-zero value here next to `renderer: "webgl"` is the
|
|
1098
|
+
* device-loss rebuild having happened. */
|
|
1099
|
+
webgpuDeviceLosses: number;
|
|
1100
|
+
/** GAUGE — `uncapturederror` events on the page-wide device. Non-zero means a frame was silently
|
|
1101
|
+
* WRONG: WebGPU reports most command-level mistakes this way and nothing else says so. */
|
|
1102
|
+
webgpuErrors: number;
|
|
1103
|
+
/** GAUGE — BAKE DONORS this runtime is holding right now (see `donateStill`): bindings whose node
|
|
1104
|
+
* is gone but whose surface is kept alive, out of the DOM, only long enough to encode the frame
|
|
1105
|
+
* their successors will claim. Sampled on each `stats()` read. A number pinned at the bound means
|
|
1106
|
+
* bakes are not draining — check `staticStillDonorBakes` against `staticStillDonorsDropped`. */
|
|
1107
|
+
staticStillDonors: number;
|
|
1108
|
+
/** COUNTER. Donor bakes that PUBLISHED, i.e. banked a frame nothing had encoded yet. Each one is a
|
|
1109
|
+
* key the next binding to reach it can claim for free. */
|
|
1110
|
+
staticStillDonorBakes: number;
|
|
1111
|
+
/** COUNTER. Donors released WITHOUT publishing — evicted by the donor bound, or dropped at runtime
|
|
1112
|
+
* teardown. A donor is speculative work by construction, so these are not failures; a run where
|
|
1113
|
+
* they dominate `staticStillDonorBakes` means the bound is too small for the scene's churn, or
|
|
1114
|
+
* that retention (`encode.stillCacheBytes`) is off and every bake is refused. */
|
|
1115
|
+
staticStillDonorsDropped: number;
|
|
1116
|
+
}
|
|
1117
|
+
/** A persistent particle runtime for a mounted scene root (see `createParticleRuntime`). */
|
|
1118
|
+
interface ParticleRuntime {
|
|
1119
|
+
/** Diff the current `[data-godot-particle-runtime]` nodes against the live bindings:
|
|
1120
|
+
* unchanged specs keep their running simulation, changed/new specs re-init that node only,
|
|
1121
|
+
* gone nodes are disposed. */
|
|
1122
|
+
reconcile(): void;
|
|
1123
|
+
/** Live retune the backing-store resolution (devicePixelRatio × clamped `scale`) without a
|
|
1124
|
+
* dispose+recreate — running simulations keep going, only the canvas density changes. While a pin
|
|
1125
|
+
* is in force (see `setStaticParticlePixelRatio`) AND the runtime is frozen this re-sizes nothing:
|
|
1126
|
+
* the frozen canvases are deliberately held still. The new scale applies to live bindings as soon
|
|
1127
|
+
* as frozen mode is left. */
|
|
1128
|
+
setRenderScale(scale: number): void;
|
|
1129
|
+
/** Live set/clear the pinned FROZEN backing-store ratio (see `staticParticlePixelRatio`). Pass
|
|
1130
|
+
* `undefined` (or a non-positive value) to un-pin. Only frozen bindings are affected. */
|
|
1131
|
+
setStaticParticlePixelRatio(ratio: number | undefined): void;
|
|
1132
|
+
/** Live retune the particle FPS cap (0 = uncapped). */
|
|
1133
|
+
setFps(fps: number): void;
|
|
1134
|
+
/** Live toggle frozen (single-shot) mode, mirroring `WebglShaderRuntime.setStaticShaders`: warm each system
|
|
1135
|
+
* to a representative mid-flight state, draw ONCE, then park the loop (true); or resume live simulation (false).
|
|
1136
|
+
* The low-cost fallback that shows a frozen spray of particles instead of a per-frame CPU sim + GL draw. */
|
|
1137
|
+
setStaticParticles(value: boolean): void;
|
|
1138
|
+
/** Live toggle of the frozen-surface image swap (see the `staticParticleImages` option and
|
|
1139
|
+
* `../surface-image-swap`), mirroring `WebglShaderRuntime.setStaticShaderImages`. Turning it OFF
|
|
1140
|
+
* reverts every live swap immediately and revokes its object URLs — the kill switch, safe to
|
|
1141
|
+
* throw at any time. Turning it ON arms the runtime's CONFIGURED policy (a boolean here never
|
|
1142
|
+
* replaces it) and every binding re-earns its window; a runtime left at the DEFAULT (`false`,
|
|
1143
|
+
* since this option is opt-in) has no configured policy to arm, so ON gives it the same
|
|
1144
|
+
* quiet-window policy `staticParticleImages: true` means — otherwise this switch could never
|
|
1145
|
+
* turn the mechanism on for the consumers it exists for. */
|
|
1146
|
+
setStaticParticleImages(value: boolean): void;
|
|
1147
|
+
/** HOST-DRIVEN revert of the surface image swap, WITHOUT blocking, mirroring the shader runtime's:
|
|
1148
|
+
* with no argument every swapped surface is handed back to its canvas; with a node list, only the
|
|
1149
|
+
* bindings at (or under) those elements. Each affected surface restarts its gate and re-earns the
|
|
1150
|
+
* swap on its own. This is how a host that knows something the runtime cannot see — it is about
|
|
1151
|
+
* to re-parent a subtree, it just re-themed, its own occlusion pass changed its mind —
|
|
1152
|
+
* un-shadows a stale stand-in immediately instead of waiting for a watchdog window. */
|
|
1153
|
+
invalidateStaticSurfaces(nodes?: Iterable<HTMLElement>): void;
|
|
1154
|
+
/** The runtime's live counters (see `ParticleRuntimeStats`). Returns the SAME live object every
|
|
1155
|
+
* call — read-only by convention; snapshot (spread) it to diff. All-zero on the no-op handle.
|
|
1156
|
+
* `.profile` carries the opt-in per-frame cost attribution (`effectsProfiling`) and is NULL
|
|
1157
|
+
* whenever it was not measured, the no-op handle included. */
|
|
1158
|
+
stats(): ParticleRuntimeStats;
|
|
1159
|
+
/**
|
|
1160
|
+
* TEST / DIAGNOSTIC HOOK: this node's binding re-rendered into an offscreen texture and read back
|
|
1161
|
+
* as tightly-packed RGBA (PREMULTIPLIED, top-down, at the canvas's BACKING-STORE size), or null
|
|
1162
|
+
* when there is nothing to read — no binding at (or under) `node`, no surface yet, a zero-sized
|
|
1163
|
+
* canvas, or a runtime rendering on WebGL.
|
|
1164
|
+
*
|
|
1165
|
+
* WEBGL RETURNS NULL ON PURPOSE, and it is not a gap: that canvas holds readable 2D pixels, so a
|
|
1166
|
+
* caller who wants them uses `getImageData` on it. This exists because a WebGPU canvas has no such
|
|
1167
|
+
* path — `drawImage`/`toDataURL` from one are blank under headless Chrome and pathological on
|
|
1168
|
+
* Android (docs/perf-harness.md S7) — so the frame has to be produced a SECOND time, into a
|
|
1169
|
+
* texture that `copyTextureToBuffer` can reach (`../webgpu/readback`). It renders the binding's
|
|
1170
|
+
* CURRENT state, which for a frozen/static binding is exactly the frame on screen.
|
|
1171
|
+
*
|
|
1172
|
+
* This is the WebGL↔WebGPU image-parity harness's capture path, and the seam the readback-based
|
|
1173
|
+
* surface image swap encodes from — `attachSurfaceSwap` gives a WebGPU binding a `captureCanvas`
|
|
1174
|
+
* hook built out of exactly this production (`../surface-image-swap`).
|
|
1175
|
+
*/
|
|
1176
|
+
captureNodePixels?(node: HTMLElement): Promise<Uint8Array | null>;
|
|
1177
|
+
/** Tear down every binding and stop the loop. */
|
|
1178
|
+
dispose(): void;
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Create a persistent particle runtime over a mounted scene root. The caller keeps this handle
|
|
1182
|
+
* and calls `reconcile()` on each re-render. Bindings
|
|
1183
|
+
* are keyed by their outer node element; a binding whose `data-godot-particle-specs` is
|
|
1184
|
+
* unchanged keeps its running simulation (so ambient emitters and in-flight one-shot bursts are
|
|
1185
|
+
* NOT reset by an unrelated re-render), while a changed spec re-inits only that node — the seam
|
|
1186
|
+
* a host uses to re-trigger a one-shot burst (bump a value in the spec). A no-op handle when
|
|
1187
|
+
* WebGL2/particles are unavailable.
|
|
1188
|
+
*/
|
|
1189
|
+
declare function createParticleRuntime(root: HTMLElement, options: GodotHtmlRuntimeOptions): ParticleRuntime;
|
|
1190
|
+
//#endregion
|
|
1191
|
+
//#region src/particles/spec.d.ts
|
|
1192
|
+
/** HTML attribute transport: portable parameters plus DOM placement and resource URLs. */
|
|
1193
|
+
interface ParticleSpecConfig extends ParticleRenderConfig {
|
|
1194
|
+
originX: number;
|
|
1195
|
+
originY: number;
|
|
1196
|
+
boxOffsetX: number;
|
|
1197
|
+
boxOffsetY: number;
|
|
1198
|
+
textureUrl: string | null;
|
|
1199
|
+
maskUrl?: string | null;
|
|
1200
|
+
}
|
|
1201
|
+
declare function normalizeParticleSpecConfig(raw: Partial<ParticleSpecConfig> | null | undefined): ParticleSpecConfig;
|
|
1202
|
+
/** Decode the DOM attribute without putting its transport contract in the simulator. */
|
|
1203
|
+
declare function parseParticleSpecConfig(json: string | null | undefined): ParticleSpecConfig | null;
|
|
1204
|
+
//#endregion
|
|
1205
|
+
//#region src/particles/render-backend.d.ts
|
|
1206
|
+
/** The structural subset of a texture entry the runtime and the draw carry around: enough to size a
|
|
1207
|
+
* sprite (`width`/`height`), to know whether the real pixels have arrived (`loaded`, the frozen
|
|
1208
|
+
* frame's cache gate) and to be told when they do (`listeners`, see `onTextureLoaded`). Deliberately
|
|
1209
|
+
* NOT `TextureEntry`: the GPU resource itself is the backend's business, and a non-WebGL entry
|
|
1210
|
+
* carries no `WebGLTexture`. */
|
|
1211
|
+
interface ParticleTextureHandle {
|
|
1212
|
+
width: number;
|
|
1213
|
+
height: number;
|
|
1214
|
+
loaded: boolean;
|
|
1215
|
+
listeners: Set<() => void>;
|
|
1216
|
+
}
|
|
1217
|
+
//#endregion
|
|
1218
|
+
//#region src/particles/extents.d.ts
|
|
1219
|
+
declare function spriteExtentPad(cfg: ParticleSpecConfig, texture: ParticleTextureHandle | null): number;
|
|
1220
|
+
declare function emissionExtentPad(cfg: ParticleSpecConfig): number;
|
|
1221
|
+
//#endregion
|
|
1222
|
+
export { UnsupportedRenderReporter as $, DEFAULT_QUIET_WINDOW_MS as A, rewritePxLengths as At, StaticSurfaceEncodePacing as B, DEFAULT_ENCODE_MAX_DIM as C, GodotContentScaleSize as Ct, DEFAULT_ENCODE_SLOW_MS as D, contentScaleStageStyle as Dt, DEFAULT_ENCODE_SLOW_BACKOFF_MS as E, contentScaleScript as Et, STATIC_SURFACE_IMAGE_ATTR as F, StaticSurfaceTimerHandle as G, StaticSurfaceOption as H, StaticImageRevertCause as I, EffectsLoopPacing as J, createStaticImageSwapCounters as K, StaticImageSwapCounters as L, DEFAULT_SURFACE_WATCHDOG_MS as M, STABLE_OBSERVATIONS_BEFORE_SWAP as N, DEFAULT_ENCODE_TASK_GAP_MS as O, observeContentScale as Ot, STATIC_CAPTURE_BLANK as P, UnsupportedRenderKind as Q, StaticSurfaceCapture as R, DEFAULT_ENCODE_INTERVAL_MS as S, GodotContentScaleAspect as St, DEFAULT_ENCODE_SLICE as T, ResolvedContentScale as Tt, StaticSurfacePolicy as U, StaticSurfaceGate as V, StaticSurfaceQuietWindowGate as W, createEffectsLoopPacer as X, PARK_SLOP_S as Y, UnsupportedRenderInfo as Z, effectivePixelRatio as _, TEXT_SCALE_VAR as _t, parseParticleSpecConfig as a, GodotHtmlModel as at, GodotHtmlRuntimeOptions as b, textScaleLength as bt, ParticleRuntimeStats as c, GodotHtmlRenderOptions as ct, GpuInfo as d, GodotShaderLoadingFallback as dt, reportUnsupportedRender as et, MAX_PINNED_BACKING_DIM as f, GodotTextAutoFitDirective as ft, describeGpu as g, ResolvedTextScale as gt, backingStoreSize as h, GodotTextScaleOption as ht, normalizeParticleSpecConfig as i, GodotHtmlFontFace as it, DEFAULT_STILL_CACHE_BYTES as j, scaleStyleRecord as jt, DEFAULT_PARKED_STILL_BYTES as k, resolveContentScale as kt, createParticleRuntime as l, GodotHtmlTintFilter as lt, SURFACE_PIXEL_RATIO_ATTR as m, GodotTextScale as mt, spriteExtentPad as n, GodotEffectRenderInfo as nt, ParticleProfile as o, GodotHtmlNode as ot, MAX_SURFACE_PIXEL_RATIO as p, GodotTextAutoFitNominalMetrics as pt, EffectsLoopPacer as q, ParticleSpecConfig as r, GodotHtmlContainerLayout as rt, ParticleRuntime as s, GodotHtmlPositioning as st, emissionExtentPad as t, GodotBbcodeTagDescriptor as tt, WebgpuFallbackReason as u, GodotResolvedResource as ut, parseSurfacePixelRatio as v, resolveTextScale as vt, DEFAULT_ENCODE_PER_TASK as w, GodotContentScaleTechnique as wt, DEFAULT_ENCODE_BUSY_MAX_DEFER_MS as x, GodotContentScale as xt, GodotHtmlMountOptions as y, setGodotTextScale as yt, StaticSurfaceContentKeyGate as z };
|
|
1223
|
+
//# sourceMappingURL=extents-R8RIim2S.d.ts.map
|