@aethervtt/shard-gpu-webgl2 0.1.0 → 0.2.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/dist/commands.d.ts +8 -1
- package/dist/commands.js +30 -5
- package/dist/copies.js +8 -0
- package/dist/device.d.ts +9 -1
- package/dist/device.js +19 -2
- package/dist/fake-gl.d.ts +22 -0
- package/dist/fake-gl.js +31 -0
- package/dist/gpu.js +5 -3
- package/dist/queries.d.ts +41 -0
- package/dist/queries.js +110 -0
- package/dist/replay.d.ts +2 -0
- package/dist/replay.js +21 -0
- package/dist/resources.d.ts +3 -0
- package/dist/resources.js +2 -0
- package/package.json +3 -3
package/dist/commands.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Webgl2BindGroup } from './binding.js';
|
|
2
2
|
import type { Webgl2RenderPipeline } from './pipeline.js';
|
|
3
|
+
import type { Webgl2QuerySet } from './queries.js';
|
|
3
4
|
import type { Webgl2Buffer, Webgl2TextureView } from './resources.js';
|
|
4
5
|
export declare const OP: {
|
|
5
6
|
readonly BEGIN_PASS: 1;
|
|
@@ -19,6 +20,9 @@ export declare const OP: {
|
|
|
19
20
|
readonly COPY_TEXTURE_TO_BUFFER: 15;
|
|
20
21
|
readonly COPY_TEXTURE: 16;
|
|
21
22
|
readonly CLEAR_BUFFER: 17;
|
|
23
|
+
readonly BEGIN_QUERY: 18;
|
|
24
|
+
readonly END_QUERY: 19;
|
|
25
|
+
readonly RESOLVE_QUERIES: 20;
|
|
22
26
|
};
|
|
23
27
|
export declare class CommandStream {
|
|
24
28
|
nums: Float64Array<ArrayBuffer>;
|
|
@@ -41,6 +45,8 @@ export declare class Webgl2CommandBuffer {
|
|
|
41
45
|
type Extent = GPUExtent3D;
|
|
42
46
|
export declare class Webgl2CommandEncoder {
|
|
43
47
|
label: string;
|
|
48
|
+
/** @internal The pass being recorded times itself (`timestampWrites`): its end ends the query. */
|
|
49
|
+
timedPass: boolean;
|
|
44
50
|
private stream;
|
|
45
51
|
private readonly pass;
|
|
46
52
|
private readonly buffer;
|
|
@@ -56,7 +62,8 @@ export declare class Webgl2CommandEncoder {
|
|
|
56
62
|
copyTextureToBuffer(source: GPUTexelCopyTextureInfo, destination: GPUTexelCopyBufferInfo, size: Extent): void;
|
|
57
63
|
copyTextureToTexture(source: GPUTexelCopyTextureInfo, destination: GPUTexelCopyTextureInfo, size: Extent): void;
|
|
58
64
|
clearBuffer(buffer: Webgl2Buffer, offset?: number, size?: number): void;
|
|
59
|
-
|
|
65
|
+
/** Pairs of the set to the buffer: written when the buffer is mapped (queries.ts). */
|
|
66
|
+
resolveQuerySet(querySet: Webgl2QuerySet, firstQuery: number, queryCount: number, destination: Webgl2Buffer, destinationOffset: number): void;
|
|
60
67
|
writeTimestamp(): never;
|
|
61
68
|
pushDebugGroup(): void;
|
|
62
69
|
popDebugGroup(): void;
|
package/dist/commands.js
CHANGED
|
@@ -16,7 +16,10 @@ const OP = {
|
|
|
16
16
|
COPY_BUFFER_TO_TEXTURE: 14,
|
|
17
17
|
COPY_TEXTURE_TO_BUFFER: 15,
|
|
18
18
|
COPY_TEXTURE: 16,
|
|
19
|
-
CLEAR_BUFFER: 17
|
|
19
|
+
CLEAR_BUFFER: 17,
|
|
20
|
+
BEGIN_QUERY: 18,
|
|
21
|
+
END_QUERY: 19,
|
|
22
|
+
RESOLVE_QUERIES: 20
|
|
20
23
|
};
|
|
21
24
|
class CommandStream {
|
|
22
25
|
nums = new Float64Array(1024);
|
|
@@ -70,6 +73,8 @@ function colorOf(c, i) {
|
|
|
70
73
|
}
|
|
71
74
|
class Webgl2CommandEncoder {
|
|
72
75
|
label = "";
|
|
76
|
+
/** @internal The pass being recorded times itself (`timestampWrites`): its end ends the query. */
|
|
77
|
+
timedPass = false;
|
|
73
78
|
stream;
|
|
74
79
|
pass;
|
|
75
80
|
buffer;
|
|
@@ -92,6 +97,14 @@ class Webgl2CommandEncoder {
|
|
|
92
97
|
beginRenderPass(d) {
|
|
93
98
|
const s = this.recording;
|
|
94
99
|
if (d.occlusionQuerySet) throw unsupported(`run occlusion queries ("${d.label ?? this.label}")`);
|
|
100
|
+
const writes = d.timestampWrites;
|
|
101
|
+
const index = writes?.beginningOfPassWriteIndex ?? writes?.endOfPassWriteIndex;
|
|
102
|
+
this.timedPass = writes !== void 0 && index !== void 0;
|
|
103
|
+
if (this.timedPass) {
|
|
104
|
+
s.num(OP.BEGIN_QUERY);
|
|
105
|
+
s.num(index >> 1);
|
|
106
|
+
s.ref(writes.querySet);
|
|
107
|
+
}
|
|
95
108
|
s.num(OP.BEGIN_PASS);
|
|
96
109
|
const colors = d.colorAttachments;
|
|
97
110
|
s.num(colors.length);
|
|
@@ -211,11 +224,18 @@ class Webgl2CommandEncoder {
|
|
|
211
224
|
s.num(size ?? buffer.size - offset);
|
|
212
225
|
s.ref(buffer);
|
|
213
226
|
}
|
|
214
|
-
|
|
215
|
-
|
|
227
|
+
/** Pairs of the set to the buffer: written when the buffer is mapped (queries.ts). */
|
|
228
|
+
resolveQuerySet(querySet, firstQuery, queryCount, destination, destinationOffset) {
|
|
229
|
+
const s = this.recording;
|
|
230
|
+
s.num(OP.RESOLVE_QUERIES);
|
|
231
|
+
s.num(firstQuery);
|
|
232
|
+
s.num(queryCount);
|
|
233
|
+
s.num(destinationOffset);
|
|
234
|
+
s.ref(querySet);
|
|
235
|
+
s.ref(destination);
|
|
216
236
|
}
|
|
217
237
|
writeTimestamp() {
|
|
218
|
-
throw unsupported("write timestamps", "
|
|
238
|
+
throw unsupported("write timestamps", "WebGL2 times passes (timestampWrites) only.");
|
|
219
239
|
}
|
|
220
240
|
pushDebugGroup() {
|
|
221
241
|
}
|
|
@@ -345,7 +365,12 @@ class Webgl2RenderPassEncoder {
|
|
|
345
365
|
insertDebugMarker() {
|
|
346
366
|
}
|
|
347
367
|
end() {
|
|
348
|
-
this.encoder.recording
|
|
368
|
+
const s = this.encoder.recording;
|
|
369
|
+
s.num(OP.END_PASS);
|
|
370
|
+
if (this.encoder.timedPass) {
|
|
371
|
+
s.num(OP.END_QUERY);
|
|
372
|
+
this.encoder.timedPass = false;
|
|
373
|
+
}
|
|
349
374
|
}
|
|
350
375
|
}
|
|
351
376
|
function layerOf(view, depthSlice) {
|
package/dist/copies.js
CHANGED
|
@@ -232,6 +232,14 @@ class Copier {
|
|
|
232
232
|
gl.bindBuffer(GL.COPY_READ_BUFFER, src.gl);
|
|
233
233
|
gl.bindBuffer(GL.COPY_WRITE_BUFFER, dst.gl);
|
|
234
234
|
gl.copyBufferSubData(GL.COPY_READ_BUFFER, GL.COPY_WRITE_BUFFER, srcOffset, dstOffset, size);
|
|
235
|
+
if (src.queryReads.length > 0) {
|
|
236
|
+
for (let i = src.queryReads.length - 1; i >= 0; i--) {
|
|
237
|
+
const read = src.queryReads[i];
|
|
238
|
+
if (read.offset < srcOffset || read.offset >= srcOffset + size) continue;
|
|
239
|
+
src.queryReads.splice(i, 1);
|
|
240
|
+
dst.queryReads.push({ offset: read.offset - srcOffset + dstOffset, queries: read.queries });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
235
243
|
if (dst.shadow) {
|
|
236
244
|
if (src.shadow) dst.shadow.set(src.shadow.subarray(srcOffset, srcOffset + size), dstOffset);
|
|
237
245
|
else gl.getBufferSubData(GL.COPY_WRITE_BUFFER, dstOffset, dst.shadow, dstOffset, size);
|
package/dist/device.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Copier } from './copies.js';
|
|
|
5
5
|
import { ShimError } from './errors.js';
|
|
6
6
|
import { FramebufferCache } from './fbo.js';
|
|
7
7
|
import { Webgl2RenderPipeline, Webgl2ShaderModule } from './pipeline.js';
|
|
8
|
+
import { QueryPool, Webgl2QuerySet } from './queries.js';
|
|
8
9
|
import { Webgl2Queue } from './queue.js';
|
|
9
10
|
import { Replayer } from './replay.js';
|
|
10
11
|
import { Webgl2Buffer, Webgl2Sampler, Webgl2Texture } from './resources.js';
|
|
@@ -71,6 +72,8 @@ export interface Webgl2DeviceOptions {
|
|
|
71
72
|
/** Extensions the device calls into, enabled by the adapter. */
|
|
72
73
|
export interface Webgl2Extensions {
|
|
73
74
|
clipControl: unknown;
|
|
75
|
+
/** EXT_disjoint_timer_query_webgl2: TIME_ELAPSED queries time passes (0074). */
|
|
76
|
+
timer: unknown;
|
|
74
77
|
indexed: OES_draw_buffers_indexed | null;
|
|
75
78
|
lose: WEBGL_lose_context | null;
|
|
76
79
|
}
|
|
@@ -85,6 +88,10 @@ export declare class Webgl2Device extends EventTarget {
|
|
|
85
88
|
readonly state: GlState;
|
|
86
89
|
readonly fbos: FramebufferCache;
|
|
87
90
|
readonly copier: Copier;
|
|
91
|
+
/** Query objects for timed passes (`timestamp-query`, 0074). */
|
|
92
|
+
readonly queries: QueryPool;
|
|
93
|
+
/** Where the next timed pass's stamps start: one timeline across frames, as GPU clocks are. */
|
|
94
|
+
timerAt: bigint;
|
|
88
95
|
readonly replayer: Replayer;
|
|
89
96
|
readonly queue: Webgl2Queue;
|
|
90
97
|
readonly features: ReadonlySet<string>;
|
|
@@ -122,7 +129,8 @@ export declare class Webgl2Device extends EventTarget {
|
|
|
122
129
|
createRenderPipelineAsync(descriptor: GPURenderPipelineDescriptor): Promise<Webgl2RenderPipeline>;
|
|
123
130
|
createComputePipeline(descriptor: GPUComputePipelineDescriptor): never;
|
|
124
131
|
createComputePipelineAsync(descriptor: GPUComputePipelineDescriptor): Promise<never>;
|
|
125
|
-
|
|
132
|
+
/** Timestamp query sets where the timer extension exists (see queries.ts). */
|
|
133
|
+
createQuerySet(descriptor: GPUQuerySetDescriptor): Webgl2QuerySet;
|
|
126
134
|
createRenderBundleEncoder(): never;
|
|
127
135
|
importExternalTexture(): never;
|
|
128
136
|
createCommandEncoder(descriptor?: GPUCommandEncoderDescriptor): Webgl2CommandEncoder;
|
package/dist/device.js
CHANGED
|
@@ -7,6 +7,7 @@ import { ShimError, unsupported } from "./errors.js";
|
|
|
7
7
|
import { FramebufferCache } from "./fbo.js";
|
|
8
8
|
import { CLIP, GL } from "./gl.js";
|
|
9
9
|
import { Webgl2RenderPipeline, Webgl2ShaderModule } from "./pipeline.js";
|
|
10
|
+
import { finishQueryReads, QueryPool, Webgl2QuerySet } from "./queries.js";
|
|
10
11
|
import { Webgl2Queue } from "./queue.js";
|
|
11
12
|
import { Replayer } from "./replay.js";
|
|
12
13
|
import { Webgl2Buffer, Webgl2Sampler, Webgl2Texture } from "./resources.js";
|
|
@@ -19,6 +20,10 @@ class Webgl2Device extends EventTarget {
|
|
|
19
20
|
state;
|
|
20
21
|
fbos;
|
|
21
22
|
copier;
|
|
23
|
+
/** Query objects for timed passes (`timestamp-query`, 0074). */
|
|
24
|
+
queries;
|
|
25
|
+
/** Where the next timed pass's stamps start: one timeline across frames, as GPU clocks are. */
|
|
26
|
+
timerAt = 1n;
|
|
22
27
|
replayer;
|
|
23
28
|
queue;
|
|
24
29
|
features;
|
|
@@ -62,6 +67,7 @@ class Webgl2Device extends EventTarget {
|
|
|
62
67
|
this.fbos.incomplete = (status) => this.raise("validation", `A framebuffer is incomplete (0x${status.toString(16)})`);
|
|
63
68
|
}
|
|
64
69
|
this.copier = new Copier(this);
|
|
70
|
+
this.queries = new QueryPool(gl);
|
|
65
71
|
this.replayer = new Replayer(this);
|
|
66
72
|
this.queue = new Webgl2Queue(this);
|
|
67
73
|
gl.pixelStorei(GL.UNPACK_ALIGNMENT, 1);
|
|
@@ -150,8 +156,12 @@ class Webgl2Device extends EventTarget {
|
|
|
150
156
|
)
|
|
151
157
|
);
|
|
152
158
|
}
|
|
159
|
+
/** Timestamp query sets where the timer extension exists (see queries.ts). */
|
|
153
160
|
createQuerySet(descriptor) {
|
|
154
|
-
|
|
161
|
+
if (descriptor.type !== "timestamp" || !this.features.has("timestamp-query")) {
|
|
162
|
+
throw unsupported(`make a ${descriptor.type} query set`);
|
|
163
|
+
}
|
|
164
|
+
return new Webgl2QuerySet(this, descriptor);
|
|
155
165
|
}
|
|
156
166
|
createRenderBundleEncoder() {
|
|
157
167
|
throw unsupported("record render bundles");
|
|
@@ -199,6 +209,7 @@ class Webgl2Device extends EventTarget {
|
|
|
199
209
|
lose(reason, message) {
|
|
200
210
|
if (this.isLost) return;
|
|
201
211
|
this.isLost = true;
|
|
212
|
+
this.queries.clear();
|
|
202
213
|
this.canvas?.removeEventListener("webglcontextlost", this.onContextLost);
|
|
203
214
|
for (const f of this.fences.splice(0)) f.resolve();
|
|
204
215
|
this.resolveLost({ reason, message });
|
|
@@ -282,7 +293,7 @@ class Webgl2Device extends EventTarget {
|
|
|
282
293
|
return Promise.reject(error);
|
|
283
294
|
}
|
|
284
295
|
buffer.mapState = "pending";
|
|
285
|
-
return this.afterGpu().then(() => {
|
|
296
|
+
return this.afterGpu().then(async () => {
|
|
286
297
|
if (buffer.destroyed || buffer.mapState !== "pending") {
|
|
287
298
|
const error = new Error(`Mapping "${buffer.label}" was aborted`);
|
|
288
299
|
error.name = "AbortError";
|
|
@@ -304,6 +315,12 @@ class Webgl2Device extends EventTarget {
|
|
|
304
315
|
}
|
|
305
316
|
buffer.reads.length = 0;
|
|
306
317
|
}
|
|
318
|
+
if (buffer.queryReads.length > 0) await finishQueryReads(this, buffer, bytes, offset);
|
|
319
|
+
if (buffer.destroyed || buffer.mapState !== "pending") {
|
|
320
|
+
const error = new Error(`Mapping "${buffer.label}" was aborted`);
|
|
321
|
+
error.name = "AbortError";
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
307
324
|
buffer.mapped = bytes.buffer;
|
|
308
325
|
buffer.mappedOffset = offset;
|
|
309
326
|
buffer.mapState = "mapped";
|
package/dist/fake-gl.d.ts
CHANGED
|
@@ -116,6 +116,14 @@ export interface FakeGlOptions {
|
|
|
116
116
|
linkPolls?: number;
|
|
117
117
|
/** UNIFORM_BLOCK_DATA_SIZE by block name prefix (default 0: whatever is bound). */
|
|
118
118
|
blockSizes?: Record<string, number>;
|
|
119
|
+
/**
|
|
120
|
+
* Offers EXT_disjoint_timer_query_webgl2 (0074): every TIME_ELAPSED query takes `elapsedNs`, and
|
|
121
|
+
* its result is available after `polls` reads of QUERY_RESULT_AVAILABLE (default 0).
|
|
122
|
+
*/
|
|
123
|
+
timerQuery?: {
|
|
124
|
+
elapsedNs: number;
|
|
125
|
+
polls?: number;
|
|
126
|
+
};
|
|
119
127
|
}
|
|
120
128
|
export declare class FakeGl {
|
|
121
129
|
readonly canvas: {
|
|
@@ -309,6 +317,20 @@ export declare class FakeGl {
|
|
|
309
317
|
drawArraysInstanced(mode: number, first: number, count: number, instances: number): void;
|
|
310
318
|
drawElementsInstanced(mode: number, count: number, type: number, offset: number, instances: number): void;
|
|
311
319
|
private record;
|
|
320
|
+
/** Queries begun so far, and the one running. */
|
|
321
|
+
queriesBegun: number;
|
|
322
|
+
private activeQuery;
|
|
323
|
+
createQuery(): object;
|
|
324
|
+
deleteQuery(): void;
|
|
325
|
+
beginQuery(_target: number, query: {
|
|
326
|
+
polls: number;
|
|
327
|
+
ended: boolean;
|
|
328
|
+
}): void;
|
|
329
|
+
endQuery(): void;
|
|
330
|
+
getQueryParameter(query: {
|
|
331
|
+
polls: number;
|
|
332
|
+
ended: boolean;
|
|
333
|
+
}, p: number): number | boolean;
|
|
312
334
|
fenceSync(): object;
|
|
313
335
|
getSyncParameter(): number;
|
|
314
336
|
deleteSync(): void;
|
package/dist/fake-gl.js
CHANGED
|
@@ -155,6 +155,7 @@ class FakeGl {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
};
|
|
158
|
+
if (options.timerQuery) all.EXT_disjoint_timer_query_webgl2 = {};
|
|
158
159
|
for (const [name, ext] of Object.entries(all)) {
|
|
159
160
|
if (!options.without?.includes(name)) this.extensions.set(name, ext);
|
|
160
161
|
}
|
|
@@ -893,6 +894,36 @@ class FakeGl {
|
|
|
893
894
|
});
|
|
894
895
|
}
|
|
895
896
|
// --- sync -----------------------------------------------------------------------------
|
|
897
|
+
// --- timer queries (EXT_disjoint_timer_query_webgl2) --------------------------------
|
|
898
|
+
/** Queries begun so far, and the one running. */
|
|
899
|
+
queriesBegun = 0;
|
|
900
|
+
activeQuery = null;
|
|
901
|
+
createQuery() {
|
|
902
|
+
return { kind: "query", id: this.next++, polls: 0, ended: false };
|
|
903
|
+
}
|
|
904
|
+
deleteQuery() {
|
|
905
|
+
}
|
|
906
|
+
beginQuery(_target, query) {
|
|
907
|
+
query.polls = this.options.timerQuery?.polls ?? 0;
|
|
908
|
+
query.ended = false;
|
|
909
|
+
this.activeQuery = query;
|
|
910
|
+
this.queriesBegun++;
|
|
911
|
+
}
|
|
912
|
+
endQuery() {
|
|
913
|
+
if (this.activeQuery) this.activeQuery.ended = true;
|
|
914
|
+
this.activeQuery = null;
|
|
915
|
+
}
|
|
916
|
+
getQueryParameter(query, p) {
|
|
917
|
+
if (p === 34919) {
|
|
918
|
+
if (!query.ended) return false;
|
|
919
|
+
if (query.polls > 0) {
|
|
920
|
+
query.polls--;
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
return this.options.timerQuery?.elapsedNs ?? 0;
|
|
926
|
+
}
|
|
896
927
|
fenceSync() {
|
|
897
928
|
return { kind: "sync", id: this.next++ };
|
|
898
929
|
}
|
package/dist/gpu.js
CHANGED
|
@@ -70,7 +70,7 @@ class Webgl2Adapter {
|
|
|
70
70
|
const { caps, extensions } = capsOf(opened.gl, options.profile ?? "native");
|
|
71
71
|
this.caps = caps;
|
|
72
72
|
this.extensions = extensions;
|
|
73
|
-
this.features = featuresOf(opened.gl, caps);
|
|
73
|
+
this.features = featuresOf(opened.gl, caps, extensions.timer !== null);
|
|
74
74
|
this.limits = limitsOf(caps);
|
|
75
75
|
this.info = infoOf(opened.gl);
|
|
76
76
|
}
|
|
@@ -113,6 +113,7 @@ function capsOf(gl, profile) {
|
|
|
113
113
|
const floatLinear = ext("OES_texture_float_linear");
|
|
114
114
|
const indexed = ext("OES_draw_buffers_indexed");
|
|
115
115
|
const lose = gl.getExtension("WEBGL_lose_context");
|
|
116
|
+
const timer = ext("EXT_disjoint_timer_query_webgl2");
|
|
116
117
|
const bc = ext("WEBGL_compressed_texture_s3tc") !== null && ext("WEBGL_compressed_texture_s3tc_srgb") !== null && ext("EXT_texture_compression_rgtc") !== null && ext("EXT_texture_compression_bptc") !== null;
|
|
117
118
|
const caps = {
|
|
118
119
|
maxSamples: minimum ? Math.min(4, p(GL.MAX_SAMPLES)) : p(GL.MAX_SAMPLES),
|
|
@@ -141,10 +142,11 @@ function capsOf(gl, profile) {
|
|
|
141
142
|
etc2: ext("WEBGL_compressed_texture_etc") !== null
|
|
142
143
|
}
|
|
143
144
|
};
|
|
144
|
-
return { caps, extensions: { clipControl, indexed, lose } };
|
|
145
|
+
return { caps, extensions: { clipControl, indexed, lose, timer } };
|
|
145
146
|
}
|
|
146
|
-
function featuresOf(gl, caps) {
|
|
147
|
+
function featuresOf(gl, caps, timer) {
|
|
147
148
|
const features = /* @__PURE__ */ new Set(["depth32float-stencil8"]);
|
|
149
|
+
if (timer) features.add("timestamp-query");
|
|
148
150
|
if (caps.compression.bc) features.add("texture-compression-bc");
|
|
149
151
|
if (caps.compression.astc) features.add("texture-compression-astc");
|
|
150
152
|
if (caps.compression.etc2) features.add("texture-compression-etc2");
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Webgl2Device } from './device.js';
|
|
2
|
+
import type { Webgl2Buffer } from './resources.js';
|
|
3
|
+
export declare const TIME_ELAPSED_EXT = 35007;
|
|
4
|
+
export declare const GPU_DISJOINT_EXT = 36795;
|
|
5
|
+
/** Queries resolved into a buffer, waiting for results: pair k's begin/end at `offset + 16k`. */
|
|
6
|
+
export interface QueryRead {
|
|
7
|
+
offset: number;
|
|
8
|
+
queries: (WebGLQuery | null)[];
|
|
9
|
+
}
|
|
10
|
+
export declare class Webgl2QuerySet {
|
|
11
|
+
readonly type: GPUQueryType;
|
|
12
|
+
readonly count: number;
|
|
13
|
+
label: string;
|
|
14
|
+
/** The query recording each pair this frame (one per pass), until resolved. */
|
|
15
|
+
private readonly pairs;
|
|
16
|
+
private readonly device;
|
|
17
|
+
private destroyed;
|
|
18
|
+
constructor(device: Webgl2Device, descriptor: GPUQuerySetDescriptor);
|
|
19
|
+
/** @internal Replay: starts timing pair `pair` (a pass with timestamp writes begins). */
|
|
20
|
+
begin(pair: number): void;
|
|
21
|
+
/** @internal Replay: the timed pass ended. */
|
|
22
|
+
end(): void;
|
|
23
|
+
/** @internal Replay: hands queries `first` to `first + count` to `buffer` at `offset`. */
|
|
24
|
+
resolve(first: number, count: number, buffer: Webgl2Buffer, offset: number): void;
|
|
25
|
+
destroy(): void;
|
|
26
|
+
}
|
|
27
|
+
/** Query objects, reused: a frame times a few dozen passes, every frame. */
|
|
28
|
+
export declare class QueryPool {
|
|
29
|
+
private readonly free;
|
|
30
|
+
private readonly gl;
|
|
31
|
+
constructor(gl: WebGL2RenderingContext);
|
|
32
|
+
take(): WebGLQuery | null;
|
|
33
|
+
give(query: WebGLQuery): void;
|
|
34
|
+
/** Forgets the pool (the context was lost: its queries went with it). */
|
|
35
|
+
clear(): void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Waits for `buffer`'s pending query results and writes them into `bytes` (the mapped range, which
|
|
39
|
+
* starts at `mapOffset`), then returns the queries to the pool.
|
|
40
|
+
*/
|
|
41
|
+
export declare function finishQueryReads(device: Webgl2Device, buffer: Webgl2Buffer, bytes: Uint8Array, mapOffset: number): Promise<void>;
|
package/dist/queries.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const TIME_ELAPSED_EXT = 35007;
|
|
2
|
+
const GPU_DISJOINT_EXT = 36795;
|
|
3
|
+
const QUERY_RESULT = 34918;
|
|
4
|
+
const QUERY_RESULT_AVAILABLE = 34919;
|
|
5
|
+
const WAIT_MS = 2e3;
|
|
6
|
+
class Webgl2QuerySet {
|
|
7
|
+
type = "timestamp";
|
|
8
|
+
count;
|
|
9
|
+
label;
|
|
10
|
+
/** The query recording each pair this frame (one per pass), until resolved. */
|
|
11
|
+
pairs;
|
|
12
|
+
device;
|
|
13
|
+
destroyed = false;
|
|
14
|
+
constructor(device, descriptor) {
|
|
15
|
+
this.device = device;
|
|
16
|
+
this.count = descriptor.count;
|
|
17
|
+
this.label = descriptor.label ?? "";
|
|
18
|
+
this.pairs = new Array(Math.ceil(descriptor.count / 2)).fill(null);
|
|
19
|
+
}
|
|
20
|
+
/** @internal Replay: starts timing pair `pair` (a pass with timestamp writes begins). */
|
|
21
|
+
begin(pair) {
|
|
22
|
+
if (this.destroyed) return;
|
|
23
|
+
const query = this.device.queries.take();
|
|
24
|
+
if (!query) return;
|
|
25
|
+
this.device.gl.beginQuery(TIME_ELAPSED_EXT, query);
|
|
26
|
+
this.pairs[pair] = query;
|
|
27
|
+
}
|
|
28
|
+
/** @internal Replay: the timed pass ended. */
|
|
29
|
+
end() {
|
|
30
|
+
this.device.gl.endQuery(TIME_ELAPSED_EXT);
|
|
31
|
+
}
|
|
32
|
+
/** @internal Replay: hands queries `first` to `first + count` to `buffer` at `offset`. */
|
|
33
|
+
resolve(first, count, buffer, offset) {
|
|
34
|
+
const from = first >> 1;
|
|
35
|
+
const to = first + count + 1 >> 1;
|
|
36
|
+
const queries = [];
|
|
37
|
+
for (let p = from; p < to; p++) {
|
|
38
|
+
queries.push(this.pairs[p] ?? null);
|
|
39
|
+
this.pairs[p] = null;
|
|
40
|
+
}
|
|
41
|
+
buffer.queryReads.push({ offset, queries });
|
|
42
|
+
}
|
|
43
|
+
destroy() {
|
|
44
|
+
this.destroyed = true;
|
|
45
|
+
for (let p = 0; p < this.pairs.length; p++) {
|
|
46
|
+
const q = this.pairs[p];
|
|
47
|
+
if (q) this.device.queries.give(q);
|
|
48
|
+
this.pairs[p] = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
class QueryPool {
|
|
53
|
+
free = [];
|
|
54
|
+
gl;
|
|
55
|
+
constructor(gl) {
|
|
56
|
+
this.gl = gl;
|
|
57
|
+
}
|
|
58
|
+
take() {
|
|
59
|
+
return this.free.pop() ?? this.gl.createQuery();
|
|
60
|
+
}
|
|
61
|
+
give(query) {
|
|
62
|
+
this.free.push(query);
|
|
63
|
+
}
|
|
64
|
+
/** Forgets the pool (the context was lost: its queries went with it). */
|
|
65
|
+
clear() {
|
|
66
|
+
this.free.length = 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const waitTick = () => new Promise((resolve) => setTimeout(resolve, 4));
|
|
70
|
+
async function finishQueryReads(device, buffer, bytes, mapOffset) {
|
|
71
|
+
const reads = buffer.queryReads.splice(0);
|
|
72
|
+
if (reads.length === 0) return;
|
|
73
|
+
const gl = device.gl;
|
|
74
|
+
const deadline = performance.now() + WAIT_MS;
|
|
75
|
+
const ready = () => reads.every(
|
|
76
|
+
(r) => r.queries.every((q) => !q || gl.getQueryParameter(q, QUERY_RESULT_AVAILABLE) === true)
|
|
77
|
+
);
|
|
78
|
+
while (!device.isLost && !ready() && performance.now() < deadline) await waitTick();
|
|
79
|
+
const lost = device.isLost;
|
|
80
|
+
const disjoint = !lost && gl.getParameter(GPU_DISJOINT_EXT) === true;
|
|
81
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
82
|
+
for (const read of reads) {
|
|
83
|
+
let at = device.timerAt;
|
|
84
|
+
for (let k = 0; k < read.queries.length; k++) {
|
|
85
|
+
const q = read.queries[k];
|
|
86
|
+
const pos = read.offset - mapOffset + k * 16;
|
|
87
|
+
let begin = 0n;
|
|
88
|
+
let end = 0n;
|
|
89
|
+
if (q && !lost && !disjoint && gl.getQueryParameter(q, QUERY_RESULT_AVAILABLE) === true) {
|
|
90
|
+
const elapsed = BigInt(gl.getQueryParameter(q, QUERY_RESULT));
|
|
91
|
+
begin = at;
|
|
92
|
+
end = at + elapsed;
|
|
93
|
+
at = end;
|
|
94
|
+
device.timerAt = end;
|
|
95
|
+
}
|
|
96
|
+
if (pos >= 0 && pos + 16 <= bytes.byteLength) {
|
|
97
|
+
view.setBigUint64(pos, begin, true);
|
|
98
|
+
view.setBigUint64(pos + 8, end, true);
|
|
99
|
+
}
|
|
100
|
+
if (q && !lost) device.queries.give(q);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export {
|
|
105
|
+
GPU_DISJOINT_EXT,
|
|
106
|
+
QueryPool,
|
|
107
|
+
TIME_ELAPSED_EXT,
|
|
108
|
+
Webgl2QuerySet,
|
|
109
|
+
finishQueryReads
|
|
110
|
+
};
|
package/dist/replay.d.ts
CHANGED
package/dist/replay.js
CHANGED
|
@@ -12,6 +12,8 @@ class Replayer {
|
|
|
12
12
|
stream;
|
|
13
13
|
i = 0;
|
|
14
14
|
r = 0;
|
|
15
|
+
/** The query set timing the pass being replayed. */
|
|
16
|
+
timed;
|
|
15
17
|
// The pass.
|
|
16
18
|
passFbo = null;
|
|
17
19
|
passWidth = 0;
|
|
@@ -245,6 +247,25 @@ class Replayer {
|
|
|
245
247
|
this.device.copier.clearBuffer(this.ref(), offset, size);
|
|
246
248
|
break;
|
|
247
249
|
}
|
|
250
|
+
case OP.BEGIN_QUERY: {
|
|
251
|
+
const pair = this.num();
|
|
252
|
+
const set = this.ref();
|
|
253
|
+
set.begin(pair);
|
|
254
|
+
this.timed = set;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case OP.END_QUERY:
|
|
258
|
+
this.timed?.end();
|
|
259
|
+
this.timed = void 0;
|
|
260
|
+
break;
|
|
261
|
+
case OP.RESOLVE_QUERIES: {
|
|
262
|
+
const first = this.num();
|
|
263
|
+
const count = this.num();
|
|
264
|
+
const offset = this.num();
|
|
265
|
+
const set = this.ref();
|
|
266
|
+
set.resolve(first, count, this.ref(), offset);
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
248
269
|
default:
|
|
249
270
|
throw new Error(`Unknown recorded command ${op}`);
|
|
250
271
|
}
|
package/dist/resources.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReadJob } from './copies.js';
|
|
2
2
|
import type { Webgl2Device } from './device.js';
|
|
3
3
|
import { type GlFormat } from './formats.js';
|
|
4
|
+
import type { QueryRead } from './queries.js';
|
|
4
5
|
export declare class Webgl2Buffer {
|
|
5
6
|
readonly id: number;
|
|
6
7
|
readonly size: number;
|
|
@@ -28,6 +29,8 @@ export declare class Webgl2Buffer {
|
|
|
28
29
|
mappedOffset: number;
|
|
29
30
|
/** Texture reads into this buffer, repacked when it's mapped. */
|
|
30
31
|
readonly reads: ReadJob[];
|
|
32
|
+
/** Timer queries resolved into this buffer, written when it's mapped (0074). */
|
|
33
|
+
readonly queryReads: QueryRead[];
|
|
31
34
|
destroyed: boolean;
|
|
32
35
|
private readonly device;
|
|
33
36
|
constructor(device: Webgl2Device, descriptor: GPUBufferDescriptor);
|
package/dist/resources.js
CHANGED
|
@@ -30,6 +30,8 @@ class Webgl2Buffer {
|
|
|
30
30
|
mappedOffset = 0;
|
|
31
31
|
/** Texture reads into this buffer, repacked when it's mapped. */
|
|
32
32
|
reads = [];
|
|
33
|
+
/** Timer queries resolved into this buffer, written when it's mapped (0074). */
|
|
34
|
+
queryReads = [];
|
|
33
35
|
destroyed = false;
|
|
34
36
|
device;
|
|
35
37
|
constructor(device, descriptor) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aethervtt/shard-gpu-webgl2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"README.md"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@aethervtt/shard-core": "0.
|
|
24
|
+
"@aethervtt/shard-core": "0.2.0"
|
|
25
25
|
},
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
@@ -33,6 +33,6 @@
|
|
|
33
33
|
"provenance": true
|
|
34
34
|
},
|
|
35
35
|
"shard": {
|
|
36
|
-
"buildSha": "
|
|
36
|
+
"buildSha": "32c22a3944ed23f5d1baa90bf7cc9fae724f5fd8"
|
|
37
37
|
}
|
|
38
38
|
}
|