vireglass 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ // GPU timing for `VireGlassRenderer.getLastGpuMs()`, isolated from the renderer so it can be
2
+ // unit-tested against a fake: `EXT_disjoint_timer_query_webgl2` is unavailable in the headless
3
+ // Chromium the shader gates run in, so nothing in this repository otherwise exercises the ring
4
+ // logic at all.
5
+
6
+ /** The two tokens the extension adds; everything else the timer needs is core WebGL2. */
7
+ export type GpuTimerExtension = {
8
+ readonly TIME_ELAPSED_EXT: number;
9
+ readonly GPU_DISJOINT_EXT: number;
10
+ };
11
+
12
+ /** The slice of WebGL2 this module needs, kept narrow so a test can fake it without a real GL
13
+ * context. A `WebGL2RenderingContext` satisfies this structurally — no cast needed at the call
14
+ * site in `renderer.ts`. */
15
+ export type GpuTimerContext = {
16
+ readonly QUERY_RESULT_AVAILABLE: number;
17
+ readonly QUERY_RESULT: number;
18
+ getExtension(name: string): unknown;
19
+ createQuery(): WebGLQuery | null;
20
+ deleteQuery(query: WebGLQuery): void;
21
+ beginQuery(target: number, query: WebGLQuery): void;
22
+ endQuery(target: number): void;
23
+ getQueryParameter(query: WebGLQuery, pname: number): unknown;
24
+ getParameter(pname: number): unknown;
25
+ };
26
+
27
+ /** Queries in flight at once. `EXT_disjoint_timer_query_webgl2` results arrive asynchronously,
28
+ * often a few frames late, so one query per frame would mean starting a new query before the
29
+ * previous one's result is back — undefined by spec. */
30
+ export const GPU_TIMER_RING_SIZE = 3;
31
+
32
+ type TimerSlot = { query: WebGLQuery; pending: boolean };
33
+
34
+ export type GpuTimer = {
35
+ /** Runs `fn` as one timed sample. Always calls the matching `endQuery` even if `fn` throws, so
36
+ * a frame that fails partway through (a broken backdrop pass, a validation error) never leaves
37
+ * a query open for the next call to trip over. */
38
+ time<T>(fn: () => T): T;
39
+ /** `null` without the extension or while the ring hasn't produced a non-disjoint result yet. */
40
+ getLastMs(): number | null;
41
+ destroy(): void;
42
+ };
43
+
44
+ /** Missing the extension (most headless/CI setups, and every browser but Chromium-family in
45
+ * practice) degrades every method here to a no-op / `null` — never an error. */
46
+ export function createGpuTimer(gl: GpuTimerContext): GpuTimer {
47
+ const ext = gl.getExtension('EXT_disjoint_timer_query_webgl2') as GpuTimerExtension | null;
48
+ const slots: TimerSlot[] = ext
49
+ ? Array.from({ length: GPU_TIMER_RING_SIZE }, () => ({
50
+ query: gl.createQuery() as WebGLQuery,
51
+ pending: false,
52
+ }))
53
+ : [];
54
+ let cursor = 0;
55
+ let active: TimerSlot | null = null;
56
+ let lastMs: number | null = null;
57
+
58
+ function poll(): void {
59
+ if (!ext) return;
60
+ for (const slot of slots) {
61
+ if (!slot.pending) continue;
62
+ if (!gl.getQueryParameter(slot.query, gl.QUERY_RESULT_AVAILABLE)) continue;
63
+ slot.pending = false;
64
+ // A disjoint event (e.g. the GPU clock changed) invalidates every query since the last
65
+ // check — the sample is discarded, not reported as zero or stale.
66
+ if (gl.getParameter(ext.GPU_DISJOINT_EXT)) continue;
67
+ const ns = gl.getQueryParameter(slot.query, gl.QUERY_RESULT) as number;
68
+ lastMs = ns / 1e6;
69
+ }
70
+ }
71
+
72
+ function begin(): void {
73
+ active = null;
74
+ if (!ext || slots.length === 0) return;
75
+ poll();
76
+ const slot = slots[cursor];
77
+ // The ring hasn't caught up (this slot's previous result isn't back yet) — skip timing this
78
+ // frame rather than reuse a query that's still in flight.
79
+ if (slot.pending) return;
80
+ gl.beginQuery(ext.TIME_ELAPSED_EXT, slot.query);
81
+ active = slot;
82
+ }
83
+
84
+ function end(): void {
85
+ if (!ext || !active) return;
86
+ gl.endQuery(ext.TIME_ELAPSED_EXT);
87
+ active.pending = true;
88
+ cursor = (cursor + 1) % slots.length;
89
+ active = null;
90
+ }
91
+
92
+ function time<T>(fn: () => T): T {
93
+ begin();
94
+ try {
95
+ return fn();
96
+ } finally {
97
+ end();
98
+ }
99
+ }
100
+
101
+ function getLastMs(): number | null {
102
+ poll();
103
+ return lastMs;
104
+ }
105
+
106
+ function destroy(): void {
107
+ for (const slot of slots) gl.deleteQuery(slot.query);
108
+ }
109
+
110
+ return { time, getLastMs, destroy };
111
+ }
package/src/web/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './renderer';
2
2
  export * from './probe';
3
+ export * from './gl';