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.
- package/LICENSE.md +674 -0
- package/README.md +234 -0
- package/dist/advanced.d.mts +76 -0
- package/dist/advanced.mjs +81 -0
- package/dist/assemble-BT3CXbSx.mjs +1574 -0
- package/dist/camera-view-DHmMiKvP.d.mts +326 -0
- package/dist/frame-mHNdKRpF.mjs +135 -0
- package/dist/index-CmMZCMJT.d.mts +39 -0
- package/dist/index-DkJfpntS.d.mts +2417 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +6612 -0
- package/dist/internal.d.mts +892 -0
- package/dist/internal.mjs +566 -0
- package/dist/logger-DSyBF3Y_.mjs +15 -0
- package/dist/particles.d.mts +816 -0
- package/dist/particles.mjs +4804 -0
- package/dist/pipeline-BWCAZTKx.mjs +470 -0
- package/dist/pipeline-DE3a1Pnk.d.mts +115 -0
- package/dist/reactivity-B7I0pvzm.mjs +191 -0
- package/dist/reactivity.d.mts +2 -0
- package/dist/reactivity.mjs +2 -0
- package/dist/renderer-DzZqd1bY.d.mts +4566 -0
- package/dist/root-CHradZKM.mjs +30 -0
- package/dist/shape-DfZP9Jdk.mjs +349 -0
- package/dist/space-CeDnj6eu.mjs +11240 -0
- package/dist/spatial-Bd3Ay8I2.d.mts +85 -0
- package/dist/spatial-hash-C1crBjTo.mjs +77 -0
- package/dist/spatial.d.mts +2 -0
- package/dist/spatial.mjs +121 -0
- package/dist/text-font-D7GGDtTK.d.mts +185 -0
- package/dist/text-ttf.d.mts +91 -0
- package/dist/text-ttf.mjs +298 -0
- package/dist/texture-dABoqFoP.mjs +131 -0
- package/dist/viewport.d.mts +2 -0
- package/dist/viewport.mjs +274 -0
- package/package.json +69 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import { n as resolveRoot } from "./root-CHradZKM.mjs";
|
|
2
|
+
import { a as ARC_FLOATS, c as CURVE_CAP_SQUARE, d as SHAPE_BYTES, f as SHAPE_CIRCLE, g as TRIANGLE_FLOATS, l as CURVE_FLOATS, m as SHAPE_RECT, o as CURVE_CAP_BUTT, p as SHAPE_ELLIPSE, s as CURVE_CAP_ROUND, u as SEGMENT_FLOATS } from "./assemble-BT3CXbSx.mjs";
|
|
3
|
+
import { $ as repackCurves, A as packCamera, B as HEAD_BYTES, C as COMPOSITE_UNIFORM_BYTES, Ct as FRAME_RING_CAPACITY, D as bakeViewProjection, Dt as convertRect, E as packFullScreenComposite, Et as captureSpaceMap, G as cullCommands, H as SLOT_BYTES, K as worldFrustumAabb, M as GLYPH_PARAMS_BYTES, N as GLYPH_PARAMS_FLOATS, O as createBakePipelines, P as createGlyphPipeline, Q as repackArcs, R as createOITPipelines, S as RetainedTier, St as RendererDebug, T as createCompositePipeline, Tt as summarize, U as buildSpatialGrid, W as queryRanges, _ as INSTANCE_DRAW_BYTES, a as blitBackbuffer, at as nextByteCapacity, b as createInstancePipelines, et as repackSegments, g as spriteOitEmitWgsl, h as createSpritePipelines, i as createTestRenderer, it as UberPack, j as runBakePass, k as createBakeTexture, m as createSpriteOitEmitPipeline, s as resolveGroupTransform, t as project, v as INSTANCE_DRAW_FLOATS, w as COMPOSITE_UNIFORM_FLOATS, wt as FrameRing, x as instanceOitEmitWgsl, y as createInstanceOitEmitPipeline, z as ABuffer } from "./space-CeDnj6eu.mjs";
|
|
4
|
+
import { l as SpriteSchema, s as ShapeSchema } from "./pipeline-BWCAZTKx.mjs";
|
|
5
|
+
import { i as padFrameRect } from "./frame-mHNdKRpF.mjs";
|
|
6
|
+
import { a as SPATIAL_HASH_WORKGROUP_SIZE, i as SPATIAL_HASH_PARAMS_WGSL, n as CLEAR_SPATIAL_HASH_WGSL, r as SPATIAL_HASH_PARAMS_BYTES, t as BUILD_SPATIAL_HASH_WGSL } from "./spatial-hash-C1crBjTo.mjs";
|
|
7
|
+
import { DirtyChannel, ManualScheduler, PointerRouter, RAFScheduler, RectSpace, SceneNode, SceneRoot, rectOverlaps } from "./spatial.mjs";
|
|
8
|
+
import "typegpu";
|
|
9
|
+
//#region src/shared/dynamic-buffer.ts
|
|
10
|
+
function nextPow2(value) {
|
|
11
|
+
if (value <= 1) return 1;
|
|
12
|
+
if (value > 1073741824) return 2 ** Math.ceil(Math.log2(value));
|
|
13
|
+
value--;
|
|
14
|
+
value |= value >> 1;
|
|
15
|
+
value |= value >> 2;
|
|
16
|
+
value |= value >> 4;
|
|
17
|
+
value |= value >> 8;
|
|
18
|
+
value |= value >> 16;
|
|
19
|
+
value++;
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/pack/region-index.ts
|
|
24
|
+
/**
|
|
25
|
+
* Maps group identities to their byte spans within the `UberPack` buffer, and
|
|
26
|
+
* maintains an accumulated set of dirty byte ranges for the current frame.
|
|
27
|
+
*
|
|
28
|
+
* Default region granularity = one `Group` object. Callers that do not use
|
|
29
|
+
* explicit group tracking can pass a single sentinel object as `groupId`.
|
|
30
|
+
*
|
|
31
|
+
* Usage per frame:
|
|
32
|
+
* ```ts
|
|
33
|
+
* regions.reset();
|
|
34
|
+
* for (const [group, items] of scene.byGroup()) {
|
|
35
|
+
* regions.beginRegion(group, pack.byteLength);
|
|
36
|
+
* for (const item of items) pack.append(...);
|
|
37
|
+
* regions.endRegion(group, pack.byteLength);
|
|
38
|
+
* }
|
|
39
|
+
* // At mutation time:
|
|
40
|
+
* regions.markDirty(hoveredGroup);
|
|
41
|
+
* // Before upload:
|
|
42
|
+
* const ranges = regions.coalesced();
|
|
43
|
+
* regions.clearDirty();
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
var RegionIndex = class {
|
|
47
|
+
/** group identity → byte span in the current pack. */
|
|
48
|
+
_spans = /* @__PURE__ */ new Map();
|
|
49
|
+
/** Accumulated dirty ranges for this frame (may overlap / be out of order). */
|
|
50
|
+
_dirty = [];
|
|
51
|
+
/**
|
|
52
|
+
* Record the byte offset at which a group's instances BEGIN being packed.
|
|
53
|
+
* `byteOffset` is the value of `UberPack.byteLength` BEFORE the first
|
|
54
|
+
* `append` call for this group.
|
|
55
|
+
*/
|
|
56
|
+
beginRegion(groupId, byteOffset) {
|
|
57
|
+
this._spans.set(groupId, {
|
|
58
|
+
start: byteOffset,
|
|
59
|
+
end: byteOffset
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Record the byte offset at which a group's instances END being packed.
|
|
64
|
+
* `byteOffset` is the value of `UberPack.byteLength` AFTER the last
|
|
65
|
+
* `append` call for this group.
|
|
66
|
+
*/
|
|
67
|
+
endRegion(groupId, byteOffset) {
|
|
68
|
+
const span = this._spans.get(groupId);
|
|
69
|
+
if (span !== void 0) span.end = byteOffset;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Reset all span records for a new frame. Call before re-packing. Does NOT
|
|
73
|
+
* clear dirty ranges (use `clearDirty` for that).
|
|
74
|
+
*/
|
|
75
|
+
reset() {
|
|
76
|
+
this._spans.clear();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Mark a group's byte span as dirty. If the group has no recorded span
|
|
80
|
+
* (e.g. not packed this frame), this is a no-op.
|
|
81
|
+
*/
|
|
82
|
+
markDirty(groupId) {
|
|
83
|
+
const span = this._spans.get(groupId);
|
|
84
|
+
if (span === void 0 || span.end === span.start) return;
|
|
85
|
+
this._dirty.push({
|
|
86
|
+
byteOffset: span.start,
|
|
87
|
+
byteLength: span.end - span.start
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Return the minimal, sorted, non-overlapping set of dirty byte ranges.
|
|
92
|
+
*
|
|
93
|
+
* - Output is sorted ascending by `byteOffset`.
|
|
94
|
+
* - Adjacent or overlapping input ranges are merged into a single output range.
|
|
95
|
+
* - Non-destructive: does not mutate the internal `_dirty` list.
|
|
96
|
+
* - Returns an empty array when no ranges are dirty.
|
|
97
|
+
*/
|
|
98
|
+
coalesced() {
|
|
99
|
+
if (this._dirty.length === 0) return [];
|
|
100
|
+
const sorted = this._dirty.slice().sort((a, b) => a.byteOffset - b.byteOffset);
|
|
101
|
+
const out = [{
|
|
102
|
+
byteOffset: sorted[0].byteOffset,
|
|
103
|
+
byteLength: sorted[0].byteLength
|
|
104
|
+
}];
|
|
105
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
106
|
+
const last = out[out.length - 1];
|
|
107
|
+
const cur = sorted[i];
|
|
108
|
+
const lastEnd = last.byteOffset + last.byteLength;
|
|
109
|
+
if (cur.byteOffset <= lastEnd) {
|
|
110
|
+
const curEnd = cur.byteOffset + cur.byteLength;
|
|
111
|
+
if (curEnd > lastEnd) last.byteLength = curEnd - last.byteOffset;
|
|
112
|
+
} else out.push({
|
|
113
|
+
byteOffset: cur.byteOffset,
|
|
114
|
+
byteLength: cur.byteLength
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Clear accumulated dirty ranges. Call after the GPU upload is complete.
|
|
121
|
+
* Does NOT reset span records — use `reset()` for that.
|
|
122
|
+
*/
|
|
123
|
+
clearDirty() {
|
|
124
|
+
this._dirty.length = 0;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Return the recorded byte span for a group, or `undefined` if the group
|
|
128
|
+
* was not packed this frame. For testing / debugging only.
|
|
129
|
+
*/
|
|
130
|
+
spanOf(groupId) {
|
|
131
|
+
const s = this._spans.get(groupId);
|
|
132
|
+
return s !== void 0 ? {
|
|
133
|
+
start: s.start,
|
|
134
|
+
end: s.end
|
|
135
|
+
} : void 0;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/tiers/tile-replay.ts
|
|
140
|
+
/**
|
|
141
|
+
* Row-major grid of command buckets over screen device-px space. The grid is
|
|
142
|
+
* built once per layout: every command is assigned to the tiles its dilated
|
|
143
|
+
* AABB touches, and a dirty-rect query returns the union of the touched tiles'
|
|
144
|
+
* buckets.
|
|
145
|
+
*/
|
|
146
|
+
var TileGrid = class {
|
|
147
|
+
/** Tile edge length in device px (== {@link RendererConfig.tileSize}). */
|
|
148
|
+
cellSize;
|
|
149
|
+
cols;
|
|
150
|
+
rows;
|
|
151
|
+
/** Flat row-major array: `buckets[tileId]` = cmdIndex list (insertion order). */
|
|
152
|
+
buckets;
|
|
153
|
+
constructor(canvasW, canvasH, cellSize) {
|
|
154
|
+
if (cellSize <= 0) throw new RangeError(`TileGrid: cellSize must be positive, got ${cellSize}`);
|
|
155
|
+
this.cellSize = cellSize;
|
|
156
|
+
this.cols = Math.max(1, Math.ceil(canvasW / cellSize));
|
|
157
|
+
this.rows = Math.max(1, Math.ceil(canvasH / cellSize));
|
|
158
|
+
this.buckets = Array.from({ length: this.cols * this.rows }, () => []);
|
|
159
|
+
}
|
|
160
|
+
/** Row-major tile id for a `(col, row)` cell. */
|
|
161
|
+
tileId(col, row) {
|
|
162
|
+
return row * this.cols + col;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Rasterize an AABB (device px, half-open `[min, max)`) to the tile ids it
|
|
166
|
+
* touches. The AABB is dilated by +1px on every side before rasterizing so a
|
|
167
|
+
* shape's analytic anti-aliased fringe (which spills ≤1px past the geometric
|
|
168
|
+
* edge) is conservatively included — a shape sitting on a tile seam lands in
|
|
169
|
+
* both tiles. Out-of-range cells are clamped to the grid; a fully off-grid
|
|
170
|
+
* AABB yields an empty list.
|
|
171
|
+
*/
|
|
172
|
+
tilesForAabb(minX, minY, maxX, maxY) {
|
|
173
|
+
const s = this.cellSize;
|
|
174
|
+
const c0 = Math.max(0, Math.floor((minX - 1) / s));
|
|
175
|
+
const r0 = Math.max(0, Math.floor((minY - 1) / s));
|
|
176
|
+
const c1 = Math.min(this.cols - 1, Math.floor((maxX + 1) / s));
|
|
177
|
+
const r1 = Math.min(this.rows - 1, Math.floor((maxY + 1) / s));
|
|
178
|
+
const ids = [];
|
|
179
|
+
for (let r = r0; r <= r1; r++) for (let c = c0; c <= c1; c++) ids.push(this.tileId(c, r));
|
|
180
|
+
return ids;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Rasterize a {@link FrameRect} (CSS px) to a tile-id set. The rect is first
|
|
184
|
+
* padded by 1px — the SAME `padFrameRect(rect, 1)` the T22 clear-quad step
|
|
185
|
+
* applies — then converted to device px via `dpr`, then handed to
|
|
186
|
+
* {@link tilesForAabb} (which adds its own +1px AA dilation). The query rect
|
|
187
|
+
* thus covers at least the pixels the per-rect erase will actually repaint.
|
|
188
|
+
*/
|
|
189
|
+
tilesForRect(rect, dpr) {
|
|
190
|
+
const padded = padFrameRect(rect, 1);
|
|
191
|
+
const minX = padded.x * dpr;
|
|
192
|
+
const minY = padded.y * dpr;
|
|
193
|
+
const maxX = (padded.x + padded.width) * dpr;
|
|
194
|
+
const maxY = (padded.y + padded.height) * dpr;
|
|
195
|
+
return new Set(this.tilesForAabb(minX, minY, maxX, maxY));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Assign a command (by its index in the renderer's command array) to every
|
|
199
|
+
* tile its screen AABB intersects. The same +1px AA dilation as
|
|
200
|
+
* {@link tilesForAabb} applies, so a command straddling a tile boundary is
|
|
201
|
+
* dropped into every touched bucket — the dirty-rect query then issues it
|
|
202
|
+
* exactly once per region pass (via {@link commandsForTiles} dedup).
|
|
203
|
+
*/
|
|
204
|
+
assign(cmdIndex, aabb) {
|
|
205
|
+
const ids = this.tilesForAabb(aabb.minX, aabb.minY, aabb.maxX, aabb.maxY);
|
|
206
|
+
for (const id of ids) this.buckets[id].push(cmdIndex);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Union of command indices across a set of tile ids, deduplicated and sorted
|
|
210
|
+
* ascending. Ascending = original scene/submission order, so the caller's
|
|
211
|
+
* opaque-reverse sweep (`for i = n-1 … 0`) and transparent-forward sweep match
|
|
212
|
+
* v1's painter stacking. A command in two dirty tiles appears once.
|
|
213
|
+
*/
|
|
214
|
+
commandsForTiles(tileIds) {
|
|
215
|
+
const seen = /* @__PURE__ */ new Set();
|
|
216
|
+
const result = [];
|
|
217
|
+
for (const id of tileIds) {
|
|
218
|
+
const bucket = this.buckets[id];
|
|
219
|
+
if (bucket === void 0) continue;
|
|
220
|
+
for (const cmdIndex of bucket) if (!seen.has(cmdIndex)) {
|
|
221
|
+
seen.add(cmdIndex);
|
|
222
|
+
result.push(cmdIndex);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return result.sort((a, b) => a - b);
|
|
226
|
+
}
|
|
227
|
+
/** Total tile count (`cols × rows`). */
|
|
228
|
+
get totalTiles() {
|
|
229
|
+
return this.cols * this.rows;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* Full-frame fallback threshold. When the dirty-tile fraction exceeds this,
|
|
234
|
+
* {@link dirtyTileSet} returns `null` so the caller skips bucket replay and runs
|
|
235
|
+
* a single full-frame pass instead — thousands of scissor switches across most
|
|
236
|
+
* of the canvas cost more than one unscissored re-render.
|
|
237
|
+
*/
|
|
238
|
+
const FULL_FRAME_TILE_FRACTION = .5;
|
|
239
|
+
/**
|
|
240
|
+
* Map damage regions to the set of tile ids to replay. Unions the tiles each
|
|
241
|
+
* region touches (via {@link TileGrid.tilesForRect}). Returns `null` — NOT an
|
|
242
|
+
* empty set — when the dirty fraction strictly exceeds
|
|
243
|
+
* {@link FULL_FRAME_TILE_FRACTION}, signalling "fall back to a full frame". An
|
|
244
|
+
* empty/zero-area region set returns an empty `Set` (nothing to replay), which
|
|
245
|
+
* the caller distinguishes from `null`.
|
|
246
|
+
*/
|
|
247
|
+
function dirtyTileSet(grid, regions, dpr) {
|
|
248
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
249
|
+
for (const region of regions) for (const id of grid.tilesForRect(region, dpr)) dirty.add(id);
|
|
250
|
+
if (dirty.size / grid.totalTiles > .5) return null;
|
|
251
|
+
return dirty;
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/damage/spine.ts
|
|
255
|
+
const FULL_PLANE_INTEREST = [{
|
|
256
|
+
rect: {
|
|
257
|
+
x: -1e9,
|
|
258
|
+
y: -1e9,
|
|
259
|
+
w: 2e9,
|
|
260
|
+
h: 2e9
|
|
261
|
+
},
|
|
262
|
+
kind: "paint"
|
|
263
|
+
}];
|
|
264
|
+
/**
|
|
265
|
+
* Frame-loop primitive for v3. Owns a DirtyChannel<DirtyRegion> wired to a
|
|
266
|
+
* Scheduler. Callers mark damage via mark(); the channel coalesces and
|
|
267
|
+
* schedules a single flush that invokes onFlush with the union of all marked
|
|
268
|
+
* regions. Empty flushes (no mark since last pump) are suppressed by the
|
|
269
|
+
* DirtyChannel itself.
|
|
270
|
+
*
|
|
271
|
+
* T61's GpuSceneRoot owns one FrameSpine and delegates damage marking to it.
|
|
272
|
+
*/
|
|
273
|
+
var FrameSpine = class {
|
|
274
|
+
channel;
|
|
275
|
+
scheduler;
|
|
276
|
+
#unsubscribe;
|
|
277
|
+
constructor({ scheduler, onFlush }) {
|
|
278
|
+
this.scheduler = scheduler ?? new RAFScheduler();
|
|
279
|
+
this.channel = new DirtyChannel(RectSpace, this.scheduler);
|
|
280
|
+
this.#unsubscribe = this.channel.subscribe(() => FULL_PLANE_INTEREST, (dirty) => onFlush(dirty));
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Accumulate damage and schedule a flush. An empty regions array is a
|
|
284
|
+
* fast-path no-op — nothing is scheduled.
|
|
285
|
+
*/
|
|
286
|
+
mark(regions) {
|
|
287
|
+
if (regions.length === 0) return;
|
|
288
|
+
this.channel.mark(regions);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Dispose the spine. Removes the subscriber and cancels any pending
|
|
292
|
+
* scheduled flush.
|
|
293
|
+
*/
|
|
294
|
+
dispose() {
|
|
295
|
+
this.#unsubscribe();
|
|
296
|
+
this.scheduler.cancel?.();
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Creates a FrameSpine backed by a ManualScheduler. Call pump() to drain
|
|
301
|
+
* any pending flush synchronously.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* const { spine, pump } = createTestSpine((r) => calls.push(r));
|
|
305
|
+
* spine.mark([{ rect: { x: 0, y: 0, w: 10, h: 10 }, kind: "paint" }]);
|
|
306
|
+
* pump(); // fires onFlush once
|
|
307
|
+
*/
|
|
308
|
+
function createTestSpine(onFlush) {
|
|
309
|
+
const scheduler = new ManualScheduler();
|
|
310
|
+
return {
|
|
311
|
+
spine: new FrameSpine({
|
|
312
|
+
scheduler,
|
|
313
|
+
onFlush
|
|
314
|
+
}),
|
|
315
|
+
pump: () => scheduler.pump()
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/damage/scene-root.ts
|
|
320
|
+
/**
|
|
321
|
+
* Type guard for {@link PaintCtx}. Guards the `paint(ctx: unknown)` downcast in
|
|
322
|
+
* {@link GpuSceneNode}: the structural contract is that the recursive descent
|
|
323
|
+
* always passes a live `PaintCtx` object — but the method signature is `unknown`
|
|
324
|
+
* to satisfy the base `SceneNode.paint` contract, which does not know about v3
|
|
325
|
+
* frame state. The guard makes the expectation explicit and avoids a bare `as`.
|
|
326
|
+
*/
|
|
327
|
+
function isPaintCtx(ctx) {
|
|
328
|
+
return ctx !== null && typeof ctx === "object" && "layer" in ctx && "projectOpts" in ctx;
|
|
329
|
+
}
|
|
330
|
+
var GpuSceneNode = class extends SceneNode {
|
|
331
|
+
/**
|
|
332
|
+
* Paint this node's WHOLE subtree (self + every descendant), threading the
|
|
333
|
+
* SAME `ctx`. This is the unculled entry used when a caller wants to force a
|
|
334
|
+
* subtree repaint regardless of damage. The damage-tracked frame path does NOT
|
|
335
|
+
* call this — it uses {@link GpuSceneRoot}'s recursive cull, which tests every
|
|
336
|
+
* node's bounds against the damage regions and prunes missed subtrees. Keeping
|
|
337
|
+
* the cull out of `paint` means a hit subtree still walks fully, while the cull
|
|
338
|
+
* decides which subtrees to enter.
|
|
339
|
+
*/
|
|
340
|
+
paint(ctx) {
|
|
341
|
+
if (!isPaintCtx(ctx)) throw new Error("[insomni v3] GpuSceneNode.paint called with a non-PaintCtx argument. Only the v3 frame spine should call paint(); use drawSelf(ctx) directly for programmatic repaints.");
|
|
342
|
+
this.drawSelf(ctx);
|
|
343
|
+
for (const child of this.children) child.paint(ctx);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Project this node's bounds to a screen-pixel `FrameRect` via the T21
|
|
347
|
+
* `project` hook. Call this BEFORE `markDamaged` when a node authors in a
|
|
348
|
+
* non-`ui` space so the emitted damage rect already lives in the screen-pixel
|
|
349
|
+
* space the cull + scissor share. (For a plain `ui` scene graph the bounds are
|
|
350
|
+
* already CSS px and `markDamaged` may be called directly.)
|
|
351
|
+
*
|
|
352
|
+
* `space` defaults to `"ui"` — the natural space for a CSS-pixel scene graph
|
|
353
|
+
* whose `bounds` are authored in screen pixels.
|
|
354
|
+
*/
|
|
355
|
+
projectBounds(ctx, space = "ui") {
|
|
356
|
+
const b = this.bounds;
|
|
357
|
+
return project(b.x, b.y, b.x + b.w, b.y + b.h, space, ctx.projectOpts);
|
|
358
|
+
}
|
|
359
|
+
/** Mark a visual-only change: repaint, no data/layout rebuild. */
|
|
360
|
+
invalidatePaint(rect) {
|
|
361
|
+
this.markDamaged("paint", rect);
|
|
362
|
+
}
|
|
363
|
+
/** Mark a layout change (bounds/children moved). Triggers doLayout + paint. */
|
|
364
|
+
invalidateLayout(rect) {
|
|
365
|
+
this.markDamaged("layout", rect);
|
|
366
|
+
}
|
|
367
|
+
/** Mark a data change (inputs replaced). Triggers rebuildData → layout → paint. */
|
|
368
|
+
invalidateData(rect) {
|
|
369
|
+
this.markDamaged("data", rect);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
var GpuSceneRoot = class extends SceneRoot {
|
|
373
|
+
/** The scene layer that descendant nodes paint into. */
|
|
374
|
+
layer;
|
|
375
|
+
/** The underlying v3 WebGPU renderer. */
|
|
376
|
+
gpu;
|
|
377
|
+
/** Reverse-z pointer routing with per-pointerId capture/drag/hover. */
|
|
378
|
+
pointer;
|
|
379
|
+
/**
|
|
380
|
+
* When `true`, repaint every node on any damage and ignore the per-node cull
|
|
381
|
+
* (set from {@link GpuSceneRootOptions.fullFrame}, default `false`).
|
|
382
|
+
* Passed through to `endFrame()` so the renderer does a full repaint.
|
|
383
|
+
*/
|
|
384
|
+
fullFrame;
|
|
385
|
+
_spine;
|
|
386
|
+
_projectOpts;
|
|
387
|
+
_groups;
|
|
388
|
+
_dataKey;
|
|
389
|
+
/** Our own copy of the timing hook — the base keeps `onFrameTiming` private,
|
|
390
|
+
* and we override `_renderFrame`, so we read it directly. */
|
|
391
|
+
_onFrameTiming;
|
|
392
|
+
/** Damage regions handed to the in-flight `beginFrame`; consumed by `endFrame`. */
|
|
393
|
+
_currentRegions = [];
|
|
394
|
+
constructor(gpu, layer, options) {
|
|
395
|
+
const inertRenderer = {
|
|
396
|
+
beginFrame() {},
|
|
397
|
+
endFrame() {}
|
|
398
|
+
};
|
|
399
|
+
const baseOpts = {
|
|
400
|
+
scheduler: options.scheduler,
|
|
401
|
+
onFrameTiming: options.onFrameTiming
|
|
402
|
+
};
|
|
403
|
+
super(inertRenderer, baseOpts);
|
|
404
|
+
this.gpu = gpu;
|
|
405
|
+
this.layer = layer;
|
|
406
|
+
this.fullFrame = options.fullFrame ?? false;
|
|
407
|
+
this._projectOpts = options.projectOpts;
|
|
408
|
+
this._groups = options.groups;
|
|
409
|
+
this._dataKey = options.dataKey;
|
|
410
|
+
this._onFrameTiming = options.onFrameTiming;
|
|
411
|
+
this._spine = new FrameSpine({
|
|
412
|
+
scheduler: options.scheduler,
|
|
413
|
+
onFlush: (dirty) => this._renderFrameV3(dirty)
|
|
414
|
+
});
|
|
415
|
+
this.pointer = new PointerRouter(this);
|
|
416
|
+
}
|
|
417
|
+
/** Stash the frame's damage regions and clear the scene layer. */
|
|
418
|
+
beginFrame(regions) {
|
|
419
|
+
this._currentRegions = regions;
|
|
420
|
+
this.layer.clear();
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Submit one v3 GPU frame. Each damage `Rect` maps to ONE `Bounds2D` region
|
|
424
|
+
* (1:1 — never unioned), so the renderer's per-rect scissor path repaints each
|
|
425
|
+
* disjoint rect independently. `viewKey` folds the active group transforms +
|
|
426
|
+
* data key so a group move forces a full frame.
|
|
427
|
+
*/
|
|
428
|
+
endFrame() {
|
|
429
|
+
this.gpu.render([this.layer], {
|
|
430
|
+
regions: this._currentRegions.map(rectToBounds),
|
|
431
|
+
fullFrame: this.fullFrame,
|
|
432
|
+
viewKey: this._viewKey()
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Called by `SceneNode.markDamaged` (structural-type contract:
|
|
437
|
+
* `typeof root._emitDamage === "function"`). Routes the damage into the
|
|
438
|
+
* FrameSpine instead of the base SceneRoot's own channel, so a single
|
|
439
|
+
* coalesced flush per frame drives `_renderFrameV3`.
|
|
440
|
+
*/
|
|
441
|
+
_emitDamage(damage) {
|
|
442
|
+
this._spine.mark([damage]);
|
|
443
|
+
}
|
|
444
|
+
/** Force a full repaint of the whole root bounds on the next scheduled frame. */
|
|
445
|
+
requestPaint() {
|
|
446
|
+
this._emitDamage({
|
|
447
|
+
rect: this.bounds,
|
|
448
|
+
kind: "paint"
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Mark a paint-damage region directly on the root (CSS px). Useful when an
|
|
453
|
+
* external surface (a resize, a host overlay) damages a screen rect that is
|
|
454
|
+
* not owned by a single node. Routes through the spine like any node damage.
|
|
455
|
+
*/
|
|
456
|
+
requestPaintRegion(rect) {
|
|
457
|
+
this._emitDamage({
|
|
458
|
+
rect,
|
|
459
|
+
kind: "paint"
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Run one frame from a coalesced dirty set. Mirrors the base SceneRoot
|
|
464
|
+
* pipeline (rebuildData on data-damage, doLayout on non-paint damage) but
|
|
465
|
+
* replaces the one-level `_paintCulled` with the recursive cull and uses our
|
|
466
|
+
* own `beginFrame`/`endFrame`. `fullFrame` repaints the whole bounds.
|
|
467
|
+
*/
|
|
468
|
+
_renderFrameV3(dirty) {
|
|
469
|
+
const timing = this._onFrameTiming;
|
|
470
|
+
const t0 = timing ? performance.now() : 0;
|
|
471
|
+
for (const d of dirty) {
|
|
472
|
+
const node = d.node;
|
|
473
|
+
if (d.kind === "data" && node?.rebuildData) node.rebuildData();
|
|
474
|
+
}
|
|
475
|
+
for (const d of dirty) {
|
|
476
|
+
const node = d.node;
|
|
477
|
+
if (d.kind !== "paint" && node?.doLayout) node.doLayout();
|
|
478
|
+
}
|
|
479
|
+
const regions = this.fullFrame ? [this.bounds] : dirty.map((d) => d.rect);
|
|
480
|
+
const t1 = timing ? performance.now() : 0;
|
|
481
|
+
this.beginFrame(regions);
|
|
482
|
+
const ctx = {
|
|
483
|
+
layer: this.layer,
|
|
484
|
+
projectOpts: this._projectOpts()
|
|
485
|
+
};
|
|
486
|
+
const paintedNodes = this._paintCulledRecursive(this, regions, ctx);
|
|
487
|
+
this.endFrame();
|
|
488
|
+
if (timing) timing({
|
|
489
|
+
layoutMs: t1 - t0,
|
|
490
|
+
paintMs: performance.now() - t1,
|
|
491
|
+
paintedNodes
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Recursive damage cull — the v3 fix for v1's one-level cull. For each child
|
|
496
|
+
* of `node`: if its bounds miss EVERY region, prune the entire subtree (no
|
|
497
|
+
* `drawSelf`, no recursion into ITS children). On a hit, paint the child via
|
|
498
|
+
* `drawSelf(ctx)` and recurse — so the SAME per-region test applies at every
|
|
499
|
+
* depth, not just the root's direct children. A leaf deep in a large container
|
|
500
|
+
* therefore repaints only the nodes whose AABB actually touches a damage rect,
|
|
501
|
+
* and the cost scales with the damaged area, not the scene size.
|
|
502
|
+
*
|
|
503
|
+
* Pruning a missed subtree is sound: a node's bounds are expected to bound its
|
|
504
|
+
* own painted geometry (and, when it `clipsOverflow`, its descendants too), so
|
|
505
|
+
* a subtree whose AABB misses every damage rect contributes nothing inside any
|
|
506
|
+
* rect. (Nodes that paint outside their declared bounds must widen `bounds`.)
|
|
507
|
+
*
|
|
508
|
+
* Note: this does NOT call `GpuSceneNode.paint` (which would recurse
|
|
509
|
+
* unconditionally and defeat the deep cull) — it owns the descent itself.
|
|
510
|
+
*/
|
|
511
|
+
_paintCulledRecursive(node, regions, ctx) {
|
|
512
|
+
let painted = 0;
|
|
513
|
+
for (const child of node.children) {
|
|
514
|
+
let hit = false;
|
|
515
|
+
for (const region of regions) if (rectOverlaps(region, child.bounds)) {
|
|
516
|
+
hit = true;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
if (!hit) continue;
|
|
520
|
+
child.drawSelf(ctx);
|
|
521
|
+
painted++;
|
|
522
|
+
painted += this._paintCulledRecursive(child, regions, ctx);
|
|
523
|
+
}
|
|
524
|
+
return painted;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Composite `viewKey`: fold every active group's compounded transform, then
|
|
528
|
+
* the data key, into one string. Handed to `render(..., { viewKey })`; a
|
|
529
|
+
* change flips the renderer's view fingerprint and forces a full frame.
|
|
530
|
+
*/
|
|
531
|
+
_viewKey() {
|
|
532
|
+
let key = "";
|
|
533
|
+
const groups = this._groups?.();
|
|
534
|
+
if (groups) for (const g of groups) {
|
|
535
|
+
const m = resolveGroupTransform(g);
|
|
536
|
+
key += `${m[0]},${m[1]},${m[3]},${m[4]},${m[6]},${m[7]};`;
|
|
537
|
+
}
|
|
538
|
+
const dk = this._dataKey?.();
|
|
539
|
+
if (dk !== void 0) key += `|${dk}`;
|
|
540
|
+
return key;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Dispatch a pointer event through the {@link PointerRouter}: `down` hit-tests
|
|
544
|
+
* (reverse-z, last-child-wins, depth-first — the inherited `SceneRoot.hitTest`)
|
|
545
|
+
* and captures by `pointerId`; `move`/`up`/`cancel` route to the captured node
|
|
546
|
+
* (or, for an uncaptured `move`, re-hit). Returns the receiving node or null.
|
|
547
|
+
*/
|
|
548
|
+
dispatchPointer(e) {
|
|
549
|
+
return this.pointer.dispatch(e);
|
|
550
|
+
}
|
|
551
|
+
/** Dispose the frame spine (removes the subscriber, cancels any pending flush). */
|
|
552
|
+
dispose() {
|
|
553
|
+
this._spine.dispose();
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
/** Convert a spatial `Rect` to the renderer's `Bounds2D` damage-region shape. */
|
|
557
|
+
function rectToBounds(r) {
|
|
558
|
+
return {
|
|
559
|
+
minX: r.x,
|
|
560
|
+
minY: r.y,
|
|
561
|
+
maxX: r.x + r.w,
|
|
562
|
+
maxY: r.y + r.h
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
//#endregion
|
|
566
|
+
export { ABuffer, ARC_FLOATS, BUILD_SPATIAL_HASH_WGSL, CLEAR_SPATIAL_HASH_WGSL, COMPOSITE_UNIFORM_BYTES, COMPOSITE_UNIFORM_FLOATS, CURVE_CAP_BUTT, CURVE_CAP_ROUND, CURVE_CAP_SQUARE, CURVE_FLOATS, FRAME_RING_CAPACITY, FULL_FRAME_TILE_FRACTION, FrameRing, FrameSpine, GLYPH_PARAMS_BYTES, GLYPH_PARAMS_FLOATS, GpuSceneNode, GpuSceneRoot, HEAD_BYTES, INSTANCE_DRAW_BYTES, INSTANCE_DRAW_FLOATS, RegionIndex, RendererDebug, 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, ShapeSchema, 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 };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/core/logger.ts
|
|
2
|
+
let _logger = console;
|
|
3
|
+
/**
|
|
4
|
+
* Install the process-wide diagnostic logger. Pass `null`/`undefined` to reset
|
|
5
|
+
* back to the {@link console} default.
|
|
6
|
+
*/
|
|
7
|
+
function setLogger(logger) {
|
|
8
|
+
_logger = logger ?? console;
|
|
9
|
+
}
|
|
10
|
+
/** The active diagnostic logger. Defaults to {@link console}. */
|
|
11
|
+
function getLogger() {
|
|
12
|
+
return _logger;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { setLogger as n, getLogger as t };
|