diff --git a/package.json b/package.json index 2032f6f3..9061e78f 100644 --- a/package.json +++ b/package.json @@ -35,4 +35,4 @@ "pnpm": "10.33.0" }, "packageManager": "pnpm@10.33.0" -} +} \ No newline at end of file diff --git a/packages/scatterbrain/CHANGELOG.md b/packages/scatterbrain/changelog.md similarity index 100% rename from packages/scatterbrain/CHANGELOG.md rename to packages/scatterbrain/changelog.md diff --git a/packages/scatterbrain/package.json b/packages/scatterbrain/package.json index a0c0a732..67858e9e 100644 --- a/packages/scatterbrain/package.json +++ b/packages/scatterbrain/package.json @@ -1,6 +1,6 @@ { "name": "@alleninstitute/vis-scatterbrain", - "version": "0.1.0", + "version": "0.0.4", "contributors": [ { "name": "Lane Sawyer", @@ -38,8 +38,7 @@ "scripts": { "typecheck": "tsc --noEmit", "build": "parcel build --no-cache", - "dev": "parcel watch --port 1239", - "demo": "vite", + "dev": "parcel watch --no-cache --port 1239", "test": "vitest --watch", "test:ci": "vitest run", "coverage": "vitest run --coverage", @@ -52,9 +51,11 @@ "dependencies": { "@alleninstitute/vis-core": "workspace:*", "@alleninstitute/vis-geometry": "workspace:*", - "lodash": "4.18.1", + "lodash-es": "4.18.1", "regl": "2.1.0", "ts-pattern": "5.9.0", + "typegpu": "0.11.2", + "webgpu-utils": "2.0.2", "zod": "4.3.6" }, "publishConfig": { @@ -63,6 +64,8 @@ }, "packageManager": "pnpm@9.14.2", "devDependencies": { - "@types/lodash": "4.17.24" + "@types/lodash-es": "4.17.12", + "@types/node": "22.19.15", + "@webgpu/types": "0.1.69" } -} +} \ No newline at end of file diff --git a/packages/scatterbrain/src/cache-client.ts b/packages/scatterbrain/src/cache-client.ts index 384cf778..e9615f8a 100644 --- a/packages/scatterbrain/src/cache-client.ts +++ b/packages/scatterbrain/src/cache-client.ts @@ -1,5 +1,5 @@ import type { Cacheable, SharedPriorityCache } from '@alleninstitute/vis-core'; -import reduce from 'lodash/reduce'; +import reduce from 'lodash-es/reduce'; import type { WebGLSafeBasicType } from './typed-array'; import type { ColumnRequest, Item } from './types'; @@ -19,7 +19,7 @@ export function buildScatterbrainCacheClient( ...acc, [key]: `${dataset.metadata.metadataFileEndpoint}/${node.file}/${col.name}`, }), - {} + {}, ); }, fetch: (item) => { diff --git a/packages/scatterbrain/src/dataset.ts b/packages/scatterbrain/src/dataset.ts index 4ec5f05b..e62ab8b8 100644 --- a/packages/scatterbrain/src/dataset.ts +++ b/packages/scatterbrain/src/dataset.ts @@ -10,7 +10,7 @@ import { visitBFSMaybe, } from '@alleninstitute/vis-geometry'; import type { PointAttribute, ScatterbrainDataset, SlideviewScatterbrainDataset, TreeNode, volumeBound } from './types'; -import reduce from 'lodash/reduce'; +import reduce from 'lodash-es/reduce'; import * as z from 'zod'; type Dataset = ScatterbrainDataset | SlideviewScatterbrainDataset; diff --git a/packages/scatterbrain/src/demo.html b/packages/scatterbrain/src/demo.html new file mode 100644 index 00000000..12405798 --- /dev/null +++ b/packages/scatterbrain/src/demo.html @@ -0,0 +1,32 @@ + + + + + + + hello webgpu + + + + + + + + + \ No newline at end of file diff --git a/packages/scatterbrain/src/demo.ts b/packages/scatterbrain/src/demo.ts new file mode 100644 index 00000000..89b3a245 --- /dev/null +++ b/packages/scatterbrain/src/demo.ts @@ -0,0 +1,111 @@ +/** biome-ignore-all lint/suspicious/noNonNullAssertedOptionalChain: */ +/** biome-ignore-all lint/style/noNonNullAssertion: */ + +import { SharedPriorityCache } from '@alleninstitute/vis-core'; +import { Box2D, type vec4 } from '@alleninstitute/vis-geometry'; +import { loadDataset } from './dataset'; +import { buildRenderFrameFn, type ShaderSettings } from './render/webgpu/renderer'; +import type { ScatterbrainDataset } from './types'; + +const tenx = + 'https://bkp-2d-visualizations-stage.s3.amazonaws.com/wmb_tenx_01172024_stage-20240128193624/G4I4GFJXJB9ATZ3PTX1/ScatterBrain.json'; + +async function loadRawJson() { + return await (await fetch(tenx)).json(); +} +const makeFakeColors = (n: number) => { + const stuff: Record = {}; + for (let i = 0; i < n; i++) { + stuff[i] = { + color: [Math.random(), Math.random(), Math.random(), 1], + // 80% of either category are filtered in, at random: + filteredIn: Math.random() > 0.2, + }; + } + return stuff; +}; + +export async function whatever() { + const gradientData = new Uint8Array(256 * 4); + for (let i = 0; i < 256; i += 4) { + gradientData[i * 4 + 0] = i; + gradientData[i * 4 + 1] = i; + gradientData[i * 4 + 2] = i; + gradientData[i * 4 + 3] = 255; + } + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter?.requestDevice()!; + // buildTest(root.device) + + const categories = { + '4MV7HA5DG2XJZ3UD8G9': makeFakeColors(40), // nt type + FS00DXV0T9R1X9FJ4QE: makeFakeColors(40), // class + }; + + const settings: Omit = { + categoricalFilters: { '4MV7HA5DG2XJZ3UD8G9': 40, FS00DXV0T9R1X9FJ4QE: 40 }, + colorBy: { kind: 'metadata', column: 'FS00DXV0T9R1X9FJ4QE' }, + // an alternative color-by setting, swap it to see quantitative coloring + // colorBy: { kind: 'quantitative', column: '27683', gradient: 'viridis', range: { min: 0, max: 10 } }, + mode: 'color', + quantitativeFilters: [], + highlightByColumn: { kind: 'metadata', column: 'FS00DXV0T9R1X9FJ4QE' }, + }; + + const dataset = await loadDataset(await loadRawJson()); + if (!dataset) { + throw new Error('blerg this data is toast'); + } + const cache = new SharedPriorityCache(new Map(), 1024 * 1024 * 2000); + const { render, connectToCache } = buildRenderFrameFn(device, { ...settings, dataset }); + + const cnvs: HTMLCanvasElement = document.getElementById('canvas') as HTMLCanvasElement; + cnvs.width = 1500; + cnvs.height = 1500; + const ctx = cnvs.getContext('webgpu'); + ctx?.configure({ + device: device, + format: navigator.gpu.getPreferredCanvasFormat(), + alphaMode: 'premultiplied', + }); + + const bound = (dataset as ScatterbrainDataset).metadata.tightBoundingBox; + const view = Box2D.create([bound.lx, bound.ly], [bound.ux, bound.uy]); + const client = connectToCache(cache, () => { + // redraw? + // console.log('new data arrived...') + requestAnimationFrame(() => { + // biome-ignore lint/suspicious/noConsole: + console.log('re render!'); + + render({ + categories, + client, + gradient: gradientData, + target: ctx!.getCurrentTexture().createView(), + uniforms: { + camera: { view, screenResolution: [1500, 1500] }, + filteredOutColor: [0.5, 0.5, 0.5, 1.0], + highlightedValue: 22, + offset: [0, 0], + quantitativeRangeFilters: {}, + spatialFilterBox: view, + }, + }); + }); + }); + render({ + categories, + client, + gradient: gradientData, + target: ctx!.getCurrentTexture().createView(), + uniforms: { + camera: { view, screenResolution: [1500, 1500] }, + filteredOutColor: [0.5, 0.5, 0.5, 1.0], + highlightedValue: 22, + offset: [0, 0], + quantitativeRangeFilters: {}, + spatialFilterBox: view, + }, + }); +} diff --git a/packages/scatterbrain/src/index.ts b/packages/scatterbrain/src/index.ts index ebdd683f..b9ef1d6d 100644 --- a/packages/scatterbrain/src/index.ts +++ b/packages/scatterbrain/src/index.ts @@ -1,8 +1,6 @@ export { buildScatterbrainCacheClient } from './cache-client'; export { getVisibleItems, loadDataset as loadScatterbrainDataset } from './dataset'; -export { - buildRenderFrameFn as buildScatterbrainRenderFn, - setCategoricalLookupTableValues, - updateCategoricalValue, -} from './renderer'; +export * from './render/webgl/index'; +export * from './render/webgpu/index'; export * from './types'; + diff --git a/packages/scatterbrain/src/render/webgl/index.ts b/packages/scatterbrain/src/render/webgl/index.ts new file mode 100644 index 00000000..21acecef --- /dev/null +++ b/packages/scatterbrain/src/render/webgl/index.ts @@ -0,0 +1,8 @@ + +// because the webGL and webGPU implementations of these renderers are very similar, +// they end up having identical names for the same conceptual parts - +// so lets export them namespaced +import * as WGL from './renderer' +export const WebGL = { + ...WGL +} \ No newline at end of file diff --git a/packages/scatterbrain/src/renderer.ts b/packages/scatterbrain/src/render/webgl/renderer.ts similarity index 94% rename from packages/scatterbrain/src/renderer.ts rename to packages/scatterbrain/src/render/webgl/renderer.ts index 7fa79cb9..6b0cedd4 100644 --- a/packages/scatterbrain/src/renderer.ts +++ b/packages/scatterbrain/src/render/webgl/renderer.ts @@ -1,13 +1,13 @@ import type { SharedPriorityCache } from '@alleninstitute/vis-core'; import { Box2D, type box2D, type vec4 } from '@alleninstitute/vis-geometry'; -import keys from 'lodash/keys'; -import reduce from 'lodash/reduce'; +import keys from 'lodash-es/keys'; +import reduce from 'lodash-es/reduce'; import type REGL from 'regl'; -import { buildScatterbrainCacheClient } from './cache-client'; -import { getVisibleItems, type NodeWithBounds } from './dataset'; +import { buildScatterbrainCacheClient } from '../../cache-client'; +import { getVisibleItems, type NodeWithBounds } from '../../dataset'; import { buildScatterbrainRenderCommand, type Config, configureShader, type ShaderSettings, VBO } from './shader'; -import { MakeTaggedBufferView } from './typed-array'; -import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset, TreeNode } from './types'; +import { MakeTaggedBufferView } from '../../typed-array'; +import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset, TreeNode } from '../../types'; export type Item = Readonly<{ dataset: SlideviewScatterbrainDataset | ScatterbrainDataset; @@ -149,7 +149,7 @@ export function buildRenderFrameFn(regl: REGL.Regl, settings: ShaderSettings) { } }; const connectToCache = (cache: SharedPriorityCache, onDataArrived: () => void) => { - const client = buildScatterbrainCacheClient( + const client = buildScatterbrainCacheClient( cache, (buff, type) => { const typed = MakeTaggedBufferView(type, buff); @@ -159,7 +159,7 @@ export function buildRenderFrameFn(regl: REGL.Regl, settings: ShaderSettings) { type: 'buffer', }); }, - onDataArrived + onDataArrived, ); return client; }; diff --git a/packages/scatterbrain/src/shader.test.ts b/packages/scatterbrain/src/render/webgl/shader.test.ts similarity index 99% rename from packages/scatterbrain/src/shader.test.ts rename to packages/scatterbrain/src/render/webgl/shader.test.ts index 948f9777..0b7d0452 100644 --- a/packages/scatterbrain/src/shader.test.ts +++ b/packages/scatterbrain/src/render/webgl/shader.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; import { buildShaders, type Config, configureShader } from './shader'; -import type { ScatterbrainDataset } from './types'; +import type { ScatterbrainDataset } from '../../types'; const tenx: ScatterbrainDataset = { type: 'normal', diff --git a/packages/scatterbrain/src/shader.ts b/packages/scatterbrain/src/render/webgl/shader.ts similarity index 97% rename from packages/scatterbrain/src/shader.ts rename to packages/scatterbrain/src/render/webgl/shader.ts index bb314a5a..a862a304 100644 --- a/packages/scatterbrain/src/shader.ts +++ b/packages/scatterbrain/src/render/webgl/shader.ts @@ -1,10 +1,10 @@ /** biome-ignore-all lint/style/noUnusedTemplateLiteral: not at all helpful*/ import type REGL from 'regl'; -import type { ScatterbrainDataset, SlideviewScatterbrainDataset } from './types'; -import type { Cacheable, CachedVertexBuffer } from '@alleninstitute/vis-core'; +import type { ScatterbrainDataset, SlideviewScatterbrainDataset } from '../../types'; +import type { CachedVertexBuffer, Cacheable } from '@alleninstitute/vis-core'; import { Box2D, type box2D, type Interval, type vec2, type vec4 } from '@alleninstitute/vis-geometry'; -import * as lodash from 'lodash'; +import * as lodash from 'lodash-es'; const { keys, mapValues, reduce } = lodash; // the set of columns and what to do with them can vary @@ -27,7 +27,7 @@ const { keys, mapValues, reduce } = lodash; // patterns we've seen in our shaders so far! you could easily generate your own // totally custom shaders! -type ScatterbrainShaderUtils = { +export type ScatterbrainShaderUtils = { uniforms: string; // the GLSL declarations of the uniforms for this shader attributes: string; // the GLSL declarations of the vertex attributes for this shader commonUtilsGLSL: string; // prepend any GLSL to the final vertex shader @@ -309,8 +309,8 @@ export function generate(config: Config): ScatterbrainShaderUtils { return mix(filteredOutColor,${colorize},isFilteredIn()); ` : categoryColumnIndex === -1 - ? colorByQuantitativeValue - : colorByCategoricalId; + ? colorByQuantitativeValue + : colorByCategoricalId; return { attributes, uniforms, @@ -332,8 +332,8 @@ export type ShaderSettings = { quantitativeFilters: readonly string[]; // the names of quantitative variables mode: 'color' | 'info'; colorBy: - | { kind: 'metadata'; column: string } - | { kind: 'quantitative'; column: string; gradient: 'viridis' | 'inferno'; range: Interval }; + | { kind: 'metadata'; column: string } + | { kind: 'quantitative'; column: string; gradient: 'viridis' | 'inferno'; range: Interval }; }; export function configureShader(settings: ShaderSettings): { diff --git a/packages/scatterbrain/src/render/webgpu/index.ts b/packages/scatterbrain/src/render/webgpu/index.ts new file mode 100644 index 00000000..0ebae8a9 --- /dev/null +++ b/packages/scatterbrain/src/render/webgpu/index.ts @@ -0,0 +1,8 @@ + +// because the webGL and webGPU implementations of these renderers are very similar, +// they end up having identical names for the same conceptual parts - +// so lets export them namespaced +import * as WGPU from './renderer' +export const WebGPU = { + ...WGPU +} \ No newline at end of file diff --git a/packages/scatterbrain/src/render/webgpu/lookup-texture.ts b/packages/scatterbrain/src/render/webgpu/lookup-texture.ts new file mode 100644 index 00000000..01057898 --- /dev/null +++ b/packages/scatterbrain/src/render/webgpu/lookup-texture.ts @@ -0,0 +1,100 @@ +import type { vec4 } from '@alleninstitute/vis-geometry'; + +/** + * a helper function that MUTATES ALL the values in the given @param texture + * to set them to the color and filter status as given in the categories record + * note that the texture's maping to categories is based on a lexical sorting of the names of the + * categories + * @param categories + * @param regl + * @param texture + */ +export function setCategoricalLookupTableValues( + categories: Record>, + device: GPUDevice, + texture: GPUTexture, +) { + const bytesPerPixel = 4; // rgba8 + const categoryKeys = Object.keys(categories).toSorted(); + const columns = categoryKeys.length; + const rows = categoryKeys.reduce((highest, category) => Math.max(highest, + Object.keys(categories[category]).length), 1); + const data = new Uint8Array(columns * rows * 4); + const rgbf = [0, 0, 0, 0]; + const empty = [0, 0, 0, 0] as const; + if (texture.width !== columns || texture.height !== rows) { + if (texture) { + texture.destroy(); + } + // create a texture! + // biome-ignore lint/style/noParameterAssign: + texture = device.createTexture({ + format: 'rgba8unorm', + size: { width: columns, height: rows }, + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, + }); + } + // write the rgb of the color, and encode the filter boolean into the alpha channel + for (let columnIndex = 0; columnIndex < columns; columnIndex += 1) { + const category = categories[categoryKeys[columnIndex]]; + const nRows = Object.keys(category).length; + for (let rowIndex = 0; rowIndex < nRows; rowIndex += 1) { + const color = category[rowIndex]?.color ?? empty; + const filtered = category[rowIndex]?.filteredIn ?? false; + rgbf[0] = color[0] * 255; + rgbf[1] = color[1] * 255; + rgbf[2] = color[2] * 255; + rgbf[3] = filtered ? 255 : 0; + data.set(rgbf, rowIndex * columns * 4 + columnIndex * 4); + } + } + device.queue.writeTexture( + { texture }, + data, + { bytesPerRow: columns * bytesPerPixel, rowsPerImage: rows }, + { + width: columns, + height: rows, + }, + ); + return texture; +} + +/** + * same as setCategoricalLookupTableValues, except it only writes a single value update to the texture. + * note that the list of categories given must match those used to construct the texture, and are needed here + * due to the lexical sorting order determining the column order of the @param texture + * @param categories + * @param update + * @param regl + * @param texture + */ +export function updateCategoricalValue( + categories: readonly string[], + update: { category: string; row: number; color: vec4; filteredIn: boolean }, + device: GPUDevice, + texture: GPUTexture, +) { + const { category, row, color, filteredIn } = update; + const col = categories.toSorted().indexOf(category); + if (texture.width <= col || texture.height <= row || row < 0 || col < 0) { + // todo - it might be better to let regl throw the same error... think about it + throw new Error( + `attempted to update metadata lookup table with invalid coordinates: row=${row},col=${col} is not within ${texture.width}, ${texture.height}`, + ); + } + const data = new Uint8Array(4); + data[0] = color[0] * 255; + data[1] = color[1] * 255; + data[2] = color[2] * 255; + data[3] = filteredIn ? 255 : 0; + device.queue.writeTexture( + { texture, origin: { x: col, y: row } }, + data, + { bytesPerRow: 4, rowsPerImage: 1 }, + { + width: 1, + height: 1, + }, + ); +} diff --git a/packages/scatterbrain/src/render/webgpu/renderer.ts b/packages/scatterbrain/src/render/webgpu/renderer.ts new file mode 100644 index 00000000..be465879 --- /dev/null +++ b/packages/scatterbrain/src/render/webgpu/renderer.ts @@ -0,0 +1,239 @@ +/** biome-ignore-all lint/performance/noAccumulatingSpread: leave me be */ +import type { Cacheable, SharedPriorityCache } from '@alleninstitute/vis-core'; +import { Box2D, type box2D, type vec2, type vec4 } from '@alleninstitute/vis-geometry'; +import { buildScatterbrainCacheClient } from '~/src/cache-client'; +import { getVisibleItems, type NodeWithBounds } from '~/src/dataset'; +import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset, WebGLSafeBasicType } from '~/src/types'; +import { buildPipeline, type Config, type Uniforms } from './shader'; +import { beginValidate, endValidate } from './validate'; + +export { setCategoricalLookupTableValues, updateCategoricalValue } from './lookup-texture'; + + +export type Head> = T extends readonly [] ? never : T[0]; +export type Tail> = T extends readonly [infer _I, ...infer rest] ? rest : never; + +export type OR> = T extends readonly [infer K] ? K : Head | OR>; + +export type ShaderSettings = { + dataset: ScatterbrainDataset | SlideviewScatterbrainDataset; + categoricalFilters: Record; // category name -> maximum # of distinct values in that category + quantitativeFilters: readonly string[]; // the names of quantitative variables + mode: 'color' | 'info'; + colorBy: + | { kind: 'metadata'; column: string } + | { kind: 'quantitative'; column: string }; + highlightByColumn: { kind: 'quantitative' | 'metadata'; column: string }; +}; + +export class VBO implements Cacheable { + constructor(readonly buffer: GPUBuffer) { } + destroy() { + this.buffer.destroy(); + } + sizeInBytes() { + return this.buffer.size; + } +} + +function columnsForItem( + config: Config, + col2shader: Record, + dataset: ScatterbrainDataset | SlideviewScatterbrainDataset, +) { + const columns: Record = {}; + const s2c = Object.keys(col2shader).reduce( + (acc, col) => ({ ...acc, [col2shader[col]]: col }), + {} as Record, + ); + + for (const c of config.categoricalColumns) { + columns[c] = { type: 'METADATA', name: s2c[c] }; + } + for (const m of config.quantitativeColumns) { + columns[m] = { type: 'QUANTITATIVE', name: s2c[m] }; + } + columns[config.positionColumn] = { type: 'METADATA', name: dataset.metadata.spatialColumn }; + return (item: T) => { + return { ...item, dataset, columns }; + }; +} + +export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) { + const { dataset } = settings; + const { config, columnNameToShaderName } = configureShader(settings); + const { pipeline, makeUniformBuffer, updateUniforms, uniformSize } = buildPipeline(device, config); + const prepareQtCell = columnsForItem(config, columnNameToShaderName, dataset); + + const toGpuBuffer = (buffer: ArrayBuffer, type: WebGLSafeBasicType) => { + if (type === 'uint16') { + // seems like uint16 is cursed - vertex buffers have to have a stride of at least 4... + const B = device.createBuffer({ + size: buffer.byteLength * 2, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + }); + // now we have to copy the uint16 buffer and sorta kinda expand each value... + const u32 = new Uint32Array(new Uint16Array(buffer)); + device.queue.writeBuffer(B, 0, u32.buffer); + return new VBO(B); + } + const B = device.createBuffer({ + size: buffer.byteLength, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + }); + device.queue.writeBuffer(B, 0, buffer, 0, buffer.byteLength); + return new VBO(B); + }; + const connectToCache = (cache: SharedPriorityCache, onDataArrived: () => void) => { + const client = buildScatterbrainCacheClient(cache, toGpuBuffer, onDataArrived); + return client; + }; + const unis = makeUniformBuffer(); + const ubo = device.createBuffer({ + size: uniformSize, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, + label: 'scatterbrin uniform buffer', + }); + const render = (props: RenderPassProps & { client: ReturnType> }) => { + const { target, camera, offset, filteredOutColor, spatialFilterBox, quantitativeRangeFilters, highlightedValue, client, + categoricalLookupTable, gradient } = props; + + + const uniforms: Uniforms = { + view: Box2D.toFlatArray(camera.view), + offset, + filteredOutColor, + highlightColor: [1, 1, 0, 1], + screenSize: camera.screenResolution, + spatialFilterBox: Box2D.toFlatArray(spatialFilterBox), + highlightValue: highlightedValue, + ...quantitativeRangeFilters + } + beginValidate(device); + + updateUniforms(uniforms, unis); + // TODO there will be a big bug here: + // in the REGL mental model, uniform values are tied up with the very notion of a draw call + // here - if we want to draw(...) a bit, and then change uniforms and draw(...more...) + // TLDR there is no way to do that which does not require a pre-allocated buffer of + // uniform buffer objects - although we could spare some memory by making a seprate bind-group for just the things that can change per node... + // (so far, that would be nodeDepth and offset (for slideview)) + // I... could use dynamic bind-group offsets (https://webgpufundamentals.org/webgpu/lessons/webgpu-bind-group-layouts.html) + // however the offset in question has to be a multiple of 256... thats a lot of bytes, so its a bit overkill for just the nodeDepth and slideOffset! + // ugh, it also requires bindgroup layouts in non-auto mode - such a slog + // ok - seems like the most normal-person thing to do here would be to split out the per-qt-node and the per-frame uniforms into 2 groups + // then create the per-qt-node data when we load it, and stow it in the cache... that would be ok, although it does require + // a bindGroupLayout (non-automode) separate from the creation of the pipeline... thats gonna be a good idea anyway if anyone ever changes any settings for these... + device.queue.writeBuffer(ubo, 0, unis.arrayBuffer); + const entries: GPUBindGroupEntry[] = [{ binding: 0, resource: ubo }]; + if (Object.keys(Object.keys(settings.categoricalFilters)).length > 0) { + entries.push({ binding: 1, resource: categoricalLookupTable }); + } + if (Object.keys(quantitativeRangeFilters).length > 0) { + entries.push({ binding: 2, resource: gradient }); + } + const bg = device.createBindGroup({ + label: 'single bg', + entries, + layout: pipeline.getBindGroupLayout(0), + }); + + const enc = device.createCommandEncoder({ label: 'encoder for scatterbrain render pass' }); + const pass = enc.beginRenderPass({ + colorAttachments: [ + { + clearValue: [0, 0, 0.15, 1], + loadOp: 'clear', + storeOp: 'store', + view: target, + }, + ], + }); + pass.setPipeline(pipeline); + + pass.setBindGroup(0, bg); + + // now - actually start submitting stuff + const visible = getVisibleItems(dataset, camera, 0.1).map(prepareQtCell); + client.setPriorities(visible, []); + for (const node of visible) { + if (client.has(node)) { + const drawable = client.get(node); + if (drawable) { + const columns = drawable; + const count = node.node.numSpecimens; + for (let i = 0; i < config.vertexLocationOrder.length; i++) { + pass.setVertexBuffer(i, columns[config.vertexLocationOrder[i]].buffer); + } + pass.draw(4, count); + + } + } + } + pass.end(); + device.queue.submit([enc.finish()]); + endValidate(device); + }; + return { render, connectToCache, makeUniformBuffer, pipeline, updateUniforms }; +} + +export type RenderPassProps = { + target: GPUTextureView; + + camera: { view: box2D; screenResolution: vec2 }; + offset: vec2; + filteredOutColor: vec4; + spatialFilterBox: box2D; + quantitativeRangeFilters: Record; + highlightedValue: number; + categoricalLookupTable: GPUTextureView; + gradient: GPUTextureView; +}; + +export function configureShader(settings: ShaderSettings): { + config: Config; + columnNameToShaderName: Record; +} { + // given settings that make sense to a caller (stuff about the data we want to visualize) + // produce an object that can be used to set up some internal config of the shader that would + // do the visualization + const { dataset, categoricalFilters, quantitativeFilters, colorBy, mode, highlightByColumn } = settings; + // figure out the columns we care about + // assign them names that are safe to use in the shader (A,B,C, whatever) + const categories = Object.keys(categoricalFilters).toSorted(); + + // the goal here is to associate column names with shader-safe names + const initialQuantitativeAttrs: Record = + colorBy.kind === 'metadata' ? {} : { [colorBy.column]: 'COLOR_BY_MEASURE' }; + const initialCategoricalAttrs: Record = + colorBy.kind === 'metadata' ? { [colorBy.column]: 'COLOR_BY_CATEGORY' } : {}; + // we map each quantitative filter name to the shader-safe attribute name: MEASURE_{i} + const qAttrs = quantitativeFilters.toSorted().reduce( + (quantAttrs, quantFilter, i) => ({ ...quantAttrs, [quantFilter]: `MEASURE_${i.toFixed(0)}` }), + initialQuantitativeAttrs, + ); + // we map each categorical filter's name to the shader-safe attribute name: CATEGORY_{i} + const cAttrs = categories.reduce( + (catAttrs, categoricalFilter, i) => ({ ...catAttrs, [categoricalFilter]: `CATEGORY_${i.toFixed(0)}` }), + initialCategoricalAttrs, + ); + + const colToAttribute = { + ...qAttrs, + ...cAttrs, + [dataset.metadata.spatialColumn]: 'position', + }; + const ordered = [...categories, ...quantitativeFilters.toSorted()].map((col) => colToAttribute[col]); + const config: Config = { + categoricalColumns: Object.keys(cAttrs).map((columnName) => colToAttribute[columnName]), + quantitativeColumns: Object.keys(qAttrs).map((columnName) => colToAttribute[columnName]), + categoricalTable: 'lookup', + gradientTable: 'gradient', + colorByColumn: colToAttribute[colorBy.column], + mode, + positionColumn: 'position', + highlightByColumn: { ...highlightByColumn, column: colToAttribute[highlightByColumn.column] }, + vertexLocationOrder: ['position', ...ordered], + }; + return { config, columnNameToShaderName: colToAttribute }; +} diff --git a/packages/scatterbrain/src/render/webgpu/shader.ts b/packages/scatterbrain/src/render/webgpu/shader.ts new file mode 100644 index 00000000..10d28fea --- /dev/null +++ b/packages/scatterbrain/src/render/webgpu/shader.ts @@ -0,0 +1,298 @@ +import { beginValidate, endValidate } from './validate'; +import * as wgh from 'webgpu-utils'; +import type { vec2, vec4 } from '@alleninstitute/vis-geometry'; +import { + $a, + constant, + fragmentEntry, + func, + location, + member, + param, + returns, + shader, + struct, + texture, + uniform, + vertexEntry, +} from './shaders'; + +function rangeFor(col: string): `${string}_range` { + return `${col}_range`; +} +function rangeFilterExpression(quantitativeColumns: readonly string[]) { + return quantitativeColumns.map((attrib) => /*wgsl*/ `within(v.${attrib},unis.${rangeFor(attrib)})`).join(' * '); +} +function categoricalFilterExpression(categoricalColumns: readonly string[], tableName: string) { + // categorical columns are in order - this array will have the same order as the col in the texture + return categoricalColumns + .map((attrib, i) => /*wgsl*/ `step(0.01,textureLoad(${tableName}, vec2u(${i.toFixed(0)},v.${attrib}),0).a)`) + .join(' * '); +} + +export type Config = { + mode: 'color' | 'info'; + quantitativeColumns: string[]; + categoricalColumns: string[]; + categoricalTable: string; + gradientTable: string; + positionColumn: string; + colorByColumn: string; + highlightByColumn: { kind: 'quantitative' | 'metadata'; column: string }; + vertexLocationOrder: string[]; +}; + +type QuantitativeFilterRanges = Record<`${string}_range`, vec2>; + +// the type of the uniforms on the TS side of the fence +export type Uniforms = { + view: vec4; + spatialFilterBox: vec4; + filteredOutColor: vec4; + highlightColor: vec4; + screenSize: vec2; + offset: vec2; + highlightValue: number; +} & QuantitativeFilterRanges; + +export const applyCamera = func( + 'applyCamera', + [param('dataPos', 'vec2f'), param('view', 'vec4f')], + /*wgsl*/ ` + let size = view.zw-view.xy; + let unit = (dataPos.xy-view.xy)/size; + return vec4f((unit*2.0)-1.0,0.0,1.0); + `, + returns('vec4f') +); + +export const rangeParameter = func( + 'rangeParameter', + [param('v', 'f32'), param('range', 'vec2f')], + /*wgsl*/ ` + return (v-range.x)/(range.y-range.x); + `, + returns('f32') +); + +export const within = func( + 'within', + [param('v', 'f32'), param('range', 'vec2f')], + /*wgsl*/ ` + return step(range.x,v)*step(v,range.y); + `, + returns('f32') +); + +const makeVertexStruct = (config: Config) => { + const { positionColumn, categoricalColumns, quantitativeColumns } = config; + const catStart = 1; + const quantStart = catStart + categoricalColumns.length; + return struct('Vertex', [ + member('vIndex', 'u32', [$a.builtin('vertex_index')]), + member(positionColumn, 'vec2f', [$a.location(0)]), + ...categoricalColumns.map((col, i) => member(col, 'u32', [$a.location(i + catStart)])), + ...quantitativeColumns.map((col, i) => member(col, 'f32', [$a.location(i + quantStart)])), + ]); +}; + +const vsOutputStruct = struct('VsOutput', [ + member('position', 'vec4f', [$a.builtin('position')]), + member('color', 'vec4f', [$a.location(0)]), +]); + +const makeUniformStruct = (config: Config) => { + const { quantitativeColumns } = config; + return struct('Uniforms', [ + member('view', 'vec4f'), + member('spatialFilterBox', 'vec4f'), + member('filteredOutColor', 'vec4f'), + member('highlightColor', 'vec4f'), + member('screenSize', 'vec2f'), + member('offset', 'vec2f'), + member('highlightValue', 'u32'), + ...quantitativeColumns.map((col) => member(rangeFor(col), 'vec2f')), + ]); +}; + +export function generate(config: Config): string { + const { + mode: _mode, + quantitativeColumns, + categoricalColumns, + categoricalTable, + gradientTable, + positionColumn, + colorByColumn, + highlightByColumn, + } = config; + const catFilter = categoricalFilterExpression(categoricalColumns, categoricalTable); + const rangeFilter = rangeFilterExpression(quantitativeColumns); + + const categoryColumnIndex = categoricalColumns.indexOf(colorByColumn); + + const colorByCategorical = /*wgsl*/ ` + vec4(textureLoad(${categoricalTable},vec2u(${categoryColumnIndex.toFixed(0)},v.${colorByColumn}),0).rgb,1.0)`; + + const colorByQuantitative = /*wgsl*/ ` + textureLoad(${gradientTable},vec2u(vec2(rangeParameter(${colorByColumn},unis.${rangeFor(colorByColumn)})*f32(textureDimensions(${gradientTable}).x),0.0)),0) + `; + const colorize = categoryColumnIndex !== -1 ? colorByCategorical : colorByQuantitative; + + const vertexStruct = makeVertexStruct(config); + const uniformStruct = makeUniformStruct(config); + + return shader([ + vertexStruct, + uniformStruct, + vsOutputStruct, + uniform('unis', uniformStruct.name, 0, 0), + texture(categoricalTable, 'texture_2d', 0, 1), + texture(gradientTable, 'texture_2d', 0, 2), + applyCamera, + rangeParameter, + within, + constant('clip', /*wgsl*/ `array(vec2f(1, -1), vec2f(1, 1), vec2f(-1, -1), vec2f(-1, 1))`), + vertexEntry( + 'vmain', + [param('v', vertexStruct.name)], + /*wgsl*/ ` + var out: ${vsOutputStruct.name}; + + // lets directly compute stuff, rather than helper functions + // this might be what people want with tgpu - much easier to synthesize a shader + // but also crazy annoying in its own way I think... + let p = v.${positionColumn}; + let withinFilterBox = within(p.x,unis.spatialFilterBox.xz)*within(p.y,unis.spatialFilterBox.yw); + let filteredIn: f32 = withinFilterBox * + ${catFilter.length > 0 ? catFilter : '1.0'} + * ${rangeFilter.length > 0 ? rangeFilter : '1.0'}; + + // highlighting + let highlighted = 1.0-step(0.1,abs(f32(v.${highlightByColumn.column}-unis.highlightValue))); + + // from filtering, we can compute color + let baseColor = ${colorize}; + let clr = mix(unis.filteredOutColor, baseColor, filteredIn); + + // point size (todo make this a uniform...) + // todo: handle offset (slides) + let R = 0.02; + let dPos = clip[v.vIndex]*R + p; + out.color = clr; + out.position = applyCamera(dPos,unis.view); + return out; + `, + returns(vsOutputStruct.name) + ), + fragmentEntry( + 'fmain', + [param('v', vsOutputStruct.name)], + /*wgsl*/ `return v.color;`, + returns('vec4f', [location(0)]) + ), + ]).asSource(); +} +function generateVertexBufferLayout(config: Config) { + // position at 0 + // then categorical + // then quant + // note that colorBy must be in either quantitative or categorical... + // then highlightBy + const { categoricalColumns, quantitativeColumns } = config; + const catStart = 1; + const quantStart = catStart + categoricalColumns.length; + const what: GPUVertexBufferLayout[] = [ + { + arrayStride: 8, // xy floats + stepMode: 'instance', + attributes: [ + { + shaderLocation: 0, + format: 'float32x2', + offset: 0, + }, + ], + }, + ...categoricalColumns.map( + (_cat, i): GPUVertexBufferLayout => ({ + arrayStride: 4, + attributes: [ + { + format: 'uint32', + offset: 0, + shaderLocation: catStart + i, + }, + ], + stepMode: 'instance', + }) + ), + ...quantitativeColumns.map( + (_q, i): GPUVertexBufferLayout => ({ + arrayStride: 4, + attributes: [ + { + format: 'float32', + offset: 0, + shaderLocation: quantStart + i, + }, + ], + stepMode: 'instance', + }) + ), + ]; + return what; +} +export function buildPipeline(device: GPUDevice, config: Config) { + const shader = generate(config); + beginValidate(device); + const module = device.createShaderModule({ + code: shader, + label: 'scatterbrain shader mod', + }); + const defs = wgh.makeShaderDataDefinitions(shader); + const vertexLayout = generateVertexBufferLayout(config); + const blend: GPUBlendState = { + alpha: { + operation: 'add', + srcFactor: 'one', + dstFactor: 'one', + }, + color: { + operation: 'add', + srcFactor: 'src-alpha', + dstFactor: 'one-minus-src-alpha', + }, + }; //TODO generate blendmode settings from config + const pipeline = device.createRenderPipeline({ + layout: 'auto', + vertex: { + module, + buffers: vertexLayout, + entryPoint: 'vmain', + }, + fragment: { + module, + entryPoint: 'fmain', + targets: [ + { + format: 'bgra8unorm', + blend, + }, + ], + }, + primitive: { + topology: 'triangle-strip', + }, + }); + endValidate(device); + + // make a buffer for the uniforms, and a little utility to update it + + const { size } = defs.uniforms['unis']; + const makeUniformBuffer = () => wgh.makeStructuredView(defs.uniforms.unis); + const updateUniforms = (updates: Partial, view: ReturnType) => { + view.set(updates); + }; + return { pipeline, makeUniformBuffer, updateUniforms, uniformSize: size }; +} diff --git a/packages/scatterbrain/src/render/webgpu/shaders/attributes.ts b/packages/scatterbrain/src/render/webgpu/shaders/attributes.ts new file mode 100644 index 00000000..7900c7fe --- /dev/null +++ b/packages/scatterbrain/src/render/webgpu/shaders/attributes.ts @@ -0,0 +1,338 @@ +/** + * This file defines TypeScript types and helper functions for representing WGSL shader attributes in a type-safe way. + * Each attribute is represented as an object with a specific shape, and includes a __gen method that generates the + * corresponding WGSL syntax for that attribute. The file also includes type guards for each attribute type, as well as + * constructors that validate input and create the attribute objects. + * + * Summary of Attributes: + * - align(x: i32 | u32 that is a power of 2 > 0) [can only be applied to a member of a struct] + * - binding(num >= 0) [can only be applied to a Resource variable] + * - blend_src(0 | 1) [only valid in specific feature-triggered scenarios; must be on a struct member with @location] + * - builtin(builtin-name) [only valid on a struct member, entrypoint argument, or entrypoint return type] + * - const [only allowed on non-user-defined functions; not relevant to our use case] + * - diagnostic(ShaderSeverityControlName, string) + * - group(num >= 0) [can only be applied to a Resource variable] + * - id(num >= 0) [can only be applied to an override variable with a scalar type] + * - interpolate(ShaderIntroplationType, ShaderInterpolationSamplingType?) [can only be applied to declarations with a @location attribute] + * - invariant [can only be applied to a @builtin(position) declaration; only has effect if applied to vertex position output] + * - location(num >= 0) [structure members or entrypoint inputs/outputs only; numeric scalar or vector declarations only; not allowed in compute shaders] + * - must_use [function declarations with return types only] + * - size(num >= 1) [only applicable to struct members with a size known at shader creation time] + * - workgroup_size(x: u32 >= 1, [y?: u32 >= 1, [z?: u32 >= 1]]) [only on compute shader entry points] + * + * Shader Stage indicator attributes: + * - vertex + * - fragment + * - compute + */ + +/// TYPES + +type ShaderSeverityControlName = 'error' | 'warning' | 'info' | 'off'; +const SHADER_SEVERITY_CONTROL_NAMES: ShaderSeverityControlName[] = ['error', 'warning', 'info', 'off']; + +type ShaderIntroplationType = 'perspective' | 'linear' | 'flat'; +const SHADER_INTERPOLATION_TYPES: ShaderIntroplationType[] = ['perspective', 'linear', 'flat']; + +type ShaderInterpolationSamplingType = 'center' | 'centroid' | 'sample' | 'first' | 'either'; +const SHADER_INTERPOLATION_SAMPLING_TYPES: ShaderInterpolationSamplingType[] = [ + 'center', + 'centroid', + 'sample', + 'first', + 'either', +]; + +type ShaderBuiltins = + | 'clip_distances' + | 'frag_depth' + | 'front_facing' + | 'global_invocation_id' + | 'global_invocation_index' + | 'instance_index' + | 'local_invocation_id' + | 'local_invocation_index' + | 'num_workgroups' + | 'position' + | 'primitive_index' + | 'sample_index' + | 'sample_mask' + | 'vertex_index' + | 'workgroup_id' + | 'workgroup_index' + | 'subgroup_invocation_id' + | 'subgroup_size' + | 'subgroup_id' + | 'num_subgroups'; + +const SHADER_BUILTINS: ShaderBuiltins[] = [ + 'clip_distances', + 'frag_depth', + 'front_facing', + 'global_invocation_id', + 'global_invocation_index', + 'instance_index', + 'local_invocation_id', + 'local_invocation_index', + 'num_workgroups', + 'position', + 'primitive_index', + 'sample_index', + 'sample_mask', + 'vertex_index', + 'workgroup_id', + 'workgroup_index', + 'subgroup_invocation_id', + 'subgroup_size', + 'subgroup_id', + 'num_subgroups', +]; + +export type DeclarationAttribute = { + __gen: () => string; +}; + +export type AlignAttribute = DeclarationAttribute & { + align: number; +}; + +/* + * NOTE: The `binding` attribute is intentionally omitted from the public API for now, + * as its usage is handled by setting the `binding` property on a Resource variable + * declaration. + **/ +// export type BindingAttribute = DeclarationAttribute & { +// binding: number; +// }; + +export type BlendSrcAttribute = DeclarationAttribute & { + blend_src: 0 | 1; +}; + +export type BuiltinAttribute = DeclarationAttribute & { + builtin: ShaderBuiltins; +}; + +export type ConstAttribute = DeclarationAttribute & { + const: true; +}; + +export type DiagnosticAttribute = DeclarationAttribute & { + diagnostic: [ShaderSeverityControlName, string]; +}; + +/* + * NOTE: The `group` attribute is intentionally omitted from the public API for now, + * as its usage is handled by setting the `group` property on a Resource variable + * declaration. + **/ +// export type GroupAttribute = DeclarationAttribute & { +// group: number; +// }; + +export type IdAttribute = DeclarationAttribute & { + id: number; +}; + +export type InterpolateAttribute = DeclarationAttribute & { + interpolate: [ShaderIntroplationType, ShaderInterpolationSamplingType?]; +}; + +export type InvariantAttribute = DeclarationAttribute & { + invariant: true; +}; + +export type LocationAttribute = DeclarationAttribute & { + location: number; +}; + +export type MustUseAttribute = DeclarationAttribute & { + must_use: true; +}; + +export type SizeAttribute = DeclarationAttribute & { + size: number; +}; + +export type WorkgroupSizeAttribute = DeclarationAttribute & { + workgroup_size: [number] | [number, number] | [number, number, number]; +}; + +export type VertexAttribute = DeclarationAttribute & { + vertex: true; +}; + +export type FragmentAttribute = DeclarationAttribute & { + fragment: true; +}; + +export type ComputeAttribute = DeclarationAttribute & { + compute: true; +}; + +export type VariableOrValueAttribute = + | AlignAttribute + | BlendSrcAttribute + | BuiltinAttribute + | DiagnosticAttribute + | IdAttribute + | InterpolateAttribute + | InvariantAttribute + | LocationAttribute + | SizeAttribute + | WorkgroupSizeAttribute; + +export type FunctionAttribute = + | ConstAttribute + | MustUseAttribute + | VertexAttribute + | FragmentAttribute + | ComputeAttribute; + +/// CONSTRUCTORS + +export function align(n: number): AlignAttribute { + if (n <= 0 || (n & (n - 1)) !== 0) { + throw new Error('Alignment must be a positive power of 2'); + } + return { align: n, __gen: () => `@align(${n})` }; +} + +/* + * NOTE: The `binding` attribute is intentionally omitted from the public API for now, + * as its usage is handled by setting the `binding` property on a Resource variable + * declaration. + **/ +// export function binding(n: number): BindingAttribute { +// if (n < 0) { +// throw new Error('Binding number must be a non-negative integer'); +// } +// return { binding: n, __gen: () => `@binding(${n})` }; +// } + +export function blendSrc(value: 0 | 1): BlendSrcAttribute { + if (value !== 0 && value !== 1) { + throw new Error('blend_src value must be either 0 or 1'); + } + return { blend_src: value, __gen: () => `@blend_src(${value})` }; +} + +export function builtin(name: ShaderBuiltins): BuiltinAttribute { + if (!SHADER_BUILTINS.includes(name)) { + throw new Error(`Invalid builtin name: ${name}`); + } + return { builtin: name, __gen: () => `@builtin(${name})` }; +} + +export function constAttr(): ConstAttribute { + return { const: true, __gen: () => `@const` }; +} + +export function diagnostic(severity: ShaderSeverityControlName, message: string): DiagnosticAttribute { + if (!SHADER_SEVERITY_CONTROL_NAMES.includes(severity)) { + throw new Error(`Invalid shader severity control name: ${severity}`); + } + if (typeof message !== 'string' || message.length === 0) { + throw new Error('Diagnostic message must be a non-empty string'); + } + return { diagnostic: [severity, message], __gen: () => `@diagnostic(${severity}, "${message}")` }; +} + +/* + * NOTE: The `group` attribute is intentionally omitted from the public API for now, + * as its usage is handled by setting the `group` property on a Resource variable + * declaration. + **/ +// export function group(n: number): GroupAttribute { +// if (n < 0) { +// throw new Error('Group number must be a non-negative integer'); +// } +// return { group: n, __gen: () => `@group(${n})` }; +// } + +export function id(n: number): IdAttribute { + if (n < 0) { + throw new Error('ID number must be a non-negative integer'); + } + return { id: n, __gen: () => `@id(${n})` }; +} + +export function interpolate( + type: ShaderIntroplationType, + samplingType?: ShaderInterpolationSamplingType +): InterpolateAttribute { + if (!SHADER_INTERPOLATION_TYPES.includes(type)) { + throw new Error(`Invalid interpolation type: ${type}`); + } + if (samplingType !== undefined && !SHADER_INTERPOLATION_SAMPLING_TYPES.includes(samplingType)) { + throw new Error(`Invalid interpolation sampling type: ${samplingType}`); + } + return { + interpolate: samplingType !== undefined ? [type, samplingType] : [type], + __gen: () => `@interpolate(${type}${samplingType !== undefined ? `, ${samplingType}` : ''})`, + }; +} + +export function invariant(): InvariantAttribute { + return { invariant: true, __gen: () => `@invariant` }; +} + +export function location(n: number): LocationAttribute { + if (n < 0) { + throw new Error('Location number must be a non-negative integer'); + } + return { location: n, __gen: () => `@location(${n})` }; +} + +export function mustUse(): MustUseAttribute { + return { must_use: true, __gen: () => `@must_use` }; +} + +export function size(n: number): SizeAttribute { + if (n <= 0) { + throw new Error('Size must be a positive number'); + } + return { size: n, __gen: () => `@size(${n})` }; +} + +export function workgroupSize( + ...sizes: [number] | [number, number] | [number, number, number] +): WorkgroupSizeAttribute { + if (sizes.length < 1 || sizes.length > 3) { + throw new Error('Workgroup size must have 1 to 3 dimensions'); + } + if (!sizes.every((n) => typeof n === 'number' && n > 0)) { + throw new Error('Workgroup size dimensions must be positive numbers'); + } + return { workgroup_size: sizes, __gen: () => `@workgroup_size(${sizes.join(', ')})` }; +} + +export function vertex(): VertexAttribute { + return { vertex: true, __gen: () => `@vertex` }; +} + +export function fragment(): FragmentAttribute { + return { fragment: true, __gen: () => `@fragment` }; +} + +export function compute(): ComputeAttribute { + return { compute: true, __gen: () => `@compute` }; +} + +export const constructors = { + align, + blendSrc, + builtin, + constant: constAttr, + diagnostic, + id, + interpolate, + invariant, + location, + mustUse, + size, + workgroupSize, + vertex, + fragment, + compute, +}; diff --git a/packages/scatterbrain/src/render/webgpu/shaders/declarations.ts b/packages/scatterbrain/src/render/webgpu/shaders/declarations.ts new file mode 100644 index 00000000..147da2cd --- /dev/null +++ b/packages/scatterbrain/src/render/webgpu/shaders/declarations.ts @@ -0,0 +1,405 @@ +/** + * This file defines the various types of declarations that can be used in our shader generation system, + * including variables, constants, structs, and functions. Each declaration type includes a __gen method + * that generates the corresponding WGSL code for that declaration. + */ + +import { compute, fragment, vertex, type DeclarationAttribute, type FunctionAttribute, type VariableOrValueAttribute } from './attributes'; + +function renderAttrs(attrs: DeclarationAttribute[] | undefined): string { + return attrs && attrs.length > 0 ? attrs.map((attr) => `${attr.__gen()}`).join(' ') + ' ' : ''; +} + +/// TYPES + +export type DeclarationGenerator = { + __gen: () => string; +}; + +export type IdentifierDeclaration = { + readonly name: string; +}; + +export type ConstValueDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + __identType: 'value'; + readonly assignmentType: 'const'; + readonly type?: string; + readonly initializer: unknown; + }; + +export type OverrideValueDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + __identType: 'value'; + readonly assignmentType: 'override'; + readonly attributes?: VariableOrValueAttribute[]; + } & ( + | { + readonly type: string; + readonly initializer?: unknown; + } + | { + readonly type?: string; + readonly initializer: unknown; + } + ); + +// NOTE: skipping function-scoped vars because those are handled entirely within a function body and have no direct +// relationship to the resource interface of a shader, nor are they defined outside of function bodies so we don't +// need to include them for the sake of generating the shader itself +export type ValueDeclaration = ConstValueDeclaration | OverrideValueDeclaration; + +export type PrivateVariableDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + __identType: 'variable'; + readonly assignmentType: 'private'; + readonly type?: string; + readonly initializer?: unknown; + }; + +export type WorkgroupVariableDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + __identType: 'variable'; + readonly assignmentType: 'workgroup'; + readonly type: string; + }; + +// NOTE: currently, these "Resource Interface" declarations hard-code their group and binding, but +// at least in theory these could also be specified in the "attributes" array, which is not ideal. +// Need to revisit this at some point to reduce the duplication. +export type ResourceIdentifierDeclaration = { + group: number; + binding: number; +}; + +export type UniformVariableDeclaration = IdentifierDeclaration & + ResourceIdentifierDeclaration & + DeclarationGenerator & { + __identType: 'variable'; + readonly assignmentType: 'uniform'; + readonly type: string; + readonly attributes?: VariableOrValueAttribute[]; + }; + +export type TextureVariableDeclaration = IdentifierDeclaration & + ResourceIdentifierDeclaration & + DeclarationGenerator & { + __identType: 'variable'; + readonly assignmentType: 'texture'; + readonly type: `texture_${string}`; + readonly attributes?: VariableOrValueAttribute[]; + }; + +export type SamplerVariableDeclaration = IdentifierDeclaration & + ResourceIdentifierDeclaration & + DeclarationGenerator & { + __identType: 'variable'; + readonly assignmentType: 'sampler'; + readonly type: 'sampler' | 'sampler_comparison'; + readonly attributes?: VariableOrValueAttribute[]; + }; + +export type StorageVariableDeclaration = IdentifierDeclaration & + ResourceIdentifierDeclaration & + DeclarationGenerator & { + __identType: 'variable'; + readonly assignmentType: 'storage'; + readonly type: string; + readonly accessMode?: 'read' | 'write' | 'read_write'; + readonly attributes?: VariableOrValueAttribute[]; + }; + +export type ResourceDeclaration = + | UniformVariableDeclaration + | TextureVariableDeclaration + | SamplerVariableDeclaration + | StorageVariableDeclaration; + +export type StructMemberDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + type: string; + attributes?: VariableOrValueAttribute[]; + }; + +export type StructDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + __identType: 'struct'; + name: string; + fields: StructMemberDeclaration[]; + }; + +export type FunctionParameterDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + type: string; + attributes?: VariableOrValueAttribute[]; + }; + +export type FunctionReturnTypeDeclaration = DeclarationGenerator & { + type: string; + attributes?: VariableOrValueAttribute[]; +}; + +export type FunctionDeclaration = IdentifierDeclaration & + DeclarationGenerator & { + __identType: 'function'; + parameters: FunctionParameterDeclaration[]; + body: string; + returnType?: FunctionReturnTypeDeclaration; + attributes?: FunctionAttribute[]; + }; + +export type Declaration = ValueDeclaration | StructDeclaration | ResourceDeclaration | FunctionDeclaration; + +/// CONSTRUCTORS + +export function constant(name: string, initializer: unknown, type?: string): ConstValueDeclaration { + return { + __identType: 'value', + assignmentType: 'const', + name, + ...(type !== undefined && { type }), + initializer, + __gen: () => `const ${name}${type !== undefined ? `: ${type}` : ''} = ${initializer}`, + }; +} + +export function override( + name: string, + type?: string, + initializer?: unknown, + attributes?: VariableOrValueAttribute[] +): OverrideValueDeclaration { + if (type === undefined && initializer === undefined) { + throw new Error('Override declaration must have at least a type or an initializer'); + } + const __gen = () => `${renderAttrs(attributes)}var ${name}${type !== undefined ? `: ${type}` : ''}${initializer !== undefined ? ` = ${initializer}` : ''}`; + if (type === undefined) { + return { + __identType: 'value', + assignmentType: 'override', + name, + initializer, + ...(attributes !== undefined && { attributes }), + __gen, + }; + } + return { + __identType: 'value' as const, + assignmentType: 'override' as const, + name, + type, + ...(initializer !== undefined && { initializer }), + ...(attributes !== undefined && { attributes }), + __gen, + }; +} + +export function privateVar(name: string, type?: string, initializer?: unknown): PrivateVariableDeclaration { + return { + __identType: 'variable', + assignmentType: 'private', + name, + ...(type !== undefined && { type }), + ...(initializer !== undefined && { initializer }), + __gen: () => + `var ${name}${type !== undefined ? `: ${type}` : ''}${initializer !== undefined ? ` = ${initializer}` : ''}`, + }; +} + +export function workgroupVar(name: string, type: string): WorkgroupVariableDeclaration { + return { + __identType: 'variable', + assignmentType: 'workgroup', + name, + type, + __gen: () => `var ${name}: ${type}`, + }; +} + +export function uniform( + name: string, + type: string, + group: number, + binding: number, + attributes?: VariableOrValueAttribute[] +): UniformVariableDeclaration { + return { + __identType: 'variable', + assignmentType: 'uniform', + name, + type, + group, + binding, + ...(attributes !== undefined && { attributes }), + __gen: () => + `${renderAttrs(attributes)}@group(${group}) @binding(${binding}) var ${name}: ${type}`, + }; +} + +export function texture( + name: string, + type: `texture_${string}`, + group: number, + binding: number, + attributes?: VariableOrValueAttribute[] +): TextureVariableDeclaration { + return { + __identType: 'variable', + assignmentType: 'texture', + name, + type, + group, + binding, + ...(attributes !== undefined && { attributes }), + __gen: () => + `${renderAttrs(attributes)}@group(${group}) @binding(${binding}) var ${name}: ${type}`, + }; +} + +export function sampler( + name: string, + type: 'sampler' | 'sampler_comparison', + group: number, + binding: number, + attributes?: VariableOrValueAttribute[] +): SamplerVariableDeclaration { + return { + __identType: 'variable', + assignmentType: 'sampler', + name, + type, + group, + binding, + __gen: () => + `${renderAttrs(attributes)}@group(${group}) @binding(${binding}) var ${name}: ${type}`, + ...(attributes !== undefined && { attributes }), + }; +} + +export function storage( + name: string, + type: string, + group: number, + binding: number, + accessMode?: 'read' | 'write' | 'read_write', + attributes?: VariableOrValueAttribute[] +): StorageVariableDeclaration { + return { + __identType: 'variable', + assignmentType: 'storage', + name, + type, + group, + binding, + ...(accessMode !== undefined && { accessMode }), + ...(attributes !== undefined && { attributes }), + __gen: () => + `${renderAttrs(attributes)}@group(${group}) @binding(${binding}) var ${name}: ${type}`, + }; +} + +export function member(name: string, type: string, attributes?: VariableOrValueAttribute[]): StructMemberDeclaration { + return { + name, + type, + ...(attributes !== undefined && { attributes }), + __gen: () => `${renderAttrs(attributes)}${name}: ${type}`, + }; +} + +export function struct(name: string, fields: StructMemberDeclaration[]): StructDeclaration { + return { + __identType: 'struct', + name, + fields, + __gen: () => `struct ${name} { ${fields.map((f) => f.__gen()).join(', ')} }`, + }; +} + +export function param( + name: string, + type: string, + attributes?: VariableOrValueAttribute[] +): FunctionParameterDeclaration { + return { + name, + type, + ...(attributes !== undefined && { attributes }), + __gen: () => `${renderAttrs(attributes)}${name}: ${type}`, + }; +} + +export function returns(type: string, attributes?: VariableOrValueAttribute[]): FunctionReturnTypeDeclaration { + return { + type, + ...(attributes !== undefined && { attributes }), + __gen: () => `${renderAttrs(attributes)}${type}`, + }; +} + +export function func( + name: string, + parameters: FunctionParameterDeclaration[], + body: string, + returnType?: FunctionReturnTypeDeclaration, + attributes?: FunctionAttribute[] +): FunctionDeclaration { + return { + __identType: 'function', + name, + parameters, + body, + ...(returnType !== undefined && { returnType }), + ...(attributes !== undefined && { attributes }), + __gen: () => { + const params = parameters.map((p) => p.__gen()).join(', '); + const ret = returnType ? ` -> ${returnType.__gen()}` : ''; + return `${renderAttrs(attributes)}fn ${name}(${params})${ret} { ${body} }`; + }, + }; +} + +export function vertexEntry( + name: string, + parameters: FunctionParameterDeclaration[], + body: string, + returnType?: FunctionReturnTypeDeclaration +): FunctionDeclaration { + return func(name, parameters, body, returnType, [vertex()]); +} + +export function fragmentEntry( + name: string, + parameters: FunctionParameterDeclaration[], + body: string, + returnType?: FunctionReturnTypeDeclaration +): FunctionDeclaration { + return func(name, parameters, body, returnType, [fragment()]); +} + +export function computeEntry( + name: string, + parameters: FunctionParameterDeclaration[], + body: string, + returnType?: FunctionReturnTypeDeclaration +): FunctionDeclaration { + return func(name, parameters, body, returnType, [compute()]); +} + +export const constructors = { + constant, + override, + privateVar, + workgroupVar, + uniform, + texture, + sampler, + member, + struct, + param, + returns, + func, + vertexEntry, + fragmentEntry, + computeEntry, +}; diff --git a/packages/scatterbrain/src/render/webgpu/shaders/index.ts b/packages/scatterbrain/src/render/webgpu/shaders/index.ts new file mode 100644 index 00000000..af156809 --- /dev/null +++ b/packages/scatterbrain/src/render/webgpu/shaders/index.ts @@ -0,0 +1,80 @@ +export type { + AlignAttribute, + BlendSrcAttribute, + BuiltinAttribute, + ConstAttribute, + DiagnosticAttribute, + IdAttribute, + InterpolateAttribute, + InvariantAttribute, + LocationAttribute, + MustUseAttribute, + SizeAttribute, + WorkgroupSizeAttribute, + VertexAttribute, + FragmentAttribute, + ComputeAttribute, + VariableOrValueAttribute, + FunctionAttribute, +} from './attributes'; + +export type { + IdentifierDeclaration, + ConstValueDeclaration, + OverrideValueDeclaration, + ValueDeclaration, + PrivateVariableDeclaration, + WorkgroupVariableDeclaration, + ResourceIdentifierDeclaration, + UniformVariableDeclaration, + TextureVariableDeclaration, + SamplerVariableDeclaration, + StorageVariableDeclaration, + ResourceDeclaration, + StructMemberDeclaration, + StructDeclaration, + FunctionParameterDeclaration, + FunctionReturnTypeDeclaration, + FunctionDeclaration, + Declaration, +} from './declarations'; + +export { + align, + blendSrc, + builtin, + constAttr, + diagnostic, + id, + interpolate, + invariant, + location, + mustUse, + size, + vertex, + fragment, + compute, + workgroupSize, + constructors as $a, +} from './attributes'; + +export { + constant, + override, + privateVar, + workgroupVar, + uniform, + texture, + sampler, + member, + struct, + param, + returns, + func, + vertexEntry, + fragmentEntry, + computeEntry, + constructors as $s +} from './declarations'; + +export { WGSLShader, shader } from './shader'; diff --git a/packages/scatterbrain/src/render/webgpu/shaders/shader.ts b/packages/scatterbrain/src/render/webgpu/shaders/shader.ts new file mode 100644 index 00000000..61367cea --- /dev/null +++ b/packages/scatterbrain/src/render/webgpu/shaders/shader.ts @@ -0,0 +1,34 @@ +/** + * This file defines the `WGSLShader` class, which represents a shader program in WGSL. + * It includes methods for serializing and deserializing the shader definition, as well + * as generating the WGSL source code from the defined declarations. The `shader` + * function is a simple helper function for creating a new `WGSLShader` instance from an + * array of declarations. + */ + +import type { Declaration } from "./declarations"; + +export class WGSLShader { + #definition: { declarations: Declaration[] }; + + constructor(declarations: Declaration[]) { + this.#definition = { declarations }; + } + + static deserialize(serialized: string): WGSLShader { + const definition = JSON.parse(serialized); + return new WGSLShader(definition.declarations); + } + + serialize(): string { + return JSON.stringify(this.#definition); + } + + asSource(): string { + return this.#definition.declarations.map((d) => d.__gen()).join(';\n'); + } +} + +export function shader(declarations: Declaration[]): WGSLShader { + return new WGSLShader(declarations); +} diff --git a/packages/scatterbrain/src/render/webgpu/validate.ts b/packages/scatterbrain/src/render/webgpu/validate.ts new file mode 100644 index 00000000..e389fe48 --- /dev/null +++ b/packages/scatterbrain/src/render/webgpu/validate.ts @@ -0,0 +1,16 @@ +/** biome-ignore-all lint/suspicious/noConsole: this is debugging for developers its fine*/ +const VALIDATE = true; // todo turn me off for prod... +export function beginValidate(device: GPUDevice) { + if (VALIDATE) { + device.pushErrorScope('validation'); + } +} +export function endValidate(device: GPUDevice) { + if (VALIDATE) { + device.popErrorScope().then((errs) => { + if (errs) { + console.error(errs); + } + }); + } +} diff --git a/packages/scatterbrain/src/types.ts b/packages/scatterbrain/src/types.ts index 21782531..2ad4b942 100644 --- a/packages/scatterbrain/src/types.ts +++ b/packages/scatterbrain/src/types.ts @@ -2,7 +2,6 @@ // there are 2 variants, slideview and regular - they are distinguished at runtime import type { box2D } from '@alleninstitute/vis-geometry'; - // by checking the parsed metadata for the 'slides' field export type WebGLSafeBasicType = 'uint8' | 'uint16' | 'int8' | 'int16' | 'uint32' | 'int32' | 'float'; diff --git a/packages/scatterbrain/tsconfig.json b/packages/scatterbrain/tsconfig.json index d8a6412f..4fc3ebcc 100644 --- a/packages/scatterbrain/tsconfig.json +++ b/packages/scatterbrain/tsconfig.json @@ -7,7 +7,8 @@ "moduleResolution": "Bundler", "module": "es6", "target": "es2024", - "lib": ["es2024", "DOM"] + "lib": ["es2024", "DOM"], + "types": ["@webgpu/types"] }, - "include": ["./src/index.ts"] + "include": ["./src/**/*"] } diff --git a/packages/scatterbrain/vite.config.ts b/packages/scatterbrain/vite.config.ts new file mode 100644 index 00000000..b19a2a79 --- /dev/null +++ b/packages/scatterbrain/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'node:path'; +import dts from 'vite-plugin-dts'; + +export default defineConfig({ + build: { + lib: { + entry: resolve(import.meta.dirname, 'src/index.ts'), + formats: ['es'], + fileName: 'main', + }, + }, + resolve: { + alias: { + '~': resolve(import.meta.dirname, './'), + }, + }, + plugins: [ + dts({ + tsconfigPath: './tsconfig.json', + rollupTypes: true, + }), + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87d79c21..8a1564c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 2.16.4(@parcel/core@2.16.4(@swc/helpers@0.5.17))(typescript@5.9.3) '@vitest/coverage-istanbul': specifier: 4.1.1 - version: 4.1.1(vitest@4.1.1(vite@7.3.1(lightningcss@1.30.2)(yaml@2.8.1))) + version: 4.1.1(vitest@4.1.1(@types/node@22.19.15)(vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.7)(yaml@2.8.1))) buffer: specifier: 6.0.3 version: 6.0.3 @@ -46,7 +46,7 @@ importers: version: 5.9.3 vitest: specifier: 4.1.1 - version: 4.1.1(vite@7.3.1(lightningcss@1.30.2)(yaml@2.8.1)) + version: 4.1.1(@types/node@22.19.15)(vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.7)(yaml@2.8.1)) packages/core: dependencies: @@ -107,7 +107,7 @@ importers: '@alleninstitute/vis-geometry': specifier: workspace:* version: link:../geometry - lodash: + lodash-es: specifier: 4.18.1 version: 4.18.1 regl: @@ -116,13 +116,25 @@ importers: ts-pattern: specifier: 5.9.0 version: 5.9.0 + typegpu: + specifier: 0.11.2 + version: 0.11.2 + webgpu-utils: + specifier: 2.0.2 + version: 2.0.2 zod: specifier: 4.3.6 version: 4.3.6 devDependencies: - '@types/lodash': - specifier: 4.17.24 - version: 4.17.24 + '@types/lodash-es': + specifier: 4.17.12 + version: 4.17.12 + '@types/node': + specifier: 22.19.15 + version: 22.19.15 + '@webgpu/types': + specifier: 0.1.69 + version: 0.1.69 site: dependencies: @@ -146,13 +158,13 @@ importers: version: 0.9.6(prettier-plugin-astro@0.14.1)(prettier@3.5.3)(typescript@5.9.3) '@astrojs/mdx': specifier: 4.3.13 - version: 4.3.13(astro@5.17.1(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1)) + version: 4.3.13(astro@5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1)) '@astrojs/react': specifier: 4.4.2 - version: 4.4.2(@types/node@22.1.0)(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(lightningcss@1.30.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yaml@2.8.1) + version: 4.4.2(@types/node@22.1.0)(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(lightningcss@1.32.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yaml@2.8.1) '@astrojs/starlight': specifier: 0.37.6 - version: 0.37.6(astro@5.17.1(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1)) + version: 0.37.6(astro@5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1)) '@types/lodash': specifier: 4.17.23 version: 4.17.23 @@ -164,7 +176,7 @@ importers: version: 19.2.3(@types/react@19.2.13) astro: specifier: 5.17.1 - version: 5.17.1(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1) + version: 5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1) file-saver: specifier: 2.0.5 version: 2.0.5 @@ -199,6 +211,9 @@ importers: '@types/node': specifier: 22.1.0 version: 22.1.0 + '@webgpu/types': + specifier: 0.1.69 + version: 0.1.69 packages: @@ -441,9 +456,18 @@ packages: '@emmetio/stream-reader@2.2.0': resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + '@emnapi/runtime@1.8.1': resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.25.10': resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} engines: {node: '>=18'} @@ -1014,9 +1038,18 @@ packages: cpu: [x64] os: [win32] + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@oxc-project/types@0.124.0': + resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@oxfmt/binding-android-arm-eabi@0.46.0': resolution: {integrity: sha512-b1doV4WRcJU+BESSlCvCjV+5CEr/T6h0frArAdV26Nir+gGNFNaylvDiiMPfF1pxeV0txZEs38ojzJaxBYg+ng==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1570,9 +1603,107 @@ packages: peerDependencies: '@parcel/core': ^2.16.4 + '@rolldown/binding-android-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rolldown/pluginutils@1.0.0-rc.15': + resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -1971,6 +2102,9 @@ packages: '@swc/types@0.1.25': resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -2007,6 +2141,9 @@ packages: '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + '@types/lodash@4.17.23': resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==} @@ -2031,6 +2168,9 @@ packages: '@types/node@22.1.0': resolution: {integrity: sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==} + '@types/node@22.19.15': + resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -2118,6 +2258,9 @@ packages: '@vscode/l10n@0.0.18': resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + '@webgpu/types@0.1.69': + resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==} + '@zarrita/storage@0.1.3': resolution: {integrity: sha512-ZyCMYN3LuCNtKxro9876r/KyHyXV+ie2Bhk1qYsJR4Jp+sAjoVRRNNSJPsJxk64ZgFFezayO5S2hCu88/1Odwg==} @@ -2874,30 +3017,60 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.30.2: resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.30.2: resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.30.2: resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.30.2: resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.30.2: resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} engines: {node: '>= 12.0.0'} @@ -2905,6 +3078,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} @@ -2912,6 +3092,13 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} @@ -2919,6 +3106,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} @@ -2926,26 +3120,52 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.30.2: resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss@1.30.2: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lmdb@2.8.5: resolution: {integrity: sha512-9bMdFfc80S+vSldBmG3HOuLVHnxRdNTlpzR6QDnzqCQtCzGUEAGTzBKYMeIM+I/sU4oZfgbcbS7X7F65/z/oxQ==} hasBin: true + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -3502,6 +3722,11 @@ packages: retext@9.0.0: resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + rolldown@1.0.0-rc.15: + resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup@4.57.1: resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3647,6 +3872,10 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyest@0.3.1: + resolution: {integrity: sha512-SJNnjbvTEo5VmIjsMYpUFL34b9RyaI382r1v7gyVXZpd4VnjIZKMrGk1mphXM4zkhrs3hZfO1Xwv63DoZX50yw==} + engines: {node: '>=12.20.0'} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -3697,6 +3926,9 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsover-runtime@0.0.6: + resolution: {integrity: sha512-5h/j9l4SwMSVfTMLVC/d+dkRjgh2xj+WHkivs5hhSwqACbG3JKXxp+9jneRoT07oJRHb97b5nKafsPtZEAdQMg==} + type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -3705,6 +3937,13 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + typed-binary@4.3.3: + resolution: {integrity: sha512-W2hLsSzt3k/tg38gDE4Fn/QiwcoqGuUHBc2cb3mXuH7KcYxe/GM57vzW14s2/bawB4R5knGgGq8Xb57vsaJ4Sg==} + + typegpu@0.11.2: + resolution: {integrity: sha512-wJeYeW25uidpNMyD4+5lehn39qd9iVpH5gXuJNwHYSOig69xHSLMBeHy6XnbiBdh+F5fh9dDehdVARl9g7n2qw==} + engines: {node: '>=12.20.0'} + typesafe-path@0.2.2: resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} @@ -3731,6 +3970,9 @@ packages: undici-types@6.13.0: resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -3909,15 +4151,16 @@ packages: yaml: optional: true - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + vite@8.0.8: + resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 - lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: '>=0.54.8' @@ -3928,12 +4171,14 @@ packages: peerDependenciesMeta: '@types/node': optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true jiti: optional: true less: optional: true - lightningcss: - optional: true sass: optional: true sass-embedded: @@ -4090,6 +4335,9 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + webgpu-utils@2.0.2: + resolution: {integrity: sha512-uoReAiZwl15ITelmp7hHL+eXg/E6VsRDFqWP4ZHkDruAO8pXS/cZGNY+vOWAc6LALJ8zefK1skUl9AVRuv5ijg==} + which-pm-runs@1.1.0: resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} engines: {node: '>=4'} @@ -4255,12 +4503,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/mdx@4.3.13(astro@5.17.1(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1))': + '@astrojs/mdx@4.3.13(astro@5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1))': dependencies: '@astrojs/markdown-remark': 6.3.10 '@mdx-js/mdx': 3.1.1 acorn: 8.15.0 - astro: 5.17.1(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1) + astro: 5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1) es-module-lexer: 1.7.0 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -4278,15 +4526,15 @@ snapshots: dependencies: prismjs: 1.30.0 - '@astrojs/react@4.4.2(@types/node@22.1.0)(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(lightningcss@1.30.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yaml@2.8.1)': + '@astrojs/react@4.4.2(@types/node@22.1.0)(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(lightningcss@1.32.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yaml@2.8.1)': dependencies: '@types/react': 19.2.13 '@types/react-dom': 19.2.3(@types/react@19.2.13) - '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1)) + '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@22.1.0)(lightningcss@1.32.0)(yaml@2.8.1)) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) ultrahtml: 1.6.0 - vite: 6.4.1(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1) + vite: 6.4.1(@types/node@22.1.0)(lightningcss@1.32.0)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -4307,17 +4555,17 @@ snapshots: stream-replace-string: 2.0.0 zod: 3.25.76 - '@astrojs/starlight@0.37.6(astro@5.17.1(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1))': + '@astrojs/starlight@0.37.6(astro@5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1))': dependencies: '@astrojs/markdown-remark': 6.3.10 - '@astrojs/mdx': 4.3.13(astro@5.17.1(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1)) + '@astrojs/mdx': 4.3.13(astro@5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1)) '@astrojs/sitemap': 3.6.0 '@pagefind/default-ui': 1.4.0 '@types/hast': 3.0.4 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 - astro: 5.17.1(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1) - astro-expressive-code: 0.41.3(astro@5.17.1(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1)) + astro: 5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1) + astro-expressive-code: 0.41.3(astro@5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1)) bcp-47: 2.1.0 hast-util-from-html: 2.0.3 hast-util-select: 6.0.4 @@ -4539,11 +4787,27 @@ snapshots: '@emmetio/stream-reader@2.2.0': {} + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.25.10': optional: true @@ -4920,8 +5184,17 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': optional: true + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + '@oslojs/encoding@1.1.0': {} + '@oxc-project/types@0.124.0': {} + '@oxfmt/binding-android-arm-eabi@0.46.0': optional: true @@ -5672,8 +5945,59 @@ snapshots: transitivePeerDependencies: - napi-wasm + '@rolldown/binding-android-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + optional: true + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rolldown/pluginutils@1.0.0-rc.15': {} + '@rollup/pluginutils@5.3.0(rollup@4.60.2)': dependencies: '@types/estree': 1.0.8 @@ -5928,6 +6252,11 @@ snapshots: dependencies: '@swc/counter': 0.1.3 + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.2 @@ -5974,6 +6303,10 @@ snapshots: '@types/js-yaml@4.0.9': {} + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.24 + '@types/lodash@4.17.23': {} '@types/lodash@4.17.24': {} @@ -5996,6 +6329,10 @@ snapshots: dependencies: undici-types: 6.13.0 + '@types/node@22.19.15': + dependencies: + undici-types: 6.21.0 + '@types/react-dom@19.2.3(@types/react@19.2.13)': dependencies: '@types/react': 19.2.13 @@ -6006,7 +6343,7 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 22.1.0 + '@types/node': 22.19.15 '@types/unist@2.0.11': {} @@ -6014,7 +6351,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.1.0)(lightningcss@1.32.0)(yaml@2.8.1))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -6022,11 +6359,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1) + vite: 6.4.1(@types/node@22.1.0)(lightningcss@1.32.0)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@vitest/coverage-istanbul@4.1.1(vitest@4.1.1(vite@7.3.1(lightningcss@1.30.2)(yaml@2.8.1)))': + '@vitest/coverage-istanbul@4.1.1(vitest@4.1.1(@types/node@22.19.15)(vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.7)(yaml@2.8.1)))': dependencies: '@babel/core': 7.29.0 '@istanbuljs/schema': 0.1.3 @@ -6038,7 +6375,7 @@ snapshots: magicast: 0.5.2 obug: 2.1.1 tinyrainbow: 3.1.0 - vitest: 4.1.1(vite@7.3.1(lightningcss@1.30.2)(yaml@2.8.1)) + vitest: 4.1.1(@types/node@22.19.15)(vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.7)(yaml@2.8.1)) transitivePeerDependencies: - supports-color @@ -6051,13 +6388,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.1(vite@7.3.1(lightningcss@1.30.2)(yaml@2.8.1))': + '@vitest/mocker@4.1.1(vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.7)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.1.1 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(lightningcss@1.30.2)(yaml@2.8.1) + vite: 8.0.8(@types/node@22.19.15)(esbuild@0.27.7)(yaml@2.8.1) '@vitest/pretty-format@4.1.1': dependencies: @@ -6133,6 +6470,8 @@ snapshots: '@vscode/l10n@0.0.18': {} + '@webgpu/types@0.1.69': {} + '@zarrita/storage@0.1.3': dependencies: reference-spec-reader: 0.2.0 @@ -6191,12 +6530,12 @@ snapshots: astring@1.9.0: {} - astro-expressive-code@0.41.3(astro@5.17.1(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1)): + astro-expressive-code@0.41.3(astro@5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1)): dependencies: - astro: 5.17.1(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1) + astro: 5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1) rehype-expressive-code: 0.41.3 - astro@5.17.1(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1): + astro@5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.2)(typescript@5.9.3)(yaml@2.8.1): dependencies: '@astrojs/compiler': 2.13.0 '@astrojs/internal-helpers': 0.7.5 @@ -6253,8 +6592,8 @@ snapshots: unist-util-visit: 5.0.0 unstorage: 1.17.4 vfile: 6.0.3 - vite: 6.4.1(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1) - vitefu: 1.1.1(vite@6.4.1(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1)) + vite: 6.4.1(@types/node@22.1.0)(lightningcss@1.32.0)(yaml@2.8.1) + vitefu: 1.1.1(vite@6.4.1(@types/node@22.1.0)(lightningcss@1.32.0)(yaml@2.8.1)) xxhash-wasm: 1.1.0 yargs-parser: 21.1.1 yocto-spinner: 0.2.3 @@ -6609,6 +6948,7 @@ snapshots: '@esbuild/win32-arm64': 0.27.7 '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 + optional: true escalade@3.2.0: {} @@ -7064,36 +7404,69 @@ snapshots: lightningcss-android-arm64@1.30.2: optional: true + lightningcss-android-arm64@1.32.0: + optional: true + lightningcss-darwin-arm64@1.30.2: optional: true + lightningcss-darwin-arm64@1.32.0: + optional: true + lightningcss-darwin-x64@1.30.2: optional: true + lightningcss-darwin-x64@1.32.0: + optional: true + lightningcss-freebsd-x64@1.30.2: optional: true + lightningcss-freebsd-x64@1.32.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.30.2: optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + lightningcss-linux-arm64-gnu@1.30.2: optional: true + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + lightningcss-linux-arm64-musl@1.30.2: optional: true + lightningcss-linux-arm64-musl@1.32.0: + optional: true + lightningcss-linux-x64-gnu@1.30.2: optional: true + lightningcss-linux-x64-gnu@1.32.0: + optional: true + lightningcss-linux-x64-musl@1.30.2: optional: true + lightningcss-linux-x64-musl@1.32.0: + optional: true + lightningcss-win32-arm64-msvc@1.30.2: optional: true + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + lightningcss-win32-x64-msvc@1.30.2: optional: true + lightningcss-win32-x64-msvc@1.32.0: + optional: true + lightningcss@1.30.2: dependencies: detect-libc: 2.1.2 @@ -7110,6 +7483,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lmdb@2.8.5: dependencies: msgpackr: 1.11.5 @@ -7125,6 +7514,8 @@ snapshots: '@lmdb/lmdb-linux-x64': 2.8.5 '@lmdb/lmdb-win32-x64': 2.8.5 + lodash-es@4.18.1: {} + lodash@4.17.21: {} lodash@4.17.23: {} @@ -8070,6 +8461,27 @@ snapshots: retext-stringify: 4.0.0 unified: 11.0.5 + rolldown@1.0.0-rc.15: + dependencies: + '@oxc-project/types': 0.124.0 + '@rolldown/pluginutils': 1.0.0-rc.15 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-x64': 1.0.0-rc.15 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 + rollup@4.57.1: dependencies: '@types/estree': 1.0.8 @@ -8131,6 +8543,7 @@ snapshots: '@rollup/rollup-win32-x64-gnu': 4.60.2 '@rollup/rollup-win32-x64-msvc': 4.60.2 fsevents: 2.3.3 + optional: true s.color@0.0.15: {} @@ -8284,6 +8697,8 @@ snapshots: tinybench@2.9.0: {} + tinyest@0.3.1: {} + tinyexec@1.0.2: {} tinyexec@1.0.4: {} @@ -8318,10 +8733,20 @@ snapshots: tslib@2.8.1: {} + tsover-runtime@0.0.6: {} + type-fest@0.20.2: {} type-fest@4.41.0: {} + typed-binary@4.3.3: {} + + typegpu@0.11.2: + dependencies: + tinyest: 0.3.1 + tsover-runtime: 0.0.6 + typed-binary: 4.3.3 + typesafe-path@0.2.2: {} typescript-auto-import-cache@0.3.6: @@ -8340,6 +8765,8 @@ snapshots: undici-types@6.13.0: {} + undici-types@6.21.0: {} + unicorn-magic@0.3.0: {} unified@11.0.5: @@ -8453,7 +8880,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@6.4.1(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1): + vite@6.4.1(@types/node@22.1.0)(lightningcss@1.32.0)(yaml@2.8.1): dependencies: esbuild: 0.25.10 fdir: 6.5.0(picomatch@4.0.4) @@ -8464,30 +8891,30 @@ snapshots: optionalDependencies: '@types/node': 22.1.0 fsevents: 2.3.3 - lightningcss: 1.30.2 + lightningcss: 1.32.0 yaml: 2.8.1 - vite@7.3.1(lightningcss@1.30.2)(yaml@2.8.1): + vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.7)(yaml@2.8.1): dependencies: - esbuild: 0.27.7 - fdir: 6.5.0(picomatch@4.0.4) + lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.12 - rollup: 4.60.2 + rolldown: 1.0.0-rc.15 tinyglobby: 0.2.16 optionalDependencies: + '@types/node': 22.19.15 + esbuild: 0.27.7 fsevents: 2.3.3 - lightningcss: 1.30.2 yaml: 2.8.1 - vitefu@1.1.1(vite@6.4.1(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1)): + vitefu@1.1.1(vite@6.4.1(@types/node@22.1.0)(lightningcss@1.32.0)(yaml@2.8.1)): optionalDependencies: - vite: 6.4.1(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1) + vite: 6.4.1(@types/node@22.1.0)(lightningcss@1.32.0)(yaml@2.8.1) - vitest@4.1.1(vite@7.3.1(lightningcss@1.30.2)(yaml@2.8.1)): + vitest@4.1.1(@types/node@22.19.15)(vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.7)(yaml@2.8.1)): dependencies: '@vitest/expect': 4.1.1 - '@vitest/mocker': 4.1.1(vite@7.3.1(lightningcss@1.30.2)(yaml@2.8.1)) + '@vitest/mocker': 4.1.1(vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.7)(yaml@2.8.1)) '@vitest/pretty-format': 4.1.1 '@vitest/runner': 4.1.1 '@vitest/snapshot': 4.1.1 @@ -8504,8 +8931,10 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 7.3.1(lightningcss@1.30.2)(yaml@2.8.1) + vite: 8.0.8(@types/node@22.19.15)(esbuild@0.27.7)(yaml@2.8.1) why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.15 transitivePeerDependencies: - msw @@ -8610,6 +9039,8 @@ snapshots: web-namespaces@2.0.1: {} + webgpu-utils@2.0.2: {} + which-pm-runs@1.1.0: {} which@2.0.2: diff --git a/site/package.json b/site/package.json index bf30f0e3..95ad6159 100644 --- a/site/package.json +++ b/site/package.json @@ -42,7 +42,8 @@ "@types/file-saver": "2.0.7", "@types/node": "22.1.0", "@types/react": "18.3.0", - "@types/react-dom": "18.3.0" + "@types/react-dom": "18.3.0", + "@webgpu/types": "0.1.69" }, "dependencies": { "@alleninstitute/vis-core": "workspace:*", @@ -68,5 +69,8 @@ "sharp": "0.34.5", "zarrita": "0.5.1" }, - "packageManager": "pnpm@9.14.2" + "packageManager": "pnpm@9.14.2", + "volta": { + "node": "24.15.0" + } } diff --git a/site/src/content/docs/examples/scatterbrain-webgpu.mdx b/site/src/content/docs/examples/scatterbrain-webgpu.mdx new file mode 100644 index 00000000..c59fe8b7 --- /dev/null +++ b/site/src/content/docs/examples/scatterbrain-webgpu.mdx @@ -0,0 +1,8 @@ +--- +title: Scatterbrain (via WebGPU) +tableOfContents: false +--- + +import { ScatterBrainDemo } from '../../../examples/scatterbrain/webgpu-demo.tsx'; + + diff --git a/site/src/examples/common/react/cache-provider.tsx b/site/src/examples/common/react/cache-provider.tsx new file mode 100644 index 00000000..99e32bd2 --- /dev/null +++ b/site/src/examples/common/react/cache-provider.tsx @@ -0,0 +1,20 @@ +import { logger, SharedPriorityCache } from '@alleninstitute/vis-core'; +import { createContext, useEffect, useRef, type PropsWithChildren } from 'react'; + +export const SharedCacheContext = createContext(null); + +export function SharedCacheProvider(props: PropsWithChildren) { + const state = useRef(undefined); + const { children } = props; + if (!state.current) { + logger.info('server started...'); + state.current = new SharedPriorityCache(new Map(), 2000 * 1024 * 1024, 50); + } + useEffect(() => { + return () => { + logger.info('shared cache disposed...'); + state.current = undefined; + }; + }, []); + return {children}; +} diff --git a/site/src/examples/common/react/gpu-device-provider.tsx b/site/src/examples/common/react/gpu-device-provider.tsx new file mode 100644 index 00000000..82805127 --- /dev/null +++ b/site/src/examples/common/react/gpu-device-provider.tsx @@ -0,0 +1,19 @@ +import { logger } from '@alleninstitute/vis-core'; +import { createContext, useEffect, useRef, useState, type PropsWithChildren } from 'react'; + +export const GpuContext = createContext(null); + +export function GpuDeviceProvider(props: PropsWithChildren) { + const { children } = props; + const [device, setDevice] = useState(null); + useEffect(() => { + navigator.gpu.requestAdapter().then((adapter) => { + adapter?.requestDevice().then((dev) => setDevice(dev)); + }); + return () => { + device?.destroy(); + logger.info('gpu device released'); + }; + }, []); + return {children}; +} diff --git a/site/src/examples/scatterbrain/demo.tsx b/site/src/examples/scatterbrain/demo.tsx index e88be784..11225787 100644 --- a/site/src/examples/scatterbrain/demo.tsx +++ b/site/src/examples/scatterbrain/demo.tsx @@ -1,14 +1,9 @@ import type { vec2, vec4 } from '@alleninstitute/vis-geometry'; import { SharedCacheContext, SharedCacheProvider } from '../common/react/priority-cache-provider'; import { useContext, useEffect, useRef, useState } from 'react'; -import { - buildScatterbrainRenderFn, - loadScatterbrainDataset, - setCategoricalLookupTableValues, - type Dataset, - type ShaderSettings, -} from '@alleninstitute/vis-scatterbrain'; - +import { loadScatterbrainDataset, WebGL, type Dataset } from '@alleninstitute/vis-scatterbrain'; +const { setCategoricalLookupTableValues, buildRenderFrameFn } = WebGL; +type ShaderSettings = Parameters[1]; const screenSize: vec2 = [800, 800]; const tenx = 'https://bkp-2d-visualizations-stage.s3.amazonaws.com/wmb_tenx_01172024_stage-20240128193624/G4I4GFJXJB9ATZ3PTX1/ScatterBrain.json'; @@ -76,7 +71,7 @@ function Demo(props: Props) { setCategoricalLookupTableValues(categories, lookup); - const { render, connectToCache } = buildScatterbrainRenderFn( + const { render, connectToCache } = buildRenderFrameFn( // @ts-expect-error we'll deal with this later regl, { ...settings, dataset } diff --git a/site/src/examples/scatterbrain/webgpu-demo.tsx b/site/src/examples/scatterbrain/webgpu-demo.tsx new file mode 100644 index 00000000..a3858f8b --- /dev/null +++ b/site/src/examples/scatterbrain/webgpu-demo.tsx @@ -0,0 +1,123 @@ +import type { vec2, vec4 } from '@alleninstitute/vis-geometry'; +import { GpuDeviceProvider } from '../common/react/gpu-device-provider'; +import { useContext, useEffect, useRef, useState } from 'react'; +import { WebGPU, loadScatterbrainDataset, type Dataset } from '@alleninstitute/vis-scatterbrain'; +import { GpuContext } from '../common/react/gpu-device-provider'; +import { SharedPriorityCache } from '@alleninstitute/vis-core'; + +const screenSize: vec2 = [800, 800]; +const tenx = + 'https://bkp-2d-visualizations-stage.s3.amazonaws.com/wmb_tenx_01172024_stage-20240128193624/G4I4GFJXJB9ATZ3PTX1/ScatterBrain.json'; +export function ScatterBrainDemo() { + return ( + + + + ); +} + +const makeFakeColors = (n: number) => { + const stuff: Record = {}; + for (let i = 0; i < n; i++) { + stuff[i] = { + color: [Math.random(), Math.random(), Math.random(), 1], + // 80% of either category are filtered in, at random: + filteredIn: Math.random() > 0.2, + }; + } + return stuff; +}; +// fake color and filter tables, as a demo: +const categories = { + '4MV7HA5DG2XJZ3UD8G9': makeFakeColors(40), // nt type + FS00DXV0T9R1X9FJ4QE: makeFakeColors(40), // class +}; +// const settings: Omit = { +// categoricalFilters: { '4MV7HA5DG2XJZ3UD8G9': 40, FS00DXV0T9R1X9FJ4QE: 40 }, +// colorBy: { kind: 'metadata', column: 'FS00DXV0T9R1X9FJ4QE' }, +// // an alternative color-by setting, swap it to see quantitative coloring +// // colorBy: { kind: 'quantitative', column: '27683', gradient: 'viridis', range: { min: 0, max: 10 } }, +// mode: 'color', +// quantitativeFilters: [], +// }; +async function loadRawJson() { + return await (await fetch(tenx)).json(); +} +type Props = { screenSize: vec2 }; +function Demo(props: Props) { + const { screenSize } = props; + const cnvs = useRef(null); + const device = useContext(GpuContext); + const cache = useRef(new SharedPriorityCache(new Map(), 2048 * 1024 * 1024, 20)); + const [dataset, setDataset] = useState(undefined); + useEffect(() => { + loadRawJson().then((raw) => setDataset(loadScatterbrainDataset(raw))); + }, []); + // todo handlers, etc + useEffect(() => { + // build the renderer + + if (device && dataset && cnvs.current) { + const ctx = cnvs.current?.getContext('webgpu'); + + if (ctx && ctx.getConfiguration() === null) { + ctx.configure({ device, format: 'bgra8unorm' }); + } + // cache.current + // const lookup = regl.texture({ width: 10, height: 10, format: 'rgba' }); + let lookup = device.createTexture({ + format: 'rgba8unorm', + size: { width: 10, height: 10 }, + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, + }); + const gradientData = new Uint8Array(256 * 4); + for (let i = 0; i < 256; i += 4) { + gradientData[i * 4 + 0] = i; + gradientData[i * 4 + 1] = i; + gradientData[i * 4 + 2] = i; + gradientData[i * 4 + 3] = 255; + } + const gradientTexture = device.createTexture({ + format: 'rgba8unorm', + size: { width: 256, height: 1 }, + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, + }); + // const gradient = regl.texture({ width: 256, height: 1, format: 'rgba', data: gradientData }); + // const tgt = regl.framebuffer(screenSize[0], screenSize[1]); + // make up random colors for the coloring, and add random filtering + + lookup = WebGPU.setCategoricalLookupTableValues(categories, device, lookup); + + const { render, connectToCache } = WebGPU.buildRenderFrameFn(device, { + categoricalFilters: { '4MV7HA5DG2XJZ3UD8G9': 40, FS00DXV0T9R1X9FJ4QE: 40 }, + colorBy: { kind: 'metadata', column: 'FS00DXV0T9R1X9FJ4QE' }, + dataset, + highlightByColumn: { kind: 'metadata', column: 'FS00DXV0T9R1X9FJ4QE' }, + mode: 'color', + quantitativeFilters: [], + }); + const renderOneFrame = () => { + if (ctx) { + render({ + client, + gradient: gradientTexture.createView(), + categoricalLookupTable: lookup.createView(), + target: ctx.getCurrentTexture().createView(), + camera: { + view: { minCorner: [-17, -17], maxCorner: [26, 26] }, + screenResolution: screenSize, + }, + filteredOutColor: [1, 0, 0, 1], + highlightedValue: 22, + offset: [0, 0], + quantitativeRangeFilters: {}, + spatialFilterBox: { minCorner: [-17, -17], maxCorner: [30, 30] }, + }); + } + }; + const client = connectToCache(cache.current, renderOneFrame); + renderOneFrame(); + } + }, [dataset, device, screenSize]); + return ; +} diff --git a/site/tsconfig.json b/site/tsconfig.json index c50435cf..a45b83cb 100644 --- a/site/tsconfig.json +++ b/site/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "astro/tsconfigs/strict", - "include": [".astro/types.d.ts", "**/*"], + "include": [".astro/types.d.ts", "**/*", "**/*.tsx"], "exclude": ["dist"], "compilerOptions": { "baseUrl": "./", "jsx": "react-jsx", - "jsxImportSource": "react" + "jsxImportSource": "react", + "types": ["@webgpu/types"] } }