thimbleberry
Version:
WebGPU utilities
1,737 lines (1,698 loc) • 52 kB
JavaScript
// 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