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/package.json b/packages/scatterbrain/package.json index dfa0abb1..8d29138a 100644 --- a/packages/scatterbrain/package.json +++ b/packages/scatterbrain/package.json @@ -1,6 +1,6 @@ { "name": "@alleninstitute/vis-scatterbrain", - "version": "0.0.3", + "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.17.23", + "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..3a293907 --- /dev/null +++ b/packages/scatterbrain/src/render/webgpu/shader.ts @@ -0,0 +1,327 @@ +import { beginValidate, endValidate } from './validate'; +import * as wgh from 'webgpu-utils'; +import type { vec2, vec4 } from '@alleninstitute/vis-geometry'; + +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 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; + + // todo support picking mode + const catStart = 1; + const quantStart = catStart + categoricalColumns.length; + return /*wgsl*/ ` + // attribs // + struct Vertex { + @builtin(vertex_index) vIndex: u32, + @location(0) ${positionColumn}: vec2f, + ${categoricalColumns.map((col, i) => /*wgsl*/ `@location(${i + catStart}) ${col}:u32,`).join('\n')} + ${quantitativeColumns.map((col, i) => /*wgsl*/ `@location(${i + quantStart}) ${col}:f32,`).join('\n')} + }; + // uniforms // + struct Uniforms { + view: vec4f, + spatialFilterBox:vec4f, + filteredOutColor: vec4f, + highlightColor: vec4f, + screenSize:vec2f, + offset:vec2f, + highlightValue: u32, + // quantitative columns each need a range value - its the min,max in a vec2 + ${quantitativeColumns.map((col) => /*wgsl*/ `${rangeFor(col)}:vec2f,`).join('\n')} + }; + + @group(0) @binding(0) + var unis:Uniforms; + + // texture bindings... no longer considered uniform... + // TIL textureSampler is banned in vertex stage... neat + @group(0) @binding(1) var ${categoricalTable}: texture_2d; + @group(0) @binding(2) var ${gradientTable}: texture_2d; + + // utility functions // + fn applyCamera(dataPos:vec2f, view:vec4f)->vec4f { + let size = view.zw-view.xy; + let unit = (dataPos.xy-view.xy)/size; + return vec4f((unit*2.0)-1.0,0.0,1.0); + } + fn rangeParameter(v:f32,range:vec2f)->f32{ + return (v-range.x)/(range.y-range.x); + } + fn within( v:f32, range:vec2f)->f32{ + return step(range.x,v)*step(v,range.y); + } + + struct VsOutput { + @builtin(position) position: vec4f, + @location(0) color: vec4f, + }; + + const clip = array( + vec2f(1, -1), + vec2f(1, 1), + vec2f(-1, -1), + vec2f(-1, 1) + ); + + @vertex + fn vmain(v:Vertex)->VsOutput{ + var out: VsOutput; + + // 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; + } + @fragment + fn fmain(v:VsOutput)->@location(0) vec4f { + return v.color; // todo: round points with discard? + } + `; +} +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 uniformView = wgh.makeStructuredView(defs.uniforms.unis); + // const uniBuffer = device.createBuffer({ + // size, + // usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, + // label: 'scatterbrin uniform buffer', + // }); + + // let gradientTexture = device.createTexture({ + // format: 'rgba8unorm', + // size: { width: 256, height: 1 }, + // usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, + // }); + // const updateGradient = (data: Uint8Array) => { + // beginValidate(device); + // if (data.byteLength >= 256 * 4) { + // gradientTexture.destroy(); + // gradientTexture = device.createTexture({ + // format: 'rgba8unorm', + // size: { width: 256, height: 1 }, + // usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, + // }); + // device.queue.writeTexture( + // { texture: gradientTexture }, + // data, + // { bytesPerRow: 4 * 256, rowsPerImage: 1 }, + // { width: 256, height: 1 }, + // ); + // } else { + // // warn - we didnt updat the gradient + // // biome-ignore lint/suspicious/noConsole: + // console.warn('warning - not enough data to update gradient texture'); + // } + + // endValidate(device); + // return { binding: 2, resource: gradientTexture }; + // }; + // const updateUniforms = (unis: Partial) => { + // uniformView.set(unis); + // // now we write that to the stashed buffer + // device.queue.writeBuffer(uniBuffer, 0, uniformView.arrayBuffer); + // return { binding: 0, resource: uniBuffer }; + // }; + // const lastCategories = {}; + // let lookupTable = device.createTexture({ + // format: 'rgba8unorm', + // size: { width: 1, height: 1 }, + // usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, + // }); + // const updateCategorical = ( + // categories: Readonly>>>, + // ) => { + // // first - determine the diff what what needs to change + // if (categories === lastCategories || isEqual(categories, lastCategories)) { + // // no change - return early, change nothing + // return { binding: 1, resource: lookupTable }; + // } + // if (isEqual(Object.keys(categories).toSorted(), Object.keys(lastCategories).toSorted())) { + // // the set of categories stayed the same - great + // // but something in here changed... + // // TODO: optimize this to detect if we just change one pixel - a common case when filtering via the UI + // // for now, overwrite the whole thing + // lookupTable = setCategoricalLookupTableValues(categories, device, lookupTable); + // } else { + // // otherwise - re-build the whole thing, including the size... + // lookupTable = setCategoricalLookupTableValues(categories, device, lookupTable); + // } + // return { binding: 1, resource: lookupTable }; + // // bindGroups dont have a destroy() - so I'm assuming its totally fine to leak them!! + // }; + 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/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 eacbe12d..8e428f87 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,22 +107,34 @@ importers: '@alleninstitute/vis-geometry': specifier: workspace:* version: link:../geometry - lodash: - specifier: 4.17.23 - version: 4.17.23 + lodash-es: + specifier: 4.18.1 + version: 4.18.1 regl: specifier: 2.1.0 version: 2.1.0 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,17 @@ 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 +1602,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 +2101,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 +2140,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 +2167,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: @@ -2117,6 +2256,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==} @@ -2873,30 +3015,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'} @@ -2904,6 +3076,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'} @@ -2911,6 +3090,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'} @@ -2918,6 +3104,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'} @@ -2925,26 +3118,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==} @@ -3498,6 +3717,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'} @@ -3643,6 +3867,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'} @@ -3693,6 +3921,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'} @@ -3701,6 +3932,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==} @@ -3727,6 +3965,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'} @@ -3905,15 +4146,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' @@ -3924,12 +4166,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: @@ -4086,6 +4330,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'} @@ -4251,12 +4498,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 @@ -4274,15 +4521,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 @@ -4303,17 +4550,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 @@ -4535,11 +4782,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 @@ -4916,8 +5179,16 @@ 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 @@ -5668,8 +5939,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 @@ -5924,6 +6246,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 @@ -5970,6 +6297,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': {} @@ -5992,6 +6323,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 @@ -6002,7 +6337,7 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 22.1.0 + '@types/node': 22.19.15 '@types/unist@2.0.11': {} @@ -6010,7 +6345,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) @@ -6018,11 +6353,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 @@ -6034,7 +6369,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 @@ -6047,13 +6382,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: @@ -6129,6 +6464,8 @@ snapshots: '@vscode/l10n@0.0.18': {} + '@webgpu/types@0.1.69': {} + '@zarrita/storage@0.1.3': dependencies: reference-spec-reader: 0.2.0 @@ -6187,12 +6524,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 @@ -6249,8 +6586,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 @@ -6605,6 +6942,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: {} @@ -7060,36 +7398,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 @@ -7106,6 +7477,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 @@ -7121,6 +7508,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: {} @@ -8064,6 +8453,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 @@ -8125,6 +8535,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: {} @@ -8278,6 +8689,8 @@ snapshots: tinybench@2.9.0: {} + tinyest@0.3.1: {} + tinyexec@1.0.2: {} tinyexec@1.0.4: {} @@ -8312,10 +8725,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: @@ -8334,6 +8757,8 @@ snapshots: undici-types@6.13.0: {} + undici-types@6.21.0: {} + unicorn-magic@0.3.0: {} unified@11.0.5: @@ -8447,7 +8872,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) @@ -8458,30 +8883,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.10 - 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 @@ -8498,8 +8923,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 @@ -8604,6 +9031,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 308577e8..2d2f473a 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:*", 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"] } }