Moved the WebGPU code off of "std140" and onto gpu-uniforms.

This commit is contained in:
Rezmason
2021-11-08 02:23:33 -08:00
parent 8f226be368
commit 61a3a6d783
10 changed files with 82 additions and 202 deletions

View File

@@ -10,9 +10,9 @@ WebGPU
Try to change post processing to compute shaders once they're easier to support 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 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 Document and share it

View File

@@ -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"; import { loadTexture, loadShader, makeUniformBuffer, makePassFBO, makePass } from "./utils.js";
// Multiplies the rendered rain and bloom by a loaded in image // 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 { config, adapter, device, canvasContext } = context;
const ditherMagnitude = 0.05; const ditherMagnitude = 0.05;
const configLayout = std140(["f32", "vec3<f32>"]);
const configBuffer = makeUniformBuffer(device, configLayout, [ditherMagnitude, config.backgroundColor]);
const linearSampler = device.createSampler({ const linearSampler = device.createSampler({
magFilter: "linear", magFilter: "linear",
minFilter: "linear", minFilter: "linear",

View File

@@ -1,4 +1,4 @@
import std140 from "./std140.js"; import uniforms from "/lib/gpu-uniforms.js";
import { getCanvasSize, makeUniformBuffer, makePipeline } from "./utils.js"; import { getCanvasSize, makeUniformBuffer, makePipeline } from "./utils.js";
import makeRain from "./rainPass.js"; import makeRain from "./rainPass.js";
@@ -38,8 +38,8 @@ export default async (canvas, config) => {
GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST, GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST,
}; };
const timeLayout = std140(["f32", "i32"]); const timeUniforms = uniforms.read(`[[block]] struct Time { seconds : f32; frames : i32; };`).Time;
const timeBuffer = makeUniformBuffer(device, timeLayout); const timeBuffer = makeUniformBuffer(device, timeUniforms);
const context = { const context = {
config, config,
@@ -54,7 +54,7 @@ export default async (canvas, config) => {
await Promise.all(pipeline.map((step) => step.ready)); await Promise.all(pipeline.map((step) => step.ready));
let frame = 0; let frames = 0;
let start = NaN; let start = NaN;
const renderLoop = (now) => { const renderLoop = (now) => {
@@ -68,8 +68,8 @@ export default async (canvas, config) => {
pipeline.forEach((step) => step.setSize(...canvasSize)); pipeline.forEach((step) => step.setSize(...canvasSize));
} }
device.queue.writeBuffer(timeBuffer, 0, timeLayout.build([(now - start) / 1000, frame])); device.queue.writeBuffer(timeBuffer, 0, timeUniforms.write({ seconds: (now - start) / 1000, frames }));
frame++; frames++;
const encoder = device.createCommandEncoder(); const encoder = device.createCommandEncoder();
pipeline.forEach((step) => step.execute(encoder)); pipeline.forEach((step) => step.execute(encoder));

View File

@@ -1,4 +1,4 @@
import std140 from "./std140.js"; import uniforms from "/lib/gpu-uniforms.js";
import { loadShader, makeUniformBuffer, makePassFBO, makePass } from "./utils.js"; import { loadShader, makeUniformBuffer, makePassFBO, makePass } from "./utils.js";
// Maps the brightness of the rendered rain and bloom to colors // 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({ const paletteBuffer = device.createBuffer({
size: (3 + 1) * PALETTE_SIZE * Float32Array.BYTES_PER_ELEMENT, 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 { config, adapter, device, canvasContext, timeBuffer } = context;
const ditherMagnitude = 0.05; const ditherMagnitude = 0.05;
const configLayout = std140(["f32", "vec3<f32>"]); const configUniforms = uniforms.read(`struct Config { ditherMagnitude : f32; backgroundColor: vec3<f32>; };`).Config;
const configBuffer = makeUniformBuffer(device, configLayout, [ditherMagnitude, config.backgroundColor]); const configBuffer = makeUniformBuffer(device, configUniforms, { ditherMagnitude, backgroundColor: config.backgroundColor });
const paletteBuffer = makePalette(device, config.paletteEntries); const paletteBuffer = makePalette(device, config.paletteEntries);

View File

@@ -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"; import { makePassFBO, loadTexture, loadShader, makeUniformBuffer, makePass } from "./utils.js";
const { mat4, vec3 } = glMatrix; const { mat4, vec3 } = glMatrix;
@@ -15,58 +15,20 @@ const cycleStyles = {
const numVerticesPerQuad = 2 * 3; const numVerticesPerQuad = 2 * 3;
const makeConfigBuffer = (device, config, density, gridSize) => { const makeConfigBuffer = (device, configUniforms, config, density, gridSize) => {
// Various effect-related values const configData = {
const rippleType = config.rippleTypeName in rippleTypes ? rippleTypes[config.rippleTypeName] : -1; ...config,
const cycleStyle = config.cycleStyleName in cycleStyles ? cycleStyles[config.cycleStyleName] : 0; gridSize,
const slantVec = [Math.cos(config.slant), Math.sin(config.slant)]; density,
const slantScale = 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1); showComputationTexture: config.effect === "none",
const showComputationTexture = config.effect === "none"; cycleStyle: config.cycleStyleName in cycleStyles ? cycleStyles[config.cycleStyleName] : 0,
rippleType: config.rippleTypeName in rippleTypes ? rippleTypes[config.rippleTypeName] : -1,
const configData = [ slantScale: 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1),
// common slantVec: [Math.cos(config.slant), Math.sin(config.slant)],
{ 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<f32>", 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<f32>", value: slantVec },
{ name: "volumetric", type: "i32", value: config.volumetric },
];
console.table(configData); console.table(configData);
return makeUniformBuffer( return makeUniformBuffer(device, configUniforms, configData);
device,
std140(configData.map((field) => field.type)),
configData.map((field) => field.value)
);
}; };
export default (context, getInputs) => { export default (context, getInputs) => {
@@ -84,13 +46,10 @@ export default (context, getInputs) => {
// rather than a single quad for our geometry // rather than a single quad for our geometry
const numQuads = config.volumetric ? numCells : 1; const numQuads = config.volumetric ? numCells : 1;
const configBuffer = makeConfigBuffer(device, config, density, gridSize); // TODO: uniforms should be updated to provide this too
const sceneLayout = std140(["vec2<f32>", "mat4x4<f32>", "mat4x4<f32>"]);
const sceneBuffer = makeUniformBuffer(device, sceneLayout);
const cellsBuffer = device.createBuffer({ const cellsBuffer = device.createBuffer({
size: numCells * std140(["vec4<f32>"]).size, size: numCells * 4 * Float32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.STORAGE, usage: GPUBufferUsage.STORAGE,
}); });
@@ -115,6 +74,9 @@ export default (context, getInputs) => {
const presentationFormat = canvasContext.getPreferredFormat(adapter); const presentationFormat = canvasContext.getPreferredFormat(adapter);
let configBuffer;
let sceneUniforms;
let sceneBuffer;
let computePipeline; let computePipeline;
let renderPipeline; let renderPipeline;
let computeBindGroup; let computeBindGroup;
@@ -124,6 +86,12 @@ export default (context, getInputs) => {
const ready = (async () => { const ready = (async () => {
const [msdfTexture, rainShader] = await Promise.all(assets); 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({ computePipeline = device.createComputePipeline({
compute: { compute: {
module: rainShader.module, module: rainShader.module,
@@ -183,7 +151,7 @@ export default (context, getInputs) => {
const aspectRatio = width / height; const aspectRatio = width / height;
mat4.perspectiveZO(camera, (Math.PI / 180) * 90, aspectRatio, 0.0001, 1000); mat4.perspectiveZO(camera, (Math.PI / 180) * 90, aspectRatio, 0.0001, 1000);
const screenSize = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1]; 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 // Update
output?.destroy(); output?.destroy();

View File

@@ -1,4 +1,4 @@
import std140 from "./std140.js"; import uniforms from "/lib/gpu-uniforms.js";
import { loadShader, makeUniformBuffer, makePassFBO, makePass } from "./utils.js"; import { loadShader, makeUniformBuffer, makePassFBO, makePass } from "./utils.js";
// Matrix Resurrections isn't in theaters yet, // Matrix Resurrections isn't in theaters yet,
@@ -15,8 +15,8 @@ export default (context, getInputs) => {
const { config, adapter, device, canvasContext, timeBuffer } = context; const { config, adapter, device, canvasContext, timeBuffer } = context;
const ditherMagnitude = 0.05; const ditherMagnitude = 0.05;
const configLayout = std140(["f32", "vec3<f32>"]); const configUniforms = uniforms.read(`struct Config { ditherMagnitude : f32; backgroundColor: vec3<f32>; };`).Config;
const configBuffer = makeUniformBuffer(device, configLayout, [ditherMagnitude, config.backgroundColor]); const configBuffer = makeUniformBuffer(device, configUniforms, { ditherMagnitude, backgroundColor: config.backgroundColor });
const linearSampler = device.createSampler({ const linearSampler = device.createSampler({
magFilter: "linear", magFilter: "linear",

View File

@@ -1,77 +0,0 @@
const supportedTypes = {
["i32"]: [1, 1, "i32"],
["u32"]: [1, 1, "u32"],
["f32"]: [1, 1, "f32"],
["atomic<i32>"]: [1, 1, "i32"],
["vec2<i32>"]: [2, 2, "i32"],
["vec3<i32>"]: [4, 3, "i32"],
["vec4<i32>"]: [4, 4, "i32"],
["atomic<u32>"]: [1, 1, "u32"],
["vec2<u32>"]: [2, 2, "u32"],
["vec3<u32>"]: [4, 3, "u32"],
["vec4<u32>"]: [4, 4, "u32"],
["atomic<f32>"]: [1, 1, "f32"],
["vec2<f32>"]: [2, 2, "f32"],
["vec3<f32>"]: [4, 3, "f32"],
["vec4<f32>"]: [4, 4, "f32"],
["mat2x2<f32>"]: [2, 4, "f32"],
["mat3x2<f32>"]: [2, 6, "f32"],
["mat4x2<f32>"]: [2, 8, "f32"],
["mat2x3<f32>"]: [4, 8, "f32"],
["mat3x3<f32>"]: [4, 12, "f32"],
["mat4x3<f32>"]: [4, 16, "f32"],
["mat2x4<f32>"]: [4, 8, "f32"],
["mat3x4<f32>"]: [4, 12, "f32"],
["mat4x4<f32>"]: [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;

View File

@@ -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"; import { loadShader, make1DTexture, makeUniformBuffer, makePassFBO, makePass } from "./utils.js";
// Multiplies the rendered rain and bloom by a 1D gradient texture // 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 { config, adapter, device, canvasContext, timeBuffer } = context;
const ditherMagnitude = 0.05; const ditherMagnitude = 0.05;
const configLayout = std140(["f32", "vec3<f32>"]); const configUniforms = uniforms.read(`struct Config { ditherMagnitude : f32; backgroundColor: vec3<f32>; };`).Config;
const configBuffer = makeUniformBuffer(device, configLayout, [ditherMagnitude, config.backgroundColor]); const configBuffer = makeUniformBuffer(device, configUniforms, { ditherMagnitude, backgroundColor: config.backgroundColor });
// Expand and convert stripe colors into 1D texture data // Expand and convert stripe colors into 1D texture data
const stripeColors = const stripeColors =

View File

@@ -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({ const buffer = device.createBuffer({
size: structLayout.size, size: uniforms.minSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
mappedAtCreation: values != null, mappedAtCreation: data != null,
}); });
if (values != null) { if (data != null) {
structLayout.build(values, buffer.getMappedRange()); uniforms.write(data, buffer.getMappedRange());
buffer.unmap(); buffer.unmap();
} }
return buffer; return buffer;

View File

@@ -166,6 +166,10 @@ const parseStructLayoutsFromShader = (wgsl) => {
const makeDataForLayout = (structLayouts, layout) => Object.fromEntries(layout.fields.map((field) => [field.identifier, field.defaultValue()])); const makeDataForLayout = (structLayouts, layout) => Object.fromEntries(layout.fields.map((field) => [field.identifier, field.defaultValue()]));
const writeField = (allLayouts, field, value, views, byteOffset) => { const writeField = (allLayouts, field, value, views, byteOffset) => {
if (value == null) {
console.warn(`Property missing: ${field.identifier}`);
return;
}
if (field.isArray) { if (field.isArray) {
const count = field.isFixedSize ? field.mult : value.length; const count = field.isFixedSize ? field.mult : value.length;
for (let i = 0; i < field.mult; i++) { for (let i = 0; i < field.mult; i++) {
@@ -182,46 +186,34 @@ const writeField = (allLayouts, field, value, views, byteOffset) => {
} }
}; };
export default class Uniforms { const makeGenerator = (layout, structLayouts) => {
static fromWGSL(wgsl) { 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); 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; export default api;
#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;
}
}