Skip to content

Commit 1da3240

Browse files
committed
wip2
1 parent 7e641c2 commit 1da3240

File tree

2 files changed

+184
-18
lines changed

2 files changed

+184
-18
lines changed

webgpu/timing-helper.js

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
export class RollingAverage {
2+
#total = 0;
3+
#samples = [];
4+
#cursor = 0;
5+
#numSamples;
6+
constructor(numSamples = 30) {
7+
this.#numSamples = numSamples;
8+
}
9+
addSample(v) {
10+
if (!Number.isNaN(v) && Number.isFinite(v)) {
11+
this.#total += v - (this.#samples[this.#cursor] || 0);
12+
this.#samples[this.#cursor] = v;
13+
this.#cursor = (this.#cursor + 1) % this.#numSamples;
14+
}
15+
}
16+
get() {
17+
return this.#total / this.#samples.length;
18+
}
19+
}
20+
21+
function assert(cond, msg = '') {
22+
if (!cond) {
23+
throw new Error(msg);
24+
}
25+
}
26+
27+
export class TimingHelper {
28+
#canTimestamp;
29+
#device;
30+
#querySet;
31+
#resolveBuffer;
32+
#resultBuffer;
33+
#resultBuffers = [];
34+
// state can be 'free', 'need resolve', 'wait for result'
35+
#state = 'free';
36+
37+
constructor(device) {
38+
this.#device = device;
39+
this.#canTimestamp = device.features.has('timestamp-query');
40+
if (this.#canTimestamp) {
41+
this.#querySet = device.createQuerySet({
42+
type: 'timestamp',
43+
count: 2,
44+
});
45+
this.#resolveBuffer = device.createBuffer({
46+
size: this.#querySet.count * 8,
47+
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
48+
});
49+
}
50+
}
51+
52+
#beginTimestampPass(encoder, fnName, descriptor) {
53+
if (this.#canTimestamp) {
54+
assert(this.#state === 'free', 'state not free');
55+
this.#state = 'need resolve';
56+
57+
const pass = encoder[fnName]({
58+
...descriptor,
59+
...{
60+
timestampWrites: {
61+
querySet: this.#querySet,
62+
beginningOfPassWriteIndex: 0,
63+
endOfPassWriteIndex: 1,
64+
},
65+
},
66+
});
67+
68+
const resolve = () => this.#resolveTiming(encoder);
69+
pass.end = (function(origFn) {
70+
return function() {
71+
origFn.call(this);
72+
resolve();
73+
};
74+
})(pass.end);
75+
76+
return pass;
77+
} else {
78+
return encoder[fnName](descriptor);
79+
}
80+
}
81+
82+
beginRenderPass(encoder, descriptor = {}) {
83+
return this.#beginTimestampPass(encoder, 'beginRenderPass', descriptor);
84+
}
85+
86+
beginComputePass(encoder, descriptor = {}) {
87+
return this.#beginTimestampPass(encoder, 'beginComputePass', descriptor);
88+
}
89+
90+
#resolveTiming(encoder) {
91+
if (!this.#canTimestamp) {
92+
return;
93+
}
94+
assert(this.#state === 'need resolve', 'must call addTimestampToPass');
95+
this.#state = 'wait for result';
96+
97+
this.#resultBuffer = this.#resultBuffers.pop() || this.#device.createBuffer({
98+
size: this.#resolveBuffer.size,
99+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
100+
});
101+
102+
encoder.resolveQuerySet(this.#querySet, 0, this.#querySet.count, this.#resolveBuffer, 0);
103+
encoder.copyBufferToBuffer(this.#resolveBuffer, 0, this.#resultBuffer, 0, this.#resultBuffer.size);
104+
}
105+
106+
async getResult() {
107+
if (!this.#canTimestamp) {
108+
return 0;
109+
}
110+
assert(this.#state === 'wait for result', 'must call resolveTiming');
111+
this.#state = 'free';
112+
113+
const resultBuffer = this.#resultBuffer;
114+
await resultBuffer.mapAsync(GPUMapMode.READ);
115+
const times = new BigInt64Array(resultBuffer.getMappedRange());
116+
const duration = Number(times[1] - times[0]);
117+
resultBuffer.unmap();
118+
this.#resultBuffers.push(resultBuffer);
119+
return duration;
120+
}
121+
}

webgpu/webgpu-optimization-none.html

+63-18
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@
4545
<script type="module">
4646
import GUI from '/3rdparty/muigui-0.x.module.js';
4747
import {mat4, mat3, vec3} from '/3rdparty/wgpu-matrix.module.js';
48+
import {RollingAverage, TimingHelper} from './timing-helper.js';
49+
50+
const fpsAverage = new RollingAverage();
51+
const jsAverage = new RollingAverage();
52+
const gpuAverage = new RollingAverage();
53+
const mathAverage = new RollingAverage();
4854

4955
const cssColorToRGBA8 = (() => {
5056
const canvas = new OffscreenCanvas(1, 1);
@@ -77,12 +83,17 @@
7783

7884
async function main() {
7985
const adapter = await navigator.gpu?.requestAdapter();
80-
const device = await adapter?.requestDevice();
86+
const canTimestamp = adapter.features.has('timestamp-query');
87+
const device = await adapter?.requestDevice({
88+
requiredFeatures: [
89+
...(canTimestamp ? ['timestamp-query'] : []),
90+
],
91+
});
8192
if (!device) {
82-
fail('need a browser that supports WebGPU');
83-
return;
93+
fail('could not init WebGPU');
8494
}
8595

96+
const timingHelper = new TimingHelper(device);
8697
const infoElem = document.querySelector('#info');
8798

8899
// Get a WebGPU context from the canvas and configure it
@@ -266,7 +277,7 @@
266277
minFilter: 'nearest',
267278
});
268279

269-
const maxObjects = 100;
280+
const maxObjects = 10000;
270281
const objectInfos = [];
271282

272283
for (let i = 0; i < maxObjects; ++i) {
@@ -349,6 +360,7 @@
349360
colorAttachments: [
350361
{
351362
// view: <- to be filled out when we render
363+
clearValue: [0.3, 0.3, 0.3, 1],
352364
loadOp: 'clear',
353365
storeOp: 'store',
354366
},
@@ -361,10 +373,11 @@
361373
},
362374
};
363375

376+
const canvasToSizeMap = new WeakMap();
364377
const degToRad = d => d * Math.PI / 180;
365378

366379
const settings = {
367-
numObjects: maxObjects,
380+
numObjects: 1000,
368381
render: true,
369382
};
370383

@@ -384,6 +397,20 @@
384397

385398
const startTimeMs = performance.now();
386399

400+
let width = 1;
401+
let height = 1;
402+
if (settings.render) {
403+
const entry = canvasToSizeMap.get(canvas);
404+
if (entry) {
405+
width = Math.max(1, entry.contentBoxSize[0].inlineSize, device.limits.maxTextureDimension2D);
406+
height = Math.max(1, entry.contentBoxSize[0].blockSize, device.limits.maxTextureDimension2D);
407+
}
408+
}
409+
if (canvas.width !== width || canvas.height !== height) {
410+
canvas.width = width;
411+
canvas.height = height;
412+
}
413+
387414
// Get the current texture from the canvas context and
388415
// set it as the texture to render to.
389416
const canvasTexture = context.getCurrentTexture();
@@ -406,7 +433,7 @@
406433
renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView();
407434

408435
const encoder = device.createCommandEncoder();
409-
const pass = encoder.beginRenderPass(renderPassDescriptor);
436+
const pass = timingHelper.beginRenderPass(encoder, renderPassDescriptor);
410437
pass.setPipeline(pipeline);
411438
pass.setVertexBuffer(0, positionBuffer);
412439
pass.setVertexBuffer(1, normalBuffer);
@@ -431,6 +458,8 @@
431458
// Combine the view and projection matrixes
432459
const viewProjectionMatrix = mat4.multiply(projection, viewMatrix);
433460

461+
let mathElapsedTimeMs = 0;
462+
434463
for (let i = 0; i < settings.numObjects; ++i) {
435464
const {
436465
bindGroup,
@@ -452,14 +481,23 @@
452481
scale,
453482
shininess,
454483
} = objectInfos[i];
484+
const mathTimeStartMs = performance.now();
485+
455486
// Copy the viewProjectionMatrix into the uniform values for this object
456487
viewProjectionValue.set(viewProjectionMatrix);
457488

458489
// Compute a world matrix
490+
// mat4.identity(worldValue);
491+
// mat4.axisRotate(worldValue, axis, time * speed, worldValue);
492+
// mat4.translate(worldValue, [radius, 0, 0], worldValue);
493+
// mat4.rotateY(worldValue, rotationSpeed * time, worldValue);
494+
// mat4.scale(worldValue, [scale, scale, scale], worldValue);
495+
459496
mat4.identity(worldValue);
460-
mat4.axisRotate(worldValue, axis, time * speed, worldValue);
461-
mat4.translate(worldValue, [radius, 0, 0], worldValue);
462-
mat4.rotateY(worldValue, rotationSpeed * time, worldValue);
497+
mat4.axisRotate(worldValue, axis, i + time * speed, worldValue);
498+
mat4.translate(worldValue, [0, 0, Math.sin(i * 3.721 + time * speed) * radius], worldValue);
499+
mat4.translate(worldValue, [0, 0, Math.sin(i * 9.721 + time * 0.1) * radius], worldValue);
500+
mat4.rotateX(worldValue, time * rotationSpeed + i, worldValue);
463501
mat4.scale(worldValue, [scale, scale, scale], worldValue);
464502

465503
// Inverse and transpose it into the worldInverseTranspose value
@@ -470,6 +508,8 @@
470508
viewWorldPositionValue.set(eye);
471509
shininessValue[0] = shininess;
472510

511+
mathElapsedTimeMs += performance.now() - mathTimeStartMs;
512+
473513
// upload the uniform values to the uniform buffer
474514
device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
475515

@@ -482,24 +522,29 @@
482522
const commandBuffer = encoder.finish();
483523
device.queue.submit([commandBuffer]);
484524

525+
timingHelper.getResult().then(gpuTime => {
526+
gpuAverage.addSample(gpuTime / 1000);
527+
});
528+
485529
const elapsedTimeMs = performance.now() - startTimeMs;
530+
fpsAverage.addSample(1 / deltaTime);
531+
jsAverage.addSample(elapsedTimeMs);
532+
mathAverage.addSample(mathElapsedTimeMs);
533+
534+
486535
infoElem.textContent = `\
487-
js : ${elapsedTimeMs.toFixed(0)}ms
488-
fps: ${(1 / deltaTime).toFixed(1)}
536+
js : ${jsAverage.get().toFixed(1)}ms
537+
math: ${mathAverage.get().toFixed(1)}ms
538+
fps : ${fpsAverage.get().toFixed(0)}
539+
gpu : ${canTimestamp ? `${(gpuAverage.get() / 1000).toFixed(1)}ms` : 'N/A'}
489540
`;
490541

491542
requestAnimationFrame(render);
492543
}
493544
requestAnimationFrame(render);
494545

495546
const observer = new ResizeObserver(entries => {
496-
for (const entry of entries) {
497-
const canvas = entry.target;
498-
const width = entry.contentBoxSize[0].inlineSize;
499-
const height = entry.contentBoxSize[0].blockSize;
500-
canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D));
501-
canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D));
502-
}
547+
entries.forEach(e => canvasToSizeMap.set(e.target, e));
503548
});
504549
observer.observe(canvas);
505550
}

0 commit comments

Comments
 (0)