@ifc-lite/renderer 1.49.0 → 1.50.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.
Files changed (86) hide show
  1. package/dist/camera.d.ts +24 -0
  2. package/dist/camera.d.ts.map +1 -1
  3. package/dist/camera.js +72 -1
  4. package/dist/camera.js.map +1 -1
  5. package/dist/device.d.ts +11 -0
  6. package/dist/device.d.ts.map +1 -1
  7. package/dist/device.js +71 -5
  8. package/dist/device.js.map +1 -1
  9. package/dist/frame-timing-cpu.d.ts +31 -0
  10. package/dist/frame-timing-cpu.d.ts.map +1 -0
  11. package/dist/frame-timing-cpu.js +25 -0
  12. package/dist/frame-timing-cpu.js.map +1 -0
  13. package/dist/frame-timing-gpu.d.ts +144 -0
  14. package/dist/frame-timing-gpu.d.ts.map +1 -0
  15. package/dist/frame-timing-gpu.js +176 -0
  16. package/dist/frame-timing-gpu.js.map +1 -0
  17. package/dist/frame-timing-stats.d.ts +100 -0
  18. package/dist/frame-timing-stats.d.ts.map +1 -0
  19. package/dist/frame-timing-stats.js +116 -0
  20. package/dist/frame-timing-stats.js.map +1 -0
  21. package/dist/frame-timing.d.ts +102 -0
  22. package/dist/frame-timing.d.ts.map +1 -0
  23. package/dist/frame-timing.js +112 -0
  24. package/dist/frame-timing.js.map +1 -0
  25. package/dist/index.d.ts +35 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +229 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/lod-simplify.d.ts +0 -9
  30. package/dist/lod-simplify.d.ts.map +1 -1
  31. package/dist/lod-simplify.js +45 -7
  32. package/dist/lod-simplify.js.map +1 -1
  33. package/dist/pipeline.d.ts +16 -0
  34. package/dist/pipeline.d.ts.map +1 -1
  35. package/dist/pipeline.js +89 -3
  36. package/dist/pipeline.js.map +1 -1
  37. package/dist/pointcloud/point-cloud-spatial-index.d.ts +81 -9
  38. package/dist/pointcloud/point-cloud-spatial-index.d.ts.map +1 -1
  39. package/dist/pointcloud/point-cloud-spatial-index.js +140 -29
  40. package/dist/pointcloud/point-cloud-spatial-index.js.map +1 -1
  41. package/dist/raycast-engine.d.ts.map +1 -1
  42. package/dist/raycast-engine.js +10 -3
  43. package/dist/raycast-engine.js.map +1 -1
  44. package/dist/scene.d.ts.map +1 -1
  45. package/dist/scene.js +42 -5
  46. package/dist/scene.js.map +1 -1
  47. package/dist/section-2d-overlay.d.ts.map +1 -1
  48. package/dist/section-2d-overlay.js +6 -3
  49. package/dist/section-2d-overlay.js.map +1 -1
  50. package/dist/section-plane-basis.d.ts +17 -11
  51. package/dist/section-plane-basis.d.ts.map +1 -1
  52. package/dist/section-plane-basis.js +55 -16
  53. package/dist/section-plane-basis.js.map +1 -1
  54. package/dist/section-plane.d.ts +11 -0
  55. package/dist/section-plane.d.ts.map +1 -1
  56. package/dist/section-plane.js.map +1 -1
  57. package/dist/shaders/main.wgsl.d.ts +1 -1
  58. package/dist/shaders/main.wgsl.d.ts.map +1 -1
  59. package/dist/shaders/main.wgsl.js +87 -4
  60. package/dist/shaders/main.wgsl.js.map +1 -1
  61. package/dist/shaders/shadow.wgsl.d.ts +31 -0
  62. package/dist/shaders/shadow.wgsl.d.ts.map +1 -0
  63. package/dist/shaders/shadow.wgsl.js +141 -0
  64. package/dist/shaders/shadow.wgsl.js.map +1 -0
  65. package/dist/shadow-light-matrix.d.ts +106 -0
  66. package/dist/shadow-light-matrix.d.ts.map +1 -0
  67. package/dist/shadow-light-matrix.js +185 -0
  68. package/dist/shadow-light-matrix.js.map +1 -0
  69. package/dist/shadow-occluders.d.ts +106 -0
  70. package/dist/shadow-occluders.d.ts.map +1 -0
  71. package/dist/shadow-occluders.js +156 -0
  72. package/dist/shadow-occluders.js.map +1 -0
  73. package/dist/shadow-pass.d.ts +132 -0
  74. package/dist/shadow-pass.d.ts.map +1 -0
  75. package/dist/shadow-pass.js +342 -0
  76. package/dist/shadow-pass.js.map +1 -0
  77. package/dist/snap-detector.d.ts +10 -0
  78. package/dist/snap-detector.d.ts.map +1 -1
  79. package/dist/snap-detector.js.map +1 -1
  80. package/dist/symbolic-overlay-pipelines.d.ts +35 -0
  81. package/dist/symbolic-overlay-pipelines.d.ts.map +1 -1
  82. package/dist/symbolic-overlay-pipelines.js +31 -15
  83. package/dist/symbolic-overlay-pipelines.js.map +1 -1
  84. package/dist/types.d.ts +40 -0
  85. package/dist/types.d.ts.map +1 -1
  86. package/package.json +4 -4
