@ifc-lite/renderer 1.49.1 → 2.0.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/README.md +4 -0
- package/dist/camera-animation.d.ts.map +1 -1
- package/dist/camera-animation.js +37 -27
- package/dist/camera-animation.js.map +1 -1
- package/dist/camera-controls.d.ts +26 -15
- package/dist/camera-controls.d.ts.map +1 -1
- package/dist/camera-controls.js +32 -18
- package/dist/camera-controls.js.map +1 -1
- package/dist/camera-framing.d.ts +60 -21
- package/dist/camera-framing.d.ts.map +1 -1
- package/dist/camera-framing.js +168 -131
- package/dist/camera-framing.js.map +1 -1
- package/dist/camera-preset-view.d.ts +2 -2
- package/dist/camera-preset-view.d.ts.map +1 -1
- package/dist/camera-preset-view.js +52 -42
- package/dist/camera-preset-view.js.map +1 -1
- package/dist/camera.d.ts +50 -6
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +113 -13
- package/dist/camera.js.map +1 -1
- package/dist/device.d.ts +11 -0
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +71 -5
- package/dist/device.js.map +1 -1
- package/dist/federation-registry.d.ts.map +1 -1
- package/dist/federation-registry.js +19 -2
- package/dist/federation-registry.js.map +1 -1
- package/dist/frame-timing-cpu.d.ts +31 -0
- package/dist/frame-timing-cpu.d.ts.map +1 -0
- package/dist/frame-timing-cpu.js +25 -0
- package/dist/frame-timing-cpu.js.map +1 -0
- package/dist/frame-timing-gpu.d.ts +144 -0
- package/dist/frame-timing-gpu.d.ts.map +1 -0
- package/dist/frame-timing-gpu.js +176 -0
- package/dist/frame-timing-gpu.js.map +1 -0
- package/dist/frame-timing-stats.d.ts +100 -0
- package/dist/frame-timing-stats.d.ts.map +1 -0
- package/dist/frame-timing-stats.js +116 -0
- package/dist/frame-timing-stats.js.map +1 -0
- package/dist/frame-timing.d.ts +102 -0
- package/dist/frame-timing.d.ts.map +1 -0
- package/dist/frame-timing.js +112 -0
- package/dist/frame-timing.js.map +1 -0
- package/dist/index.d.ts +77 -59
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +272 -73
- package/dist/index.js.map +1 -1
- package/dist/instanced-override-color.d.ts +17 -0
- package/dist/instanced-override-color.d.ts.map +1 -0
- package/dist/instanced-override-color.js +22 -0
- package/dist/instanced-override-color.js.map +1 -0
- package/dist/instanced-render.d.ts +10 -0
- package/dist/instanced-render.d.ts.map +1 -1
- package/dist/instanced-render.js +13 -3
- package/dist/instanced-render.js.map +1 -1
- package/dist/lod-simplify.d.ts +0 -9
- package/dist/lod-simplify.d.ts.map +1 -1
- package/dist/lod-simplify.js +45 -7
- package/dist/lod-simplify.js.map +1 -1
- package/dist/pick-resolve.d.ts +125 -0
- package/dist/pick-resolve.d.ts.map +1 -0
- package/dist/pick-resolve.js +127 -0
- package/dist/pick-resolve.js.map +1 -0
- package/dist/picker.d.ts.map +1 -1
- package/dist/picker.js +20 -46
- package/dist/picker.js.map +1 -1
- package/dist/picking-manager.d.ts.map +1 -1
- package/dist/picking-manager.js +13 -1
- package/dist/picking-manager.js.map +1 -1
- package/dist/pipeline.d.ts +16 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +89 -3
- package/dist/pipeline.js.map +1 -1
- package/dist/pointcloud/point-cloud-spatial-index.d.ts +81 -9
- package/dist/pointcloud/point-cloud-spatial-index.d.ts.map +1 -1
- package/dist/pointcloud/point-cloud-spatial-index.js +140 -29
- package/dist/pointcloud/point-cloud-spatial-index.js.map +1 -1
- package/dist/raycast-engine.d.ts +4 -4
- package/dist/raycast-engine.d.ts.map +1 -1
- package/dist/raycast-engine.js +10 -3
- package/dist/raycast-engine.js.map +1 -1
- package/dist/renderer-overlays.d.ts +3 -21
- package/dist/renderer-overlays.d.ts.map +1 -1
- package/dist/renderer-overlays.js +57 -85
- package/dist/renderer-overlays.js.map +1 -1
- package/dist/renderer-symbolic-overlays.d.ts +29 -2
- package/dist/renderer-symbolic-overlays.d.ts.map +1 -1
- package/dist/renderer-symbolic-overlays.js +61 -16
- package/dist/renderer-symbolic-overlays.js.map +1 -1
- package/dist/scene-contents.d.ts +132 -0
- package/dist/scene-contents.d.ts.map +1 -0
- package/dist/scene-contents.js +5 -0
- package/dist/scene-contents.js.map +1 -0
- package/dist/scene-raycaster.d.ts +14 -8
- package/dist/scene-raycaster.d.ts.map +1 -1
- package/dist/scene-raycaster.js +6 -2
- package/dist/scene-raycaster.js.map +1 -1
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +63 -12
- package/dist/scene.js.map +1 -1
- package/dist/section-2d-overlay.d.ts +52 -63
- package/dist/section-2d-overlay.d.ts.map +1 -1
- package/dist/section-2d-overlay.js +75 -133
- package/dist/section-2d-overlay.js.map +1 -1
- package/dist/section-plane.d.ts +11 -0
- package/dist/section-plane.d.ts.map +1 -1
- package/dist/section-plane.js.map +1 -1
- package/dist/shaders/main.wgsl.d.ts +1 -1
- package/dist/shaders/main.wgsl.d.ts.map +1 -1
- package/dist/shaders/main.wgsl.js +87 -4
- package/dist/shaders/main.wgsl.js.map +1 -1
- package/dist/shaders/section-2d-overlay.wgsl.d.ts +2 -2
- package/dist/shaders/section-2d-overlay.wgsl.d.ts.map +1 -1
- package/dist/shaders/section-2d-overlay.wgsl.js +2 -2
- package/dist/shaders/section-2d-overlay.wgsl.js.map +1 -1
- package/dist/shaders/shadow.wgsl.d.ts +31 -0
- package/dist/shaders/shadow.wgsl.d.ts.map +1 -0
- package/dist/shaders/shadow.wgsl.js +141 -0
- package/dist/shaders/shadow.wgsl.js.map +1 -0
- package/dist/shadow-light-matrix.d.ts +106 -0
- package/dist/shadow-light-matrix.d.ts.map +1 -0
- package/dist/shadow-light-matrix.js +185 -0
- package/dist/shadow-light-matrix.js.map +1 -0
- package/dist/shadow-occluders.d.ts +106 -0
- package/dist/shadow-occluders.d.ts.map +1 -0
- package/dist/shadow-occluders.js +156 -0
- package/dist/shadow-occluders.js.map +1 -0
- package/dist/shadow-pass.d.ts +132 -0
- package/dist/shadow-pass.d.ts.map +1 -0
- package/dist/shadow-pass.js +342 -0
- package/dist/shadow-pass.js.map +1 -0
- package/dist/snap-detector.d.ts +10 -0
- package/dist/snap-detector.d.ts.map +1 -1
- package/dist/snap-detector.js.map +1 -1
- package/dist/symbolic-overlay-pipelines.d.ts +34 -0
- package/dist/symbolic-overlay-pipelines.d.ts.map +1 -1
- package/dist/symbolic-overlay-pipelines.js +31 -20
- package/dist/symbolic-overlay-pipelines.js.map +1 -1
- package/dist/types.d.ts +45 -19
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
* Creates a `CpuFrameTicker`. The clock read (`performance.now()` in a real
|
|
6
|
+
* caller) happens OUTSIDE this module — `tick(nowMs)` takes the timestamp as
|
|
7
|
+
* a parameter rather than reading a clock itself, so this module has no
|
|
8
|
+
* dependency on real elapsed time and its own tests (see
|
|
9
|
+
* `frame-timing-cpu.test.ts`) can feed synthetic values deterministically.
|
|
10
|
+
*/
|
|
11
|
+
export function createCpuFrameTicker() {
|
|
12
|
+
let lastMs = null;
|
|
13
|
+
const deltas = [];
|
|
14
|
+
return {
|
|
15
|
+
tick(nowMs) {
|
|
16
|
+
if (lastMs !== null)
|
|
17
|
+
deltas.push(nowMs - lastMs);
|
|
18
|
+
lastMs = nowMs;
|
|
19
|
+
},
|
|
20
|
+
deltasMs() {
|
|
21
|
+
return deltas.slice();
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=frame-timing-cpu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame-timing-cpu.js","sourceRoot":"","sources":["../src/frame-timing-cpu.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AA0B/D;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB;IAClC,IAAI,MAAM,GAAkB,IAAI,CAAC;IACjC,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,OAAO;QACL,IAAI,CAAC,KAAa;YAChB,IAAI,MAAM,KAAK,IAAI;gBAAE,MAAM,CAAC,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC;YACjD,MAAM,GAAG,KAAK,CAAC;QACjB,CAAC;QACD,QAAQ;YACN,OAAO,MAAM,CAAC,KAAK,EAAE,CAAC;QACxB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin GPU-facing half of frame/pass timing (issue #2670 perf-verdict gate).
|
|
3
|
+
* Everything with judgement — statistics, unit conversion, mode selection —
|
|
4
|
+
* lives in `frame-timing.ts` / `frame-timing-stats.ts`, which are pure and
|
|
5
|
+
* fully covered by synthetic-value tests. This file is deliberately as small
|
|
6
|
+
* as it can be: it only creates the `GPUQuerySet`, writes `timestampWrites`
|
|
7
|
+
* into pass descriptors, resolves the query set into a readback buffer, and
|
|
8
|
+
* hands the raw nanosecond pairs to the pure aggregator. None of it runs in
|
|
9
|
+
* this environment (`navigator.gpu` is absent here, so there is no test file
|
|
10
|
+
* for this module — a mock `GPUDevice` would only prove the mock is
|
|
11
|
+
* internally consistent, not that the real WebGPU calls are correct) and
|
|
12
|
+
* none of it should be trusted without exercising it on a real
|
|
13
|
+
* `'timestamp-query'`-capable adapter.
|
|
14
|
+
*
|
|
15
|
+
* OPT-IN, NOT WIRED BY DEFAULT: nothing in this codebase constructs a
|
|
16
|
+
* `GpuFrameTimingRecorder` today. A caller enables it explicitly:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* const recorder = GpuFrameTimingRecorder.create(device); // null if unsupported
|
|
20
|
+
* if (recorder) {
|
|
21
|
+
* const pass = encoder.beginRenderPass({
|
|
22
|
+
* ...descriptor,
|
|
23
|
+
* timestampWrites: recorder.beginPass('main'),
|
|
24
|
+
* });
|
|
25
|
+
* // ...draw calls...
|
|
26
|
+
* pass.end();
|
|
27
|
+
* recorder.endFrame(encoder);
|
|
28
|
+
* device.queue.submit([encoder.finish()]);
|
|
29
|
+
* const samples = await recorder.readback(); // PassTimingSample[] | null
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* Measuring every frame changes what you're measuring (query resolution and
|
|
34
|
+
* the readback `mapAsync` are not free), so a caller should sample
|
|
35
|
+
* intermittently (e.g. every Nth frame) rather than every frame in a
|
|
36
|
+
* shipped build — this module does not impose that policy, it only makes
|
|
37
|
+
* one frame's measurement cheap and correct.
|
|
38
|
+
*/
|
|
39
|
+
import type { PassTimingSample } from './frame-timing.js';
|
|
40
|
+
/** 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). */
|
|
41
|
+
export declare function hasTimestampQueryFeature(features: {
|
|
42
|
+
has(name: string): boolean;
|
|
43
|
+
} | null | undefined): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Byte size of the resolve/readback buffers needed for `passCount` passes —
|
|
46
|
+
* 2 timestamp queries (begin + end) per pass, `BYTES_PER_TIMESTAMP` each.
|
|
47
|
+
* Pure arithmetic, decidable without a device: extracted out of `create()`
|
|
48
|
+
* so the sizing formula itself is unit-tested rather than only ever
|
|
49
|
+
* exercised as a side effect of a real `device.createBuffer` call.
|
|
50
|
+
*/
|
|
51
|
+
export declare function queryBufferSizeBytes(passCount: number): number;
|
|
52
|
+
/**
|
|
53
|
+
* Allocates the next pair of query-set write indices for one pass, or
|
|
54
|
+
* `null` if `maxPasses` passes have already been begun this frame. Pure:
|
|
55
|
+
* given the current cursor and the frame's pass budget, the next
|
|
56
|
+
* (begin, end, cursor) triple — or exhaustion — is fully determined; no
|
|
57
|
+
* `GPUQuerySet` is touched to decide it. Extracted out of
|
|
58
|
+
* `GpuFrameTimingRecorder.beginPass` so this index bookkeeping (the part
|
|
59
|
+
* most likely to hide an off-by-one — see the exhaustion boundary test) is
|
|
60
|
+
* checked with synthetic cursor/maxPasses values instead of only ever
|
|
61
|
+
* running inside a live recording session.
|
|
62
|
+
*/
|
|
63
|
+
export declare function allocatePassQueryIndices(nextQueryIndex: number, maxPasses: number): {
|
|
64
|
+
beginningOfPassWriteIndex: number;
|
|
65
|
+
endOfPassWriteIndex: number;
|
|
66
|
+
nextQueryIndex: number;
|
|
67
|
+
} | null;
|
|
68
|
+
/**
|
|
69
|
+
* Pairs each recorded pass `label` (in recording order) with its
|
|
70
|
+
* (start, end) nanosecond timestamps at `timestamps[i*2]` /
|
|
71
|
+
* `timestamps[i*2+1]` — the layout `GpuFrameTimingRecorder` writes via
|
|
72
|
+
* `timestampWrites`. Pure: given a labels array and a `BigInt64Array`, the
|
|
73
|
+
* resulting `PassTimingSample[]` is fully determined; no `GPUBuffer`
|
|
74
|
+
* mapping is involved. Extracted out of `readback()` so this pairing
|
|
75
|
+
* arithmetic — the part that would silently mis-attribute a duration to
|
|
76
|
+
* the wrong label on an off-by-one — is checked directly.
|
|
77
|
+
*
|
|
78
|
+
* `GpuFrameTimingRecorder.readback()` always calls this with
|
|
79
|
+
* `timestamps.length === labels.length * 2` (it sizes the readback slice
|
|
80
|
+
* from the same `nextQueryIndex` cursor that `beginPass` pushed each label
|
|
81
|
+
* against — see `readback()`'s call site), so a short buffer cannot occur
|
|
82
|
+
* on that path today. But this function is exported precisely so it can be
|
|
83
|
+
* exercised standalone, and a caller passing a corrupted or hand-built
|
|
84
|
+
* buffer must not get back a sample whose `startNs`/`endNs` is `undefined`
|
|
85
|
+
* where the type says `bigint`: that silently propagates into
|
|
86
|
+
* `frameTotalMs`/`passDurationsMs` (`frame-timing.ts`), which throws a
|
|
87
|
+
* `TypeError` mixing `BigInt` and `undefined` for a one-short buffer, or
|
|
88
|
+
* silently computes `NaN` for a two-short buffer — two different failure
|
|
89
|
+
* shapes for what is really the same input error, and neither is the
|
|
90
|
+
* "never throws, never fabricates" contract the rest of this module's
|
|
91
|
+
* siblings hold themselves to. A label whose (start, end) pair does not
|
|
92
|
+
* fully fit in `timestamps` is dropped rather than pushed with a missing
|
|
93
|
+
* field.
|
|
94
|
+
*/
|
|
95
|
+
export declare function pairTimestampsWithLabels(labels: readonly string[], timestamps: BigInt64Array): PassTimingSample[];
|
|
96
|
+
/**
|
|
97
|
+
* Records GPU timestamp queries for the passes of one frame and resolves
|
|
98
|
+
* them into `PassTimingSample[]` (nanosecond pairs; see `frame-timing.ts`
|
|
99
|
+
* for what happens to them next). One instance is good for one frame's
|
|
100
|
+
* worth of passes up to `maxPasses`, then must be recreated (or reset via
|
|
101
|
+
* `beginFrame()`) for the next — this keeps the query-set/readback-buffer
|
|
102
|
+
* lifetime unambiguous rather than trying to make it silently reusable
|
|
103
|
+
* across frames while a previous frame's readback might still be pending.
|
|
104
|
+
*/
|
|
105
|
+
export declare class GpuFrameTimingRecorder {
|
|
106
|
+
private readonly maxPasses;
|
|
107
|
+
private readonly querySet;
|
|
108
|
+
private readonly resolveBuffer;
|
|
109
|
+
private readonly readbackBuffer;
|
|
110
|
+
private labels;
|
|
111
|
+
private nextQueryIndex;
|
|
112
|
+
private resolved;
|
|
113
|
+
private constructor();
|
|
114
|
+
/**
|
|
115
|
+
* Returns a recorder, or `null` if the device's adapter did not advertise
|
|
116
|
+
* `'timestamp-query'` — callers must treat `null` as "cannot measure this
|
|
117
|
+
* way" and either fall back to CPU-side timing (`decideTimingMode` in
|
|
118
|
+
* `frame-timing.ts`) or skip measurement, never throw.
|
|
119
|
+
*/
|
|
120
|
+
static create(device: GPUDevice, maxPasses?: number): GpuFrameTimingRecorder | null;
|
|
121
|
+
/**
|
|
122
|
+
* Returns the `timestampWrites` object for the next pass, labelled
|
|
123
|
+
* `label`. Pass it straight into `beginRenderPass`'s descriptor. Returns
|
|
124
|
+
* `null` once `maxPasses` passes have been begun this frame — a caller
|
|
125
|
+
* that hits this should raise `maxPasses` at construction, not retry.
|
|
126
|
+
*/
|
|
127
|
+
beginPass(label: string): GPURenderPassTimestampWrites | null;
|
|
128
|
+
/** Resolves every query written this frame into the readback buffer. Call once, after every pass has been `.end()`-ed, before `queue.submit`. */
|
|
129
|
+
endFrame(encoder: GPUCommandEncoder): void;
|
|
130
|
+
/**
|
|
131
|
+
* Maps the readback buffer and returns this frame's `PassTimingSample[]`,
|
|
132
|
+
* or `null` if `endFrame` was never called (nothing was recorded, or the
|
|
133
|
+
* caller forgot). Async because `mapAsync` is: the caller's queue submit
|
|
134
|
+
* must have completed first for the buffer to contain real data — WebGPU
|
|
135
|
+
* enforces this by making `mapAsync` wait for pending GPU work that
|
|
136
|
+
* touches the buffer.
|
|
137
|
+
*/
|
|
138
|
+
readback(): Promise<PassTimingSample[] | null>;
|
|
139
|
+
/** Resets for the next frame's recording. Does not reallocate the query set or buffers — they are sized once at `create()` and reused. */
|
|
140
|
+
beginFrame(): void;
|
|
141
|
+
/** Releases the GPU query set and buffers. Call when timing is turned off. */
|
|
142
|
+
destroy(): void;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=frame-timing-gpu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame-timing-gpu.d.ts","sourceRoot":"","sources":["../src/frame-timing-gpu.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,iNAAiN;AACjN,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE;IAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE7G;AAID;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,MAAM,GAChB;IAAE,yBAAyB,EAAE,MAAM,CAAC;IAAC,mBAAmB,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAOnG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,UAAU,EAAE,aAAa,GAAG,gBAAgB,EAAE,CAOjH;AAED;;;;;;;;GAQG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAY;IAC1C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAY;IAC3C,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO;IAOP;;;;;OAKG;IACH,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,SAAI,GAAG,sBAAsB,GAAG,IAAI;IAiB9E;;;;;OAKG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,4BAA4B,GAAG,IAAI;IAY7D,iJAAiJ;IACjJ,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;IAO1C;;;;;;;OAOG;IACG,QAAQ,IAAI,OAAO,CAAC,gBAAgB,EAAE,GAAG,IAAI,CAAC;IAUpD,0IAA0I;IAC1I,UAAU,IAAI,IAAI;IAMlB,8EAA8E;IAC9E,OAAO,IAAI,IAAI;CAKhB"}
|
|
@@ -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"}
|