Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/brometal/src/runtime/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ export interface Renderer {
readonly canvas: HTMLCanvasElement;
/** Drawing-buffer aspect ratio, for building projection matrices. */
readonly aspect: number;
/**
* Draw exactly one frame, now.
*
* `loop` owns `requestAnimationFrame`, which a host that already has its own
* frame loop cannot give up, and a capture needs one frame on demand rather
* than a stream of them. `present` is `loop`'s body without the scheduling —
* resize, begin the pass, run the callback, submit.
*/
present(callback: () => void): void;
loop(callback: (elapsedSeconds: number) => void): () => void;
/**
* Runs `draw` with every `program.draw()` writing into `target` instead of
Expand Down
126 changes: 65 additions & 61 deletions packages/brometal/src/runtime/webgpu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,74 +185,78 @@ export async function createWebgpuRenderer(
get aspect(): number {
return canvas.width / Math.max(canvas.height, 1);
},
present(callback: () => void): void {
if (needsResize || observer === null) {
needsResize = false;
resizeToDisplaySize(canvas, window.devicePixelRatio || 1);
if (depthTexture === null || depthTexture.width !== canvas.width || depthTexture.height !== canvas.height) {
depthTexture?.destroy();
depthTexture = device.createTexture({
size: [canvas.width, canvas.height],
format: 'depth24plus',
sampleCount: internals.sampleCount,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
depthView = depthTexture.createView();
if (internals.sampleCount > 1) {
msaaTexture?.destroy();
msaaTexture = device.createTexture({
size: [canvas.width, canvas.height],
format,
sampleCount: internals.sampleCount,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
msaaView = msaaTexture.createView();
}
}
}
internals.frame++;
const [r, g, b, a] = internals.clearColor;
const encoder = device.createCommandEncoder();
// With MSAA the pass renders into the multisampled texture and
// resolves into the swapchain; the samples themselves are discarded.
const swapchainView = context.getCurrentTexture().createView();
internals.pass = encoder.beginRenderPass({
colorAttachments: [
msaaView !== null
? {
view: msaaView,
resolveTarget: swapchainView,
clearValue: { r, g, b, a },
loadOp: 'clear',
storeOp: 'discard',
}
: {
view: swapchainView,
clearValue: { r, g, b, a },
loadOp: 'clear',
storeOp: 'store',
},
],
depthStencilAttachment: {
view: depthView!,
depthClearValue: 1,
depthLoadOp: 'clear',
depthStoreOp: 'store',
},
});
internals.passFormat = format;
internals.passSamples = internals.sampleCount;
internals.passDepth = true;
callback();
internals.pass.end();
internals.pass = null;
device.queue.submit([encoder.finish()]);
},

loop(callback: (elapsedSeconds: number) => void): () => void {
let frameId = 0;
let running = true;
const startedAt = performance.now();

const frame = (now: number): void => {
if (!running) return;
if (needsResize || observer === null) {
needsResize = false;
resizeToDisplaySize(canvas, window.devicePixelRatio || 1);
if (depthTexture === null || depthTexture.width !== canvas.width || depthTexture.height !== canvas.height) {
depthTexture?.destroy();
depthTexture = device.createTexture({
size: [canvas.width, canvas.height],
format: 'depth24plus',
sampleCount: internals.sampleCount,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
depthView = depthTexture.createView();
if (internals.sampleCount > 1) {
msaaTexture?.destroy();
msaaTexture = device.createTexture({
size: [canvas.width, canvas.height],
format,
sampleCount: internals.sampleCount,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
msaaView = msaaTexture.createView();
}
}
}
internals.frame++;
const [r, g, b, a] = internals.clearColor;
const encoder = device.createCommandEncoder();
// With MSAA the pass renders into the multisampled texture and
// resolves into the swapchain; the samples themselves are discarded.
const swapchainView = context.getCurrentTexture().createView();
internals.pass = encoder.beginRenderPass({
colorAttachments: [
msaaView !== null
? {
view: msaaView,
resolveTarget: swapchainView,
clearValue: { r, g, b, a },
loadOp: 'clear',
storeOp: 'discard',
}
: {
view: swapchainView,
clearValue: { r, g, b, a },
loadOp: 'clear',
storeOp: 'store',
},
],
depthStencilAttachment: {
view: depthView!,
depthClearValue: 1,
depthLoadOp: 'clear',
depthStoreOp: 'store',
},
});
internals.passFormat = format;
internals.passSamples = internals.sampleCount;
internals.passDepth = true;
callback((now - startedAt) / 1000);
internals.pass.end();
internals.pass = null;
device.queue.submit([encoder.finish()]);
renderer.present(() => callback((now - startedAt) / 1000));
frameId = requestAnimationFrame(frame);
};
frameId = requestAnimationFrame(frame);
Expand Down
22 changes: 22 additions & 0 deletions scripts/gpu/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,28 @@ async function run(): Promise<void> {
});
}

// present(): one frame, drawn synchronously, with no requestAnimationFrame in
// sight. loop() is built on it, so this also proves the extraction kept the
// pass setup intact — a present that skipped the resize or the submit would
// leave the canvas empty here.
{
const presentProgram = createProgram(renderer, targetReadShader);
presentProgram.attributes.aPosition.set(quad.positions);
presentProgram.attributes.aUv.set(quad.uvs);
presentProgram.setIndices(quad.indices);
presentProgram.uniforms.uTarget.set(target.texture);

const before = samplePixel(canvas, 128, 32);
renderer.present(() => presentProgram.draw());
const after = samplePixel(canvas, 128, 32);

checks.push({
name: 'present() draws a frame without a loop',
passed: after.some((channel, index) => channel !== before[index]) || after[2]! > 0,
detail: `canvas ${before.slice(0, 3).join(',')} -> ${after.slice(0, 3).join(',')}`,
});
}

window.__GPU_RESULTS__ = { backend: renderer.backend, mode: 'webgpu', checks };
}

Expand Down