insomni 0.2.0-alpha.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.
@@ -0,0 +1,4566 @@
1
+ import { N as FrameRect, R as Vec2, S as Mat3, i as ResolvedCameraState, n as CameraState, r as FitCameraToBoundsOptions, t as Bounds2D } from "./camera-view-DHmMiKvP.mjs";
2
+ import { g as GPUOwner, m as TextureRegion, p as Texture, t as GlyphAtlas } from "./text-font-D7GGDtTK.mjs";
3
+ import { StorageFlag, TgpuBuffer, TgpuRoot, d } from "typegpu";
4
+
5
+ //#region src/math/color.d.ts
6
+ interface Color {
7
+ r: number;
8
+ g: number;
9
+ b: number;
10
+ a: number;
11
+ }
12
+ declare function rgba(r: number, g: number, b: number, a?: number): Color;
13
+ /** Parse a 24-bit hex color, e.g. hex(0xff6600). Alpha is always 1. */
14
+ declare function hex(value: number): Color;
15
+ /** Parse a CSS hex string, e.g. cssHex("#ff6600") or cssHex("#ff660080"). */
16
+ declare function cssHex(value: string): Color;
17
+ /**
18
+ * HSL → RGB. All inputs in `[0, 1]`. Hue wraps automatically.
19
+ * `hsv` is the cousin of this for value-based color picking.
20
+ */
21
+ declare function hsl(h: number, s: number, l: number, a?: number): Color;
22
+ /** HSV → RGB. All inputs in `[0, 1]`. Hue wraps automatically. */
23
+ declare function hsv(h: number, s: number, v: number, a?: number): Color;
24
+ /** Alias of `hsl` for parity with code that explicitly calls out the conversion. */
25
+ declare const hslToRgb: typeof hsl;
26
+ /** Alias of `hsl` — accepts the same `(h, s, l, a)` signature. */
27
+ declare const hslToRgba: typeof hsl;
28
+ /** Linearly interpolate two colors channel-wise. */
29
+ declare function lerpColor(a: Color, b: Color, t: number): Color;
30
+ /** Alias of `lerpColor` matching the `mix()` naming used in shaders. */
31
+ declare const mixColor: typeof lerpColor;
32
+ /** Return a copy of `color` with the given alpha. */
33
+ declare function withAlpha(color: Color, alpha: number): Color;
34
+ /** Multiply alpha — useful for fading layered colors without losing the base value. */
35
+ declare function fade(color: Color, factor: number): Color;
36
+ /** Mix `color` with white by `t` ∈ [0, 1]. */
37
+ declare function lighten(color: Color, t: number): Color;
38
+ /** Mix `color` with black by `t` ∈ [0, 1]. */
39
+ declare function darken(color: Color, t: number): Color;
40
+ declare const TRANSPARENT: Color;
41
+ declare const BLACK: Color;
42
+ declare const WHITE: Color;
43
+ //#endregion
44
+ //#region src/kinds/polyline.d.ts
45
+ type LineCap = "butt" | "round" | "square";
46
+ type LineJoin = "miter" | "bevel" | "round";
47
+ interface PolylineShape {
48
+ points: readonly Vec2[];
49
+ color: Color;
50
+ width?: number;
51
+ cap?: LineCap;
52
+ join?: LineJoin;
53
+ closed?: boolean;
54
+ miterLimit?: number;
55
+ /** Dash pattern as alternating [dash, gap, dash, gap, ...] lengths. Empty = solid. */
56
+ dashPattern?: readonly number[];
57
+ /** Offset into the dash pattern. Default: 0 */
58
+ dashOffset?: number;
59
+ }
60
+ interface TessellatedTriangle {
61
+ x1: number;
62
+ y1: number;
63
+ x2: number;
64
+ y2: number;
65
+ x3: number;
66
+ y3: number;
67
+ }
68
+ /**
69
+ * Note on SDF-shape dash parity: the rendering engine's SDF rect/circle/ellipse
70
+ * pipeline does not natively support dashed strokes (polylines do). When
71
+ * higher-level code needs a dashed/dotted border on an SDF shape, generate the
72
+ * border via {@link polylineEllipseRing} / {@link polylineRectRing} and stroke
73
+ * it through {@link tessellatePolyline} with a `dashPattern`. The visual
74
+ * difference at marker-typical sizes is negligible and the trade is a small
75
+ * tessellation cost in exchange for staying off the SDF shader's hot path.
76
+ */
77
+ interface EllipseRingOptions {
78
+ cx: number;
79
+ cy: number;
80
+ rx: number;
81
+ ry: number;
82
+ /** Rotation around (cx, cy) in radians. Default 0. */
83
+ rotation?: number;
84
+ /** Number of vertices around the ring. Default scales with radius (16..128). */
85
+ segments?: number;
86
+ }
87
+ interface RectRingOptions {
88
+ x: number;
89
+ y: number;
90
+ width: number;
91
+ height: number;
92
+ /** Corner radius. Default 0 (sharp corners → 4 vertices total). */
93
+ cornerRadius?: number;
94
+ /** Vertices per corner arc when `cornerRadius > 0`. Default 4. */
95
+ cornerSegments?: number;
96
+ }
97
+ /**
98
+ * Generate a closed-loop polyline approximating an ellipse. Vertices are
99
+ * spaced evenly in angle (not arc length) — for small markers this matches
100
+ * SDF rendering to within a sub-pixel. Returns points in CCW math order (which
101
+ * is CW in screen coords where y grows downward). The returned array does NOT
102
+ * include a duplicate closing point — pass `closed: true` to `tessellatePolyline`.
103
+ */
104
+ declare function polylineEllipseRing(opts: EllipseRingOptions): Vec2[];
105
+ /**
106
+ * Generate a closed-loop polyline for a rectangle outline. Sharp by default;
107
+ * supply `cornerRadius` to get rounded corners via small arcs at each corner.
108
+ * Like {@link polylineEllipseRing}, the returned array is unclosed — feed it
109
+ * to `tessellatePolyline` with `closed: true`.
110
+ */
111
+ declare function polylineRectRing(opts: RectRingOptions): Vec2[];
112
+ /**
113
+ * Tessellate a polyline into triangles for GPU rendering.
114
+ * Returns an array of triangles representing the stroke geometry.
115
+ */
116
+ declare function tessellatePolyline(shape: PolylineShape): TessellatedTriangle[];
117
+ //#endregion
118
+ //#region src/shared/pack.d.ts
119
+ /**
120
+ * Bytes per shape instance in the packed vertex buffer. Layout, little-endian:
121
+ * 0: float32 pos.x
122
+ * 4: float32 pos.y
123
+ * 8: float16 size.x
124
+ * 10: float16 size.y
125
+ * 12: u32 fill (unorm8x4; R in low byte)
126
+ * 16: u32 stroke (unorm8x4)
127
+ * 20: float16 rotation
128
+ * 22: float16 strokeWidth
129
+ * 24: float16 cornerRadius
130
+ * 26: u16 _pad (zero)
131
+ * 28: u32 shapeType
132
+ */
133
+ declare const SHAPE_BYTES = 32;
134
+ declare const TRIANGLE_FLOATS = 12;
135
+ declare const SPRITE_FLOATS$1 = 16;
136
+ /**
137
+ * Floats per segment instance in the packed segment buffer (24 B total).
138
+ * Layout:
139
+ * float 0: start.x (f32)
140
+ * float 1: start.y (f32)
141
+ * float 2: end.x (f32)
142
+ * float 3: end.y (f32)
143
+ * float 4: colorBits (u32 reinterpret; straight RGBA unorm8x4)
144
+ * float 5: width (f32; layer-local units)
145
+ *
146
+ * Color is stored unpremultiplied (matching the shape pipeline); the shader
147
+ * multiplies by alpha at output to produce premultiplied blended results.
148
+ */
149
+ declare const SEGMENT_FLOATS = 6;
150
+ /**
151
+ * Floats per curve instance in the packed curve buffer (48 B total — a
152
+ * multiple of 16 B for storage-buffer struct-array alignment). Layout:
153
+ * float 0..1: p0.xy (f32)
154
+ * float 2..3: p1.xy (f32)
155
+ * float 4..5: p2.xy (f32)
156
+ * float 6..7: p3.xy (f32)
157
+ * float 8: colorBits (u32 reinterpret; unorm8x4 RGBA, unpremultiplied)
158
+ * float 9: widthPacked (u32 reinterpret; pack2x16float(widthStart, widthEnd))
159
+ * float 10: flagsBits (u32 reinterpret; cap style in low 2 bits, rest reserved)
160
+ * float 11: _pad (zero)
161
+ *
162
+ * widthPacked carries two f16 widths from day one so curve taper can land
163
+ * later without a schema migration. v1 callers pass the same width for both.
164
+ * flagsBits reserves the remaining 30 bits for dash / phase / outline.
165
+ */
166
+ declare const CURVE_FLOATS = 12;
167
+ declare const CURVE_CAP_ROUND = 0;
168
+ declare const CURVE_CAP_BUTT = 1;
169
+ declare const CURVE_CAP_SQUARE = 2;
170
+ /**
171
+ * Floats per arc instance in the packed arc buffer (32 B total — multiple
172
+ * of 16 B for storage-buffer struct-array alignment). Layout:
173
+ * float 0..1: center.xy (f32)
174
+ * float 2: radius (f32)
175
+ * float 3: theta0 (f32; radians)
176
+ * float 4: theta1 (f32; radians)
177
+ * float 5: width (f32; layer-local units)
178
+ * float 6: colorBits (u32 reinterpret; unorm8x4 RGBA, unpremultiplied)
179
+ * float 7: _pad (zero)
180
+ *
181
+ * Arc is the short path from `theta0` to `theta1`; rendering depends on the
182
+ * absolute span only. Use the {@link ArcShape} interface — the layer/pack
183
+ * normalises into this form. Butt caps; no joins.
184
+ */
185
+ declare const ARC_FLOATS = 8;
186
+ declare const SHAPE_RECT = 0;
187
+ declare const SHAPE_CIRCLE = 1;
188
+ declare const SHAPE_ELLIPSE = 2;
189
+ interface RectShape {
190
+ x: number;
191
+ y: number;
192
+ width: number;
193
+ height: number;
194
+ fill?: Color;
195
+ stroke?: Color;
196
+ strokeWidth?: number;
197
+ cornerRadius?: number;
198
+ rotation?: number;
199
+ }
200
+ interface CircleShape {
201
+ cx: number;
202
+ cy: number;
203
+ radius: number;
204
+ fill?: Color;
205
+ stroke?: Color;
206
+ strokeWidth?: number;
207
+ }
208
+ interface EllipseShape {
209
+ cx: number;
210
+ cy: number;
211
+ rx: number;
212
+ ry: number;
213
+ fill?: Color;
214
+ stroke?: Color;
215
+ strokeWidth?: number;
216
+ rotation?: number;
217
+ }
218
+ interface LineShape {
219
+ x1: number;
220
+ y1: number;
221
+ x2: number;
222
+ y2: number;
223
+ color: Color;
224
+ width?: number;
225
+ }
226
+ /**
227
+ * A 2D line segment drawn by the dedicated segment pipeline. Uses a compact
228
+ * 24 B record (f32 endpoints + unorm8x4 color + f32 width) and a 1D SDF
229
+ * fragment — roughly 25% of the storage of a `LineShape` and a simpler
230
+ * fragment. Prefer over `LineShape` for high-count use cases (phylo trees,
231
+ * edge bundles). Butt caps only — joins are the caller's responsibility.
232
+ */
233
+ interface SegmentShape {
234
+ x1: number;
235
+ y1: number;
236
+ x2: number;
237
+ y2: number;
238
+ color: Color;
239
+ /** Line thickness in layer-local units. Default 1. */
240
+ width?: number;
241
+ }
242
+ /**
243
+ * A 2D cubic Bezier curve drawn by the dedicated curve pipeline. Uses a
244
+ * 48 B record (4 f32 control points + unorm8x4 color + f16×2 widths +
245
+ * u32 flags) and an SDF fragment that iteratively solves distance-to-curve.
246
+ *
247
+ * Round caps are free (fall out of the clamped-t SDF); `butt` and `square`
248
+ * discard fragments past the tangent at the endpoint.
249
+ *
250
+ * For taper — pass different start/end widths via `widthStart` + `widthEnd`.
251
+ * Omit either to fall back to the uniform `width`.
252
+ */
253
+ interface CurveShape {
254
+ p0: Vec2;
255
+ p1: Vec2;
256
+ p2: Vec2;
257
+ p3: Vec2;
258
+ color: Color;
259
+ /** Uniform line thickness in layer-local units. Default 1. Overridden by widthStart/widthEnd if provided. */
260
+ width?: number;
261
+ /** Thickness at `p0`. Falls back to `width`. */
262
+ widthStart?: number;
263
+ /** Thickness at `p3`. Falls back to `width`. */
264
+ widthEnd?: number;
265
+ /** End cap style. Default `round`. */
266
+ cap?: LineCap;
267
+ }
268
+ /**
269
+ * A 2D circular arc drawn by the dedicated arc pipeline. SDF-based — stays
270
+ * pixel-crisp at any zoom, unlike a tessellated polyline. The rendered shape
271
+ * is the band of width `width` straddling the arc curve, with butt caps at
272
+ * each endpoint.
273
+ *
274
+ * `theta0` / `theta1` are absolute angles in radians; the renderer uses the
275
+ * absolute span (clamped to 2π — pass `theta1 = theta0 + 2π` for a full ring).
276
+ * Direction is irrelevant to the rendered geometry.
277
+ */
278
+ interface ArcShape {
279
+ cx: number;
280
+ cy: number;
281
+ radius: number;
282
+ theta0: number;
283
+ theta1: number;
284
+ color: Color;
285
+ /** Stroke thickness in layer-local units. Default 1. */
286
+ width?: number;
287
+ }
288
+ interface PolygonShape$1 {
289
+ points: readonly Vec2[];
290
+ holes?: readonly (readonly Vec2[])[];
291
+ fill?: Color;
292
+ stroke?: Color;
293
+ strokeWidth?: number;
294
+ fillEnabled?: boolean;
295
+ }
296
+ type PolygonOpts = Omit<PolygonShape$1, "points">;
297
+ //#endregion
298
+ //#region src/schema/instance.d.ts
299
+ /** The TypeGPU primitive a field maps to. */
300
+ type FieldKind = "f32" | "vec4f" | "u32";
301
+ /** One field in the universal per-instance record. */
302
+ interface InstanceField {
303
+ /** Field name — shared verbatim by the CPU offset table, the TypeGPU struct
304
+ * key, and the WGSL accessor local. */
305
+ name: string;
306
+ /** The TypeGPU primitive this field maps to. */
307
+ kind: FieldKind;
308
+ /** Float count this field occupies: `f32` = 1, `u32` = 1, `vec4f` = 4. */
309
+ floats: number;
310
+ }
311
+ /**
312
+ * Universal per-instance record: 16 floats / 64 bytes. Field order here IS the
313
+ * GPU memory order; do not reorder without re-deriving every consumer.
314
+ *
315
+ * Depth is carried by the explicit `order` field (float-offset 10), never
316
+ * derived from the instance index. Index-derived Z couples depth precision to
317
+ * draw count and requires a per-draw uniform; the explicit field removes both.
318
+ *
319
+ * Layout (float offset / byte offset):
320
+ * geom0 @ 0 / 0 vec4f — geometry slot 0 (kind-specific)
321
+ * geom1 @ 4 / 16 vec4f — geometry slot 1 (kind-specific)
322
+ * colorBits @ 8 / 32 u32 — packed unorm8x4 color
323
+ * typeFlag @ 9 / 36 u32 — shape-kind discriminant
324
+ * order @ 10 / 40 f32 — EXPLICIT depth; never index-derived
325
+ * lane0 @ 11 / 44 u32 — kind-specific payload
326
+ * lane1 @ 12 / 48 u32
327
+ * lane2 @ 13 / 52 u32
328
+ * lane3 @ 14 / 56 u32
329
+ * lane4 @ 15 / 60 u32 — see emphasis note below
330
+ *
331
+ * `lane4` is dual-purpose, kind-by-kind. No *shape* kind (rect/circle/ellipse/
332
+ * segment/curve/arc/triangle/polyline) reads it, so for shapes it carries the
333
+ * **emphasis key** (Phase 4): the uber `transparent` / OIT-build fragments
334
+ * compare it against the `Emphasis.focusedKey` uniform to dim non-focused
335
+ * PARTICIPATING instances (`shader/assemble.ts`). EXEMPT-ZERO: `lane4 == 0u` —
336
+ * the deterministic default `UberPack.append` writes — is the opt-out sentinel;
337
+ * a key-0 shape is NEVER dimmed (instances opt INTO emphasis with a key ≥ 1), so
338
+ * untagged shapes stay full-alpha during a mark emphasis. A shape's `lane4` is
339
+ * never stale. The glyph pack (`glyph/glyph-pack.ts`) independently uses `lane4`
340
+ * for the anchored-text world anchor Y — that is fine because glyphs render
341
+ * through the dedicated glyph pipeline, which never reads the emphasis uniform.
342
+ */
343
+ declare const INSTANCE_FIELDS: readonly InstanceField[];
344
+ /** Total floats per instance — derived from {@link INSTANCE_FIELDS}. */
345
+ declare const INSTANCE_FLOATS: number;
346
+ /** Total bytes per instance — derived from {@link INSTANCE_FLOATS}. */
347
+ declare const INSTANCE_BYTES: number;
348
+ //#endregion
349
+ //#region src/schema/codegen.d.ts
350
+ /** A single field's position within the packed instance record. */
351
+ interface FieldOffset {
352
+ /** Offset from the start of the instance, in floats. */
353
+ floatOffset: number;
354
+ /** Offset from the start of the instance, in bytes (`floatOffset * 4`). */
355
+ byteOffset: number;
356
+ }
357
+ /**
358
+ * The derived instance layout: CPU offsets, the TypeGPU struct, and the WGSL
359
+ * accessor — all produced from {@link INSTANCE_FIELDS} in one pass.
360
+ */
361
+ interface InstanceLayout {
362
+ /** Bytes per instance (`INSTANCE_BYTES`). */
363
+ byteStride: number;
364
+ /** Floats per instance (`INSTANCE_FLOATS`). */
365
+ floatStride: number;
366
+ /** Per-field float/byte offsets, keyed by field name. */
367
+ offsets: Record<string, FieldOffset>;
368
+ /** TypeGPU struct mirroring `INSTANCE_FIELDS` field-for-field, in order. */
369
+ tgpuStruct: ReturnType<typeof d.struct>;
370
+ /** WGSL snippet reading every field of an `InstanceStruct` value by name. */
371
+ wgslAccessor: string;
372
+ }
373
+ /**
374
+ * Derive the full instance layout from {@link INSTANCE_FIELDS}. Pure and
375
+ * deterministic — calling it twice yields structurally identical layouts.
376
+ */
377
+ declare function deriveLayout(): InstanceLayout;
378
+ /** Eagerly-derived canonical instance layout — the shared v3 contract. */
379
+ declare const INSTANCE_LAYOUT: InstanceLayout;
380
+ //#endregion
381
+ //#region src/kinds/kind.d.ts
382
+ /** Shape-kind discriminant written into `InstanceRecord.typeFlag`. */
383
+ declare const TYPE_RECT = 0;
384
+ declare const TYPE_CIRCLE = 1;
385
+ declare const TYPE_ELLIPSE = 2;
386
+ declare const TYPE_SEGMENT = 3;
387
+ declare const TYPE_CURVE = 4;
388
+ /**
389
+ * Reserved for a future Phase-4 in-uber sprite kind (texture binding in the
390
+ * uber-shader + `Layer.pushSprite`). In Phase 3, sprites render via the
391
+ * {@link BufferLayer} + sibling pipeline path — see
392
+ * `pipelines/sprite-pipelines.ts` (`createSpritePipelines`) and
393
+ * `layers/buffer-layer.ts` (`BufferLayerSpritesOptions`). `TYPE_SPRITE` is
394
+ * intentionally absent from `ORDERED_KINDS` in `kinds/index.ts`: the shader
395
+ * assembler (`assembleShader`) does NOT emit a dispatch branch for it, and
396
+ * there is no concrete sprite kind module under `kinds/`.
397
+ */
398
+ declare const TYPE_SPRITE = 5;
399
+ declare const TYPE_TRIANGLE = 6;
400
+ declare const TYPE_ARC = 7;
401
+ /**
402
+ * Glyph quad packed by `GlyphPack`. One instance per visible glyph;
403
+ * atlas UV, atlas glyph id, and per-string flags live in the lane slots.
404
+ * The MSDF glyph pipeline (RTT bake) selects this kind at fragment time.
405
+ */
406
+ declare const TYPE_GLYPH = 8;
407
+ /**
408
+ * Analytic SDF stroke-path (`kinds/stroke.ts`). Up to 4 points per instance; an
409
+ * N-point stroke chains `ceil((M-1)/3)` instances. The min-of-segment SDF in
410
+ * the fragment computes coverage analytically (no CPU triangulation) — the
411
+ * opt-in alternative to the tessellated `pushPolyline` path for dense strokes.
412
+ */
413
+ declare const TYPE_STROKE_PATH = 9;
414
+ /** World-space axis-aligned bounding box. Used by tile-cull / damage tracking. */
415
+ interface WorldAABB {
416
+ minX: number;
417
+ minY: number;
418
+ maxX: number;
419
+ maxY: number;
420
+ }
421
+ /**
422
+ * A primitive-kind strategy. `R` is the kind's plain record type (the shape the
423
+ * caller supplies). Each kind owns the four facts the renderer needs about a
424
+ * primitive and nothing else.
425
+ */
426
+ interface PrimitiveKind<R> {
427
+ /** Matches the `TYPE_*` constant written into `typeFlag`. */
428
+ readonly typeFlag: number;
429
+ /**
430
+ * Pack one record into the flat instance buffer at `offset` (in floats).
431
+ * Writes exactly `INSTANCE_FLOATS` floats using the `InstanceRecord` field
432
+ * layout: `geom0` @ +0, `geom1` @ +4, `colorBits` @ +8, `typeFlag` @ +9,
433
+ * `order` @ +10, `lane0..lane4` @ +11..+15. `f32` and `u32` alias the same
434
+ * buffer; float fields are written through `f32`, bit-packed fields through
435
+ * `u32`.
436
+ *
437
+ * `order` (float-offset 10) carries explicit depth and is the caller's
438
+ * concern — the packer leaves it at whatever the caller pre-seeded, so the
439
+ * scene layer owns Z assignment. (Distinct from v2, which derived Z from the
440
+ * instance index via a per-draw uniform.)
441
+ */
442
+ pack(record: R, f32: Float32Array, u32: Uint32Array, offset: number): void;
443
+ /**
444
+ * World-space AABB of the primitive, padded by at least 1px of anti-alias
445
+ * fringe beyond the stroke boundary (so tile-cull never clips the AA edge).
446
+ */
447
+ aabb(record: R): WorldAABB;
448
+ /**
449
+ * WGSL geometry-expansion snippet for the vertex stage. Run when
450
+ * `shapeType == this.typeFlag`; reads the unpacked instance fields and the
451
+ * per-vertex quad, and assigns `world` (vec2f) plus the varyings the SDF
452
+ * fragment consumes. Authored once per kind — never copy-pasted between
453
+ * kinds.
454
+ */
455
+ wgslGeom(): string;
456
+ /**
457
+ * WGSL SDF + anti-alias snippet for the fragment stage. Run when
458
+ * `shapeType == this.typeFlag`; reads the varyings and instance fields and
459
+ * assigns the premultiplied `outColor` (vec4f). Authored once per kind.
460
+ *
461
+ * NOTE: AA floor varies by kind. Closed shapes (rect/circle/ellipse) clamp
462
+ * `pixelLocal` to max(..., 0.5); 1D/curved shapes (segment/curve/arc) use
463
+ * max(..., 1e-9) to avoid over-softening thin strokes.
464
+ */
465
+ wgslSdf(): string;
466
+ }
467
+ //#endregion
468
+ //#region src/shader/assemble.d.ts
469
+ /** The WGSL stage sources produced from one assembly pass. */
470
+ interface AssembledShader {
471
+ /** Full vertex-stage WGSL source (geometry expansion + camera transform). */
472
+ vertex: string;
473
+ /** Full fragment-stage WGSL source — with the opaque `discard`. */
474
+ opaque: string;
475
+ /** Full fragment-stage WGSL source — alpha-blended, no `discard`. */
476
+ transparent: string;
477
+ /**
478
+ * Full fragment-stage WGSL source for the OIT (A-buffer) **build** pass.
479
+ *
480
+ * It runs the SAME geometry expansion (the shared `vertex`) and the SAME
481
+ * per-kind SDF switch that produces `outColor` as {@link transparent} — the
482
+ * geometry/SDF logic is generated from the kinds exactly once, never
483
+ * hand-copied. Where the transparent variant returns `outColor` for hardware
484
+ * blending, this variant instead inserts `outColor` (premultiplied) plus the
485
+ * fragment depth into the per-pixel bounded-K A-buffer (atomic slot reserve,
486
+ * write-once). Color writes are masked off by the pipeline (`writeMask:0`);
487
+ * the returned `vec4f(0.0)` is discarded by the hardware.
488
+ *
489
+ * Adds, relative to the opaque/transparent fragment input, a
490
+ * `@builtin(position)` so it can compute the integer pixel index, and three
491
+ * extra `@group(2)` bindings (heads / slots / oitViewport). The vertex stage
492
+ * and binding groups 0/1 are unchanged, so {@link vertex} feeds this variant
493
+ * verbatim. `pipelines/oit-pipelines.ts` builds the build pipeline from this
494
+ * source; it must not re-author the geometry/SDF body.
495
+ */
496
+ oitBuild: string;
497
+ }
498
+ /**
499
+ * Assemble the full uber-shader from the kind contributions. Produces the
500
+ * vertex source plus the opaque and transparent fragment sources from a SINGLE
501
+ * fragment body — the two variants differ only by the trailing `discard`, so
502
+ * they cannot drift.
503
+ *
504
+ * @param kinds The primitive kinds to dispatch over. Every kind's `typeFlag`
505
+ * gets a geom branch in the vertex and an sdf branch in the fragment.
506
+ * @param _accessorWgsl Deprecated no-op parameter kept for call-site
507
+ * compatibility. Kinds read `inst.*` directly from the `InstanceStruct`
508
+ * binding; the generated `readInstance` accessor was dead code and has been
509
+ * removed. Pass any string (e.g. `INSTANCE_LAYOUT.wgslAccessor`) — it is
510
+ * ignored.
511
+ */
512
+ declare function assembleShader(kinds: PrimitiveKind<unknown>[], _accessorWgsl?: string): AssembledShader;
513
+ //#endregion
514
+ //#region src/pipelines/pipelines.d.ts
515
+ interface Pipelines {
516
+ /** Opaque pass: depth write on, no blend (src-replace). */
517
+ opaque: GPURenderPipeline;
518
+ /** Transparent pass: depth write off, ALPHA_BLEND premultiplied src-over. */
519
+ transparent: GPURenderPipeline;
520
+ /**
521
+ * Clear-quad pass: one oversized triangle-list triangle that resets color
522
+ * and depth (`z = 1.0`, `depthCompare:"always"`, write enabled) within the
523
+ * scissor rect. Used by partial-redraw to clear each damage rect before
524
+ * its overlapping geometry is redrawn.
525
+ */
526
+ clearQuad: GPURenderPipeline;
527
+ /**
528
+ * Shared bind-group layout for `@group(0)` — the camera uniform
529
+ * (`Camera { vpCol0, vpCol1, vpCol2 }`), visible to the vertex stage.
530
+ * Both {@link opaque} and {@link transparent} are built against this exact
531
+ * layout object so ONE camera bind group serves both passes. (With
532
+ * `layout:"auto"` the two pipelines would receive distinct, mutually
533
+ * incompatible auto-layouts and the renderer would need a separate bind
534
+ * group per pass.)
535
+ */
536
+ cameraLayout: GPUBindGroupLayout;
537
+ /**
538
+ * Shared bind-group layout for `@group(1)` — the read-only instance storage
539
+ * buffer (`array<InstanceStruct>`), visible to vertex + fragment. Like
540
+ * {@link cameraLayout}, shared by both passes so one instances bind group
541
+ * serves opaque + transparent.
542
+ */
543
+ instanceLayout: GPUBindGroupLayout;
544
+ }
545
+ /**
546
+ * Construct the three v3 render pipeline variants.
547
+ *
548
+ * @param root TypeGPU root (provides `root.device`).
549
+ * @param format Swapchain texture format.
550
+ * @param shader Assembled uber-shader from {@link assembleShader}.
551
+ */
552
+ declare function createPipelines(root: TgpuRoot, format: GPUTextureFormat, shader: AssembledShader): Pipelines;
553
+ //#endregion
554
+ //#region src/core/logger.d.ts
555
+ /**
556
+ * Injectable diagnostic logger.
557
+ *
558
+ * insomni emits a handful of development-time diagnostics (font-mismatch
559
+ * warnings, atlas-overflow warnings, device-loss / GPU validation errors, effect
560
+ * cleanup failures). Host apps need to be able to redirect or suppress that
561
+ * output, so library code routes every diagnostic through {@link getLogger}
562
+ * instead of calling `console.*` directly.
563
+ *
564
+ * The active logger is a single module-level value (defaulting to {@link console})
565
+ * rather than a value threaded through every call site: the diagnostic sites live
566
+ * in unrelated modules (text packing, glyph atlas, reactivity, SVG export) that
567
+ * have no handle on renderer/init options. Callers install their logger once via
568
+ * {@link setLogger} (or by passing `logger` to `initGPU` / `createRenderer`),
569
+ * which is a process-wide setting for the library.
570
+ */
571
+ interface Logger {
572
+ /** Non-fatal diagnostic — recoverable, but the caller should know. */
573
+ warn(...args: unknown[]): void;
574
+ /** Error diagnostic — something failed (device loss, GPU validation, …). */
575
+ error(...args: unknown[]): void;
576
+ /** Verbose/optional diagnostic. Defaults to a no-op when unset. */
577
+ debug?(...args: unknown[]): void;
578
+ }
579
+ /**
580
+ * Install the process-wide diagnostic logger. Pass `null`/`undefined` to reset
581
+ * back to the {@link console} default.
582
+ */
583
+ declare function setLogger(logger: Logger | null | undefined): void;
584
+ /** The active diagnostic logger. Defaults to {@link console}. */
585
+ declare function getLogger(): Logger;
586
+ //#endregion
587
+ //#region src/scene/group.d.ts
588
+ interface GroupOptions {
589
+ /** Transform applied by this group. Default: `IDENTITY`. */
590
+ transform?: Mat3;
591
+ /** Parent group — transforms compound up the chain. */
592
+ parent?: Group;
593
+ }
594
+ interface Group {
595
+ /** Mutable — update between frames; takes effect without re-packing. */
596
+ transform: Mat3;
597
+ /** Parent group. Immutable after creation. */
598
+ readonly parent: Group | null;
599
+ }
600
+ /** Mint a fresh group. Defaults to an identity transform with no parent. */
601
+ declare function createGroup(options?: GroupOptions): Group;
602
+ /**
603
+ * Walk the parent chain and return the compounded transform (root applied
604
+ * first, leaf last). Returns `IDENTITY` when `group` is `null`.
605
+ *
606
+ * Start with the leaf's transform, then pre-multiply each ancestor so the root
607
+ * sits leftmost
608
+ * in `multiply(parent, child)` (which applies `child` first, then `parent`).
609
+ */
610
+ declare function resolveGroupTransform(group: Group | null): Mat3;
611
+ //#endregion
612
+ //#region src/scene/space.d.ts
613
+ /** Coordinate space a layer's shapes are authored in. */
614
+ type LayerSpace = "world" | "ui";
615
+ /** Per-frame inputs the `project` hook needs to map a world AABB to pixels. */
616
+ interface ProjectOptions {
617
+ /** Resolved camera ({x, y, zoom, rotation}). Read only by `space:"world"`. */
618
+ camera: ResolvedCameraState;
619
+ /** Group whose compounded transform applies before the camera. `world` only. */
620
+ group: Group | null;
621
+ /**
622
+ * Device-pixel ratio. NOT applied to `ui` rects (they return CSS px — the
623
+ * renderer's `applyScissor` is the single dpr owner). Read only by `world`:
624
+ * the world projection maps through the renderer's CSS-px camera baseline via
625
+ * `worldToCssMatrix` (which divides the device dims it is fed by `dpr`), so
626
+ * device dims are passed as `viewportWidth/Height × dpr`.
627
+ */
628
+ dpr: number;
629
+ /** Viewport width in CSS pixels (the camera's projection target). */
630
+ viewportWidth: number;
631
+ /** Viewport height in CSS pixels. */
632
+ viewportHeight: number;
633
+ }
634
+ /**
635
+ * Project a layer-space AABB `(minX, minY)`–`(maxX, maxY)` to a **CSS-pixel**
636
+ * `FrameRect` suitable as a damage / scissor region. The renderer applies the
637
+ * device-pixel ratio exactly once (`applyScissor`); `project` does NOT
638
+ * pre-multiply dpr (the debt #11 single-owner fix).
639
+ *
640
+ * - `ui`: `{x,y,w,h}` in CSS px. Camera + dpr are never applied to the value.
641
+ * - `world`: resolve the group transform, transform all four corners through it
642
+ * and the world → CSS-px camera view (`worldToCssMatrix`, the CSS-px camera
643
+ * baseline), take the screen min/max (CSS px).
644
+ */
645
+ declare function project(minX: number, minY: number, maxX: number, maxY: number, space: LayerSpace, opts: ProjectOptions): FrameRect;
646
+ //#endregion
647
+ //#region src/kinds/rect.d.ts
648
+ /** A rounded rectangle. `(x, y)` is the top-left corner; `width`/`height` are
649
+ * the full extents (matching v1's `RectShape`). */
650
+ interface RectRecord {
651
+ x: number;
652
+ y: number;
653
+ width: number;
654
+ height: number;
655
+ fill?: Color;
656
+ stroke?: Color;
657
+ strokeWidth?: number;
658
+ cornerRadius?: number;
659
+ rotation?: number;
660
+ }
661
+ declare const rect: PrimitiveKind<RectRecord>;
662
+ //#endregion
663
+ //#region src/kinds/circle.d.ts
664
+ /** A circle centered at `(cx, cy)` with `radius`. */
665
+ interface CircleRecord {
666
+ cx: number;
667
+ cy: number;
668
+ radius: number;
669
+ fill?: Color;
670
+ stroke?: Color;
671
+ strokeWidth?: number;
672
+ }
673
+ declare const circle: PrimitiveKind<CircleRecord>;
674
+ //#endregion
675
+ //#region src/kinds/ellipse.d.ts
676
+ /** An ellipse centered at `(cx, cy)` with radii `rx`/`ry`. */
677
+ interface EllipseRecord {
678
+ cx: number;
679
+ cy: number;
680
+ rx: number;
681
+ ry: number;
682
+ fill?: Color;
683
+ stroke?: Color;
684
+ strokeWidth?: number;
685
+ rotation?: number;
686
+ }
687
+ declare const ellipse: PrimitiveKind<EllipseRecord>;
688
+ //#endregion
689
+ //#region src/kinds/segment.d.ts
690
+ /** A straight line segment from `(x1, y1)` to `(x2, y2)`. */
691
+ interface SegmentRecord {
692
+ x1: number;
693
+ y1: number;
694
+ x2: number;
695
+ y2: number;
696
+ color: Color;
697
+ /** Line thickness in layer-local units. Default 1. */
698
+ width?: number;
699
+ }
700
+ declare const segment: PrimitiveKind<SegmentRecord>;
701
+ //#endregion
702
+ //#region src/kinds/curve.d.ts
703
+ /** End cap style for a curve / open stroke. */
704
+ type CurveCap = "round" | "butt" | "square";
705
+ /** A cubic Bezier stroke through `p0..p3`. */
706
+ interface CurveRecord {
707
+ p0: Vec2;
708
+ p1: Vec2;
709
+ p2: Vec2;
710
+ p3: Vec2;
711
+ color: Color;
712
+ /** Uniform thickness. Default 1. Overridden by `widthStart`/`widthEnd`. */
713
+ width?: number;
714
+ /** Thickness at `p0`. Falls back to `width`. */
715
+ widthStart?: number;
716
+ /** Thickness at `p3`. Falls back to `width`. */
717
+ widthEnd?: number;
718
+ /** End cap style. Default `round`. */
719
+ cap?: CurveCap;
720
+ }
721
+ declare const curve: PrimitiveKind<CurveRecord>;
722
+ //#endregion
723
+ //#region src/kinds/arc.d.ts
724
+ /** A circular arc centred at `(cx, cy)`, radius `radius`, spanning
725
+ * `theta0`→`theta1` (absolute radians; direction irrelevant). */
726
+ interface ArcRecord {
727
+ cx: number;
728
+ cy: number;
729
+ radius: number;
730
+ theta0: number;
731
+ theta1: number;
732
+ color: Color;
733
+ /** Stroke thickness in layer-local units. Default 1. */
734
+ width?: number;
735
+ }
736
+ declare const arc: PrimitiveKind<ArcRecord>;
737
+ //#endregion
738
+ //#region src/kinds/triangle.d.ts
739
+ /** A flat-filled triangle through `p1`, `p2`, `p3`. */
740
+ interface TriangleRecord {
741
+ p1: Vec2;
742
+ p2: Vec2;
743
+ p3: Vec2;
744
+ fill: Color;
745
+ }
746
+ declare const triangle: PrimitiveKind<TriangleRecord>;
747
+ //#endregion
748
+ //#region src/kinds/stroke.d.ts
749
+ /** Maximum points packed into one stroke instance (geom0+geom1 = 4 vec2f). */
750
+ declare const STROKE_MAX_POINTS = 4;
751
+ /** Live sub-segments per full instance (= STROKE_MAX_POINTS - 1). */
752
+ declare const STROKE_SEGMENTS_PER_INSTANCE: number;
753
+ /**
754
+ * One packed stroke-path instance: up to {@link STROKE_MAX_POINTS} points plus
755
+ * the stroke style. Produced by {@link chunkStroke}; the public authoring entry
756
+ * point is `Layer.pushStroke`, which builds the chunk list from a polyline.
757
+ */
758
+ interface StrokeInstanceRecord {
759
+ /** Live points for this instance (2..4). Slots beyond `points.length` are padded. */
760
+ points: readonly Vec2[];
761
+ color: Color;
762
+ /** Stroke thickness. Default 1. */
763
+ width?: number;
764
+ /** Cap at the path START — only applied when {@link isChainStart}. Default round. */
765
+ capStart?: LineCap;
766
+ /** Cap at the path END — only applied when {@link isChainEnd}. Default round. */
767
+ capEnd?: LineCap;
768
+ /** Join style at interior vertices. Default round. */
769
+ join?: LineJoin;
770
+ /** Miter limit (ratio). Default 4. */
771
+ miterLimit?: number;
772
+ /** True when this instance carries the path's first point (apply `capStart`). */
773
+ isChainStart?: boolean;
774
+ /** True when this instance carries the path's last point (apply `capEnd`). */
775
+ isChainEnd?: boolean;
776
+ }
777
+ /** Solid-stroke input for {@link chunkStroke} (the polyline before SDF chunking). */
778
+ interface StrokePathShape {
779
+ points: readonly Vec2[];
780
+ color: Color;
781
+ width?: number;
782
+ cap?: LineCap;
783
+ join?: LineJoin;
784
+ miterLimit?: number;
785
+ /** When true, the first and last points are joined into a loop. */
786
+ closed?: boolean;
787
+ }
788
+ /**
789
+ * Number of SDF instances a stroke of `pointCount` points produces:
790
+ * `ceil((pointCount - 1) / 3)` (3 live sub-segments per K=4 instance). A
791
+ * `closed` stroke adds one synthetic segment back to the start, so it uses
792
+ * `pointCount` segments → `ceil(pointCount / 3)`.
793
+ */
794
+ declare function strokeInstanceCount(pointCount: number, closed?: boolean): number;
795
+ /**
796
+ * Chunk a solid stroke into {@link StrokeInstanceRecord}s of up to
797
+ * {@link STROKE_MAX_POINTS} points each. Consecutive instances overlap on their
798
+ * shared boundary point (instance `k` spans points `[3k .. 3k+3]`), so the
799
+ * round-join blob at the boundary covers the seam. Returns `[]` for a degenerate
800
+ * stroke (fewer than 2 points after closing).
801
+ *
802
+ * Cap application: `capStart` is set ONLY on the instance carrying the path's
803
+ * first point and `capEnd` ONLY on the one carrying the last point. A `closed`
804
+ * stroke has no caps (the wrap segment makes both ends interior joins).
805
+ */
806
+ declare function chunkStroke(shape: StrokePathShape): StrokeInstanceRecord[];
807
+ /**
808
+ * Pack the {@link StrokeInstanceRecord} flag word for `lane1` (see the lane
809
+ * layout comment at the top of this file). Exported for the unit tests to
810
+ * round-trip without re-deriving the bit packing.
811
+ */
812
+ declare function packStrokeFlags(rec: StrokeInstanceRecord): number;
813
+ declare const stroke: PrimitiveKind<StrokeInstanceRecord>;
814
+ //#endregion
815
+ //#region src/kinds/index.d.ts
816
+ /**
817
+ * Every concrete primitive kind, sorted ascending by `typeFlag`. This is the
818
+ * canonical kind list `assembleShader()` dispatches over (one `if (typeFlag ==
819
+ * N)` branch per kind, in this order) and the order the renderer factory feeds
820
+ * the shader assembler.
821
+ *
822
+ * Note the gaps vs the `TYPE_*` discriminants: `TYPE_SPRITE` (5) has no kind
823
+ * module yet (sprites go through the textured-quad composite path), and
824
+ * `TYPE_GLYPH` (8) is NOT a uber-shader kind — glyphs render only inside the RTT
825
+ * bake via the dedicated MSDF glyph pipeline (`pipelines/glyph-pipelines.ts`).
826
+ * Both are intentionally absent from the dispatch chain.
827
+ */
828
+ declare const ORDERED_KINDS: readonly PrimitiveKind<unknown>[];
829
+ //#endregion
830
+ //#region src/pack/uber-pack.d.ts
831
+ /**
832
+ * A contiguous span of instances in `UberPack.buffer` that share a kind and
833
+ * opacity, suitable for a single GPU draw call. Byte offsets align to
834
+ * `INSTANCE_BYTES` (64 bytes).
835
+ */
836
+ interface DrawCommand {
837
+ /** `PrimitiveKind.typeFlag` identifying the shape kind for this span. */
838
+ kind: number;
839
+ /** True when all instances in this span are fully opaque. */
840
+ opaque: boolean;
841
+ /** Byte start of this span within the `UberPack` buffer. */
842
+ byteOffset: number;
843
+ /** Byte length of this span (always a multiple of `INSTANCE_BYTES`). */
844
+ byteCount: number;
845
+ /**
846
+ * Optional per-command clip rect (CSS px). `null` / absent means "no clip"
847
+ * (the command draws across the full damage rect). Present on clipped commands
848
+ * from scene layers; the partial-redraw path intersects it with each damage
849
+ * rect to cull zero-area draws.
850
+ */
851
+ clipRect?: FrameRect | null;
852
+ /**
853
+ * Optional per-command `Group` (P3-T8). `null` / absent means "no group" (the
854
+ * command draws through the layer's base space view-projection). When present
855
+ * the renderer composes the group's resolved transform INTO the space VP and
856
+ * routes this command to a distinct per-(space × group) camera slot — geometry
857
+ * transforms live, with NO repack (the group is CPU-side draw metadata only;
858
+ * the packed instance bytes never carry it). A group-identity change is a
859
+ * `_mergeCommand` boundary, so two shapes in different groups never coalesce
860
+ * into one draw (they need different slots).
861
+ */
862
+ group?: Group | null;
863
+ }
864
+ /**
865
+ * CPU-side flat instance buffer + draw-command list for the v3 renderer.
866
+ *
867
+ * Usage per frame:
868
+ * ```ts
869
+ * pack.clear();
870
+ * for (const item of scene) {
871
+ * pack.append(item.kind, item.record, item.opaque);
872
+ * }
873
+ * // Upload pack.buffer[0..pack.byteLength) to GPU, then iterate pack.commands.
874
+ * pack.tickShrink();
875
+ * ```
876
+ */
877
+ declare class UberPack {
878
+ private _buf;
879
+ private _f32;
880
+ private _u32;
881
+ /** Occupied byte count within `_buf`. */
882
+ private _byteLen;
883
+ private _commands;
884
+ /** Monotonic mutation counter — incremented by `append` and `clear`. */
885
+ private _version;
886
+ private _aabbs;
887
+ /** Count of AABBs written (== instance count). */
888
+ private _aabbCount;
889
+ private _lowWaterFrames;
890
+ private _peakBytes;
891
+ constructor();
892
+ /** The underlying `ArrayBuffer`. Only the first `byteLength` bytes are valid. */
893
+ get buffer(): ArrayBuffer;
894
+ /** Number of valid (occupied) bytes at the start of `buffer`. */
895
+ get byteLength(): number;
896
+ /** Read-only view of the current draw commands. Invalidated by `clear()`. */
897
+ get commands(): readonly DrawCommand[];
898
+ /**
899
+ * Parallel per-instance world-space AABBs — 4 floats per instance
900
+ * (`minX, minY, maxX, maxY`), in `append` order. Only the first
901
+ * `instanceCount * 4` floats are valid. Instance `i` reads `aabbs[i*4 .. i*4+3]`.
902
+ * Consumed by the v3 frustum-cull stage; populated lazily by `append`.
903
+ */
904
+ get aabbs(): Float32Array;
905
+ /**
906
+ * Monotonic mutation counter. Incremented by every `append` and `clear`
907
+ * call. Consumers (e.g. `MirrorLayer` simplify path) compare this across
908
+ * frames to detect whether a rebuild is necessary.
909
+ */
910
+ get version(): number;
911
+ /**
912
+ * Append one instance to the buffer. Calls `kind.pack` to write the record
913
+ * into the tail of the buffer, then merges or pushes a `DrawCommand`.
914
+ *
915
+ * `group` (P3-T8, optional) is CPU-side draw metadata — it is NOT packed into
916
+ * the instance bytes (geometry packing + the instance schema are UNTOUCHED).
917
+ * It rides along on the resulting `DrawCommand` so the renderer can compose
918
+ * the group transform into the camera slot live; a group-identity change is a
919
+ * merge boundary (see {@link _mergeCommand}). Omitting it (the common case)
920
+ * is byte-identical to the pre-T8 path: `group` defaults to `null`.
921
+ *
922
+ * `emphasisKey` (Phase 4, optional) is the per-instance emphasis key written
923
+ * into `lane4`. It is NOT geometry — no shape kind reads `lane4` — so it is
924
+ * stamped after `kind.pack`. The default `0` is written unconditionally so the
925
+ * key is deterministic (the buffer is reused across frames without zeroing).
926
+ * With the default emphasis uniform (`t == 0`) this has no visual effect.
927
+ */
928
+ append<R>(kind: PrimitiveKind<R>, rec: R, opaque: boolean, group?: Group | null, emphasisKey?: number): void;
929
+ /**
930
+ * Append `count` pre-packed instances verbatim from `src` into the tail of
931
+ * the buffer, then merge/push a single `DrawCommand` for the whole span.
932
+ *
933
+ * This is the byte-level analogue of {@link append}: rather than calling a
934
+ * `PrimitiveKind.pack` callback per instance, it `set()`s an already-packed
935
+ * `INSTANCE_BYTES * count` byte block (e.g. a slice copied out of another
936
+ * pack's buffer) directly. The caller owns the `typeFlag` — it is written onto
937
+ * the resulting command for the renderer's draw-call dispatch exactly as if
938
+ * each instance had been packed by a kind with that `typeFlag`.
939
+ *
940
+ * Used by {@link MirrorLayer}'s simplify rebuild, which copies/strips spans
941
+ * out of the primary pack's buffer and re-appends them. Replaces the prior
942
+ * synthetic-`PrimitiveKind` closure (which faked a kind with mutable per-call
943
+ * state, violating the kind contract).
944
+ *
945
+ * AABBs: per-instance world AABBs are NOT recoverable from packed bytes alone,
946
+ * so each appended instance gets a zero AABB (`0,0,0,0`) — identical to what
947
+ * the prior synthetic kind produced. The frustum-cull stage treats a degenerate
948
+ * AABB as "always submit", so a raw-appended span is never wrongly culled.
949
+ *
950
+ * `group` / `clipRect` flow onto the command exactly like {@link append}'s
951
+ * group: they participate in the {@link _mergeCommand} boundary (a span with a
952
+ * different group OR clip never coalesces with the previous command) and ride
953
+ * along on the pushed `DrawCommand`. A `null`/absent group + clip merges like
954
+ * `append` did before — byte- and command-identical.
955
+ *
956
+ * No-op when `count === 0`.
957
+ */
958
+ appendRaw(src: Uint8Array, typeFlag: number, opaque: boolean, count: number, group?: Group | null, clipRect?: FrameRect | null): void;
959
+ /**
960
+ * Reset the buffer for the next frame. Does not release memory — the
961
+ * allocated capacity is reused. Clears both the command list and the
962
+ * occupied byte count.
963
+ */
964
+ clear(): void;
965
+ /**
966
+ * Call once per frame end (after rendering). Tracks the low-water mark and
967
+ * reallocates to a smaller buffer after `SHRINK_FRAMES` consecutive frames
968
+ * where the occupied bytes are below 50 % of the buffer capacity.
969
+ *
970
+ * Safe: never reallocates when `_byteLen > newSize`.
971
+ */
972
+ tickShrink(): void;
973
+ /**
974
+ * Merge into the last `DrawCommand` if kind + opaque + group + contiguous,
975
+ * otherwise push a new command. Two instances tagged with DIFFERENT
976
+ * `Group` objects (by reference; `null` === "no group") never coalesce, since
977
+ * each group routes to its own per-(space × group) camera slot in the
978
+ * renderer. This RESTORES the architecture doc's "same-group merge" — the
979
+ * pre-T8 path dropped the group entirely and wrongly merged across groups.
980
+ * A `null`/absent group keeps the old behaviour exactly.
981
+ */
982
+ private _mergeCommand;
983
+ /**
984
+ * Ensure at least `additional` more bytes can be written at `_byteLen`.
985
+ * Growth policy: pow-of-2 doubling below `POW2_THRESHOLD_BYTES`; 1.25×
986
+ * linear at or above. Throws on overflow (exceeds `Number.MAX_SAFE_INTEGER`).
987
+ */
988
+ private _ensureCapacity;
989
+ /**
990
+ * Ensure the parallel AABB array holds at least `instances` instances (4 floats
991
+ * each). Doubles on demand and copies the existing AABBs across — the count is
992
+ * driven by `append` and never exceeds the instance count.
993
+ */
994
+ private _ensureAabbCapacity;
995
+ /** Reallocate to exactly `newByteLen` bytes, copying existing data. */
996
+ private _reallocate;
997
+ }
998
+ /**
999
+ * Compute the next buffer byte capacity given the current byte capacity and
1000
+ * the minimum required bytes.
1001
+ *
1002
+ * - Below `POW2_THRESHOLD_BYTES`: power-of-2 doubling from `current` until ≥ `required`.
1003
+ * - At or above threshold: `ceil(current * LINEAR_FACTOR)`, clamped to `required`.
1004
+ *
1005
+ * Throws a `RangeError` if `required` exceeds `Number.MAX_SAFE_INTEGER`.
1006
+ */
1007
+ declare function nextByteCapacity(currentBytes: number, requiredBytes: number): number;
1008
+ //#endregion
1009
+ //#region src/scene/sprite-pack.d.ts
1010
+ /** Floats per sprite record — matches the 64 B `SpriteSchema`. */
1011
+ declare const SPRITE_FLOATS = 16;
1012
+ /** Bytes per sprite record (64 B). */
1013
+ declare const SPRITE_BYTES: number;
1014
+ /**
1015
+ * One sprite to push onto a `Layer`.
1016
+ */
1017
+ interface SpriteShape {
1018
+ /** World/ui/device-space position of the sprite anchor. */
1019
+ pos: Vec2;
1020
+ /** Sprite size in the layer's coordinate space. */
1021
+ size: Vec2;
1022
+ /** Source texture or a borrowed sub-region of one. */
1023
+ texture: Texture | TextureRegion;
1024
+ /** Multiplicative tint (premultiplied in-shader). Default opaque white. */
1025
+ tint?: Color;
1026
+ /** Rotation in radians about the anchor. Default 0. */
1027
+ rotation?: number;
1028
+ /** Normalized anchor within the sprite. Default `[0.5, 0.5]` (center). */
1029
+ anchor?: Vec2;
1030
+ /**
1031
+ * When `true`, routes into the opaque (depth-write) pass so the sprite
1032
+ * contributes to early-Z occlusion. Default `false` (texture alpha unknown at
1033
+ * pack time). Set only when the texture is fully opaque.
1034
+ */
1035
+ opaque?: boolean;
1036
+ }
1037
+ /**
1038
+ * A merge-coalesced sprite draw command: a run of `count` contiguous sprite
1039
+ * records (from float offset `offset * SPRITE_FLOATS`) sharing one `texture` and
1040
+ * `opaque` flag. Mirrors v1's `DrawCommand` (kind `"sprites"`).
1041
+ */
1042
+ interface SpriteCommand {
1043
+ texture: Texture;
1044
+ /** Item index of the first sprite in this run. */
1045
+ offset: number;
1046
+ /** Number of contiguous sprites in this run. */
1047
+ count: number;
1048
+ opaque: boolean;
1049
+ }
1050
+ /**
1051
+ * CPU-side sprite accumulator backing `Layer.pushSprite`. Holds a flat
1052
+ * `SpriteSchema`-layout float buffer (grown power-of-2) and a per-texture,
1053
+ * adjacency-merged command list. The renderer reads {@link data} / {@link commands}
1054
+ * after the main pass and draws each command through the interop sprites pipeline.
1055
+ */
1056
+ declare class SpritePack {
1057
+ private _data;
1058
+ private _count;
1059
+ private readonly _commands;
1060
+ constructor(initialCapacity?: number);
1061
+ /** Number of sprites packed. */
1062
+ get count(): number;
1063
+ /** Tight view of the packed sprite floats (`count * SPRITE_FLOATS`). */
1064
+ get data(): Float32Array;
1065
+ /** Merge-coalesced per-texture draw commands, in submission order. */
1066
+ get commands(): readonly SpriteCommand[];
1067
+ /** True when no sprites have been pushed (renderer can skip the layer). */
1068
+ get isEmpty(): boolean;
1069
+ /**
1070
+ * Append one sprite and emit (or extend) its draw command. Adjacent sprites
1071
+ * sharing a texture + opaque flag coalesce into one command (verbatim with
1072
+ * v1's `appendSpriteCommand`).
1073
+ */
1074
+ push(sprite: SpriteShape): void;
1075
+ /**
1076
+ * Stamp the `order` (depth, float-offset 15) of the sprites in
1077
+ * `[start, start + count)` to `value`. The owning {@link Layer} calls this at
1078
+ * finalize with depths derived from the layer's UNIFIED push sequence (one
1079
+ * continuous domain across shapes/glyphs/sprites), so a sprite z-interleaves
1080
+ * with the shapes/glyphs pushed around it. No-op when `count <= 0`.
1081
+ */
1082
+ stampOrder(start: number, count: number, value: number): void;
1083
+ /** Reset for the next frame. Capacity is reused (no reallocation). */
1084
+ clear(): void;
1085
+ /** Pack one sprite's 16 floats in the exact `SpriteSchema` byte layout. */
1086
+ private _appendRecord;
1087
+ /** Emit or extend the trailing draw command (adjacency merge by texture). */
1088
+ private _appendCommand;
1089
+ /** Grow the float buffer power-of-2 to hold `additional` more sprites. */
1090
+ private _ensureCapacity;
1091
+ }
1092
+ //#endregion
1093
+ //#region src/schema/glyph.d.ts
1094
+ /** Per-block style. Layout matches `TextStyleTable` in text/effects.ts (144 B). */
1095
+ declare const TextStyleSchema: d.WgslStruct<{
1096
+ fillColor: d.Vec4f;
1097
+ outlineColor: d.Vec4f;
1098
+ shadowColor: d.Vec4f;
1099
+ gradientFrom: d.Vec4f;
1100
+ gradientTo: d.Vec4f;
1101
+ blockOrigin: d.Vec2f;
1102
+ blockExtent: d.Vec2f;
1103
+ shadowOffset: d.Vec2f;
1104
+ gradientDir: d.Vec2f;
1105
+ outlineWidth: d.F32;
1106
+ outlineSoftness: d.F32;
1107
+ shadowSoftness: d.F32;
1108
+ pxRange: d.F32;
1109
+ flags: d.U32;
1110
+ _pad0: d.U32;
1111
+ _pad1: d.Vec2u;
1112
+ }>;
1113
+ //#endregion
1114
+ //#region src/text/effects.d.ts
1115
+ type StyleStorageBuffer = TgpuBuffer<d.WgslArray<typeof TextStyleSchema>> & StorageFlag;
1116
+ interface TextOutline {
1117
+ color: Color;
1118
+ /** Outline half-width in screen pixels. */
1119
+ width: number;
1120
+ /** AA softness in screen pixels. Default 1. */
1121
+ softness?: number;
1122
+ }
1123
+ interface TextShadow {
1124
+ color: Color;
1125
+ /** Offset in screen pixels (positive Y = down on screen). */
1126
+ dx: number;
1127
+ dy: number;
1128
+ /** Edge softness in screen pixels. Default 2. */
1129
+ softness?: number;
1130
+ }
1131
+ interface TextGradient {
1132
+ from: Color;
1133
+ to: Color;
1134
+ /** Angle in radians, 0 = left→right, π/2 = top→bottom. Default 0. */
1135
+ angle?: number;
1136
+ }
1137
+ interface TextStyle {
1138
+ fill: Color;
1139
+ outline?: TextOutline;
1140
+ shadow?: TextShadow;
1141
+ gradient?: TextGradient;
1142
+ }
1143
+ interface BlockGeometry {
1144
+ /** World-space top-left of the block's content bbox. */
1145
+ originX: number;
1146
+ originY: number;
1147
+ /** World-space size of the block's content bbox. */
1148
+ width: number;
1149
+ height: number;
1150
+ }
1151
+ /**
1152
+ * Manages the GPU style-table storage buffer. One entry per visible text
1153
+ * block per frame. `reset()` is called at the start of each frame; `add()`
1154
+ * returns a `blockId` that the per-glyph instance data references.
1155
+ *
1156
+ * Layout matches `TextStyleSchema` in `schema/glyph.ts`.
1157
+ */
1158
+ declare class TextStyleTable {
1159
+ private readonly root;
1160
+ /** CPU staging buffer; resized as power-of-2 when capacity grows. */
1161
+ private cpuBuffer;
1162
+ /** Same backing memory as `cpuBuffer`, viewed as u32 for flag/u32 writes. */
1163
+ private cpuBufferU32;
1164
+ /** GPU buffer. Reallocated when capacity grows. */
1165
+ private gpu;
1166
+ private _capacity;
1167
+ private _count;
1168
+ private _dirty;
1169
+ /** Set when any added style enables the shadow flag this frame. */
1170
+ private _hasShadow;
1171
+ /**
1172
+ * Per-frame intern table. Key is a string-serialized style payload; value
1173
+ * is the blockId previously allocated for that payload. Cleared on reset()
1174
+ * — block geometry varies by call site, so dedupe is single-frame scoped.
1175
+ */
1176
+ private internMap;
1177
+ constructor(owner: GPUOwner, options?: {
1178
+ label?: string;
1179
+ initialCapacity?: number;
1180
+ });
1181
+ get count(): number;
1182
+ /** Raw GPU buffer (advanced use). */
1183
+ get gpuBuffer(): GPUBuffer;
1184
+ /** TgpuBuffer for the style storage. Identity changes on `grow()`. */
1185
+ get tgpuBuffer(): StyleStorageBuffer;
1186
+ get hasShadow(): boolean;
1187
+ /** Drop all entries. Call at the start of each frame. */
1188
+ reset(): void;
1189
+ /**
1190
+ * Append a style entry, return its blockId.
1191
+ * `geometry` provides the block's world-space AABB so the gradient can
1192
+ * sample in block-local space.
1193
+ */
1194
+ add(style: TextStyle, geometry: BlockGeometry, pxRange: number): number;
1195
+ /**
1196
+ * Like `add`, but returns an existing blockId when called with a
1197
+ * byte-equivalent style. Geometry is excluded from the dedupe key when no
1198
+ * gradient is configured (the fragment shader only reads
1199
+ * `blockOrigin`/`blockExtent` on the gradient path), so labels at
1200
+ * different positions can share a single style entry. With a gradient,
1201
+ * geometry is part of the key — falls back to per-block allocation.
1202
+ */
1203
+ intern(style: TextStyle, geometry: BlockGeometry, pxRange: number): number;
1204
+ /** Upload dirty CPU data to the GPU buffer. Idempotent within a frame. */
1205
+ flush(): void;
1206
+ destroy(): void;
1207
+ private createGpuBuffer;
1208
+ private grow;
1209
+ private write;
1210
+ }
1211
+ //#endregion
1212
+ //#region src/glyph/glyph-pack.d.ts
1213
+ /**
1214
+ * Per-string rendering flags. Replace the three-pack split in v1.
1215
+ *
1216
+ * - `needsShaping` — full shaper (kerning, multi-line, wrap). Default false.
1217
+ * The fast LUT/simple path is used when false.
1218
+ * - `anchored` — world anchor stored in lane3/lane4 (f32 bits) without CPU
1219
+ * camera projection; geom0.xy holds the origin-relative CSS-px pen position.
1220
+ * Bit 0 of lane2 is set so the vertex shader projects the anchor per-frame and
1221
+ * adds the glyph quad as a screen-px NDC delta. Pan never triggers a repack.
1222
+ * - `screenSized` — em size is CSS px, not world units; bit 1 of lane2 is set
1223
+ * so the vertex shader scales by DPR instead of camera scale.
1224
+ */
1225
+ interface GlyphPackFlags {
1226
+ needsShaping?: boolean;
1227
+ anchored?: boolean;
1228
+ screenSized?: boolean;
1229
+ }
1230
+ /**
1231
+ * Shape descriptor for `GlyphPack.pushText`. Mirrors v1's `TextShape` fields
1232
+ * for call-site compatibility; fields not used by the v3 CPU packer (style
1233
+ * effects — `outline`, `shadow`, `gradient`) are accepted but silently ignored.
1234
+ * Effect support is a future v3 milestone.
1235
+ */
1236
+ interface GlyphTextShape {
1237
+ text: string;
1238
+ /**
1239
+ * World-space x (or CSS-px x when `screenSized`). When `anchored`, this is
1240
+ * the WORLD anchor x — projected through the camera per-frame by the shader,
1241
+ * not summed into the glyph geometry.
1242
+ */
1243
+ x: number;
1244
+ /** World-space y (or CSS-px y when `screenSized`). Top of the first line.
1245
+ * When `anchored`, the WORLD anchor y. */
1246
+ y: number;
1247
+ /**
1248
+ * CSS-px offset from the projected anchor, applied in screen space. Only
1249
+ * meaningful when `anchored` (mirrors v1's `AnchoredStringShape.offsetX/Y`).
1250
+ * Default 0.
1251
+ */
1252
+ offsetX?: number;
1253
+ offsetY?: number;
1254
+ /** Fill color. Default BLACK. */
1255
+ color?: Color;
1256
+ /** Font size in world units per em (or CSS px when `screenSized`). Default 16. */
1257
+ fontSize?: number;
1258
+ align?: "left" | "center" | "right";
1259
+ /** Hard wrap width in world units. */
1260
+ maxWidth?: number;
1261
+ /** Line spacing in world units. Default: fontSize * 1.2. */
1262
+ lineHeight?: number;
1263
+ /**
1264
+ * Skip kerning, multi-line, and wrap (matches v1's `TextShape.simple`).
1265
+ * Equivalent to passing `{ needsShaping: false }` in flags.
1266
+ */
1267
+ simple?: boolean;
1268
+ /**
1269
+ * Glyph outline (stroke around each glyph). Accepted for call-site
1270
+ * compatibility with v1's `TextShape`; silently ignored by the v3 packer
1271
+ * until outline support is added to the glyph pipeline.
1272
+ */
1273
+ outline?: TextOutline;
1274
+ /**
1275
+ * Drop/inner shadow. Accepted for call-site compatibility with v1's
1276
+ * `TextShape`; silently ignored by the v3 packer until shadow support is
1277
+ * added to the glyph pipeline.
1278
+ */
1279
+ shadow?: TextShadow;
1280
+ /**
1281
+ * Linear gradient applied across the glyph fill. Accepted for call-site
1282
+ * compatibility with v1's `TextShape`; silently ignored by the v3 packer
1283
+ * until gradient support is added to the glyph pipeline.
1284
+ */
1285
+ gradient?: TextGradient;
1286
+ }
1287
+ /** Layout metrics returned by `pushText`. */
1288
+ interface GlyphMetricsOut {
1289
+ glyphCount: number;
1290
+ bbox: {
1291
+ minX: number;
1292
+ minY: number;
1293
+ maxX: number;
1294
+ maxY: number;
1295
+ };
1296
+ }
1297
+ /**
1298
+ * One plain-text push, recorded for auto-damage push-level diffing (D10
1299
+ * revised). The fields are EVERY layout-affecting input — two records that
1300
+ * compare equal are guaranteed to have produced identical pixels (the glyph
1301
+ * stream is a pure function of these plus the atlas), so an unchanged push
1302
+ * re-issued next frame (the clear-and-redraw HUD pattern) contributes no
1303
+ * damage; only genuinely changed pushes damage their old/new bboxes.
1304
+ */
1305
+ interface GlyphPushRecord {
1306
+ /** The pushed string (reference — never copied). */
1307
+ readonly text: string;
1308
+ readonly x: number;
1309
+ readonly y: number;
1310
+ readonly fontSize: number;
1311
+ readonly align: "left" | "center" | "right";
1312
+ readonly lineHeight: number;
1313
+ readonly maxWidth: number | undefined;
1314
+ /** Layout switches that change glyph placement for the same text. */
1315
+ readonly kerning: boolean;
1316
+ readonly simple: boolean;
1317
+ readonly colorBits: number;
1318
+ /** The push's resulting layer-space layout bbox. */
1319
+ readonly minX: number;
1320
+ readonly minY: number;
1321
+ readonly maxX: number;
1322
+ readonly maxY: number;
1323
+ }
1324
+ /**
1325
+ * CPU-side glyph instance buffer for v3.
1326
+ *
1327
+ * Each glyph is one 16-float `InstanceRecord` with `typeFlag=TYPE_GLYPH`.
1328
+ * The pack is independent of any GPU device — construct and fill it in any
1329
+ * environment and hand the `buffer` to the renderer for upload.
1330
+ *
1331
+ * Capacity grows power-of-2; `dataU32` is always re-aliased after growth.
1332
+ */
1333
+ declare class GlyphPack {
1334
+ private data;
1335
+ private dataU32;
1336
+ private _glyphCount;
1337
+ private _version;
1338
+ private _bboxMinX;
1339
+ private _bboxMinY;
1340
+ private _bboxMaxX;
1341
+ private _bboxMaxY;
1342
+ private _hasUnboundedGlyphs;
1343
+ private _pushRecords;
1344
+ private _retainAllRecords;
1345
+ constructor(initialCap?: number, retainAllRecords?: boolean);
1346
+ /** Number of glyph instances currently packed. */
1347
+ get count(): number;
1348
+ /**
1349
+ * Monotonic content version. Increments on every mutating call (`pushText` /
1350
+ * `pushString` / `pushAnchoredString` / `clear`) and never otherwise — so two
1351
+ * reads with the same value guarantee the packed bytes are unchanged. The
1352
+ * renderer uses it (alongside `count`) to skip the per-frame glyph upload on a
1353
+ * frame that only moved the camera.
1354
+ */
1355
+ get version(): number;
1356
+ /**
1357
+ * The underlying flat `Float32Array`. Valid range: `[0, count * 16)`.
1358
+ *
1359
+ * May be reallocated on any push. Always re-read the reference after
1360
+ * calling push methods if you hold a pre-push reference.
1361
+ */
1362
+ get buffer(): Float32Array;
1363
+ /**
1364
+ * Cumulative layer-space bbox of every PLAIN glyph pushed since `clear()`,
1365
+ * or `null` when none. Anchored / screen-sized glyphs do NOT contribute (see
1366
+ * {@link hasUnboundedGlyphs}). Returns a fresh object — safe to retain.
1367
+ */
1368
+ get contentBbox(): {
1369
+ minX: number;
1370
+ minY: number;
1371
+ maxX: number;
1372
+ maxY: number;
1373
+ } | null;
1374
+ /**
1375
+ * Whether any anchored or screen-sized glyphs were pushed since `clear()`.
1376
+ * Their pixels are placed by the camera projection at draw time, so
1377
+ * {@link contentBbox} cannot bound them — auto-damage must not trust the
1378
+ * bbox for this pack and falls back to a full-frame promote (D10).
1379
+ */
1380
+ get hasUnboundedGlyphs(): boolean;
1381
+ /**
1382
+ * The plain pushes since `clear()` as {@link GlyphPushRecord}s, or `null`
1383
+ * once the pack overflowed {@link GLYPH_PUSH_RECORD_CAP}. Auto-damage diffs
1384
+ * these against the previous frame's records so an unchanged push (same
1385
+ * text, position, layout) contributes NO damage — without this, one changed
1386
+ * HUD string damages the union of EVERY text block in the layer.
1387
+ */
1388
+ get pushRecords(): readonly GlyphPushRecord[] | null;
1389
+ /**
1390
+ * Push all visible glyphs from `shape` into the pack.
1391
+ *
1392
+ * Routing:
1393
+ * - Single-line AND (`simple=true` OR no `\n` + no `maxWidth`):
1394
+ * fast path — cache lookup + memcpy + per-glyph patch (translate + colorBits + lane2).
1395
+ * - Otherwise:
1396
+ * slow path — full `shapeText` call + per-glyph pack.
1397
+ *
1398
+ * `flags.needsShaping` overrides the routing decision: when true the slow
1399
+ * path is always used; when false the fast path is always used.
1400
+ */
1401
+ pushText(shape: GlyphTextShape, atlas: GlyphAtlas, flags?: GlyphPackFlags): GlyphMetricsOut;
1402
+ /**
1403
+ * LUT (no-kerning) alias for `pushText`. Equivalent to calling `pushText`
1404
+ * with `{ needsShaping: false }`. Preserved for v1 call-site parity
1405
+ * (`TextStringPack.pushString`).
1406
+ */
1407
+ pushString(shape: GlyphTextShape, atlas: GlyphAtlas): number;
1408
+ /**
1409
+ * Stamp the `order` (depth, float-offset 10) of the glyph instances in
1410
+ * `[start, start + count)` to `value`. The owning {@link Layer} calls this at
1411
+ * finalize with depths derived from the layer's UNIFIED push sequence (one
1412
+ * continuous domain across shapes/glyphs/sprites), so a glyph z-interleaves
1413
+ * with the shapes pushed around it. No-op when `count <= 0`. Does NOT bump
1414
+ * {@link version} — `order` is renderer-facing depth metadata, not glyph
1415
+ * geometry, and the layer stamps it every frame regardless.
1416
+ */
1417
+ stampOrder(start: number, count: number, value: number): void;
1418
+ /** Reset for the next frame. Retains allocated capacity. */
1419
+ clear(): this;
1420
+ private _pushFastPath;
1421
+ /**
1422
+ * Look up or compute the origin-relative glyph stream for a single-line
1423
+ * shape. Cache key: `${fontSize}|${align}|${kerning ? 1 : 0}|${text}`.
1424
+ */
1425
+ private _getOrShapeFast;
1426
+ private _pushSlowPath;
1427
+ /**
1428
+ * Ensure capacity for `add` more glyph instances. Returns the float offset
1429
+ * where the new instances start. Re-aliases `dataU32` after any growth so
1430
+ * both views are always consistent (never stale after reallocation).
1431
+ */
1432
+ private _ensureCapacity;
1433
+ }
1434
+ //#endregion
1435
+ //#region src/scene/layer.d.ts
1436
+ /**
1437
+ * Optional `group` tag accepted by every `Layer.pushXxx` method. The group is
1438
+ * forwarded into `UberPack.append` and the renderer transforms the shape's
1439
+ * geometry live by composing the group's resolved transform into a dedicated
1440
+ * per-(space × group) camera slot (P3-T8) — no repack on a group move. Mirrors
1441
+ * v1's `GroupedShape`.
1442
+ */
1443
+ interface GroupedShape {
1444
+ group?: Group;
1445
+ /**
1446
+ * Per-instance emphasis key (Phase 4). Written into the instance's `lane4`;
1447
+ * the uber `transparent` / OIT-build fragments dim every PARTICIPATING instance
1448
+ * whose key does NOT equal the renderer's `Emphasis.focusedKey` uniform (set via
1449
+ * {@link Renderer2D.setEmphasis}). Instances sharing a key emphasize together
1450
+ * (e.g. a whole series).
1451
+ *
1452
+ * Defaults to `0`, which is the EXEMPT / opt-out sentinel: a key-0 instance is
1453
+ * NON-participating and never dimmed, regardless of `t`. An instance OPTS IN by
1454
+ * tagging a key ≥ 1 — so axis / grid / overlay shapes (which leave the default)
1455
+ * stay full-alpha while chart marks dim. With emphasis inactive (`t == 0`) the
1456
+ * key has no visual effect either way. Plot assigns keys in Phase 5.
1457
+ *
1458
+ * A tagged instance (`emphasisKey >= 1`) ALWAYS renders on the transparent
1459
+ * path — dimming is a blend toward the backdrop, which the opaque
1460
+ * (non-blending, discard-based) pipeline cannot express; visually identical
1461
+ * for alpha-1 colors (src-over with a=1 replaces). So an alpha-1 mark that
1462
+ * opts into emphasis is force-classified transparent at push time (see the
1463
+ * `pushXxx` opacity computation) even though its color alpha is 1.
1464
+ */
1465
+ emphasisKey?: number;
1466
+ }
1467
+ interface GroupedRectRecord extends RectRecord, GroupedShape {}
1468
+ interface GroupedCircleRecord extends CircleRecord, GroupedShape {}
1469
+ interface GroupedEllipseRecord extends EllipseRecord, GroupedShape {}
1470
+ interface GroupedSegmentRecord extends SegmentRecord, GroupedShape {}
1471
+ interface GroupedCurveRecord extends CurveRecord, GroupedShape {}
1472
+ interface GroupedArcRecord extends ArcRecord, GroupedShape {}
1473
+ interface GroupedTriangleRecord extends TriangleRecord, GroupedShape {}
1474
+ interface GroupedPolylineShape extends PolylineShape, GroupedShape {}
1475
+ /**
1476
+ * A solid stroke rendered through the analytic SDF `stroke-path` kind
1477
+ * ({@link Layer.pushStroke}) instead of the CPU-tessellated `pushPolyline`
1478
+ * path. Shares `PolylineShape`'s authoring surface (points / color / width /
1479
+ * cap / join / closed / miterLimit) so call sites can opt in by swapping the
1480
+ * method name. `dashPattern` is NOT supported on the SDF path — see
1481
+ * {@link Layer.pushStroke}.
1482
+ */
1483
+ interface GroupedStrokeShape extends PolylineShape, GroupedShape {}
1484
+ /**
1485
+ * Hole-aware filled polygon. `points` is the outer ring (CCW or CW — earcut is
1486
+ * orientation-agnostic), `holes` are inner rings (opposite winding). Mirrors
1487
+ * v1's `ShapePack.appendPolygon` fill path: rings are flattened into one
1488
+ * coordinate array with `holeIndices`, and `earcut(coords, holeIndices)`
1489
+ * triangulates the result. The `{ triangles }` form (pre-triangulated) is the
1490
+ * other accepted overload of `Layer.pushPolygon`.
1491
+ */
1492
+ interface PolygonShape {
1493
+ points: readonly Vec2[];
1494
+ holes?: readonly (readonly Vec2[])[];
1495
+ fill: Color;
1496
+ group?: Group;
1497
+ /**
1498
+ * Per-instance emphasis key (Phase 4) written onto EVERY synthesized triangle
1499
+ * of this polygon — exactly like {@link GroupedPolylineShape} threads one key
1500
+ * across every tessellated segment — so a polygon-filled mark dims as one unit
1501
+ * under the GPU emphasis uniform. Defaults to `0` (EXEMPT). A tagged polygon
1502
+ * (`emphasisKey >= 1`) is force-classified transparent (see {@link GroupedShape}).
1503
+ */
1504
+ emphasisKey?: number;
1505
+ }
1506
+ /** Pre-triangulated polygon: the caller supplies the triangle list directly. */
1507
+ interface TriangulatedPolygon {
1508
+ triangles: readonly TriangleRecord[];
1509
+ group?: Group;
1510
+ /**
1511
+ * Per-instance emphasis key (Phase 4) written onto every supplied triangle.
1512
+ * See {@link PolygonShape.emphasisKey}.
1513
+ */
1514
+ emphasisKey?: number;
1515
+ }
1516
+ /** Text shape plus the optional `group` tag (mirrors v1's `GroupedTextShape`). */
1517
+ interface GroupedTextShape extends GlyphTextShape, GroupedShape {}
1518
+ /**
1519
+ * World-anchored, screen-sized string shape — the input to
1520
+ * {@link Layer.pushAnchoredString}. Mirrors v1's `AnchoredStringShape`: the
1521
+ * anchor is a world coordinate (projected per-frame by the glyph shader) and
1522
+ * `fontSize` / `offsetX` / `offsetY` are CSS px. `x`/`y` are accepted as
1523
+ * fallbacks for the anchor so older `{ x, y }` call sites keep working.
1524
+ */
1525
+ interface GroupedAnchoredStringShape extends GroupedShape {
1526
+ text: string;
1527
+ /** Anchor in world coordinates. Projected through the camera at draw time. */
1528
+ worldX?: number;
1529
+ worldY?: number;
1530
+ /** Fallback anchor (used when `worldX`/`worldY` are absent). */
1531
+ x?: number;
1532
+ y?: number;
1533
+ /** Offset from the projected anchor, in CSS px. Default (0, 0). */
1534
+ offsetX?: number;
1535
+ offsetY?: number;
1536
+ /** Em size in CSS px (not world units). Default 16. */
1537
+ fontSize?: number;
1538
+ /** Fill color. Default BLACK. */
1539
+ color?: Color;
1540
+ /** Horizontal anchor. Default `"left"`. */
1541
+ align?: "left" | "center" | "right";
1542
+ /** CSS-family hint for non-GPU consumers (SVG export); GPU uses the atlas font. */
1543
+ fontFamily?: string;
1544
+ }
1545
+ /**
1546
+ * Bulk single-line string batch for {@link Layer.pushStringsBulkInto} — the v3
1547
+ * port of v1's `BulkStringBatch`. Structure-of-arrays input for the hot
1548
+ * anchored-labels fast path: one append call shapes & packs `count` short
1549
+ * labels into the layer's glyph pack with no per-string object allocation.
1550
+ *
1551
+ * NOTE (Phase-4): v1's `pushStringsBulkInto(pack, batch, options?)` took an
1552
+ * explicit `TextStringPack` as its first arg. v3 has no separate string-pack —
1553
+ * the layer owns one `GlyphPack` — so the first arg is dropped: v3 callers pass
1554
+ * just `(batch, options?)`. The `anchored-labels.ts` call site (which currently
1555
+ * passes `pack`) must drop that argument when it migrates.
1556
+ */
1557
+ interface BulkStringBatch {
1558
+ /** Number of strings in this batch. */
1559
+ count: number;
1560
+ /** World-x per string. Length ≥ count. */
1561
+ x: ArrayLike<number>;
1562
+ /** World-y per string. Length ≥ count. */
1563
+ y: ArrayLike<number>;
1564
+ /** Per-string font size in world units. Length ≥ count. */
1565
+ fontSize: ArrayLike<number>;
1566
+ /** Per-string text. Length ≥ count. */
1567
+ texts: readonly string[];
1568
+ /** Applies to every string in the batch. Mutually exclusive with `colors`. */
1569
+ color?: Color;
1570
+ /** Per-string color override. Length ≥ count when set; mutually exclusive with `color`. */
1571
+ colors?: readonly Color[];
1572
+ /** Horizontal align, shared by all strings. Default `"left"`. */
1573
+ align?: "left" | "center" | "right";
1574
+ }
1575
+ interface LayerOptions {
1576
+ /**
1577
+ * Coordinate space for this layer:
1578
+ * - `"world"` (default): camera-transformed world coordinates.
1579
+ * - `"ui"`: CSS-pixel coordinates; camera ignored (UI overlays, text).
1580
+ *
1581
+ * Raw device px is no longer an authoring space (it is renderer-internal);
1582
+ * for density-aware drawing author in `"ui"` and scale by `devicePixelRatio`.
1583
+ */
1584
+ space?: LayerSpace;
1585
+ /**
1586
+ * Crop this layer to the given device-pixel rectangle (applied as a scissor).
1587
+ * Replaces v1's `clipRect` with a clearer name; `clipRect` is kept as an alias.
1588
+ */
1589
+ clip?: FrameRect;
1590
+ /** Alias for `clip`, kept for v1 back-compat. `clip` wins when both are set. */
1591
+ clipRect?: FrameRect;
1592
+ /**
1593
+ * MSDF glyph atlas backing this layer's text. Required to call `pushText` /
1594
+ * `pushString` / `pushAnchoredString` (mirrors v1's
1595
+ * `renderer.createLayer({ font })`, which mints the layer's `TextPack`). When
1596
+ * omitted the text methods throw — a layer with no atlas cannot shape glyphs.
1597
+ */
1598
+ atlas?: GlyphAtlas;
1599
+ /**
1600
+ * Layer-wide default group (see {@link Layer.group}). Applied to commands
1601
+ * without a per-push group so all of the layer's geometry projects through
1602
+ * the group's transform — the navigator uses this to render its world content
1603
+ * through the overview camera.
1604
+ */
1605
+ group?: Group;
1606
+ /**
1607
+ * Composite z-band for this layer (see {@link Layer.zIndex} for the full
1608
+ * flat-z-band contract). `undefined` (default) → the layer keeps array
1609
+ * insertion order, sorting ABOVE every layer that sets an explicit `zIndex`.
1610
+ */
1611
+ zIndex?: number;
1612
+ /**
1613
+ * Cache policy (see {@link Layer.cache}). Default `"auto"`.
1614
+ */
1615
+ cache?: CacheHint;
1616
+ /**
1617
+ * Optional human-readable label for this layer (see {@link Layer.label}). Pure
1618
+ * metadata — never affects rendering. Surfaced in `FrameDebug.layers` /
1619
+ * `bakeEvents`; absent → a stable `layer#<index-in-render-array>` fallback.
1620
+ */
1621
+ label?: string;
1622
+ /**
1623
+ * Retain a {@link GlyphPushRecord} for every text push without the
1624
+ * {@link GLYPH_PUSH_RECORD_CAP} limit. Used by the static SVG exporter so
1625
+ * every label can emit a semantic <text>. Off by default (the live/GPU path
1626
+ * keeps bounded damage records).
1627
+ */
1628
+ retainAllPushRecords?: boolean;
1629
+ /**
1630
+ * Whether this layer is drawn (see {@link Layer.visible}). Default `true`. A
1631
+ * `false` layer is EXCLUDED from drawing; flipping it demotes the next frame to
1632
+ * a full repaint (folded into the view fingerprint).
1633
+ */
1634
+ visible?: boolean;
1635
+ /**
1636
+ * Optional static debug metadata (see {@link Layer.debugData}). Pure
1637
+ * metadata — NEVER affects rendering. Read only while the debug probe is
1638
+ * capturing and surfaced in `FrameDebug.layers[].debugData` (decision D7).
1639
+ */
1640
+ debugData?: Record<string, unknown>;
1641
+ /**
1642
+ * When `true`, forces all shapes pushed to this layer to be classified as
1643
+ * transparent, routing them into the transparent pass (with depth write disabled).
1644
+ *
1645
+ * **Use Case**:
1646
+ * Useful for layers containing interleaved opaque and transparent primitives (e.g.
1647
+ * glowing cells or charts with status fills) where alternating opacities would
1648
+ * otherwise fragment draw commands and break batching.
1649
+ *
1650
+ * **Caveats**:
1651
+ * - **Fill-rate Cost**: Disables GPU Early-Z occlusion for all shapes on this layer,
1652
+ * running the full fragment shader for hidden/occluded pixels.
1653
+ * - **OIT Buffer Cap**: Under OIT (`config.oit === true`), routing large volumes of
1654
+ * opaque geometry into the transparent pass increases the risk of overflowing the
1655
+ * GPU linked-list A-buffer (default max 8 fragments per pixel), which can cause
1656
+ * dropped pixels or rendering glitches.
1657
+ */
1658
+ forceTransparent?: boolean;
1659
+ /**
1660
+ * When `true` and the renderer runs with OIT enabled (`config.oit`), this
1661
+ * layer is EXEMPT from the per-pixel A-buffer: its shapes/glyphs/sprites skip
1662
+ * the OIT emit sweeps and instead alpha-blend directly into the backbuffer
1663
+ * AFTER the OIT resolve, in painter's order, always ON TOP of the resolved
1664
+ * scene (depth is reset for the exempt draw, so scene geometry never occludes
1665
+ * it).
1666
+ *
1667
+ * **Use Case**: cursor-attached UI overlays (tooltips / crosshairs). The
1668
+ * A-buffer holds at most `oitFragmentsPerPixel` fragments per pixel and
1669
+ * overflow drops fragments in APPEND order — an overlay layer appends LAST,
1670
+ * so over dense transparent marks its fragments are the first dropped and the
1671
+ * overlay renders ghost-faint. Exemption takes the overlay out of the budget
1672
+ * entirely.
1673
+ *
1674
+ * **Caveats**: an exempt layer does not z-interleave with OIT content — it is
1675
+ * unconditionally topmost. With OIT off the flag is inert (the layer draws on
1676
+ * the ordinary depth-stamped fallback path).
1677
+ */
1678
+ oitExempt?: boolean;
1679
+ }
1680
+ /**
1681
+ * How the renderer decides whether to back a layer with a cached RTT snapshot
1682
+ * (Phase 3, decisions.md D5):
1683
+ *
1684
+ * - `"auto"` (default) — a cheap static heuristic (`instanceCount` + text)
1685
+ * decides: expensive layers bake, cheap ones stay live. NOT runtime-measured
1686
+ * (that is Phase 6).
1687
+ * - `"always"` — always bake to an RTT texture and composite the snapshot;
1688
+ * re-bake only when the layer's content or the view changes. Pinned: an
1689
+ * `"always"` layer is exempt from memory-budget eviction.
1690
+ * - `"never"` — always rasterize live; never bake, however expensive.
1691
+ *
1692
+ * The hint is SUBORDINATE to z-soundness (T-ZBAKE). A bake is a translucent image
1693
+ * that can only composite UNDER all live geometry (a leading z-run) or OVER it (a
1694
+ * trailing z-run); it cannot depth-interleave with the live opaque sweep. A layer
1695
+ * whose bake would sit BETWEEN two live layers therefore stays LIVE even under
1696
+ * `"always"` (composite correctness wins) — the cache policy never creates such a
1697
+ * bake, and a manual one is demoted to live for any sandwiched frame. Bakes
1698
+ * accelerate layers at the bottom and top of the z-stack.
1699
+ */
1700
+ type CacheHint = "auto" | "always" | "never";
1701
+ /**
1702
+ * Result of {@link Layer.finalizeOrder} — the per-layer unified z-order domain.
1703
+ *
1704
+ * `total` is N, the layer's instance count across shapes + glyphs + sprites.
1705
+ * `shapeOrderSeq[j]` is the unified push index `i ∈ [0, N)` of the j-th UberPack
1706
+ * shape instance (pack order). PHASE-4: the renderer consumes this to stamp
1707
+ * shape `order = 1 - (i + 0.5)/total` in the SAME domain it now stamps glyph +
1708
+ * sprite order — replacing today's cross-layer shape-only concat domain.
1709
+ */
1710
+ interface OrderFinalization {
1711
+ total: number;
1712
+ shapeOrderSeq: number[];
1713
+ }
1714
+ /**
1715
+ * A bag of shapes backed by a single `UberPack`. Push-methods delegate to the
1716
+ * per-kind packers; `clear()` resets the pack for the next frame.
1717
+ */
1718
+ declare class Layer {
1719
+ readonly space: LayerSpace;
1720
+ /** Device-pixel crop rect, or `undefined` for no clip. */
1721
+ clipRect: FrameRect | undefined;
1722
+ /** The underlying instance buffer + draw-command list. */
1723
+ readonly pack: UberPack;
1724
+ /** MSDF atlas the text methods shape against (`undefined` for shape-only layers). */
1725
+ readonly atlas: GlyphAtlas | undefined;
1726
+ /**
1727
+ * Layer-wide default group: applied to every shape command (live OR retained)
1728
+ * that carries no explicit per-push `group`, so ALL of the layer's geometry
1729
+ * projects through the group's resolved transform. The escape hatch for
1730
+ * content whose pushes don't set a group — e.g. the navigator routes its
1731
+ * world content through an overview-camera group. Mutable so the group's
1732
+ * transform can be re-derived per frame. A per-push `group` still wins.
1733
+ */
1734
+ group: Group | undefined;
1735
+ /**
1736
+ * Composite z-band (decisions.md D3 — the FLAT-Z-BAND contract):
1737
+ *
1738
+ * - A layer is a flat z-band: layer N is entirely ABOVE layer N−1. Inter-
1739
+ * layer compositing is painter's order (a stack of `over`); there is NO
1740
+ * per-shape z-interleave ACROSS layers. A thing that must interleave with
1741
+ * another layer's shapes belongs in the same layer (where intra-layer OIT
1742
+ * resolves overlap) or must be split so the band order is unambiguous.
1743
+ * - Ordering: explicit `zIndex` sorts in ascending numeric order (lower =
1744
+ * drawn first = below). `undefined` keeps array insertion order and sorts
1745
+ * ABOVE all explicit-`zIndex` layers (the default "top" band). Equal keys
1746
+ * (including all-`undefined`) preserve insertion order — the sort is STABLE.
1747
+ * - Mutable: a pure `zIndex` change between frames is a COMPOSITE-only
1748
+ * invalidation (restack/recomposite, NO content re-render). The renderer
1749
+ * folds the z-order into its view fingerprint so a reorder demotes a
1750
+ * partial frame to a clean full repaint rather than ghosting (P2-T3).
1751
+ */
1752
+ zIndex: number | undefined;
1753
+ /**
1754
+ * Cache policy hint (Phase 3, decisions.md D5). The renderer reads this each
1755
+ * frame to decide whether to back the layer with a cached RTT snapshot
1756
+ * (composite the texture) or rasterize it live. See {@link CacheHint}. Mutable:
1757
+ * flipping it between frames re-applies the policy on the next `render()`.
1758
+ * Default `"auto"`.
1759
+ *
1760
+ * The hint never overrides z-order (T-ZBAKE): a bake composites UNDER all live
1761
+ * geometry or OVER it, so a layer whose bake would sit between two live layers
1762
+ * stays live even under `"always"` (composite correctness wins). Caching
1763
+ * accelerates only the bottom and top of the z-stack.
1764
+ */
1765
+ cache: CacheHint;
1766
+ /**
1767
+ * Optional human-readable label. Pure metadata — it NEVER affects rendering.
1768
+ * The renderer surfaces it in `FrameDebug.layers[].label` and the bake-event
1769
+ * lists so tooling can name layers; when absent the renderer falls back to a
1770
+ * stable `layer#<index-in-render-array>` id. Mutable.
1771
+ */
1772
+ label: string | undefined;
1773
+ /**
1774
+ * Whether this layer is drawn (default `true`). A `false` layer is EXCLUDED
1775
+ * from the frame entirely — it is filtered out before the z-sort, so the
1776
+ * concat / cache policy / cull / glyph / sprite paths all skip it consistently.
1777
+ *
1778
+ * Hiding SKIPS COMPOSITING an existing bake (it does NOT drop the bake), so a
1779
+ * quick toggle does not thrash the RTT cache — re-showing composites the same
1780
+ * texture again with no re-bake.
1781
+ *
1782
+ * INVALIDATION: a visibility flip MUST demote the NEXT frame to a full repaint
1783
+ * (a hidden layer's pixels would otherwise linger under partial redraw). The
1784
+ * renderer folds a visibility fingerprint into its view fingerprint (same
1785
+ * pattern as the z-order fold), so any flip forces a clean full frame. Mutable.
1786
+ */
1787
+ visible: boolean;
1788
+ /**
1789
+ * Optional static debug metadata (decision D7). Pure metadata — it NEVER
1790
+ * affects rendering and is READ ONLY while the debug probe is capturing. The
1791
+ * renderer surfaces it (reference copy, not a clone) in
1792
+ * `FrameDebug.layers[].debugData` so a presenter can show static extras
1793
+ * (e.g. plot geom kinds) without re-binding to the layer. Mutable.
1794
+ */
1795
+ debugData: Record<string, unknown> | undefined;
1796
+ /**
1797
+ * When `true`, all shapes pushed to this layer are forced to be classified
1798
+ * as transparent, routing them into the transparent pass.
1799
+ */
1800
+ readonly forceTransparent: boolean;
1801
+ /**
1802
+ * When `true`, this layer skips the OIT A-buffer under an OIT renderer and is
1803
+ * drawn post-resolve, alpha-blended, always on top (see {@link LayerOptions.oitExempt}).
1804
+ */
1805
+ readonly oitExempt: boolean;
1806
+ /**
1807
+ * @internal Lazily-minted glyph instance buffer. `null` until the first
1808
+ * `pushText`/`pushString`/`pushAnchoredString`; the bake path reads it through
1809
+ * the `glyphPack` getter and renders glyphs via the dedicated MSDF glyph
1810
+ * pipeline (`pipelines/glyph-pipelines.ts`), never the uber-shader.
1811
+ */
1812
+ private _glyphPack;
1813
+ private _retainAllPushRecords;
1814
+ /**
1815
+ * @internal Lazily-minted CPU sprite accumulator. `null` until the first
1816
+ * `pushSprite`/`addSprites`. The renderer reads it through the {@link spritePack}
1817
+ * getter and draws each per-texture command through the standalone interop
1818
+ * sprites pipeline POST-MAIN (a textured sibling pass — sprites cannot join the
1819
+ * uber-pack; see `sprite-pack.ts`). Reset by `clear()` / `destroy()`.
1820
+ */
1821
+ private _spritePack;
1822
+ /**
1823
+ * @internal Running union of the world-space content bbox of every glyph
1824
+ * pushed into this layer. `null` until the first text push. Shape AABBs are
1825
+ * NOT tracked here — they live in `pack.aabbs` and are folded in by the
1826
+ * {@link effectiveLocalBounds} getter. Reset by `clear()` / `destroy()`.
1827
+ */
1828
+ private _textBounds;
1829
+ private readonly _orderEvents;
1830
+ /** Pack instance count already folded into a shape event (flush watermark). */
1831
+ private _shapesFlushed;
1832
+ /** Pack instance count (shape instances appended to the UberPack). */
1833
+ private _packInstanceCount;
1834
+ /**
1835
+ * Fold any UberPack instances appended since the last flush into one trailing
1836
+ * shape event, preserving their position relative to interleaved glyph/sprite
1837
+ * pushes. Called before each glyph/sprite event and once at finalize.
1838
+ */
1839
+ private _flushPendingShapes;
1840
+ /** Log a glyph event (call AFTER {@link _flushPendingShapes}). No-op for 0 glyphs. */
1841
+ private _logGlyphs;
1842
+ constructor(pack: UberPack, options?: LayerOptions);
1843
+ /**
1844
+ * @deprecated Use the `pack` field directly. This getter exists only for
1845
+ * call sites that reached `_pack` before it was renamed; it will be removed
1846
+ * once all callers migrate.
1847
+ * @internal
1848
+ */
1849
+ get _pack(): UberPack;
1850
+ /**
1851
+ * This layer's glyph instances, or `null` when the layer has pushed no text.
1852
+ *
1853
+ * The uber-shader has no glyph branch — a layer's glyphs are drawn through the
1854
+ * dedicated MSDF glyph pipeline (`pipelines/glyph-pipelines.ts`), never the
1855
+ * uber-shader. Two render paths consume this pack:
1856
+ *
1857
+ * - LIVE (P3-T2): `Renderer2D.render([layer])` draws the glyphs in the main
1858
+ * pass as a sibling instanced draw — non-OIT in-pass (depth-tested against
1859
+ * shapes) or, under OIT, in a depth-attached pass after the resolve so text
1860
+ * lands above the transparent layer. Anchored glyphs re-project through the
1861
+ * camera every frame, so a pan never repacks them.
1862
+ * - BAKED: `Renderer2D.cacheLayer` → `runBakePass` freezes the glyphs into
1863
+ * the layer's RTT snapshot (`tiers/bake.ts`). A cached layer is composited,
1864
+ * NOT drawn live — the renderer's `_bakes` filter gates EITHER-live-OR-baked.
1865
+ */
1866
+ get glyphPack(): GlyphPack | null;
1867
+ /** Lazily mint the glyph pack on first text push. */
1868
+ private _glyphs;
1869
+ /**
1870
+ * This layer's CPU sprite accumulator, or `null` when no sprite was pushed.
1871
+ *
1872
+ * Sprites do NOT enter the {@link pack} uber-buffer — a textured quad carries
1873
+ * an external texture binding the uber/OIT fragment cannot own (decisions.md
1874
+ * §unifying-rule). The renderer reads this pack after the main pass and draws
1875
+ * each per-texture command through the standalone interop sprites pipeline
1876
+ * (camera-projected, depth/tint/rotation), stacking by submission order — the
1877
+ * same post-main slot as BufferLayer / CustomDrawable. `@internal` for the
1878
+ * renderer; consumers use {@link pushSprite}.
1879
+ * @internal
1880
+ */
1881
+ get spritePack(): SpritePack | null;
1882
+ /** Lazily mint the sprite pack on first sprite push. */
1883
+ private _sprites;
1884
+ /** Require the atlas configured at construction, or throw with v1's guidance. */
1885
+ private _requireAtlas;
1886
+ pushRect(shape: GroupedRectRecord): this;
1887
+ pushCircle(shape: GroupedCircleRecord): this;
1888
+ pushEllipse(shape: GroupedEllipseRecord): this;
1889
+ pushSegment(shape: GroupedSegmentRecord): this;
1890
+ /**
1891
+ * Back-compat alias for v1's `pushLine`. v1's `LineShape` is just a straight
1892
+ * stroked segment, so it collapses onto `pushSegment` in v3.
1893
+ */
1894
+ pushLine(shape: GroupedSegmentRecord): this;
1895
+ pushCurve(shape: GroupedCurveRecord): this;
1896
+ pushArc(shape: GroupedArcRecord): this;
1897
+ pushTriangle(shape: GroupedTriangleRecord): this;
1898
+ /**
1899
+ * Stroke a polyline. The shape is tessellated on the CPU (port of v1's
1900
+ * `tessellatePolyline` — caps butt/round/square, joins miter/bevel/round,
1901
+ * miter-limit fallback, dashes, closed loops) into a triangle list, and each
1902
+ * triangle is appended through the `triangle` kind so adjacent same-fill
1903
+ * triangles merge into one draw command. The polyline's `color` becomes each
1904
+ * triangle's `fill`. Mirrors v1's `Layer.pushPolyline`; the optional `group`
1905
+ * tag is forwarded to every synthesized triangle so the whole polyline shares
1906
+ * one per-(space × group) camera slot and transforms live (see the class doc).
1907
+ */
1908
+ pushPolyline(shape: GroupedPolylineShape): this;
1909
+ /**
1910
+ * Stroke a polyline through the analytic SDF `stroke-path` kind — the opt-in
1911
+ * alternative to {@link pushPolyline}'s CPU triangulation. The polyline is
1912
+ * chunked into `ceil((M-1)/3)` instances of up to 4 points each (3 live
1913
+ * sub-segments per instance) via {@link chunkStroke}; the fragment shader
1914
+ * computes coverage with a min-of-segment SDF and analytic AA. For dense
1915
+ * branch lines this collapses ~26 triangle instances per 8-point curve down to
1916
+ * 3 stroke instances (≈8.7×), eliminating the per-triangle `pack.append` cost.
1917
+ *
1918
+ * All instances of one stroke share kind + group, so they self-merge into ONE
1919
+ * draw command. The `group` and `emphasisKey` tags are forwarded to every
1920
+ * synthesized instance (like {@link pushPolyline}), and opacity is classified
1921
+ * identically (`colorOpaque(color)` unless tagged).
1922
+ *
1923
+ * SCOPE: solid strokes only. `dashPattern` is NOT supported on the SDF path —
1924
+ * a dashed shape THROWS here; route dashes through {@link pushPolyline}
1925
+ * (CPU-tessellated) instead.
1926
+ *
1927
+ * GUARANTEES (GPU pixel-parity verified): round joins render exactly, and
1928
+ * round/butt caps render exactly. Square caps are APPROXIMATE at the corners
1929
+ * (the analytic clip rounds-then-flattens the corner rather than producing the
1930
+ * tessellator's true square extension). Miter/bevel joins are NOT rendered by
1931
+ * the SDF kind: a single instance cannot see the join angle at a point that
1932
+ * lives on its own chunk/closure boundary, so those seam vertices would render
1933
+ * round. Such shapes are DELEGATED to the tessellated {@link pushPolyline}
1934
+ * path, which is exact. (Full analytic miter/bevel seam joins are a tracked
1935
+ * follow-up requiring a neighbor-point encoding in the instance.)
1936
+ *
1937
+ * JOIN ROUTING: the polyline default join is `"miter"` (see
1938
+ * `tessellatePolyline`: `shape.join ?? "miter"`). An UNDEFINED `join` therefore
1939
+ * means miter and is routed to {@link pushPolyline} as well — the SDF kind is
1940
+ * used ONLY when `join` is explicitly `"round"`. After this guard the stroke
1941
+ * kind only ever receives round joins.
1942
+ *
1943
+ * KEEPS {@link pushPolyline} as the default, unchanged — this is purely additive.
1944
+ */
1945
+ pushStroke(shape: GroupedStrokeShape): this;
1946
+ /** Bulk SDF-stroke append. Mirrors {@link addLines} for {@link pushStroke}. */
1947
+ addStrokes(shapes: readonly GroupedStrokeShape[], options?: {
1948
+ group?: Group;
1949
+ }): this;
1950
+ /**
1951
+ * A polygon is rendered as a triangle list. Two forms are accepted:
1952
+ *
1953
+ * - `{ triangles }` — pre-triangulated. Each triangle is appended directly
1954
+ * (unchanged from the original v3 API).
1955
+ * - `{ points, holes? }` — earcut-triangulated hole-aware on the CPU,
1956
+ * mirroring v1's `ShapePack` fill path (flatten outer ring + hole rings
1957
+ * into one coords array with `holeIndices`, call `earcut`). The polygon's
1958
+ * `fill` becomes each triangle's fill.
1959
+ *
1960
+ * Adjacent same-fill triangles merge into one draw command.
1961
+ */
1962
+ pushPolygon(shape: TriangulatedPolygon | PolygonShape): this;
1963
+ addRects(shapes: readonly GroupedRectRecord[], options?: {
1964
+ group?: Group;
1965
+ }): this;
1966
+ addCircles(shapes: readonly GroupedCircleRecord[], options?: {
1967
+ group?: Group;
1968
+ }): this;
1969
+ addEllipses(shapes: readonly GroupedEllipseRecord[], options?: {
1970
+ group?: Group;
1971
+ }): this;
1972
+ /** Bulk straight-segment append. Mirrors v1's `addLines` (LineShape collapses onto `pushSegment`). */
1973
+ addLines(shapes: readonly GroupedSegmentRecord[], options?: {
1974
+ group?: Group;
1975
+ }): this;
1976
+ addTriangles(shapes: readonly GroupedTriangleRecord[], options?: {
1977
+ group?: Group;
1978
+ }): this;
1979
+ addPolygons(shapes: readonly PolygonShape[], options?: {
1980
+ group?: Group;
1981
+ }): this;
1982
+ /**
1983
+ * Bulk-append `opts.count` segments from a pre-packed v1-stride buffer
1984
+ * (`SEGMENT_FLOATS` floats per record: `[x1, y1, x2, y2, colorBits, width]`).
1985
+ * Repacked into v3's 16-float instance layout and submitted as one draw
1986
+ * command. Byte-identical to looping {@link pushSegment} over the records.
1987
+ */
1988
+ addSegmentsPacked(data: ArrayLike<number>, opts: {
1989
+ count: number;
1990
+ opaque: boolean;
1991
+ group?: Group;
1992
+ }): this;
1993
+ /**
1994
+ * Bulk-append `opts.count` cubic curves from a pre-packed v1-stride buffer
1995
+ * (`CURVE_FLOATS` floats per record: four control points, then `colorBits`,
1996
+ * `widthPacked` (pack2x16float), `flagsBits` (cap in low 2 bits), and a pad).
1997
+ * Byte-identical to looping {@link pushCurve} over the records.
1998
+ */
1999
+ addCurvesPacked(data: ArrayLike<number>, opts: {
2000
+ count: number;
2001
+ opaque: boolean;
2002
+ group?: Group;
2003
+ }): this;
2004
+ /**
2005
+ * Bulk-append `opts.count` circular arcs from a pre-packed v1-stride buffer
2006
+ * (`ARC_FLOATS` floats per record: `[cx, cy, radius, theta0, theta1, width,
2007
+ * colorBits, _pad]`). Byte-identical to looping {@link pushArc} over the
2008
+ * records.
2009
+ */
2010
+ addArcsPacked(data: ArrayLike<number>, opts: {
2011
+ count: number;
2012
+ opaque: boolean;
2013
+ group?: Group;
2014
+ }): this;
2015
+ /**
2016
+ * Append one textured sprite (port of v1's `Layer.pushSprite`). Adjacent
2017
+ * sprites sharing a texture coalesce into one draw command. Returns `this` for
2018
+ * chaining. The sprite draws above this layer's shape geometry by submission.
2019
+ */
2020
+ pushSprite(sprite: SpriteShape): this;
2021
+ /** Append many sprites (port of v1's `Layer.addSprites`). */
2022
+ addSprites(sprites: readonly SpriteShape[]): this;
2023
+ /**
2024
+ * Shape `shape.text` and append its glyphs to this layer's glyph pack. Mirrors
2025
+ * v1's `Layer.pushText` — full shaper (kerning, multi-line, wrap) unless
2026
+ * `flags.needsShaping` / `shape.simple` opt into the fast path. Returns the
2027
+ * layout metrics (glyph count + world bbox).
2028
+ */
2029
+ pushText(shape: GroupedTextShape, flags?: GlyphPackFlags): GlyphMetricsOut;
2030
+ /**
2031
+ * Fast single-line, no-kerning text append. Mirrors v1's `Layer.pushString`
2032
+ * (`TextStringPack.pushString`): equivalent to `pushText` with
2033
+ * `{ needsShaping: false }`. Returns `this` for chaining.
2034
+ */
2035
+ pushString(shape: GroupedTextShape): this;
2036
+ /**
2037
+ * World-anchored text append. Mirrors v1's `Layer.pushAnchoredString`: the
2038
+ * anchor stays in world coordinates (no CPU camera projection) and the
2039
+ * vertex shader projects it per-frame, so a pan never repacks the glyphs.
2040
+ * Sets the `anchored` flag (lane2 bit 0).
2041
+ */
2042
+ pushAnchoredString(shape: GroupedAnchoredStringShape, flags?: GlyphPackFlags): this;
2043
+ /**
2044
+ * Bulk single-line label fast path (port of v1's `Layer.pushStringsBulkInto`).
2045
+ * Shapes & packs every string in `batch` into this layer's glyph pack with no
2046
+ * per-string object allocation — the structure-of-arrays input is walked once.
2047
+ *
2048
+ * v1 took an explicit `TextStringPack` as the first argument; v3 has a single
2049
+ * layer-owned `GlyphPack`, so that argument is dropped (see {@link BulkStringBatch}).
2050
+ * Returns `this` for chaining. The optional `group` is accepted for call-site
2051
+ * parity but, like all v3 text, glyphs carry no per-instance group.
2052
+ */
2053
+ pushStringsBulkInto(batch: BulkStringBatch, _options?: {
2054
+ group?: Group;
2055
+ }): this;
2056
+ /**
2057
+ * Finalize this layer's UNIFIED z-order for a frame. Walks the per-push event
2058
+ * log in true interleaved order, assigns every instance — shape, glyph,
2059
+ * sprite — a monotonic index `i ∈ [0, N)` (N = total instances across the
2060
+ * three packs), and stamps `order = 1 - (i + 0.5)/N ∈ (0, 1)` into the GLYPH
2061
+ * and SPRITE packs so they z-interleave with the shapes pushed around them.
2062
+ *
2063
+ * The returned `shapeOrderSeq` is the unified index `i` of each UberPack shape
2064
+ * instance, in pack order, and `total` is N.
2065
+ *
2066
+ * PHASE-4: shape `order` is still stamped by the renderer's gather
2067
+ * (`renderer.ts`, `ORDER_FLOAT_OFFSET`) over the CROSS-LAYER concat with
2068
+ * N = total shapes across ALL layers — a DIFFERENT domain from this per-layer
2069
+ * unified one. Until the renderer adopts this layer-local domain (consuming
2070
+ * {@link OrderFinalization.shapeOrderSeq} + {@link OrderFinalization.total}),
2071
+ * shapes and glyphs/sprites do NOT yet share one continuous domain at draw
2072
+ * time. This method is the single source of truth for that reconciliation.
2073
+ *
2074
+ * Idempotent within a frame: re-deriving `order` from the same event log
2075
+ * yields the same values. The glyph/sprite CPU stamp is done here regardless
2076
+ * so a glyph/sprite pack always carries live depth; wiring the renderer to
2077
+ * call this + consume `shapeOrderSeq` is the PHASE-4 follow-up.
2078
+ */
2079
+ finalizeOrder(): OrderFinalization;
2080
+ /** Reset the pack (and glyph pack) for the next frame. Capacity is reused. */
2081
+ clear(): this;
2082
+ /**
2083
+ * Set this layer's device-pixel crop rect (port of v1's `Layer.setClipRect`).
2084
+ * Pass `undefined` to disable clipping. Sets the {@link clipRect} field and
2085
+ * returns `this` for chaining. `mount`'s `applyPipelineFrames` calls this each
2086
+ * frame to confine marks to the plot frame.
2087
+ */
2088
+ setClipRect(rect: FrameRect | undefined): this;
2089
+ /**
2090
+ * Number of SDF-shape instances (rect / circle / ellipse) packed into this
2091
+ * layer. Port of v1's `Layer.shapeCount`. Counts only the closed SDF shapes —
2092
+ * triangles are reported by {@link triangleCount}; segments / curves / arcs /
2093
+ * glyphs are not included (matching v1's `ShapePack.shapeCount`).
2094
+ */
2095
+ get shapeCount(): number;
2096
+ /**
2097
+ * Number of triangle instances packed into this layer (port of v1's
2098
+ * `Layer.triangleCount`). Polygons and tessellated polylines each append one
2099
+ * triangle instance per tri, so this is their combined tri count.
2100
+ */
2101
+ get triangleCount(): number;
2102
+ /**
2103
+ * Effective local-space AABB of this layer's content, or `null` when empty
2104
+ * (port of v1's `Layer.effectiveLocalBounds`). The union of every packed
2105
+ * shape's world AABB (read from `pack.aabbs`, which {@link UberPack} captures
2106
+ * at pack time) plus every pushed glyph block's content bbox. Used by `mount`
2107
+ * and `PhyloScene` to derive overlay damage rects.
2108
+ *
2109
+ * Bounds are in the layer's local shape space, ignoring any per-shape group
2110
+ * transform — matching v1.
2111
+ */
2112
+ get effectiveLocalBounds(): WorldAABB | null;
2113
+ /**
2114
+ * Drop CPU-side buffers held by this layer (port of v1's `Layer.destroy`).
2115
+ * After `destroy()` the layer should not be pushed to or rendered again.
2116
+ * Anchored-labels and plot's mount teardown call this. Renderer-side state
2117
+ * keyed off the layer lives in a `WeakMap` and is reclaimed once the caller
2118
+ * drops their reference.
2119
+ */
2120
+ destroy(): void;
2121
+ /** Fold a glyph block's content bbox into the running text-bounds union. */
2122
+ private _expandTextBounds;
2123
+ }
2124
+ /** Mint a fresh `Layer` over a new `UberPack`. */
2125
+ declare function createLayer(options?: LayerOptions): Layer;
2126
+ //#endregion
2127
+ //#region src/debug/space-map.d.ts
2128
+ /** A coordinate space a debug rect may be expressed in (decision D2). */
2129
+ type DebugSpace = "world" | "ui" | "device" | "css-layout";
2130
+ /** A 2D size in pixels (units depend on the space it describes). */
2131
+ interface DebugSize {
2132
+ readonly w: number;
2133
+ readonly h: number;
2134
+ }
2135
+ /**
2136
+ * Per-frame snapshot of the coordinate spaces the debug tooling converts between.
2137
+ *
2138
+ * - `dpr` — device-pixel ratio (`device = ui × dpr`).
2139
+ * - `deviceSize` — backing-store size in device px (`canvas.width/height`).
2140
+ * - `uiSize` — `deviceSize ÷ dpr`; the camera's projection target and the space
2141
+ * core regions are reported in.
2142
+ * - `cssBox` — the canvas CSS layout box (`clientWidth/clientHeight`), or `null`
2143
+ * when unknown (headless tests / no DOM). Used only to map ui ↔ css-layout.
2144
+ * - `worldToUi` — base world-camera view matrix into ui px (= `viewFromCamera`
2145
+ * over the CSS-px viewport, matching the draw/scissor path).
2146
+ */
2147
+ interface SpaceMap {
2148
+ readonly dpr: number;
2149
+ readonly deviceSize: DebugSize;
2150
+ readonly uiSize: DebugSize;
2151
+ readonly cssBox: DebugSize | null;
2152
+ readonly worldToUi: Mat3;
2153
+ }
2154
+ /** An axis-aligned rect (no space tag — the caller passes `from`/`to`). */
2155
+ interface RectXYWH {
2156
+ readonly x: number;
2157
+ readonly y: number;
2158
+ readonly width: number;
2159
+ readonly height: number;
2160
+ }
2161
+ /**
2162
+ * Build a {@link SpaceMap} from plain inputs — pure, no DOM access. The renderer
2163
+ * (P1-T2) gathers `camera` / sizes / `dpr` / `cssBox` and calls this only while
2164
+ * the probe is enabled (decision D8). `worldToUi` is the canonical
2165
+ * `worldToCssMatrix(camera, deviceSize.w, deviceSize.h, dpr)` — `viewFromCamera`
2166
+ * over the CSS-px viewport (`device ÷ dpr`). The renderer's world camera
2167
+ * baseline is CSS px and dpr is applied once matrix-side when drawing to the
2168
+ * device backbuffer, so this is the exact world → ui map of what landed on
2169
+ * screen, including under HiDPI.
2170
+ */
2171
+ declare function captureSpaceMap(inputs: {
2172
+ camera: ResolvedCameraState;
2173
+ deviceSize: DebugSize;
2174
+ dpr: number;
2175
+ cssBox?: DebugSize | null;
2176
+ }): SpaceMap;
2177
+ /**
2178
+ * Convert `rect` from space `from` to space `to`, routing through ui as the hub.
2179
+ *
2180
+ * - world ↔ ui via `worldToUi` (four corners min/max — correct under rotation).
2181
+ * - ui ↔ device via `dpr`.
2182
+ * - ui ↔ css-layout via `cssBox / uiSize` per-axis (identity when `cssBox` null).
2183
+ *
2184
+ * Same-space conversion returns the rect unchanged.
2185
+ */
2186
+ declare function convertRect(rect: RectXYWH, from: DebugSpace, to: DebugSpace, map: SpaceMap): RectXYWH;
2187
+ //#endregion
2188
+ //#region src/config.d.ts
2189
+ /**
2190
+ * Per-frame timing/diagnostics record emitted via {@link RendererConfig.onFrameTiming}.
2191
+ *
2192
+ * The `totalMs`/`uploadMs`/`encodeMs`/`drawCalls`/`damageRects` fields are the
2193
+ * v3 canonical surface. The trailing `cpuMs`/`renderNs`/`computeNs` getters are
2194
+ * v1-back-compat ALIASES (P3-T7) so Phase-4 consumers reading the v1
2195
+ * `FrameTiming` shape keep working without a code change; build the record with
2196
+ * {@link frameTiming} so the aliases stay derived (single source of truth — the
2197
+ * canonical fields) rather than separately settable.
2198
+ */
2199
+ interface FrameTiming {
2200
+ /** Wall-clock ms for the full render call. */
2201
+ totalMs: number;
2202
+ /** ms spent in GPU upload (writeBuffer calls). */
2203
+ uploadMs: number;
2204
+ /** ms spent encoding + submitting (createCommandEncoder → queue.submit). */
2205
+ encodeMs: number;
2206
+ /** Number of draw calls issued this frame. */
2207
+ drawCalls: number;
2208
+ /** Number of damage rects processed (0 = full frame). */
2209
+ damageRects: number;
2210
+ /** v1 alias of {@link totalMs} — wall-clock CPU ms of the render call. */
2211
+ readonly cpuMs: number;
2212
+ /**
2213
+ * v1 alias: CPU-side encode time in NANOSECONDS (`encodeMs * 1e6`), as a
2214
+ * `bigint` to match v1's `renderNs` type. v3 does not run a GPU
2215
+ * timestamp-query, so this is the closest CPU analog of v1's GPU render-pass
2216
+ * duration, NOT a true GPU measurement.
2217
+ */
2218
+ readonly renderNs: bigint;
2219
+ /**
2220
+ * v1 alias: always `0n`. The v3 `render()` spine issues no compute passes
2221
+ * (v1's `computeNs` summed `renderer.compute()` passes, which v3's render path
2222
+ * has no equivalent of), so there is nothing to report.
2223
+ */
2224
+ readonly computeNs: bigint;
2225
+ }
2226
+ /**
2227
+ * Build a {@link FrameTiming} from the v3 canonical fields, deriving the v1
2228
+ * back-compat aliases (`cpuMs`/`renderNs`/`computeNs`) as getters over them so
2229
+ * the canonical fields stay the single source of truth. See {@link FrameTiming}
2230
+ * for the alias mapping rationale.
2231
+ */
2232
+ declare function frameTiming(canonical: {
2233
+ totalMs: number;
2234
+ uploadMs: number;
2235
+ encodeMs: number;
2236
+ drawCalls: number;
2237
+ damageRects: number;
2238
+ }): FrameTiming;
2239
+ /**
2240
+ * A named PART of the composite view fingerprint (decision D1/render-debug v2).
2241
+ * When a `"view-changed"` full frame fires while the probe is enabled, the
2242
+ * renderer diffs the previous frame's parts against this frame's and reports the
2243
+ * subset that actually changed in {@link FrameDebug.fullCause}`.changed`. The
2244
+ * `"emphasis"` part is folded in by P4-T1 (emphasis-in-fingerprint, D6).
2245
+ */
2246
+ type FingerprintPart = "camera" | "size" | "dpr" | "background" | "view-key" | "z-order" | "visibility" | "buffer-layer" | "emphasis";
2247
+ /**
2248
+ * A space-tagged axis-aligned rect crossing the debug boundary (decision D2).
2249
+ * Core's damage regions are reported in `"ui"` (device ÷ dpr); a presenter
2250
+ * converts to its own space only via {@link convertRect} + the frame's
2251
+ * {@link SpaceMap}.
2252
+ */
2253
+ interface DebugRect {
2254
+ /** The coordinate space `x/y/width/height` are expressed in. */
2255
+ space: DebugSpace;
2256
+ x: number;
2257
+ y: number;
2258
+ width: number;
2259
+ height: number;
2260
+ }
2261
+ /**
2262
+ * Per-layer diagnostic snapshot inside a {@link FrameDebug.layers} array, in
2263
+ * the frame's z-sorted DRAW order (`orderLayersByZ`, NOT insertion order). One
2264
+ * entry per `Layer` passed to `render()` (BufferLayers / CustomDrawables are not
2265
+ * Layers and are excluded). Allocated only when the debug probe is capturing —
2266
+ * never on the zero-cost path.
2267
+ *
2268
+ * Carries a DIRECT {@link Layer} reference (`layer`) so a presenter binds to the
2269
+ * live object by identity — there is no label-then-index re-binding heuristic
2270
+ * (the v1 `matchRows` footgun, decision D1).
2271
+ */
2272
+ interface LayerDebugInfo {
2273
+ /** Direct reference to the live `Layer` (identity — not a clone). */
2274
+ layer: Layer;
2275
+ /** The layer's {@link Layer.label}, or a stable fallback `layer#<insertion-index>`. */
2276
+ label: string;
2277
+ /** The layer's composite z-band, or `undefined` (top band — array order). */
2278
+ zIndex: number | undefined;
2279
+ /** Coordinate space (`"world" | "ui"`). */
2280
+ space: LayerSpace;
2281
+ /** {@link Layer.visible} — `false` layers are excluded from drawing. */
2282
+ visible: boolean;
2283
+ /** Packed shape-instance count (`pack.byteLength / INSTANCE_BYTES`). */
2284
+ instances: number;
2285
+ /** Packed glyph count (`glyphPack?.count ?? 0`). */
2286
+ glyphs: number;
2287
+ /**
2288
+ * Static cost estimate (the `"auto"` cache heuristic's `_costEstimate`:
2289
+ * `instances + glyphs * GLYPH_COST_WEIGHT`).
2290
+ */
2291
+ cost: number;
2292
+ /**
2293
+ * Where the layer landed in the frame's draw plan (derived from the T-ZBAKE
2294
+ * bake partition + visibility filter):
2295
+ *
2296
+ * - `"hidden"` — `visible === false`; excluded from drawing.
2297
+ * - `"live"` — rasterized live this frame (no bake, or a demoted bake).
2298
+ * - `"baked-below"` — composited UNDER all live geometry (leading z-run bake).
2299
+ * - `"baked-above"` — composited OVER all live geometry (trailing z-run bake).
2300
+ * - `"bake-demoted"`— has a bake but sat sandwiched between live layers, so it
2301
+ * was demoted to live for this frame (T-ZBAKE).
2302
+ */
2303
+ cacheState: "live" | "baked-below" | "baked-above" | "bake-demoted" | "hidden";
2304
+ /**
2305
+ * The layer's {@link Layer.debugData} metadata (reference copy, not a clone),
2306
+ * or `undefined`. Wired in P1-T5; surfaced here so a presenter can read static
2307
+ * geom-kind extras without re-binding to the layer.
2308
+ */
2309
+ debugData?: Record<string, unknown>;
2310
+ }
2311
+ /**
2312
+ * Per-frame DEBUG record (render-debug v2) — the single rich event the core
2313
+ * `renderer.debug` probe dispatches each frame. The history ring stores leaner
2314
+ * SUMMARIES of these (decision D3); the live event carries direct {@link Layer}
2315
+ * references + the {@link SpaceMap}.
2316
+ *
2317
+ * Replaces the v1 `FrameDebug` (deleted, not deprecated — decision D1). The key
2318
+ * upgrades: `fullCause.changed[]` (fingerprint-PART diff, not just an umbrella
2319
+ * reason), space-tagged `regionsCaller`/`regionsFinal`, direct layer refs, a
2320
+ * per-frame {@link SpaceMap}, and cache/emphasis detail.
2321
+ *
2322
+ * ZERO-COST-WHEN-OFF (HARD requirement, mirrors `insomni-profiler`): when the
2323
+ * probe is not capturing, the renderer constructs NO record, retains NO previous
2324
+ * fingerprint parts, reads NO `cssBox` from the DOM, and builds NO `layers` /
2325
+ * region arrays — the entire assembly is guarded behind the capture check.
2326
+ */
2327
+ interface FrameDebug {
2328
+ /** Monotonic frame index (`Renderer2D` `_frameIndex`). */
2329
+ frame: number;
2330
+ /** `"full"` (loadOp:"clear" whole-canvas repaint) or `"partial"` (scissored). */
2331
+ kind: "full" | "partial";
2332
+ /**
2333
+ * Why this frame was FULL (only set when `kind === "full"`). `reason` is the
2334
+ * kebab-case umbrella vocabulary (`"no-regions"`, `"bake-created"`,
2335
+ * `"force-full"`, `"view-changed"`, …); on a `"view-changed"` full while the
2336
+ * probe is capturing, `changed` lists the fingerprint PARTS that actually moved
2337
+ * (`undefined` for every other reason).
2338
+ */
2339
+ fullCause?: {
2340
+ reason: string;
2341
+ changed?: readonly FingerprintPart[];
2342
+ };
2343
+ /**
2344
+ * Damage rects exactly as the caller passed them, space-tagged `"ui"`. Empty
2345
+ * on a `damage: "auto"` frame — the caller passed no rects there; the
2346
+ * core-derived ones appear in `regionsFinal` (+ `autoDamage` provenance).
2347
+ */
2348
+ regionsCaller: readonly DebugRect[];
2349
+ /** Damage rects post-pad/clamp (what the GPU scissored), space-tagged `"ui"`. */
2350
+ regionsFinal: readonly DebugRect[];
2351
+ /**
2352
+ * Provenance for CORE-derived damage (`damage: "auto"`) — which layer produced
2353
+ * which rects, BEFORE coalesce (so a tick can be attributed to the mark that
2354
+ * moved). Space-tagged `"ui"`. Present only on an auto PARTIAL frame; absent on
2355
+ * caller-`regions`, full, and non-auto frames.
2356
+ */
2357
+ autoDamage?: {
2358
+ perLayer: readonly {
2359
+ label: string;
2360
+ rects: readonly DebugRect[];
2361
+ }[];
2362
+ };
2363
+ /**
2364
+ * Per-frame coordinate-space snapshot (decision D2). `cssBox` is read from the
2365
+ * canvas `clientWidth/clientHeight` ONLY here and only while capturing.
2366
+ */
2367
+ spaces: SpaceMap;
2368
+ /** One entry per `Layer` passed to `render()`, in z-sorted draw order. */
2369
+ layers: readonly LayerDebugInfo[];
2370
+ /** Cached-layer (bake) detail this frame. */
2371
+ cache: {
2372
+ /** `config.layerCacheBudgetBytes` (0 = unlimited). */budgetBytes: number; /** Sum of resident bake texture bytes. */
2373
+ usedBytes: number; /** One entry per live bake. */
2374
+ bakes: readonly {
2375
+ label: string;
2376
+ bytes: number;
2377
+ state: string;
2378
+ lastChangeFrame: number;
2379
+ }[]; /** Synthetic ids of bakes evicted this frame (records already gone). */
2380
+ evicted: readonly string[]; /** Spaces of bakes DEMOTED to live this frame (T-ZBAKE). */
2381
+ demoted: readonly string[];
2382
+ };
2383
+ /** Decoded emphasis uniform mirror (`setEmphasis` state). */
2384
+ emphasis: {
2385
+ focusedKey: number;
2386
+ dimAlpha: number;
2387
+ t: number;
2388
+ };
2389
+ /**
2390
+ * Caller-supplied annotation (decision D7). Wired in P1-T5 — declared now so
2391
+ * the shape is stable. `reason` is the caller's intent next to core's decision;
2392
+ * `data` is freeform.
2393
+ */
2394
+ annotation?: {
2395
+ reason?: string;
2396
+ data?: Record<string, unknown>;
2397
+ };
2398
+ /** Wall-clock ms for the full render call. */
2399
+ totalMs: number;
2400
+ /** ms spent in GPU upload (writeBuffer calls). */
2401
+ uploadMs: number;
2402
+ /** ms spent encoding + submitting. */
2403
+ encodeMs: number;
2404
+ /** Number of draw calls issued this frame. */
2405
+ drawCalls: number;
2406
+ /** Number of damage rects processed (0 = full frame). */
2407
+ damageRects: number;
2408
+ }
2409
+ /** Construction-time configuration for the v3 renderer. */
2410
+ interface RendererConfig {
2411
+ oit: boolean;
2412
+ oitFragmentsPerPixel: number;
2413
+ /**
2414
+ * When `navigator.deviceMemory` (GiB) is below this threshold, OIT is
2415
+ * automatically disabled even if `oit: true`. Default `2` GiB.
2416
+ * Set to `0` to disable the auto-check. Non-standard API: absent on Firefox;
2417
+ * treated as "unknown / assume sufficient" when missing.
2418
+ */
2419
+ autoDisableOitBelowMemoryGiB?: number;
2420
+ partialRedraw: boolean;
2421
+ /**
2422
+ * Frustum cull: when `true` (the default), the render spine narrows each
2423
+ * `world`-space draw command to the contiguous sub-ranges whose per-instance
2424
+ * AABB intersects the viewport frustum, emitting split draws against the same
2425
+ * concat buffer (no compaction) and stamping depth over survivors only.
2426
+ * `ui` / `device` chrome is never frustum-culled. When `false` the cull stage
2427
+ * is skipped entirely and every command issues one full-span draw (the path is
2428
+ * byte-identical to the pre-cull behavior). Read LIVE per frame in the spine
2429
+ * (like `partialRedraw`), so a runtime toggle takes effect on the next render
2430
+ * — it is NOT seeded into a cached `_useCull` field.
2431
+ */
2432
+ cull: boolean;
2433
+ msaaBakes: 1 | 4;
2434
+ persistent: boolean;
2435
+ /**
2436
+ * Total resident memory budget (bytes) for renderer-managed cached-layer RTT
2437
+ * textures (P3-T3). When a new `"auto"`/`"always"` bake would push the total
2438
+ * cached-texture bytes over this cap, the policy evicts the least-valuable
2439
+ * auto-managed bake(s) first (lowest `cost × staleness`); `"always"` layers and
2440
+ * explicit `cacheLayer()` bakes are pinned (never auto-evicted). Read LIVE per
2441
+ * frame. `0` (or negative) disables the budget (unlimited). Default 128 MiB
2442
+ * (≈ eight 4096×2048 RGBA8 snapshots). Manual `cacheLayer()` is not counted
2443
+ * against the budget for eviction decisions beyond its resident bytes.
2444
+ */
2445
+ layerCacheBudgetBytes: number;
2446
+ /**
2447
+ * Initial clear color, applied once at construction to seed the live
2448
+ * `_background` (mutate at runtime via `setBackground`). Default
2449
+ * fully-transparent.
2450
+ */
2451
+ initialBackground: Color;
2452
+ /**
2453
+ * Initial CSS→device pixel ratio, applied once at construction to seed the
2454
+ * live `_dpr` (mutate at runtime via `setDpr`). The renderer NEVER reads
2455
+ * `window.devicePixelRatio` itself — the app opts in via `detectDpr()` +
2456
+ * `setDpr()` (HARD boundary 4). Default `1`.
2457
+ */
2458
+ initialDpr: number;
2459
+ /**
2460
+ * Cap applied to `detectDpr()` results. Set to `2` for mobile to limit
2461
+ * fill cost (fragment cost scales as DPR²). Callers pass this to
2462
+ * `watchDpr(renderer, canvas, { maxDpr })`. Default `undefined` (no cap).
2463
+ */
2464
+ maxDpr?: number;
2465
+ /**
2466
+ * Color-target format the bake + composite pipelines are built at. CONSTRAINED
2467
+ * (HARD boundary 3): it MUST match the injected pipelines AND the swap-chain
2468
+ * format. It does NOT configure the swap chain — that always uses
2469
+ * `navigator.gpu.getPreferredCanvasFormat()`. `undefined` means "default to
2470
+ * the swap-chain's preferred format" (resolved at construction).
2471
+ */
2472
+ colorFormat?: GPUTextureFormat;
2473
+ onFrameTiming?: (t: FrameTiming) => void;
2474
+ onGpuFrameTiming?: (ms: number) => void;
2475
+ }
2476
+ /** Default v3 renderer configuration (OIT + partial redraw + frustum cull enabled + 4× MSAA bakes on). */
2477
+ declare const DEFAULT_CONFIG: Readonly<RendererConfig>;
2478
+ //#endregion
2479
+ //#region src/debug/frame-ring.d.ts
2480
+ /** Capacity of the core history ring (decision D3). */
2481
+ declare const FRAME_RING_CAPACITY = 240;
2482
+ /** A scalar per-layer row in a {@link FrameSummary} (no `Layer` ref). */
2483
+ interface LayerSummary {
2484
+ label: string;
2485
+ cacheState: LayerDebugInfo["cacheState"];
2486
+ instances: number;
2487
+ glyphs: number;
2488
+ }
2489
+ /**
2490
+ * A compact, JSON-serializable per-frame summary kept in the {@link FrameRing}.
2491
+ * Carries no `Layer` references and no `SpaceMap` matrices (decision D3) — only
2492
+ * scalars — so the ring never retains a scene graph.
2493
+ */
2494
+ interface FrameSummary {
2495
+ frame: number;
2496
+ kind: "full" | "partial";
2497
+ /** `fullCause.reason` on a full frame; `undefined` on a partial. */
2498
+ reason: string | undefined;
2499
+ /** `fullCause.changed` fingerprint parts (only on a view-changed full). */
2500
+ changed: readonly FingerprintPart[] | undefined;
2501
+ /** Number of final damage rects (0 on a full frame). */
2502
+ rectCount: number;
2503
+ /** Summed area of the final damage rects in ui px² (0 on a full frame). */
2504
+ rectArea: number;
2505
+ totalMs: number;
2506
+ uploadMs: number;
2507
+ encodeMs: number;
2508
+ drawCalls: number;
2509
+ layers: readonly LayerSummary[];
2510
+ }
2511
+ /** Derive a {@link FrameSummary} (scalars only) from a rich {@link FrameDebug}. */
2512
+ declare function summarize(d: FrameDebug): FrameSummary;
2513
+ /**
2514
+ * Fixed-capacity ring buffer of {@link FrameSummary}. `push` appends and drops
2515
+ * the oldest once full; `toArray` returns oldest→newest in insertion order.
2516
+ */
2517
+ declare class FrameRing {
2518
+ readonly capacity: number;
2519
+ private buf;
2520
+ private head;
2521
+ private full;
2522
+ constructor(capacity?: number);
2523
+ push(s: FrameSummary): void;
2524
+ get size(): number;
2525
+ /** Oldest → newest. */
2526
+ toArray(): FrameSummary[];
2527
+ /** Cumulative full/partial counts over the resident window. */
2528
+ ratio(): {
2529
+ full: number;
2530
+ partial: number;
2531
+ };
2532
+ /** The resident timeline (oldest→newest) as a JSON string (D3 copy-JSON). */
2533
+ exportJson(): string;
2534
+ clear(): void;
2535
+ }
2536
+ //#endregion
2537
+ //#region src/debug/probe.d.ts
2538
+ /** A per-frame FrameDebug v2 listener. Returns nothing. */
2539
+ type FrameDebugListener = (d: FrameDebug) => void;
2540
+ /**
2541
+ * The narrow slice of `Renderer2D` the probe drives. The renderer implements this
2542
+ * inline at construction so the probe never holds the public renderer surface (or
2543
+ * imports the class — no cycle). All methods are called ONLY while capturing.
2544
+ */
2545
+ interface DebugHost {
2546
+ /** Release the per-frame retention (caller layer array + prev fingerprint parts). */
2547
+ releaseDebugRetention(): void;
2548
+ /** The exact `Layer[]` reference passed to the most recent `render()`, or null. */
2549
+ getLastLayers(): readonly Layer[] | null;
2550
+ /** Re-render the retained last layers as a full frame (no-op if none retained). */
2551
+ requestRender(): void;
2552
+ /**
2553
+ * Project the union of `layer`'s packed instance AABBs into a single `ui`-space
2554
+ * rect (the layer's live space view-projection), or `null` when the pack is
2555
+ * empty. The probe converts to the requested space via the frame's SpaceMap.
2556
+ */
2557
+ layerAabbUi(layer: Layer): DebugRect | null;
2558
+ /** Convert a `ui`-space rect to `space` using the most recent frame's SpaceMap. */
2559
+ convertFromUi(rect: DebugRect, space: DebugSpace): DebugRect | null;
2560
+ }
2561
+ /**
2562
+ * Core-owned render-debug probe. Constructed lazily by `Renderer2D.debug`; see
2563
+ * the module header for the zero-cost-off lifecycle.
2564
+ */
2565
+ declare class RendererDebug {
2566
+ private readonly host;
2567
+ private readonly listeners;
2568
+ private _enabled;
2569
+ /** Fixed-capacity history ring of frame SUMMARIES (decision D3). */
2570
+ readonly ring: FrameRing;
2571
+ constructor(host: DebugHost);
2572
+ /**
2573
+ * Whether the renderer should assemble a FrameDebug v2 record this frame. True
2574
+ * while explicitly enabled OR while any `onFrame` listener is attached. Read by
2575
+ * the renderer once per frame — the zero-cost-off gate.
2576
+ */
2577
+ get isCapturing(): boolean;
2578
+ /** Enable capture unconditionally (idempotent). The app gates the call (D4). */
2579
+ enable(): void;
2580
+ /**
2581
+ * Disable explicit capture (idempotent). Capture continues if listeners remain;
2582
+ * once neither enabled nor listened-to, the host's per-frame retention is freed.
2583
+ */
2584
+ disable(): void;
2585
+ /**
2586
+ * Subscribe to the per-frame FrameDebug v2 event. Returns an unsubscribe fn.
2587
+ * Attaching a listener makes the probe capture; removing the last one (while not
2588
+ * explicitly enabled) releases retention. Safe to unsubscribe during dispatch.
2589
+ */
2590
+ onFrame(cb: FrameDebugListener): () => void;
2591
+ /**
2592
+ * The exact `Layer[]` reference passed to the most recent `render()` while
2593
+ * capturing, or `null`. A presenter reads this to mutate the caller's live
2594
+ * layers (visibility / cache-hint) by identity.
2595
+ */
2596
+ get lastLayers(): readonly Layer[] | null;
2597
+ /** Re-render the retained last layers as a full frame (no-op if none). */
2598
+ requestRender(): void;
2599
+ /**
2600
+ * Project the union of `layer`'s packed instance AABBs into a damage rect in
2601
+ * `space` (default `"ui"`), or `null` when the pack is empty / no frame has
2602
+ * been captured (the SpaceMap is unknown before the first capturing frame).
2603
+ * Uses the same `viewProjectionForSpace` the draw + `regionsFromLayerAabbs` use.
2604
+ */
2605
+ layerAabb(layer: Layer, space?: DebugSpace): DebugRect | null;
2606
+ /**
2607
+ * Dispatch a freshly-assembled frame to the ring + listeners. Renderer-internal:
2608
+ * called by the host ONLY when `isCapturing`. Snapshots the listener set first so
2609
+ * a callback may unsubscribe (or others subscribe) mid-dispatch without skipping
2610
+ * or double-invoking.
2611
+ */
2612
+ dispatch(d: FrameDebug): void;
2613
+ }
2614
+ //#endregion
2615
+ //#region src/shared/cull.d.ts
2616
+ interface AABB {
2617
+ minX: number;
2618
+ minY: number;
2619
+ maxX: number;
2620
+ maxY: number;
2621
+ }
2622
+ //#endregion
2623
+ //#region src/cull.d.ts
2624
+ /**
2625
+ * The cull stage's view of one rebased draw command: a contiguous run of global
2626
+ * instance indices `[firstInstance, firstInstance + instanceCount)` that share a
2627
+ * kind + opacity, plus whether they live in `world` space (frustum-culled) or a
2628
+ * screen-fixed space (`ui` / `device`, passed through unculled). Shaped as a
2629
+ * structural subset of the renderer's `RebasedCommand` so the renderer feeds its
2630
+ * commands straight in.
2631
+ */
2632
+ interface CullCommand {
2633
+ firstInstance: number;
2634
+ instanceCount: number;
2635
+ /**
2636
+ * `true` when this command's coordinate space is `world` (camera-transformed),
2637
+ * so the frustum cull applies. `false` for `ui` / `device` — screen-fixed
2638
+ * chrome that is NEVER frustum-culled (invariant 5).
2639
+ */
2640
+ world: boolean;
2641
+ }
2642
+ /**
2643
+ * One narrowed sub-range of a {@link CullCommand}: a contiguous span of global
2644
+ * instance indices that survived the cull, suitable for a single `pass.draw`.
2645
+ * Multiple ranges are emitted when a command's survivors are non-contiguous
2646
+ * (a culled middle gap splits one command into several draws — invariant 2).
2647
+ */
2648
+ interface CulledRange {
2649
+ firstInstance: number;
2650
+ instanceCount: number;
2651
+ /**
2652
+ * Index into the input `commands` array of the command this range came from,
2653
+ * so the renderer recovers the source command's pipeline kind / opacity /
2654
+ * camera slot / clip when issuing the draw.
2655
+ */
2656
+ commandIndex: number;
2657
+ }
2658
+ /** Result of {@link cullCommands}: narrowed ranges + the ordered survivor list. */
2659
+ interface CullResult {
2660
+ /**
2661
+ * The narrowed draw ranges, in the SAME order as the input commands (and, for
2662
+ * a split command, ascending by `firstInstance`). When cull keeps an entire
2663
+ * command, exactly ONE range is emitted with the original first/count
2664
+ * (all-survive coalesces — invariant 4).
2665
+ *
2666
+ * `firstInstance` indexes the GLOBAL concat buffer directly — the buffer is
2667
+ * NOT compacted (invariant 2), so these are issued to `pass.draw` as-is.
2668
+ */
2669
+ ranges: CulledRange[];
2670
+ /**
2671
+ * Global instance indices of every survivor, in ascending (submission) order.
2672
+ * The renderer stamps depth over THESE ONLY, with a running survivor counter
2673
+ * and `N = survivorIndices.length`, so z re-densifies over (0,1) without
2674
+ * moving any instance in the buffer (invariants 1 & 2).
2675
+ */
2676
+ survivorIndices: number[];
2677
+ }
2678
+ /**
2679
+ * Derive the world-space frustum AABB for the WORLD camera slot — the rectangle
2680
+ * of world coordinates visible in a `width × height` (CSS-px) viewport under the
2681
+ * given camera, inflated by {@link CULL_AA_EPSILON} CSS px converted to world
2682
+ * units (`/zoom`). This is the single place the AA pad lives (it is NOT baked
2683
+ * into every shape's AABB — that would dominate tiny-world content).
2684
+ *
2685
+ * Rotation is handled CONSERVATIVELY: under a rotated camera the visible world
2686
+ * region is the rotated viewport rectangle, whose axis-aligned bounding box has
2687
+ * half-extents `halfW·|cos θ| + halfH·|sin θ|` (and the transpose for Y) — the
2688
+ * standard AABB of a rotated rect. This is an over-approximation (it always
2689
+ * contains the true rotated frustum), so the cull never wrongly drops a visible
2690
+ * shape; at `θ = 0` it collapses to the exact `width/zoom × height/zoom` box.
2691
+ */
2692
+ declare function worldFrustumAabb(camera: {
2693
+ x: number;
2694
+ y: number;
2695
+ zoom: number;
2696
+ rotation?: number;
2697
+ }, width: number, height: number): AABB;
2698
+ /**
2699
+ * Narrow each draw command into the contiguous sub-ranges whose per-instance
2700
+ * world AABB intersects `view`. Pure + device-free.
2701
+ *
2702
+ * @param commands Rebased draw commands (global instance ranges + `world` flag).
2703
+ * @param aabbs Flat per-instance world AABBs, 4 floats per GLOBAL instance
2704
+ * index (`minX, minY, maxX, maxY`) — i.e. instance `g` reads
2705
+ * `aabbs[g*4 .. g*4+3]`. Sourced from `kind.aabb()` at pack time.
2706
+ * @param view World-space frustum AABB (see {@link worldFrustumAabb}).
2707
+ *
2708
+ * Behavior:
2709
+ * - A `world` command keeps each maximal run of consecutive visible instances
2710
+ * as one range; a culled gap splits it (invariant 2 — no compaction, split
2711
+ * draws). A fully-visible command yields exactly one range with its original
2712
+ * first/count (invariant 4 — all-survive coalesces). A fully off-screen
2713
+ * command yields no range (invariant: off-screen dropped).
2714
+ * - A `ui` / `device` command (`world === false`) is passed through UNCULLED —
2715
+ * one range with its original first/count (invariant 5 — screen chrome is
2716
+ * never frustum-culled).
2717
+ *
2718
+ * Survivor RELATIVE order is preserved (commands processed in input order, each
2719
+ * scanned ascending), so the painter stack is intact.
2720
+ */
2721
+ declare function cullCommands(commands: readonly CullCommand[], aabbs: Float32Array, view: AABB): CullResult;
2722
+ //#endregion
2723
+ //#region src/tiers/spatial-grid.d.ts
2724
+ /**
2725
+ * A cell-binned spatial grid over a retained pack's per-instance AABBs. Slot
2726
+ * indices (`cellStart`, `smallEnd`, ranges) index the REORDERED buffer the
2727
+ * renderer uploads (per {@link buildSpatialGrid}'s `newOrder`), NOT the original
2728
+ * `append` order.
2729
+ */
2730
+ interface SpatialGrid {
2731
+ readonly gridW: number;
2732
+ readonly gridH: number;
2733
+ /**
2734
+ * Per-axis cell size — sized independently so elongated content (a phylo tree
2735
+ * whose Y span dominates X) keeps fine X-granularity. Mirrors v1's split.
2736
+ */
2737
+ readonly cellSizeX: number;
2738
+ readonly cellSizeY: number;
2739
+ readonly originX: number;
2740
+ readonly originY: number;
2741
+ /**
2742
+ * Length `gridW * gridH + 1`. `cellStart[i]..cellStart[i+1]` is the slot range
2743
+ * for cell `i` in the reordered buffer. The last value equals {@link smallEnd}
2744
+ * (oversized items live past it and are not cell-binned).
2745
+ */
2746
+ readonly cellStart: Uint32Array;
2747
+ /** Per-cell max half-extent on X/Y; query inflates each cell bbox by these. */
2748
+ readonly cellMaxHalfW: Float32Array;
2749
+ readonly cellMaxHalfH: Float32Array;
2750
+ /** Boundary between cell-binned small items `[0, smallEnd)` and the oversized tail. */
2751
+ readonly smallEnd: number;
2752
+ /** Flat `[minX,minY,maxX,maxY]×(count-smallEnd)` for the oversized tail; `null` when none. */
2753
+ readonly oversizedAabbs: Float32Array | null;
2754
+ /** Total instances indexed — matches the retained pack's instance count. */
2755
+ readonly count: number;
2756
+ }
2757
+ /** One contiguous slot range of the reordered buffer suitable for a single `pass.draw`. */
2758
+ interface SpatialRange {
2759
+ /** First slot index in the reordered buffer. */
2760
+ offset: number;
2761
+ count: number;
2762
+ }
2763
+ /** Result of {@link buildSpatialGrid}. */
2764
+ interface SpatialGridResult {
2765
+ index: SpatialGrid;
2766
+ /**
2767
+ * Permutation: `newOrder[newSlot] = originalInstanceIndex`. The renderer copies
2768
+ * each instance's bytes from its original slot to `newSlot`, so a cell's items
2769
+ * land contiguously and {@link queryRanges} emits few draws.
2770
+ */
2771
+ newOrder: Uint32Array;
2772
+ }
2773
+ /** Options for {@link buildSpatialGrid}. Mirrors v1's `BuildSpatialIndexOptions`. */
2774
+ interface SpatialGridOptions {
2775
+ /** Target cells along the dominant axis. Default 32. */
2776
+ cellsPerAxis?: number;
2777
+ /** Below this instance count the grid is not worth building → returns `null`. Default 1024. */
2778
+ minItems?: number;
2779
+ }
2780
+ /**
2781
+ * Build a cell-binned grid + permutation from flat per-instance AABBs
2782
+ * (`[minX,minY,maxX,maxY]×count`, the `UberPack.aabbs` layout).
2783
+ *
2784
+ * Returns `null` when `count < minItems` or the content bounds are degenerate —
2785
+ * the caller then retains the pack WITHOUT an index (drawn as one full-span
2786
+ * draw, the existing non-indexed retained path). Faithful port of
2787
+ * `renderer/spatial-index.ts:buildGridFromAabbs`.
2788
+ */
2789
+ declare function buildSpatialGrid(aabbs: Float32Array, count: number, options?: SpatialGridOptions): SpatialGridResult | null;
2790
+ /**
2791
+ * Query the grid for the contiguous slot ranges (in the reordered buffer) whose
2792
+ * cell bboxes intersect `view`. Writes into `out` (cleared first) and returns
2793
+ * it. Faithful port of `renderer/spatial-index.ts:queryRanges` — fast-path when
2794
+ * `view` contains the whole grid, a tier-1 small-cell sweep, and a tier-2
2795
+ * oversized-tail per-item test, all coalescing consecutive survivors into runs.
2796
+ */
2797
+ declare function queryRanges(index: SpatialGrid, view: AABB, out: SpatialRange[]): SpatialRange[];
2798
+ //#endregion
2799
+ //#region src/tiers/encode-types.d.ts
2800
+ /**
2801
+ * A draw command rebased onto the dynamic concat instance buffer: the source
2802
+ * command plus its `[firstInstance, firstInstance+instanceCount)` slice, the
2803
+ * camera slot it binds, and the per-frame cull/clip/exempt facts the encode
2804
+ * paths need. The clip + cull are wired so a future clipped command (or the
2805
+ * scene layer) drops in with no spine change.
2806
+ */
2807
+ interface RebasedCommand {
2808
+ cmd: DrawCommand;
2809
+ firstInstance: number;
2810
+ instanceCount: number;
2811
+ clipRect: FrameRect | null;
2812
+ /**
2813
+ * Camera slot this command's draw binds at `@group(0)`. For an un-grouped
2814
+ * command this is the base space slot; for a group-tagged command (P3-T8) it
2815
+ * is a dynamic per-(space × group) slot whose uploaded matrix is `spaceVP ∘
2816
+ * resolveGroupTransform(group)`, assigned per frame by `Renderer2D.render`.
2817
+ * The encode paths bind `cameraBindGroups[spaceSlot]` — they are agnostic to
2818
+ * whether the slot is a base or group slot.
2819
+ */
2820
+ spaceSlot: number;
2821
+ /**
2822
+ * `true` when this command's layer is `world` space (camera-transformed), so
2823
+ * the frustum-cull stage applies; `false` for `ui` / `device` (screen-fixed
2824
+ * chrome, passed through unculled — invariant 5). Fed to `cullCommands`.
2825
+ */
2826
+ world: boolean;
2827
+ /**
2828
+ * `true` when this command's layer sets `Layer.oitExempt` AND the renderer
2829
+ * runs with OIT: the command skips BOTH main-pass sweeps (opaque Pass 1 and
2830
+ * the OIT build Pass 2) and instead draws post-resolve in the exempt overlay
2831
+ * pass (`encodeExempt`). Always `false` on a non-OIT renderer — the flag is
2832
+ * inert there and the command takes the ordinary path.
2833
+ */
2834
+ exempt: boolean;
2835
+ }
2836
+ /**
2837
+ * One live-glyph instanced draw: a contiguous slice of the concatenated glyph
2838
+ * instance buffer (`[first, first+count)`) bound against `atlas`'s @group(2) and
2839
+ * the camera `slot` for the source layer's space. Built once per frame in the
2840
+ * gather pass; consumed by `Renderer2D._encodeGlyphs` in BOTH the non-OIT
2841
+ * in-pass draw and the OIT post-resolve pass.
2842
+ */
2843
+ interface GlyphDraw {
2844
+ /** The source layer's MSDF atlas — its own @group(2) binding (invariant 9). */
2845
+ atlas: GlyphAtlas;
2846
+ /** Camera slot for the layer's coordinate space. */
2847
+ slot: number;
2848
+ /** First glyph instance index in the concatenated glyph buffer. */
2849
+ first: number;
2850
+ /** Glyph instance count for this layer. */
2851
+ count: number;
2852
+ /**
2853
+ * `true` when the source layer sets `Layer.oitExempt`: under OIT these
2854
+ * glyphs skip the OIT emit sweep and draw post-resolve in the exempt overlay
2855
+ * pass via the direct-color pipeline. Inert on the non-OIT path.
2856
+ */
2857
+ exempt: boolean;
2858
+ }
2859
+ /**
2860
+ * A registered RTT bake for a cached layer. The `texture` is a single-sample
2861
+ * snapshot of the layer's shapes + glyphs; `composite` is its per-bake uniform
2862
+ * (NDC rect + UV rect) bind group + buffer. Instead of re-rasterizing the
2863
+ * layer's pack, `render()` composites `texture` through the textured-quad
2864
+ * pipeline — UNDER the live geometry (in-main-pass, leading z-run) or OVER it
2865
+ * (post-main pass, trailing z-run) per the bake's z-position (T-ZBAKE). A bake
2866
+ * sandwiched between two live layers is DEMOTED (rasterized live for that frame).
2867
+ */
2868
+ interface BakeRecord {
2869
+ /** The cached layer (kept so `uncacheLayer(layer)` can find its pack). */
2870
+ layer: Layer;
2871
+ /** Single-sample bake texture (caller-owned; destroyed on uncache). */
2872
+ texture: GPUTexture;
2873
+ /** Composite uniform buffer (NDC + UV rect). */
2874
+ uniform: GPUBuffer;
2875
+ /** Composite bind group (uniform + bake texture view + sampler). */
2876
+ bindGroup: GPUBindGroup;
2877
+ /**
2878
+ * Content + view fingerprint at bake time (P3-T1). The cache policy re-bakes a
2879
+ * renderer-managed layer when `Renderer2D._cacheKeyFor` no longer matches
2880
+ * (content version changed, or — for a world-space layer — the camera moved).
2881
+ */
2882
+ cacheKey: string;
2883
+ /**
2884
+ * `true` when the bake was created by the per-frame cache policy (`"always"`
2885
+ * or `"auto"` hint), `false` when created by an explicit `Renderer2D.cacheLayer`
2886
+ * call. The policy only auto-evicts / auto-drops bakes it manages, so a manual
2887
+ * cacheLayer keeps its prior "caller owns the lifecycle" semantics.
2888
+ */
2889
+ managed: boolean;
2890
+ /** Resident texture bytes (`w*h*4`) — summed against the memory budget (P3-T3). */
2891
+ bytes: number;
2892
+ /** Frame index of the last content change — staleness for eviction (P3-T3). */
2893
+ lastChangeFrame: number;
2894
+ }
2895
+ /**
2896
+ * A registered RETAINED layer. Its `UberPack` owns a STABLE GPU instance buffer
2897
+ * managed by `RetainedTier` (keyed on the pack); the renderer draws it as a
2898
+ * DISTINCT appended draw over its OWN bind group — it never joins the dynamic
2899
+ * concat instance buffer (whose `@builtin(instance_index)` mapping and z stamp
2900
+ * shift every frame as the live set grows/shrinks). The buffer is re-uploaded
2901
+ * ONLY when `key` changes (or the pack's `version` advances under that key); an
2902
+ * unchanged retained layer issues ZERO re-upload and still draws — the whole
2903
+ * performance win. Mirrors the `_bakes` ownership model: keyed by `layer.pack`,
2904
+ * evicted on `unretainLayer` + `destroy()`.
2905
+ */
2906
+ interface RetainedRecord {
2907
+ /** The retained layer (kept so `unretainLayer(layer)` can find its pack). */
2908
+ layer: Layer;
2909
+ /**
2910
+ * Stable per-layer retention key. A change (or a `pack.version` bump under the
2911
+ * same key) triggers exactly one rebuild + re-upload; an unchanged key skips
2912
+ * the re-concat/re-upload entirely.
2913
+ */
2914
+ key: string;
2915
+ /** `pack.version` captured at the last build — guards an in-place mutation. */
2916
+ version: number;
2917
+ /** `@group(1)` bind group over the tier's stable buffer for this pack. */
2918
+ bindGroup: GPUBindGroup;
2919
+ /**
2920
+ * Instance count of the retained pack captured at build time
2921
+ * (`pack.byteLength / INSTANCE_BYTES`). Stored so `render()` issues the
2922
+ * appended `draw(4, count, …)` without recomputing it per frame.
2923
+ */
2924
+ count: number;
2925
+ /** Camera slot for this layer's coordinate space. */
2926
+ spaceSlot: number;
2927
+ /**
2928
+ * Whether this layer is `world`-space (informational; retained draws are
2929
+ * unculled — they live outside the dynamic concat the cull stage narrows).
2930
+ */
2931
+ world: boolean;
2932
+ /**
2933
+ * Optional cell-binned grid over this pack's per-instance AABBs (built when
2934
+ * `retainLayer(layer, key, { spatialIndex: true })`). The stable GPU buffer is
2935
+ * uploaded in the grid's reordered slot order; on a `world`-space frame the
2936
+ * renderer range-queries this index against the viewport frustum and issues
2937
+ * one draw per visible cell-run — the v3 `cacheLayer({ spatialIndex: true })`
2938
+ * equivalent. `null` when no index was requested or the pack was too small to
2939
+ * index (draws as one full-span draw). The grid only narrows WHICH survivors
2940
+ * draw, never their relative (frozen, per-slot) painter order.
2941
+ */
2942
+ grid: SpatialGrid | null;
2943
+ }
2944
+ //#endregion
2945
+ //#region src/oit/abuffer.d.ts
2946
+ /**
2947
+ * Compile-time max K the resolve shader can sort/blend. The resolve pass
2948
+ * (`oit-pipelines.ts`) sizes its per-pixel local arrays to this constant (WGSL
2949
+ * arrays must be compile-time sized) and composites only `min(count, K, this)`
2950
+ * slots. A runtime `K` larger than this would therefore be SILENTLY truncated
2951
+ * by the resolve — fragments beyond slot 15 would be dropped in rasterization
2952
+ * order with no signal — which violates the A-buffer's loud contract. So the
2953
+ * {@link ABuffer} constructor hard-errors when `K` exceeds this cap.
2954
+ *
2955
+ * This is the single source of truth: `oit-pipelines.ts` imports it to size the
2956
+ * resolve shader's local arrays, so the cap and the shader can never drift.
2957
+ */
2958
+ declare const RESOLVE_MAX_K = 16;
2959
+ /** Bytes per A-buffer slot: `rgba` (packed unorm8x4 → u32) + `depth` (f32). */
2960
+ declare const SLOT_BYTES = 8;
2961
+ /** Bytes per head entry: one `atomic<u32>` insert counter per pixel. */
2962
+ declare const HEAD_BYTES = 4;
2963
+ /** Construction options for {@link ABuffer}. */
2964
+ interface ABufferOptions {
2965
+ /** K — per-pixel fragment budget (`RendererConfig.oitFragmentsPerPixel`). */
2966
+ fragmentsPerPixel: number;
2967
+ }
2968
+ /**
2969
+ * Per-pixel bounded-K A-buffer (heads + slots) plus the build/resolve bind-group
2970
+ * layouts and bind groups. When the slots buffer would exceed the device's
2971
+ * `maxStorageBufferBindingSize`, {@link resize} auto-reduces K (with a console
2972
+ * warning) rather than throwing. Prefer pre-clamping via {@link clampKForDevice}
2973
+ * so the effective K is stable from construction.
2974
+ */
2975
+ declare class ABuffer {
2976
+ /**
2977
+ * Per-pixel fragment budget (≥ 1). Set at construction; may be reduced at
2978
+ * {@link resize} time when the slots buffer would exceed the device storage
2979
+ * limit. Read the value after resize to get the effective K.
2980
+ */
2981
+ K: number;
2982
+ /** `array<atomic<u32>>` insert counters — one per pixel. */
2983
+ heads: GPUBuffer;
2984
+ /** `array<OITNode>` of `width*height*K` slots. Slot `p*K + i` for pixel `p`. */
2985
+ slots: GPUBuffer;
2986
+ /** `{ width, height, K, _pad }` uniform read by the build + resolve shaders. */
2987
+ viewportUniform: GPUBuffer;
2988
+ /** Build-pass layout: heads + slots `storage`, viewport `uniform`. */
2989
+ readonly buildLayout: GPUBindGroupLayout;
2990
+ /** Resolve-pass layout: heads + slots `read-only-storage`, viewport `uniform`. */
2991
+ readonly resolveLayout: GPUBindGroupLayout;
2992
+ /** Bind group for the build pass (re-created on each {@link resize}). */
2993
+ buildBindGroup: GPUBindGroup;
2994
+ /** Bind group for the resolve pass (re-created on each {@link resize}). */
2995
+ resolveBindGroup: GPUBindGroup;
2996
+ private readonly device;
2997
+ private width;
2998
+ private height;
2999
+ /**
3000
+ * The opaque-pass depth view, supplied by the renderer after construction.
3001
+ * `undefined` until {@link setDepthView} is called. The build bind group is
3002
+ * not constructed until BOTH the A-buffer GPU buffers (`heads`/`slots`) AND
3003
+ * this view are present — see {@link _buildBuildBindGroup}.
3004
+ */
3005
+ private depthView?;
3006
+ constructor(device: GPUDevice, options: ABufferOptions);
3007
+ /**
3008
+ * (Re)allocate the heads + slots buffers for a `width × height` surface and
3009
+ * rebuild the bind groups. No-op when the dimensions are unchanged.
3010
+ *
3011
+ * When the slots buffer (`pixels * K * SLOT_BYTES`) would exceed the device's
3012
+ * `maxStorageBufferBindingSize`, K is auto-reduced to the largest value that
3013
+ * fits (minimum 1) and a console warning is emitted. The effective K after the
3014
+ * call is available as {@link K}. Prefer pre-clamping via
3015
+ * {@link clampKForDevice} so K is stable from construction. (`heads` is
3016
+ * `pixels * HEAD_BYTES`, far smaller, so slots are always the binding-limit
3017
+ * gate.)
3018
+ */
3019
+ resize(width: number, height: number): void;
3020
+ /**
3021
+ * Supply (or refresh) the opaque depth view that will be bound at
3022
+ * `@binding(3)` in every subsequent build pass.
3023
+ *
3024
+ * **Call-order contract:** Construction and resize are independent — the build
3025
+ * bind group is only (re)created once BOTH the A-buffer GPU buffers AND the
3026
+ * depth view are present. Callers may therefore call `setDepthView` before or
3027
+ * after `resize` and the bind group will be correct:
3028
+ *
3029
+ * - `setDepthView` BEFORE `resize`: the view is stored; `resize` builds the
3030
+ * bind group when it allocates the buffers (because `depthView` is already
3031
+ * set by then).
3032
+ * - `resize` BEFORE `setDepthView`: `resize` skips the build bind-group step
3033
+ * (buffers exist but no view yet); `setDepthView` builds it on arrival.
3034
+ * - Either path produces one build bind group — no double-creation.
3035
+ *
3036
+ * The renderer must call this on construction (`createRenderer`) and again
3037
+ * on every canvas resize (after `CanvasContext.depthView` is refreshed by
3038
+ * `resizeCanvas`). Because `buildBindGroup` is recreated on every `resize`,
3039
+ * the depth view is refreshed in lockstep with the buffer allocation — no
3040
+ * staleness as long as the renderer always passes the freshest `ctx.depthView`.
3041
+ *
3042
+ * @param view The `GPUTextureView` for the `depth24plus` texture created by
3043
+ * `createDepth` in `context.ts`. MUST be a single-sample depth view —
3044
+ * multisampled depth cannot be bound as `texture_depth_2d`.
3045
+ */
3046
+ setDepthView(view: GPUTextureView): void;
3047
+ /**
3048
+ * (Re)build the build-pass bind group from the current heads/slots/viewport
3049
+ * buffers and the opaque depth view.
3050
+ *
3051
+ * **No-op guard:** returns immediately when `depthView` is not yet available.
3052
+ * This preserves order-independence: `resize` (which calls this) can run
3053
+ * before `setDepthView` without crashing. The caller (`setDepthView`) will
3054
+ * build the group on arrival of the view. This means `buildBindGroup` may be
3055
+ * temporarily uninitialized between a `resize` and the first `setDepthView`;
3056
+ * the renderer must not issue build passes in that window.
3057
+ */
3058
+ private _buildBuildBindGroup;
3059
+ /**
3060
+ * Create a **build-style** A-buffer bind-group layout suitable for a pipeline
3061
+ * that binds the A-buffer at `@group(group)`. The entries are identical to
3062
+ * {@link buildLayout} (heads + slots `storage`, viewport `uniform`) — a
3063
+ * `GPUBindGroupLayout` does not encode the group index, so the layout is the
3064
+ * same regardless of `group`; the index is fixed by the layout's POSITION in
3065
+ * the pipeline layout's `bindGroupLayouts` array.
3066
+ *
3067
+ * Use this for the glyph / sprite OIT emit pipelines, which keep their own
3068
+ * texture/atlas at `@group(2)` and the A-buffer at `@group(3)`:
3069
+ *
3070
+ * ```ts
3071
+ * const abLayout = abuffer.buildLayoutAt(3);
3072
+ * device.createPipelineLayout({
3073
+ * bindGroupLayouts: [cameraLayout, instanceLayout, atlasLayout, abLayout],
3074
+ * });
3075
+ * // then: pass.setBindGroup(3, abuffer.buildBindGroupFor());
3076
+ * ```
3077
+ *
3078
+ * Passing `group === 2` returns the existing {@link buildLayout} object (the
3079
+ * shape build slot), so callers can share one layout instance there.
3080
+ *
3081
+ * @param group The `@group(n)` index this layout will occupy (only used to
3082
+ * pick the shared label / reuse the canonical `@group(2)` layout; the entry
3083
+ * list is invariant).
3084
+ */
3085
+ buildLayoutAt(group: number): GPUBindGroupLayout;
3086
+ /**
3087
+ * The build-pass bind group (`heads`/`slots`/`oitViewport`) for binding the
3088
+ * A-buffer at ANY group index. A `GPUBindGroup` is index-agnostic — the same
3089
+ * object can be `setBindGroup(2, ...)` on the shape build pipeline and
3090
+ * `setBindGroup(3, ...)` on the glyph/sprite emit pipelines in the same pass,
3091
+ * as long as the pipeline's layout slot was created from a layout with the
3092
+ * matching entries ({@link buildLayoutAt}). It is re-created on every
3093
+ * {@link resize}, so always read it fresh (do not cache across a resize).
3094
+ *
3095
+ * Returns the SAME object as {@link buildBindGroup}; this accessor exists to
3096
+ * make the "reuse the one bind group at a different slot" contract explicit at
3097
+ * the emit call sites (Phases 2/3).
3098
+ */
3099
+ buildBindGroupFor(): GPUBindGroup;
3100
+ /**
3101
+ * Reset the per-pixel insert counters to 0 at the start of every frame. Only
3102
+ * `heads` is cleared — `slots` are write-once per build pass and the resolve
3103
+ * pass reads only the first `min(count, K)` per pixel, so stale slot bytes are
3104
+ * never observed. There is no global counter to reset.
3105
+ */
3106
+ clearFrame(encoder: GPUCommandEncoder): void;
3107
+ /** Release all GPU buffers. The bind-group layouts are not GPU-owned memory. */
3108
+ destroy(): void;
3109
+ }
3110
+ //#endregion
3111
+ //#region src/pipelines/oit-pipelines.d.ts
3112
+ /** The two OIT render pipelines plus the build pipeline's explicit layout. */
3113
+ interface OITPipelines {
3114
+ /**
3115
+ * Explicit pipeline layout for the build pass:
3116
+ * `[cameraLayout, instanceLayout, abuffer.buildLayout]`. Exposed so the
3117
+ * renderer can confirm its bind-group ordering matches.
3118
+ */
3119
+ buildLayout: GPUPipelineLayout;
3120
+ /**
3121
+ * Build pass: assembled vertex + `oitBuild` fragment. `writeMask:0` (color
3122
+ * masked), `depthWriteEnabled:false` + `depthCompare:"less"` (test against
3123
+ * opaque depth, never write), `triangle-strip` (matches the uber vertex
3124
+ * quad layout), single-sample.
3125
+ */
3126
+ buildPipeline: GPURenderPipeline;
3127
+ /**
3128
+ * Resolve pass: full-screen `triangle-list` (3 verts), premultiplied over
3129
+ * blend, NO depth attachment.
3130
+ */
3131
+ resolvePipeline: GPURenderPipeline;
3132
+ }
3133
+ /** Bind-group layouts the build pass shares with the main passes (groups 0/1). */
3134
+ interface OITPipelineDeps {
3135
+ /** `@group(0)` camera uniform layout (from {@link createPipelines}). */
3136
+ cameraLayout: GPUBindGroupLayout;
3137
+ /** `@group(1)` instances storage layout (from {@link createPipelines}). */
3138
+ instanceLayout: GPUBindGroupLayout;
3139
+ }
3140
+ /**
3141
+ * Build the OIT build + resolve pipelines.
3142
+ *
3143
+ * @param device WebGPU device.
3144
+ * @param format Swapchain texture format (color target for both passes).
3145
+ * @param abuffer A-buffer resources — supplies `@group(2)` build layout and the
3146
+ * `@group(0)` resolve layout.
3147
+ * @param shader Assembled uber-shader (T12). `shader.vertex` is reused verbatim
3148
+ * for the build vertex stage; `shader.oitBuild` is the build fragment — neither
3149
+ * is re-authored here.
3150
+ * @param deps The camera + instance bind-group layouts shared with the main
3151
+ * opaque/transparent passes, so ONE camera / ONE instances bind group binds
3152
+ * against the build pass too.
3153
+ */
3154
+ declare function createOITPipelines(device: GPUDevice, format: GPUTextureFormat, abuffer: ABuffer, shader: AssembledShader, deps: OITPipelineDeps): OITPipelines;
3155
+ //#endregion
3156
+ //#region src/damage/auto-damage.d.ts
3157
+ /**
3158
+ * Per-layer snapshot of the previous auto frame, used to diff against the
3159
+ * current pack and derive damage regions (P4-T3). Captured at the END of an
3160
+ * auto frame while `pack.aabbs` is still valid (before the caller's next-frame
3161
+ * `clear()`).
3162
+ */
3163
+ interface Snapshot {
3164
+ /** `UberPack.version` at capture time — a cheap "did anything repack?" gate. */
3165
+ packVersion: number;
3166
+ /** Shape-instance count (`pack.aabbs` valid range is `count * 4` floats). */
3167
+ count: number;
3168
+ /**
3169
+ * COPY of the valid prefix of `pack.aabbs` (`count * 4` floats, 4 per
3170
+ * instance: `minX, minY, maxX, maxY`). Copied — NOT a live reference — so the
3171
+ * caller's next-frame repack does not mutate it under us.
3172
+ */
3173
+ aabbs: Float32Array;
3174
+ /**
3175
+ * Per-instance style hash — one `u32` per instance, computed by XOR-folding
3176
+ * the 8 u32 words at float offsets 8–15 of each packed instance record
3177
+ * (`colorBits, typeFlag, order, lane0-4`). A hash mismatch on an instance
3178
+ * whose AABB is geometrically unchanged (old AABB == new AABB) still produces
3179
+ * a damage rect at that AABB position, so pure style changes (color, opacity,
3180
+ * z-order, kind-specific lanes) are correctly caught by auto-damage without
3181
+ * requiring the caller to hand-track damage manually.
3182
+ */
3183
+ styleHashes: Uint32Array;
3184
+ /** `GlyphPack.version` at capture, or `0` when the layer has no glyph pack. */
3185
+ glyphVersion: number;
3186
+ /** Glyph-instance count (`glyphPack?.count ?? 0`). */
3187
+ glyphCount: number;
3188
+ /**
3189
+ * The glyph pack's cumulative layer-space content bbox at capture (`null`
3190
+ * when no plain glyphs). On a glyph-version change the diff damages
3191
+ * `old ∪ new` of this bbox instead of promoting the frame to full (D10
3192
+ * revised) — valid only while `glyphUnbounded` is false on BOTH sides.
3193
+ */
3194
+ glyphBbox: {
3195
+ minX: number;
3196
+ minY: number;
3197
+ maxX: number;
3198
+ maxY: number;
3199
+ } | null;
3200
+ /**
3201
+ * Whether the pack held anchored / screen-sized glyphs at capture. Their
3202
+ * pixels are camera-projected, so `glyphBbox` cannot bound them — a glyph
3203
+ * change on such a layer keeps the full `"auto-damage-glyphs"` promote.
3204
+ */
3205
+ glyphUnbounded: boolean;
3206
+ /**
3207
+ * COPY of the pack's per-push records at capture, or `null` when the pack
3208
+ * overflowed `GLYPH_PUSH_RECORD_CAP`. With records on BOTH sides the glyph
3209
+ * diff matches unchanged pushes (same text/position/layout → identical
3210
+ * pixels) and damages only the changed ones — without them, one changed HUD
3211
+ * string damages the union of every text block in the layer. Copied (not a
3212
+ * live reference): a push WITHOUT an intervening `clear()` appends to the
3213
+ * pack's live array and must not grow this retained view.
3214
+ */
3215
+ glyphRecords: readonly GlyphPushRecord[] | null;
3216
+ }
3217
+ /**
3218
+ * Per-renderer auto-damage state: a `WeakMap` of the most recent {@link
3219
+ * Snapshot} per live {@link Layer}. Lazily constructed by the renderer the
3220
+ * first time a `damage: "auto"` frame runs (zero-cost when auto is never used).
3221
+ *
3222
+ * P4-T2 surface: `has` / `get` (the seed gate reads these) + `update` (capture
3223
+ * at frame end). The diff is added in P4-T3.
3224
+ */
3225
+ declare class AutoDamageState {
3226
+ private readonly _snapshots;
3227
+ /** Whether a snapshot exists for `layer` (false → this layer needs seeding). */
3228
+ has(layer: Layer): boolean;
3229
+ /** The retained snapshot for `layer`, or `undefined` if none. */
3230
+ get(layer: Layer): Snapshot | undefined;
3231
+ /**
3232
+ * Capture / refresh the snapshot for `layer` from its CURRENT pack state.
3233
+ * Called at the end of every auto frame (seed or partial) for every live
3234
+ * layer, so next frame's diff has a fresh baseline.
3235
+ */
3236
+ update(layer: Layer): void;
3237
+ /**
3238
+ * Refresh snapshots for every layer in `layers`. Convenience for the
3239
+ * frame-end seed/update loop in the renderer.
3240
+ */
3241
+ updateAll(layers: readonly Layer[]): void;
3242
+ }
3243
+ //#endregion
3244
+ //#region src/layers/buffer-layer.d.ts
3245
+ type BufferLayerKind = "instances";
3246
+ /** A `d.arrayOf` over the v3 uber `InstanceStruct` (16 f / 64 B). The element
3247
+ * type is {@link INSTANCE_LAYOUT}'s `tgpuStruct` — the GPU-layout source of
3248
+ * truth — so a kernel-written buffer (`writeRectInstance`) lines up field-for-
3249
+ * field with the live uber concat. */
3250
+ type InstanceArray = d.WgslArray<typeof INSTANCE_LAYOUT.tgpuStruct>;
3251
+ /**
3252
+ * Options for an instance {@link BufferLayer} (v3 64 B `InstanceStruct` external
3253
+ * buffer — the "bring-your-own-GPU-buffer" seam).
3254
+ *
3255
+ * This buffer holds the v3 uber `InstanceStruct` (16 f / 64 B, `schema/instance.ts`)
3256
+ * — the SAME record the live `render()` concat packs and the SAME one a compute
3257
+ * kernel emits via `writeRectInstance`. The renderer draws it through the
3258
+ * external-instance pipelines (`pipelines/instance-pipelines.ts`), which decode
3259
+ * `typeFlag`/lanes with the live uber per-kind geometry + SDF. No texture
3260
+ * (instances are SDF primitives, not textured quads).
3261
+ *
3262
+ * An instance buffer stacks ABOVE all live geometry + bakes by submission order
3263
+ * (no z-interleave) and carries a constant per-buffer near-z — the accepted
3264
+ * limitation of the retained/bake post-passes.
3265
+ */
3266
+ interface BufferLayerInstancesOptions {
3267
+ kind: "instances";
3268
+ /**
3269
+ * Storage buffer of v3 uber instances, typically
3270
+ * `root.createBuffer(d.arrayOf(INSTANCE_LAYOUT.tgpuStruct, N)).$usage("storage")`.
3271
+ * Each record is the 64 B `InstanceStruct` (`schema/instance.ts`).
3272
+ */
3273
+ buffer: TgpuBuffer<InstanceArray> & StorageFlag;
3274
+ /** How many instances from the buffer to draw this frame. Mutable on the returned layer. */
3275
+ count: number;
3276
+ /** Optional transform group applied to all instances in this buffer. */
3277
+ group?: Group;
3278
+ /** Coordinate space. Default: `"world"`. */
3279
+ space?: LayerSpace;
3280
+ /** CSS-pixel crop rectangle applied as a scissor on the GPU backend. */
3281
+ clipRect?: FrameRect;
3282
+ /**
3283
+ * When `true`, routes into the opaque (depth-write) pass. Caller must guarantee
3284
+ * every instance is fully opaque (`fill.a === 1`, `stroke.a === 1`). Default `false`.
3285
+ */
3286
+ opaque?: boolean;
3287
+ }
3288
+ type BufferLayerOptions = BufferLayerInstancesOptions;
3289
+ /**
3290
+ * A drawable whose data lives entirely in a GPU buffer (no CPU `UberPack`).
3291
+ * Use this to render output from a compute pass without a CPU round-trip.
3292
+ *
3293
+ * Holds `instances` (v3 uber 64 B `InstanceStruct`, decoded by the live uber
3294
+ * per-kind geometry/SDF). See `schema/instance.ts` + `writeRectInstance` for the
3295
+ * field encoding.
3296
+ */
3297
+ declare class BufferLayer {
3298
+ readonly kind: BufferLayerKind;
3299
+ readonly space: LayerSpace;
3300
+ readonly group: Group | null;
3301
+ /** @internal */
3302
+ readonly _buffer: TgpuBuffer<InstanceArray> & StorageFlag;
3303
+ /** Number of items drawn this frame. Mutate freely between frames. */
3304
+ count: number;
3305
+ /** CSS-pixel crop rectangle. Mutable — renderer reads it at submit time. */
3306
+ clipRect: FrameRect | undefined;
3307
+ /** See option doc. Mutable. */
3308
+ opaque: boolean;
3309
+ /**
3310
+ * Monotonic content version. The renderer folds `version` (with `count` and
3311
+ * buffer identity) into its partial-redraw view fingerprint, so a change here
3312
+ * demotes the next frame to a full (loadOp:"clear") repaint automatically.
3313
+ *
3314
+ * CALLER CONTRACT: a BufferLayer's GPU bytes change OUT-OF-BAND from the camera
3315
+ * (a compute pass writes them), and the renderer cannot introspect buffer
3316
+ * contents. So EVERY time a compute pass mutates the buffer this layer draws,
3317
+ * the caller MUST {@link bumpVersion} (or set `version` directly). Forget to,
3318
+ * and a damage frame will ghost (stale pixels outside the damage rects). Use
3319
+ * {@link bumpVersion} from the same place you `compute(...)`.
3320
+ */
3321
+ version: number;
3322
+ /**
3323
+ * Treat this buffer's bounds as an occluder. Default `false`. Requires the
3324
+ * caller to pass `occluderAabb` when enabling.
3325
+ */
3326
+ occluder: boolean;
3327
+ /**
3328
+ * World-space bounding box used when `occluder` is true. Caller supplies
3329
+ * since the renderer cannot introspect buffer contents.
3330
+ */
3331
+ occluderAabb: {
3332
+ minX: number;
3333
+ minY: number;
3334
+ maxX: number;
3335
+ maxY: number;
3336
+ } | null;
3337
+ constructor(options: BufferLayerOptions);
3338
+ setClipRect(rect: FrameRect | undefined): this;
3339
+ /**
3340
+ * Set the draw count for the next frame AND bump {@link version} so the
3341
+ * partial-redraw fingerprint observes the change. Convenience for the common
3342
+ * "compute wrote N items this frame" path — equivalent to assigning `count`
3343
+ * then calling {@link bumpVersion}.
3344
+ */
3345
+ setCount(count: number): this;
3346
+ /**
3347
+ * Bump {@link version} to signal the buffer's GPU contents changed. Call this
3348
+ * from wherever you record the `compute(...)` pass that wrote the buffer, so
3349
+ * the next `render()` repaints in full instead of ghosting on a damage frame.
3350
+ */
3351
+ bumpVersion(): this;
3352
+ }
3353
+ declare function createBufferLayer(options: BufferLayerOptions): BufferLayer;
3354
+ //#endregion
3355
+ //#region src/layers/custom-drawable.d.ts
3356
+ /**
3357
+ * Context provided to {@link CustomDrawable.record} on every frame.
3358
+ *
3359
+ * The encoder is in the middle of a render pass — do NOT begin or end passes.
3360
+ * The scissor rect (if any) is applied by the renderer before the call.
3361
+ */
3362
+ interface CustomDrawableContext {
3363
+ /** The active GPU device. */
3364
+ device: GPUDevice;
3365
+ /** The command encoder for the current frame. */
3366
+ encoder: GPUCommandEncoder;
3367
+ /**
3368
+ * The scissor rect the renderer has applied (device pixels), or `null` when
3369
+ * rendering full-viewport. Informational only — the renderer has already set
3370
+ * the scissor on the pass.
3371
+ */
3372
+ scissor: FrameRect | null;
3373
+ /** The coordinate space this drawable was created with. */
3374
+ space: LayerSpace;
3375
+ /**
3376
+ * The renderer's shared per-frame camera bind group resolved for this
3377
+ * drawable's `space`. Bind at `@group(0)`. The renderer supplies it via ctx
3378
+ * (drawables must not fetch it themselves).
3379
+ */
3380
+ cameraBindGroup: GPUBindGroup;
3381
+ }
3382
+ /**
3383
+ * A raw render-pass hook. Implement this to bring custom pipelines, bind
3384
+ * groups, and draw calls into the renderer's frame. The renderer hands the
3385
+ * pass encoder directly to `record`; the drawable issues its own GPU commands.
3386
+ *
3387
+ * Unlike v1 `CustomDrawable`:
3388
+ * - Any `space` is allowed (`"world"`, `"ui"`).
3389
+ * - Any tier is supported (opaque or transparent).
3390
+ * - Optional `clipRect` is supported — the renderer applies it as a scissor
3391
+ * before calling `record`.
3392
+ */
3393
+ interface CustomDrawable {
3394
+ /** Brand tag — used by `isCustomDrawable` runtime checks. */
3395
+ readonly __customDrawable: true;
3396
+ /** Coordinate space this drawable occupies. */
3397
+ readonly space: LayerSpace;
3398
+ /**
3399
+ * Optional scissor rect (device pixels). The renderer applies this before
3400
+ * invoking `record`. `undefined` = full viewport.
3401
+ */
3402
+ readonly clipRect: FrameRect | undefined;
3403
+ /**
3404
+ * When `true`, this drawable's output is fully opaque — the renderer may
3405
+ * route it into the opaque pass (depth writes on). Default: `false`.
3406
+ */
3407
+ readonly opaque: boolean;
3408
+ /**
3409
+ * Called once per frame. `pass` is an active `GPURenderPassEncoder` — record
3410
+ * pipelines, bind groups, and draw calls here. Do NOT begin or end the pass.
3411
+ *
3412
+ * @param pass The active render pass encoder.
3413
+ * @param ctx Frame context (device, encoder, applied scissor, space).
3414
+ */
3415
+ record(pass: GPURenderPassEncoder, ctx: CustomDrawableContext): void;
3416
+ /** Optional cleanup. The renderer does not manage the drawable's lifetime. */
3417
+ destroy?(): void;
3418
+ }
3419
+ /** True when `node` is a `CustomDrawable` (brand-tag check). */
3420
+ declare function isCustomDrawable(node: unknown): node is CustomDrawable;
3421
+ interface CustomDrawableOptions {
3422
+ /** Optional scissor rect (device pixels). Default: `undefined`. */
3423
+ clipRect?: FrameRect;
3424
+ /**
3425
+ * When `true`, declares this drawable fully opaque. Default: `false`.
3426
+ */
3427
+ opaque?: boolean;
3428
+ /** Optional cleanup callback invoked by {@link CustomDrawable.destroy}. */
3429
+ onDestroy?: () => void;
3430
+ }
3431
+ /**
3432
+ * Create a `CustomDrawable` from a `record` callback. Any `LayerSpace` is
3433
+ * accepted; `clipRect` and `opaque` are optional.
3434
+ *
3435
+ * @param space Coordinate space the drawable occupies.
3436
+ * @param record Called once per frame with the active render pass encoder.
3437
+ * @param options Clip rect, opaque flag, and destroy hook.
3438
+ */
3439
+ declare function createCustomDrawable(space: LayerSpace, record: (pass: GPURenderPassEncoder, ctx: CustomDrawableContext) => void, options?: CustomDrawableOptions): CustomDrawable;
3440
+ //#endregion
3441
+ //#region src/scene/renderer-registry.d.ts
3442
+ /**
3443
+ * The {@link Renderer2D} most recently constructed against `canvas`, or
3444
+ * `undefined` if none is registered. Debug/tooling lookup only (last-constructed
3445
+ * wins — see the WeakMap above); not a general renderer registry.
3446
+ */
3447
+ declare function rendererForCanvas(canvas: HTMLCanvasElement): Renderer2D | undefined;
3448
+ type RendererCreatedListener = (renderer: Renderer2D) => void;
3449
+ /** Record a newly-constructed renderer and notify subscribers. */
3450
+ /**
3451
+ * Subscribe to renderer construction. The callback fires immediately for every
3452
+ * still-live renderer, then again for each future construction. Returns an
3453
+ * unsubscribe function. No strong renderer references are held in module scope
3454
+ * (live renderers are tracked via pruned `WeakRef`s — D9), so subscribing does
3455
+ * not keep a renderer alive.
3456
+ */
3457
+ declare function onRendererCreated(cb: RendererCreatedListener): () => void;
3458
+ //#endregion
3459
+ //#region src/scene/view-fingerprint.d.ts
3460
+ /** Renderer-state snapshot for one `compute()` call. */
3461
+ interface FingerprintParams {
3462
+ camera: {
3463
+ x: number;
3464
+ y: number;
3465
+ zoom: number;
3466
+ rotation: number;
3467
+ };
3468
+ ctxWidth: number;
3469
+ ctxHeight: number;
3470
+ dpr: number;
3471
+ background: Color;
3472
+ emphasisMirror: {
3473
+ focusedKey: number;
3474
+ dimAlpha: number;
3475
+ t: number;
3476
+ };
3477
+ viewKey?: string;
3478
+ bufferLayers?: readonly BufferLayer[] | null;
3479
+ zOrderKey?: string;
3480
+ visibilityKey?: string;
3481
+ /** When capturing: a fresh `{}` the callee fills with per-part substrings. */
3482
+ outParts?: Record<FingerprintPart, string>;
3483
+ }
3484
+ //#endregion
3485
+ //#region src/renderer-frame.d.ts
3486
+ /**
3487
+ * Narrow read/delegation surface `Renderer2D` exposes to the frame spine. Every
3488
+ * member here is a handle the phases READ or a method that delegates to an
3489
+ * already-extracted collaborator (`tiers/encode.ts`, the camera/instance/bake/
3490
+ * glyph/sprite/interop managers) — the phases never reach past this interface
3491
+ * into renderer privates, and `Renderer2D` remains the sole owner of GPU
3492
+ * lifetime + the public API.
3493
+ *
3494
+ * @internal
3495
+ */
3496
+ interface FrameHost {
3497
+ readonly frameRoot: {
3498
+ device: GPUDevice;
3499
+ };
3500
+ readonly frameCtx: {
3501
+ width: number;
3502
+ height: number;
3503
+ persistent: boolean;
3504
+ backbufferView: GPUTextureView | null;
3505
+ backbuffer: GPUTexture | null;
3506
+ depthView: GPUTextureView;
3507
+ gpuContext: GPUCanvasContext;
3508
+ canvas: {
3509
+ clientWidth?: number;
3510
+ clientHeight?: number;
3511
+ };
3512
+ };
3513
+ readonly frameConfig: {
3514
+ readonly oit: boolean;
3515
+ readonly partialRedraw: boolean;
3516
+ readonly cull: boolean;
3517
+ readonly onFrameTiming?: (t: ReturnType<typeof frameTiming>) => void;
3518
+ readonly onGpuFrameTiming?: (ms: number) => void;
3519
+ };
3520
+ readonly frameAbuffer: {
3521
+ clearFrame(encoder: GPUCommandEncoder): void;
3522
+ } | null;
3523
+ readonly frameOitPipelines: {
3524
+ resolvePipeline: GPURenderPipeline;
3525
+ } | null;
3526
+ readonly frameUseOit: boolean;
3527
+ readonly frameCamera: {
3528
+ x: number;
3529
+ y: number;
3530
+ zoom: number;
3531
+ rotation: number;
3532
+ };
3533
+ readonly frameDpr: number;
3534
+ readonly frameBackground: Color;
3535
+ readonly frameEmphasisMirror: {
3536
+ focusedKey: number;
3537
+ dimAlpha: number;
3538
+ t: number;
3539
+ };
3540
+ readonly frameRetainedFarBand: boolean;
3541
+ readonly frameCapturing: boolean;
3542
+ readonly cameraUniformData: Float32Array;
3543
+ cameraUniformSlotStrideFloats(): number;
3544
+ readonly cameraUniformSlotStride: number;
3545
+ cameraUniformBuffer(): GPUBuffer;
3546
+ ensureCameraSlots(count: number): void;
3547
+ ensureStaging(floats: number): void;
3548
+ stagingArray(): Float32Array;
3549
+ ensureCullAabbs(instances: number): Float32Array;
3550
+ ensureInstanceCapacity(bytes: number): void;
3551
+ instanceBuffer(): GPUBuffer;
3552
+ hasBakes(): boolean;
3553
+ hasRetained(): boolean;
3554
+ bakeFor(pack: object): BakeRecord | undefined;
3555
+ retainedFor(pack: object): RetainedRecord | undefined;
3556
+ bakeCacheApplyCachePolicy(layers: readonly Layer[]): void;
3557
+ bakeCacheClearDemotions(): void;
3558
+ bakeCacheRecordDemotion(space: LayerSpace): void;
3559
+ readonly cullStats: {
3560
+ instancesTotal: number;
3561
+ instancesDrawn: number;
3562
+ instancesCulled: number;
3563
+ commandsTotal: number;
3564
+ drawRanges: number;
3565
+ culled: boolean;
3566
+ };
3567
+ stampHeterogeneousOrder(ranges: readonly {
3568
+ layer: Layer;
3569
+ packBase: number;
3570
+ instInPack: number;
3571
+ }[], n: number, retainedPresent: boolean): void;
3572
+ glyphUploadInstances(layers: readonly Layer[], sig: string, device: GPUDevice): {
3573
+ glyphDraws: GlyphDraw[];
3574
+ };
3575
+ glyphPackId(pack: object): number;
3576
+ uploadLayerSprites(layers: readonly Layer[]): number[];
3577
+ encodeFull(pass: GPURenderPassEncoder, cmds: readonly RebasedCommand[], retained: readonly RetainedRecord[], useOit: boolean, bakedBelow: readonly BakeRecord[], phase?: "all" | "opaque" | "transparent"): number;
3578
+ encodePartial(pass: GPURenderPassEncoder, cmds: readonly RebasedCommand[], retained: readonly RetainedRecord[], regions: readonly FrameRect[], clearValue: {
3579
+ r: number;
3580
+ g: number;
3581
+ b: number;
3582
+ a: number;
3583
+ }, useOit: boolean, bakedBelow: readonly BakeRecord[], phase?: "all" | "opaque" | "transparent"): number;
3584
+ encodeExempt(pass: GPURenderPassEncoder, cmds: readonly RebasedCommand[] | null, glyphDraws: readonly GlyphDraw[] | null, base: FrameRect | null): number;
3585
+ encodeGlyphs(pass: GPURenderPassEncoder, draws: readonly GlyphDraw[]): number;
3586
+ encodeGlyphsOitEmit(pass: GPURenderPassEncoder, draws: readonly GlyphDraw[]): number;
3587
+ encodeSpritesOitEmit(pass: GPURenderPassEncoder, layers: readonly Layer[], bases: readonly number[]): number;
3588
+ encodeLayerSprites(pass: GPURenderPassEncoder, layers: readonly Layer[], bases: readonly number[], oitOpaqueOnly: boolean): number;
3589
+ encodeBufferLayer(pass: GPURenderPassEncoder, bl: BufferLayer): number;
3590
+ frameApplyScissor(pass: GPURenderPassEncoder, rect: FrameRect | null): void;
3591
+ cameraBindGroupForSpace(space: LayerSpace): GPUBindGroup;
3592
+ compositePipeline(): {
3593
+ pipeline: GPURenderPipeline;
3594
+ } | null;
3595
+ abufferHandle(): {
3596
+ resolveBindGroup: GPUBindGroup;
3597
+ } | null;
3598
+ frameAutoDamage(): AutoDamageState | null;
3599
+ setFrameAutoDamage(state: AutoDamageState): void;
3600
+ setAutoFullReason(reason: string | null): void;
3601
+ setAutoUpdateLayers(layers: readonly Layer[] | null): void;
3602
+ clearFrameProvenance(): void;
3603
+ noteAutoProvenance(perLayer: unknown): void;
3604
+ fingerprintDecide(args: {
3605
+ params: FingerprintParams;
3606
+ hasDamageRegions: boolean;
3607
+ fullFrame: boolean;
3608
+ capturing: boolean;
3609
+ }): {
3610
+ viewChanged: boolean;
3611
+ forcedReason: string | null;
3612
+ changedParts: FingerprintPart[] | undefined;
3613
+ visibilityChanged: boolean;
3614
+ partial: boolean;
3615
+ };
3616
+ resolveGpuTimer(): {
3617
+ timestampWrites: GPURenderPassTimestampWrites | undefined;
3618
+ resolve(encoder: GPUCommandEncoder): {
3619
+ read(): Promise<bigint>;
3620
+ } | null;
3621
+ } | null;
3622
+ drainPendingCompute(encoder: GPUCommandEncoder): void;
3623
+ refreshAutoSnapshots(): void;
3624
+ emitFrameDebug(facts: FrameDebugLite): void;
3625
+ }
3626
+ /**
3627
+ * The {@link FrameDebugFacts} fields the spine knows — everything EXCEPT
3628
+ * `autoFullReason` and `autoProvenance`, which the renderer fills from its own
3629
+ * state inside `emitFrameDebug` (they live in `_autoFullReason` /
3630
+ * `_debugRetention.autoProvenance`, host-owned). Built only while capturing.
3631
+ *
3632
+ * @internal
3633
+ */
3634
+ interface FrameDebugLite {
3635
+ partial: boolean;
3636
+ damageRegions: readonly FrameRect[] | null;
3637
+ allDrawLayers: readonly Layer[];
3638
+ layers: readonly (Layer | BufferLayer | CustomDrawable)[];
3639
+ bakedBelow: readonly BakeRecord[];
3640
+ bakedAbove: readonly BakeRecord[];
3641
+ forcedReason: string | null;
3642
+ viewChanged: boolean;
3643
+ changedParts: FingerprintPart[] | undefined;
3644
+ visibilityChanged: boolean;
3645
+ demoteReason: string | null;
3646
+ autoDerived: boolean;
3647
+ fullFrameOpt: boolean;
3648
+ annotation: {
3649
+ reason?: string;
3650
+ data?: Record<string, unknown>;
3651
+ } | undefined;
3652
+ totalMs: number;
3653
+ uploadMs: number;
3654
+ encodeMs: number;
3655
+ drawCalls: number;
3656
+ damageRects: number;
3657
+ }
3658
+ //#endregion
3659
+ //#region src/renderer.d.ts
3660
+ /** Per-frame options. Shaped like v1's `render(layers, { … })`. Supplying
3661
+ * `regions` on a persistent renderer whose composite view is unchanged since
3662
+ * the last frame activates the partial (damage-tracked) redraw path; `viewKey`
3663
+ * lets the caller fold its own group transforms + data-domain state into the
3664
+ * composite fingerprint (see {@link Renderer2D.viewFingerprint}). */
3665
+ interface RenderOptions {
3666
+ /**
3667
+ * Damage rects to repaint, in CSS pixels (`Bounds2D = {minX,minY,maxX,maxY}`).
3668
+ * On a persistent frame with an unchanged view these activate the partial
3669
+ * (damage-tracked) redraw path; otherwise they are ignored and a full repaint
3670
+ * runs. An empty / absent list always means a full frame.
3671
+ */
3672
+ regions?: readonly Bounds2D[];
3673
+ /** Force a full repaint this frame even if `regions` are supplied. */
3674
+ fullFrame?: boolean;
3675
+ /**
3676
+ * Ask core to DERIVE the damage regions itself instead of the caller passing
3677
+ * `regions` (decision D5). When `"auto"`, the renderer retains a per-layer
3678
+ * snapshot of the previous auto frame (pack version + instance count + a copy
3679
+ * of the per-instance world AABBs) and diffs it against the current pack to
3680
+ * project the small `old ∪ new` rects of whatever moved (P4-T3). The interact
3681
+ * demo's hand-rolled damage source is replaced by this.
3682
+ *
3683
+ * Precedence (D5): `fullFrame: true` > explicit `regions` > `damage: "auto"` >
3684
+ * default full. So `damage: "auto"` is consulted ONLY when no explicit
3685
+ * `regions` are supplied. The FIRST auto frame (or any frame with a layer
3686
+ * lacking a snapshot) is a full repaint that SEEDS the snapshots
3687
+ * (`fullCause.reason === "auto-damage-seed"`). A frame whose union damage
3688
+ * exceeds 60% of the canvas promotes to full (`"auto-damage-area"`); a layer
3689
+ * whose glyph version changed promotes to full (`"auto-damage-glyphs"`, D10 —
3690
+ * glyph packs carry no per-glyph AABBs to diff). Otherwise the derived rects
3691
+ * scissor a partial frame.
3692
+ */
3693
+ damage?: "auto";
3694
+ /**
3695
+ * Caller-supplied extension of the composite view fingerprint. The renderer
3696
+ * draws flat `UberPack`s and cannot see the scene's `Group` transforms or a
3697
+ * plot's data domain, so the caller (scene layer / plot / phylo) passes a
3698
+ * string that changes whenever ANY active group transform or data domain
3699
+ * changes. Folding it into the fingerprint fixes the old group-omission
3700
+ * footgun (groups silently excluded, forcing manual `forceFullFrame()`):
3701
+ * here a group move flips the fingerprint and forces a full frame
3702
+ * automatically.
3703
+ */
3704
+ viewKey?: string;
3705
+ /**
3706
+ * Optional caller-supplied annotation (decision D7) — the caller's INTENT for
3707
+ * this frame shown next to core's own decision in `FrameDebug.annotation`.
3708
+ * `reason` is a free-form tag (e.g. `"hover-moved"`, `"pan"`); `data` is
3709
+ * freeform extras. READ ONLY while the debug probe is capturing — the option
3710
+ * is never touched on the zero-cost path (the property access itself is guarded,
3711
+ * D8). Has no effect on rendering.
3712
+ */
3713
+ debug?: {
3714
+ reason?: string;
3715
+ data?: Record<string, unknown>;
3716
+ };
3717
+ }
3718
+ /** Construction-time options for {@link Renderer2D}. */
3719
+ interface RendererConstructorOptions {
3720
+ /** Partial config overriding {@link DEFAULT_CONFIG}. */
3721
+ config?: Partial<RendererConfig>;
3722
+ /**
3723
+ * The three render pipelines + shared bind-group layouts, created by the
3724
+ * caller via `createPipelines(root, format, assembleShader(...))`.
3725
+ * Dependency-injected so the renderer is unit-testable without a GPU.
3726
+ */
3727
+ pipelines: Pipelines;
3728
+ /** Initial camera state. Default `{ x: 0, y: 0, zoom: 1, rotation: 0 }`. */
3729
+ camera?: CameraState;
3730
+ /**
3731
+ * Initial clear color. Legacy side-channel: folded into config as
3732
+ * `initialBackground` at construction (it OVERRIDES `config.initialBackground`
3733
+ * when supplied). Prefer `config.initialBackground`. Default fully transparent.
3734
+ * Mutate the live value via {@link Renderer2D.setBackground}.
3735
+ */
3736
+ background?: Color;
3737
+ /**
3738
+ * Device-pixel ratio used to map CSS-pixel damage rects → device-pixel scissor
3739
+ * rects (the canvas backing store / backbuffer is in device pixels). Default
3740
+ * `1`. Also folded into the view fingerprint so a DPR change forces a full
3741
+ * frame. Legacy side-channel: folded into config as `initialDpr` at
3742
+ * construction (OVERRIDES `config.initialDpr` when supplied). Prefer
3743
+ * `config.initialDpr`. Update the live value via {@link Renderer2D.setDpr}.
3744
+ */
3745
+ dpr?: number;
3746
+ /**
3747
+ * Order-independent-transparency resources. **Required** when `config.oit` is
3748
+ * `true` (the {@link DEFAULT_CONFIG} default): the renderer never compiles
3749
+ * WGSL itself (cf. {@link pipelines}), so the per-pixel A-buffer and the
3750
+ * build/resolve pipelines are dependency-injected exactly like
3751
+ * {@link Pipelines}. Construct both with
3752
+ * `new ABuffer(device, { fragmentsPerPixel: config.oitFragmentsPerPixel })`
3753
+ * and `createOITPipelines(device, format, abuffer, shader, { cameraLayout,
3754
+ * instanceLayout })` (the OIT build layout is `[cameraLayout, instanceLayout,
3755
+ * abuffer.buildLayout]`, so the A-buffer must exist before the pipelines).
3756
+ * Ignored when `config.oit` is `false`; the renderer then routes transparent
3757
+ * commands through the sorted alpha-blend {@link Pipelines.transparent} pass.
3758
+ */
3759
+ oit?: {
3760
+ /** Per-pixel bounded-K A-buffer (heads + slots + viewport uniform). */abuffer: ABuffer; /** Build + resolve pipelines bound against the A-buffer + shared layouts. */
3761
+ pipelines: OITPipelines;
3762
+ };
3763
+ /**
3764
+ * The assembled uber-shader these pipelines were built from. OPTIONAL for the
3765
+ * core render path (the renderer never compiles WGSL), but REQUIRED to use
3766
+ * {@link Renderer2D.cacheLayer}: the RTT bake builds its own MSAA shape
3767
+ * pipelines from this same source (`createBakePipelines`). When omitted,
3768
+ * `cacheLayer` throws. {@link createRenderer} always supplies it.
3769
+ */
3770
+ shader?: AssembledShader;
3771
+ /**
3772
+ * Color-target format the pipelines were built against (the canvas format).
3773
+ * Used by {@link Renderer2D.cacheLayer} to build the bake + composite
3774
+ * pipelines at the same format. Legacy side-channel: folded into config as
3775
+ * `colorFormat` at construction (OVERRIDES `config.colorFormat` when
3776
+ * supplied). CONSTRAINED — must match the injected pipelines + swap chain
3777
+ * (HARD boundary 3). Prefer `config.colorFormat`. Defaults to
3778
+ * `navigator.gpu.getPreferredCanvasFormat()`.
3779
+ */
3780
+ format?: GPUTextureFormat;
3781
+ /**
3782
+ * Diagnostic logger for library output (warnings, GPU errors). Installed
3783
+ * process-wide via {@link setLogger} when supplied so the diagnostic sites in
3784
+ * unrelated modules pick it up too. Defaults to `console`.
3785
+ */
3786
+ logger?: Logger;
3787
+ }
3788
+ /**
3789
+ * Per-frame frustum-cull statistics for the LAST {@link Renderer2D.render} call,
3790
+ * read off the decoupled cull stage (`cull.ts`). Counts are over the DYNAMIC
3791
+ * concat instance buffer
3792
+ * only (the path the cull stage narrows); retained / baked / glyph draws live
3793
+ * outside the concat and are not counted here.
3794
+ *
3795
+ * When `config.cull` is `false`, `culled` is `false`, `instancesCulled` is `0`,
3796
+ * and `instancesDrawn === instancesTotal` (every command issues a full-span
3797
+ * draw). `drawRanges` reflects the number of split draws the cull emitted —
3798
+ * with cull off it equals `commandsTotal`.
3799
+ */
3800
+ interface CullStats {
3801
+ /** Total dynamic-concat instances considered this frame (before cull). */
3802
+ instancesTotal: number;
3803
+ /** Instances whose command survived the frustum cull (issued to draw). */
3804
+ instancesDrawn: number;
3805
+ /** Instances dropped by the frustum cull (`instancesTotal − instancesDrawn`). */
3806
+ instancesCulled: number;
3807
+ /** Draw commands gathered before cull narrowing. */
3808
+ commandsTotal: number;
3809
+ /** Draw ranges issued after cull narrowing (split draws against the concat). */
3810
+ drawRanges: number;
3811
+ /** Whether the frustum-cull stage actually ran this frame (`config.cull`). */
3812
+ culled: boolean;
3813
+ }
3814
+ declare class Renderer2D implements FrameHost {
3815
+ private readonly root;
3816
+ private readonly ctx;
3817
+ private readonly config;
3818
+ private readonly pipelines;
3819
+ private readonly abuffer;
3820
+ private readonly oitPipelines;
3821
+ private readonly _useOit;
3822
+ private readonly _cameraUniform;
3823
+ private readonly _instanceStaging;
3824
+ private readonly clearColorBuffer;
3825
+ private readonly clearColorData;
3826
+ private clearColorBindGroup;
3827
+ private readonly _emphasisBytes;
3828
+ private readonly _emphasisU32;
3829
+ private readonly _emphasisF32;
3830
+ private _debug;
3831
+ private _gpuTimer;
3832
+ private _emphasisMirror;
3833
+ private readonly _debugRetention;
3834
+ private _camera;
3835
+ private _background;
3836
+ /** CSS→device pixel scale for scissor mapping; folded into the fingerprint. */
3837
+ private _dpr;
3838
+ private readonly _fingerprint;
3839
+ private _autoDamage;
3840
+ private _autoUpdateLayers;
3841
+ private _autoFullReason;
3842
+ private _pendingCompute;
3843
+ private readonly _shader;
3844
+ private readonly _format;
3845
+ private readonly _bakeCache;
3846
+ private readonly _retainedLayers;
3847
+ private readonly _lastCullStats;
3848
+ private get _compositePipelines();
3849
+ /** Exposed for tests that inspect r["_bakes"] directly. Delegates to BakeCache. */
3850
+ private get _bakes();
3851
+ private readonly _interopEncoder;
3852
+ private readonly _spriteEncoder;
3853
+ private readonly _glyphEncoder;
3854
+ constructor(owner: GPUOwner, canvas: HTMLCanvasElement, options: RendererConstructorOptions);
3855
+ get width(): number;
3856
+ get height(): number;
3857
+ /**
3858
+ * The canvas this renderer draws into. Exposed so tooling holding only a
3859
+ * renderer (e.g. via {@link onRendererCreated}) can anchor DOM overlays.
3860
+ */
3861
+ get canvas(): HTMLCanvasElement;
3862
+ /**
3863
+ * Read-only snapshot of the live A-buffer (OIT) configuration, or `null` when
3864
+ * `config.oit` is `false` (the sorted alpha-blend fallback). Side-effect free —
3865
+ * reads already-resident state for tooling (e.g. `insomni-devtools`). The
3866
+ * fields are STATIC config + the current slots-buffer allocation; this does NOT
3867
+ * touch the GPU or read back per-pixel occupancy.
3868
+ *
3869
+ * - `K` — `config.oitFragmentsPerPixel` (per-pixel fragment budget).
3870
+ * - `maxK` — {@link RESOLVE_MAX_K} compile-time resolve cap.
3871
+ * - `slotsBytes` — current slots-buffer size (`width*height*K*SLOT_BYTES`),
3872
+ * reflecting the live backbuffer extent.
3873
+ */
3874
+ get oitInfo(): {
3875
+ enabled: true;
3876
+ K: number;
3877
+ maxK: number;
3878
+ slotsBytes: number;
3879
+ } | null;
3880
+ resize(w: number, h: number): void;
3881
+ setCamera(state: CameraState): void;
3882
+ setBackground(color: Color): void;
3883
+ /** Set the CSS→device pixel ratio used for scissor mapping and the fingerprint. */
3884
+ setDpr(dpr: number): void;
3885
+ /**
3886
+ * Set the global-emphasis state (Phase 4, decisions.md D2). Instances whose
3887
+ * `lane4` emphasis key equals `focusedKey` render at full alpha; every other
3888
+ * (`transparent` / OIT) instance is multiplied by `mix(1, dimAlpha, t)`.
3889
+ *
3890
+ * EXEMPT-ZERO (T-ZBAKE companion change): an instance with key `0` (the default
3891
+ * `UberPack.append` writes — i.e. anything that never opted into emphasis, such
3892
+ * as axis / grid / overlay shapes) is NEVER dimmed, regardless of `t`. Only
3893
+ * instances tagged with a key ≥ 1 participate. So `focusedKey: 0` does NOT focus
3894
+ * the default instances — key 0 is the opt-out sentinel, not a focus target;
3895
+ * `focusedKey: 0, t: 1` dims every TAGGED instance and focuses none.
3896
+ *
3897
+ * This writes ONLY the 16-byte emphasis uniform — no instance repack — so
3898
+ * animating `t` from 0→1 is a one-`writeBuffer` per frame. `t === 0` (the
3899
+ * default, and the `dimAlpha` default of `1`) is an exact no-op.
3900
+ *
3901
+ * The opaque pass and text are intentionally NOT dimmed: dimming reduces alpha
3902
+ * (a transparent effect), and the opaque pass's `a < 0.5` discard would punch
3903
+ * holes. Emphasis-dimmable marks must therefore render in the transparent/OIT
3904
+ * path (the same path plot's hover-dim already uses).
3905
+ *
3906
+ * Does NOT itself trigger a redraw — the caller renders after updating
3907
+ * emphasis. Partial-redraw note (P4-T1, D6): emphasis is global GPU state the
3908
+ * per-instance damage diff cannot observe, so it is folded into the view
3909
+ * fingerprint (`viewFingerprint`) — a `setEmphasis` change between frames flips
3910
+ * the fingerprint and forces the next frame full (`changed: ["emphasis"]`),
3911
+ * which is what makes `damage:"auto"` sound without caller cooperation. A
3912
+ * caller that already passes its own `regions` covering every dimmed/focused
3913
+ * instance still works (its frame is just promoted to full); hand-gating
3914
+ * full-frames-while-animating becomes redundant, not broken.
3915
+ */
3916
+ setEmphasis(state: {
3917
+ focusedKey?: number;
3918
+ dimAlpha?: number;
3919
+ t?: number;
3920
+ }): void;
3921
+ /**
3922
+ * Project the union of `layer`'s instances at `indices` into a single padded
3923
+ * CSS-px damage region (Phase 4, P4-T2) — feed it to `render({ regions })` to
3924
+ * repaint exactly the changed marks (a focused-point halo, a bring-to-front,
3925
+ * an emphasis dim/undim). Uses the layer's live space view-projection (the
3926
+ * SAME `viewProjectionForSpace` the draw uses), so the rect tracks the current
3927
+ * camera/zoom/rotation. Returns `null` when no in-range index contributes
3928
+ * (nothing to repaint). For several far-apart clusters, call once per cluster.
3929
+ */
3930
+ regionsFromLayerAabbs(layer: Layer, indices: Iterable<number>, pad?: number): Bounds2D | null;
3931
+ /**
3932
+ * Stateless opt-in DPR probe (HARD boundary 4). Returns the host's
3933
+ * `window.devicePixelRatio` (or `1` when `window` is absent — SSR / Node /
3934
+ * worker). The renderer NEVER calls this itself: the APP reads it and applies
3935
+ * the result via {@link setDpr}, so device-pixel-ratio policy stays the app's
3936
+ * decision and the renderer has no hidden `window` dependency. `static`
3937
+ * because it reads no instance state.
3938
+ * @param maxDpr Optional cap. Recommended: `Math.min(detectDpr(), 2)` on mobile
3939
+ * (fragment cost scales as DPR²). Pass this result to {@link setDpr}.
3940
+ */
3941
+ static detectDpr(): number;
3942
+ /**
3943
+ * Force the next `render()` to a full (loadOp:"clear") repaint, ignoring any
3944
+ * damage `regions`. The composite fingerprint already covers camera, group
3945
+ * transforms (via `viewKey`), dpr, size, and background; call this only for a
3946
+ * backbuffer-content change the fingerprint can't observe (e.g. an external
3947
+ * blit into the backbuffer, or a forced first frame).
3948
+ */
3949
+ forceFullFrame(): void;
3950
+ /**
3951
+ * Record a one-shot full-frame override AND its kebab-case `reason` (for
3952
+ * `FrameDebug.fullReason`). Delegates to {@link ViewFingerprint.forceFull}.
3953
+ */
3954
+ private _forceFull;
3955
+ /**
3956
+ * The core-owned render-debug probe (decision D1) — the single source of truth
3957
+ * for render-debug data. Lazily constructed on first access; constructing it
3958
+ * does NOT enable capture (zero-cost-off, D8) — a presenter calls
3959
+ * `renderer.debug.enable()` / `onFrame(...)` to start capturing. Replaces the v1
3960
+ * public `setFrameDebug` / `config.onFrameDebug` (deleted).
3961
+ */
3962
+ get debug(): RendererDebug;
3963
+ /**
3964
+ * Whether the probe exists AND is capturing this frame. The single zero-cost-off
3965
+ * gate read once per `render()`: with no probe (or an idle one) the renderer
3966
+ * builds no FrameDebug record, retains no layer array / fingerprint parts, and
3967
+ * reads no `cssBox` from the DOM.
3968
+ */
3969
+ private get _capturing();
3970
+ /**
3971
+ * Release the per-frame debug retention — the caller layer array, SpaceMap,
3972
+ * AND the prev-frame fingerprint parts. Called by the probe when it stops
3973
+ * capturing (disable / last listener removed) so no scene graph or part
3974
+ * record is held while nothing is watching (the zero-cost invariant).
3975
+ * Delegates layer/SpaceMap/autoProvenance release to {@link DebugRetention}.
3976
+ */
3977
+ private releaseDebugRetention;
3978
+ /**
3979
+ * Build the narrow {@link DebugHost} the probe drives. All methods are invoked
3980
+ * by the probe ONLY while capturing; keeping them in a closure avoids exposing
3981
+ * renderer internals on the public surface.
3982
+ */
3983
+ private _debugHost;
3984
+ /**
3985
+ * Queue a compute callback for the next {@link render}. The callback receives
3986
+ * the frame's `GPUCommandEncoder`; open one or more `beginComputePass()` on it
3987
+ * and dispatch. Callbacks run in submission order, immediately before the
3988
+ * render pass (and after the OIT heads-clear), so their outputs are visible to
3989
+ * any draw in the same frame — including a {@link BufferLayer} reading a buffer
3990
+ * the compute pass just wrote.
3991
+ *
3992
+ * The caller owns its compute pipelines, bind groups, ping-pong, and dispatch
3993
+ * order. This is the v3 equivalent of v1's `compute(encoder => …)`, minus the
3994
+ * eager submit: nothing is dispatched until {@link render} is called (which
3995
+ * drains the queue). Mirrors `clearFrame` in being recorded onto the frame
3996
+ * encoder before `beginRenderPass` — a `beginComputePass` inside a render pass
3997
+ * is illegal, so this hook is the supported place for it.
3998
+ *
3999
+ * IMPORTANT (partial-redraw contract): a compute pass that mutates a buffer a
4000
+ * {@link BufferLayer} draws MUST bump that layer's `version` (or the renderer
4001
+ * cannot tell the GPU bytes changed and a damage frame will ghost). See
4002
+ * {@link BufferLayer.version}.
4003
+ */
4004
+ compute(cb: (encoder: GPUCommandEncoder) => void): void;
4005
+ /**
4006
+ * The `@group(0)` camera bind-group layout (the 6-float `Camera { vpCol0,
4007
+ * vpCol1, vpCol2 }` uniform, visible to the vertex stage). Exposed so a
4008
+ * consumer that builds its OWN camera-bound draw (a `CustomDrawable`, a
4009
+ * particle pipeline) can bind against the renderer's live per-space camera
4010
+ * slots without re-deriving the layout. Pair with {@link cameraBindGroupForSpace}
4011
+ * to get the matching bind group for a coordinate space.
4012
+ */
4013
+ getCameraBindGroupLayout(): GPUBindGroupLayout;
4014
+ /** The active GPU device. Exposed so a `CustomDrawable` can build pipelines. */
4015
+ get device(): GPUDevice;
4016
+ /** The renderer's color attachment format — a custom pipeline's target format. */
4017
+ get colorFormat(): GPUTextureFormat;
4018
+ /**
4019
+ * The camera bind group for a coordinate `space` — bound against
4020
+ * {@link getCameraBindGroupLayout} over the renderer's shared per-frame camera
4021
+ * buffer at that space's slot. A pan/zoom rewrites only the camera buffer, so
4022
+ * this bind group is stable across frames; a caller-built draw binds it at
4023
+ * `@group(0)`.
4024
+ */
4025
+ cameraBindGroupForSpace(space: LayerSpace): GPUBindGroup;
4026
+ /** Current resolved camera state (live `_camera`, fully defaulted). */
4027
+ getCamera(): {
4028
+ x: number;
4029
+ y: number;
4030
+ zoom: number;
4031
+ rotation: number;
4032
+ };
4033
+ /**
4034
+ * World→screen view matrix for the live camera (CSS-pixel screen space), via
4035
+ * the shared `worldToCssMatrix`. The world camera baseline is CSS px (zoom=1 ⇒
4036
+ * 1 world unit = 1 CSS px), so this builds `viewFromCamera` over the CSS-px
4037
+ * viewport (`ctx.width/height ÷ dpr`) — independent of dpr in its output. This
4038
+ * is the `world`-space VIEW (camera) matrix, NOT the full NDC view-projection;
4039
+ * `screenToWorld` / `worldToScreen` invert / apply it. (For the NDC matrix a
4040
+ * draw uploads, see `viewProjectionForSpace`.)
4041
+ */
4042
+ getViewMatrix(): Mat3;
4043
+ /**
4044
+ * Map a CSS-pixel screen point to world coordinates (inverse of the live
4045
+ * world-space view matrix). Returns the input unchanged if the view matrix is
4046
+ * singular (zoom 0) — matching v1's `screenToWorld` fallback.
4047
+ */
4048
+ screenToWorld(sx: number, sy: number): Vec2;
4049
+ /** Map a world point to its CSS-pixel screen position (forward view matrix). */
4050
+ worldToScreen(wx: number, wy: number): Vec2;
4051
+ /**
4052
+ * Set the live camera so `bounds` fits the viewport (optional padding factor
4053
+ * in (0,1]). Reuses the shared `cameraStateForBounds` solver, then routes
4054
+ * through `setCamera` so the result is a normal live-camera mutation (a moved
4055
+ * camera flips the view fingerprint → next frame is a full repaint).
4056
+ *
4057
+ * The world camera baseline is CSS px, so the solver is fed the **CSS-px**
4058
+ * viewport (`ctx.width/height ÷ dpr`) — the resulting `zoom` is then in CSS-px
4059
+ * units and renders identically across dpr (the renderer applies dpr once,
4060
+ * matrix-side, in `viewProjectionForSpace`).
4061
+ */
4062
+ fitCameraToBounds(bounds: Bounds2D, options?: FitCameraToBoundsOptions): void;
4063
+ /**
4064
+ * Bake a layer's shapes + glyphs into an offscreen single-sample texture and
4065
+ * register it, so subsequent `render()` calls COMPOSITE the snapshot instead
4066
+ * of re-packing / re-rasterizing the layer's geometry every frame. The
4067
+ * composite is z-aware (T-ZBAKE): the bake lands UNDER all live geometry when
4068
+ * its z-band sits below the first live layer, OVER all live geometry when it
4069
+ * sits above the last, and is DEMOTED back to a live draw for any frame in
4070
+ * which it is sandwiched between two live layers (z-order is always honored).
4071
+ * The bake also draws the layer's MSDF glyphs INSIDE the bake pass
4072
+ * (`tiers/bake.ts`) — so a cached layer's text is part of the snapshot and
4073
+ * does not desynchronize during pan/zoom.
4074
+ *
4075
+ * The bake runs at `config.msaaBakes`× MSAA and resolves into a canvas-sized
4076
+ * single-sample texture. `bakeViewProjection(layer.space, …)` FREEZES the
4077
+ * camera at cache time for a `world`-space layer; `ui`/`device` layers bake in
4078
+ * their own coordinate space. A subsequent camera move does NOT re-bake the
4079
+ * layer — the caller must `uncacheLayer` + `cacheLayer` to refresh.
4080
+ *
4081
+ * Requires the renderer to have been constructed with an assembled `shader`
4082
+ * (use {@link createRenderer}, which supplies it) — the bake builds its own
4083
+ * MSAA shape pipelines from that source. Throws otherwise.
4084
+ *
4085
+ * Caching a layer forces the next frame to a full repaint (a new composited
4086
+ * texture changes pixels the damage fingerprint cannot observe).
4087
+ */
4088
+ cacheLayer(layer: Layer, managed?: boolean): void;
4089
+ /**
4090
+ * Drop a layer's bake: destroy the bake texture + composite uniform, remove
4091
+ * the registration (so `render()` resumes rasterizing the layer's pack live),
4092
+ * and force the next frame to a full repaint (the composited pixels vanish
4093
+ * from outside any damage rect). No-op when the layer is not cached.
4094
+ */
4095
+ uncacheLayer(layer: Layer): void;
4096
+ /** True iff `layer`'s pack has a registered bake (composited, not live). */
4097
+ isCached(layer: Layer): boolean;
4098
+ /**
4099
+ * Cached-layer textures the memory-budget policy evicted on the most recent
4100
+ * `render()` (P3-T3). Empty when nothing was evicted. Surfaced so a caller /
4101
+ * test can confirm what the budget dropped rather than mistaking a silent
4102
+ * eviction for "everything stayed cached". Returns a copy.
4103
+ */
4104
+ get lastEvictedCaches(): readonly {
4105
+ space: LayerSpace;
4106
+ bytes: number;
4107
+ }[];
4108
+ /**
4109
+ * Bakes DEMOTED on the most recent `render()` (T-ZBAKE): a layer whose pack
4110
+ * has a registered bake but which sits SANDWICHED between two live layers in
4111
+ * the z-sorted stack, so its bake could not be composited soundly (it would
4112
+ * either occlude or be occluded incorrectly relative to the live layers it is
4113
+ * meant to interleave with). A demoted bake stays LIVE for that frame — its
4114
+ * pack still holds the CPU bytes, so the live draw is visually identical to the
4115
+ * composite. Empty when no bake was sandwiched. Surfaced (mirroring
4116
+ * {@link lastEvictedCaches}) so a caller / test can confirm a bake was kept
4117
+ * live rather than mistaking it for a cache miss. Returns a copy.
4118
+ */
4119
+ get lastDemotedBakes(): readonly {
4120
+ space: LayerSpace;
4121
+ }[];
4122
+ /**
4123
+ * Frustum-cull statistics for the most recent {@link render} call, read off
4124
+ * the decoupled cull stage (`cull.ts`). Returned BY COPY so a caller can hold
4125
+ * the value
4126
+ * across frames without it mutating under them. See {@link CullStats} for the
4127
+ * field semantics (counts are over the dynamic concat buffer only).
4128
+ */
4129
+ get lastCullStats(): CullStats;
4130
+ /**
4131
+ * Mark `layer` RETAINED under a stable `key`. A retained layer hands its
4132
+ * `UberPack` to the {@link RetainedTier}, which keeps a STABLE GPU instance
4133
+ * buffer for it. On subsequent `render()` calls a retained pack is NOT
4134
+ * re-concatenated into the dynamic instance buffer or re-uploaded — it draws
4135
+ * as a DISTINCT appended draw over its own stable buffer (design option B:
4136
+ * separate bound draw, NOT in the live concat, because the concat's
4137
+ * `@builtin(instance_index)` mapping and per-frame z stamp shift as the live
4138
+ * set changes size frame to frame).
4139
+ *
4140
+ * Re-upload happens ONLY when `key` changes (or the pack's `version` advances
4141
+ * under the same key) — the unchanged-key path is a pure draw from the stable
4142
+ * buffer (zero `writeBuffer`), which is the entire performance win. The
4143
+ * internal painter order of a retained pack is FROZEN at build time via
4144
+ * `stampRetainedOrder`. When `_retainedLayers.farBand` is on (default), the
4145
+ * frozen order is mapped into the RESERVED FAR BAND `[Z_SPLIT, 1)` of the
4146
+ * global NDC depth domain so the OIT opaque-occlusion gate (which compares a
4147
+ * live glyph's depth against the retained pack's opaque depth) compares
4148
+ * COMMENSURABLE values — without this, the two depths live in incommensurable
4149
+ * private domains and the gate silently drops all labels behind a retained
4150
+ * branch. Live shapes are compressed into `(0, Z_SPLIT)` the same frame,
4151
+ * preserving all live-vs-live occlusion while ensuring live glyphs have
4152
+ * `z < Z_SPLIT <= retainedZ`.
4153
+ *
4154
+ * **Precondition for `_retainedLayers.farBand` correctness**: all retained content
4155
+ * must be z-BEHIND all live content (i.e. no retained instance should occlude a
4156
+ * live-transparent fragment from above). All current callers satisfy this
4157
+ * invariant (phylo overzoom is the bottom of the z-stack; the navigator
4158
+ * overview is clipped to a minimap rect). A future caller that needs retained
4159
+ * content z-INTERLEAVED with live geometry should use Strategy 2 (per-record
4160
+ * viewport `minDepth/maxDepth` depth-range remap in `encodeRetained`) instead
4161
+ * of `retainLayer`.
4162
+ *
4163
+ * Retained content stacks ABOVE the dynamic concat by submission order
4164
+ * (accepted; the renderer does NOT z-interleave a retained buffer with dynamic
4165
+ * geometry — this is the same semantics as before, now made explicit).
4166
+ *
4167
+ * Calling `retainLayer` with a NEW key for an already-retained layer evicts
4168
+ * the old stable buffer and rebuilds. Mirrors the `cacheLayer`/`_bakes`
4169
+ * ownership model (keyed by `layer.pack`); the next frame is forced full (a
4170
+ * newly-built retained draw changes pixels the damage fingerprint can't see).
4171
+ *
4172
+ * T-CULLNAV: pass `{ spatialIndex: true }` (the v3 `cacheLayer({ spatialIndex
4173
+ * })` equivalent) to build a cell-binned grid over the pack's per-instance
4174
+ * AABBs. The pack's instance bytes are REORDERED into cell-contiguous slot
4175
+ * order before upload, and on a `world`-space frame the renderer range-queries
4176
+ * the grid against the viewport frustum and issues one draw per visible
4177
+ * cell-run — collapsing a huge baked overzoom tile into a handful of draws.
4178
+ * The reorder permutes WHICH survivors draw, never their relative painter
4179
+ * order (the frozen per-slot z is re-stamped over the reordered slots), so the
4180
+ * retained buffer still stacks by submission and never joins the dynamic
4181
+ * `z = 1 − (gi+0.5)/N` stamp. Below the grid's `minItems` (or for degenerate
4182
+ * bounds) the index is skipped and the pack draws as one full-span draw.
4183
+ */
4184
+ retainLayer(layer: Layer, key: string, opts?: {
4185
+ spatialIndex?: boolean;
4186
+ }): void;
4187
+ /**
4188
+ * Drop a layer's retention: evict its stable GPU buffer from the
4189
+ * {@link RetainedLayers} (and its inner {@link RetainedTier}), remove the
4190
+ * registration so `render()` resumes drawing the layer live through the
4191
+ * dynamic concat, and force the next frame full. No-op when the layer is not
4192
+ * retained.
4193
+ */
4194
+ unretainLayer(layer: Layer): void;
4195
+ /** True iff `layer`'s pack is registered as retained (drawn from a stable buffer). */
4196
+ isRetained(layer: Layer): boolean;
4197
+ /**
4198
+ * PHASE-4 — heterogeneous z-order reconciliation (the correctness crux).
4199
+ *
4200
+ * The renderer stamps SHAPE `order` in a GLOBAL cross-layer painter domain:
4201
+ * shape global index `g ∈ [0, N)` (N = `totalInstances`, all live shapes
4202
+ * concatenated in draw order) maps to `order = 1 - (g + 0.5)/N`. That global
4203
+ * domain is what makes the shared depth buffer occlude opaque shapes ACROSS
4204
+ * layers (layer 0's shapes are farther than layer 1's). `Layer.finalizeOrder()`
4205
+ * instead works in a PER-LAYER domain (`1 - (i + 0.5)/T`, T = the layer's own
4206
+ * shape+glyph+sprite count) — a DIFFERENT scale. The two must be reconciled so
4207
+ * a glyph/sprite lands at the correct depth relative to BOTH its own layer's
4208
+ * shapes and the whole live stack.
4209
+ *
4210
+ * Reconciliation: each layer's shapes occupy global indices `[packBase,
4211
+ * packBase + instInPack)`. `finalizeOrder()` returns `shapeOrderSeq` — each
4212
+ * pack shape's PER-LAYER interleave seq (ascending in pack order) — and stamps
4213
+ * each glyph/sprite a per-layer local order `L = 1 - (s + 0.5)/T` (T = the
4214
+ * layer's shape+glyph+sprite count). From `L` we recover the glyph/sprite's
4215
+ * local seq `s` and count how many of the layer's SHAPES were pushed before it
4216
+ * (`shapesBefore` = #`shapeOrderSeq` < s). That places it at the global
4217
+ * fractional shape index `packBase + shapesBefore - 0.25`: just FARTHER than
4218
+ * the layer's `shapesBefore`-th shape and just NEARER than its
4219
+ * `(shapesBefore-1)`-th — exactly its push position relative to the layer's
4220
+ * shapes — then `order = 1 - (fracIndex + 0.5)/N`. The `-0.25` breaks the
4221
+ * depth-test tie so an opaque shape pushed AFTER a glyph (nearer) OCCLUDES it
4222
+ * and one pushed BEFORE (farther) is OCCLUDED BY it.
4223
+ *
4224
+ * A glyph-/sprite-only layer (no shape seqs) maps every instance to
4225
+ * `packBase - 0.25`: just NEARER than the last shape of all PRIOR layers and
4226
+ * just FARTHER than the first shape of the next — correct painter order for a
4227
+ * text/sprite-only band.
4228
+ *
4229
+ * `N` (the denominator) is the FULL un-culled shape count, so a glyph's order
4230
+ * is independent of frustum-cull survivor densification — stable across a pure
4231
+ * pan, which keeps the version-gated glyph upload cache sound (the cull may
4232
+ * re-densify SHAPE depth without touching glyph order). Under heavy cull a
4233
+ * glyph follows the un-culled painter scale while drawn shapes are densified;
4234
+ * relative order among ON-SCREEN (surviving) neighbors is preserved
4235
+ * (densification is monotonic), so the interleave a viewer can SEE is correct.
4236
+ * Documented approximation; exact survivor-densified glyph depth is a follow-up
4237
+ * if a measured artifact appears.
4238
+ *
4239
+ * @param ranges Per-live-layer `{ layer, packBase, instInPack }` in draw order.
4240
+ * `packBase` = count of shapes in all prior live layers.
4241
+ * @param n Total live shape count N (the global painter denominator).
4242
+ * @param retainedPresent When true AND `_retainedLayers.farBand` is on,
4243
+ * glyph/sprite `order` values are compressed by `* Z_SPLIT` so they land
4244
+ * inside `(0, Z_SPLIT)` — the same band as live shapes — and remain strictly
4245
+ * below the retained far band `[Z_SPLIT, 1)`. Without this compression a
4246
+ * glyph-only layer (e.g. `labelLayer`) would compute an `order` near 1.0
4247
+ * (full-range denominator), which the OIT gate would wrongly compare against
4248
+ * a retained branch's far-band depth and silently drop. The `ORDER_EPS` clamp
4249
+ * is applied AFTER compression so it pins to the compressed open range
4250
+ * `(0, Z_SPLIT)`.
4251
+ */
4252
+ private _stampHeterogeneousOrder;
4253
+ /**
4254
+ * Draw one frame. Packs from `layers` are uploaded in submission order into
4255
+ * one storage buffer; their `DrawCommand`s are issued in a single render
4256
+ * pass — opaque first, then transparent, with the explicit per-instance
4257
+ * `order` as the sole depth source.
4258
+ *
4259
+ * `opts` mirrors v1's `render(layers, { … })`. When `opts.regions` are
4260
+ * supplied on a persistent renderer whose composite view is unchanged since
4261
+ * the last frame, the frame takes the partial (damage-tracked) path:
4262
+ * `loadOp:"load"` preserves untouched pixels and only the padded damage rects
4263
+ * are erased + repainted under a scissor. Any view change (camera, group
4264
+ * transforms via `viewKey`, dpr, size, background), an absent/empty
4265
+ * `regions`, `fullFrame:true`, `forceFullFrame()`, or a non-persistent
4266
+ * renderer falls back to a full (`loadOp:"clear"`) repaint.
4267
+ *
4268
+ * `layers` may also include {@link BufferLayer}s (GPU-resident buffers from a
4269
+ * compute pass). A BufferLayer CANNOT join the concatenated dynamic instance
4270
+ * buffer (it owns its own buffer with the v1 schema), so it draws as a DISTINCT
4271
+ * bound draw in a post-main-pass region, stacking ABOVE all dynamic geometry +
4272
+ * bakes by submission order (no z-interleave). A BufferLayer's `version` /
4273
+ * `count` / buffer identity are folded into the partial-redraw fingerprint, so
4274
+ * a changed buffer demotes the frame to full automatically — provided the
4275
+ * caller bumps `version` when its compute pass mutates the buffer.
4276
+ */
4277
+ render(layers: readonly (Layer | BufferLayer | CustomDrawable)[], opts?: RenderOptions): void;
4278
+ /** @internal */
4279
+ get frameRoot(): {
4280
+ device: GPUDevice;
4281
+ };
4282
+ /** @internal */
4283
+ get frameCtx(): FrameHost["frameCtx"];
4284
+ /** @internal */
4285
+ get frameConfig(): FrameHost["frameConfig"];
4286
+ /** @internal */
4287
+ get frameAbuffer(): FrameHost["frameAbuffer"];
4288
+ /** @internal */
4289
+ get frameOitPipelines(): FrameHost["frameOitPipelines"];
4290
+ /** @internal */
4291
+ get frameUseOit(): boolean;
4292
+ /** @internal */
4293
+ get frameCamera(): {
4294
+ x: number;
4295
+ y: number;
4296
+ zoom: number;
4297
+ rotation: number;
4298
+ };
4299
+ /** @internal */
4300
+ get frameDpr(): number;
4301
+ /** @internal */
4302
+ get frameBackground(): Color;
4303
+ /** @internal */
4304
+ get frameEmphasisMirror(): {
4305
+ focusedKey: number;
4306
+ dimAlpha: number;
4307
+ t: number;
4308
+ };
4309
+ /** @internal */
4310
+ get frameRetainedFarBand(): boolean;
4311
+ /** @internal */
4312
+ get frameCapturing(): boolean;
4313
+ /** @internal */
4314
+ get cameraUniformData(): Float32Array;
4315
+ /** @internal */
4316
+ cameraUniformSlotStrideFloats(): number;
4317
+ /** @internal */
4318
+ get cameraUniformSlotStride(): number;
4319
+ /** @internal */
4320
+ cameraUniformBuffer(): GPUBuffer;
4321
+ /** @internal */
4322
+ ensureCameraSlots(count: number): void;
4323
+ /** @internal */
4324
+ ensureStaging(floats: number): void;
4325
+ /** @internal */
4326
+ stagingArray(): Float32Array;
4327
+ /** @internal */
4328
+ ensureCullAabbs(instances: number): Float32Array;
4329
+ /** @internal */
4330
+ ensureInstanceCapacity(bytes: number): void;
4331
+ /** @internal */
4332
+ instanceBuffer(): GPUBuffer;
4333
+ /** @internal */
4334
+ hasBakes(): boolean;
4335
+ /** @internal */
4336
+ hasRetained(): boolean;
4337
+ /** @internal */
4338
+ bakeFor(pack: object): BakeRecord | undefined;
4339
+ /** @internal */
4340
+ retainedFor(pack: object): RetainedRecord | undefined;
4341
+ /** @internal */
4342
+ bakeCacheApplyCachePolicy(layers: readonly Layer[]): void;
4343
+ /** @internal */
4344
+ bakeCacheClearDemotions(): void;
4345
+ /** @internal */
4346
+ bakeCacheRecordDemotion(space: LayerSpace): void;
4347
+ /** @internal */
4348
+ get cullStats(): CullStats;
4349
+ /** @internal */
4350
+ stampHeterogeneousOrder(ranges: readonly {
4351
+ layer: Layer;
4352
+ packBase: number;
4353
+ instInPack: number;
4354
+ }[], n: number, retainedPresent: boolean): void;
4355
+ /** @internal */
4356
+ glyphUploadInstances(layers: readonly Layer[], sig: string, device: GPUDevice): {
4357
+ glyphDraws: GlyphDraw[];
4358
+ };
4359
+ /** @internal */
4360
+ glyphPackId(pack: object): number;
4361
+ /** @internal */
4362
+ uploadLayerSprites(layers: readonly Layer[]): number[];
4363
+ /** @internal */
4364
+ encodeFull(pass: GPURenderPassEncoder, cmds: readonly RebasedCommand[], retained: readonly RetainedRecord[], useOit: boolean, bakedBelow: readonly BakeRecord[], phase?: "all" | "opaque" | "transparent"): number;
4365
+ /** @internal */
4366
+ encodePartial(pass: GPURenderPassEncoder, cmds: readonly RebasedCommand[], retained: readonly RetainedRecord[], regions: readonly FrameRect[], clearValue: {
4367
+ r: number;
4368
+ g: number;
4369
+ b: number;
4370
+ a: number;
4371
+ }, useOit: boolean, bakedBelow: readonly BakeRecord[], phase?: "all" | "opaque" | "transparent"): number;
4372
+ /** @internal */
4373
+ encodeExempt(pass: GPURenderPassEncoder, cmds: readonly RebasedCommand[] | null, glyphDraws: readonly GlyphDraw[] | null, base: FrameRect | null): number;
4374
+ /** @internal */
4375
+ encodeGlyphs(pass: GPURenderPassEncoder, draws: readonly GlyphDraw[]): number;
4376
+ /** @internal */
4377
+ frameApplyScissor(pass: GPURenderPassEncoder, rect: FrameRect | null): void;
4378
+ /** @internal */
4379
+ encodeGlyphsOitEmit(pass: GPURenderPassEncoder, draws: readonly GlyphDraw[]): number;
4380
+ /** @internal */
4381
+ encodeSpritesOitEmit(pass: GPURenderPassEncoder, layers: readonly Layer[], bases: readonly number[]): number;
4382
+ /** @internal */
4383
+ encodeLayerSprites(pass: GPURenderPassEncoder, layers: readonly Layer[], bases: readonly number[], oitOpaqueOnly: boolean): number;
4384
+ /** @internal */
4385
+ encodeBufferLayer(pass: GPURenderPassEncoder, bl: BufferLayer): number;
4386
+ /** @internal */
4387
+ abufferHandle(): {
4388
+ resolveBindGroup: GPUBindGroup;
4389
+ } | null;
4390
+ /** @internal */
4391
+ compositePipeline(): {
4392
+ pipeline: GPURenderPipeline;
4393
+ } | null;
4394
+ /** @internal */
4395
+ frameAutoDamage(): AutoDamageState | null;
4396
+ /** @internal */
4397
+ setFrameAutoDamage(state: AutoDamageState): void;
4398
+ /** @internal */
4399
+ setAutoFullReason(reason: string | null): void;
4400
+ /** @internal */
4401
+ setAutoUpdateLayers(layers: readonly Layer[] | null): void;
4402
+ /** @internal */
4403
+ clearFrameProvenance(): void;
4404
+ /** @internal */
4405
+ noteAutoProvenance(perLayer: unknown): void;
4406
+ /** @internal */
4407
+ fingerprintDecide(args: {
4408
+ params: FingerprintParams;
4409
+ hasDamageRegions: boolean;
4410
+ fullFrame: boolean;
4411
+ capturing: boolean;
4412
+ }): {
4413
+ viewChanged: boolean;
4414
+ forcedReason: string | null;
4415
+ changedParts: FingerprintPart[] | undefined;
4416
+ visibilityChanged: boolean;
4417
+ partial: boolean;
4418
+ };
4419
+ /** @internal */
4420
+ resolveGpuTimer(): {
4421
+ timestampWrites: GPURenderPassTimestampWrites | undefined;
4422
+ resolve(encoder: GPUCommandEncoder): {
4423
+ read(): Promise<bigint>;
4424
+ } | null;
4425
+ } | null;
4426
+ /** @internal */
4427
+ drainPendingCompute(encoder: GPUCommandEncoder): void;
4428
+ /** @internal */
4429
+ refreshAutoSnapshots(): void;
4430
+ /** @internal */
4431
+ emitFrameDebug(f: FrameDebugLite): void;
4432
+ /**
4433
+ * Assemble + dispatch the {@link FrameDebug} record — see {@link emitFrameDebug}.
4434
+ * Called ONLY when the probe is capturing (the caller guards). Delegates
4435
+ * SpaceMap + Layer retention to {@link DebugRetention} via `capture()`.
4436
+ */
4437
+ private _emitFrameDebug;
4438
+ /** Release renderer-owned GPU resources. The renderer is unusable after. */
4439
+ destroy(): void;
4440
+ /**
4441
+ * Bind the camera bind group for `slot` at `@group(0)` unless it is already
4442
+ * bound. Returns the now-bound slot so the caller can thread it through a loop
4443
+ * (commands are grouped by layer, so the rebind fires only at layer/space
4444
+ * boundaries — typically once or twice per frame). Pass `bound = -1` to force
4445
+ * a bind (e.g. after the clear-quad rebinds `@group(0)`).
4446
+ */
4447
+ private _bindCameraSlot;
4448
+ /**
4449
+ * Build the {@link EncodeCtx} the encode tier (`tiers/encode.ts`) draws
4450
+ * against: read-only renderer handles plus the scissor/camera/clear-color
4451
+ * primitives bound to this renderer. Constructed fresh per encode call (a
4452
+ * handful per frame) so it always reflects lazily-built pipelines / bind
4453
+ * groups; allocating a small handle object is negligible against the draws.
4454
+ */
4455
+ private _encodeCtx;
4456
+ /** Full-frame encode — see {@link encodeFull}. */
4457
+ private _encodeFull;
4458
+ /** Partial (damage-tracked) encode — see {@link encodePartial}. */
4459
+ private _encodePartial;
4460
+ /** OIT-exempt overlay encode — see {@link encodeExempt}. */
4461
+ private _encodeExempt;
4462
+ /** Upload the premultiplied background into the clear-quad uniform. */
4463
+ private _writeClearColor;
4464
+ /**
4465
+ * Set the scissor for one command within a padded damage rect. Returns
4466
+ * `false` (skip the draw) when the command carries a non-null `clipRect` whose
4467
+ * intersection with the padded rect is zero-area. A `null`/absent clip never
4468
+ * culls (the padded rect passes through).
4469
+ */
4470
+ private _scissorForCommand;
4471
+ /**
4472
+ * Set a device-pixel scissor from a CSS-pixel rect. `null` restores the full
4473
+ * attachment. CSS→device uses `_dpr`; the rect is floored on the near corner
4474
+ * and ceiled on the far corner (so a damage rect never under-covers a partial
4475
+ * device pixel), then clamped to the attachment bounds.
4476
+ */
4477
+ private applyScissor;
4478
+ /**
4479
+ * Lazily build + cache the clear-quad bind group. The clear-quad pipeline uses
4480
+ * `layout:"auto"`, so its group-0 layout is only available off the compiled
4481
+ * pipeline (`getBindGroupLayout(0)`), not from the shared explicit layouts.
4482
+ */
4483
+ private _ensureClearColorBindGroup;
4484
+ /**
4485
+ * Ensure the camera buffer + bind groups hold at least `count` slots (P3-T8
4486
+ * per-(space × group) routing). Delegates growth to {@link CameraUniform.ensureSlots}.
4487
+ *
4488
+ * When the buffer is reallocated (ensureSlots returns `true`), any bind group
4489
+ * built against the OLD buffer (the live-glyph + interop per-slot camera bind
4490
+ * groups) is invalidated and reset to `null` here so it rebuilds lazily against
4491
+ * the new buffer on its next use. A group-tagged draw is rare relative to a pure
4492
+ * pan; this grow happens at most a handful of times before the pool stabilizes.
4493
+ */
4494
+ private _ensureCameraSlots;
4495
+ /**
4496
+ * Facade: encode the live MSDF glyph draws (direct-color path). Delegates to
4497
+ * {@link GlyphEncoder.encode}. Called on the non-OIT main pass and the
4498
+ * post-resolve EXEMPT pass.
4499
+ */
4500
+ private _encodeGlyphs;
4501
+ /**
4502
+ * Facade: encode glyph OIT-emit appends into the A-buffer build sweep.
4503
+ * Delegates to {@link GlyphEncoder.encodeOitEmit}. The binding-3 opaqueDepth
4504
+ * gate is preserved byte-identically (see `GlyphEncoder.encodeOitEmit` docs).
4505
+ */
4506
+ private _encodeGlyphsOitEmit;
4507
+ /** Facade: delegate BufferLayer encode to the interop encoder. */
4508
+ private _encodeBufferLayer;
4509
+ /** Facade: upload all sprite-bearing layers' packs and return base offsets. */
4510
+ private _uploadLayerSprites;
4511
+ /** Facade: draw per-texture sprite commands in the post-main pass. */
4512
+ private _encodeLayerSprites;
4513
+ /** Facade: encode transparent-sprite OIT-emit appends into the A-buffer. */
4514
+ private _encodeSpritesOitEmit;
4515
+ }
4516
+ /**
4517
+ * @internal Test helper — used only by unit tests that supply pre-built
4518
+ * `Pipelines` without a real GPU. Application code should use
4519
+ * {@link createRenderer}, which assembles the shader, pipelines, and
4520
+ * (when `config.oit` is on) the OIT A-buffer automatically.
4521
+ */
4522
+ declare function createTestRenderer(owner: GPUOwner, canvas: HTMLCanvasElement, pipelines: Pipelines, config?: Partial<RendererConfig>): Renderer2D;
4523
+ /** Construction options for {@link createRenderer} — the batteries-included
4524
+ * factory. Everything is optional; the defaults give a working OIT renderer. */
4525
+ interface RendererOptions {
4526
+ /** Partial config overriding {@link DEFAULT_CONFIG}. */
4527
+ config?: Partial<RendererConfig>;
4528
+ /** Initial camera state. */
4529
+ camera?: CameraState;
4530
+ /** Initial clear color. */
4531
+ background?: Color;
4532
+ /** CSS→device pixel ratio. */
4533
+ dpr?: number;
4534
+ /** Diagnostic logger for library output. Defaults to `console`. */
4535
+ logger?: Logger;
4536
+ /**
4537
+ * Color-target format the pipelines + A-buffer are built against. Defaults to
4538
+ * `navigator.gpu.getPreferredCanvasFormat()` — the same format
4539
+ * `createCanvasContext` configures the swap chain with — so the assembled
4540
+ * pipelines match the canvas the renderer renders into.
4541
+ */
4542
+ format?: GPUTextureFormat;
4543
+ }
4544
+ /**
4545
+ * Batteries-included v3 renderer factory. Given a GPU owner + canvas it does the
4546
+ * full assembly the renderer constructor intentionally does NOT do (the
4547
+ * constructor takes pre-built pipelines so it stays GPU-compilation-free and
4548
+ * unit-testable):
4549
+ *
4550
+ * 1. `assembleShader(ORDERED_KINDS, INSTANCE_LAYOUT.wgslAccessor)` — one
4551
+ * uber-shader from every kind in `typeFlag` order.
4552
+ * 2. `createPipelines(root, format, shader)` — opaque / transparent /
4553
+ * clear-quad pipelines + shared camera/instance layouts.
4554
+ * 3. When `config.oit` is on (the {@link DEFAULT_CONFIG} default): an
4555
+ * {@link ABuffer} sized to the canvas + `createOITPipelines(...)`, injected
4556
+ * into the constructor so the default config renders order-independent
4557
+ * transparency out of the box. When `config.oit` is `false` no OIT
4558
+ * resources are built and the renderer uses the sorted alpha-blend path.
4559
+ *
4560
+ * The advanced DI path (`new Renderer2D(owner, canvas, { pipelines, oit })`)
4561
+ * stays available for callers that share pipelines across renderers or supply
4562
+ * their own A-buffer.
4563
+ */
4564
+ declare function createRenderer(owner: GPUOwner, canvas: HTMLCanvasElement, options?: RendererOptions): Renderer2D;
4565
+ //#endregion
4566
+ export { RendererConfig as $, SEGMENT_FLOATS as $n, CurveRecord as $t, SpatialGridResult as A, TYPE_SEGMENT as An, mixColor as Ar, TextStyleTable as At, FrameDebugListener as B, ARC_FLOATS as Bn, STROKE_MAX_POINTS as Bt, ABuffer as C, PrimitiveKind as Cn, hex as Cr, GlyphPackFlags as Ct, SLOT_BYTES as D, TYPE_ELLIPSE as Dn, hsv as Dr, TextOutline as Dt, RESOLVE_MAX_K as E, TYPE_CURVE as En, hslToRgba as Er, TextGradient as Et, CullResult as F, INSTANCE_LAYOUT as Fn, SpriteShape as Ft, LayerSummary as G, CURVE_FLOATS as Gn, packStrokeFlags as Gt, FRAME_RING_CAPACITY as H, CURVE_CAP_BUTT as Hn, StrokeInstanceRecord as Ht, CulledRange as I, deriveLayout as In, DrawCommand as It, DebugRect as J, EllipseShape as Jn, TriangleRecord as Jt, summarize as K, CircleShape as Kn, stroke as Kt, cullCommands as L, INSTANCE_BYTES as Ln, UberPack as Lt, buildSpatialGrid as M, TYPE_STROKE_PATH as Mn, withAlpha as Mr, SPRITE_FLOATS as Mt, queryRanges as N, TYPE_TRIANGLE as Nn, SpriteCommand as Nt, SpatialGrid as O, TYPE_GLYPH as On, lerpColor as Or, TextShadow as Ot, CullCommand as P, WorldAABB as Pn, SpritePack as Pt, LayerDebugInfo as Q, RectShape as Qn, CurveCap as Qt, worldFrustumAabb as R, INSTANCE_FIELDS as Rn, nextByteCapacity as Rt, createOITPipelines as S, assembleShader as Sn, fade as Sr, GlyphPack as St, HEAD_BYTES as T, TYPE_CIRCLE as Tn, hslToRgb as Tr, BlockGeometry as Tt, FrameRing as U, CURVE_CAP_ROUND as Un, StrokePathShape as Ut, RendererDebug as V, ArcShape as Vn, STROKE_SEGMENTS_PER_INSTANCE as Vt, FrameSummary as W, CURVE_CAP_SQUARE as Wn, chunkStroke as Wt, FrameDebug as X, PolygonOpts as Xn, ArcRecord as Xt, FingerprintPart as Y, LineShape as Yn, triangle as Yt, FrameTiming as Z, PolygonShape$1 as Zn, arc as Zt, BufferLayerKind as _, getLogger as _n, Color as _r, GroupedTriangleRecord as _t, RendererOptions as a, CircleRecord as an, SegmentShape as ar, convertRect as at, OITPipelineDeps as b, createPipelines as bn, cssHex as br, createLayer as bt, onRendererCreated as c, rect as cn, LineCap as cr, GroupedArcRecord as ct, CustomDrawableContext as d, project as dn, RectRingOptions as dr, GroupedEllipseRecord as dt, curve as en, SHAPE_BYTES as er, DebugSize as et, CustomDrawableOptions as f, Group as fn, TessellatedTriangle as fr, GroupedRectRecord as ft, BufferLayerInstancesOptions as g, Logger as gn, BLACK as gr, GroupedTextShape as gt, BufferLayer as h, resolveGroupTransform as hn, tessellatePolyline as hr, GroupedStrokeShape as ht, RendererConstructorOptions as i, ellipse as in, SPRITE_FLOATS$1 as ir, captureSpaceMap as it, SpatialRange as j, TYPE_SPRITE as jn, rgba as jr, SPRITE_BYTES as jt, SpatialGridOptions as k, TYPE_RECT as kn, lighten as kr, TextStyle as kt, rendererForCanvas as l, LayerSpace as ln, LineJoin as lr, GroupedCircleRecord as lt, isCustomDrawable as m, createGroup as mn, polylineRectRing as mr, GroupedShape as mt, RenderOptions as n, segment as nn, SHAPE_ELLIPSE as nr, RectXYWH as nt, createRenderer as o, circle as on, TRIANGLE_FLOATS as or, CacheHint as ot, createCustomDrawable as p, GroupOptions as pn, polylineEllipseRing as pr, GroupedSegmentRecord as pt, DEFAULT_CONFIG as q, CurveShape as qn, strokeInstanceCount as qt, Renderer2D as r, EllipseRecord as rn, SHAPE_RECT as rr, SpaceMap as rt, createTestRenderer as s, RectRecord as sn, EllipseRingOptions as sr, GroupedAnchoredStringShape as st, CullStats as t, SegmentRecord as tn, SHAPE_CIRCLE as tr, DebugSpace as tt, CustomDrawable as u, ProjectOptions as un, PolylineShape as ur, GroupedCurveRecord as ut, BufferLayerOptions as v, setLogger as vn, TRANSPARENT as vr, Layer as vt, ABufferOptions as w, TYPE_ARC as wn, hsl as wr, GlyphTextShape as wt, OITPipelines as x, AssembledShader as xn, darken as xr, GlyphMetricsOut as xt, createBufferLayer as y, Pipelines as yn, WHITE as yr, LayerOptions as yt, AABB as z, INSTANCE_FLOATS as zn, ORDERED_KINDS as zt };