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,