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,892 @@
1
+ import { i as ShapeSchema, o as SpriteSchema } from "./pipeline-DE3a1Pnk.mjs";
2
+ import { $ as RendererConfig, $n as SEGMENT_FLOATS, A as SpatialGridResult, B as FrameDebugListener, Bn as ARC_FLOATS, C as ABuffer, D as SLOT_BYTES, F as CullResult, G as LayerSummary, Gn as CURVE_FLOATS, H as FRAME_RING_CAPACITY, Hn as CURVE_CAP_BUTT, I as CulledRange, It as DrawCommand, K as summarize, L as cullCommands, Lt as UberPack, M as buildSpatialGrid, N as queryRanges, O as SpatialGrid, P as CullCommand, R as worldFrustumAabb, Rt as nextByteCapacity, S as createOITPipelines, St as GlyphPack, T as HEAD_BYTES, U as FrameRing, Un as CURVE_CAP_ROUND, V as RendererDebug, W as FrameSummary, Wn as CURVE_CAP_SQUARE, _r as Color, at as convertRect, b as OITPipelineDeps, er as SHAPE_BYTES, et as DebugSize, fn as Group, it as captureSpaceMap, j as SpatialRange, k as SpatialGridOptions, ln as LayerSpace, nr as SHAPE_ELLIPSE, nt as RectXYWH, or as TRIANGLE_FLOATS, r as Renderer2D, rr as SHAPE_RECT, rt as SpaceMap, s as createTestRenderer, tr as SHAPE_CIRCLE, tt as DebugSpace, un as ProjectOptions, vt as Layer, x as OITPipelines, xn as AssembledShader } from "./renderer-DzZqd1bY.mjs";
3
+ import { N as FrameRect, S as Mat3, n as CameraState, t as Bounds2D } from "./camera-view-DHmMiKvP.mjs";
4
+ import { _ as resolveRoot, g as GPUOwner } from "./text-font-D7GGDtTK.mjs";
5
+ import { D as SpatialRenderer2D, E as SpatialPointerEvent, S as Scheduler, T as SpatialFrameTiming, b as SceneRoot, g as Rect, i as DirtyRegion, m as PointerRouter, r as DirtyChannel, t as Damage, v as SceneNode } from "./spatial-Bd3Ay8I2.mjs";
6
+ import { StorageFlag, TgpuBindGroup, TgpuBuffer, TgpuRoot, d } from "typegpu";
7
+
8
+ //#region src/shared/dynamic-buffer.d.ts
9
+ declare function nextPow2(value: number): number;
10
+ //#endregion
11
+ //#region src/gpu/spatial-hash.d.ts
12
+ declare const SPATIAL_HASH_WORKGROUP_SIZE = 64;
13
+ declare const SPATIAL_HASH_PARAMS_BYTES = 32;
14
+ declare const SPATIAL_HASH_PARAMS_WGSL = "\nstruct SpatialHashParams {\n rangeStart: u32,\n rangeCount: u32,\n bucketMask: u32,\n bucketCount: u32,\n invCellSize: f32,\n weight: f32,\n _pad0: f32,\n _pad1: f32,\n};\n\nconst EMPTY_SLOT: u32 = 0xffffffffu;\nconst DEAD_CELL_COORD: i32 = 2147483647;\n\nfn hashCell(cell: vec2i, mask: u32) -> u32 {\n let x = bitcast<u32>(cell.x);\n let y = bitcast<u32>(cell.y);\n var h = x * 0x9e3779b9u;\n h = (h << 6u) ^ (h >> 2u) ^ (y * 0x85ebca6bu);\n return h & mask;\n}\n\nfn worldCell(pos: vec2f, invCellSize: f32) -> vec2i {\n return vec2i(floor(pos * invCellSize));\n}\n";
15
+ declare const CLEAR_SPATIAL_HASH_WGSL = "\nstruct SpatialHashParams {\n rangeStart: u32,\n rangeCount: u32,\n bucketMask: u32,\n bucketCount: u32,\n invCellSize: f32,\n weight: f32,\n _pad0: f32,\n _pad1: f32,\n};\n\nconst EMPTY_SLOT: u32 = 0xffffffffu;\nconst DEAD_CELL_COORD: i32 = 2147483647;\n\nfn hashCell(cell: vec2i, mask: u32) -> u32 {\n let x = bitcast<u32>(cell.x);\n let y = bitcast<u32>(cell.y);\n var h = x * 0x9e3779b9u;\n h = (h << 6u) ^ (h >> 2u) ^ (y * 0x85ebca6bu);\n return h & mask;\n}\n\nfn worldCell(pos: vec2f, invCellSize: f32) -> vec2i {\n return vec2i(floor(pos * invCellSize));\n}\n\n@group(0) @binding(0) var<storage, read_write> heads: array<atomic<u32>>;\n@group(0) @binding(1) var<uniform> params: SpatialHashParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3u) {\n let i = gid.x;\n if (i >= params.bucketCount) {\n return;\n }\n atomicStore(&heads[i], EMPTY_SLOT);\n}\n";
16
+ declare const BUILD_SPATIAL_HASH_WGSL = "\nstruct SpatialHashParams {\n rangeStart: u32,\n rangeCount: u32,\n bucketMask: u32,\n bucketCount: u32,\n invCellSize: f32,\n weight: f32,\n _pad0: f32,\n _pad1: f32,\n};\n\nconst EMPTY_SLOT: u32 = 0xffffffffu;\nconst DEAD_CELL_COORD: i32 = 2147483647;\n\nfn hashCell(cell: vec2i, mask: u32) -> u32 {\n let x = bitcast<u32>(cell.x);\n let y = bitcast<u32>(cell.y);\n var h = x * 0x9e3779b9u;\n h = (h << 6u) ^ (h >> 2u) ^ (y * 0x85ebca6bu);\n return h & mask;\n}\n\nfn worldCell(pos: vec2f, invCellSize: f32) -> vec2i {\n return vec2i(floor(pos * invCellSize));\n}\n\n@group(0) @binding(0) var<storage, read_write> heads: array<atomic<u32>>;\n@group(0) @binding(1) var<storage, read_write> next: array<u32>;\n@group(0) @binding(2) var<storage, read_write> cells: array<vec2i>;\n@group(0) @binding(3) var<storage, read> nodeIds: array<u32>;\n@group(0) @binding(4) var<storage, read> positions: array<vec4f>;\n@group(0) @binding(5) var<storage, read> alive: array<u32>;\n@group(0) @binding(6) var<uniform> params: SpatialHashParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3u) {\n let role = gid.x;\n if (role >= params.rangeCount) {\n return;\n }\n\n let entry = params.rangeStart + role;\n let nodeId = nodeIds[entry];\n next[entry] = EMPTY_SLOT;\n\n if (alive[nodeId] == 0u) {\n cells[entry] = vec2i(DEAD_CELL_COORD);\n return;\n }\n\n let cell = worldCell(positions[nodeId].xy, params.invCellSize);\n cells[entry] = cell;\n let bucket = hashCell(cell, params.bucketMask);\n let prev = atomicExchange(&heads[bucket], entry);\n next[entry] = prev;\n}\n";
17
+ //#endregion
18
+ //#region src/pack/region-index.d.ts
19
+ /** A byte range within the `UberPack` buffer that must be re-uploaded. */
20
+ interface DirtyRange {
21
+ /** Byte offset of the dirty span within the `UberPack` buffer. */
22
+ byteOffset: number;
23
+ /** Byte length of the dirty span. */
24
+ byteLength: number;
25
+ }
26
+ /**
27
+ * Maps group identities to their byte spans within the `UberPack` buffer, and
28
+ * maintains an accumulated set of dirty byte ranges for the current frame.
29
+ *
30
+ * Default region granularity = one `Group` object. Callers that do not use
31
+ * explicit group tracking can pass a single sentinel object as `groupId`.
32
+ *
33
+ * Usage per frame:
34
+ * ```ts
35
+ * regions.reset();
36
+ * for (const [group, items] of scene.byGroup()) {
37
+ * regions.beginRegion(group, pack.byteLength);
38
+ * for (const item of items) pack.append(...);
39
+ * regions.endRegion(group, pack.byteLength);
40
+ * }
41
+ * // At mutation time:
42
+ * regions.markDirty(hoveredGroup);
43
+ * // Before upload:
44
+ * const ranges = regions.coalesced();
45
+ * regions.clearDirty();
46
+ * ```
47
+ */
48
+ declare class RegionIndex {
49
+ /** group identity → byte span in the current pack. */
50
+ private _spans;
51
+ /** Accumulated dirty ranges for this frame (may overlap / be out of order). */
52
+ private _dirty;
53
+ /**
54
+ * Record the byte offset at which a group's instances BEGIN being packed.
55
+ * `byteOffset` is the value of `UberPack.byteLength` BEFORE the first
56
+ * `append` call for this group.
57
+ */
58
+ beginRegion(groupId: object, byteOffset: number): void;
59
+ /**
60
+ * Record the byte offset at which a group's instances END being packed.
61
+ * `byteOffset` is the value of `UberPack.byteLength` AFTER the last
62
+ * `append` call for this group.
63
+ */
64
+ endRegion(groupId: object, byteOffset: number): void;
65
+ /**
66
+ * Reset all span records for a new frame. Call before re-packing. Does NOT
67
+ * clear dirty ranges (use `clearDirty` for that).
68
+ */
69
+ reset(): void;
70
+ /**
71
+ * Mark a group's byte span as dirty. If the group has no recorded span
72
+ * (e.g. not packed this frame), this is a no-op.
73
+ */
74
+ markDirty(groupId: object): void;
75
+ /**
76
+ * Return the minimal, sorted, non-overlapping set of dirty byte ranges.
77
+ *
78
+ * - Output is sorted ascending by `byteOffset`.
79
+ * - Adjacent or overlapping input ranges are merged into a single output range.
80
+ * - Non-destructive: does not mutate the internal `_dirty` list.
81
+ * - Returns an empty array when no ranges are dirty.
82
+ */
83
+ coalesced(): DirtyRange[];
84
+ /**
85
+ * Clear accumulated dirty ranges. Call after the GPU upload is complete.
86
+ * Does NOT reset span records — use `reset()` for that.
87
+ */
88
+ clearDirty(): void;
89
+ /**
90
+ * Return the recorded byte span for a group, or `undefined` if the group
91
+ * was not packed this frame. For testing / debugging only.
92
+ */
93
+ spanOf(groupId: object): {
94
+ start: number;
95
+ end: number;
96
+ } | undefined;
97
+ }
98
+ //#endregion
99
+ //#region src/pack/repack-v1.d.ts
100
+ /**
101
+ * Repack `count` v1-stride segment records (`SEGMENT_FLOATS` floats each) into a
102
+ * freshly-allocated v3 instance buffer (`count * INSTANCE_FLOATS` floats).
103
+ * Byte-identical to looping `Layer.pushSegment` over the same records.
104
+ */
105
+ declare function repackSegments(src: ArrayLike<number>, count: number): Float32Array;
106
+ /**
107
+ * Repack `count` v1-stride curve records (`CURVE_FLOATS` floats each) into a
108
+ * freshly-allocated v3 instance buffer. Byte-identical to looping
109
+ * `Layer.pushCurve` over the same records.
110
+ */
111
+ declare function repackCurves(src: ArrayLike<number>, count: number): Float32Array;
112
+ /**
113
+ * Repack `count` v1-stride arc records (`ARC_FLOATS` floats each) into a
114
+ * freshly-allocated v3 instance buffer. Byte-identical to looping
115
+ * `Layer.pushArc` over the same records.
116
+ */
117
+ declare function repackArcs(src: ArrayLike<number>, count: number): Float32Array;
118
+ //#endregion
119
+ //#region src/target/blit.d.ts
120
+ /**
121
+ * Copy the entire persistent backbuffer onto the swap-chain texture.
122
+ *
123
+ * Both textures share the same format, size, and sample count (single-sample),
124
+ * so this is a raw texel copy — no sampler, blend, or premultiply round-trip.
125
+ *
126
+ * Always copies the full `width × height`. Never pass a sub-rect: a partial
127
+ * copy leaves pixels outside the rect cleared on the presented physical buffer
128
+ * (see the module header). Width/height are in DEVICE pixels (the backbuffer /
129
+ * swap-chain texel dimensions).
130
+ *
131
+ * @param encoder Command encoder for the current frame (the copy is recorded
132
+ * after `pass.end()` and before `device.queue.submit`).
133
+ * @param src Persistent backbuffer texture (`ctx.backbuffer`).
134
+ * @param dst Swap-chain texture (`ctx.gpuContext.getCurrentTexture()`).
135
+ * @param width Device-pixel width of both textures (`ctx.width`).
136
+ * @param height Device-pixel height of both textures (`ctx.height`).
137
+ */
138
+ declare function blitBackbuffer(encoder: GPUCommandEncoder, src: GPUTexture, dst: GPUTexture, width: number, height: number): void;
139
+ //#endregion
140
+ //#region src/tiers/retained.d.ts
141
+ /**
142
+ * Metadata for a single UberPack's stable GPU buffer. Stored in the
143
+ * WeakMap keyed on the pack itself.
144
+ */
145
+ interface RetainedRecord {
146
+ /** Stable GPU buffer — NOT re-uploaded when damage is clear. */
147
+ gpuBuffer: GPUBuffer;
148
+ /** Byte capacity of gpuBuffer. */
149
+ gpuCapacityBytes: number;
150
+ /** Frames elapsed since the start of the current shrink window. */
151
+ framesSinceCheck: number;
152
+ /** Peak byte usage observed within the current shrink window. */
153
+ windowPeakBytes: number;
154
+ }
155
+ /**
156
+ * WeakMap-keyed retained tier: manages stable GPU instance buffers for
157
+ * `UberPack` instances whose paint and data damage bits are both clear.
158
+ *
159
+ * Lifecycle per frame:
160
+ * ```ts
161
+ * if (!tier.has(pack)) {
162
+ * tier.build(pack); // damage present — reallocate/upload
163
+ * }
164
+ * tier.tickShrink(pack); // always call at frame end
165
+ * const rec = tier.get(pack)!; // use rec.gpuBuffer for draw calls
166
+ * ```
167
+ */
168
+ declare class RetainedTier {
169
+ private readonly root;
170
+ private readonly records;
171
+ constructor(root: TgpuRoot);
172
+ /**
173
+ * Returns `true` iff a retained record exists for `pack` (damage-miss path:
174
+ * caller skips `build`). The RetainedTier itself never suppresses a build
175
+ * — that decision belongs to the caller.
176
+ */
177
+ has(pack: UberPack): boolean;
178
+ /**
179
+ * Retrieve the retained record for `pack` without triggering a build.
180
+ * Returns `undefined` if `has(pack)` is false.
181
+ */
182
+ get(pack: UberPack): RetainedRecord | undefined;
183
+ /**
184
+ * Build or rebuild the GPU buffer for `pack`. Call only when paint/data
185
+ * damage is present.
186
+ *
187
+ * - Reuses the existing buffer if it is large enough (range write only).
188
+ * - Reallocates with the pow2-then-1.25× policy when the buffer is too small.
189
+ */
190
+ build(pack: UberPack): RetainedRecord;
191
+ /**
192
+ * Advance the shrink window for `pack`. Call once per frame per retained
193
+ * pack (regardless of whether `build` was called this frame).
194
+ *
195
+ * After `SHRINK_WINDOW_FRAMES` consecutive frames where the peak byte usage
196
+ * is below `gpuCapacityBytes / SHRINK_PEAK_RATIO`, the GPU buffer is
197
+ * reallocated to a smaller size.
198
+ */
199
+ tickShrink(pack: UberPack): void;
200
+ /**
201
+ * Destroy the GPU buffer for `pack` and remove its record. Call when the
202
+ * layer is uncached or destroyed.
203
+ */
204
+ evict(pack: UberPack): void;
205
+ /**
206
+ * Drop all retained state. This method is intentionally a no-op: the WeakMap
207
+ * holds no strong references and cannot iterate to destroy GPU buffers — callers
208
+ * MUST call `evict(pack)` for every registered pack before calling `destroy()`.
209
+ * After this call the `RetainedTier` is unusable.
210
+ *
211
+ * Contract: all GPU buffers are the owner's responsibility. `destroy()` does
212
+ * not destroy them; failing to evict before drop leaks GPU memory.
213
+ */
214
+ destroy(): void;
215
+ }
216
+ //#endregion
217
+ //#region src/tiers/tile-replay.d.ts
218
+ /** Screen-device-px AABB used by {@link TileGrid.assign}. Mirrors the v3
219
+ * `WorldAABB` shape (`kinds/kind.ts`) but in already-projected screen pixels. */
220
+ interface ScreenAABB {
221
+ minX: number;
222
+ minY: number;
223
+ maxX: number;
224
+ maxY: number;
225
+ }
226
+ /**
227
+ * Row-major grid of command buckets over screen device-px space. The grid is
228
+ * built once per layout: every command is assigned to the tiles its dilated
229
+ * AABB touches, and a dirty-rect query returns the union of the touched tiles'
230
+ * buckets.
231
+ */
232
+ declare class TileGrid {
233
+ /** Tile edge length in device px (== {@link RendererConfig.tileSize}). */
234
+ readonly cellSize: number;
235
+ readonly cols: number;
236
+ readonly rows: number;
237
+ /** Flat row-major array: `buckets[tileId]` = cmdIndex list (insertion order). */
238
+ private readonly buckets;
239
+ constructor(canvasW: number, canvasH: number, cellSize: number);
240
+ /** Row-major tile id for a `(col, row)` cell. */
241
+ tileId(col: number, row: number): number;
242
+ /**
243
+ * Rasterize an AABB (device px, half-open `[min, max)`) to the tile ids it
244
+ * touches. The AABB is dilated by +1px on every side before rasterizing so a
245
+ * shape's analytic anti-aliased fringe (which spills ≤1px past the geometric
246
+ * edge) is conservatively included — a shape sitting on a tile seam lands in
247
+ * both tiles. Out-of-range cells are clamped to the grid; a fully off-grid
248
+ * AABB yields an empty list.
249
+ */
250
+ tilesForAabb(minX: number, minY: number, maxX: number, maxY: number): number[];
251
+ /**
252
+ * Rasterize a {@link FrameRect} (CSS px) to a tile-id set. The rect is first
253
+ * padded by 1px — the SAME `padFrameRect(rect, 1)` the T22 clear-quad step
254
+ * applies — then converted to device px via `dpr`, then handed to
255
+ * {@link tilesForAabb} (which adds its own +1px AA dilation). The query rect
256
+ * thus covers at least the pixels the per-rect erase will actually repaint.
257
+ */
258
+ tilesForRect(rect: FrameRect, dpr: number): Set<number>;
259
+ /**
260
+ * Assign a command (by its index in the renderer's command array) to every
261
+ * tile its screen AABB intersects. The same +1px AA dilation as
262
+ * {@link tilesForAabb} applies, so a command straddling a tile boundary is
263
+ * dropped into every touched bucket — the dirty-rect query then issues it
264
+ * exactly once per region pass (via {@link commandsForTiles} dedup).
265
+ */
266
+ assign(cmdIndex: number, aabb: ScreenAABB): void;
267
+ /**
268
+ * Union of command indices across a set of tile ids, deduplicated and sorted
269
+ * ascending. Ascending = original scene/submission order, so the caller's
270
+ * opaque-reverse sweep (`for i = n-1 … 0`) and transparent-forward sweep match
271
+ * v1's painter stacking. A command in two dirty tiles appears once.
272
+ */
273
+ commandsForTiles(tileIds: Set<number>): number[];
274
+ /** Total tile count (`cols × rows`). */
275
+ get totalTiles(): number;
276
+ }
277
+ /**
278
+ * Full-frame fallback threshold. When the dirty-tile fraction exceeds this,
279
+ * {@link dirtyTileSet} returns `null` so the caller skips bucket replay and runs
280
+ * a single full-frame pass instead — thousands of scissor switches across most
281
+ * of the canvas cost more than one unscissored re-render.
282
+ */
283
+ declare const FULL_FRAME_TILE_FRACTION = 0.5;
284
+ /**
285
+ * Map damage regions to the set of tile ids to replay. Unions the tiles each
286
+ * region touches (via {@link TileGrid.tilesForRect}). Returns `null` — NOT an
287
+ * empty set — when the dirty fraction strictly exceeds
288
+ * {@link FULL_FRAME_TILE_FRACTION}, signalling "fall back to a full frame". An
289
+ * empty/zero-area region set returns an empty `Set` (nothing to replay), which
290
+ * the caller distinguishes from `null`.
291
+ */
292
+ declare function dirtyTileSet(grid: TileGrid, regions: readonly FrameRect[], dpr: number): Set<number> | null;
293
+ //#endregion
294
+ //#region src/damage/spine.d.ts
295
+ interface FrameSpineOptions {
296
+ /**
297
+ * Scheduler implementation. Defaults to RAFScheduler (production). Pass a
298
+ * ManualScheduler (or createTestSpine) for deterministic unit tests.
299
+ */
300
+ scheduler?: Scheduler;
301
+ /**
302
+ * Called each flush with the coalesced dirty regions. Fires only when at
303
+ * least one non-empty mark() was accumulated since the last flush.
304
+ */
305
+ onFlush: (regions: DirtyRegion) => void;
306
+ }
307
+ /**
308
+ * Frame-loop primitive for v3. Owns a DirtyChannel<DirtyRegion> wired to a
309
+ * Scheduler. Callers mark damage via mark(); the channel coalesces and
310
+ * schedules a single flush that invokes onFlush with the union of all marked
311
+ * regions. Empty flushes (no mark since last pump) are suppressed by the
312
+ * DirtyChannel itself.
313
+ *
314
+ * T61's GpuSceneRoot owns one FrameSpine and delegates damage marking to it.
315
+ */
316
+ declare class FrameSpine {
317
+ #private;
318
+ readonly channel: DirtyChannel<DirtyRegion>;
319
+ readonly scheduler: Scheduler;
320
+ constructor({
321
+ scheduler,
322
+ onFlush
323
+ }: FrameSpineOptions);
324
+ /**
325
+ * Accumulate damage and schedule a flush. An empty regions array is a
326
+ * fast-path no-op — nothing is scheduled.
327
+ */
328
+ mark(regions: DirtyRegion): void;
329
+ /**
330
+ * Dispose the spine. Removes the subscriber and cancels any pending
331
+ * scheduled flush.
332
+ */
333
+ dispose(): void;
334
+ }
335
+ /**
336
+ * Creates a FrameSpine backed by a ManualScheduler. Call pump() to drain
337
+ * any pending flush synchronously.
338
+ *
339
+ * @example
340
+ * const { spine, pump } = createTestSpine((r) => calls.push(r));
341
+ * spine.mark([{ rect: { x: 0, y: 0, w: 10, h: 10 }, kind: "paint" }]);
342
+ * pump(); // fires onFlush once
343
+ */
344
+ declare function createTestSpine(onFlush: (r: DirtyRegion) => void): {
345
+ spine: FrameSpine;
346
+ pump: () => void;
347
+ };
348
+ //#endregion
349
+ //#region src/pipelines/glyph-pipelines.d.ts
350
+ /**
351
+ * Per-bake glyph parameters uniform. 16 bytes (one vec4f slot):
352
+ * pxRange — atlas distance range in texels (`GlyphAtlas.pxRange`).
353
+ * atlasSize — atlas side length in texels (`GlyphAtlas.atlasSize`).
354
+ * _pad0, _pad1 — reserved (keep the uniform 16-byte aligned).
355
+ *
356
+ * Both values are constant for one atlas, so one upload per bake is enough.
357
+ */
358
+ declare const GLYPH_PARAMS_FLOATS = 4;
359
+ declare const GLYPH_PARAMS_BYTES: number;
360
+ /**
361
+ * Bind-group layouts + render pipeline for the MSDF glyph bake pass.
362
+ *
363
+ * `cameraLayout` / `instanceLayout` are structurally identical to
364
+ * `Pipelines.cameraLayout` / `.instanceLayout`, but they are built HERE
365
+ * (own objects) so the glyph pipeline does not depend on `pipelines.ts` plumbing
366
+ * the bake never receives. The bake builds ONE camera bind group + ONE instances
367
+ * bind group against the bake-shape layouts and reuses them for the glyph draw —
368
+ * see the binding-compatibility note in `tiers/bake.ts`.
369
+ */
370
+ interface GlyphPipelineSet {
371
+ /** Transparent (alpha-blended, depth-test-only) MSDF glyph pipeline. */
372
+ pipeline: GPURenderPipeline;
373
+ /** `@group(0)` camera uniform layout (Camera { vpCol0, vpCol1, vpCol2 }). */
374
+ cameraLayout: GPUBindGroupLayout;
375
+ /** `@group(1)` read-only instance storage layout. */
376
+ instanceLayout: GPUBindGroupLayout;
377
+ /** `@group(2)` atlas texture + sampler + glyph-params uniform layout. */
378
+ atlasLayout: GPUBindGroupLayout;
379
+ }
380
+ /**
381
+ * Build the MSDF glyph bake pipeline + its three bind-group layouts.
382
+ *
383
+ * @param device The GPU device (from `root.device`).
384
+ * @param format Bake color-target format (the canvas/ctx format).
385
+ * @param sampleCount MSAA sample count — `config.msaaBakes` (1 | 4). MUST match
386
+ * the shape bake pipelines and the bake pass's color/depth attachments.
387
+ */
388
+ declare function createGlyphPipeline(device: GPUDevice, format: GPUTextureFormat, sampleCount: number): GlyphPipelineSet;
389
+ //#endregion
390
+ //#region src/tiers/bake.d.ts
391
+ /**
392
+ * Write a 3x3 view-projection `Mat3` plus the viewport/dpr tail into the
393
+ * 10-float Camera uniform layout. `viewportDevW`/`viewportDevH` are the render
394
+ * target's DEVICE-pixel extent; `dpr` the device pixel ratio. They default to 0
395
+ * for the bake path (anchored text re-projects live, not from a bake), where
396
+ * only the matrix prefix is meaningful.
397
+ */
398
+ declare function packCamera(vp: Mat3, out: Float32Array, viewportDevW?: number, viewportDevH?: number, dpr?: number): void;
399
+ /** Minimal canvas-size source — width/height in device pixels. */
400
+ interface BakeSizeSource {
401
+ readonly width: number;
402
+ readonly height: number;
403
+ }
404
+ /**
405
+ * Compute the bake-time view projection for a layer.
406
+ *
407
+ * - `world` : `projection(w, h) x scaling(dpr) x viewFromCamera(camera,
408
+ * w/dpr, h/dpr)` (CSS-px camera baseline) — the camera is FROZEN at the
409
+ * instant the bake is requested (the matrix is computed from the camera
410
+ * snapshot passed in; subsequent camera mutation does not alter the
411
+ * returned matrix).
412
+ * - `ui` : `projection(w / dpr, h / dpr)` (CSS-px coordinate space).
413
+ *
414
+ * `cacheSize` overrides the target dimensions (matches v1's `layer.cacheSize`);
415
+ * when absent the canvas `ctx` size is used.
416
+ */
417
+ declare function bakeViewProjection(space: LayerSpace, ctx: BakeSizeSource, camera: CameraState, dpr: number, cacheSize?: {
418
+ width: number;
419
+ height: number;
420
+ }): Mat3;
421
+ /**
422
+ * Allocate the single-sample bake target texture. The MSAA pass resolves into
423
+ * this; it is also `TEXTURE_BINDING` so the composited result can be sampled
424
+ * back as a sprite by the layer that owns the bake. Caller must `destroy()` it.
425
+ */
426
+ declare function createBakeTexture(device: GPUDevice, format: GPUTextureFormat, width: number, height: number): GPUTexture;
427
+ /**
428
+ * The pipelines a bake draws with. Built once per (format, sampleCount) and
429
+ * reused across bakes. ALL pipelines share `sampleCount` and `DEPTH_FORMAT` so
430
+ * one MSAA color + one MSAA depth attachment serve every draw in the pass.
431
+ */
432
+ interface BakePipelineSet {
433
+ /** Opaque shape pass (depth write on, src-replace). MSAA. */
434
+ shapeOpaque: GPURenderPipeline;
435
+ /** Transparent shape pass (depth test only, premultiplied src-over). MSAA. */
436
+ shapeTransparent: GPURenderPipeline;
437
+ /** MSDF glyph pipeline + its bind-group layouts. MSAA. */
438
+ glyph: GlyphPipelineSet;
439
+ /** `@group(0)` camera layout shared by shape + glyph passes. */
440
+ cameraLayout: GPUBindGroupLayout;
441
+ /** `@group(1)` instance-storage layout shared by shape + glyph passes. */
442
+ instanceLayout: GPUBindGroupLayout;
443
+ }
444
+ /**
445
+ * Build the bake pipeline set for a `(format, sampleCount)` pair.
446
+ *
447
+ * The shape pipelines reuse `assembleShader`'s `vertex` + `opaque` / `transparent`
448
+ * WGSL VERBATIM — the same source the main (single-sample) pipelines use — but
449
+ * are created with `multisample: { count: sampleCount }`. Their `@group(0)` and
450
+ * `@group(1)` layouts are structurally identical to `Pipelines.cameraLayout`
451
+ * / `.instanceLayout` (and to the glyph pipeline's), so ONE camera bind group +
452
+ * ONE instances bind group bind against every draw in the bake pass.
453
+ *
454
+ * @param sampleCount `config.msaaBakes` (1 | 4).
455
+ */
456
+ declare function createBakePipelines(device: GPUDevice, format: GPUTextureFormat, shader: AssembledShader, sampleCount: number): BakePipelineSet;
457
+ /** Inputs the bake draws into the target. */
458
+ interface BakeInput {
459
+ /** Shape instances (UberPack). May be empty (text-only bake). */
460
+ shapes: UberPack;
461
+ /**
462
+ * Optional glyph instances (GlyphPack). When present and non-empty, drawn
463
+ * after the shapes, inside the SAME render pass.
464
+ */
465
+ glyphs?: GlyphPack | null;
466
+ /**
467
+ * MSDF atlas the glyphs reference: provides the texture/sampler and the
468
+ * `pxRange` / `atlasSize` for the screen-px AA. Required iff `glyphs` is
469
+ * non-empty.
470
+ */
471
+ atlas?: BakeAtlas | null;
472
+ }
473
+ /**
474
+ * The atlas facts the glyph pass needs. A `GlyphAtlas` (`text/text-font.ts`)
475
+ * satisfies this — `atlas.texture.view` is the bindable `GPUTextureView`.
476
+ */
477
+ interface BakeAtlas {
478
+ /** Atlas texture (its `.view` is bound to `@group(2) @binding(0)`). */
479
+ readonly texture: {
480
+ readonly view: GPUTextureView;
481
+ };
482
+ /** Distance range in atlas texels. */
483
+ readonly pxRange: number;
484
+ /** Atlas side length in texels (square). */
485
+ readonly atlasSize: number;
486
+ }
487
+ /** Per-call bake options. */
488
+ interface BakeOptions {
489
+ /** Caller-owned single-sample resolve target (from `createBakeTexture`). */
490
+ target: GPUTexture;
491
+ /** Target width in pixels. */
492
+ width: number;
493
+ /** Target height in pixels. */
494
+ height: number;
495
+ /** Frozen view-projection (from `bakeViewProjection`). */
496
+ viewProjection: Mat3;
497
+ /** Clear color for the bake (typically transparent). */
498
+ clearColor: Color;
499
+ /**
500
+ * Sampler for the glyph atlas (filtering). Only needed when glyphs are drawn.
501
+ * The renderer owns/caches this; the bake binds it for one pass.
502
+ */
503
+ glyphSampler?: GPUSampler | null;
504
+ /**
505
+ * Optional crop rect in DEVICE pixels (the layer's `clipRect` resolved by the
506
+ * caller). When set, the bake pass is scissored to it so the snapshot matches
507
+ * the clipped live render — geometry outside the rect stays the cleared
508
+ * (transparent) background and never composites. `null`/absent = no crop.
509
+ */
510
+ clip?: {
511
+ x: number;
512
+ y: number;
513
+ width: number;
514
+ height: number;
515
+ } | null;
516
+ }
517
+ /**
518
+ * Run a single-submit RTT bake pass: `config.msaaBakes`x MSAA -> resolve into
519
+ * `opts.target`. Shapes (opaque reverse, transparent forward) then glyphs
520
+ * (transparent, scene order) are drawn into ONE render pass so the baked
521
+ * snapshot is complete — no live text re-emission needed on top.
522
+ *
523
+ * Scratch MSAA color + depth textures and all per-bake GPU buffers are created
524
+ * and destroyed within this call. The camera uniform / instances buffers /
525
+ * glyph-params uniform are uploaded fresh each bake (a bake is a rare,
526
+ * settle-time event — not a per-frame cost).
527
+ */
528
+ declare function runBakePass(device: GPUDevice, config: RendererConfig, input: BakeInput, pipelines: BakePipelineSet, opts: BakeOptions): void;
529
+ //#endregion
530
+ //#region src/damage/scene-root.d.ts
531
+ interface PaintCtx {
532
+ /** The in-scope scene layer that descendant nodes paint into. */
533
+ readonly layer: Layer;
534
+ /** Per-frame inputs for the `project` hook (camera / group / dpr / viewport). */
535
+ readonly projectOpts: ProjectOptions;
536
+ }
537
+ declare abstract class GpuSceneNode extends SceneNode {
538
+ /**
539
+ * Emit this node's geometry into `ctx.layer`. Called with the live frame
540
+ * context — NO `gpuRoot()` re-walk. Implementations push shapes via the
541
+ * `Layer.pushXxx` API and may read `ctx.projectOpts` for screen-pixel math.
542
+ */
543
+ abstract drawSelf(ctx: PaintCtx): void;
544
+ /**
545
+ * Paint this node's WHOLE subtree (self + every descendant), threading the
546
+ * SAME `ctx`. This is the uncull­ed entry used when a caller wants to force a
547
+ * subtree repaint regardless of damage. The damage-tracked frame path does NOT
548
+ * call this — it uses {@link GpuSceneRoot}'s recursive cull, which tests every
549
+ * node's bounds against the damage regions and prunes missed subtrees. Keeping
550
+ * the cull out of `paint` means a hit subtree still walks fully, while the cull
551
+ * decides which subtrees to enter.
552
+ */
553
+ paint(ctx: unknown): void;
554
+ /**
555
+ * Project this node's bounds to a screen-pixel `FrameRect` via the T21
556
+ * `project` hook. Call this BEFORE `markDamaged` when a node authors in a
557
+ * non-`ui` space so the emitted damage rect already lives in the screen-pixel
558
+ * space the cull + scissor share. (For a plain `ui` scene graph the bounds are
559
+ * already CSS px and `markDamaged` may be called directly.)
560
+ *
561
+ * `space` defaults to `"ui"` — the natural space for a CSS-pixel scene graph
562
+ * whose `bounds` are authored in screen pixels.
563
+ */
564
+ protected projectBounds(ctx: PaintCtx, space?: "world" | "ui"): FrameRect;
565
+ /** Mark a visual-only change: repaint, no data/layout rebuild. */
566
+ invalidatePaint(rect?: Rect): void;
567
+ /** Mark a layout change (bounds/children moved). Triggers doLayout + paint. */
568
+ invalidateLayout(rect?: Rect): void;
569
+ /** Mark a data change (inputs replaced). Triggers rebuildData → layout → paint. */
570
+ invalidateData(rect?: Rect): void;
571
+ }
572
+ interface GpuSceneRootOptions {
573
+ /**
574
+ * Scheduler for the FrameSpine (and the base SceneRoot's inert channel).
575
+ * Defaults to a RAFScheduler in production; pass a ManualScheduler (or use a
576
+ * test spine) for deterministic unit tests.
577
+ */
578
+ scheduler?: Scheduler;
579
+ /**
580
+ * Repaint every node on any damage and ignore the per-node cull. Default
581
+ * `false` — the recursive cull is the whole point. Set `true` only as a
582
+ * cost-comparison baseline or when the scene layer is cleared+re-emitted whole
583
+ * each frame and culled-out nodes would otherwise vanish.
584
+ */
585
+ fullFrame?: boolean;
586
+ /** Optional per-frame CPU timing hook (layout vs paint, painted-node count). */
587
+ onFrameTiming?: (timing: SpatialFrameTiming) => void;
588
+ /**
589
+ * Per-frame `project` inputs (camera / group / dpr / viewport). A thunk so the
590
+ * caller can update the camera/dpr/viewport between frames without rebuilding
591
+ * the root. The returned options seed `PaintCtx.projectOpts` and the `viewKey`
592
+ * fold. Required so the screen-pixel project space matches the renderer.
593
+ */
594
+ projectOpts: () => ProjectOptions;
595
+ /**
596
+ * Active group transforms folded into the `viewKey`. A thunk re-read each
597
+ * frame: whenever ANY returned group's compounded transform changes, the
598
+ * folded key changes and the renderer forces a full frame (no ghosting). Pass
599
+ * the groups whose moves should invalidate the partial path.
600
+ */
601
+ groups?: () => readonly Group[];
602
+ /**
603
+ * Extra data-domain key folded into the `viewKey` after the group transforms.
604
+ * Change it whenever a non-transform input (a plot's data domain, a palette)
605
+ * changes the whole-canvas mapping. A thunk re-read each frame.
606
+ */
607
+ dataKey?: () => string;
608
+ }
609
+ declare class GpuSceneRoot extends SceneRoot implements SpatialRenderer2D {
610
+ /** The scene layer that descendant nodes paint into. */
611
+ readonly layer: Layer;
612
+ /** The underlying v3 WebGPU renderer. */
613
+ readonly gpu: Renderer2D;
614
+ /** Reverse-z pointer routing with per-pointerId capture/drag/hover. */
615
+ readonly pointer: PointerRouter;
616
+ /**
617
+ * When `true`, repaint every node on any damage and ignore the per-node cull
618
+ * (set from {@link GpuSceneRootOptions.fullFrame}, default `false`).
619
+ * Passed through to `endFrame()` so the renderer does a full repaint.
620
+ */
621
+ fullFrame: boolean;
622
+ private readonly _spine;
623
+ private readonly _projectOpts;
624
+ private readonly _groups?;
625
+ private readonly _dataKey?;
626
+ /** Our own copy of the timing hook — the base keeps `onFrameTiming` private,
627
+ * and we override `_renderFrame`, so we read it directly. */
628
+ private readonly _onFrameTiming?;
629
+ /** Damage regions handed to the in-flight `beginFrame`; consumed by `endFrame`. */
630
+ private _currentRegions;
631
+ constructor(gpu: Renderer2D, layer: Layer, options: GpuSceneRootOptions);
632
+ /** Stash the frame's damage regions and clear the scene layer. */
633
+ beginFrame(regions: readonly Rect[]): void;
634
+ /**
635
+ * Submit one v3 GPU frame. Each damage `Rect` maps to ONE `Bounds2D` region
636
+ * (1:1 — never unioned), so the renderer's per-rect scissor path repaints each
637
+ * disjoint rect independently. `viewKey` folds the active group transforms +
638
+ * data key so a group move forces a full frame.
639
+ */
640
+ endFrame(): void;
641
+ /**
642
+ * Called by `SceneNode.markDamaged` (structural-type contract:
643
+ * `typeof root._emitDamage === "function"`). Routes the damage into the
644
+ * FrameSpine instead of the base SceneRoot's own channel, so a single
645
+ * coalesced flush per frame drives `_renderFrameV3`.
646
+ */
647
+ _emitDamage(damage: Damage): void;
648
+ /** Force a full repaint of the whole root bounds on the next scheduled frame. */
649
+ requestPaint(): void;
650
+ /**
651
+ * Mark a paint-damage region directly on the root (CSS px). Useful when an
652
+ * external surface (a resize, a host overlay) damages a screen rect that is
653
+ * not owned by a single node. Routes through the spine like any node damage.
654
+ */
655
+ requestPaintRegion(rect: Rect): void;
656
+ /**
657
+ * Run one frame from a coalesced dirty set. Mirrors the base SceneRoot
658
+ * pipeline (rebuildData on data-damage, doLayout on non-paint damage) but
659
+ * replaces the one-level `_paintCulled` with the recursive cull and uses our
660
+ * own `beginFrame`/`endFrame`. `fullFrame` repaints the whole bounds.
661
+ */
662
+ private _renderFrameV3;
663
+ /**
664
+ * Recursive damage cull — the v3 fix for v1's one-level cull. For each child
665
+ * of `node`: if its bounds miss EVERY region, prune the entire subtree (no
666
+ * `drawSelf`, no recursion into ITS children). On a hit, paint the child via
667
+ * `drawSelf(ctx)` and recurse — so the SAME per-region test applies at every
668
+ * depth, not just the root's direct children. A leaf deep in a large container
669
+ * therefore repaints only the nodes whose AABB actually touches a damage rect,
670
+ * and the cost scales with the damaged area, not the scene size.
671
+ *
672
+ * Pruning a missed subtree is sound: a node's bounds are expected to bound its
673
+ * own painted geometry (and, when it `clipsOverflow`, its descendants too), so
674
+ * a subtree whose AABB misses every damage rect contributes nothing inside any
675
+ * rect. (Nodes that paint outside their declared bounds must widen `bounds`.)
676
+ *
677
+ * Note: this does NOT call `GpuSceneNode.paint` (which would recurse
678
+ * unconditionally and defeat the deep cull) — it owns the descent itself.
679
+ */
680
+ private _paintCulledRecursive;
681
+ /**
682
+ * Composite `viewKey`: fold every active group's compounded transform, then
683
+ * the data key, into one string. Handed to `render(..., { viewKey })`; a
684
+ * change flips the renderer's view fingerprint and forces a full frame.
685
+ */
686
+ private _viewKey;
687
+ /**
688
+ * Dispatch a pointer event through the {@link PointerRouter}: `down` hit-tests
689
+ * (reverse-z, last-child-wins, depth-first — the inherited `SceneRoot.hitTest`)
690
+ * and captures by `pointerId`; `move`/`up`/`cancel` route to the captured node
691
+ * (or, for an uncaptured `move`, re-hit). Returns the receiving node or null.
692
+ */
693
+ dispatchPointer(e: SpatialPointerEvent): SceneNode | null;
694
+ /** Dispose the frame spine (removes the subscriber, cancels any pending flush). */
695
+ dispose(): void;
696
+ }
697
+ /** Convert a spatial `Rect` to the renderer's `Bounds2D` damage-region shape. */
698
+ declare function rectToBounds(r: Rect): Bounds2D;
699
+ //#endregion
700
+ //#region src/pipelines/composite-pipelines.d.ts
701
+ /**
702
+ * Composite-quad uniform: destination NDC rect + source UV rect. 8 floats / 32
703
+ * bytes (two vec4f):
704
+ * rectNdc = (ndcMinX, ndcMinY, ndcMaxX, ndcMaxY)
705
+ * rectUv = (uvMinX, uvMinY, uvMaxX, uvMaxY)
706
+ */
707
+ declare const COMPOSITE_UNIFORM_FLOATS = 8;
708
+ declare const COMPOSITE_UNIFORM_BYTES: number;
709
+ /** Bind-group layout + pipeline for the textured-quad composite. */
710
+ interface CompositePipelineSet {
711
+ /** Premultiplied-over textured-quad pipeline (no depth, single-sample). */
712
+ pipeline: GPURenderPipeline;
713
+ /**
714
+ * In-MAIN-PASS composite variant (T-ZBAKE). Identical shader / bind-group
715
+ * layout / topology / target blend as {@link pipeline}, but it declares the
716
+ * main pass's depth attachment (WebGPU requires a pipeline's `depthStencil` to
717
+ * match the pass it runs in) while neither writing nor testing depth
718
+ * (`depthWriteEnabled:false`, `depthCompare:"always"`). A bake is a translucent
719
+ * IMAGE — writing depth would let it occlude live geometry through its own
720
+ * transparent texels — so depth is intentionally inert. Drawn at the START of
721
+ * the main pass (full frame) or right after the per-rect erase (partial frame)
722
+ * so the LEADING z-run of bakes lands UNDER all live geometry.
723
+ */
724
+ pipelineInPass: GPURenderPipeline;
725
+ /**
726
+ * `@group(0)` layout: the composite uniform (vertex+fragment) + the sampled
727
+ * texture + a filtering sampler.
728
+ */
729
+ layout: GPUBindGroupLayout;
730
+ }
731
+ /**
732
+ * Build the textured-quad composite pipeline + its bind-group layout.
733
+ *
734
+ * @param device WebGPU device.
735
+ * @param format Color-target format (the canvas/backbuffer format).
736
+ */
737
+ declare function createCompositePipeline(device: GPUDevice, format: GPUTextureFormat): CompositePipelineSet;
738
+ /**
739
+ * Fill the composite uniform for a full-screen bake (the cacheLayer case): the
740
+ * destination covers the whole NDC viewport and the source samples the entire
741
+ * texture, with the Y-UV flipped so a texture rendered in NDC (+y up) reads back
742
+ * upright in UV (v=0 at top).
743
+ */
744
+ declare function packFullScreenComposite(out: Float32Array): void;
745
+ //#endregion
746
+ //#region src/pipelines/sprite-pipelines.d.ts
747
+ /** A built sprite pipeline pair (opaque + transparent variants share layouts). */
748
+ interface SpritePipelineSet {
749
+ /** Opaque variant: depth-write on, src-replace, alpha-discard at 0.5. */
750
+ opaque: GPURenderPipeline;
751
+ /** Transparent variant: depth-test only, premultiplied ALPHA_BLEND. */
752
+ transparent: GPURenderPipeline;
753
+ /** `@group(0)` camera layout — 6-float Camera, binds the v3 camera slots. */
754
+ cameraLayout: GPUBindGroupLayout;
755
+ /** `@group(1)` read-only-storage layout over the external sprite buffer. */
756
+ bufferLayout: GPUBindGroupLayout;
757
+ /** `@group(2)` texture + sampler layout. */
758
+ textureLayout: GPUBindGroupLayout;
759
+ /** Per-draw `InteropDraw { z }` uniform layout (`@group(N)`). */
760
+ drawLayout: GPUBindGroupLayout;
761
+ /** Group index of the {@link drawLayout} — 3 for sprites. */
762
+ drawGroup: number;
763
+ }
764
+ /**
765
+ * Built sprite OIT emit pipeline. Appends premultiplied `(rgba, depth)` fragments
766
+ * into the shared A-buffer at `@group(3)` instead of alpha-blending to the color
767
+ * target. The existing opaque/transparent sprite pipelines stay; Phase 4 routes
768
+ * transparent sprites to this pipeline when OIT is enabled.
769
+ */
770
+ interface SpriteOitEmitPipeline {
771
+ /** The single pipeline: `writeMask:0`, depth-test-only, OIT A-buffer append. */
772
+ pipeline: GPURenderPipeline;
773
+ /** `@group(0)` camera layout — 6-float Camera, binds v3 camera slots. */
774
+ cameraLayout: GPUBindGroupLayout;
775
+ /** `@group(1)` read-only-storage layout over the `SpriteSchema` buffer. */
776
+ bufferLayout: GPUBindGroupLayout;
777
+ /** `@group(2)` texture + sampler layout (one texture per draw command). */
778
+ textureLayout: GPUBindGroupLayout;
779
+ /**
780
+ * `@group(3)` A-buffer layout (build-style: heads + slots `storage`, viewport
781
+ * `uniform`). Created via {@link ABuffer.buildLayoutAt}(3).
782
+ */
783
+ abufferLayout: GPUBindGroupLayout;
784
+ }
785
+ /**
786
+ * WGSL for the sprite OIT emit pipeline. The VS reads per-instance `sp.order`
787
+ * (float offset 15, stamped by Phase 1 {@link SpritePack.stampOrder}) for
788
+ * `out.pos.z` so sprites z-interleave with shapes/glyphs in true push order.
789
+ * The FS computes premultiplied color, calls `oitAppend`, and returns `vec4f(0.0)`
790
+ * into a `writeMask:0` target.
791
+ *
792
+ * @internal — exported for unit testing only.
793
+ */
794
+ declare function spriteOitEmitWgsl(): string;
795
+ /**
796
+ * Build the SPRITE OIT emit pipeline. Samples the per-command texture at
797
+ * `@group(2)` and appends premultiplied fragments into the shared A-buffer at
798
+ * `@group(3)`. Writes no color (`writeMask:0`); depth-tests but does not write
799
+ * (`depthWriteEnabled:false`).
800
+ *
801
+ * Bind-group convention (per design.md):
802
+ * `@group(0)` — 6-float Camera (v3 camera slots)
803
+ * `@group(1)` — read-only-storage `SpriteSchema` array
804
+ * `@group(2)` — texture + sampler (one per draw command)
805
+ * `@group(3)` — A-buffer (via `abuffer.buildLayoutAt(3)` / `buildBindGroupFor()`)
806
+ *
807
+ * @param device WebGPU device.
808
+ * @param format Color attachment format (must match the render pass; `writeMask:0`
809
+ * means nothing is written, but the target format must still be declared).
810
+ * @param abuffer {@link ABuffer} instance — provides `buildLayoutAt(3)` for the
811
+ * pipeline layout and `buildBindGroupFor()` for per-pass bind.
812
+ * @param sampleCount Multi-sample count for the main render pass (1 for live OIT —
813
+ * NOT the bake MSAA count; see design.md §Risks "MSAA mismatch").
814
+ */
815
+ declare function createSpriteOitEmitPipeline(device: GPUDevice, format: GPUTextureFormat, abuffer: ABuffer, sampleCount?: number): SpriteOitEmitPipeline;
816
+ /**
817
+ * Build the SPRITE pipeline pair (v1 64 B `SpriteSchema` external buffer
818
+ * + a texture). `@group(0)` = 6-float Camera (v3 slots), `@group(1)` =
819
+ * read-only-storage `SpriteSchema` array, `@group(2)` = texture + sampler,
820
+ * `@group(3)` = `InteropDraw { z }`. Drives the `Layer.pushSprite` path
821
+ * (`_encodeLayerSprites`) — textured quads accumulated CPU-side into a per-layer
822
+ * `SpritePack` (e.g. plot's heatmap geom drawing a compute texture).
823
+ */
824
+ declare function createSpritePipelines(device: GPUDevice, format: GPUTextureFormat): SpritePipelineSet;
825
+ //#endregion
826
+ //#region src/pipelines/instance-pipelines.d.ts
827
+ /** Per-buffer draw uniform: a single near-z, 16 B (one vec4f slot). Shared
828
+ * wire format with `sprite-pipelines.ts` (same `InteropDraw` WGSL struct). */
829
+ declare const INSTANCE_DRAW_FLOATS = 4;
830
+ declare const INSTANCE_DRAW_BYTES: number;
831
+ /** A built external-instance pipeline pair (opaque + transparent share layouts). */
832
+ interface InstancePipelineSet {
833
+ /** Opaque variant: depth-write on, src-replace, alpha-discard at 0.5. */
834
+ opaque: GPURenderPipeline;
835
+ /** Transparent variant: depth-test only, premultiplied ALPHA_BLEND. */
836
+ transparent: GPURenderPipeline;
837
+ /** `@group(0)` camera layout — 6-float Camera, binds the v3 camera slots. */
838
+ cameraLayout: GPUBindGroupLayout;
839
+ /** `@group(1)` read-only-storage layout over the external `Instance` buffer. */
840
+ bufferLayout: GPUBindGroupLayout;
841
+ /** Per-buffer `InteropDraw { z }` uniform layout (`@group(2)`). */
842
+ drawLayout: GPUBindGroupLayout;
843
+ /** Group index of {@link drawLayout} — always 2 for instances. */
844
+ drawGroup: number;
845
+ }
846
+ /**
847
+ * Build the EXTERNAL-INSTANCE pipeline pair (v3 64 B `Instance` external buffer).
848
+ *
849
+ * `@group(0)` = 6-float Camera (binds v3 camera slots), `@group(1)` =
850
+ * read-only-storage `array<InstanceStruct>`, `@group(2)` = `InteropDraw { z }`.
851
+ * Opaque + transparent variants share all three layouts.
852
+ */
853
+ declare function createInstancePipelines(device: GPUDevice, format: GPUTextureFormat): InstancePipelineSet;
854
+ /** A built external-instance OIT-emit pipeline. Appends premultiplied `(rgba,
855
+ * depth)` into the shared A-buffer at `@group(3)` instead of blending. */
856
+ interface InstanceOitEmitPipeline {
857
+ /** The single pipeline: `writeMask:0`, depth-test-only, OIT A-buffer append. */
858
+ pipeline: GPURenderPipeline;
859
+ /** `@group(0)` camera layout — 6-float Camera, binds v3 camera slots. */
860
+ cameraLayout: GPUBindGroupLayout;
861
+ /** `@group(1)` read-only-storage layout over the external `Instance` buffer. */
862
+ bufferLayout: GPUBindGroupLayout;
863
+ /** `@group(2)` per-buffer `InteropDraw { z }` uniform layout. */
864
+ drawLayout: GPUBindGroupLayout;
865
+ /** `@group(3)` A-buffer layout (via {@link ABuffer.buildLayoutAt}(3)). */
866
+ abufferLayout: GPUBindGroupLayout;
867
+ }
868
+ /**
869
+ * WGSL for the external-instance OIT-emit pipeline. Same SDF switch as the
870
+ * opaque/transparent variants; the FS calls `oitAppend` with the premultiplied
871
+ * `outColor` and the per-buffer near-z (`in.pos.z`, written from `drawParams.z`
872
+ * in the vertex stage) so external instances composite through the unchanged
873
+ * resolve pass.
874
+ *
875
+ * @internal — exported for unit testing only.
876
+ */
877
+ declare function instanceOitEmitWgsl(): string;
878
+ /**
879
+ * Build the EXTERNAL-INSTANCE OIT-emit pipeline. Shares the vertex stage with
880
+ * {@link createInstancePipelines} (so the geometry expansion + near-z stamp are
881
+ * identical) and appends premultiplied fragments into the shared A-buffer at
882
+ * `@group(3)`. Writes no color (`writeMask:0`); depth-tests but does not write.
883
+ *
884
+ * Bind-group convention:
885
+ * `@group(0)` — 6-float Camera (v3 camera slots)
886
+ * `@group(1)` — read-only-storage `array<InstanceStruct>`
887
+ * `@group(2)` — `InteropDraw { z }` per-buffer near-z
888
+ * `@group(3)` — A-buffer (via `abuffer.buildLayoutAt(3)` / `buildBindGroupFor()`)
889
+ */
890
+ declare function createInstanceOitEmitPipeline(device: GPUDevice, format: GPUTextureFormat, abuffer: ABuffer, sampleCount?: number): InstanceOitEmitPipeline;
891
+ //#endregion
892
+ export { ABuffer, ARC_FLOATS, BUILD_SPATIAL_HASH_WGSL, type BakeAtlas, type BakeInput, type BakeOptions, type BakePipelineSet, type BakeSizeSource, CLEAR_SPATIAL_HASH_WGSL, COMPOSITE_UNIFORM_BYTES, COMPOSITE_UNIFORM_FLOATS, CURVE_CAP_BUTT, CURVE_CAP_ROUND, CURVE_CAP_SQUARE, CURVE_FLOATS, type CompositePipelineSet, type CullCommand, type CullResult, type CulledRange, type DebugSize, type DebugSpace, type DirtyRange, type DirtyRegion, type DrawCommand, FRAME_RING_CAPACITY, FULL_FRAME_TILE_FRACTION, type FrameDebugListener, FrameRing, FrameSpine, type FrameSpineOptions, type FrameSummary, GLYPH_PARAMS_BYTES, GLYPH_PARAMS_FLOATS, type GPUOwner, type GlyphPipelineSet, GpuSceneNode, GpuSceneRoot, type GpuSceneRootOptions, HEAD_BYTES, INSTANCE_DRAW_BYTES, INSTANCE_DRAW_FLOATS, type InstanceOitEmitPipeline, type InstancePipelineSet, type LayerSummary, type OITPipelineDeps, type OITPipelines, type PaintCtx, type RectXYWH, RegionIndex, RendererDebug, type RetainedRecord, RetainedTier, SEGMENT_FLOATS, SHAPE_BYTES, SHAPE_CIRCLE, SHAPE_ELLIPSE, SHAPE_RECT, SLOT_BYTES, SPATIAL_HASH_PARAMS_BYTES, SPATIAL_HASH_PARAMS_WGSL, SPATIAL_HASH_WORKGROUP_SIZE, type ScreenAABB, ShapeSchema, type SpaceMap, type SpatialGrid, type SpatialGridOptions, type SpatialGridResult, type SpatialRange, type SpriteOitEmitPipeline, type SpritePipelineSet, SpriteSchema, TRIANGLE_FLOATS, TileGrid, UberPack, bakeViewProjection, blitBackbuffer, buildSpatialGrid, captureSpaceMap, convertRect, createBakePipelines, createBakeTexture, createCompositePipeline, createGlyphPipeline, createInstanceOitEmitPipeline, createInstancePipelines, createOITPipelines, createSpriteOitEmitPipeline, createSpritePipelines, createTestRenderer, createTestSpine, cullCommands, dirtyTileSet, instanceOitEmitWgsl, nextByteCapacity, nextPow2, packCamera, packFullScreenComposite, queryRanges, rectToBounds, repackArcs, repackCurves, repackSegments, resolveRoot, runBakePass, spriteOitEmitWgsl, summarize, worldFrustumAabb };