From 1b61e304a57abae76ef09e2d7a6da94828d39c24 Mon Sep 17 00:00:00 2001 From: Rezmason Date: Mon, 15 Nov 2021 00:30:09 -0800 Subject: [PATCH] Refactoring the pass and pipeline, so that inputs and size are handed to and returned from the build function (formerly setSize). This is now the earliest place to build bind groups, which makes sense, because it's also the earliest place to create textures that are proportional to the size of the canvas. --- TODO.txt | 2 - js/webgpu/bloomPass.js | 74 +++++++++++++++++++---------------- js/webgpu/endPass.js | 17 ++++---- js/webgpu/imagePass.js | 37 ++++++++---------- js/webgpu/main.js | 6 +-- js/webgpu/palettePass.js | 37 ++++++++---------- js/webgpu/rainPass.js | 26 ++++++------ js/webgpu/resurrectionPass.js | 39 +++++++++--------- js/webgpu/stripePass.js | 28 +++++++------ js/webgpu/utils.js | 20 +++++----- 10 files changed, 142 insertions(+), 144 deletions(-) diff --git a/TODO.txt b/TODO.txt index 59b4aa4..72f2a23 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,8 +1,6 @@ TODO: WebGPU - Rename setSize to rebuild — it is the function that receives inputs as well as screen size - Create and store the bloom bind groups on resize Make sure everything is properly commented Update links in issues Get rid of end pass once it's possible to copy a bgra8unorm to a canvas texture diff --git a/js/webgpu/bloomPass.js b/js/webgpu/bloomPass.js index 683959e..8f55b18 100644 --- a/js/webgpu/bloomPass.js +++ b/js/webgpu/bloomPass.js @@ -1,7 +1,7 @@ import { structs } from "/lib/gpu-buffer.js"; import { makeComputeTarget, makePyramidView, loadShader, makeUniformBuffer, makeBindGroup, makePass } from "./utils.js"; -export default (context, getInputs) => { +export default (context) => { const { config, device } = context; const pyramidHeight = 4; @@ -14,8 +14,7 @@ export default (context, getInputs) => { // If there's no bloom to apply, return a no-op pass with an empty bloom texture if (!enabled) { const emptyTexture = makeComputeTarget(device, 1, 1); - const getOutputs = () => ({ ...getInputs(), bloom: emptyTexture }); - return makePass(getOutputs); + return makePass(null, (size, inputs) => ({ ...inputs, bloom: emptyTexture })); } const assets = [loadShader(device, "shaders/wgsl/bloomBlur.wgsl"), loadShader(device, "shaders/wgsl/bloomCombine.wgsl")]; @@ -26,21 +25,19 @@ export default (context, getInputs) => { }); let blurPipeline; - let combinePipeline; let hBlurPyramid; let vBlurPyramid; let hBlurBuffer; let vBlurBuffer; + let hBlurBindGroups; + let vBlurBindGroups; + let combinePipeline; let combineBuffer; + let combineBindGroup; let output; let scaledScreenSize; - const getOutputs = () => ({ - primary: getInputs().primary, - bloom: output, - }); - - const ready = (async () => { + const loaded = (async () => { const [blurShader, combineShader] = await Promise.all(assets); blurPipeline = device.createComputePipeline({ @@ -65,47 +62,58 @@ export default (context, getInputs) => { combineBuffer = makeUniformBuffer(device, combineUniforms, { bloomStrength, pyramidHeight }); })(); - const setSize = (width, height) => { + const build = (screenSize, inputs) => { + scaledScreenSize = screenSize.map((x) => Math.floor(x * bloomSize)); + hBlurPyramid?.destroy(); - hBlurPyramid = makeComputeTarget(device, Math.floor(width * bloomSize), Math.floor(height * bloomSize), pyramidHeight); + hBlurPyramid = makeComputeTarget(device, scaledScreenSize, pyramidHeight); vBlurPyramid?.destroy(); - vBlurPyramid = makeComputeTarget(device, Math.floor(width * bloomSize), Math.floor(height * bloomSize), pyramidHeight); + vBlurPyramid = makeComputeTarget(device, scaledScreenSize, pyramidHeight); output?.destroy(); - output = makeComputeTarget(device, Math.floor(width * bloomSize), Math.floor(height * bloomSize)); - scaledScreenSize = [Math.floor(width * bloomSize), Math.floor(height * bloomSize)]; + output = makeComputeTarget(device, scaledScreenSize); + + const hBlurPyramidViews = []; + const vBlurPyramidViews = []; + hBlurBindGroups = []; + vBlurBindGroups = []; + + for (let i = 0; i < pyramidHeight; i++) { + hBlurPyramidViews[i] = makePyramidView(hBlurPyramid, i); + vBlurPyramidViews[i] = makePyramidView(vBlurPyramid, i); + const srcView = i === 0 ? inputs.highPass.createView() : hBlurPyramidViews[i - 1]; + hBlurBindGroups[i] = makeBindGroup(device, blurPipeline, 0, [hBlurBuffer, linearSampler, srcView, hBlurPyramidViews[i]]); + vBlurBindGroups[i] = makeBindGroup(device, blurPipeline, 0, [vBlurBuffer, linearSampler, hBlurPyramidViews[i], vBlurPyramidViews[i]]); + } + + combineBindGroup = makeBindGroup(device, combinePipeline, 0, [combineBuffer, linearSampler, vBlurPyramid.createView(), output.createView()]); + + return { + ...inputs, + bloom: output, + }; }; - const execute = (encoder) => { - const inputs = getInputs(); - const tex = inputs.primary; - + const run = (encoder) => { const computePass = encoder.beginComputePass(); computePass.setPipeline(blurPipeline); - const hBlurPyramidViews = Array(pyramidHeight) - .fill() - .map((_, level) => makePyramidView(hBlurPyramid, level)); - const vBlurPyramidViews = Array(pyramidHeight) - .fill() - .map((_, level) => makePyramidView(vBlurPyramid, level)); for (let i = 0; i < pyramidHeight; i++) { const downsample = 2 ** -i; - const size = [Math.ceil(Math.floor(scaledScreenSize[0] * downsample) / 32), Math.floor(Math.floor(scaledScreenSize[1] * downsample)), 1]; - const srcView = i === 0 ? tex.createView() : hBlurPyramidViews[i - 1]; - computePass.setBindGroup(0, makeBindGroup(device, blurPipeline, 0, [hBlurBuffer, linearSampler, srcView, hBlurPyramidViews[i]])); - computePass.dispatch(...size); - computePass.setBindGroup(0, makeBindGroup(device, blurPipeline, 0, [vBlurBuffer, linearSampler, hBlurPyramidViews[i], vBlurPyramidViews[i]])); - computePass.dispatch(...size); + const dispatchSize = [Math.ceil(Math.floor(scaledScreenSize[0] * downsample) / 32), Math.floor(Math.floor(scaledScreenSize[1] * downsample)), 1]; + computePass.setBindGroup(0, hBlurBindGroups[i]); + computePass.dispatch(...dispatchSize); + computePass.setBindGroup(0, vBlurBindGroups[i]); + computePass.dispatch(...dispatchSize); } computePass.setPipeline(combinePipeline); - computePass.setBindGroup(0, makeBindGroup(device, combinePipeline, 0, [combineBuffer, linearSampler, vBlurPyramid.createView(), output.createView()])); + computePass.setBindGroup(0, combineBindGroup); computePass.dispatch(Math.ceil(scaledScreenSize[0] / 32), scaledScreenSize[1], 1); computePass.endPass(); }; - return makePass(getOutputs, ready, setSize, execute); + return makePass(loaded, build, run); }; diff --git a/js/webgpu/endPass.js b/js/webgpu/endPass.js index ad62d8d..525bd30 100644 --- a/js/webgpu/endPass.js +++ b/js/webgpu/endPass.js @@ -2,7 +2,7 @@ import { loadShader, makeBindGroup, makePass } from "./utils.js"; const numVerticesPerQuad = 2 * 3; -export default (context, getInputs) => { +export default (context) => { const { config, device, canvasFormat, canvasContext } = context; const linearSampler = device.createSampler({ @@ -21,10 +21,11 @@ export default (context, getInputs) => { }; let renderPipeline; + let renderBindGroup; const assets = [loadShader(device, "shaders/wgsl/endPass.wgsl")]; - const ready = (async () => { + const loaded = (async () => { const [imageShader] = await Promise.all(assets); renderPipeline = device.createRenderPipeline({ @@ -44,10 +45,12 @@ export default (context, getInputs) => { }); })(); - const execute = (encoder) => { - const inputs = getInputs(); - const tex = inputs.primary; - const renderBindGroup = makeBindGroup(device, renderPipeline, 0, [linearSampler, tex.createView()]); + const build = (size, inputs) => { + renderBindGroup = makeBindGroup(device, renderPipeline, 0, [linearSampler, inputs.primary.createView()]); + return null; + }; + + const run = (encoder) => { renderPassConfig.colorAttachments[0].view = canvasContext.getCurrentTexture().createView(); const renderPass = encoder.beginRenderPass(renderPassConfig); renderPass.setPipeline(renderPipeline); @@ -56,5 +59,5 @@ export default (context, getInputs) => { renderPass.endPass(); }; - return makePass(null, ready, null, execute); + return makePass(loaded, build, run); }; diff --git a/js/webgpu/imagePass.js b/js/webgpu/imagePass.js index 7ea908d..5997cb2 100644 --- a/js/webgpu/imagePass.js +++ b/js/webgpu/imagePass.js @@ -4,7 +4,7 @@ import { makeComputeTarget, loadTexture, loadShader, makeBindGroup, makePass } f const defaultBGURL = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Flammarion_Colored.jpg/917px-Flammarion_Colored.jpg"; -export default (context, getInputs) => { +export default (context) => { const { config, device } = context; const bgURL = "bgURL" in config ? config.bgURL : defaultBGURL; @@ -19,12 +19,9 @@ export default (context, getInputs) => { let output; let screenSize; let backgroundTex; + let computeBindGroup; - const getOutputs = () => ({ - primary: output, - }); - - const ready = (async () => { + const loaded = (async () => { const [bgTex, imageShader] = await Promise.all(assets); backgroundTex = bgTex; @@ -37,29 +34,27 @@ export default (context, getInputs) => { }); })(); - const setSize = (width, height) => { + const build = (size, inputs) => { output?.destroy(); - output = makeComputeTarget(device, width, height); - screenSize = [width, height]; - }; - - const execute = (encoder) => { - const inputs = getInputs(); - const tex = inputs.primary; - const bloomTex = inputs.bloom; - const computePass = encoder.beginComputePass(); - computePass.setPipeline(computePipeline); - const computeBindGroup = makeBindGroup(device, computePipeline, 0, [ + output = makeComputeTarget(device, size); + screenSize = size; + computeBindGroup = makeBindGroup(device, computePipeline, 0, [ linearSampler, - tex.createView(), - bloomTex.createView(), + inputs.primary.createView(), + inputs.bloom.createView(), backgroundTex.createView(), output.createView(), ]); + return { primary: output }; + }; + + const run = (encoder) => { + const computePass = encoder.beginComputePass(); + computePass.setPipeline(computePipeline); computePass.setBindGroup(0, computeBindGroup); computePass.dispatch(Math.ceil(screenSize[0] / 32), screenSize[1], 1); computePass.endPass(); }; - return makePass(getOutputs, ready, setSize, execute); + return makePass(loaded, build, run); }; diff --git a/js/webgpu/main.js b/js/webgpu/main.js index e919698..0ef660b 100644 --- a/js/webgpu/main.js +++ b/js/webgpu/main.js @@ -54,7 +54,7 @@ export default async (canvas, config) => { const effectName = config.effect in effects ? config.effect : "plain"; const pipeline = makePipeline(context, [makeRain, makeBloomPass, effects[effectName], makeEndPass]); - await Promise.all(pipeline.map((step) => step.ready)); + await Promise.all(pipeline.map((step) => step.loaded)); let frames = 0; let start = NaN; @@ -67,14 +67,14 @@ export default async (canvas, config) => { if (canvasSize[0] !== canvasConfig.size[0] || canvasSize[1] !== canvasConfig.size[1]) { canvasConfig.size = canvasSize; canvasContext.configure(canvasConfig); - pipeline.forEach((step) => step.setSize(...canvasSize)); + pipeline.reduce((outputs, step) => step.build(canvasSize, outputs), null); } device.queue.writeBuffer(timeBuffer, 0, timeUniforms.toBuffer({ seconds: (now - start) / 1000, frames })); frames++; const encoder = device.createCommandEncoder(); - pipeline.forEach((step) => step.execute(encoder)); + pipeline.forEach((step) => step.run(encoder)); // Eventually, when WebGPU allows it, we'll remove the endPass and just copy from our pipeline's output to the canvas texture. // encoder.copyTextureToTexture({ texture: pipeline[pipeline.length - 1].getOutputs().primary }, { texture: canvasContext.getCurrentTexture() }, canvasSize); device.queue.submit([encoder.finish()]); diff --git a/js/webgpu/palettePass.js b/js/webgpu/palettePass.js index 1df4c97..42f8327 100644 --- a/js/webgpu/palettePass.js +++ b/js/webgpu/palettePass.js @@ -75,7 +75,7 @@ const makePalette = (device, paletteUniforms, entries) => { // won't persist across subsequent frames. This is a safe trick // in screen space. -export default (context, getInputs) => { +export default (context) => { const { config, device, timeBuffer } = context; const linearSampler = device.createSampler({ @@ -86,16 +86,13 @@ export default (context, getInputs) => { let computePipeline; let configBuffer; let paletteBuffer; + let computeBindGroup; let output; let screenSize; - const getOutputs = () => ({ - primary: output, - }); - const assets = [loadShader(device, "shaders/wgsl/palettePass.wgsl")]; - const ready = (async () => { + const loaded = (async () => { const [paletteShader] = await Promise.all(assets); computePipeline = device.createComputePipeline({ @@ -113,31 +110,29 @@ export default (context, getInputs) => { paletteBuffer = makePalette(device, paletteUniforms, config.paletteEntries); })(); - const setSize = (width, height) => { + const build = (size, inputs) => { output?.destroy(); - output = makeComputeTarget(device, width, height); - screenSize = [width, height]; - }; - - const execute = (encoder) => { - const inputs = getInputs(); - const tex = inputs.primary; - const bloomTex = inputs.bloom; - const computePass = encoder.beginComputePass(); - computePass.setPipeline(computePipeline); - const computeBindGroup = makeBindGroup(device, computePipeline, 0, [ + output = makeComputeTarget(device, size); + screenSize = size; + computeBindGroup = makeBindGroup(device, computePipeline, 0, [ configBuffer, paletteBuffer, timeBuffer, linearSampler, - tex.createView(), - bloomTex.createView(), + inputs.primary.createView(), + inputs.bloom.createView(), output.createView(), ]); + return { primary: output }; + }; + + const run = (encoder) => { + const computePass = encoder.beginComputePass(); + computePass.setPipeline(computePipeline); computePass.setBindGroup(0, computeBindGroup); computePass.dispatch(Math.ceil(screenSize[0] / 32), screenSize[1], 1); computePass.endPass(); }; - return makePass(getOutputs, ready, setSize, execute); + return makePass(loaded, build, run); }; diff --git a/js/webgpu/rainPass.js b/js/webgpu/rainPass.js index fe991b8..3ff12ea 100644 --- a/js/webgpu/rainPass.js +++ b/js/webgpu/rainPass.js @@ -31,7 +31,7 @@ const makeConfigBuffer = (device, configUniforms, config, density, gridSize) => return makeUniformBuffer(device, configUniforms, configData); }; -export default (context, getInputs) => { +export default (context) => { const { config, device, timeBuffer, canvasFormat } = context; const assets = [loadTexture(device, config.glyphTexURL), loadShader(device, "shaders/wgsl/rainPass.wgsl")]; @@ -92,12 +92,7 @@ export default (context, getInputs) => { let output; let highPassOutput; - const getOutputs = () => ({ - primary: output, - highPass: highPassOutput, - }); - - const ready = (async () => { + const loaded = (async () => { const [msdfTexture, rainShader] = await Promise.all(assets); const rainShaderUniforms = structs.from(rainShader.code); @@ -150,9 +145,9 @@ export default (context, getInputs) => { renderBindGroup = makeBindGroup(device, renderPipeline, 0, [configBuffer, timeBuffer, sceneBuffer, linearSampler, msdfTexture.createView(), cellsBuffer]); })(); - const setSize = (width, height) => { + const build = (size) => { // Update scene buffer: camera and transform math for the volumetric mode - const aspectRatio = width / height; + const aspectRatio = size[0] / size[1]; if (config.effect === "none") { if (aspectRatio > 1) { mat4.orthoZO(camera, -1.5 * aspectRatio, 1.5 * aspectRatio, -1.5, 1.5, -1000, 1000); @@ -167,13 +162,18 @@ export default (context, getInputs) => { // Update output?.destroy(); - output = makeRenderTarget(device, width, height, canvasFormat); + output = makeRenderTarget(device, size, canvasFormat); highPassOutput?.destroy(); - highPassOutput = makeRenderTarget(device, width, height, canvasFormat); + highPassOutput = makeRenderTarget(device, size, canvasFormat); + + return { + primary: output, + highPass: highPassOutput, + }; }; - const execute = (encoder) => { + const run = (encoder) => { // We render the code into an Target using MSDFs: https://github.com/Chlumsky/msdfgen const computePass = encoder.beginComputePass(); @@ -191,5 +191,5 @@ export default (context, getInputs) => { renderPass.endPass(); }; - return makePass(getOutputs, ready, setSize, execute); + return makePass(loaded, build, run); }; diff --git a/js/webgpu/resurrectionPass.js b/js/webgpu/resurrectionPass.js index cca5604..ff25ad6 100644 --- a/js/webgpu/resurrectionPass.js +++ b/js/webgpu/resurrectionPass.js @@ -11,7 +11,7 @@ import { loadShader, makeUniformBuffer, makeComputeTarget, makeBindGroup, makePa const numVerticesPerQuad = 2 * 3; -export default (context, getInputs) => { +export default (context) => { const { config, device, timeBuffer } = context; const linearSampler = device.createSampler({ @@ -21,12 +21,13 @@ export default (context, getInputs) => { let computePipeline; let configBuffer; + let computeBindGroup; let output; let screenSize; const assets = [loadShader(device, "shaders/wgsl/resurrectionPass.wgsl")]; - const ready = (async () => { + const loaded = (async () => { const [resurrectionShader] = await Promise.all(assets); computePipeline = device.createComputePipeline({ @@ -40,34 +41,32 @@ export default (context, getInputs) => { configBuffer = makeUniformBuffer(device, configUniforms, { ditherMagnitude: 0.05, backgroundColor: config.backgroundColor }); })(); - const setSize = (width, height) => { + const build = (size, inputs) => { output?.destroy(); - output = makeComputeTarget(device, width, height); - screenSize = [width, height]; - }; + output = makeComputeTarget(device, size); + screenSize = size; - const getOutputs = () => ({ - primary: output, - }); - - const execute = (encoder) => { - const inputs = getInputs(); - const tex = inputs.primary; - const bloomTex = inputs.bloom; - const computePass = encoder.beginComputePass(); - computePass.setPipeline(computePipeline); - const computeBindGroup = makeBindGroup(device, computePipeline, 0, [ + computeBindGroup = makeBindGroup(device, computePipeline, 0, [ configBuffer, timeBuffer, linearSampler, - tex.createView(), - bloomTex.createView(), + inputs.primary.createView(), + inputs.bloom.createView(), output.createView(), ]); + + return { + primary: output, + }; + }; + + const run = (encoder) => { + const computePass = encoder.beginComputePass(); + computePass.setPipeline(computePipeline); computePass.setBindGroup(0, computeBindGroup); computePass.dispatch(Math.ceil(screenSize[0] / 32), screenSize[1], 1); computePass.endPass(); }; - return makePass(getOutputs, ready, setSize, execute); + return makePass(loaded, build, run); }; diff --git a/js/webgpu/stripePass.js b/js/webgpu/stripePass.js index 2c54b00..5f112d8 100644 --- a/js/webgpu/stripePass.js +++ b/js/webgpu/stripePass.js @@ -57,12 +57,14 @@ export default (context, getInputs) => { let computePipeline; let configBuffer; + let tex; + let bloomTex; let output; let screenSize; const assets = [loadShader(device, "shaders/wgsl/stripePass.wgsl")]; - const ready = (async () => { + const loaded = (async () => { const [stripeShader] = await Promise.all(assets); computePipeline = device.createComputePipeline({ @@ -76,20 +78,20 @@ export default (context, getInputs) => { configBuffer = makeUniformBuffer(device, configUniforms, { ditherMagnitude: 0.05, backgroundColor: config.backgroundColor }); })(); - const setSize = (width, height) => { + const build = (size, inputs) => { output?.destroy(); - output = makeComputeTarget(device, width, height); - screenSize = [width, height]; + output = makeComputeTarget(device, size); + screenSize = size; + + tex = inputs.primary; + bloomTex = inputs.bloom; + + return { + primary: output, + }; }; - const getOutputs = () => ({ - primary: output, - }); - - const execute = (encoder) => { - const inputs = getInputs(); - const tex = inputs.primary; - const bloomTex = inputs.bloom; + const run = (encoder) => { const computePass = encoder.beginComputePass(); computePass.setPipeline(computePipeline); const computeBindGroup = makeBindGroup(device, computePipeline, 0, [ @@ -106,5 +108,5 @@ export default (context, getInputs) => { computePass.endPass(); }; - return makePass(getOutputs, ready, setSize, execute); + return makePass(loaded, build, run); }; diff --git a/js/webgpu/utils.js b/js/webgpu/utils.js index d72e263..8fff9dc 100644 --- a/js/webgpu/utils.js +++ b/js/webgpu/utils.js @@ -27,17 +27,17 @@ const loadTexture = async (device, url) => { return texture; }; -const makeRenderTarget = (device, width, height, format, mipLevelCount = 1) => +const makeRenderTarget = (device, size, format, mipLevelCount = 1) => device.createTexture({ - size: [width, height, 1], + size: [...size, 1], mipLevelCount, format, usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, }); -const makeComputeTarget = (device, width, height, mipLevelCount = 1) => +const makeComputeTarget = (device, size, mipLevelCount = 1) => device.createTexture({ - size: [width, height, 1], + size: [...size, 1], mipLevelCount, format: "rgba8unorm", usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING, @@ -96,15 +96,13 @@ const makeBindGroup = (device, pipeline, index, entries) => })), }); -const makePass = (getOutputs, ready, setSize, execute) => ({ - getOutputs: getOutputs ?? (() => ({})), - ready: ready ?? Promise.resolve(), - setSize: setSize ?? (() => {}), - execute: execute ?? (() => {}), +const makePass = (loaded, build, run) => ({ + loaded: loaded ?? Promise.resolve(), + build: build ?? ((size, inputs) => inputs), + run: run ?? (() => {}), }); -const makePipeline = (context, steps) => - steps.filter((f) => f != null).reduce((pipeline, f, i) => [...pipeline, f(context, i == 0 ? null : pipeline[i - 1].getOutputs)], []); +const makePipeline = (context, steps) => steps.filter((f) => f != null).map((f) => f(context)); export { getCanvasSize,