@@ -0,0 +1,176 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /** Feature-detects `'timestamp-query'` on an already-created `GPUDevice`'s adapter features, without touching mode-decision logic (see `decideTimingMode` in `frame-timing.ts`, which consumes this boolean). */
5
+ export function hasTimestampQueryFeature(features) {
6
+ return features?.has('timestamp-query') ?? false;
7
+ }
8
+ const BYTES_PER_TIMESTAMP = 8; // GPUQuerySet resolves each timestamp query to one 64-bit (BigInt64) value.
9
+ /**
10
+ * Byte size of the resolve/readback buffers needed for `passCount` passes —
11
+ * 2 timestamp queries (begin + end) per pass, `BYTES_PER_TIMESTAMP` each.
12
+ * Pure arithmetic, decidable without a device: extracted out of `create()`
13
+ * so the sizing formula itself is unit-tested rather than only ever
14
+ * exercised as a side effect of a real `device.createBuffer` call.
15
+ */
16
+ export function queryBufferSizeBytes(passCount) {
17
+ return passCount * 2 * BYTES_PER_TIMESTAMP;
18
+ }
19
+ /**
20
+ * Allocates the next pair of query-set write indices for one pass, or
21
+ * `null` if `maxPasses` passes have already been begun this frame. Pure:
22
+ * given the current cursor and the frame's pass budget, the next
23
+ * (begin, end, cursor) triple — or exhaustion — is fully determined; no
24
+ * `GPUQuerySet` is touched to decide it. Extracted out of
25
+ * `GpuFrameTimingRecorder.beginPass` so this index bookkeeping (the part
26
+ * most likely to hide an off-by-one — see the exhaustion boundary test) is
27
+ * checked with synthetic cursor/maxPasses values instead of only ever
28
+ * running inside a live recording session.
29
+ */
30
+ export function allocatePassQueryIndices(nextQueryIndex, maxPasses) {
31
+ if (nextQueryIndex + 1 >= maxPasses * 2)
32
+ return null;
33
+ return {
34
+ beginningOfPassWriteIndex: nextQueryIndex,
35
+ endOfPassWriteIndex: nextQueryIndex + 1,
36
+ nextQueryIndex: nextQueryIndex + 2,
37
+ };
38
+ }
39
+ /**
40
+ * Pairs each recorded pass `label` (in recording order) with its
41
+ * (start, end) nanosecond timestamps at `timestamps[i*2]` /
42
+ * `timestamps[i*2+1]` — the layout `GpuFrameTimingRecorder` writes via
43
+ * `timestampWrites`. Pure: given a labels array and a `BigInt64Array`, the
44
+ * resulting `PassTimingSample[]` is fully determined; no `GPUBuffer`
45
+ * mapping is involved. Extracted out of `readback()` so this pairing
46
+ * arithmetic — the part that would silently mis-attribute a duration to
47
+ * the wrong label on an off-by-one — is checked directly.
48
+ *
49
+ * `GpuFrameTimingRecorder.readback()` always calls this with
50
+ * `timestamps.length === labels.length * 2` (it sizes the readback slice
51
+ * from the same `nextQueryIndex` cursor that `beginPass` pushed each label
52
+ * against — see `readback()`'s call site), so a short buffer cannot occur
53
+ * on that path today. But this function is exported precisely so it can be
54
+ * exercised standalone, and a caller passing a corrupted or hand-built
55
+ * buffer must not get back a sample whose `startNs`/`endNs` is `undefined`
56
+ * where the type says `bigint`: that silently propagates into
57
+ * `frameTotalMs`/`passDurationsMs` (`frame-timing.ts`), which throws a
58
+ * `TypeError` mixing `BigInt` and `undefined` for a one-short buffer, or
59
+ * silently computes `NaN` for a two-short buffer — two different failure
60
+ * shapes for what is really the same input error, and neither is the
61
+ * "never throws, never fabricates" contract the rest of this module's
62
+ * siblings hold themselves to. A label whose (start, end) pair does not
63
+ * fully fit in `timestamps` is dropped rather than pushed with a missing
64
+ * field.
65
+ */
66
+ export function pairTimestampsWithLabels(labels, timestamps) {
67
+ const samples = [];
68
+ for (let i = 0; i < labels.length; i++) {
69
+ if (i * 2 + 1 >= timestamps.length)
70
+ break; // buffer shorter than this (and every later) label needs — stop rather than fabricate a partial pair.
71
+ samples.push({ label: labels[i], startNs: timestamps[i * 2], endNs: timestamps[i * 2 + 1] });
72
+ }
73
+ return samples;
74
+ }
75
+ /**
76
+ * Records GPU timestamp queries for the passes of one frame and resolves
77
+ * them into `PassTimingSample[]` (nanosecond pairs; see `frame-timing.ts`
78
+ * for what happens to them next). One instance is good for one frame's
79
+ * worth of passes up to `maxPasses`, then must be recreated (or reset via
80
+ * `beginFrame()`) for the next — this keeps the query-set/readback-buffer
81
+ * lifetime unambiguous rather than trying to make it silently reusable
82
+ * across frames while a previous frame's readback might still be pending.
83
+ */
84
+ export class GpuFrameTimingRecorder {
85
+ maxPasses;
86
+ querySet;
87
+ resolveBuffer;
88
+ readbackBuffer;
89
+ labels = [];
90
+ nextQueryIndex = 0;
91
+ resolved = false;
92
+ constructor(maxPasses, querySet, resolveBuffer, readbackBuffer) {
93
+ this.maxPasses = maxPasses;
94
+ this.querySet = querySet;
95
+ this.resolveBuffer = resolveBuffer;
96
+ this.readbackBuffer = readbackBuffer;
97
+ }
98
+ /**
99
+ * Returns a recorder, or `null` if the device's adapter did not advertise
100
+ * `'timestamp-query'` — callers must treat `null` as "cannot measure this
101
+ * way" and either fall back to CPU-side timing (`decideTimingMode` in
102
+ * `frame-timing.ts`) or skip measurement, never throw.
103
+ */
104
+ static create(device, maxPasses = 8) {
105
+ if (!hasTimestampQueryFeature(device.features))
106
+ return null;
107
+ const querySet = device.createQuerySet({ type: 'timestamp', count: maxPasses * 2, label: 'frame-timing-queries' });
108
+ const resolveBuffer = device.createBuffer({
109
+ size: queryBufferSizeBytes(maxPasses),
110
+ usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
111
+ label: 'frame-timing-resolve',
112
+ });
113
+ const readbackBuffer = device.createBuffer({
114
+ size: queryBufferSizeBytes(maxPasses),
115
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
116
+ label: 'frame-timing-readback',
117
+ });
118
+ return new GpuFrameTimingRecorder(maxPasses, querySet, resolveBuffer, readbackBuffer);
119
+ }
120
+ /**
121
+ * Returns the `timestampWrites` object for the next pass, labelled
122
+ * `label`. Pass it straight into `beginRenderPass`'s descriptor. Returns
123
+ * `null` once `maxPasses` passes have been begun this frame — a caller
124
+ * that hits this should raise `maxPasses` at construction, not retry.
125
+ */
126
+ beginPass(label) {
127
+ const allocation = allocatePassQueryIndices(this.nextQueryIndex, this.maxPasses);
128
+ if (allocation === null)
129
+ return null;
130
+ this.nextQueryIndex = allocation.nextQueryIndex;
131
+ this.labels.push(label);
132
+ return {
133
+ querySet: this.querySet,
134
+ beginningOfPassWriteIndex: allocation.beginningOfPassWriteIndex,
135
+ endOfPassWriteIndex: allocation.endOfPassWriteIndex,
136
+ };
137
+ }
138
+ /** Resolves every query written this frame into the readback buffer. Call once, after every pass has been `.end()`-ed, before `queue.submit`. */
139
+ endFrame(encoder) {
140
+ if (this.nextQueryIndex === 0)
141
+ return; // no passes recorded — nothing to resolve
142
+ encoder.resolveQuerySet(this.querySet, 0, this.nextQueryIndex, this.resolveBuffer, 0);
143
+ encoder.copyBufferToBuffer(this.resolveBuffer, 0, this.readbackBuffer, 0, this.nextQueryIndex * BYTES_PER_TIMESTAMP);
144
+ this.resolved = true;
145
+ }
146
+ /**
147
+ * Maps the readback buffer and returns this frame's `PassTimingSample[]`,
148
+ * or `null` if `endFrame` was never called (nothing was recorded, or the
149
+ * caller forgot). Async because `mapAsync` is: the caller's queue submit
150
+ * must have completed first for the buffer to contain real data — WebGPU
151
+ * enforces this by making `mapAsync` wait for pending GPU work that
152
+ * touches the buffer.
153
+ */
154
+ async readback() {
155
+ if (!this.resolved)
156
+ return null;
157
+ await this.readbackBuffer.mapAsync(GPUMapMode.READ);
158
+ const raw = this.readbackBuffer.getMappedRange(0, this.nextQueryIndex * BYTES_PER_TIMESTAMP);
159
+ const timestamps = new BigInt64Array(raw.slice(0)); // copy out before unmap invalidates the ArrayBuffer
160
+ this.readbackBuffer.unmap();
161
+ return pairTimestampsWithLabels(this.labels, timestamps);
162
+ }
163
+ /** Resets for the next frame's recording. Does not reallocate the query set or buffers — they are sized once at `create()` and reused. */
164
+ beginFrame() {
165
+ this.labels = [];
166
+ this.nextQueryIndex = 0;
167
+ this.resolved = false;
168
+ }
169
+ /** Releases the GPU query set and buffers. Call when timing is turned off. */
170
+ destroy() {
171
+ this.querySet.destroy();
172
+ this.resolveBuffer.destroy();
173
+ this.readbackBuffer.destroy();
174
+ }
175
+ }
176
+ //# sourceMappingURL=frame-timing-gpu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame-timing-gpu.js","sourceRoot":"","sources":["../src/frame-timing-gpu.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Pure statistics for frame/pass GPU timing (issue #2670 perf-verdict gate).
3
+ *
4
+ * Everything in this file is arithmetic over plain numbers/bigints — no
5
+ * `GPUQuerySet`, no `performance.now()`, nothing that touches a clock or a
6
+ * device. That split is deliberate: the thin GPU-facing code in
7
+ * `frame-timing-gpu.ts` cannot be exercised without a WebGPU adapter with the
8
+ * `timestamp-query` feature (which this environment's Chromium does not
9
+ * expose — `navigator.gpu` itself is absent), so every number it produces is
10
+ * pushed through here, where it CAN be tested with synthetic values.
11
+ */
12
+ /**
13
+ * Summary statistics over a sample of frame or pass durations, in
14
+ * milliseconds.
15
+ *
16
+ * A mean hides exactly the stutter a perf verdict cares about — one frame
17
+ * that spikes to 40 ms among fifty at 8 ms barely moves a mean, but it is
18
+ * the one a user notices. `p95` (and `max`, its worst case) are the
19
+ * statistics this module treats as the headline; `mean` and `median` are
20
+ * kept for context, not as the number to gate on.
21
+ *
22
+ * `count === 0` is the explicit empty-sample marker: every other field is
23
+ * `null` rather than `0`, because a real all-zero sample and "nothing was
24
+ * measured" must never be indistinguishable to a caller reading this object.
25
+ */
26
+ export interface DurationStats {
27
+ /** Number of samples the statistics below were computed over. */
28
+ count: number;
29
+ min: number | null;
30
+ /** 50th percentile (nearest-rank on the sorted sample). */
31
+ median: number | null;
32
+ /** 95th percentile (nearest-rank on the sorted sample) — see class doc. */
33
+ p95: number | null;
34
+ max: number | null;
35
+ /** Arithmetic mean. Context only — do not gate a perf verdict on this. */
36
+ mean: number | null;
37
+ }
38
+ /**
39
+ * WebGPU timestamp queries resolve to nanoseconds (`BigInt64Array` values
40
+ * read back from the resolve buffer). The renderer's budgets and every other
41
+ * timing surface in this codebase (`FrameStats.timestamp` via
42
+ * `performance.now()`) are in milliseconds, so every raw pair gets converted
43
+ * through this one function.
44
+ *
45
+ * Takes `bigint` (the actual readback type) so a caller cannot accidentally
46
+ * pass an already-lossy `Number(timestamp)` of a value that may exceed
47
+ * `Number.MAX_SAFE_INTEGER` (a query set can hold timestamps for a
48
+ * long-running session; ~104 days of nanoseconds overflows a JS number).
49
+ * The subtraction happens in `bigint` space and only the final, small
50
+ * (sub-second, in practice sub-100ms) millisecond duration is converted to
51
+ * `number`.
52
+ *
53
+ * Clamps a negative delta (`endNs < startNs`) to `0` rather than returning
54
+ * a physically impossible negative duration. This is reachable: GPU
55
+ * timestamps are not guaranteed monotonic across a device reset (see this
56
+ * module's own doc above). `0` is the honest floor — a duration cannot be
57
+ * negative, and unlike a raw negative number it cannot silently drag a
58
+ * `min`/`mean` computed downstream into nonsense. This is the guard's one
59
+ * choke point: `frameTotalMs`/`passDurationsMs` (`frame-timing.ts`) sum
60
+ * this function's output directly, without ever routing it through
61
+ * `computeDurationStats`, so a guard placed only in `computeDurationStats`
62
+ * would miss that summation entirely. Use `isNegativeDelta` alongside this
63
+ * function where the caller wants to know a clamp happened.
64
+ */
65
+ export declare function nsToMs(startNs: bigint, endNs: bigint): number;
66
+ /**
67
+ * Reports whether `(startNs, endNs)` is a monotonicity violation (`endNs <
68
+ * startNs`) — the only case the module's own doc calls out as reachable:
69
+ * GPU timestamps "are not guaranteed monotonic across a device reset".
70
+ *
71
+ * This is the guard's true origin point: `nsToMs` alone cannot both return a
72
+ * single honest millisecond number for every downstream summation (see
73
+ * `frameTotalMs`/`passDurationsMs` in `frame-timing.ts`, which add its
74
+ * output directly into a running total and never pass through
75
+ * `computeDurationStats`) AND separately expose "this one was bad" — a
76
+ * plain `number` return has no second channel for that. Callers that want
77
+ * to surface an invalid-sample count (rather than let a clamped-to-0 value
78
+ * pass as an unremarkable real zero) call this predicate on the same raw
79
+ * pair alongside `nsToMs`. `computeDurationStats` is deliberately NOT where
80
+ * this lives: by the time a caller has an array of plain millisecond
81
+ * numbers, it has no way to tell a clamped GPU-reset zero apart from a
82
+ * genuine fast pass or a CPU-fallback delta (which is never negative — it
83
+ * comes from `performance.now()`, spec-guaranteed non-decreasing) — see
84
+ * that function's doc.
85
+ */
86
+ export declare function isNegativeDelta(startNs: bigint, endNs: bigint): boolean;
87
+ /**
88
+ * Reduces a sample of durations (milliseconds) to summary statistics. Pure:
89
+ * no side effects, no clock reads — the durations are supplied by the
90
+ * caller, whether they came from GPU timestamp queries or the CPU
91
+ * frame-delta fallback (see `frame-timing.ts`).
92
+ *
93
+ * `durationsMs.length === 0` returns `EMPTY_STATS` (`count: 0`, every other
94
+ * field `null`) rather than computing `0`s — a percentile or a mean over an
95
+ * empty array is either `NaN` (misreported as a real "instant" frame) or a
96
+ * divide-by-zero, and either would read as "this was fast" instead of "this
97
+ * was never measured".
98
+ */
99
+ export declare function computeDurationStats(durationsMs: readonly number[]): DurationStats;
100
+ //# sourceMappingURL=frame-timing-stats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame-timing-stats.d.ts","sourceRoot":"","sources":["../src/frame-timing-stats.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,aAAa;IAC5B,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,2DAA2D;IAC3D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,2EAA2E;IAC3E,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAoBD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAEvE;AAeD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,aAAa,CAclF"}
@@ -0,0 +1,116 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * The one empty-sample result, handed back BY REFERENCE to every caller of
6
+ * `computeDurationStats([])`. Frozen for exactly that reason: an unfrozen
7
+ * shared constant lets one caller's in-place edit (e.g. patching the nulls to
8
+ * zeros for its own display code) rewrite what every LATER empty result
9
+ * reports, turning an honest "nothing was measured" into a fabricated number
10
+ * process-wide. Frozen, such a write is a no-op — a `TypeError` under the
11
+ * strict mode every ES module runs in — and the constant stays honest.
12
+ */
13
+ const EMPTY_STATS = Object.freeze({
14
+ count: 0,
15
+ min: null,
16
+ median: null,
17
+ p95: null,
18
+ max: null,
19
+ mean: null,
20
+ });
21
+ /**
22
+ * WebGPU timestamp queries resolve to nanoseconds (`BigInt64Array` values
23
+ * read back from the resolve buffer). The renderer's budgets and every other
24
+ * timing surface in this codebase (`FrameStats.timestamp` via
25
+ * `performance.now()`) are in milliseconds, so every raw pair gets converted
26
+ * through this one function.
27
+ *
28
+ * Takes `bigint` (the actual readback type) so a caller cannot accidentally
29
+ * pass an already-lossy `Number(timestamp)` of a value that may exceed
30
+ * `Number.MAX_SAFE_INTEGER` (a query set can hold timestamps for a
31
+ * long-running session; ~104 days of nanoseconds overflows a JS number).
32
+ * The subtraction happens in `bigint` space and only the final, small
33
+ * (sub-second, in practice sub-100ms) millisecond duration is converted to
34
+ * `number`.
35
+ *
36
+ * Clamps a negative delta (`endNs < startNs`) to `0` rather than returning
37
+ * a physically impossible negative duration. This is reachable: GPU
38
+ * timestamps are not guaranteed monotonic across a device reset (see this
39
+ * module's own doc above). `0` is the honest floor — a duration cannot be
40
+ * negative, and unlike a raw negative number it cannot silently drag a
41
+ * `min`/`mean` computed downstream into nonsense. This is the guard's one
42
+ * choke point: `frameTotalMs`/`passDurationsMs` (`frame-timing.ts`) sum
43
+ * this function's output directly, without ever routing it through
44
+ * `computeDurationStats`, so a guard placed only in `computeDurationStats`
45
+ * would miss that summation entirely. Use `isNegativeDelta` alongside this
46
+ * function where the caller wants to know a clamp happened.
47
+ */
48
+ export function nsToMs(startNs, endNs) {
49
+ const deltaNs = endNs - startNs;
50
+ if (deltaNs < 0n)
51
+ return 0;
52
+ return Number(deltaNs) / 1_000_000;
53
+ }
54
+ /**
55
+ * Reports whether `(startNs, endNs)` is a monotonicity violation (`endNs <
56
+ * startNs`) — the only case the module's own doc calls out as reachable:
57
+ * GPU timestamps "are not guaranteed monotonic across a device reset".
58
+ *
59
+ * This is the guard's true origin point: `nsToMs` alone cannot both return a
60
+ * single honest millisecond number for every downstream summation (see
61
+ * `frameTotalMs`/`passDurationsMs` in `frame-timing.ts`, which add its
62
+ * output directly into a running total and never pass through
63
+ * `computeDurationStats`) AND separately expose "this one was bad" — a
64
+ * plain `number` return has no second channel for that. Callers that want
65
+ * to surface an invalid-sample count (rather than let a clamped-to-0 value
66
+ * pass as an unremarkable real zero) call this predicate on the same raw
67
+ * pair alongside `nsToMs`. `computeDurationStats` is deliberately NOT where
68
+ * this lives: by the time a caller has an array of plain millisecond
69
+ * numbers, it has no way to tell a clamped GPU-reset zero apart from a
70
+ * genuine fast pass or a CPU-fallback delta (which is never negative — it
71
+ * comes from `performance.now()`, spec-guaranteed non-decreasing) — see
72
+ * that function's doc.
73
+ */
74
+ export function isNegativeDelta(startNs, endNs) {
75
+ return endNs - startNs < 0n;
76
+ }
77
+ /**
78
+ * Nearest-rank percentile over `sorted` (must already be sorted ascending).
79
+ * `p` is a fraction in `[0, 1]`. Rounds the rank rather than interpolating —
80
+ * cheap, deterministic, and matches how `p95`/`median` are colloquially read
81
+ * off a sorted sample in perf work.
82
+ */
83
+ function percentile(sorted, p) {
84
+ if (sorted.length === 1)
85
+ return sorted[0];
86
+ const rank = Math.ceil(p * sorted.length) - 1;
87
+ const clamped = Math.min(Math.max(rank, 0), sorted.length - 1);
88
+ return sorted[clamped];
89
+ }
90
+ /**
91
+ * Reduces a sample of durations (milliseconds) to summary statistics. Pure:
92
+ * no side effects, no clock reads — the durations are supplied by the
93
+ * caller, whether they came from GPU timestamp queries or the CPU
94
+ * frame-delta fallback (see `frame-timing.ts`).
95
+ *
96
+ * `durationsMs.length === 0` returns `EMPTY_STATS` (`count: 0`, every other
97
+ * field `null`) rather than computing `0`s — a percentile or a mean over an
98
+ * empty array is either `NaN` (misreported as a real "instant" frame) or a
99
+ * divide-by-zero, and either would read as "this was fast" instead of "this
100
+ * was never measured".
101
+ */
102
+ export function computeDurationStats(durationsMs) {
103
+ if (durationsMs.length === 0)
104
+ return EMPTY_STATS;
105
+ const sorted = [...durationsMs].sort((a, b) => a - b);
106
+ const sum = sorted.reduce((acc, v) => acc + v, 0);
107
+ return {
108
+ count: sorted.length,
109
+ min: sorted[0],
110
+ median: percentile(sorted, 0.5),
111
+ p95: percentile(sorted, 0.95),
112
+ max: sorted[sorted.length - 1],
113
+ mean: sum / sorted.length,
114
+ };
115
+ }
116
+ //# sourceMappingURL=frame-timing-stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame-timing-stats.js","sourceRoot":"","sources":["../src/frame-timing-stats.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAyC/D;;;;;;;;GAQG;AACH,MAAM,WAAW,GAAkB,MAAM,CAAC,MAAM,CAAC;IAC/C,KAAK,EAAE,CAAC;IACR,GAAG,EAAE,IAAI;IACT,MAAM,EAAE,IAAI;IACZ,GAAG,EAAE,IAAI;IACT,GAAG,EAAE,IAAI;IACT,IAAI,EAAE,IAAI;CACX,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,MAAM,CAAC,OAAe,EAAE,KAAa;IACnD,MAAM,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAChC,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,CAAC,CAAC;IAC3B,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC;AACrC,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe,EAAE,KAAa;IAC5D,OAAO,KAAK,GAAG,OAAO,GAAG,EAAE,CAAC;AAC9B,CAAC;AAED;;;;;GAKG;AACH,SAAS,UAAU,CAAC,MAAyB,EAAE,CAAS;IACtD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;IAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC/D,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC;AACzB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAAC,WAA8B;IACjE,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,WAAW,CAAC;IAEjD,MAAM,MAAM,GAAG,CAAC,GAAG,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACtD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAElD,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,MAAM;QACpB,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;QACd,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC;QAC/B,GAAG,EAAE,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC;QAC7B,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;QAC9B,IAAI,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM;KAC1B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Frame/pass timing: mode selection and multi-frame aggregation (issue #2670
3
+ * perf-verdict gate). Pure — see `frame-timing-stats.ts` for the statistics
4
+ * primitives this builds on, and `frame-timing-gpu.ts` for the thin
5
+ * GPU-facing code that produces the raw samples consumed here.
6
+ *
7
+ * Timestamp queries (`GPUQuerySet` of type `'timestamp'`) require the
8
+ * `'timestamp-query'` adapter feature, which is NOT always available — this
9
+ * module's job is to decide, from a plain boolean feature flag, what mode to
10
+ * run in, and to turn raw per-pass nanosecond pairs into the millisecond
11
+ * statistics a caller reads. None of that requires a live device.
12
+ */
13
+ import { type DurationStats } from './frame-timing-stats.js';
14
+ /**
15
+ * How frame timing is actually running, after feature detection:
16
+ * - `'gpu-queries'` — real GPU timestamp queries (most accurate; includes
17
+ * time the CPU never sees, e.g. queued/overlapping work on the GPU).
18
+ * - `'cpu-fallback'` — wall-clock deltas around `render()` on the CPU side.
19
+ * Labelled explicitly wherever it is reported: it measures a DIFFERENT
20
+ * thing (CPU-observed frame cadence, inflated by anything that blocks the
21
+ * main thread) and must never be presented next to a GPU-queries number
22
+ * as if the two were comparable.
23
+ * - `'disabled'` — not measuring. The default; see `decideTimingMode`.
24
+ */
25
+ export type TimingMode = 'gpu-queries' | 'cpu-fallback' | 'disabled';
26
+ export interface TimingModeRequest {
27
+ /** Instrumentation is opt-in (see module doc on the renderer entry point). Defaults to not enabled by every caller in this codebase. */
28
+ enabled: boolean;
29
+ /** Whether the adapter this session got advertises the `'timestamp-query'` feature. */
30
+ hasTimestampQueryFeature: boolean;
31
+ /**
32
+ * When GPU queries are unavailable, fall back to CPU-side frame-delta
33
+ * timing instead of measuring nothing. Defaults to `true` when omitted —
34
+ * pass `false` for a caller that only ever wants labelled GPU numbers, or
35
+ * silence.
36
+ */
37
+ allowCpuFallback?: boolean;
38
+ }
39
+ /**
40
+ * Decides which timing mode to run in. Pure decision table:
41
+ *
42
+ * | enabled | hasTimestampQueryFeature | allowCpuFallback | → mode |
43
+ * |---------|--------------------------|------------------|-----------------|
44
+ * | false | * | * | disabled |
45
+ * | true | true | * | gpu-queries |
46
+ * | true | false | true (default) | cpu-fallback |
47
+ * | true | false | false | disabled |
48
+ *
49
+ * Never throws: an absent feature degrades to a labelled fallback (or to
50
+ * `disabled`), it never crashes the caller for asking.
51
+ */
52
+ export declare function decideTimingMode(request: TimingModeRequest): TimingMode;
53
+ /** One resolved GPU timestamp-query pair for a single pass within one frame. */
54
+ export interface PassTimingSample {
55
+ /** Caller-chosen label, e.g. `'shadow'`, `'main'`, `'sky'`. Passes sharing a label within one frame are summed (see `passDurationsMs`) — useful for e.g. multiple shadow cascades rendered as repeated passes under one logical name. */
56
+ label: string;
57
+ startNs: bigint;
58
+ endNs: bigint;
59
+ }
60
+ /**
61
+ * Per-pass durations (ms) for ONE frame, summed by label. A frame with two
62
+ * `'shadow'` passes and one `'main'` pass returns `{ shadow: <sum of both>,
63
+ * main: <its one duration> }` — the frame's per-pass breakdown, not a list
64
+ * of raw pairs.
65
+ */
66
+ export declare function passDurationsMs(samples: readonly PassTimingSample[]): Record<string, number>;
67
+ /**
68
+ * Total GPU time (ms) for one frame: the sum of every pass's duration.
69
+ * WebGPU passes on one queue do not overlap, so summing durations is the
70
+ * frame total — there is no separate "frame envelope" timestamp pair to
71
+ * reconcile against.
72
+ */
73
+ export declare function frameTotalMs(samples: readonly PassTimingSample[]): number;
74
+ /** Aggregated statistics across many recorded frames. */
75
+ export interface FrameTimingReport {
76
+ mode: TimingMode;
77
+ /** Statistics over each frame's total GPU (or CPU-fallback) time. */
78
+ frame: DurationStats;
79
+ /** Statistics over each pass label's per-frame duration, keyed by label. A label absent from a given frame simply contributes no sample to its stats that frame — it is not treated as a 0ms sample. */
80
+ passes: Record<string, DurationStats>;
81
+ /**
82
+ * Count of raw `(startNs, endNs)` pairs across every frame/pass where
83
+ * `endNs < startNs` — a GPU clock non-monotonicity (see `nsToMs`'s doc).
84
+ * Each such sample is clamped to a 0ms contribution in `frame`/`passes`
85
+ * above rather than dragging `min`/`mean` negative, but a clamped 0 reads
86
+ * identically to a genuine zero-duration pass unless this count is
87
+ * checked — surfaced explicitly here for the same reason `DurationStats`
88
+ * gives empty samples their own explicit shape instead of a silent `0`:
89
+ * this API does not report a number it cannot stand behind without also
90
+ * saying when it had to make one up.
91
+ */
92
+ invalidSampleCount: number;
93
+ }
94
+ /**
95
+ * Reduces a history of per-frame pass samples into a `FrameTimingReport`.
96
+ * `frames` is empty for a session where nothing was ever recorded (or the
97
+ * mode is `'disabled'`) — `computeDurationStats([])` already returns the
98
+ * explicit `count: 0` / all-`null` shape for that case, so no special
99
+ * handling is needed here beyond passing `mode` through.
100
+ */
101
+ export declare function aggregateFrameTimings(mode: TimingMode, frames: readonly (readonly PassTimingSample[])[]): FrameTimingReport;
102
+ //# sourceMappingURL=frame-timing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame-timing.d.ts","sourceRoot":"","sources":["../src/frame-timing.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAiD,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE5G;;;;;;;;;;GAUG;AACH,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,cAAc,GAAG,UAAU,CAAC;AAErE,MAAM,WAAW,iBAAiB;IAChC,wIAAwI;IACxI,OAAO,EAAE,OAAO,CAAC;IACjB,uFAAuF;IACvF,wBAAwB,EAAE,OAAO,CAAC;IAClC;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAKvE;AAED,gFAAgF;AAChF,MAAM,WAAW,gBAAgB;IAC/B,yOAAyO;IACzO,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAa5F;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAMzE;AAED,yDAAyD;AACzD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,UAAU,CAAC;IACjB,qEAAqE;IACrE,KAAK,EAAE,aAAa,CAAC;IACrB,wMAAwM;IACxM,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACtC;;;;;;;;;;OAUG;IACH,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,SAAS,CAAC,SAAS,gBAAgB,EAAE,CAAC,EAAE,GAC/C,iBAAiB,CAiCnB"}
@@ -0,0 +1,112 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * Frame/pass timing: mode selection and multi-frame aggregation (issue #2670
6
+ * perf-verdict gate). Pure — see `frame-timing-stats.ts` for the statistics
7
+ * primitives this builds on, and `frame-timing-gpu.ts` for the thin
8
+ * GPU-facing code that produces the raw samples consumed here.
9
+ *
10
+ * Timestamp queries (`GPUQuerySet` of type `'timestamp'`) require the
11
+ * `'timestamp-query'` adapter feature, which is NOT always available — this
12
+ * module's job is to decide, from a plain boolean feature flag, what mode to
13
+ * run in, and to turn raw per-pass nanosecond pairs into the millisecond
14
+ * statistics a caller reads. None of that requires a live device.
15
+ */
16
+ import { computeDurationStats, nsToMs, isNegativeDelta } from './frame-timing-stats.js';
17
+ /**
18
+ * Decides which timing mode to run in. Pure decision table:
19
+ *
20
+ * | enabled | hasTimestampQueryFeature | allowCpuFallback | → mode |
21
+ * |---------|--------------------------|------------------|-----------------|
22
+ * | false | * | * | disabled |
23
+ * | true | true | * | gpu-queries |
24
+ * | true | false | true (default) | cpu-fallback |
25
+ * | true | false | false | disabled |
26
+ *
27
+ * Never throws: an absent feature degrades to a labelled fallback (or to
28
+ * `disabled`), it never crashes the caller for asking.
29
+ */
30
+ export function decideTimingMode(request) {
31
+ if (!request.enabled)
32
+ return 'disabled';
33
+ if (request.hasTimestampQueryFeature)
34
+ return 'gpu-queries';
35
+ const allowCpuFallback = request.allowCpuFallback ?? true;
36
+ return allowCpuFallback ? 'cpu-fallback' : 'disabled';
37
+ }
38
+ /**
39
+ * Per-pass durations (ms) for ONE frame, summed by label. A frame with two
40
+ * `'shadow'` passes and one `'main'` pass returns `{ shadow: <sum of both>,
41
+ * main: <its one duration> }` — the frame's per-pass breakdown, not a list
42
+ * of raw pairs.
43
+ */
44
+ export function passDurationsMs(samples) {
45
+ // Null-prototype: labels are caller-chosen free text, and on a plain `{}` the
46
+ // inherited `Object.prototype` names are live. `totals['__proto__'] = ms`
47
+ // invokes the prototype setter and is silently dropped (the pass vanishes),
48
+ // and `totals['constructor'] ?? 0` reads the inherited `Object` function, so
49
+ // `+ ms` string-concatenates instead of summing. A bare map has no inherited
50
+ // names, so every label is only ever a key.
51
+ const totals = Object.create(null);
52
+ for (const sample of samples) {
53
+ const ms = nsToMs(sample.startNs, sample.endNs);
54
+ totals[sample.label] = (totals[sample.label] ?? 0) + ms;
55
+ }
56
+ return totals;
57
+ }
58
+ /**
59
+ * Total GPU time (ms) for one frame: the sum of every pass's duration.
60
+ * WebGPU passes on one queue do not overlap, so summing durations is the
61
+ * frame total — there is no separate "frame envelope" timestamp pair to
62
+ * reconcile against.
63
+ */
64
+ export function frameTotalMs(samples) {
65
+ let total = 0;
66
+ for (const sample of samples) {
67
+ total += nsToMs(sample.startNs, sample.endNs);
68
+ }
69
+ return total;
70
+ }
71
+ /**
72
+ * Reduces a history of per-frame pass samples into a `FrameTimingReport`.
73
+ * `frames` is empty for a session where nothing was ever recorded (or the
74
+ * mode is `'disabled'`) — `computeDurationStats([])` already returns the
75
+ * explicit `count: 0` / all-`null` shape for that case, so no special
76
+ * handling is needed here beyond passing `mode` through.
77
+ */
78
+ export function aggregateFrameTimings(mode, frames) {
79
+ const frameDurations = frames.map((f) => frameTotalMs(f));
80
+ const perLabelDurations = new Map();
81
+ let invalidSampleCount = 0;
82
+ for (const frame of frames) {
83
+ const perLabel = passDurationsMs(frame);
84
+ for (const [label, ms] of Object.entries(perLabel)) {
85
+ const list = perLabelDurations.get(label);
86
+ if (list)
87
+ list.push(ms);
88
+ else
89
+ perLabelDurations.set(label, [ms]);
90
+ }
91
+ for (const sample of frame) {
92
+ if (isNegativeDelta(sample.startNs, sample.endNs))
93
+ invalidSampleCount++;
94
+ }
95
+ }
96
+ // Same null-prototype reason as `passDurationsMs` above, and it is a
97
+ // SEPARATE accumulator: `passes['__proto__'] = stats` on a plain object
98
+ // assigns the prototype (the value is an object, so the setter takes it) and
99
+ // creates no own key, dropping the label from the report even when
100
+ // `passDurationsMs` handed it over correctly.
101
+ const passes = Object.create(null);
102
+ for (const [label, durations] of perLabelDurations) {
103
+ passes[label] = computeDurationStats(durations);
104
+ }
105
+ return {
106
+ mode,
107
+ frame: computeDurationStats(frameDurations),
108
+ passes,
109
+ invalidSampleCount,
110
+ };
111
+ }
112
+ //# sourceMappingURL=frame-timing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame-timing.js","sourceRoot":"","sources":["../src/frame-timing.ts"],"names":[],"mappings":"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"}