From 61a3a6d78370c7227f98dc48d441be319e940432 Mon Sep 17 00:00:00 2001 From: Rezmason Date: Mon, 8 Nov 2021 02:23:33 -0800 Subject: [PATCH] Moved the WebGPU code off of "std140" and onto gpu-uniforms. --- TODO.txt | 4 +- js/webgpu/imagePass.js | 5 +-- js/webgpu/main.js | 12 ++--- js/webgpu/palettePass.js | 8 ++-- js/webgpu/rainPass.js | 82 +++++++++++------------------------ js/webgpu/resurrectionPass.js | 6 +-- js/webgpu/std140.js | 77 -------------------------------- js/webgpu/stripePass.js | 6 +-- js/webgpu/utils.js | 10 ++--- lib/gpu-uniforms.js | 74 ++++++++++++++----------------- 10 files changed, 82 insertions(+), 202 deletions(-) delete mode 100644 js/webgpu/std140.js diff --git a/TODO.txt b/TODO.txt index 600441d..8da41ac 100644 --- a/TODO.txt +++ b/TODO.txt @@ -10,9 +10,9 @@ WebGPU Try to change post processing to compute shaders once they're easier to support - buffer-stuffer (was "std140") + gpu-uniforms (was "std140") Resolve the memory positions of the fields in the parse layouts - Resolve each layout into a Proxy around an ArrayBuffer + Resolve each layout into a Proxy around an ArrayBuffer and three mapped typedarrays Document and share it diff --git a/js/webgpu/imagePass.js b/js/webgpu/imagePass.js index 64c0484..d7ee681 100644 --- a/js/webgpu/imagePass.js +++ b/js/webgpu/imagePass.js @@ -1,4 +1,4 @@ -import std140 from "./std140.js"; +import uniforms from "/lib/gpu-uniforms.js"; import { loadTexture, loadShader, makeUniformBuffer, makePassFBO, makePass } from "./utils.js"; // Multiplies the rendered rain and bloom by a loaded in image @@ -10,9 +10,6 @@ export default (context, getInputs) => { const { config, adapter, device, canvasContext } = context; const ditherMagnitude = 0.05; - const configLayout = std140(["f32", "vec3"]); - const configBuffer = makeUniformBuffer(device, configLayout, [ditherMagnitude, config.backgroundColor]); - const linearSampler = device.createSampler({ magFilter: "linear", minFilter: "linear", diff --git a/js/webgpu/main.js b/js/webgpu/main.js index 74fccf5..5a20127 100644 --- a/js/webgpu/main.js +++ b/js/webgpu/main.js @@ -1,4 +1,4 @@ -import std140 from "./std140.js"; +import uniforms from "/lib/gpu-uniforms.js"; import { getCanvasSize, makeUniformBuffer, makePipeline } from "./utils.js"; import makeRain from "./rainPass.js"; @@ -38,8 +38,8 @@ export default async (canvas, config) => { GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST, }; - const timeLayout = std140(["f32", "i32"]); - const timeBuffer = makeUniformBuffer(device, timeLayout); + const timeUniforms = uniforms.read(`[[block]] struct Time { seconds : f32; frames : i32; };`).Time; + const timeBuffer = makeUniformBuffer(device, timeUniforms); const context = { config, @@ -54,7 +54,7 @@ export default async (canvas, config) => { await Promise.all(pipeline.map((step) => step.ready)); - let frame = 0; + let frames = 0; let start = NaN; const renderLoop = (now) => { @@ -68,8 +68,8 @@ export default async (canvas, config) => { pipeline.forEach((step) => step.setSize(...canvasSize)); } - device.queue.writeBuffer(timeBuffer, 0, timeLayout.build([(now - start) / 1000, frame])); - frame++; + device.queue.writeBuffer(timeBuffer, 0, timeUniforms.write({ seconds: (now - start) / 1000, frames })); + frames++; const encoder = device.createCommandEncoder(); pipeline.forEach((step) => step.execute(encoder)); diff --git a/js/webgpu/palettePass.js b/js/webgpu/palettePass.js index 64f67f0..3c93e61 100644 --- a/js/webgpu/palettePass.js +++ b/js/webgpu/palettePass.js @@ -1,4 +1,4 @@ -import std140 from "./std140.js"; +import uniforms from "/lib/gpu-uniforms.js"; import { loadShader, makeUniformBuffer, makePassFBO, makePass } from "./utils.js"; // Maps the brightness of the rendered rain and bloom to colors @@ -53,7 +53,7 @@ const makePalette = (device, entries) => { } }); - // TODO: support arrays in std140 + // TODO: try using gpu-uniforms const paletteBuffer = device.createBuffer({ size: (3 + 1) * PALETTE_SIZE * Float32Array.BYTES_PER_ELEMENT, @@ -81,8 +81,8 @@ export default (context, getInputs) => { const { config, adapter, device, canvasContext, timeBuffer } = context; const ditherMagnitude = 0.05; - const configLayout = std140(["f32", "vec3"]); - const configBuffer = makeUniformBuffer(device, configLayout, [ditherMagnitude, config.backgroundColor]); + const configUniforms = uniforms.read(`struct Config { ditherMagnitude : f32; backgroundColor: vec3; };`).Config; + const configBuffer = makeUniformBuffer(device, configUniforms, { ditherMagnitude, backgroundColor: config.backgroundColor }); const paletteBuffer = makePalette(device, config.paletteEntries); diff --git a/js/webgpu/rainPass.js b/js/webgpu/rainPass.js index ac325d6..2c5f43a 100644 --- a/js/webgpu/rainPass.js +++ b/js/webgpu/rainPass.js @@ -1,4 +1,4 @@ -import std140 from "./std140.js"; +import uniforms from "/lib/gpu-uniforms.js"; import { makePassFBO, loadTexture, loadShader, makeUniformBuffer, makePass } from "./utils.js"; const { mat4, vec3 } = glMatrix; @@ -15,58 +15,20 @@ const cycleStyles = { const numVerticesPerQuad = 2 * 3; -const makeConfigBuffer = (device, config, density, gridSize) => { - // Various effect-related values - const rippleType = config.rippleTypeName in rippleTypes ? rippleTypes[config.rippleTypeName] : -1; - const cycleStyle = config.cycleStyleName in cycleStyles ? cycleStyles[config.cycleStyleName] : 0; - const slantVec = [Math.cos(config.slant), Math.sin(config.slant)]; - const slantScale = 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1); - const showComputationTexture = config.effect === "none"; - - const configData = [ - // common - { name: "animationSpeed", type: "f32", value: config.animationSpeed }, - { name: "glyphSequenceLength", type: "i32", value: config.glyphSequenceLength }, - { name: "glyphTextureColumns", type: "i32", value: config.glyphTextureColumns }, - { name: "glyphHeightToWidth", type: "f32", value: config.glyphHeightToWidth }, - { name: "resurrectingCodeRatio", type: "f32", value: config.resurrectingCodeRatio }, - { name: "gridSize", type: "vec2", value: gridSize }, - { name: "showComputationTexture", type: "i32", value: showComputationTexture }, - - // compute - { name: "brightnessThreshold", type: "f32", value: config.brightnessThreshold }, - { name: "brightnessOverride", type: "f32", value: config.brightnessOverride }, - { name: "brightnessDecay", type: "f32", value: config.brightnessDecay }, - { name: "cursorEffectThreshold", type: "f32", value: config.cursorEffectThreshold }, - { name: "cycleSpeed", type: "f32", value: config.cycleSpeed }, - { name: "cycleFrameSkip", type: "i32", value: config.cycleFrameSkip }, - { name: "fallSpeed", type: "f32", value: config.fallSpeed }, - { name: "hasSun", type: "i32", value: config.hasSun }, - { name: "hasThunder", type: "i32", value: config.hasThunder }, - { name: "raindropLength", type: "f32", value: config.raindropLength }, - { name: "rippleScale", type: "f32", value: config.rippleScale }, - { name: "rippleSpeed", type: "f32", value: config.rippleSpeed }, - { name: "rippleThickness", type: "f32", value: config.rippleThickness }, - { name: "cycleStyle", type: "i32", value: cycleStyle }, - { name: "rippleType", type: "i32", value: rippleType }, - - // render - { name: "forwardSpeed", type: "f32", value: config.forwardSpeed }, - { name: "glyphVerticalSpacing", type: "f32", value: config.glyphVerticalSpacing }, - { name: "glyphEdgeCrop", type: "f32", value: config.glyphEdgeCrop }, - { name: "isPolar", type: "i32", value: config.isPolar }, - { name: "density", type: "f32", value: density }, - { name: "slantScale", type: "f32", value: slantScale }, - { name: "slantVec", type: "vec2", value: slantVec }, - { name: "volumetric", type: "i32", value: config.volumetric }, - ]; +const makeConfigBuffer = (device, configUniforms, config, density, gridSize) => { + const configData = { + ...config, + gridSize, + density, + showComputationTexture: config.effect === "none", + cycleStyle: config.cycleStyleName in cycleStyles ? cycleStyles[config.cycleStyleName] : 0, + rippleType: config.rippleTypeName in rippleTypes ? rippleTypes[config.rippleTypeName] : -1, + slantScale: 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1), + slantVec: [Math.cos(config.slant), Math.sin(config.slant)], + }; console.table(configData); - return makeUniformBuffer( - device, - std140(configData.map((field) => field.type)), - configData.map((field) => field.value) - ); + return makeUniformBuffer(device, configUniforms, configData); }; export default (context, getInputs) => { @@ -84,13 +46,10 @@ export default (context, getInputs) => { // rather than a single quad for our geometry const numQuads = config.volumetric ? numCells : 1; - const configBuffer = makeConfigBuffer(device, config, density, gridSize); - - const sceneLayout = std140(["vec2", "mat4x4", "mat4x4"]); - const sceneBuffer = makeUniformBuffer(device, sceneLayout); + // TODO: uniforms should be updated to provide this too const cellsBuffer = device.createBuffer({ - size: numCells * std140(["vec4"]).size, + size: numCells * 4 * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.STORAGE, }); @@ -115,6 +74,9 @@ export default (context, getInputs) => { const presentationFormat = canvasContext.getPreferredFormat(adapter); + let configBuffer; + let sceneUniforms; + let sceneBuffer; let computePipeline; let renderPipeline; let computeBindGroup; @@ -124,6 +86,12 @@ export default (context, getInputs) => { const ready = (async () => { const [msdfTexture, rainShader] = await Promise.all(assets); + const rainShaderUniforms = uniforms.read(rainShader.code); + configBuffer = makeConfigBuffer(device, rainShaderUniforms.Config, config, density, gridSize); + + sceneUniforms = rainShaderUniforms.Scene; + sceneBuffer = makeUniformBuffer(device, sceneUniforms); + computePipeline = device.createComputePipeline({ compute: { module: rainShader.module, @@ -183,7 +151,7 @@ export default (context, getInputs) => { const aspectRatio = width / height; mat4.perspectiveZO(camera, (Math.PI / 180) * 90, aspectRatio, 0.0001, 1000); const screenSize = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1]; - device.queue.writeBuffer(sceneBuffer, 0, sceneLayout.build([screenSize, camera, transform])); + device.queue.writeBuffer(sceneBuffer, 0, sceneUniforms.write({ screenSize, camera, transform })); // Update output?.destroy(); diff --git a/js/webgpu/resurrectionPass.js b/js/webgpu/resurrectionPass.js index a5a6136..b55f895 100644 --- a/js/webgpu/resurrectionPass.js +++ b/js/webgpu/resurrectionPass.js @@ -1,4 +1,4 @@ -import std140 from "./std140.js"; +import uniforms from "/lib/gpu-uniforms.js"; import { loadShader, makeUniformBuffer, makePassFBO, makePass } from "./utils.js"; // Matrix Resurrections isn't in theaters yet, @@ -15,8 +15,8 @@ export default (context, getInputs) => { const { config, adapter, device, canvasContext, timeBuffer } = context; const ditherMagnitude = 0.05; - const configLayout = std140(["f32", "vec3"]); - const configBuffer = makeUniformBuffer(device, configLayout, [ditherMagnitude, config.backgroundColor]); + const configUniforms = uniforms.read(`struct Config { ditherMagnitude : f32; backgroundColor: vec3; };`).Config; + const configBuffer = makeUniformBuffer(device, configUniforms, { ditherMagnitude, backgroundColor: config.backgroundColor }); const linearSampler = device.createSampler({ magFilter: "linear", diff --git a/js/webgpu/std140.js b/js/webgpu/std140.js deleted file mode 100644 index c2fb8f9..0000000 --- a/js/webgpu/std140.js +++ /dev/null @@ -1,77 +0,0 @@ -const supportedTypes = { - ["i32"]: [1, 1, "i32"], - ["u32"]: [1, 1, "u32"], - ["f32"]: [1, 1, "f32"], - - ["atomic"]: [1, 1, "i32"], - ["vec2"]: [2, 2, "i32"], - ["vec3"]: [4, 3, "i32"], - ["vec4"]: [4, 4, "i32"], - - ["atomic"]: [1, 1, "u32"], - ["vec2"]: [2, 2, "u32"], - ["vec3"]: [4, 3, "u32"], - ["vec4"]: [4, 4, "u32"], - - ["atomic"]: [1, 1, "f32"], - ["vec2"]: [2, 2, "f32"], - ["vec3"]: [4, 3, "f32"], - ["vec4"]: [4, 4, "f32"], - - ["mat2x2"]: [2, 4, "f32"], - ["mat3x2"]: [2, 6, "f32"], - ["mat4x2"]: [2, 8, "f32"], - ["mat2x3"]: [4, 8, "f32"], - ["mat3x3"]: [4, 12, "f32"], - ["mat4x3"]: [4, 16, "f32"], - ["mat2x4"]: [4, 8, "f32"], - ["mat3x4"]: [4, 12, "f32"], - ["mat4x4"]: [4, 16, "f32"], -}; - -const computeStructLayout = (types) => { - const fields = []; - let byteOffset = 0; - for (const type of types) { - if (supportedTypes[type] == null) { - throw new Error(`Unsupported type: ${type}`); - } - const [alignAtByte, sizeInBytes, baseType] = supportedTypes[type]; - byteOffset = Math.ceil(byteOffset / alignAtByte) * alignAtByte; - fields.push({ baseType, byteOffset }); - byteOffset += sizeInBytes; - } - - // console.log(types); - // console.log(fields); - - const size = byteOffset * Float32Array.BYTES_PER_ELEMENT; - - return { - fields, - size, - build: (values, buffer = null) => buildStruct(fields, values, buffer ?? new ArrayBuffer(size)), - }; -}; - -const buildStruct = (fields, values, buffer) => { - if (values.length !== fields.length) { - throw new Error(`This struct contains ${fields.length} values, and you supplied ${values.length}.`); - } - - const views = { - i32: new Int32Array(buffer), - u32: new Uint32Array(buffer), - f32: new Float32Array(buffer), - }; - - for (let i = 0; i < values.length; i++) { - const view = views[fields[i].baseType]; - const value = values[i]; - const array = value[Symbol.iterator] == null ? [Number(value)] : value; - view.set(array, fields[i].byteOffset); - } - return buffer; -}; - -export default computeStructLayout; diff --git a/js/webgpu/stripePass.js b/js/webgpu/stripePass.js index 5bfabbe..df39f6f 100644 --- a/js/webgpu/stripePass.js +++ b/js/webgpu/stripePass.js @@ -1,4 +1,4 @@ -import std140 from "./std140.js"; +import uniforms from "/lib/gpu-uniforms.js"; import { loadShader, make1DTexture, makeUniformBuffer, makePassFBO, makePass } from "./utils.js"; // Multiplies the rendered rain and bloom by a 1D gradient texture @@ -41,8 +41,8 @@ export default (context, getInputs) => { const { config, adapter, device, canvasContext, timeBuffer } = context; const ditherMagnitude = 0.05; - const configLayout = std140(["f32", "vec3"]); - const configBuffer = makeUniformBuffer(device, configLayout, [ditherMagnitude, config.backgroundColor]); + const configUniforms = uniforms.read(`struct Config { ditherMagnitude : f32; backgroundColor: vec3; };`).Config; + const configBuffer = makeUniformBuffer(device, configUniforms, { ditherMagnitude, backgroundColor: config.backgroundColor }); // Expand and convert stripe colors into 1D texture data const stripeColors = diff --git a/js/webgpu/utils.js b/js/webgpu/utils.js index 54dd281..0e0eb0e 100644 --- a/js/webgpu/utils.js +++ b/js/webgpu/utils.js @@ -44,14 +44,14 @@ const loadShader = async (device, url) => { }; }; -const makeUniformBuffer = (device, structLayout, values = null) => { +const makeUniformBuffer = (device, uniforms, data = null) => { const buffer = device.createBuffer({ - size: structLayout.size, + size: uniforms.minSize, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - mappedAtCreation: values != null, + mappedAtCreation: data != null, }); - if (values != null) { - structLayout.build(values, buffer.getMappedRange()); + if (data != null) { + uniforms.write(data, buffer.getMappedRange()); buffer.unmap(); } return buffer; diff --git a/lib/gpu-uniforms.js b/lib/gpu-uniforms.js index e1ffef8..7747af6 100644 --- a/lib/gpu-uniforms.js +++ b/lib/gpu-uniforms.js @@ -166,6 +166,10 @@ const parseStructLayoutsFromShader = (wgsl) => { const makeDataForLayout = (structLayouts, layout) => Object.fromEntries(layout.fields.map((field) => [field.identifier, field.defaultValue()])); const writeField = (allLayouts, field, value, views, byteOffset) => { + if (value == null) { + console.warn(`Property missing: ${field.identifier}`); + return; + } if (field.isArray) { const count = field.isFixedSize ? field.mult : value.length; for (let i = 0; i < field.mult; i++) { @@ -182,46 +186,34 @@ const writeField = (allLayouts, field, value, views, byteOffset) => { } }; -export default class Uniforms { - static fromWGSL(wgsl) { +const makeGenerator = (layout, structLayouts) => { + const minSize = layout.sizeInBytes; + return Object.freeze({ + minSize, + create: () => makeDataForLayout(structLayouts, layout), + write: (object, destination) => { + destination ??= new ArrayBuffer(layout.sizeInBytes); // TODO: expand to support runtime-sized arrays, via the length of the array on the data object + + const views = { + i32: new Int32Array(destination), + u32: new Uint32Array(destination), + f32: new Float32Array(destination), + }; + + for (const field of layout.fields) { + writeField(structLayouts, field, object[field.identifier], views, 0); + } + + return destination; + }, + }); +}; + +const api = Object.freeze({ + read: (wgsl) => { const structLayouts = parseStructLayoutsFromShader(wgsl); - return Object.fromEntries(Object.entries(structLayouts).map(([name, layout]) => [name, new Uniforms(layout, structLayouts)])); - } + return Object.fromEntries(Object.entries(structLayouts).map(([name, layout]) => [name, makeGenerator(layout, structLayouts)])); + }, +}); - #structLayouts; - #layout; - data; - minSize; - - constructor(layout, structLayouts = null) { - if (typeof layout === "string") { - structLayouts = parseStructLayoutsFromShader(layout); - layout = Object.values(structLayouts)[0]; - } - - structLayouts ??= {}; - this.#structLayouts = structLayouts; - this.#layout = layout; - this.minSize = layout.sizeInBytes; - } - - object() { - return makeDataForLayout(this.#structLayouts, this.#layout); - } - - stuff(object, destination) { - destination ??= new ArrayBuffer(this.#layout.sizeInBytes); // TODO: expand to support runtime-sized arrays, via the length of the array on the data object - - const views = { - i32: new Int32Array(destination), - u32: new Uint32Array(destination), - f32: new Float32Array(destination), - }; - - for (const field of this.#layout.fields) { - writeField(this.#structLayouts, field, object[field.identifier], views, 0); - } - - return destination; - } -} +export default api;