UNPKG

thimbleberry

Version:
1,737 lines (1,698 loc) 52 kB
// src/shader-util/BenchBatch.ts import { gpuTiming, withTimestampGroup } from "thimbleberry"; async function runBatch(device, run, batchSize, shaderGroup) { const spans = []; gpuTiming.restart(); const batchStart = performance.now(); for (let i = run, batchNum = 0; batchNum < batchSize; i++, batchNum++) { const span = runOnce(i, shaderGroup); span && spans.push({ ...span, runId: i }); } await device.queue.onSubmittedWorkDone(); const clockTime = performance.now() - batchStart; const report = await gpuTiming.results(); const averageClockTime = clockTime / batchSize; return { averageClockTime, spans, report, batchSize }; } function runOnce(id, shaderGroup) { const frameLabel = `frame-${id}`; performance.mark(frameLabel); const { span } = withTimestampGroup(frameLabel, () => { shaderGroup.dispatch(); }); if (span) { return span; } else { console.error("no span from withTimestampGroup. gpuTiming not initialized?"); } } // src/shader-util/BenchDevice.ts async function benchDevice(label = "") { const adapter = await navigator.gpu.requestAdapter(); const { maxBufferSize, maxStorageBufferBindingSize, maxComputeWorkgroupSizeX, maxComputeWorkgroupSizeY, maxComputeWorkgroupSizeZ, maxComputeInvocationsPerWorkgroup } = adapter.limits; const requiredLimits = { maxBufferSize, maxStorageBufferBindingSize, // suprisingly, larger workgroups seems to be slower on Mac M1Max maxComputeWorkgroupSizeX, maxComputeWorkgroupSizeY, maxComputeWorkgroupSizeZ, maxComputeInvocationsPerWorkgroup }; const requiredFeatures = ["timestamp-query"]; return adapter.requestDevice({ label: `${label} bench`, requiredLimits, requiredFeatures }); } // src/shader-util/BenchReport.ts import { FormattedCsv, reportDuration, reportJson } from "thimbleberry"; function logCsvReport(params) { const validParams = validateParams(params); const gpuReports = selectGpuCsv(validParams); const summaryReports = summaryCsv(validParams); const sections = [...gpuReports, summaryReports]; const msg = sections.join("\n\n") + "\n\n"; logMsg(msg); } function logMsg(msg) { console.log(msg); logWebSocket(msg); } function validateParams(config) { const defaultReportType = "median"; const defaults = { reportType: defaultReportType, tags: {}, preTags: {}, precision: 2 }; const result = { ...defaults, ...config }; const reportType = result.reportType; if (!["summary-only", "median", "fastest", "details"].includes(reportType)) { console.error( `invalid reportType: "${reportType}", using reportType: "${defaultReportType}"` ); result.reportType = defaultReportType; } return result; } function selectGpuCsv(params) { const { benchResult, reportType } = params; const { reports } = benchResult; let toReport = []; if (reportType === "summary-only") { return []; } else if (reportType === "details") { toReport = reports; } else if (reportType === "median") { toReport = medianReport(reports); } else if (reportType === "fastest") { const fastest = reports.reduce( (a, b) => reportDuration(a) < reportDuration(b) ? a : b ); toReport = [fastest]; } const reportCsv = gpuPerfCsv(toReport, params); return [reportCsv]; } function medianReport(reports) { const durations = reports.map((r) => ({ report: r, duration: reportDuration(r) })); durations.sort((a, b) => a.duration - b.duration); const median = durations[Math.floor(durations.length / 2)]; return median ? [median.report] : []; } function gpuPerfCsv(reports, params) { const { preTags, tags, precision } = params; const totalLabel = `--> gpu total`; const reportsRows = reports.map((report) => { const jsonRows = reportJson(report, totalLabel, precision); const rowsWithRun = jsonRows.map((row2) => ({ ...row2, runId: report.id })); return rowsWithRun; }); const flatRows = reportsRows.flat(); const reportFullRows = flatRows.map((row2) => ({ ...preTags, ...row2, ...tags })); const fmt = new FormattedCsv(); const csv = fmt.report(reportFullRows); return csv; } function summaryCsv(params) { const { reportType, benchResult, srcSize, preTags, tags, precision } = params; const { averageClockTime, reports } = benchResult; if (reportType === "details") { return []; } const seconds = averageClockTime / 1e3; const gigabytes = srcSize / 2 ** 30; const gbSec = (gigabytes / seconds).toFixed(2); const report = medianReport(reports); const median = report ? reportDuration(report[0]) : 0; const averageTimeMs = averageClockTime.toFixed(precision); const jsonRows = [ { "avg time / run (ms)": averageTimeMs, "median gpu time (ms)": median.toFixed(precision), "src GB/sec": gbSec, "src bytes": srcSize.toString() } ]; const fullRows = jsonRows.map((row2) => ({ ...preTags, ...row2, ...tags })); const summaryCsv2 = new FormattedCsv(); return [summaryCsv2.report(fullRows)]; } function logWebSocket(message) { const url = new URL(document.URL); const params = new URLSearchParams(url.search); const port = params.get("reportPort"); if (port) { const ws = new WebSocket(`ws://127.0.0.1:${port}`); ws.onopen = () => { ws.send(message); ws.close(); }; } } // src/shader-util/BenchRunner.ts import { initGpuTiming } from "thimbleberry"; // src/shader-util/BenchShader.ts import { ShaderGroup as ShaderGroup2, filterReport } from "thimbleberry"; async function benchShader(config, ...shaders) { const { device, runs, warmup = 15, runsPerBatch = 50 } = config; const batchResults = []; const shaderGroup = new ShaderGroup2(device, ...shaders); if (warmup) { await runBatch(device, 0, warmup, shaderGroup); } for (let i = 0; i < runs; ) { const runsThisBatch = Math.min(runsPerBatch, runs - i); const result = await runBatch(device, i, runsThisBatch, shaderGroup); batchResults.push(result); i += runsThisBatch; } shaderGroup.destroyAll(); const batchAverages = batchResults.map((r) => r.averageClockTime * r.batchSize); const averageClockTime = batchAverages.reduce((a, b) => a + b, 0) / runs; const reports = batchResults.flatMap(({ report, spans }) => { return spans.map((span) => { const filtered = filterReport(report, span); return { ...filtered, id: span.runId.toString() }; }); }); return { reports, averageClockTime }; } // src/shader-util/BenchRunner.ts var defaultControl = { reportType: "median", runs: 100, precision: 2, warmup: 15, runsPerBatch: 50 }; async function benchRunner(makeBenchables, attributes) { const testUtc = Date.now().toString(); const device = await benchDevice(); initGpuTiming(device); const benchables = []; for (const make of makeBenchables) { const { shader, srcSize } = await make.makeShader(device); benchables.push({ ...make, shader, srcSize }); } const namedResults = []; for (const b of benchables) { const { srcSize, shader } = b; const { reportType: reportType2, runs, precision: precision2, warmup, runsPerBatch } = controlParams(b); const name = shader.name || shader.constructor.name || "<shader>"; const benchResult = await benchShader({ device, runs, warmup, runsPerBatch }, shader); namedResults.push({ benchResult, name, srcSize }); logCsv(name, benchResult, srcSize, testUtc, reportType2, precision2, attributes); } const { reportType, precision } = controlParams(); if (reportType === "details") { logMsg("## Summary\n"); namedResults.forEach((result) => { const { name, benchResult, srcSize } = result; logCsv(name, benchResult, srcSize, testUtc, "summary-only", precision, attributes); }); } } function controlParams(provided) { const urlParams = urlControlParams(); const result = { ...defaultControl, ...provided, ...urlParams }; return result; } function logCsv(label, benchResult, srcSize, utc, reportType, precision, attributes) { const preTags = { benchmark: label }; const tags = { utc, ...attributes }; logCsvReport({ benchResult, srcSize, reportType, preTags, tags, precision }); } function urlControlParams() { const params = new URLSearchParams(window.location.search); return removeUndefined({ runs: intParam(params, "runs"), reportType: stringParam(params, "reportType"), warmup: intParam(params, "warmup"), runsPerBatch: intParam(params, "runsPerBatch"), precision: intParam(params, "precision") }); } function intParam(params, name) { const value = params.get(name); return value ? parseInt(value) : void 0; } function stringParam(params, name) { return params.get(name) || void 0; } function removeUndefined(obj) { const result = { ...obj }; for (const key in result) { if (result[key] === void 0) { delete result[key]; } } return result; } // src/shader-util/BinOpTemplate.ts var sumTemplate = { elementSize: 4, outputStruct: "sum: f32,", inputStruct: "sum: f32,", pureOp: "return Output(a);", flatMapOp: "return Output(a.sum + b.sum);", identityOp: "return Output(0.0);", inputOp: "return Output(a.sum);" }; var sumTemplateUnsigned = { elementSize: 4, outputStruct: "sum: u32,", inputStruct: "sum: u32,", pureOp: "return Output(a);", flatMapOp: "return Output(a.sum + b.sum);", identityOp: "return Output(0);", inputOp: "return Output(a.sum);" }; var minMaxTemplate = { elementSize: 8, outputStruct: "min: f32, max: f32,", inputStruct: "min: f32, max: f32,", flatMapOp: "return Output(min(a.min, b.min), max(a.max, b.max));", identityOp: "return Output(1e38, -1e38);", inputOp: "return Output(a.min, a.max);", // assumes Input is a struct with min and max pureOp: ` if (a > 0.0) { return Output(a, a); } else { return identityOp(); } ` }; var maxTemplate = { elementSize: 4, outputStruct: "max: f32,", inputStruct: "max: f32,", pureOp: "return Output(a);", flatMapOp: "return Output(max(a.max, b.max));", identityOp: "return Output(0.0);", inputOp: "return Output(a.max);" }; var minMaxAlphaTemplate = { elementSize: 8, outputStruct: "min: f32, max: f32,", inputStruct: "min: f32, max: f32,", flatMapOp: "return Output(min(a.min, b.min), max(a.max, b.max));", identityOp: "return Output(1e38, -1e38);", inputOp: "return Output(a.min, a.max);", // assumes Input is a struct with min and max pureOp: ` if (a > 0.0) { return Output(a, a); } else { return identityOp(); } ` }; // src/shader-util/MemoMemo.ts function memoMemo(fn, options) { const memoizer = options?.memoCache || persistentMemoCache; const keyFn = options?.keyFn || defaultKeyFn; const cache = memoizer(); return function(...args) { const key = keyFn(...args); const found = cache.get(key); if (found !== void 0) { return found; } else { const value = fn(...args); cache.set(key, value); return value; } }; } function persistentMemoCache() { return /* @__PURE__ */ new Map(); } function defaultKeyFn(...args) { return JSON.stringify(args[0] ?? ""); } // src/shader-util/CacheKeyWithDevice.ts function memoizeWithDevice(fn) { const keyFn = cacheKeyWithDevice; let memoFn; return function(paramsObj, memoCache) { if (!memoFn) { memoFn = memoMemo(fn, { keyFn, memoCache }); } return memoFn(paramsObj); }; } function cacheKeyWithDevice(paramsObj) { const deviceStr = `device: ${paramsObj.device?.label ?? "."}`; const withoutDevice = { ...paramsObj }; delete withoutDevice["device"]; const mainStr = JSON.stringify({ ...withoutDevice }); const result = `${mainStr}; ${deviceStr}`; return result; } // src/shader-util/MapN.ts function mapN(n, fn) { const result = new Array(n); const mapFn = fn || ((i) => i); for (let i = 0; i < n; i++) { result[i] = mapFn(i); } return result; } // src/shader-util/Sliceable.ts function* partitionBySize(a, count) { for (let i = 0; i < a.length; ) { const part = a.slice(i, i + count); yield part; i += part.length; } } function* filterNth(a, nth, stride) { for (let i = nth; i < a.length; i += stride) { yield a[i]; } } // src/shader-util/CircleVertices.ts function circleVerts(n) { const numVerts = Math.ceil(n); const sliceAngle = 2 * Math.PI / numVerts; let angle = sliceAngle; const vertices = [...mapN(numVerts)].map(() => { const vec = [Math.sin(angle), Math.cos(angle)]; angle += sliceAngle; return vec; }); return vertices; } function circleStrip(radius, error = 0.15) { const proposedVerts = Math.ceil(Math.PI / Math.acos(1 - error / radius)); const numVerts = Math.max(proposedVerts, 6); const center = [0, 0]; const circle = circleVerts(numVerts); const verts = [...partitionBySize(circle, 2)].flatMap(([a, b]) => { if (b !== void 0) { return [center, a, b]; } else { return [center, a]; } }); verts.push(center, circle[0]); return verts; } // src/shader-util/ConfigureCanvas.ts import { dlog } from "berry-pretty"; function configureCanvas(device, canvas, debug = false) { const context = canvas.getContext("webgpu"); if (!context) { dlog("no WebGPU context for canvas", canvas); throw new Error("no WebGPU context available"); } let usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST; if (debug) { usage |= GPUTextureUsage.COPY_SRC; } context.configure({ device, alphaMode: "opaque", format: navigator.gpu.getPreferredCanvasFormat(), usage }); return context; } // src/shader-util/ConvertTemplate.ts var rgbaFloatRedToFloat = { srcTextureType: "texture_2d<f32>", srcComponentType: "f32", destFormat: "r32float", destComponentType: "f32", processTexel: "return vec4(texel.r * 255.0, 0.0, 0.0, 0.0);" }; var rgbaUintRedToFloat = { srcComponentType: "u32", srcTextureType: "texture_2d<u32>", destFormat: "r32float", destComponentType: "f32", processTexel: "return vec4(f32(texel.r), 0.0, 0.0, 0.0);" }; // src/shader-util/CreateDebugBuffer.ts function createDebugBuffer(device, label = "debugBuffer", size = 16) { return device.createBuffer({ label, size: size * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC }); } // src/shader-util/FormattedCsv.ts var columnPadding = 2; var FormattedCsv2 = class { constructor(staticFields) { this.staticColumns = staticFields || []; } /** @returns the csv tabular report for the provided array of values */ report(valueRows) { const columns = this.columnNames(valueRows); const widths = columnWidths(columns, valueRows); const headerRows = Object.fromEntries(columns.map((c) => [c, c])); const jsonRows = [headerRows, ...valueRows]; const stringRows = jsonRows.map((r) => row(r, widths)); return stringRows.join("\n"); } columnNames(values) { const set = /* @__PURE__ */ new Set(); values.forEach((jsonRow) => Object.keys(jsonRow).forEach((k) => set.add(k))); this.staticColumns.forEach((k) => set.add(k)); return [...set]; } }; function row(rowValues, columnWidths2) { const rowStrings = Object.entries(columnWidths2).map(([name, width]) => { const value = rowValues[name] || ""; return value.slice(0, width).padStart(width, " "); }); return rowStrings.join(","); } function columnWidths(columnNames, values) { const entries = columnNames.map((name) => { const valueLength = longestColumnValue(values, name); const maxWidth = Math.max(name.length, valueLength); return [name, maxWidth + columnPadding]; }); return Object.fromEntries(entries); } function longestColumnValue(rows, name) { const column = rows.map((v) => v[name]); const widths = column.map((v) => v ? v.length : 0); const longest = widths.reduce((a, b) => a > b ? a : b); return longest; } // src/shader-util/GpuPerfReport.ts function filterReport2(report, container) { const spans = report.spans.filter( (s) => s._startDex >= container._startDex && s._endDex <= container._endDex ); const marks = report.marks.filter( (m) => m.start >= container._startDex && m._startDex <= container._endDex ); return { spans, marks }; } function reportDuration2(report) { return lastTime(report) - firstTime(report); } function firstTime(report) { const firstSpanStart = report.spans.reduce((a, s) => Math.min(s.start, a), Infinity); const firstMark = report.marks.reduce((a, s) => Math.min(s.start, a), Infinity); return Math.min(firstSpanStart, firstMark); } function lastTime(report) { const lastSpanEnd = report.spans.reduce( (a, s) => Math.max(s.start + s.duration, a), -Infinity ); const lastMark = report.marks.reduce((a, s) => Math.max(s.start, a), -Infinity); return Math.max(lastSpanEnd, lastMark); } function reportJson2(report, labelTotal, precision = 2) { const startTime = firstTime(report); const rows = []; for (const span of report.spans) { const row2 = { name: span.label, start: (span.start - startTime).toFixed(precision), duration: span.duration.toFixed(precision) }; rows.push(row2); } const total = reportDuration2(report).toFixed(precision); rows.push({ name: labelTotal || "gpu-total", start: startTime.toFixed(precision), duration: total }); return rows; } // src/shader-util/CsvReport.ts function csvReport(report, extraRows, tagColumns, label) { const csv = new FormattedCsv2(); const gpuRows = reportJson2(report, label); const addedRows = additionalRows(extraRows); const allRows = [...gpuRows, ...addedRows]; const extraValues = tagColumnValues(tagColumns); const rows = allRows.map((g) => ({ ...g, ...extraValues })); return csv.report(rows); } function tagColumnValues(tagColumns) { if (!tagColumns) return {}; const extraValues = Object.entries(tagColumns).map(([name, combinedValue]) => { const value = typeof combinedValue === "string" ? combinedValue : combinedValue.value; return [name, value]; }); return Object.fromEntries(extraValues); } function additionalRows(extraRows) { if (!extraRows) return []; const start = 0 .toFixed(2); const columnValues = Object.entries(extraRows).map(([name, value]) => ({ name, start, duration: value })); return columnValues; } // src/shader-util/FilledGPUBuffer.ts function filledGPUBuffer(device, data, usage = GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, label, ArrayConstructor = Float32Array) { const buffer = device.createBuffer({ label, size: data.length * ArrayConstructor.BYTES_PER_ELEMENT, usage, mappedAtCreation: true }); new ArrayConstructor(buffer.getMappedRange()).set(data); buffer.unmap(); return buffer; } function bufferI32(device, data, label = "bufferI32") { return filledGPUBuffer( device, data, GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, label, Int32Array ); } function bufferU32(device, data, label = "bufferU32") { return filledGPUBuffer( device, data, GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, label, Uint32Array ); } function bufferF32(device, data, label = "bufferF32") { return filledGPUBuffer( device, data, GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, label, Float32Array ); } // src/shader-util/FilledTexture.ts import { arrayToArrayBuffer, componentByteSize, mapN as mapN2, numComponents, renderAttachable, storageBindable } from "thimbleberry"; function makeTexture(device, data, format = "r16float", label) { const components = numComponents(format); if (data[0][0] instanceof Array) { console.assert(components === data[0][0].length, "data must match format"); } else { console.assert(components === 1, "data must match format"); } const size = [data[0].length, data.length]; const arrayBuffer = arrayToArrayBuffer(format, data.flat(2)); return textureFromArray(device, arrayBuffer, size, format, label); } function textureFromArray(device, data, size, format = "r16float", label) { const components = numComponents(format); const texture = makeEmptyTexture(device, size, label, format); device.queue.writeTexture( { texture }, data, { bytesPerRow: size[0] * componentByteSize(format) * components, rowsPerImage: size[1] }, size ); return texture; } function makeEmptyTexture(device, size, label, format = "r16float") { let usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC; if (renderAttachable(format)) { usage |= GPUTextureUsage.RENDER_ATTACHMENT; } if (storageBindable(format)) { usage |= GPUTextureUsage.STORAGE_BINDING; } return device.createTexture({ label, size, format, usage }); } function make2dSequence(size, step = 1) { const data = []; let i = 0; for (let y = 0; y < size[1]; y++) { const row2 = []; for (let x = 0; x < size[0]; x++) { row2.push(i * step); i++; } data.push(row2); } return data; } function make3dSequence(size, numComponents3 = 4) { let i = 0; return mapN2( size[0], () => mapN2(size[1], () => { const rgba = mapN2(numComponents3, (c) => i + c * 10); i++; return rgba; }) ); } // src/shader-util/FullFrameTriangles.ts var [top, bottom] = [1, -1]; var [left, right] = [-1, 1]; var fullFrameTriangleStrip = [ [left, bottom], [left, top], [right, top], // upper left triangle [right, bottom], [left, bottom] // lower right triangle ]; // src/shader-util/TrackUse.ts var resources = /* @__PURE__ */ new WeakMap(); var autoContextStack = []; var trackCount = 0; var destroyCount = 0; function trackUse(target, context) { const refCount = resources.get(target) || 0; if (refCount === 0) { trackCount++; } resources.set(target, refCount + 1); last(autoContextStack)?._addRef(target); context?._addRef(target); return target; } function trackRelease(target, context) { const refCount = resources.get(target) || 0; if (refCount === 1) { destroyCount++; resources.delete(target); context?._removeRef(target); last(autoContextStack)?._removeRef(target); target.destroy(); } else { resources.set(target, refCount - 1); } } var AutoContext = class { constructor() { this.refs = /* @__PURE__ */ new Set(); autoContextStack.push(this); } finish() { this.refs.forEach((target) => trackRelease(target, this)); autoContextStack.pop(); } _addRef(target) { this.refs.add(target); } _removeRef(target) { this.refs.delete(target); } }; var TrackContext = class { constructor() { this.refs = /* @__PURE__ */ new Set(); } finish() { this.refs.forEach((target) => trackRelease(target, this)); } _addRef(target) { this.refs.add(target); } _removeRef(target) { this.refs.delete(target); } }; function withUsage(fn) { const usage = new AutoContext(); let result; try { result = fn(); } finally { usage.finish(); } return result; } async function withAsyncUsage(fn) { const usage = new AutoContext(); let result; try { result = await fn(); } finally { usage.finish(); } return result; } function trackContext() { return new TrackContext(); } async function withLeakTrack(fn, expectLeak = 0) { const startTrack = trackCount; const startDestroy = destroyCount; const result = await fn(); const netDestroyed = destroyCount - startDestroy; const netTracked = trackCount - startTrack; if (netTracked - netDestroyed != expectLeak) { const message = `trackUse: ${netTracked} tracked, ${netDestroyed} destroyed`; console.error(message); throw new Error(message); } return result; } function last(a) { return a[a.length - 1]; } // src/shader-util/FullFrameVertexBuffer.ts var fullFrameVertexBuffer = memoizeWithDevice(createFrameVertexBuffer); function createFrameVertexBuffer(params) { const { device } = params; const verts = fullFrameTriangleStrip.flat(); const usage = GPUBufferUsage.VERTEX; const buffer = filledGPUBuffer(device, verts, usage, "full-screen-verts", Float32Array); trackUse(buffer); return buffer; } // src/shader-util/WithBufferCopy.ts async function withBufferCopy(device, buffer, fmt, fn) { const size = buffer.size; const copy = device.createBuffer({ size, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST }); const commands = device.createCommandEncoder({}); commands.copyBufferToBuffer(buffer, 0, copy, 0, size); const cmdBuffer = commands.finish(); device.queue.submit([cmdBuffer]); await copy.mapAsync(GPUMapMode.READ); const cpuCopy = arrayForType(fmt, copy.getMappedRange()); try { return fn(cpuCopy); } finally { copy.unmap(); copy.destroy(); } } async function copyBuffer(device, buffer, fmt = "u32") { return withBufferCopy(device, buffer, fmt, (d) => [...d]); } async function printBuffer(device, buffer, fmt = "f32", prefix2, precision) { await withBufferCopy(device, buffer, fmt, (data) => { if (buffer.label) console.log(`${prefix2 || ""}${buffer.label}:`); let stringData; if (precision) { stringData = [...data].map((d) => d.toPrecision(precision)); } else { stringData = [...data].map((d) => d.toString()); } console.log(" ", stringData.join(" ")); }); } function arrayForType(type, data) { if (type === "f32" || type.endsWith("f") || type.endsWith("h")) { return new Float32Array(data); } if (type === "u8") { return new Uint8Array(data); } if (type === "u32") { return new Uint32Array(data); } if (type === "i32") { return new Int32Array(data); } if (type === "i8") { return new Int8Array(data); } throw new Error(`Unknown type: ${type}`); } function elementStride(fmt) { let numSlots; let elemSize; if (fmt.startsWith("vec")) { elemSize = suffixTypeBytes(fmt.slice(-1)); const size = fmt[3]; if (size === "2") { numSlots = 2; } else if (size === "3" || size === "4") { numSlots = 4; } else throw new Error(`Unknown vector size: ${fmt}`); } else if (fmt.startsWith("mat")) { elemSize = suffixTypeBytes(fmt.slice(-1)); const matSize = fmt.slice(3, 6); if (matSize === "2x2") { numSlots = 4; } else if (matSize === "2x3" || matSize === "3x2" || matSize === "2x4" || matSize === "4x2") { numSlots = 8; } else if (matSize === "3x3" || matSize === "3x4" || matSize === "4x3") { numSlots = 12; } else if (matSize === "4x4") { numSlots = 16; } else throw new Error(`Unknown matrix size: ${fmt}`); } else { numSlots = 1; const found = fmt.match(/\d+/); const bits = Number.parseInt(found?.[0]); elemSize = bits / 8; } return elemSize * numSlots; } function suffixTypeBytes(suffix) { switch (suffix) { case "f": case "u": case "i": return 4; case "h": return 2; default: throw new Error(`Unknown suffix: ${suffix}`); } } // src/shader-util/GpuPerf.ts var gpuTiming2; var maxQuerySetSize = 4096; var sessionEpoch = 0; var GpuTiming = class { constructor(device, maxSize) { this.stampDex = 0; this.marks = []; this.spans = []; this.sessionNum = sessionEpoch++; this.device = device; this.capacity = maxSize ?? maxQuerySetSize; this.querySet = this.initQuerySet(); this.resultBuffer = this.createResultBuffer(); } /** restart thie timing session, dropping any unreported results */ restart() { this.stampDex = 0; this.marks.length = 0; this.spans.length = 0; this.sessionNum = sessionEpoch++; } /** * @return beginning & end timing structures for use as parameters for * beginRenderPass or beginComputePass */ timestampWrites(label) { const querySet = this.querySet; this.spans.push({ label, startDex: this.stampDex, endDex: this.stampDex + 1 }); return { querySet, beginningOfPassWriteIndex: this.stampDex++, endOfPassWriteIndex: this.stampDex++ }; } /** fetch all results from the gpu and return a report */ async results() { const { querySet, device, resultBuffer } = this; await this.device.queue.onSubmittedWorkDone(); const commands = this.device.createCommandEncoder(); commands.resolveQuerySet(querySet, 0, querySet.count, resultBuffer, 0); device.queue.submit([commands.finish()]); const report = await withBufferCopy(device, resultBuffer, "u32", (data) => { const dTime = this.toRelativeTimes(data); return this.relativeTimeToReport(dTime); }); return report; } /** * Group a set of gpu timing records, * e.g. to record a frame's worth of render and compute passes * * @returns a CompletedSpan that can be used to filter * a GpuPerfReport to just the records for this group. */ withGroup(label, fn) { const _startDex = this.stampDex; const result = fn(); const _endDex = this.stampDex; const span = { label, _startDex, _endDex }; return { result, span }; } destroy() { this.querySet.destroy(); this.resultBuffer.destroy(); } /** convert 64 bit uint timestamps from the gpu * to floating point times relative to the first event */ toRelativeTimes(data) { const validData = data.slice(0, this.stampDex * 2); const evts = [...partitionBySize(validData, 2)]; if (evts.length === 0) { return []; } const firstEvt = evts.reduce((a, b) => { const [aLo, aHi] = a; const [bLo, bHi] = b; if (aHi < bHi) { return a; } if (aLo < bLo) { return a; } else { return b; } }); const dTime = evts.slice(0, this.stampDex).map(([lo, hi]) => { const dHi = hi - firstEvt[1]; const dLo = lo - firstEvt[0]; return (dHi * 2 ** 32 + dLo) / 1e6; }); return dTime; } /** report structured gpu perf results */ relativeTimeToReport(dTime) { const markReport = this.marks.map((m) => { const start = dTime[m.stampDex]; return { start, label: m.label, _startDex: m.stampDex }; }); const spanReport = this.spans.map((s) => { const start = dTime[s.startDex]; const end = dTime[s.endDex]; const duration = end - start; return { duration, start, label: s.label, _startDex: s.startDex, _endDex: s.endDex }; }); return { marks: markReport, spans: spanReport }; } /** allocate a reusable buffer for collecting timing data */ createResultBuffer() { const { querySet, device } = this; const size = Float64Array.BYTES_PER_ELEMENT * querySet.count; const buffer = device.createBuffer({ size, usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC }); trackUse(querySet); return buffer; } /** install the query set buffer on the gpu */ initQuerySet() { const querySet = this.device.createQuerySet({ type: "timestamp", count: this.capacity, label: "gpuTiming" }); trackUse(querySet); return querySet; } }; function initGpuTiming2(device, maxSize) { if (gpuTiming2) { return; } gpuTiming2 = new GpuTiming(device, maxSize); } function destroyGpuTiming() { if (gpuTiming2) { gpuTiming2.destroy(); gpuTiming2 = void 0; } } function withTimestampGroup2(label, fn) { if (gpuTiming2) { return gpuTiming2.withGroup(label, fn); } else { return { result: fn() }; } } // src/shader-util/TextureFormats.ts import { Float16Array, setFloat16 } from "@petamoriken/float16"; function componentByteSize2(format) { if (format.includes("32")) { return 4; } if (format.includes("16")) { return 2; } if (format.includes("8")) { return 1; } throw new Error(`Unknown texture format ${format}`); } function numComponents2(format) { if (format.startsWith("bgra")) { return 4; } if (format.startsWith("rgba")) { return 4; } if (format.startsWith("rg")) { return 2; } if (format.startsWith("r")) { return 1; } throw new Error(`Unknown texture format ${format}`); } function bufferToSliceable(format, data) { const converted = gpuArrayFormat(format, data.buffer); if (converted) { return converted; } switch (format) { case "r16float": case "rg16float": case "rgba16float": return new Float16Array(data.buffer); default: throw new Error(`Unknown texture format ${format}`); } } function arrayToArrayBuffer2(format, data) { const converted = gpuArrayFormat(format, data); if (converted) { return converted; } switch (format) { case "r16float": case "rg16float": case "rgba16float": return floatsToUint16Array(data); default: throw new Error(`Unknown texture format ${format}`); } } function gpuArrayFormat(format, inData) { const data = inData; switch (format) { case "r8uint": case "rg8uint": case "rgba8uint": case "rgba8unorm": case "rgba8unorm-srgb": case "rgba8snorm": case "bgra8unorm": case "bgra8unorm-srgb": case "r8unorm": case "r8snorm": return new Uint8Array(data); case "r8sint": case "rg8sint": case "rgba8sint": return new Int8Array(data); case "r16uint": case "rg16uint": case "rgba16uint": return new Uint16Array(data); case "r16sint": case "rg16sint": case "rgba16sint": return new Int16Array(data); case "r32uint": case "rg32uint": case "rgba32uint": return new Uint32Array(data); case "r32sint": case "rg32sint": case "rgba32sint": return new Int32Array(data); case "r32float": case "rg32float": case "rgba32float": return new Float32Array(data); default: return void 0; } } function textureSampleType(format, float32Filterable = false) { if (format.includes("32float")) { return float32Filterable ? "float" : "unfilterable-float"; } if (format.includes("float") || format.includes("unorm")) { return "float"; } if (format.includes("uint")) { return "uint"; } if (format.includes("sint")) { return "sint"; } throw new Error(`native sample type unknwon for for texture format ${format}`); } function storageBindable2(format) { switch (format) { case "rgba8unorm": case "rgba8snorm": case "rgba8uint": case "rgba8sint": case "rgba16uint": case "rgba16sint": case "r32uint": case "r32sint": case "r32float": case "rg32uint": case "rg32sint": case "rg32float": case "rgba32uint": case "rgba32sint": case "rgba32float": return true; case "r8unorm": case "r8uint": case "r8snorm": case "rg8uint": case "rg8unorm": case "rgba8unorm-srgb": case "bgra8unorm": case "bgra8unorm-srgb": case "r8sint": case "rg8sint": case "r16uint": case "r16sint": case "r16float": case "rg16uint": case "rg16sint": case "rg16float": case "rgba16float": case "rgb10a2unorm": case "rg11b10ufloat": default: return false; } } function renderAttachable2(format) { switch (format) { case "r8unorm": case "r8uint": case "r8sint": case "rg8unorm": case "rg8uint": case "rg8sint": case "rgba8unorm": case "rgba8unorm-srgb": case "rgba8uint": case "rgba8sint": case "bgra8unorm": case "bgra8unorm-srgb": case "r16uint": case "r16sint": case "r16float": case "rg16uint": case "rg16sint": case "rg16float": case "rgba16uint": case "rgba16sint": case "rgba16float": case "r32uint": case "r32sint": case "r32float": case "rg32uint": case "rg32sint": case "rg32float": case "rgba32uint": case "rgba32sint": case "rgba32float": case "rgb10a2unorm": return true; case "r8snorm": case "rg8snorm": case "rgba8snorm": case "rg11b10ufloat": default: return false; } } function floatsToUint16Array(data) { const out = new Uint16Array(data.length); const toFP16 = fp16Converter(); data.forEach((n, i) => { const twoBytes = toFP16(n); out.set([twoBytes], i); }); return out; } function texelLoadType(format) { if (format.includes("float")) return "f32"; if (format.includes("unorm")) return "f32"; if (format.includes("uint")) return "u32"; if (format.includes("sint")) return "i32"; throw new Error(`unknown format ${format}`); } function fp16Converter() { const ff = new Float16Array(1); const ffView = new DataView(ff.buffer); return (n) => { setFloat16(ffView, 0, n); return ffView.getUint16(0, false); }; } // src/shader-util/ImageToTexture.ts async function textureFromImageUrl(device, url, format, gpuUsage) { const response = await fetch(url); const blob = await response.blob(); const imgBitmap = await createImageBitmap(blob); return bitmapToTexture(device, imgBitmap, format, gpuUsage, url); } function bitmapToTexture(device, source, format, gpuUsage, label) { const resolvedFormat = format || navigator.gpu.getPreferredCanvasFormat(); let usage; if (gpuUsage) { usage = gpuUsage; } else { usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC; if (renderAttachable2(resolvedFormat)) { usage |= GPUTextureUsage.RENDER_ATTACHMENT; } } const textureDescriptor = { size: { width: source.width, height: source.height }, format: resolvedFormat, usage, label }; const texture = device.createTexture(textureDescriptor); device.queue.copyExternalImageToTexture( { source }, { texture }, textureDescriptor.size ); return texture; } // src/shader-util/LabeledGpuDevice.ts var deviceId = 0; async function labeledGpuDevice(descriptor) { const adapter = await navigator.gpu.requestAdapter(); return adapter.requestDevice({ label: `device-${deviceId++}`, ...descriptor }); } // src/shader-util/LimitWorkgroupLength.ts function limitWorkgroupLength(device, proposedLength) { const maxThreads = Math.min( device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX ); let length; if (!proposedLength || proposedLength > maxThreads) { length = maxThreads; } else { length = proposedLength; } return length; } // src/shader-util/LoadTemplate.ts var loadRedComponent = { loadOp: "return a.r;" }; var loadGreenComponent = { loadOp: "return a.g;" }; var loadBlueComponent = { loadOp: "return a.b;" }; var loadAlphaComponent = { loadOp: "return a.a;" }; function loaderForComponent(component) { switch (component) { case "r": return loadRedComponent; case "g": return loadGreenComponent; case "b": return loadBlueComponent; case "a": return loadAlphaComponent; } } // src/shader-util/PlaceholderTexture.ts var placeholderTexture = memoMemo(makePlaceholderTexture); function makePlaceholderTexture(device) { return device.createTexture({ label: "placeholder texture", size: [50, 50], format: "rgba8unorm", usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC }); } // src/shader-util/PromiseDelay.ts function promiseDelay(timeout = 0) { return new Promise((resolve) => setTimeout(resolve, timeout)); } // src/shader-util/ReactiveUtil.ts import { onCleanup } from "@reactively/core"; function assignParams(target, params, defaults) { updateProperties(target, params); const paramKeys = Object.keys(params); for (const key in defaults) { if (!paramKeys.includes(key)) { target[key] = defaults[key]; } } verifyReactiveAssigned(target, params, defaults); } function verifyReactiveAssigned(target, params, defaults) { const validDefaults = defaults || {}; const assignedKeys = [...Object.keys(params), ...Object.keys(validDefaults)]; const reactiveTarget = target; for (const key in reactiveTarget.__reactive) { const node = reactiveTarget.__reactive[key]; if (node._value === void 0 && node.fn === void 0 && !assignedKeys.includes(key)) { const message = `Property ${key} is not initialized. Perhaps set a default value?`; console.error(message, target); } } } function reactiveProp(target, key) { const value = target.__reactive[key]; if (!value) { const message = `Property ${key} is not reactive`; console.error(message, target); throw new Error(message); } return target.__reactive[key]; } function reactiveTrackUse(target, context) { trackUse(target, context); onCleanup(() => trackRelease(target, context)); } function updateProperties(dest, updates) { for (const key in updates) { dest[key] = updates[key]; } } // src/shader-util/SequenceShader.ts import { HasReactive } from "@reactively/decorate"; var HasShaderSequence = class extends HasReactive { commands(encoder) { this.shaders.forEach((s) => s.commands(encoder)); } destroy() { this.shaders.forEach((s) => s.destroy?.()); } }; var SequenceShader = class extends HasShaderSequence { constructor(shaders, name) { super(); this.shaders = shaders; this.name = name || "SequenceShader"; } }; // src/shader-util/ShaderGroup.ts var frameNumber = 0; var ShaderGroup3 = class { constructor(device, ...shaders) { this.device = device; this.shaders = shaders; } dispatch() { const { device } = this; const label = `frame ${frameNumber++}`; const stampRange = withTimestampGroup2(label, () => { const commands = device.createCommandEncoder({ label }); this.shaders.forEach((s) => s.commands(commands)); device.queue.submit([commands.finish()]); }); return stampRange.span; } destroyAll() { this.shaders.forEach((s) => s.destroy?.()); } }; // src/shader-util/Template.ts var prefix = /\s*/.source; var findUnquoted = /(?<findUnquoted>[\w-<>.]+)/.source; var findQuoted = /"(?<findQuoted>[^=]+)"/.source; var replaceKey = /\s*(?<replaceKey>[\w-]+)/.source; var replaceValue = /\s*"(?<replaceValue>[^"]+)"/.source; var parseRule = new RegExp( `${prefix}(${findUnquoted}|${findQuoted})=(${replaceKey}|${replaceValue})`, "g" ); var ifRule = /\s+IF\s+(?<ifKey>[\w-]+)/gi; var ifNotRule = /\s+IF\s+!\s*(?<ifKey>[\w-]+)/gi; function applyTemplate(wgsl, dict) { const edit = wgsl.split("\n").flatMap((line, i) => line.includes("//!") ? changeLine(line, i + 1) : [line]); return edit.join("\n"); function changeLine(line, lineNum) { const [text, comment] = line.split("//!"); const ifMatches = comment.matchAll(ifRule); for (const m of ifMatches) { const ifKey = m.groups?.ifKey; if (ifKey && (!(ifKey in dict) || dict[ifKey] === false)) { return []; } } const ifNotMatches = comment.matchAll(ifNotRule); for (const m of ifNotMatches) { const ifNotKey = m.groups?.ifKey; if (ifNotKey && ifNotKey in dict && dict[ifNotKey] !== false) { return []; } } return applyPatches(text, comment, lineNum); } function applyPatches(text, comment, lineNum) { const ruleMatches = [...comment.matchAll(parseRule)]; let unpatched = text; const keysReplaced = []; const suffixes = []; ruleMatches.length; for (const patch of ruleMatches.reverse()) { const { prefix: prefix2, newSuffix, replacedKey } = applyOnePatch(unpatched, patch, lineNum); replacedKey && keysReplaced.push(replacedKey); newSuffix && suffixes.push(newSuffix); unpatched = prefix2; } const patchedLine = unpatched + suffixes.reverse().join(""); const newComment = keysReplaced.length ? `// ${keysReplaced.reverse().join(" ")}` : ""; const edited = patchedLine + newComment; return [edited]; } function applyOnePatch(src, patch, lineNum) { const findUnquoted2 = patch?.groups?.findUnquoted; const findQuoted2 = patch?.groups?.findQuoted; const find = findUnquoted2 || findQuoted2; const replaceKey2 = patch?.groups?.replaceKey; const replaceValue2 = patch?.groups?.replaceValue; if (find && (replaceValue2 || replaceKey2)) { const replacement = replaceValue2 ?? dict[replaceKey2]; if (replacement !== void 0) { const revised = replaceRightmost(find, replacement, src); if (revised.newSuffix) { return { ...revised, replacedKey: replaceValue2 ?? replaceKey2 }; } else { console.error(`${lineNum}: could not find '${find}' in ${src}`); } } else { console.error( `${lineNum}: could not find replacement for '${replaceKey2}' in: ${dict}` ); } } else { console.error(`${lineNum}: could not parse rule: ${patch}`); } return { prefix: src, newSuffix: void 0 }; } function replaceRightmost(find, replace, text) { const found = text.lastIndexOf(find); if (found >= 0) { const start = text.slice(0, found); const end = text.slice(found + find.length); const newEnd = replace + end; return { prefix: start, newSuffix: newEnd }; } else { return { prefix: text, newSuffix: void 0 }; } } } // src/shader-util/TextureLogging.ts import { prettyFloat } from "berry-pretty"; async function printTexture(device, texture, selectComponent, precision) { const components = numComponents2(texture.format); withTextureCopy(device, texture, (data) => { if (texture.label) console.log(`${texture.label}:`); const s = imageArrayToString( data, texture.height, components, selectComponent, precision ); console.log(s); }); } async function withTextureCopy(device, texture, fn) { const imageTexture = { texture }; const components = numComponents2(texture.format); const bytesPerComponent = componentByteSize2(texture.format); const textureByteWidth = texture.width * components * bytesPerComponent; const bufferByteWidth = Math.ceil(textureByteWidth / 256) * 256; const bufferBytes = bufferByteWidth * texture.height; const buffer = device.createBuffer({ label: "textureCopy", size: bufferBytes, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST }); const imageDestination = { buffer, bytesPerRow: bufferByteWidth, rowsPerImage: texture.height }; const copySize = { width: texture.width, height: texture.height, depthOrArrayLayers: texture.depthOrArrayLayers }; const commands = device.createCommandEncoder({}); commands.copyTextureToBuffer(imageTexture, imageDestination, copySize); const cmdBuffer = commands.finish(); device.queue.submit([cmdBuffer]); await buffer.mapAsync(GPUMapMode.READ); const mapped = buffer.getMappedRange(); const cpuCopy = new Uint8Array(mapped); const trimmed = trimExcess(cpuCopy, bufferByteWidth, textureByteWidth); const data = bufferToSliceable(texture.format, trimmed); try { return fn(data); } finally { buffer.unmap(); buffer.destroy(); } } function hexBytes(src, maxLength = 1024) { return [...src.slice(0, maxLength)].map((v) => v.toString(16)).join(" "); } function trimExcess(bufferArray, bytesPerRow, imageBytesPerRow) { const resultRows = []; for (const row2 of partitionBySize(bufferArray, bytesPerRow)) { const slice = row2.slice(0, imageBytesPerRow); resultRows.push(slice); } const byteArray = resultRows.flatMap((row2) => [...row2]); return new Uint8Array(byteArray); } function imageArrayToString(imageArray, imageHeight, components, selectComponent, precision = 3) { const rows = imageArrayToRows(imageArray, imageHeight, components, selectComponent); const strings = rows.map((row2, i) => { const rowString = [...row2].map((s) => prettyFloat(s, precision).padStart(precision)).join(" "); return ` ${i % 10}: ${rowString}`; }); return strings.join("\n"); } function imageArrayToRows(imageArray, imageHeight, components, selectComponent) { const imageWidth = imageArray.length / imageHeight; const rows = [...partitionBySize(imageArray, imageWidth)]; const result = rows.map((row2) => { if (selectComponent !== void 0) { return [...filterNth(row2, selectComponent, components)]; } else { return row2; } }); return result; } // src/shader-util/TextureResource.ts function textureResource(src) { if (src instanceof GPUTexture) { return src.createView({ label: `view ${src.label}` }); } else if (src instanceof GPUExternalTexture) { return src; } else { throw new Error("provide an externalSrc or a srcTexture"); } } // src/shader-util/WeakCache.ts var WeakCache = class { constructor() { this.cache = /* @__PURE__ */ new Map(); this.registry = new FinalizationRegistry((key) => { this._expire(key); }); } set(key, value) { this.cache.set(key, new WeakRef(value)); this.registry.register(value, key); } get(key) { const found = this.cache.get(key); const value = found?.deref(); if (value !== void 0) { return value; } if (found) { this.cache.delete(key); } return void 0; } has(key) { return this.get(key) !== void 0; } fetchFill(key, fn) { const found = this.get(key); if (found) { return found; } const value = fn(); this.set(key, value); return value; } // exposed for testing _expire(key) { this.cache.delete(key); } }; // src/shader-util/WeakMemoize.ts function weakMemoCache() { return new WeakCache(); } function weakMemoize(fn, options) { return memoMemo(fn, { mem