From 6b57bbabfcf3174427d906babb18c612a3da9187 Mon Sep 17 00:00:00 2001 From: noah Date: Wed, 22 Apr 2026 12:10:00 -0700 Subject: [PATCH 01/27] thinking about what it would be like to use webGPU here for scatterbrain (to bring it into connectomics quickly...) experimental, might try typeGPU again --- packages/scatterbrain/package.json | 10 ++- packages/scatterbrain/src/cache-client.ts | 66 ++++++++++++++++ packages/scatterbrain/src/index.ts | 2 +- packages/scatterbrain/src/renderer.ts | 96 ++++------------------- packages/scatterbrain/src/shader.ts | 14 ++-- packages/scatterbrain/src/types.ts | 12 +++ packages/scatterbrain/src/wgpu-shader.ts | 83 ++++++++++++++++++++ packages/scatterbrain/tsconfig.json | 2 +- pnpm-lock.yaml | 16 ++++ tsconfig.base.json | 3 +- 10 files changed, 210 insertions(+), 94 deletions(-) create mode 100644 packages/scatterbrain/src/cache-client.ts create mode 100644 packages/scatterbrain/src/wgpu-shader.ts diff --git a/packages/scatterbrain/package.json b/packages/scatterbrain/package.json index 28b5e4a5..5ef61cbf 100644 --- a/packages/scatterbrain/package.json +++ b/packages/scatterbrain/package.json @@ -1,6 +1,6 @@ { "name": "@alleninstitute/vis-scatterbrain", - "version": "0.0.2", + "version": "0.0.3", "contributors": [ { "name": "Lane Sawyer", @@ -55,7 +55,8 @@ "lodash": "4.17.23", "regl": "2.1.0", "ts-pattern": "5.9.0", - "zod": "4.3.6" + "zod": "4.3.6", + "webgpu-utils": "2.0.2" }, "publishConfig": { "registry": "https://registry.npmjs.org", @@ -63,6 +64,7 @@ }, "packageManager": "pnpm@9.14.2", "devDependencies": { - "@types/lodash": "4.17.24" + "@types/lodash": "4.17.24", + "@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 new file mode 100644 index 00000000..bcb8d91f --- /dev/null +++ b/packages/scatterbrain/src/cache-client.ts @@ -0,0 +1,66 @@ +import type { Resource, SharedPriorityCache } from '@alleninstitute/vis-core'; +import type { ColumnRequest, Item, } from './types'; +import reduce from 'lodash/reduce'; +import type { WebGLSafeBasicType } from './typed-array'; + + +type Content = Record + +export function buildScatterbrainCacheClient( + allNeededColumns: readonly string[], + cache: SharedPriorityCache, + toCacheValue: (buffer: ArrayBuffer, type: WebGLSafeBasicType) => V, + onDataArrived: () => void, +) { + const client = cache.registerClient>({ + cacheKeys: (item) => { + const { dataset, node, columns } = item; + return reduce, Record>( + columns, + (acc, col, key) => ({ + ...acc, + [key]: `${dataset.metadata.metadataFileEndpoint}/${node.file}/${col.name}`, + }), + {}, + ); + }, + fetch: (item) => { + const { dataset, node, columns } = item; + const attrs = dataset.metadata.pointAttributes; + const getColumnUrl = (columnName: string) => + `${dataset.metadata.metadataFileEndpoint}${columnName}/${dataset.metadata.visualizationReferenceId}/${node.file}`; + const getGeneUrl = (columnName: string) => + `${dataset.metadata.geneFileEndpoint}${columnName}/${dataset.metadata.visualizationReferenceId}/${node.file}`; + const getColumnInfo = (col: ColumnRequest) => + col.type === 'QUANTITATIVE' + ? ({ url: getGeneUrl(col.name), elements: 1, type: 'float' } as const) + : { url: getColumnUrl(col.name), elements: attrs[col.name].elements, type: attrs[col.name].type }; + + const proms = reduce, Record Promise>>( + columns, + (getters, col, key) => { + const { url, type } = getColumnInfo(col); + return { + ...getters, + [key]: (signal) => + fetch(url, { signal }).then((b) => + b.arrayBuffer().then((buff) => toCacheValue(buff, type)) + ), + }; + }, + {}, + ); + return proms; + }, + isValue: (v): v is Content => { + for (const column of allNeededColumns) { + if (!(column in v)) { + return false; + } + } + return true; + }, + onDataArrived, + }); + return client; +} \ No newline at end of file diff --git a/packages/scatterbrain/src/index.ts b/packages/scatterbrain/src/index.ts index bcc39ea3..9906821f 100644 --- a/packages/scatterbrain/src/index.ts +++ b/packages/scatterbrain/src/index.ts @@ -1,8 +1,8 @@ export { buildRenderFrameFn as buildScatterbrainRenderFn, - buildScatterbrainCacheClient, setCategoricalLookupTableValues, updateCategoricalValue, } from './renderer'; +export { buildScatterbrainCacheClient } from './cache-client' export * from './types'; export { getVisibleItems, loadDataset as loadScatterbrainDataset } from './dataset'; diff --git a/packages/scatterbrain/src/renderer.ts b/packages/scatterbrain/src/renderer.ts index 10a9e983..55660aa0 100644 --- a/packages/scatterbrain/src/renderer.ts +++ b/packages/scatterbrain/src/renderer.ts @@ -1,86 +1,13 @@ import type { SharedPriorityCache } from '@alleninstitute/vis-core'; -import type REGL from 'regl'; -import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset, TreeNode } from './types'; -import { Box2D, type box2D, type vec4 } from '@alleninstitute/vis-geometry'; -import { MakeTaggedBufferView } from './typed-array'; +import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset } from './types'; +import { Box2D, type vec4 } from '@alleninstitute/vis-geometry'; import keys from 'lodash/keys'; import reduce from 'lodash/reduce'; +import type REGL from 'regl' import { getVisibleItems, type NodeWithBounds } from './dataset'; import { buildScatterbrainRenderCommand, type Config, configureShader, type ShaderSettings, VBO } from './shader'; -export type Item = Readonly<{ - dataset: SlideviewScatterbrainDataset | ScatterbrainDataset; - node: TreeNode; - bounds: box2D; - columns: Record; -}>; -type Content = Record; - -export function buildScatterbrainCacheClient( - allNeededColumns: readonly string[], - regl: REGL.Regl, - cache: SharedPriorityCache, - onDataArrived: () => void, -) { - const client = cache.registerClient({ - cacheKeys: (item) => { - const { dataset, node, columns } = item; - return reduce, Record>( - columns, - (acc, col, key) => ({ - ...acc, - [key]: `${dataset.metadata.metadataFileEndpoint}/${node.file}/${col.name}`, - }), - {}, - ); - }, - fetch: (item) => { - const { dataset, node, columns } = item; - const attrs = dataset.metadata.pointAttributes; - const getColumnUrl = (columnName: string) => - `${dataset.metadata.metadataFileEndpoint}${columnName}/${dataset.metadata.visualizationReferenceId}/${node.file}`; - const getGeneUrl = (columnName: string) => - `${dataset.metadata.geneFileEndpoint}${columnName}/${dataset.metadata.visualizationReferenceId}/${node.file}`; - const getColumnInfo = (col: ColumnRequest) => - col.type === 'QUANTITATIVE' - ? ({ url: getGeneUrl(col.name), elements: 1, type: 'float' } as const) - : { url: getColumnUrl(col.name), elements: attrs[col.name].elements, type: attrs[col.name].type }; - - const proms = reduce, Record Promise>>( - columns, - (getters, col, key) => { - const { url, type } = getColumnInfo(col); - return { - ...getters, - [key]: (signal) => - fetch(url, { signal }).then((b) => - b.arrayBuffer().then((buff) => { - const typed = MakeTaggedBufferView(type, buff); - return new VBO({ - buffer: regl.buffer({ type: type, data: typed.data }), - bytes: buff.byteLength, - type: 'buffer', - }); - }), - ), - }; - }, - {}, - ); - return proms; - }, - isValue: (v): v is Content => { - for (const column of allNeededColumns) { - if (!(column in v)) { - return false; - } - } - return true; - }, - onDataArrived, - }); - return client; -} - +import { buildScatterbrainCacheClient } from './cache-client'; +import { MakeTaggedBufferView } from './typed-array' function columnsForItem( config: Config, col2shader: Record, @@ -175,7 +102,7 @@ export function updateCategoricalValue( type ScatterbrainRenderProps = Omit>[0], 'item'> & { visibilityThresholdPx: number; dataset: ScatterbrainDataset | SlideviewScatterbrainDataset; - client: ReturnType; + client: ReturnType>; }; /** * @@ -215,7 +142,16 @@ export function buildRenderFrameFn(regl: REGL.Regl, settings: ShaderSettings) { }; const connectToCache = (cache: SharedPriorityCache, onDataArrived: () => void) => { const allColumns = [...config.categoricalColumns, ...config.quantitativeColumns, config.positionColumn]; - const client = buildScatterbrainCacheClient(allColumns, regl, cache, onDataArrived); + const client = buildScatterbrainCacheClient(allColumns, cache, + (buff, type) => { + const typed = MakeTaggedBufferView(type, buff); + return new VBO({ + buffer: regl.buffer({ type: type, data: typed.data }), + bytes: buff.byteLength, + type: 'buffer', + }); + }, + onDataArrived); return client; }; return { render, connectToCache }; diff --git a/packages/scatterbrain/src/shader.ts b/packages/scatterbrain/src/shader.ts index f27a2482..7ac4c445 100644 --- a/packages/scatterbrain/src/shader.ts +++ b/packages/scatterbrain/src/shader.ts @@ -2,7 +2,7 @@ import type REGL from 'regl'; import type { ScatterbrainDataset, SlideviewScatterbrainDataset } from './types'; -import type { Cacheable, CachedVertexBuffer } from '@alleninstitute/vis-core'; +import type { CachedVertexBuffer, Resource } from '@alleninstitute/vis-core'; import { Box2D, type box2D, type Interval, type vec2, type vec4 } from '@alleninstitute/vis-geometry'; import * as lodash from 'lodash'; const { keys, mapValues, reduce } = lodash; @@ -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 @@ -38,7 +38,7 @@ type ScatterbrainShaderUtils = { getClipPosition: string; // ()-> vec4 // the position of the point in clip space - (hint - apply the camera to data-space) getPointSize: string; // ()->float }; -export class VBO implements Cacheable { +export class VBO implements Resource { buffer: CachedVertexBuffer; constructor(buffer: CachedVertexBuffer) { this.buffer = buffer; @@ -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/types.ts b/packages/scatterbrain/src/types.ts index 50a442ec..eedb6ac0 100644 --- a/packages/scatterbrain/src/types.ts +++ b/packages/scatterbrain/src/types.ts @@ -1,5 +1,7 @@ /// Types describing the metadata that gets loaded from scatterbrain.json files /// // 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'; @@ -82,3 +84,13 @@ export type SlideviewMetadata = CommonMetadata & { export type SlideviewScatterbrainDataset = { type: 'slideview'; metadata: SlideviewMetadata }; export type ScatterbrainDataset = { type: 'normal'; metadata: ScatterbrainMetadata }; + + +// renderer-specific types: + +export type Item = Readonly<{ + dataset: SlideviewScatterbrainDataset | ScatterbrainDataset; + node: TreeNode; + bounds: box2D; + columns: Record; +}>; diff --git a/packages/scatterbrain/src/wgpu-shader.ts b/packages/scatterbrain/src/wgpu-shader.ts new file mode 100644 index 00000000..43d06bfa --- /dev/null +++ b/packages/scatterbrain/src/wgpu-shader.ts @@ -0,0 +1,83 @@ + +// like the webGL shader, but in wgsl (webGPU) +import * as wgh from 'webgpu-utils' + +type Config = {} +export function buildHighlightShader(config: Config) { + return /*wgsl*/` + + struct View { + min: vec2f, + max: vec2f, + }; + struct Uniforms { + view: View, + highlight: u32, + pointSize: vec2f, // in data space + }; + + struct Vertex { + location(0) clip: vec2f, // indexed clip-space vertex, to make points bigger than 1px + location(1) position: vec2f, + location(2) colorBy: u32, + location(3) highlightBy: u32, + } + + + + fn isHighlighted(v:Vertex,u:Uniforms)->bool{ + return v.highlightBy == u.highlight; + } + fn highlightMix(v:Vertex,u:Uniforms)->f32 { + return step(0.01, abs(v.highlightBy-u.highlight)); + } + // get the clip-space position of this vertex + fn applyCamera(v:Vertex, u:Uniforms)->vec2f { + let view = u.view; + let pointSize = u.pointSize; + + let S = view.max-view.min; + let R = mix(pointSize.x,pointSize.y, highlightMix(v,u)); + let dPos = v.clip*R + v.position; + let uPos =(dPos-view.min)/S; + // now clip space + return (uPos*2.0)-1.0; + } + fn getColor(v:Vertex, u:Uniforms)->vec4f { + return mix(vec4f(0.5,0.5,0.5,1.0),vec4f(1.0,0.,0.,1.0),highlightMix(v,u)); + } + + struct VsOutput { + @builtin(position) position: vec4f, + @location(0) color: vec4f, + }; + + @group(0) @binding(0) + var unis: Uniforms; + // todo: bind a buffer (or texture) for coloring... + + @vertex + fn vmain(vert: Vertex)->VsOutput{ + var out: VsOutput; + out.color = getColor(vert,unis); + out.position = vec4f(applyCamera(vert,unis),0.5,1.0); + } + + @fragment + fn fmain(v:VsOutput)->vec4f{ + return v.color; + } + ` +} + +export function buildHighlightPipeline(device: GPUDevice) { + const prgm = buildHighlightShader({}); + const module = device.createShaderModule({ + code: prgm, + label: 'simple scatterplot highlighting' + }) + const defs = wgh.makeShaderDataDefinitions(prgm) + + +} + diff --git a/packages/scatterbrain/tsconfig.json b/packages/scatterbrain/tsconfig.json index d8a6412f..2095290d 100644 --- a/packages/scatterbrain/tsconfig.json +++ b/packages/scatterbrain/tsconfig.json @@ -9,5 +9,5 @@ "target": "es2024", "lib": ["es2024", "DOM"] }, - "include": ["./src/index.ts"] + "include": ["./src/**/*"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad508093..05adf76a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,9 @@ importers: ts-pattern: specifier: 5.9.0 version: 5.9.0 + webgpu-utils: + specifier: 2.0.2 + version: 2.0.2 zod: specifier: 4.3.6 version: 4.3.6 @@ -114,6 +117,9 @@ importers: '@types/lodash': specifier: 4.17.24 version: 4.17.24 + '@webgpu/types': + specifier: 0.1.69 + version: 0.1.69 site: dependencies: @@ -1986,6 +1992,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==} @@ -3929,6 +3938,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'} @@ -5914,6 +5926,8 @@ snapshots: '@vscode/l10n@0.0.18': {} + '@webgpu/types@0.1.69': {} + '@zarrita/storage@0.1.3': dependencies: reference-spec-reader: 0.2.0 @@ -8342,6 +8356,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/tsconfig.base.json b/tsconfig.base.json index ff280309..9254664c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -5,6 +5,7 @@ "esModuleInterop": true, "exactOptionalPropertyTypes": true, "forceConsistentCasingInFileNames": true, - "skipLibCheck": true + "skipLibCheck": true, + "types": ["@webgpu/types"] } } From 0b0d594a5a126b317de2d3ba4f1e433fccb13bea Mon Sep 17 00:00:00 2001 From: noah Date: Wed, 22 Apr 2026 12:16:47 -0700 Subject: [PATCH 02/27] switch to vite (library mode) because it supports typeGPU's wgsl transformer plugin... --- packages/scatterbrain/package.json | 13 +- packages/scatterbrain/vite.config.ts | 25 + pnpm-lock.yaml | 1084 ++++++++++++++++++++++++-- 3 files changed, 1074 insertions(+), 48 deletions(-) create mode 100644 packages/scatterbrain/vite.config.ts diff --git a/packages/scatterbrain/package.json b/packages/scatterbrain/package.json index 5ef61cbf..2c86a351 100644 --- a/packages/scatterbrain/package.json +++ b/packages/scatterbrain/package.json @@ -37,7 +37,7 @@ ], "scripts": { "typecheck": "tsc --noEmit", - "build": "parcel build --no-cache", + "build": "vite build", "dev": "parcel watch --port 1239", "demo": "vite", "test": "vitest --watch", @@ -55,8 +55,9 @@ "lodash": "4.17.23", "regl": "2.1.0", "ts-pattern": "5.9.0", - "zod": "4.3.6", - "webgpu-utils": "2.0.2" + "typegpu": "0.11.2", + "webgpu-utils": "2.0.2", + "zod": "4.3.6" }, "publishConfig": { "registry": "https://registry.npmjs.org", @@ -65,6 +66,10 @@ "packageManager": "pnpm@9.14.2", "devDependencies": { "@types/lodash": "4.17.24", - "@webgpu/types": "0.1.69" + "@types/node": "22.19.15", + "@webgpu/types": "0.1.69", + "unplugin-typegpu": "0.11.0", + "vite": "8.0.8", + "vite-plugin-dts": "4.5.4" } } \ No newline at end of file diff --git a/packages/scatterbrain/vite.config.ts b/packages/scatterbrain/vite.config.ts new file mode 100644 index 00000000..40a054b0 --- /dev/null +++ b/packages/scatterbrain/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite' +import { resolve } from 'node:path'; +import dts from 'vite-plugin-dts'; +import typegpuPlugin from 'unplugin-typegpu/vite'; + +export default defineConfig({ + build: { + lib: { + entry: resolve(import.meta.dirname, 'src/index.ts'), + formats: ['es'], + fileName: 'main', + }, + }, + resolve: { + alias: { + '@': resolve(import.meta.dirname, 'src'), + }, + }, + plugins: [ + typegpuPlugin(), + dts({ + rollupTypes: true, + }), + ], +}); \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05adf76a..d8329446 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.4)(yaml@2.8.1))) buffer: specifier: 6.0.3 version: 6.0.3 @@ -37,7 +37,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.4)(yaml@2.8.1)) packages/core: dependencies: @@ -107,6 +107,9 @@ 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 @@ -117,9 +120,21 @@ importers: '@types/lodash': specifier: 4.17.24 version: 4.17.24 + '@types/node': + specifier: 22.19.15 + version: 22.19.15 '@webgpu/types': specifier: 0.1.69 version: 0.1.69 + unplugin-typegpu: + specifier: 0.11.0 + version: 0.11.0(typegpu@0.11.2) + vite: + specifier: 8.0.8 + version: 8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(yaml@2.8.1) + vite-plugin-dts: + specifier: 4.5.4 + version: 4.5.4(@types/node@22.19.15)(rollup@4.60.0)(typescript@5.9.3)(vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(yaml@2.8.1)) site: dependencies: @@ -143,13 +158,13 @@ importers: version: 0.9.6(prettier@3.8.1)(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.0)(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.0)(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.0)(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.0)(typescript@5.9.3)(yaml@2.8.1)) '@types/lodash': specifier: 4.17.23 version: 4.17.23 @@ -161,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.0)(typescript@5.9.3)(yaml@2.8.1) + version: 5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.0)(typescript@5.9.3)(yaml@2.8.1) file-saver: specifier: 2.0.5 version: 2.0.5 @@ -438,9 +453,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'} @@ -977,6 +1001,19 @@ packages: '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} + '@microsoft/api-extractor-model@7.33.6': + resolution: {integrity: sha512-E9iI4yGEVVusbTAqSLetVFxDuBVCVqCigcoQwdJuOjsLq5Hry3MkBgUQhSZNzLCu17pgjk58MI80GRDJLht/1A==} + + '@microsoft/api-extractor@7.58.2': + resolution: {integrity: sha512-qmqWa0Fx1xn3irQy8MyuAKUs8e3CdwMJOujaPkM8gx5v/V7RcLhTjBU0/uL2kdhmROpW+5WG1FD98O441kkvQQ==} + hasBin: true + + '@microsoft/tsdoc-config@0.18.1': + resolution: {integrity: sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==} + + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@mischnic/json-sourcemap@0.1.1': resolution: {integrity: sha512-iA7+tyVqfrATAIsIRWQG+a7ZLLD0VaOCKV2Wd/v4mqIU3J9c4jx9p7S0nw1XH3gJCKNBOOwACOPYYSUu9pgT+w==} engines: {node: '>=12.0.0'} @@ -1011,9 +1048,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==} + '@pagefind/darwin-arm64@1.4.0': resolution: {integrity: sha512-2vMqkbv3lbx1Awea90gTaBsvpzgRs7MuSgKDxW0m9oV1GPZCZbZBJg/qL83GIUEN2BFlY46dtUZi54pwH+/pTQ==} cpu: [arm64] @@ -1445,9 +1491,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'} @@ -1733,6 +1877,36 @@ packages: cpu: [x64] os: [win32] + '@rushstack/node-core-library@5.22.0': + resolution: {integrity: sha512-S/Dm/N+8tkbasS6yM5cF6q4iDFt14mQQniiVIwk1fd0zpPwWESspO4qtPyIl8szEaN86XOYC1HRRzZrOowxjtw==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/problem-matcher@0.2.1': + resolution: {integrity: sha512-gulfhBs6n+I5b7DvjKRfhMGyUejtSgOHTclF/eONr8hcgF1APEDjhxIsfdUYYMzC3rvLwGluqLjbwCFZ8nxrog==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/rig-package@0.7.2': + resolution: {integrity: sha512-9XbFWuqMYcHUso4mnETfhGVUSaADBRj6HUAAEYk50nMPn8WRICmBuCphycQGNB3duIR6EEZX3Xj3SYc2XiP+9A==} + + '@rushstack/terminal@0.22.5': + resolution: {integrity: sha512-umej8J6A+WRbfQV1G/uNfnz4bMa8CzFU9IJzQb/ZcH4j7Ybg3BQ8UBKOCF3o5U3/2yah1TDU/zE71ugg2JJv+Q==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/ts-command-line@5.3.5': + resolution: {integrity: sha512-ToJQu3+o6aEdDoApGrwb/RsbwDi/NSC7jIEaAezzWM470TRrsXfSHoYAm1eWkhh34xJ+kZxU1ZzKSHiOMlOFPA==} + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -1846,6 +2020,12 @@ 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/argparse@1.0.38': + resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1906,6 +2086,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: @@ -1992,6 +2175,26 @@ packages: '@vscode/l10n@0.0.18': resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + '@vue/compiler-core@3.5.32': + resolution: {integrity: sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==} + + '@vue/compiler-dom@3.5.32': + resolution: {integrity: sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/language-core@2.2.0': + resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/shared@3.5.32': + resolution: {integrity: sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==} + '@webgpu/types@0.1.69': resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==} @@ -2011,6 +2214,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + ajv-draft-04@1.0.0: resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} peerDependencies: @@ -2019,9 +2227,23 @@ packages: ajv: optional: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + alien-signals@0.4.14: + resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} + ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -2048,6 +2270,9 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2062,6 +2287,10 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-kit@2.2.0: + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} + engines: {node: '>=20.19.0'} + astring@1.9.0: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true @@ -2083,6 +2312,13 @@ packages: bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base-64@1.0.0: resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} @@ -2109,6 +2345,13 @@ packages: resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} engines: {node: '>=18'} + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -2211,6 +2454,15 @@ packages: common-ancestor-path@1.0.1: resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -2258,6 +2510,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2355,6 +2610,14 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -2423,6 +2686,9 @@ packages: expressive-code@0.41.3: resolution: {integrity: sha512-YLnD62jfgBZYrXIPQcJ0a51Afv9h8VlWqEGK9uU2T5nL/5rb8SnA86+7+mgCZe5D34Tff5RNEA5hjNVJYHzrFg==} + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -2466,11 +2732,18 @@ packages: resolution: {integrity: sha512-piJxbLnkD9Xcyi7dWJRnqszEURixe7CrF/efBfbffe2DPyabmuIuqraruY8cXTs19QoM8VJzx47BDRVNXETM7Q==} engines: {node: '>=20'} + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + engines: {node: '>=14.14'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -2533,6 +2806,9 @@ packages: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + h3@1.15.5: resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==} @@ -2540,6 +2816,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + hast-util-embedded@3.0.0: resolution: {integrity: sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==} @@ -2600,6 +2880,10 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2625,6 +2909,10 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} @@ -2640,6 +2928,10 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -2703,6 +2995,9 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2729,6 +3024,9 @@ packages: jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + kiwi-schema@0.5.0: resolution: {integrity: sha512-X+FpfU0yTEtc6aTHS7VwbOpvQwRt70+pXXWRI5fd6CvWhe7pSVC854TVo4Zo0x5/wwcWj+/9KUlXpdcP0dY9AA==} hasBin: true @@ -2745,36 +3043,69 @@ packages: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} 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'} @@ -2782,6 +3113,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'} @@ -2789,6 +3127,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'} @@ -2796,6 +3141,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'} @@ -2803,32 +3155,62 @@ 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 + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -2839,6 +3221,14 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + magic-string-ast@1.0.3: + resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==} + engines: {node: '>=20.19.0'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3031,6 +3421,17 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + minimatch@10.2.3: + resolution: {integrity: sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==} + engines: {node: 18 || 20 || >=22} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -3166,6 +3567,9 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -3187,6 +3591,12 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + postcss-nested@6.2.0: resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} engines: {node: '>=12.0'} @@ -3235,6 +3645,9 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} @@ -3355,6 +3768,11 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + retext-latin@4.0.0: resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} @@ -3367,6 +3785,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'} @@ -3390,6 +3813,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} @@ -3439,6 +3867,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + source-map@0.7.6: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} @@ -3446,6 +3878,9 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -3455,6 +3890,10 @@ packages: stream-replace-string@2.0.0: resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==} + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -3478,6 +3917,10 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + style-to-js@1.1.17: resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} @@ -3488,6 +3931,14 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + svgo@4.0.0: resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==} engines: {node: '>=16'} @@ -3503,6 +3954,14 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyest-for-wgsl@0.3.2: + resolution: {integrity: sha512-Y5IBd0In6btjdJ3Tv0PTWeDajDc9Phwd8G/3gW3p3hbbr02JTkwqjvN9JeJsvuFmnmAStU+t05Zu/8Fl5oIOvw==} + engines: {node: '>=12.20.0'} + + 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'} @@ -3545,6 +4004,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'} @@ -3553,6 +4015,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==} @@ -3579,6 +4048,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'} @@ -3622,6 +4094,19 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unplugin-typegpu@0.11.0: + resolution: {integrity: sha512-5uEOrSbs4H+hFqlMd7PcAQAIxJy5uMl5gGdZurI6NeyQWaNBxO4jkHFo/yrxjXpSiQFiz431j7sou2eSIjjzCA==} + peerDependencies: + typegpu: ^0.11.0 + + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + unstorage@1.17.4: resolution: {integrity: sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==} peerDependencies: @@ -3717,6 +4202,15 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-plugin-dts@4.5.4: + resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} + peerDependencies: + typescript: '*' + vite: '*' + peerDependenciesMeta: + vite: + optional: true + vite@6.4.1: resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -3757,15 +4251,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' @@ -3776,12 +4271,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: @@ -3941,6 +4438,9 @@ packages: webgpu-utils@2.0.2: resolution: {integrity: sha512-uoReAiZwl15ITelmp7hHL+eXg/E6VsRDFqWP4ZHkDruAO8pXS/cZGNY+vOWAc6LALJ8zefK1skUl9AVRuv5ijg==} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + which-pm-runs@1.1.0: resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} engines: {node: '>=4'} @@ -3977,6 +4477,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml-language-server@1.19.2: resolution: {integrity: sha512-9F3myNmJzUN/679jycdMxqtydPSDRAarSj3wPiF7pchEPnO9Dg07Oc+gIYLqXR4L+g+FSEVXXv2+mr54StLFOg==} hasBin: true @@ -4105,12 +4608,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.0)(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.0)(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.0)(typescript@5.9.3)(yaml@2.8.1) + astro: 5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.0)(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 @@ -4128,15 +4631,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 @@ -4157,17 +4660,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.0)(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.0)(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.0)(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.0)(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.0)(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.0)(typescript@5.9.3)(yaml@2.8.1)) + astro: 5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.0)(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.0)(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 @@ -4389,11 +4892,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 @@ -4746,6 +5265,42 @@ snapshots: transitivePeerDependencies: - supports-color + '@microsoft/api-extractor-model@7.33.6(@types/node@22.19.15)': + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.1 + '@rushstack/node-core-library': 5.22.0(@types/node@22.19.15) + transitivePeerDependencies: + - '@types/node' + + '@microsoft/api-extractor@7.58.2(@types/node@22.19.15)': + dependencies: + '@microsoft/api-extractor-model': 7.33.6(@types/node@22.19.15) + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.1 + '@rushstack/node-core-library': 5.22.0(@types/node@22.19.15) + '@rushstack/rig-package': 0.7.2 + '@rushstack/terminal': 0.22.5(@types/node@22.19.15) + '@rushstack/ts-command-line': 5.3.5(@types/node@22.19.15) + diff: 8.0.3 + lodash: 4.18.1 + minimatch: 10.2.3 + resolve: 1.22.12 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + + '@microsoft/tsdoc-config@0.18.1': + dependencies: + '@microsoft/tsdoc': 0.16.0 + ajv: 8.18.0 + jju: 1.4.0 + resolve: 1.22.12 + + '@microsoft/tsdoc@0.16.0': {} + '@mischnic/json-sourcemap@0.1.1': dependencies: '@lezer/common': 1.2.3 @@ -4770,8 +5325,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': {} + '@pagefind/darwin-arm64@1.4.0': optional: true @@ -5465,8 +6029,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.0)': dependencies: '@types/estree': 1.0.8 @@ -5625,6 +6240,45 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.0': optional: true + '@rushstack/node-core-library@5.22.0(@types/node@22.19.15)': + dependencies: + ajv: 8.18.0 + ajv-draft-04: 1.0.0(ajv@8.18.0) + ajv-formats: 3.0.1(ajv@8.18.0) + fs-extra: 11.3.4 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.12 + semver: 7.5.4 + optionalDependencies: + '@types/node': 22.19.15 + + '@rushstack/problem-matcher@0.2.1(@types/node@22.19.15)': + optionalDependencies: + '@types/node': 22.19.15 + + '@rushstack/rig-package@0.7.2': + dependencies: + resolve: 1.22.12 + strip-json-comments: 3.1.1 + + '@rushstack/terminal@0.22.5(@types/node@22.19.15)': + dependencies: + '@rushstack/node-core-library': 5.22.0(@types/node@22.19.15) + '@rushstack/problem-matcher': 0.2.1(@types/node@22.19.15) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 22.19.15 + + '@rushstack/ts-command-line@5.3.5(@types/node@22.19.15)': + dependencies: + '@rushstack/terminal': 0.22.5(@types/node@22.19.15) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + '@sec-ant/readable-stream@0.4.1': {} '@shikijs/core@3.22.0': @@ -5721,6 +6375,13 @@ snapshots: dependencies: '@swc/counter': 0.1.3 + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/argparse@1.0.38': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.2 @@ -5789,6 +6450,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 @@ -5799,7 +6464,7 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 22.1.0 + '@types/node': 22.19.15 '@types/unist@2.0.11': {} @@ -5807,7 +6472,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) @@ -5815,11 +6480,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.4)(yaml@2.8.1)))': dependencies: '@babel/core': 7.29.0 '@istanbuljs/schema': 0.1.3 @@ -5831,7 +6496,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.4)(yaml@2.8.1)) transitivePeerDependencies: - supports-color @@ -5844,13 +6509,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.4)(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.4)(yaml@2.8.1) '@vitest/pretty-format@4.1.1': dependencies: @@ -5926,6 +6591,39 @@ snapshots: '@vscode/l10n@0.0.18': {} + '@vue/compiler-core@3.5.32': + dependencies: + '@babel/parser': 7.29.2 + '@vue/shared': 3.5.32 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.32': + dependencies: + '@vue/compiler-core': 3.5.32 + '@vue/shared': 3.5.32 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/language-core@2.2.0(typescript@5.9.3)': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.32 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.32 + alien-signals: 0.4.14 + minimatch: 9.0.9 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.9.3 + + '@vue/shared@3.5.32': {} + '@webgpu/types@0.1.69': {} '@zarrita/storage@0.1.3': @@ -5944,10 +6642,20 @@ snapshots: acorn@8.15.0: {} + acorn@8.16.0: {} + ajv-draft-04@1.0.0(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 + ajv-draft-04@1.0.0(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -5955,6 +6663,15 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + alien-signals@0.4.14: {} + ansi-align@3.0.1: dependencies: string-width: 4.2.3 @@ -5976,6 +6693,10 @@ snapshots: arg@5.0.2: {} + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} aria-query@5.3.2: {} @@ -5984,14 +6705,19 @@ snapshots: assertion-error@2.0.1: {} + ast-kit@2.2.0: + dependencies: + '@babel/parser': 7.29.2 + pathe: 2.0.3 + 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.0)(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.0)(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.0)(typescript@5.9.3)(yaml@2.8.1) + astro: 5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.0)(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.0)(typescript@5.9.3)(yaml@2.8.1): + astro@5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.0)(typescript@5.9.3)(yaml@2.8.1): dependencies: '@astrojs/compiler': 2.13.0 '@astrojs/internal-helpers': 0.7.5 @@ -6048,8 +6774,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 @@ -6097,6 +6823,10 @@ snapshots: bail@2.0.2: {} + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + base-64@1.0.0: {} base-x@3.0.11: @@ -6128,6 +6858,14 @@ snapshots: widest-line: 5.0.0 wrap-ansi: 9.0.2 + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -6208,6 +6946,12 @@ snapshots: common-ancestor-path@1.0.1: {} + compare-versions@6.1.1: {} + + confbox@0.1.8: {} + + confbox@0.2.4: {} + convert-source-map@2.0.0: {} cookie-es@1.2.2: {} @@ -6254,6 +6998,8 @@ snapshots: csstype@3.2.3: {} + de-indent@1.0.2: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -6329,6 +7075,10 @@ snapshots: entities@6.0.1: {} + entities@7.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} es-module-lexer@2.0.0: {} @@ -6404,6 +7154,7 @@ snapshots: '@esbuild/win32-arm64': 0.27.4 '@esbuild/win32-ia32': 0.27.4 '@esbuild/win32-x64': 0.27.4 + optional: true escalade@3.2.0: {} @@ -6470,6 +7221,8 @@ snapshots: '@expressive-code/plugin-shiki': 0.41.3 '@expressive-code/plugin-text-markers': 0.41.3 + exsolve@1.0.8: {} + extend@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -6502,9 +7255,17 @@ snapshots: dependencies: tiny-inflate: 1.0.3 + fs-extra@11.3.4: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -6553,6 +7314,8 @@ snapshots: dependencies: type-fest: 0.20.2 + graceful-fs@4.2.11: {} + h3@1.15.5: dependencies: cookie-es: 1.2.2 @@ -6567,6 +7330,10 @@ snapshots: has-flag@4.0.0: {} + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + hast-util-embedded@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -6756,6 +7523,8 @@ snapshots: property-information: 7.1.0 space-separated-tokens: 2.0.2 + he@1.2.0: {} + html-escaper@2.0.2: {} html-escaper@3.0.3: {} @@ -6774,6 +7543,8 @@ snapshots: ieee754@1.2.1: {} + import-lazy@4.0.0: {} + import-meta-resolve@4.2.0: {} inline-style-parser@0.2.4: {} @@ -6787,6 +7558,10 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + is-decimal@2.0.1: {} is-docker@3.0.0: {} @@ -6832,6 +7607,8 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + jju@1.4.0: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -6848,6 +7625,12 @@ snapshots: jsonc-parser@3.3.1: {} + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + kiwi-schema@0.5.0: {} kleur@3.0.3: {} @@ -6856,39 +7639,74 @@ snapshots: klona@2.0.6: {} + kolorist@1.8.0: {} + 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 @@ -6905,6 +7723,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 @@ -6920,10 +7754,18 @@ snapshots: '@lmdb/lmdb-linux-x64': 2.8.5 '@lmdb/lmdb-win32-x64': 2.8.5 + local-pkg@1.1.2: + dependencies: + mlly: 1.8.2 + pkg-types: 2.3.0 + quansync: 0.2.11 + lodash@4.17.21: {} lodash@4.17.23: {} + lodash@4.18.1: {} + longest-streak@3.1.0: {} lru-cache@11.2.6: {} @@ -6932,6 +7774,14 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + magic-string-ast@1.0.3: + dependencies: + magic-string: 0.30.21 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -7422,6 +8272,21 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + minimatch@10.2.3: + dependencies: + brace-expansion: 5.0.5 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.0 + + mlly@1.8.2: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + mrmime@2.0.1: {} ms@2.1.3: {} @@ -7582,6 +8447,8 @@ snapshots: path-key@4.0.0: {} + path-parse@1.0.7: {} + pathe@2.0.3: {} piccolore@0.1.3: {} @@ -7594,6 +8461,18 @@ snapshots: picomatch@4.0.4: {} + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + postcss-nested@6.2.0(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -7637,6 +8516,8 @@ snapshots: property-information@7.1.0: {} + quansync@0.2.11: {} + radix3@1.1.2: {} react-dom@19.2.4(react@19.2.4): @@ -7808,6 +8689,13 @@ snapshots: require-from-string@2.0.2: {} + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + retext-latin@4.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -7833,6 +8721,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 @@ -7894,6 +8803,7 @@ snapshots: '@rollup/rollup-win32-x64-gnu': 4.60.0 '@rollup/rollup-win32-x64-msvc': 4.60.0 fsevents: 2.3.3 + optional: true safe-buffer@5.2.1: {} @@ -7903,6 +8813,10 @@ snapshots: semver@6.3.1: {} + semver@7.5.4: + dependencies: + lru-cache: 6.0.0 + semver@7.7.3: {} semver@7.7.4: {} @@ -7972,16 +8886,22 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.6.1: {} + source-map@0.7.6: {} space-separated-tokens@2.0.2: {} + sprintf-js@1.0.3: {} + stackback@0.0.2: {} std-env@4.0.0: {} stream-replace-string@2.0.0: {} + string-argv@0.3.2: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -8009,6 +8929,8 @@ snapshots: strip-final-newline@4.0.0: {} + strip-json-comments@3.1.1: {} + style-to-js@1.1.17: dependencies: style-to-object: 1.0.9 @@ -8021,6 +8943,12 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + svgo@4.0.0: dependencies: commander: 11.1.0 @@ -8037,6 +8965,12 @@ snapshots: tinybench@2.9.0: {} + tinyest-for-wgsl@0.3.2: + dependencies: + tinyest: 0.3.1 + + tinyest@0.3.1: {} + tinyexec@1.0.2: {} tinyexec@1.0.4: {} @@ -8064,10 +8998,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: @@ -8086,6 +9030,8 @@ snapshots: undici-types@6.13.0: {} + undici-types@6.21.0: {} + unicorn-magic@0.3.0: {} unified@11.0.5: @@ -8155,6 +9101,31 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + universalify@2.0.1: {} + + unplugin-typegpu@0.11.0(typegpu@0.11.2): + dependencies: + '@babel/parser': 7.29.2 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + ast-kit: 2.2.0 + defu: 6.1.4 + magic-string-ast: 1.0.3 + pathe: 2.0.3 + picomatch: 4.0.4 + tinyest: 0.3.1 + tinyest-for-wgsl: 0.3.2 + typegpu: 0.11.2 + unplugin: 3.0.0 + transitivePeerDependencies: + - supports-color + + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + unstorage@1.17.4: dependencies: anymatch: 3.1.3 @@ -8199,7 +9170,26 @@ 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-plugin-dts@4.5.4(@types/node@22.19.15)(rollup@4.60.0)(typescript@5.9.3)(vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(yaml@2.8.1)): + dependencies: + '@microsoft/api-extractor': 7.58.2(@types/node@22.19.15) + '@rollup/pluginutils': 5.3.0(rollup@4.60.0) + '@volar/typescript': 2.4.28 + '@vue/language-core': 2.2.0(typescript@5.9.3) + compare-versions: 6.1.1 + debug: 4.4.3 + kolorist: 1.8.0 + local-pkg: 1.1.2 + magic-string: 0.30.21 + typescript: 5.9.3 + optionalDependencies: + vite: 8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + + 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) @@ -8210,30 +9200,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.4)(yaml@2.8.1): dependencies: - esbuild: 0.27.4 - fdir: 6.5.0(picomatch@4.0.4) + lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.8 - rollup: 4.60.0 + rolldown: 1.0.0-rc.15 tinyglobby: 0.2.15 optionalDependencies: + '@types/node': 22.19.15 + esbuild: 0.27.4 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.4)(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.4)(yaml@2.8.1)) '@vitest/pretty-format': 4.1.1 '@vitest/runner': 4.1.1 '@vitest/snapshot': 4.1.1 @@ -8250,8 +9240,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.4)(yaml@2.8.1) why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.15 transitivePeerDependencies: - msw @@ -8358,6 +9350,8 @@ snapshots: webgpu-utils@2.0.2: {} + webpack-virtual-modules@0.6.2: {} + which-pm-runs@1.1.0: {} which@2.0.2: @@ -8391,6 +9385,8 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: {} + yaml-language-server@1.19.2: dependencies: '@vscode/l10n': 0.0.18 From a64a9d8583379f1aee8468175de867f983fab5bb Mon Sep 17 00:00:00 2001 From: noah Date: Wed, 22 Apr 2026 22:13:04 -0700 Subject: [PATCH 03/27] minor import cleanup --- packages/scatterbrain/src/cache-client.ts | 6 +++--- packages/scatterbrain/src/shader.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/scatterbrain/src/cache-client.ts b/packages/scatterbrain/src/cache-client.ts index bcb8d91f..62cc51ba 100644 --- a/packages/scatterbrain/src/cache-client.ts +++ b/packages/scatterbrain/src/cache-client.ts @@ -1,12 +1,12 @@ -import type { Resource, SharedPriorityCache } from '@alleninstitute/vis-core'; +import type { Cacheable, SharedPriorityCache } from '@alleninstitute/vis-core'; import type { ColumnRequest, Item, } from './types'; import reduce from 'lodash/reduce'; import type { WebGLSafeBasicType } from './typed-array'; -type Content = Record +type Content = Record -export function buildScatterbrainCacheClient( +export function buildScatterbrainCacheClient( allNeededColumns: readonly string[], cache: SharedPriorityCache, toCacheValue: (buffer: ArrayBuffer, type: WebGLSafeBasicType) => V, diff --git a/packages/scatterbrain/src/shader.ts b/packages/scatterbrain/src/shader.ts index 7ac4c445..807e3339 100644 --- a/packages/scatterbrain/src/shader.ts +++ b/packages/scatterbrain/src/shader.ts @@ -2,7 +2,7 @@ import type REGL from 'regl'; import type { ScatterbrainDataset, SlideviewScatterbrainDataset } from './types'; -import type { CachedVertexBuffer, Resource } from '@alleninstitute/vis-core'; +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'; const { keys, mapValues, reduce } = lodash; @@ -38,7 +38,7 @@ export type ScatterbrainShaderUtils = { getClipPosition: string; // ()-> vec4 // the position of the point in clip space - (hint - apply the camera to data-space) getPointSize: string; // ()->float }; -export class VBO implements Resource { +export class VBO implements Cacheable { buffer: CachedVertexBuffer; constructor(buffer: CachedVertexBuffer) { this.buffer = buffer; From 71db3731e15b243399d454f2e1131f8648cb2406 Mon Sep 17 00:00:00 2001 From: noah Date: Wed, 22 Apr 2026 22:13:58 -0700 Subject: [PATCH 04/27] try typegpu until I go crazy, but it does (almost) work --- packages/scatterbrain/src/cache-client.ts | 3 + packages/scatterbrain/src/demo.html | 32 +++ packages/scatterbrain/src/index.ts | 1 + packages/scatterbrain/src/tgpu-shader.ts | 313 ++++++++++++++++++++++ 4 files changed, 349 insertions(+) create mode 100644 packages/scatterbrain/src/demo.html create mode 100644 packages/scatterbrain/src/tgpu-shader.ts diff --git a/packages/scatterbrain/src/cache-client.ts b/packages/scatterbrain/src/cache-client.ts index 62cc51ba..f4a76bcc 100644 --- a/packages/scatterbrain/src/cache-client.ts +++ b/packages/scatterbrain/src/cache-client.ts @@ -2,6 +2,7 @@ import type { Cacheable, SharedPriorityCache } from '@alleninstitute/vis-core'; import type { ColumnRequest, Item, } from './types'; import reduce from 'lodash/reduce'; import type { WebGLSafeBasicType } from './typed-array'; +import { keys } from 'lodash'; type Content = Record @@ -53,6 +54,8 @@ export function buildScatterbrainCacheClient( return proms; }, isValue: (v): v is Content => { + // console.log('looking for', allNeededColumns) + // console.log('in', keys(v)) for (const column of allNeededColumns) { if (!(column in v)) { return false; diff --git a/packages/scatterbrain/src/demo.html b/packages/scatterbrain/src/demo.html new file mode 100644 index 00000000..20b603c4 --- /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/index.ts b/packages/scatterbrain/src/index.ts index 9906821f..920a8cbd 100644 --- a/packages/scatterbrain/src/index.ts +++ b/packages/scatterbrain/src/index.ts @@ -6,3 +6,4 @@ export { export { buildScatterbrainCacheClient } from './cache-client' export * from './types'; export { getVisibleItems, loadDataset as loadScatterbrainDataset } from './dataset'; +export { whatever } from './tgpu-shader' \ No newline at end of file diff --git a/packages/scatterbrain/src/tgpu-shader.ts b/packages/scatterbrain/src/tgpu-shader.ts new file mode 100644 index 00000000..6471f45b --- /dev/null +++ b/packages/scatterbrain/src/tgpu-shader.ts @@ -0,0 +1,313 @@ + +// lets try and make not a full-fledged scatterbrain shader, +// with all its fancy filtering, hovering, dot sizes, etc +// but instead, some subplot shaders - so we render the dots, +// but we have no fancy filtering, just a simple highlight value, +// and a color-by attribute + + +// and lets try it with typeGPU generating our shaders for us... which I must admit seems pretty good... + +import tgpu, { d, std, type TgpuRoot } from 'typegpu'; +import { buildScatterbrainCacheClient } from './cache-client'; +import type { ShaderSettings } from './shader'; +import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset } from './types'; +import { uniq } from 'lodash'; +import { SharedPriorityCache, type Cacheable } from '@alleninstitute/vis-core'; +import type { WebGLSafeBasicType } from './typed-array'; +import { getVisibleItems, loadDataset, type NodeWithBounds } from './dataset'; +import { Box2D, type box2D, type vec2 } from '@alleninstitute/vis-geometry'; +import { pl } from 'zod/locales'; + + + + +// it seems like we can define a shader (and it will get actually created lazily) +// without needing an upfront instance of a gpu device... thats handy. + +// except... realistically, main functions will probably need uniforms! +// we can pull those in, but you need a root to even have one... +// so - + +// uh ok this is hard to read, but tgpu.vertexFn({in,out}) +// returns a function which we call immediately, +// passing it another function, which is given the things in in, and must return the things in out +// type wtf = d.unstruct({clip:d.vec2f,position:d.vec2f,colorBy:d.u16,highlightBy:d.u16 }) +// const wtf = d.unstruct({clip:d.vec2f,position:d.vec2f,colorBy:d.u32,highlightBy:d.u32 }) +// const vmain = tgpu.vertexFn({ +// in: {clip:d.vec2f,position:d.vec2f,colorBy:d.u32,highlightBy:d.u32}, +// out: { pos: d.builtin.position, uv: d.vec2f }, +// uniform: {} +// })(({clip,position,highlightBy,colorBy}) => { +// const pos = [d.vec2f(0, 0.8), d.vec2f(-0.8, -0.8), d.vec2f(0.8, -0.8)]; +// const uv = [d.vec2f(0.5, 1), d.vec2f(0, 0), d.vec2f(1, 0)]; + +// return { +// pos: d.vec4f(1,0, 0, 1), +// uv: d.vec2f(0.5,0.5), +// }; +// }); + +// lets find this after the build step runs? +// export function whatever() { +// const View = d.struct({ min: d.vec2f, max: d.vec2f }) +// const myLayout = tgpu.bindGroupLayout({ view: { uniform: View }, highlight: { uniform: d.u32 } }); +// // function vmain() { +// // 'use gpu'; + +// // } +// const vmain = tgpu.vertexFn({ +// in: { clip: d.vec2f, position: d.vec2f, colorBy: d.u32, highlightBy: d.u32 }, +// out: { pos: d.builtin.position, color: d.vec4f }, +// })(function ({ clip, position, highlightBy, colorBy }) { +// 'use gpu'; +// const size = std.sub(myLayout.$.view.max, myLayout.$.view.min) +// const p = std.div(std.sub(position, myLayout.$.view.min), size); +// const clr = std.mix(d.vec4f(0, 0, 0, 1), d.vec4f(1, 0, 0, 1), std.abs(std.sub(highlightBy, myLayout.$.highlight))) +// return { +// pos: d.vec4f(p.xy, 0, 1), +// color: clr, +// }; +// }); + +// const { code, usedBindGroupLayouts, catchall } = tgpu.resolveWithContext({ +// externals: { vmain }, template: +// /*wgsl*/` +// @vertex +// vmain +// ` }) + +// console.log(code) +// console.log(usedBindGroupLayouts) +// console.log(catchall) +// } +// whatever(); + + +// lets build an oh-so-basic typeGPU renderer for scatterbrain data... +export type SimpleSettings = { + dataset: ScatterbrainDataset | SlideviewScatterbrainDataset; + highlightBy: { kind: 'metadata', column: string }; // the name of a categorical feature by which to highlight + colorBy: { kind: 'metadata', column: string } +} +class VBO implements Cacheable { + constructor(readonly buffer: GPUBuffer) { + } + destroy() { + this.buffer.destroy(); + } + sizeInBytes() { + return this.buffer.size; + } +} +// const dType = { +// uint8: d.uint8, +// uint16: d.u16, +// uint32: d.u32, +// int8: d.i32, +// int16: d.sint16, +// int32: d.i32, +// float: d.f32, +// } as const; + +// annoying hurdle 1 - I dont know how to use root.createBuffer() correctly to upload my raw bytes +type RenderProps = { + camera: { view: box2D, screenResolution: vec2 } + dataset: ScatterbrainDataset | SlideviewScatterbrainDataset; + client: ReturnType>; + visibilityThresholdPx: number; + ctx: GPUCanvasContext +}; + +export function buildScatterbrainTGPU(root: TgpuRoot, settings: SimpleSettings) { + const toGpuBuffer = (buffer: ArrayBuffer, _type: WebGLSafeBasicType) => { + const B = root.device.createBuffer({ size: buffer.byteLength, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX }); + root.device.queue.writeBuffer(B, 0, buffer); + return new VBO(B); + } + const prepareQtCell = columnsForItem(settings); + const drawQtCell = buildRenderFn(root); + const render = (props: RenderProps) => { + const { camera, dataset, client, visibilityThresholdPx } = props; + const visibilityThreshold = (visibilityThresholdPx * Box2D.size(camera.view)[0]) / camera.screenResolution[0]; // (units*pixel)/pixel ==> units + const visibleQtNodes = getVisibleItems(dataset, camera, visibilityThreshold).map(prepareQtCell); + client.setPriorities(visibleQtNodes, []); + console.log("visible: ", visibleQtNodes.length) + for (const node of visibleQtNodes) { + if (client.has(node)) { + const drawable = client.get(node); + if (drawable) { + console.log('draw it: ', node.node.file) + drawQtCell({ + count: node.node.numSpecimens / 2, // todo this is bug, I cant figure out how to set the attrib layout correctly... I think + columns: drawable, + ctx: props.ctx, + highlight: 22, + radius: .05, + view: props.camera.view, + }) + // drawQtCell({ + // ...props, + // item: { + // columnData: drawable, + // count: node.node.numSpecimens, + // }, + // }); + } + } + } + } + + const connectToCache = (cache: SharedPriorityCache, onDataArrived: () => void) => { + return buildScatterbrainCacheClient(['position', 'colorBy', 'highlightBy'], cache, toGpuBuffer, onDataArrived); + }; + + return { render, connectToCache }; + +} + +function columnsForItem( + config: SimpleSettings, +) { + const columns: Record = { + position: { type: 'METADATA', name: config.dataset.metadata.spatialColumn }, + colorBy: { type: 'METADATA', name: config.colorBy.column }, + highlightBy: { type: 'METADATA', name: config.highlightBy.column } + }; + + return (item: T) => { + return { ...item, dataset: config.dataset, columns }; + }; +} + +function buildRenderFn(root: TgpuRoot) { + // ok - heres where we do the thing, which is to create a pipeline + // and return a function that invokes it... + + const View = d.struct({ min: d.vec2f, max: d.vec2f }) + const Vertex = { vIndex: d.builtin.vertexIndex, position: d.vec2f, highlightBy: d.u32 } + const unis = tgpu.bindGroupLayout({ view: { uniform: View }, highlight: { uniform: d.u32 }, radius: { uniform: d.f32 } }); + + // ok because we dont use interleaved buffers + // we have to hand-roll the layouts - do we need locations here? + const pLayout = tgpu.vertexLayout(d.arrayOf(d.vec2f), 'instance') + const hLayout = tgpu.vertexLayout(d.arrayOf(d.u32), 'instance') + + const vmain = tgpu.vertexFn({ + in: Vertex, + out: { pos: d.builtin.position, color: d.vec4f }, + })(function ({ vIndex, position, highlightBy }) { + 'use gpu'; + const clip = [d.vec2f(1, -1), d.vec2f(1, 1), d.vec2f(-1, -1), d.vec2f(-1, 1)]; + const size = std.sub(unis.$.view.max, unis.$.view.min); + const dP = std.add(position, std.mul(clip[vIndex], unis.$.radius)); + const p = std.sub(std.mul(std.div(std.sub(dP, unis.$.view.min), size), 2), 1); + const clr = std.mix(d.vec4f(1, 0, 0, 1), d.vec4f(0.5, 0.5, 0.5, 1), std.step(0.001, std.abs(std.sub(highlightBy, unis.$.highlight)))) + return { + pos: d.vec4f(p.xy, 0, 1), + color: clr, + }; + }); + + const fmain = tgpu.fragmentFn({ in: { pos: d.builtin.position, color: d.vec4f }, out: d.vec4f })(function ({ color }) { + return color; + }); + + // lets create some uniforms... + const view = root.createUniform(View) + const highlight = root.createUniform(d.u32) + const radius = root.createUniform(d.f32) + + const bg = root.createBindGroup(unis, { + view: view.buffer, + highlight: highlight.buffer, + radius: radius.buffer + }) + // this next part - it does pick up the types from + // the vertex shader... but some other stuff is mysterious + // also raw createPipeline has a fair bit of type-safety, although its + // true it cant line up named attribs, it only works by location index + const pipeline = root.createRenderPipeline({ + vertex: vmain, + fragment: fmain, + attribs: { + highlightBy: hLayout.attrib, + position: pLayout.attrib, + }, + primitive: { topology: 'triangle-strip', cullMode: 'back' }, + // depthStencil: { + // format: 'depth24plus', + // depthWriteEnabled: true, + // depthCompare: 'less', + // }, + }) + console.log(root.unwrap(pipeline)) + return (props: { view: box2D, radius: number, highlight: number, count: number, ctx: GPUCanvasContext, columns: Record }) => { + // write the unis... + const { position, highlightBy } = props.columns; + view.patch({ min: props.view.minCorner, max: props.view.maxCorner }); // ugh + highlight.patch(props.highlight); + radius.patch(props.radius); + pipeline + .with(bg) + .withColorAttachment({ view: props.ctx, loadOp: 'load', }) + .with(pLayout, position.buffer) + .with(hLayout, highlightBy.buffer) + .draw(4, props.count); + } +} + + +const tenx = + 'https://bkp-2d-visualizations-stage.s3.amazonaws.com/wmb_tenx_01172024_stage-20240128193624/G4I4GFJXJB9ATZ3PTX1/ScatterBrain.json'; +const Class = 'FS00DXV0T9R1X9FJ4QE' + +async function loadRawJson() { + return await (await fetch(tenx)).json(); +} + +export async function whatever() { + const root = await tgpu.init() + // const r = buildRenderFn(root) + 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 } = buildScatterbrainTGPU(root, { colorBy: { kind: 'metadata', column: Class }, highlightBy: { kind: 'metadata', column: Class }, dataset }) + // const toGpuBuffer = (buffer: ArrayBuffer, _type: WebGLSafeBasicType) => { + // const B = root.device.createBuffer({ size: buffer.byteLength, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX }); + // root.device.queue.writeBuffer(B, 0, buffer); + // return new VBO(B); + // } + + const cnvs: HTMLCanvasElement = document.getElementById('canvas') as HTMLCanvasElement; + cnvs.width = 1500; + cnvs.height = 1500; + const ctx = cnvs.getContext('webgpu') + ctx?.configure({ + device: root.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(() => { + console.log('re render!') + render({ camera: { view, screenResolution: [1500, 1500] }, client, ctx: ctx!, dataset, visibilityThresholdPx: 10 }) + }) + }) + render({ camera: { view, screenResolution: [1500, 1500] }, client, ctx: ctx!, dataset, visibilityThresholdPx: 10 }) + + // const position = toGpuBuffer(new Float32Array([3, 3, 40, 10, 25, 40]).buffer, 'float') + // const highlightBy = toGpuBuffer(new Uint32Array([0, 1, 3]).buffer, 'uint32') + // const columns = { + // position, + // highlightBy, + // } + // r({ ctx: ctx!, columns, count: 3, highlight: 3, radius: 4, view: { minCorner: [0, 0], maxCorner: [50, 50] } }) +} +whatever(); \ No newline at end of file From 8dd215caedc5567fced3251a3bfd5624e43a2806 Mon Sep 17 00:00:00 2001 From: noah Date: Thu, 23 Apr 2026 13:38:28 -0700 Subject: [PATCH 05/27] fix the uint16 mystery. implement the raw webGPU scatterplot renderer, to compare it with the type-gpu version --- packages/scatterbrain/src/tgpu-shader.ts | 72 +++++---- packages/scatterbrain/src/wgpu-shader.ts | 177 +++++++++++++++++++++-- 2 files changed, 207 insertions(+), 42 deletions(-) diff --git a/packages/scatterbrain/src/tgpu-shader.ts b/packages/scatterbrain/src/tgpu-shader.ts index 6471f45b..36f97c78 100644 --- a/packages/scatterbrain/src/tgpu-shader.ts +++ b/packages/scatterbrain/src/tgpu-shader.ts @@ -18,6 +18,8 @@ import type { WebGLSafeBasicType } from './typed-array'; import { getVisibleItems, loadDataset, type NodeWithBounds } from './dataset'; import { Box2D, type box2D, type vec2 } from '@alleninstitute/vis-geometry'; import { pl } from 'zod/locales'; +import type { F32 } from 'typegpu/data'; +import { buildRenderFn as OldSchool, VBO } from './wgpu-shader'; @@ -90,16 +92,7 @@ export type SimpleSettings = { highlightBy: { kind: 'metadata', column: string }; // the name of a categorical feature by which to highlight colorBy: { kind: 'metadata', column: string } } -class VBO implements Cacheable { - constructor(readonly buffer: GPUBuffer) { - } - destroy() { - this.buffer.destroy(); - } - sizeInBytes() { - return this.buffer.size; - } -} + // const dType = { // uint8: d.uint8, // uint16: d.u16, @@ -120,42 +113,48 @@ type RenderProps = { }; export function buildScatterbrainTGPU(root: TgpuRoot, settings: SimpleSettings) { - const toGpuBuffer = (buffer: ArrayBuffer, _type: WebGLSafeBasicType) => { + 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... + // this is probably why the typeGPU thing didnt work right either... + const B = root.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)) + root.device.queue.writeBuffer(B, 0, u32.buffer); + return new VBO(B); + } const B = root.device.createBuffer({ size: buffer.byteLength, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX }); - root.device.queue.writeBuffer(B, 0, buffer); + root.device.queue.writeBuffer(B, 0, buffer, 0, buffer.byteLength); return new VBO(B); } const prepareQtCell = columnsForItem(settings); const drawQtCell = buildRenderFn(root); + const drawQtCells = OldSchool(root.device); const render = (props: RenderProps) => { const { camera, dataset, client, visibilityThresholdPx } = props; const visibilityThreshold = (visibilityThresholdPx * Box2D.size(camera.view)[0]) / camera.screenResolution[0]; // (units*pixel)/pixel ==> units const visibleQtNodes = getVisibleItems(dataset, camera, visibilityThreshold).map(prepareQtCell); client.setPriorities(visibleQtNodes, []); - console.log("visible: ", visibleQtNodes.length) + // console.log("visible: ", visibleQtNodes.length) + const blocks: Array<{ columns: Record, count: number }> = [] for (const node of visibleQtNodes) { if (client.has(node)) { const drawable = client.get(node); if (drawable) { - console.log('draw it: ', node.node.file) - drawQtCell({ - count: node.node.numSpecimens / 2, // todo this is bug, I cant figure out how to set the attrib layout correctly... I think - columns: drawable, - ctx: props.ctx, - highlight: 22, - radius: .05, - view: props.camera.view, - }) + // // console.log('draw it: ', node.node.file) // drawQtCell({ - // ...props, - // item: { - // columnData: drawable, - // count: node.node.numSpecimens, - // }, - // }); + // count: node.node.numSpecimens, + // columns: drawable, + // ctx: props.ctx, + // highlight: 4, + // radius: .05, + // view: props.camera.view, + // }) + blocks.push({ count: node.node.numSpecimens, columns: drawable }) } } } + drawQtCells({ ctx: props.ctx, highlight: 4, radius: 0.05, view: props.camera.view, blocks }) } const connectToCache = (cache: SharedPriorityCache, onDataArrived: () => void) => { @@ -180,6 +179,10 @@ function columnsForItem( }; } +// so this version has problems +// I cannot figure out for the life of me how to get uint16 buffer in as a vertex attribute.... +// I think I'm gonna try (again) to just make the meat of the shader, then feed it to a template via resolveWithContext +// that will mean I'll roll the pipeline by hand, but honestly I dont see how else to make this work... function buildRenderFn(root: TgpuRoot) { // ok - heres where we do the thing, which is to create a pipeline // and return a function that invokes it... @@ -192,6 +195,10 @@ function buildRenderFn(root: TgpuRoot) { // we have to hand-roll the layouts - do we need locations here? const pLayout = tgpu.vertexLayout(d.arrayOf(d.vec2f), 'instance') const hLayout = tgpu.vertexLayout(d.arrayOf(d.u32), 'instance') + // const hLayout: GPUVertexBufferLayout = { + // ...wLayout, + // arrayStride: 0, + // } const vmain = tgpu.vertexFn({ in: Vertex, @@ -241,7 +248,7 @@ function buildRenderFn(root: TgpuRoot) { // depthCompare: 'less', // }, }) - console.log(root.unwrap(pipeline)) + // console.log(root.unwrap(pipeline)) return (props: { view: box2D, radius: number, highlight: number, count: number, ctx: GPUCanvasContext, columns: Record }) => { // write the unis... const { position, highlightBy } = props.columns; @@ -250,6 +257,7 @@ function buildRenderFn(root: TgpuRoot) { radius.patch(props.radius); pipeline .with(bg) + .withColorAttachment({ view: props.ctx, loadOp: 'load', }) .with(pLayout, position.buffer) .with(hLayout, highlightBy.buffer) @@ -268,6 +276,7 @@ async function loadRawJson() { export async function whatever() { const root = await tgpu.init() + // const r = buildRenderFn(root.device); // const r = buildRenderFn(root) const dataset = await loadDataset(await loadRawJson()) if (!dataset) { @@ -294,9 +303,10 @@ export async function whatever() { const view = Box2D.create([bound.lx, bound.ly], [bound.ux, bound.uy]); const client = connectToCache(cache, () => { // redraw? - console.log('new data arrived...') + // console.log('new data arrived...') requestAnimationFrame(() => { - console.log('re render!') + // console.log('re render!') + render({ camera: { view, screenResolution: [1500, 1500] }, client, ctx: ctx!, dataset, visibilityThresholdPx: 10 }) }) }) diff --git a/packages/scatterbrain/src/wgpu-shader.ts b/packages/scatterbrain/src/wgpu-shader.ts index 43d06bfa..7f9c10dd 100644 --- a/packages/scatterbrain/src/wgpu-shader.ts +++ b/packages/scatterbrain/src/wgpu-shader.ts @@ -1,7 +1,26 @@ // like the webGL shader, but in wgsl (webGPU) +import type { Cacheable } from '@alleninstitute/vis-core'; +import type { box2D } from '@alleninstitute/vis-geometry'; +import { mapValues } from 'lodash'; import * as wgh from 'webgpu-utils' +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) + } + }) + } +} + type Config = {} export function buildHighlightShader(config: Config) { return /*wgsl*/` @@ -12,15 +31,16 @@ export function buildHighlightShader(config: Config) { }; struct Uniforms { view: View, - highlight: u32, pointSize: vec2f, // in data space + highlight: u32, }; struct Vertex { - location(0) clip: vec2f, // indexed clip-space vertex, to make points bigger than 1px - location(1) position: vec2f, - location(2) colorBy: u32, - location(3) highlightBy: u32, + // location(0) clip: vec2f, // indexed clip-space vertex, to make points bigger than 1px + @builtin(vertex_index) vIndex: u32, + @location(0) position: vec2f, + @location(1) colorBy: u32, + @location(2) highlightBy: u32, } @@ -29,22 +49,28 @@ export function buildHighlightShader(config: Config) { return v.highlightBy == u.highlight; } fn highlightMix(v:Vertex,u:Uniforms)->f32 { - return step(0.01, abs(v.highlightBy-u.highlight)); + return 1.0-step(0.01, clamp(0.0,1.0, f32(abs(v.highlightBy-u.highlight)))); } // get the clip-space position of this vertex fn applyCamera(v:Vertex, u:Uniforms)->vec2f { + let clip = array( + vec2f(1, -1), + vec2f(1, 1), + vec2f(-1, -1), + vec2f(-1, 1) + ); let view = u.view; let pointSize = u.pointSize; let S = view.max-view.min; let R = mix(pointSize.x,pointSize.y, highlightMix(v,u)); - let dPos = v.clip*R + v.position; + let dPos = clip[v.vIndex]*R + v.position; let uPos =(dPos-view.min)/S; // now clip space return (uPos*2.0)-1.0; } fn getColor(v:Vertex, u:Uniforms)->vec4f { - return mix(vec4f(0.5,0.5,0.5,1.0),vec4f(1.0,0.,0.,1.0),highlightMix(v,u)); + return mix(vec4f(0.5,f32(v.colorBy)/40.0,0.5,1.0),vec4f(1.0,0.,0.,1.0),highlightMix(v,u)); } struct VsOutput { @@ -56,28 +82,157 @@ export function buildHighlightShader(config: Config) { var unis: Uniforms; // todo: bind a buffer (or texture) for coloring... + @vertex fn vmain(vert: Vertex)->VsOutput{ var out: VsOutput; + out.color = getColor(vert,unis); out.position = vec4f(applyCamera(vert,unis),0.5,1.0); + return out; } @fragment - fn fmain(v:VsOutput)->vec4f{ + fn fmain(v:VsOutput)->@location(0) vec4f{ return v.color; } ` } -export function buildHighlightPipeline(device: GPUDevice) { +export function buildRenderFn(device: GPUDevice) { + beginValidate(device); const prgm = buildHighlightShader({}); const module = device.createShaderModule({ code: prgm, label: 'simple scatterplot highlighting' }) + endValidate(device); + const defs = wgh.makeShaderDataDefinitions(prgm) + console.dir(defs) + beginValidate(device); + const pipeline = device.createRenderPipeline({ + label: 'scatterplot render pipe', + layout: 'auto', + vertex: { + module, + entryPoint: 'vmain', + // not using interleaved buffers, so we need 3 entries... + buffers: [{//position + stepMode: 'instance', + arrayStride: 8, + attributes: [{ + format: 'float32x2', + offset: 0, + shaderLocation: 0, + }] + }, {//colorBy + stepMode: 'instance', + arrayStride: 4, // the stride of an array may not be less than 4, so uint16 is basically only supported for interleaved arrays, which we cant really use! thanks WebGPU! + attributes: [{ + format: 'uint16', + offset: 0, + shaderLocation: 1, + }] + }, + { // highlightBy + stepMode: 'instance', + arrayStride: 4, + attributes: [{ + format: 'uint16', + offset: 0, + shaderLocation: 2, + }] + } + ] + }, + fragment: { + module, + entryPoint: 'fmain', + targets: [{ + format: 'bgra8unorm', + blend: { + alpha: { + operation: 'add', + srcFactor: 'one', + dstFactor: 'one', + }, + color: { + operation: 'add', + srcFactor: 'src-alpha', + dstFactor: 'one-minus-src-alpha', + }, + }, + }] + }, + primitive: { + topology: 'triangle-strip', + }, + }) + endValidate(device); + const { binding, 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', + }); + const bg0 = pipeline.getBindGroupLayout(0); + const bg = device.createBindGroup({ + layout: bg0, + label: 'scatterplot bindgroup 0', + entries: [{ binding, resource: uniBuffer }] + }) + // can I re-use an encoder? ANSWER: NO! what the hell is the point of all these if you cant keep them? + return (props: { + view: box2D, radius: number, highlight: number, ctx: GPUCanvasContext, blocks: ReadonlyArray<{ + columns: Record, + count: number, + }> + }) => { + const { view, ctx, highlight, radius, blocks } = props; + uniformView.set({ + view: { min: view.minCorner, max: view.maxCorner }, + pointSize: [radius / 2, radius * 2], + highlight + }); + // now copy the typed array to the actual gpu buffer: + device.queue.writeBuffer(uniBuffer, 0, uniformView.arrayBuffer) + const enc = device.createCommandEncoder({ label: 'scatterbrain encoder' }) + const pass = enc.beginRenderPass({ + colorAttachments: [ + { + clearValue: [0, 0, 0.5, 1], + loadOp: 'clear', + storeOp: 'store', + view: ctx.getCurrentTexture().createView(), + } + ] + }); -} + pass.setPipeline(pipeline); + pass.setBindGroup(0, bg); + + for (const block of blocks) { + const { count, columns } = block + pass.setVertexBuffer(0, columns.position.buffer) + pass.setVertexBuffer(1, columns.colorBy.buffer) + pass.setVertexBuffer(2, columns.highlightBy.buffer) + pass.draw(4, count); + } + pass.end(); + device.queue.submit([enc.finish()]); + } +} +export class VBO implements Cacheable { + constructor(readonly buffer: GPUBuffer) { + } + destroy() { + this.buffer.destroy(); + } + sizeInBytes() { + return this.buffer.size; + } +} From 376e96c8a34ee38ecd7f1a606f801743a1c45181 Mon Sep 17 00:00:00 2001 From: noah Date: Thu, 23 Apr 2026 16:10:25 -0700 Subject: [PATCH 06/27] WIP total reorg --- packages/scatterbrain/src/cache-client.ts | 2 - packages/scatterbrain/src/demo.html | 2 +- packages/scatterbrain/src/index.ts | 2 +- .../src/{ => render/webgl}/renderer.ts | 8 +- .../src/{ => render/webgl}/shader.test.ts | 2 +- .../src/{ => render/webgl}/shader.ts | 2 +- .../src/render/webgpu/generated.test.ts | 96 ++++++++++ .../src/render/webgpu/generated.ts | 146 ++++++++++++++++ .../src/render/webgpu/lookup-texture.ts | 79 +++++++++ .../scatterbrain/src/render/webgpu/shader.ts | 165 ++++++++++++++++++ .../src/render/webgpu/validate.ts | 15 ++ packages/scatterbrain/src/tgpu-shader.ts | 11 ++ packages/scatterbrain/src/wgpu-shader.ts | 164 +---------------- packages/scatterbrain/vite.config.ts | 2 +- site/src/examples/scatterbrain/demo.tsx | 2 - 15 files changed, 522 insertions(+), 176 deletions(-) rename packages/scatterbrain/src/{ => render/webgl}/renderer.ts (96%) rename packages/scatterbrain/src/{ => render/webgl}/shader.test.ts (99%) rename packages/scatterbrain/src/{ => render/webgl}/shader.ts (99%) create mode 100644 packages/scatterbrain/src/render/webgpu/generated.test.ts create mode 100644 packages/scatterbrain/src/render/webgpu/generated.ts create mode 100644 packages/scatterbrain/src/render/webgpu/lookup-texture.ts create mode 100644 packages/scatterbrain/src/render/webgpu/shader.ts create mode 100644 packages/scatterbrain/src/render/webgpu/validate.ts diff --git a/packages/scatterbrain/src/cache-client.ts b/packages/scatterbrain/src/cache-client.ts index f4a76bcc..cc6202ed 100644 --- a/packages/scatterbrain/src/cache-client.ts +++ b/packages/scatterbrain/src/cache-client.ts @@ -54,8 +54,6 @@ export function buildScatterbrainCacheClient( return proms; }, isValue: (v): v is Content => { - // console.log('looking for', allNeededColumns) - // console.log('in', keys(v)) for (const column of allNeededColumns) { if (!(column in v)) { return false; diff --git a/packages/scatterbrain/src/demo.html b/packages/scatterbrain/src/demo.html index 20b603c4..fd478c76 100644 --- a/packages/scatterbrain/src/demo.html +++ b/packages/scatterbrain/src/demo.html @@ -22,7 +22,7 @@ place-content: center center; } - + diff --git a/packages/scatterbrain/src/index.ts b/packages/scatterbrain/src/index.ts index 920a8cbd..cf0e5a4d 100644 --- a/packages/scatterbrain/src/index.ts +++ b/packages/scatterbrain/src/index.ts @@ -2,7 +2,7 @@ export { buildRenderFrameFn as buildScatterbrainRenderFn, setCategoricalLookupTableValues, updateCategoricalValue, -} from './renderer'; +} from './render/webgl/renderer'; export { buildScatterbrainCacheClient } from './cache-client' export * from './types'; export { getVisibleItems, loadDataset as loadScatterbrainDataset } from './dataset'; diff --git a/packages/scatterbrain/src/renderer.ts b/packages/scatterbrain/src/render/webgl/renderer.ts similarity index 96% rename from packages/scatterbrain/src/renderer.ts rename to packages/scatterbrain/src/render/webgl/renderer.ts index 55660aa0..162465f3 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 type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset } from './types'; +import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset } from '../../types'; import { Box2D, type vec4 } from '@alleninstitute/vis-geometry'; import keys from 'lodash/keys'; import reduce from 'lodash/reduce'; import type REGL from 'regl' -import { getVisibleItems, type NodeWithBounds } from './dataset'; +import { getVisibleItems, type NodeWithBounds } from '../../dataset'; import { buildScatterbrainRenderCommand, type Config, configureShader, type ShaderSettings, VBO } from './shader'; -import { buildScatterbrainCacheClient } from './cache-client'; -import { MakeTaggedBufferView } from './typed-array' +import { buildScatterbrainCacheClient } from '../../cache-client'; +import { MakeTaggedBufferView } from '../../typed-array' function columnsForItem( config: Config, col2shader: Record, 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 99% rename from packages/scatterbrain/src/shader.ts rename to packages/scatterbrain/src/render/webgl/shader.ts index 807e3339..4ab2a54b 100644 --- a/packages/scatterbrain/src/shader.ts +++ b/packages/scatterbrain/src/render/webgl/shader.ts @@ -1,7 +1,7 @@ /** biome-ignore-all lint/style/noUnusedTemplateLiteral: not at all helpful*/ import type REGL from 'regl'; -import type { ScatterbrainDataset, SlideviewScatterbrainDataset } from './types'; +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'; diff --git a/packages/scatterbrain/src/render/webgpu/generated.test.ts b/packages/scatterbrain/src/render/webgpu/generated.test.ts new file mode 100644 index 00000000..c8c5c226 --- /dev/null +++ b/packages/scatterbrain/src/render/webgpu/generated.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, test } from 'vitest'; + +import { generate } from './generated'; + + +describe('this shader is annoying', () => { + const good = /*wgsl*/` + // attribs // + struct Vertex { + @builtin(vertex_index) vIndex: u32, + @location(0) umapxy: vec2f, + @location(1) Class:u32, +@location(2) subClass:u32, +@location(3) cellId:u32, + @location(4) gaba:f32, + }; + // 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 + gaba_range:vec2f, + }; + + @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 lookupTexture: texture_2d; + @group(0) @binding(2) var gradientTexture: 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.umapxy; + let withinFilterBox = within(p.x,unis.spatialFilterBox.xz)*within(p.y,unis.spatialFilterBox.yw); + let filteredIn: f32 = withinFilterBox * + step(0.01,textureLoad(lookupTexture, vec2u(0,v.Class),0).a) * step(0.01,textureLoad(lookupTexture, vec2u(1,v.subClass),0).a) * step(0.01,textureLoad(lookupTexture, vec2u(2,v.cellId),0).a) + * within(v.gaba,unis.gaba_range); + + // highlighting + let highlighted = 1.0-step(0.1,abs(f32(v.cellId-unis.highlightValue))); + + // from filtering, we can compute color + let baseColor = + vec4(textureLoad(lookupTexture,vec2u(0,v.Class),0).rgb,1.0); + let clr = mix(unis.filteredOutColor, baseColor, filteredIn); + + // point size (todo make this a uniform...) + // todo: handle offset (slides) + let R = 2.0; + let dPos = clip[v.vIndex]*R + p; + out.color = clr; + out.position = applyCamera(dPos,unis.view); + return out; + }` + test('it looks good...', () => { + const shader = generate({ categoricalColumns: ['Class', 'subClass', 'cellId'], categoricalTable: 'lookupTexture', colorByColumn: 'Class', gradientTable: 'gradientTexture', highlightByColumn: 'cellId', mode: 'color', positionColumn: 'umapxy', quantitativeColumns: ['gaba'], samplerName: 'smpl', tableSize: [2, 40] }) + expect(shader).toEqual(good) + }) +}) \ No newline at end of file diff --git a/packages/scatterbrain/src/render/webgpu/generated.ts b/packages/scatterbrain/src/render/webgpu/generated.ts new file mode 100644 index 00000000..61e69d4f --- /dev/null +++ b/packages/scatterbrain/src/render/webgpu/generated.ts @@ -0,0 +1,146 @@ +import type { vec2 } from "@alleninstitute/vis-geometry"; +import type { ScatterbrainShaderUtils } from "../webgl/shader"; + +function rangeFor(col: string) { + 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[], tableSize: vec2, tableName: string) { + // categorical columns are in order - this array will have the same order as the col in the texture + const [w, h] = tableSize; + 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; + tableSize: vec2; + gradientTable: string; + positionColumn: string; + colorByColumn: string; + highlightByColumn: string; +}; + +export function generate(config: Config): string { + const { + mode, + quantitativeColumns, + categoricalColumns, + categoricalTable, + tableSize, + gradientTable, + positionColumn, + colorByColumn, + highlightByColumn, + } = config; + const catFilter = categoricalFilterExpression(categoricalColumns, tableSize, categoricalTable); + const rangeFilter = rangeFilterExpression(quantitativeColumns); + + + const categoryColumnIndex = categoricalColumns.indexOf(colorByColumn); + + const [w, h] = tableSize; + 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 + return /*wgsl*/ ` + // attribs // + struct Vertex { + @builtin(vertex_index) vIndex: u32, + @location(0) ${positionColumn}: vec2f, + ${categoricalColumns.map((col, i) => /*wgsl*/ `@location(${i + 1}) ${col}:u32,`).join('\n')} + ${quantitativeColumns.map((col, i) => /*wgsl*/ `@location(${i + 1 + categoricalColumns.length}) ${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}-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 = 2.0; + let dPos = clip[v.vIndex]*R + p; + out.color = clr; + out.position = applyCamera(dPos,unis.view); + return out; + } + `; + +} \ 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..54840bf2 --- /dev/null +++ b/packages/scatterbrain/src/render/webgpu/lookup-texture.ts @@ -0,0 +1,79 @@ +import type { vec4 } from "@alleninstitute/vis-geometry"; +import { reduce, keys } from "lodash"; + +/** + * 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 = keys(categories).toSorted(); + const columns = categoryKeys.length; + const rows = reduce(categoryKeys, (highest, category) => Math.max(highest, 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; + // 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 = 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: rows, rowsPerImage: columns * bytesPerPixel }, { + width: columns, + height: rows, + }); +} + +/** + * 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, + }); +} \ No newline at end of file diff --git a/packages/scatterbrain/src/render/webgpu/shader.ts b/packages/scatterbrain/src/render/webgpu/shader.ts new file mode 100644 index 00000000..25fb89b4 --- /dev/null +++ b/packages/scatterbrain/src/render/webgpu/shader.ts @@ -0,0 +1,165 @@ +import { beginValidate, endValidate } from "./validate"; +import * as wgh from 'webgpu-utils' + +type Config = {} +export function buildHighlightShader(config: Config) { + return /*wgsl*/` + + struct View { + min: vec2f, + max: vec2f, + }; + struct Uniforms { + view: View, + pointSize: vec2f, // in data space + highlight: u32, + }; + + struct Vertex { + @builtin(vertex_index) vIndex: u32, + @location(0) position: vec2f, + @location(1) colorBy: u32, + @location(2) highlightBy: u32, + } + + + + fn isHighlighted(v:Vertex,u:Uniforms)->bool{ + return v.highlightBy == u.highlight; + } + fn highlightMix(v:Vertex,u:Uniforms)->f32 { + return 1.0-step(0.01, clamp(0.0,1.0, f32(abs(v.highlightBy-u.highlight)))); + } + // get the clip-space position of this vertex + fn applyCamera(v:Vertex, u:Uniforms)->vec2f { + let clip = array( + vec2f(1, -1), + vec2f(1, 1), + vec2f(-1, -1), + vec2f(-1, 1) + ); + let view = u.view; + let pointSize = u.pointSize; + + let S = view.max-view.min; + let R = mix(pointSize.x,pointSize.y, highlightMix(v,u)); + let dPos = clip[v.vIndex]*R + v.position; + let uPos =(dPos-view.min)/S; + // now clip space + return (uPos*2.0)-1.0; + } + fn getColor(v:Vertex, u:Uniforms, tex: texture_2d, smpl:sampler)->vec4f { + const clr = textureSample(tex, smpl, ); + return mix(vec4f(0.5,f32(v.colorBy)/40.0,0.5,1.0),vec4f(1.0,0.,0.,1.0),highlightMix(v,u)); + } + + struct VsOutput { + @builtin(position) position: vec4f, + @location(0) color: vec4f, + }; + + @group(0) @binding(0) + var unis: Uniforms; + @group(0) @binding(1) var tex: sampler; + @group(0) @binding(2) var lookup: texture_2d; + + @vertex + fn vmain(vert: Vertex)->VsOutput{ + var out: VsOutput; + + out.color = getColor(vert,unis,tex); + out.position = vec4f(applyCamera(vert,unis),0.5,1.0); + return out; + } + + @fragment + fn fmain(v:VsOutput)->@location(0) vec4f{ + return v.color; + } + ` +} + +// the shader is very directly connected to the pipeline, so lets export that too... +export function buildHighlightPipeline(device: GPUDevice, config: Config) { + beginValidate(device); + const prgm = buildHighlightShader({}); + const module = device.createShaderModule({ + code: prgm, + label: 'simple scatterplot highlighting' + }) + endValidate(device); + + const defs = wgh.makeShaderDataDefinitions(prgm) + console.dir(defs) + beginValidate(device); + const pipeline = device.createRenderPipeline({ + label: 'scatterplot render pipe', + layout: 'auto', + vertex: { + module, + entryPoint: 'vmain', + // not using interleaved buffers, so we need 3 entries... + buffers: [{//position + stepMode: 'instance', + arrayStride: 8, + attributes: [{ + format: 'float32x2', + offset: 0, + shaderLocation: 0, + }] + }, {//colorBy + stepMode: 'instance', + arrayStride: 4, // the stride of an array may not be less than 4, so uint16 is basically only supported for interleaved arrays, which we cant really use! thanks WebGPU! + attributes: [{ + format: 'uint16', + offset: 0, + shaderLocation: 1, + }] + }, + { // highlightBy + stepMode: 'instance', + arrayStride: 4, + attributes: [{ + format: 'uint16', + offset: 0, + shaderLocation: 2, + }] + } + ] + }, + fragment: { + module, + entryPoint: 'fmain', + targets: [{ + format: 'bgra8unorm', + blend: { + alpha: { + operation: 'add', + srcFactor: 'one', + dstFactor: 'one', + }, + color: { + operation: 'add', + srcFactor: 'src-alpha', + dstFactor: 'one-minus-src-alpha', + }, + }, + }] + }, + primitive: { + topology: 'triangle-strip', + }, + }) + endValidate(device); + + // return the pipeline, plus hand-made, typesafe info to make it easy to bind: + // also - lets just go ahead and set up the buffer for the uniforms... + + return { pipeline, defs } +} + + + +export function buildScatterbrainRenderer(device: GPUDevice, config: Config) { + +} \ No newline at end of file diff --git a/packages/scatterbrain/src/render/webgpu/validate.ts b/packages/scatterbrain/src/render/webgpu/validate.ts new file mode 100644 index 00000000..20d3f4e0 --- /dev/null +++ b/packages/scatterbrain/src/render/webgpu/validate.ts @@ -0,0 +1,15 @@ +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/tgpu-shader.ts b/packages/scatterbrain/src/tgpu-shader.ts index 36f97c78..79a5f83a 100644 --- a/packages/scatterbrain/src/tgpu-shader.ts +++ b/packages/scatterbrain/src/tgpu-shader.ts @@ -20,6 +20,8 @@ import { Box2D, type box2D, type vec2 } from '@alleninstitute/vis-geometry'; import { pl } from 'zod/locales'; import type { F32 } from 'typegpu/data'; import { buildRenderFn as OldSchool, VBO } from './wgpu-shader'; +import { generate } from './render/webgpu/generated'; +import { beginValidate, endValidate } from './render/webgpu/validate'; @@ -273,9 +275,18 @@ const Class = 'FS00DXV0T9R1X9FJ4QE' async function loadRawJson() { return await (await fetch(tenx)).json(); } +function buildTest(device: GPUDevice) { + + const yay = generate({ categoricalColumns: ['Class', 'subclass', 'cellId'], categoricalTable: 'lookupTexture', colorByColumn: 'Class', gradientTable: 'gradientTexture', highlightByColumn: 'cellId', mode: 'color', positionColumn: 'umapxy', quantitativeColumns: ['gaba'], samplerName: 'smpl', tableSize: [2, 40] }) + beginValidate(device); + const module = device.createShaderModule({ code: yay, label: 'test shader' }) + endValidate(device); +} export async function whatever() { const root = await tgpu.init() + buildTest(root.device) + // const r = buildRenderFn(root.device); // const r = buildRenderFn(root) const dataset = await loadDataset(await loadRawJson()) diff --git a/packages/scatterbrain/src/wgpu-shader.ts b/packages/scatterbrain/src/wgpu-shader.ts index 7f9c10dd..9f13a922 100644 --- a/packages/scatterbrain/src/wgpu-shader.ts +++ b/packages/scatterbrain/src/wgpu-shader.ts @@ -1,175 +1,13 @@ -// like the webGL shader, but in wgsl (webGPU) +// like the webGL version, (shader.ts) but in wgsl (webGPU) import type { Cacheable } from '@alleninstitute/vis-core'; import type { box2D } from '@alleninstitute/vis-geometry'; -import { mapValues } from 'lodash'; import * as wgh from 'webgpu-utils' -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) - } - }) - } -} - -type Config = {} -export function buildHighlightShader(config: Config) { - return /*wgsl*/` - - struct View { - min: vec2f, - max: vec2f, - }; - struct Uniforms { - view: View, - pointSize: vec2f, // in data space - highlight: u32, - }; - - struct Vertex { - // location(0) clip: vec2f, // indexed clip-space vertex, to make points bigger than 1px - @builtin(vertex_index) vIndex: u32, - @location(0) position: vec2f, - @location(1) colorBy: u32, - @location(2) highlightBy: u32, - } - - - - fn isHighlighted(v:Vertex,u:Uniforms)->bool{ - return v.highlightBy == u.highlight; - } - fn highlightMix(v:Vertex,u:Uniforms)->f32 { - return 1.0-step(0.01, clamp(0.0,1.0, f32(abs(v.highlightBy-u.highlight)))); - } - // get the clip-space position of this vertex - fn applyCamera(v:Vertex, u:Uniforms)->vec2f { - let clip = array( - vec2f(1, -1), - vec2f(1, 1), - vec2f(-1, -1), - vec2f(-1, 1) - ); - let view = u.view; - let pointSize = u.pointSize; - let S = view.max-view.min; - let R = mix(pointSize.x,pointSize.y, highlightMix(v,u)); - let dPos = clip[v.vIndex]*R + v.position; - let uPos =(dPos-view.min)/S; - // now clip space - return (uPos*2.0)-1.0; - } - fn getColor(v:Vertex, u:Uniforms)->vec4f { - return mix(vec4f(0.5,f32(v.colorBy)/40.0,0.5,1.0),vec4f(1.0,0.,0.,1.0),highlightMix(v,u)); - } - - struct VsOutput { - @builtin(position) position: vec4f, - @location(0) color: vec4f, - }; - - @group(0) @binding(0) - var unis: Uniforms; - // todo: bind a buffer (or texture) for coloring... - - - @vertex - fn vmain(vert: Vertex)->VsOutput{ - var out: VsOutput; - - out.color = getColor(vert,unis); - out.position = vec4f(applyCamera(vert,unis),0.5,1.0); - return out; - } - - @fragment - fn fmain(v:VsOutput)->@location(0) vec4f{ - return v.color; - } - ` -} export function buildRenderFn(device: GPUDevice) { - beginValidate(device); - const prgm = buildHighlightShader({}); - const module = device.createShaderModule({ - code: prgm, - label: 'simple scatterplot highlighting' - }) - endValidate(device); - const defs = wgh.makeShaderDataDefinitions(prgm) - console.dir(defs) - beginValidate(device); - const pipeline = device.createRenderPipeline({ - label: 'scatterplot render pipe', - layout: 'auto', - vertex: { - module, - entryPoint: 'vmain', - // not using interleaved buffers, so we need 3 entries... - buffers: [{//position - stepMode: 'instance', - arrayStride: 8, - attributes: [{ - format: 'float32x2', - offset: 0, - shaderLocation: 0, - }] - }, {//colorBy - stepMode: 'instance', - arrayStride: 4, // the stride of an array may not be less than 4, so uint16 is basically only supported for interleaved arrays, which we cant really use! thanks WebGPU! - attributes: [{ - format: 'uint16', - offset: 0, - shaderLocation: 1, - }] - }, - { // highlightBy - stepMode: 'instance', - arrayStride: 4, - attributes: [{ - format: 'uint16', - offset: 0, - shaderLocation: 2, - }] - } - ] - }, - fragment: { - module, - entryPoint: 'fmain', - targets: [{ - format: 'bgra8unorm', - blend: { - alpha: { - operation: 'add', - srcFactor: 'one', - dstFactor: 'one', - }, - color: { - operation: 'add', - srcFactor: 'src-alpha', - dstFactor: 'one-minus-src-alpha', - }, - }, - }] - }, - primitive: { - topology: 'triangle-strip', - }, - }) - endValidate(device); const { binding, size } = defs.uniforms['unis']; const uniformView = wgh.makeStructuredView(defs.uniforms.unis); const uniBuffer = device.createBuffer({ diff --git a/packages/scatterbrain/vite.config.ts b/packages/scatterbrain/vite.config.ts index 40a054b0..eadcc68e 100644 --- a/packages/scatterbrain/vite.config.ts +++ b/packages/scatterbrain/vite.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ lib: { entry: resolve(import.meta.dirname, 'src/index.ts'), formats: ['es'], - fileName: 'main', + fileName: 'module', }, }, resolve: { diff --git a/site/src/examples/scatterbrain/demo.tsx b/site/src/examples/scatterbrain/demo.tsx index 26c11f3c..c5af213a 100644 --- a/site/src/examples/scatterbrain/demo.tsx +++ b/site/src/examples/scatterbrain/demo.tsx @@ -77,11 +77,9 @@ function Demo(props: Props) { setCategoricalLookupTableValues(categories, lookup); const { render, connectToCache } = buildScatterbrainRenderFn( - // @ts-expect-error we'll deal with this later regl, { ...settings, dataset }, ); - // this ts error is bogus, dont know why const renderOneFrame = () => { render({ client, From b04f894c7990290e7cc50d71988d69e11a511a6e Mon Sep 17 00:00:00 2001 From: noah Date: Fri, 24 Apr 2026 13:22:58 -0700 Subject: [PATCH 07/27] fix oh so many mistakes --- packages/scatterbrain/package.json | 4 +- packages/scatterbrain/src/index.ts | 3 + .../src/render/webgpu/generated.ts | 219 +++++++++- .../src/render/webgpu/lookup-texture.ts | 11 +- .../src/render/webgpu/renderer.ts | 217 ++++++++++ packages/scatterbrain/src/tgpu-shader.ts | 406 ++++++++++-------- packages/scatterbrain/src/wgpu-shader.ts | 116 +++-- packages/scatterbrain/vite.config.ts | 2 - pnpm-lock.yaml | 68 +-- 9 files changed, 722 insertions(+), 324 deletions(-) create mode 100644 packages/scatterbrain/src/render/webgpu/renderer.ts diff --git a/packages/scatterbrain/package.json b/packages/scatterbrain/package.json index 2c86a351..3c4aaa69 100644 --- a/packages/scatterbrain/package.json +++ b/packages/scatterbrain/package.json @@ -37,7 +37,8 @@ ], "scripts": { "typecheck": "tsc --noEmit", - "build": "vite build", + "vbuild": "vite build", + "build": "parcel build --no-cache", "dev": "parcel watch --port 1239", "demo": "vite", "test": "vitest --watch", @@ -68,7 +69,6 @@ "@types/lodash": "4.17.24", "@types/node": "22.19.15", "@webgpu/types": "0.1.69", - "unplugin-typegpu": "0.11.0", "vite": "8.0.8", "vite-plugin-dts": "4.5.4" } diff --git a/packages/scatterbrain/src/index.ts b/packages/scatterbrain/src/index.ts index cf0e5a4d..874be366 100644 --- a/packages/scatterbrain/src/index.ts +++ b/packages/scatterbrain/src/index.ts @@ -3,6 +3,9 @@ export { setCategoricalLookupTableValues, updateCategoricalValue, } from './render/webgl/renderer'; +export { + buildRenderFrameFn as buildWebGPUScatterbrainRenderFn, +} from './render/webgpu/renderer'; export { buildScatterbrainCacheClient } from './cache-client' export * from './types'; export { getVisibleItems, loadDataset as loadScatterbrainDataset } from './dataset'; diff --git a/packages/scatterbrain/src/render/webgpu/generated.ts b/packages/scatterbrain/src/render/webgpu/generated.ts index 61e69d4f..a64e9809 100644 --- a/packages/scatterbrain/src/render/webgpu/generated.ts +++ b/packages/scatterbrain/src/render/webgpu/generated.ts @@ -1,16 +1,18 @@ -import type { vec2 } from "@alleninstitute/vis-geometry"; -import type { ScatterbrainShaderUtils } from "../webgl/shader"; +import { difference, isEqual, keys, map } from "lodash"; +import { beginValidate, endValidate } from "./validate"; +import * as wgh from 'webgpu-utils' +import type { vec2, vec4 } from "@alleninstitute/vis-geometry"; +import { setCategoricalLookupTableValues } from "./lookup-texture"; -function rangeFor(col: string) { +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[], tableSize: vec2, tableName: string) { +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 - const [w, h] = tableSize; return categoricalColumns .map( (attrib, i) => @@ -24,12 +26,24 @@ export type Config = { quantitativeColumns: string[]; categoricalColumns: string[]; categoricalTable: string; - tableSize: vec2; gradientTable: string; positionColumn: string; colorByColumn: string; - highlightByColumn: 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 { @@ -37,19 +51,17 @@ export function generate(config: Config): string { quantitativeColumns, categoricalColumns, categoricalTable, - tableSize, gradientTable, positionColumn, colorByColumn, highlightByColumn, } = config; - const catFilter = categoricalFilterExpression(categoricalColumns, tableSize, categoricalTable); + const catFilter = categoricalFilterExpression(categoricalColumns, categoricalTable); const rangeFilter = rangeFilterExpression(quantitativeColumns); const categoryColumnIndex = categoricalColumns.indexOf(colorByColumn); - const [w, h] = tableSize; const colorByCategorical = /*wgsl*/ ` vec4(textureLoad(${categoricalTable},vec2u(${categoryColumnIndex.toFixed(0)},v.${colorByColumn}),0).rgb,1.0)`; @@ -59,13 +71,15 @@ export function generate(config: Config): string { 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 + 1}) ${col}:u32,`).join('\n')} - ${quantitativeColumns.map((col, i) => /*wgsl*/ `@location(${i + 1 + categoricalColumns.length}) ${col}:f32,`).join('\n')} + ${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 { @@ -127,7 +141,7 @@ export function generate(config: Config): string { * ${rangeFilter.length > 0 ? rangeFilter : '1.0'}; // highlighting - let highlighted = 1.0-step(0.1,abs(f32(v.${highlightByColumn}-unis.highlightValue))); + let highlighted = 1.0-step(0.1,abs(f32(v.${highlightByColumn.column}-unis.highlightValue))); // from filtering, we can compute color let baseColor = ${colorize}; @@ -135,12 +149,189 @@ export function generate(config: Config): string { // point size (todo make this a uniform...) // todo: handle offset (slides) - let R = 2.0; + 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, + }] + }, + ...map(categoricalColumns, (cat, i): GPUVertexBufferLayout => ({ + arrayStride: 4, + attributes: [{ + format: 'uint32', + offset: 0, + shaderLocation: catStart + i + }], + stepMode: 'instance' + })), + ...map(quantitativeColumns, (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', + }); + const BGL = pipeline.getBindGroupLayout(0); + // const lookupBindGroupLayout = pipeline.getBindGroupLayout(1); + // const gradientBindGroupLayout = pipeline.getBindGroupLayout(2); + // gradientBindGroupLayout.label = 'gradient bgLayout' // can I do that? + + // const bg0 = device.createBindGroup({ + // layout: BGL, + // label: 'scatterplot uniform bind group', + // entries: [ + // { binding: 0, resource: uniBuffer }, + + // ] + // }) + // hmmm, if we want to re-create the texture... we'd need to build a new bindGroup... + // that is annoying! + 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 + console.warn('warning - not enough data to update gradient texture') + } + // const bg = device.createBindGroup({ + // label: 'gradient bg', + // layout: gradientBindGroupLayout, + // entries: [ + // { binding: 2, resource: gradientTexture } + // ] + // }) + 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 } + } + let 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 device.createBindGroup({ + // layout: lookupBindGroupLayout, + // entries: [ + return { binding: 1, resource: lookupTable }; + // ] + // }); + } + if (isEqual(keys(categories).toSorted(), 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!! + } + return { pipeline, updateGradient, updateUniforms, updateCategorical }; } \ 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 index 54840bf2..b82c089e 100644 --- a/packages/scatterbrain/src/render/webgpu/lookup-texture.ts +++ b/packages/scatterbrain/src/render/webgpu/lookup-texture.ts @@ -11,7 +11,6 @@ import { reduce, keys } from "lodash"; * @param texture */ export function setCategoricalLookupTableValues( - categories: Record>, device: GPUDevice, texture: GPUTexture, @@ -23,6 +22,13 @@ export function setCategoricalLookupTableValues( 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! + 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]]; @@ -37,10 +43,11 @@ export function setCategoricalLookupTableValues( data.set(rgbf, rowIndex * columns * 4 + columnIndex * 4); } } - device.queue.writeTexture({ texture }, data, { bytesPerRow: rows, rowsPerImage: columns * bytesPerPixel }, { + device.queue.writeTexture({ texture }, data, { bytesPerRow: columns * bytesPerPixel, rowsPerImage: rows }, { width: columns, height: rows, }); + return texture; } /** diff --git a/packages/scatterbrain/src/render/webgpu/renderer.ts b/packages/scatterbrain/src/render/webgpu/renderer.ts new file mode 100644 index 00000000..ae885ba4 --- /dev/null +++ b/packages/scatterbrain/src/render/webgpu/renderer.ts @@ -0,0 +1,217 @@ +import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset, WebGLSafeBasicType } from "~/src/types"; +import { buildPipeline, generate, type Config, type Uniforms } from "./generated"; +import { keys, map, omit, pick, reduce } from "lodash"; +import type { ShaderSettings as BaseSettings } from "../webgl/shader"; +import { getVisibleItems, type NodeWithBounds } from "~/src/dataset"; +import type { Cacheable, SharedPriorityCache } from "@alleninstitute/vis-core"; +import { buildScatterbrainCacheClient } from "~/src/cache-client"; +import { Box2D, type box2D, type vec2, type vec4 } from "@alleninstitute/vis-geometry"; +import { beginValidate, endValidate } from "./validate"; + +export type ShaderSettings = BaseSettings & { + 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 = reduce( + keys(col2shader), + (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, updateCategorical, updateGradient, updateUniforms } = 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 allColumns = [...config.categoricalColumns, ...config.quantitativeColumns, config.positionColumn]; + const client = buildScatterbrainCacheClient(allColumns, cache, toGpuBuffer, onDataArrived); + return client; + }; + const viridis = new Uint8Array(256 * 4); + // ugh todo + for (let i = 0; i < 256; i += 1) { + viridis[i * 4 + 0] = i; + viridis[i * 4 + 1] = i; + viridis[i * 4 + 2] = i; + viridis[i * 4 + 3] = 255; + } + const render = (props: RenderPassProps & { client: ReturnType> }) => { + const { target, categories, uniforms, client } = props; + const { camera } = uniforms + beginValidate(device); + + const bg0 = updateUniforms({ ...omit(uniforms, 'camera', 'spatialFilterBox', 'quantitativeRangeFilters'), view: Box2D.toFlatArray(uniforms.camera.view), spatialFilterBox: Box2D.toFlatArray(uniforms.spatialFilterBox), ...uniforms.quantitativeRangeFilters }); + const bg1 = updateCategorical(categories); + const bg2 = updateGradient(viridis); // todo - dont do this every frame... + + // so... the gad damn bindings - if you dont use a binding, it needs to be omitted from + // the freaking bg.. that means our gradient texture shouldnt be added if we dont have any quant stuff... + let entries: GPUBindGroupEntry[] = [bg0] + if (keys(categories).length > 0) { + entries.push(bg1) + } + if (keys(uniforms.quantitativeRangeFilters).length > 0) { + entries.push(bg2) + } + 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, []) + // console.log('visible: ', visible.length) + 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); + } + //bind all the vbo... + // with the correct dang locations, ugh + pass.draw(4, count); + } + } + } + pass.end(); + device.queue.submit([enc.finish()]); + endValidate(device); + } + return { render, connectToCache } +} + +export type RenderPassProps = { + target: GPUTextureView + uniforms: { + camera: { view: box2D; screenResolution: vec2 }; + offset: vec2; + filteredOutColor: vec4; + spatialFilterBox: box2D; + quantitativeRangeFilters: Record; + highlightedValue: number; + } + // categoricalLookupTable: GPUTexture + // gradient: GPUTexture; + categories: Readonly>>>; + gradient: Uint8Array; + +}; + +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 = keys(categoricalFilters).toSorted(); + // const numCategories = categories.length; + // const longestCategory = reduce( + // keys(categoricalFilters), + // (highest, cur) => Math.max(highest, categoricalFilters[cur]), + // 0, + // ); + + // 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 = reduce( + quantitativeFilters.toSorted(), + (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 = reduce( + categories, + (catAttrs, categoricalFilter, i) => ({ ...catAttrs, [categoricalFilter]: `CATEGORY_${i.toFixed(0)}` }), + initialCategoricalAttrs, + ); + + const colToAttribute = { + ...qAttrs, + ...cAttrs, + [dataset.metadata.spatialColumn]: 'position' + } + const ordered = map([...categories, ...quantitativeFilters.toSorted()], (col) => colToAttribute[col]) + const config: Config = { + categoricalColumns: keys(cAttrs).map((columnName) => colToAttribute[columnName]), + quantitativeColumns: 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 }; +} \ No newline at end of file diff --git a/packages/scatterbrain/src/tgpu-shader.ts b/packages/scatterbrain/src/tgpu-shader.ts index 79a5f83a..aa2d8df9 100644 --- a/packages/scatterbrain/src/tgpu-shader.ts +++ b/packages/scatterbrain/src/tgpu-shader.ts @@ -8,20 +8,19 @@ // and lets try it with typeGPU generating our shaders for us... which I must admit seems pretty good... -import tgpu, { d, std, type TgpuRoot } from 'typegpu'; import { buildScatterbrainCacheClient } from './cache-client'; -import type { ShaderSettings } from './shader'; import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset } from './types'; import { uniq } from 'lodash'; import { SharedPriorityCache, type Cacheable } from '@alleninstitute/vis-core'; import type { WebGLSafeBasicType } from './typed-array'; import { getVisibleItems, loadDataset, type NodeWithBounds } from './dataset'; -import { Box2D, type box2D, type vec2 } from '@alleninstitute/vis-geometry'; +import { Box2D, type box2D, type vec2, type vec4 } from '@alleninstitute/vis-geometry'; import { pl } from 'zod/locales'; import type { F32 } from 'typegpu/data'; -import { buildRenderFn as OldSchool, VBO } from './wgpu-shader'; +// import { buildRenderFn as OldSchool, VBO } from './wgpu-shader'; import { generate } from './render/webgpu/generated'; import { beginValidate, endValidate } from './render/webgpu/validate'; +import { buildRenderFrameFn, type ShaderSettings } from './render/webgpu/renderer'; @@ -106,166 +105,166 @@ export type SimpleSettings = { // } as const; // annoying hurdle 1 - I dont know how to use root.createBuffer() correctly to upload my raw bytes -type RenderProps = { - camera: { view: box2D, screenResolution: vec2 } - dataset: ScatterbrainDataset | SlideviewScatterbrainDataset; - client: ReturnType>; - visibilityThresholdPx: number; - ctx: GPUCanvasContext -}; - -export function buildScatterbrainTGPU(root: TgpuRoot, settings: SimpleSettings) { - 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... - // this is probably why the typeGPU thing didnt work right either... - const B = root.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)) - root.device.queue.writeBuffer(B, 0, u32.buffer); - return new VBO(B); - } - const B = root.device.createBuffer({ size: buffer.byteLength, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX }); - root.device.queue.writeBuffer(B, 0, buffer, 0, buffer.byteLength); - return new VBO(B); - } - const prepareQtCell = columnsForItem(settings); - const drawQtCell = buildRenderFn(root); - const drawQtCells = OldSchool(root.device); - const render = (props: RenderProps) => { - const { camera, dataset, client, visibilityThresholdPx } = props; - const visibilityThreshold = (visibilityThresholdPx * Box2D.size(camera.view)[0]) / camera.screenResolution[0]; // (units*pixel)/pixel ==> units - const visibleQtNodes = getVisibleItems(dataset, camera, visibilityThreshold).map(prepareQtCell); - client.setPriorities(visibleQtNodes, []); - // console.log("visible: ", visibleQtNodes.length) - const blocks: Array<{ columns: Record, count: number }> = [] - for (const node of visibleQtNodes) { - if (client.has(node)) { - const drawable = client.get(node); - if (drawable) { - // // console.log('draw it: ', node.node.file) - // drawQtCell({ - // count: node.node.numSpecimens, - // columns: drawable, - // ctx: props.ctx, - // highlight: 4, - // radius: .05, - // view: props.camera.view, - // }) - blocks.push({ count: node.node.numSpecimens, columns: drawable }) - } - } - } - drawQtCells({ ctx: props.ctx, highlight: 4, radius: 0.05, view: props.camera.view, blocks }) - } +// type RenderProps = { +// camera: { view: box2D, screenResolution: vec2 } +// dataset: ScatterbrainDataset | SlideviewScatterbrainDataset; +// client: ReturnType>; +// visibilityThresholdPx: number; +// ctx: GPUCanvasContext +// }; + +// export function buildScatterbrainTGPU(root: TgpuRoot, settings: SimpleSettings) { +// 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... +// // this is probably why the typeGPU thing didnt work right either... +// const B = root.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)) +// root.device.queue.writeBuffer(B, 0, u32.buffer); +// return new VBO(B); +// } +// const B = root.device.createBuffer({ size: buffer.byteLength, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX }); +// root.device.queue.writeBuffer(B, 0, buffer, 0, buffer.byteLength); +// return new VBO(B); +// } +// const prepareQtCell = columnsForItem(settings); +// const drawQtCell = buildRenderFn(root); +// const drawQtCells = OldSchool(root.device); +// const render = (props: RenderProps) => { +// const { camera, dataset, client, visibilityThresholdPx } = props; +// const visibilityThreshold = (visibilityThresholdPx * Box2D.size(camera.view)[0]) / camera.screenResolution[0]; // (units*pixel)/pixel ==> units +// const visibleQtNodes = getVisibleItems(dataset, camera, visibilityThreshold).map(prepareQtCell); +// client.setPriorities(visibleQtNodes, []); +// // console.log("visible: ", visibleQtNodes.length) +// const blocks: Array<{ columns: Record, count: number }> = [] +// for (const node of visibleQtNodes) { +// if (client.has(node)) { +// const drawable = client.get(node); +// if (drawable) { +// // // console.log('draw it: ', node.node.file) +// // drawQtCell({ +// // count: node.node.numSpecimens, +// // columns: drawable, +// // ctx: props.ctx, +// // highlight: 4, +// // radius: .05, +// // view: props.camera.view, +// // }) +// blocks.push({ count: node.node.numSpecimens, columns: drawable }) +// } +// } +// } +// drawQtCells({ ctx: props.ctx, highlight: 4, radius: 0.05, view: props.camera.view, blocks }) +// } + +// const connectToCache = (cache: SharedPriorityCache, onDataArrived: () => void) => { +// return buildScatterbrainCacheClient(['position', 'colorBy', 'highlightBy'], cache, toGpuBuffer, onDataArrived); +// }; + +// return { render, connectToCache }; - const connectToCache = (cache: SharedPriorityCache, onDataArrived: () => void) => { - return buildScatterbrainCacheClient(['position', 'colorBy', 'highlightBy'], cache, toGpuBuffer, onDataArrived); - }; - - return { render, connectToCache }; - -} - -function columnsForItem( - config: SimpleSettings, -) { - const columns: Record = { - position: { type: 'METADATA', name: config.dataset.metadata.spatialColumn }, - colorBy: { type: 'METADATA', name: config.colorBy.column }, - highlightBy: { type: 'METADATA', name: config.highlightBy.column } - }; +// } - return (item: T) => { - return { ...item, dataset: config.dataset, columns }; - }; -} +// function columnsForItem( +// config: SimpleSettings, +// ) { +// const columns: Record = { +// position: { type: 'METADATA', name: config.dataset.metadata.spatialColumn }, +// colorBy: { type: 'METADATA', name: config.colorBy.column }, +// highlightBy: { type: 'METADATA', name: config.highlightBy.column } +// }; + +// return (item: T) => { +// return { ...item, dataset: config.dataset, columns }; +// }; +// } // so this version has problems // I cannot figure out for the life of me how to get uint16 buffer in as a vertex attribute.... // I think I'm gonna try (again) to just make the meat of the shader, then feed it to a template via resolveWithContext // that will mean I'll roll the pipeline by hand, but honestly I dont see how else to make this work... -function buildRenderFn(root: TgpuRoot) { - // ok - heres where we do the thing, which is to create a pipeline - // and return a function that invokes it... - - const View = d.struct({ min: d.vec2f, max: d.vec2f }) - const Vertex = { vIndex: d.builtin.vertexIndex, position: d.vec2f, highlightBy: d.u32 } - const unis = tgpu.bindGroupLayout({ view: { uniform: View }, highlight: { uniform: d.u32 }, radius: { uniform: d.f32 } }); - - // ok because we dont use interleaved buffers - // we have to hand-roll the layouts - do we need locations here? - const pLayout = tgpu.vertexLayout(d.arrayOf(d.vec2f), 'instance') - const hLayout = tgpu.vertexLayout(d.arrayOf(d.u32), 'instance') - // const hLayout: GPUVertexBufferLayout = { - // ...wLayout, - // arrayStride: 0, - // } +// function buildRenderFn(root: TgpuRoot) { +// // ok - heres where we do the thing, which is to create a pipeline +// // and return a function that invokes it... - const vmain = tgpu.vertexFn({ - in: Vertex, - out: { pos: d.builtin.position, color: d.vec4f }, - })(function ({ vIndex, position, highlightBy }) { - 'use gpu'; - const clip = [d.vec2f(1, -1), d.vec2f(1, 1), d.vec2f(-1, -1), d.vec2f(-1, 1)]; - const size = std.sub(unis.$.view.max, unis.$.view.min); - const dP = std.add(position, std.mul(clip[vIndex], unis.$.radius)); - const p = std.sub(std.mul(std.div(std.sub(dP, unis.$.view.min), size), 2), 1); - const clr = std.mix(d.vec4f(1, 0, 0, 1), d.vec4f(0.5, 0.5, 0.5, 1), std.step(0.001, std.abs(std.sub(highlightBy, unis.$.highlight)))) - return { - pos: d.vec4f(p.xy, 0, 1), - color: clr, - }; - }); +// const View = d.struct({ min: d.vec2f, max: d.vec2f }) +// const Vertex = { vIndex: d.builtin.vertexIndex, position: d.vec2f, highlightBy: d.u32 } +// const unis = tgpu.bindGroupLayout({ view: { uniform: View }, highlight: { uniform: d.u32 }, radius: { uniform: d.f32 } }); + +// // ok because we dont use interleaved buffers +// // we have to hand-roll the layouts - do we need locations here? +// const pLayout = tgpu.vertexLayout(d.arrayOf(d.vec2f), 'instance') +// const hLayout = tgpu.vertexLayout(d.arrayOf(d.u32), 'instance') +// // const hLayout: GPUVertexBufferLayout = { +// // ...wLayout, +// // arrayStride: 0, +// // } - const fmain = tgpu.fragmentFn({ in: { pos: d.builtin.position, color: d.vec4f }, out: d.vec4f })(function ({ color }) { - return color; - }); +// const vmain = tgpu.vertexFn({ +// in: Vertex, +// out: { pos: d.builtin.position, color: d.vec4f }, +// })(function ({ vIndex, position, highlightBy }) { +// 'use gpu'; +// const clip = [d.vec2f(1, -1), d.vec2f(1, 1), d.vec2f(-1, -1), d.vec2f(-1, 1)]; +// const size = std.sub(unis.$.view.max, unis.$.view.min); +// const dP = std.add(position, std.mul(clip[vIndex], unis.$.radius)); +// const p = std.sub(std.mul(std.div(std.sub(dP, unis.$.view.min), size), 2), 1); +// const clr = std.mix(d.vec4f(1, 0, 0, 1), d.vec4f(0.5, 0.5, 0.5, 1), std.step(0.001, std.abs(std.sub(highlightBy, unis.$.highlight)))) +// return { +// pos: d.vec4f(p.xy, 0, 1), +// color: clr, +// }; +// }); - // lets create some uniforms... - const view = root.createUniform(View) - const highlight = root.createUniform(d.u32) - const radius = root.createUniform(d.f32) +// const fmain = tgpu.fragmentFn({ in: { pos: d.builtin.position, color: d.vec4f }, out: d.vec4f })(function ({ color }) { +// return color; +// }); - const bg = root.createBindGroup(unis, { - view: view.buffer, - highlight: highlight.buffer, - radius: radius.buffer - }) - // this next part - it does pick up the types from - // the vertex shader... but some other stuff is mysterious - // also raw createPipeline has a fair bit of type-safety, although its - // true it cant line up named attribs, it only works by location index - const pipeline = root.createRenderPipeline({ - vertex: vmain, - fragment: fmain, - attribs: { - highlightBy: hLayout.attrib, - position: pLayout.attrib, - }, - primitive: { topology: 'triangle-strip', cullMode: 'back' }, - // depthStencil: { - // format: 'depth24plus', - // depthWriteEnabled: true, - // depthCompare: 'less', - // }, - }) - // console.log(root.unwrap(pipeline)) - return (props: { view: box2D, radius: number, highlight: number, count: number, ctx: GPUCanvasContext, columns: Record }) => { - // write the unis... - const { position, highlightBy } = props.columns; - view.patch({ min: props.view.minCorner, max: props.view.maxCorner }); // ugh - highlight.patch(props.highlight); - radius.patch(props.radius); - pipeline - .with(bg) - - .withColorAttachment({ view: props.ctx, loadOp: 'load', }) - .with(pLayout, position.buffer) - .with(hLayout, highlightBy.buffer) - .draw(4, props.count); - } -} +// // lets create some uniforms... +// const view = root.createUniform(View) +// const highlight = root.createUniform(d.u32) +// const radius = root.createUniform(d.f32) + +// const bg = root.createBindGroup(unis, { +// view: view.buffer, +// highlight: highlight.buffer, +// radius: radius.buffer +// }) +// // this next part - it does pick up the types from +// // the vertex shader... but some other stuff is mysterious +// // also raw createPipeline has a fair bit of type-safety, although its +// // true it cant line up named attribs, it only works by location index +// const pipeline = root.createRenderPipeline({ +// vertex: vmain, +// fragment: fmain, +// attribs: { +// highlightBy: hLayout.attrib, +// position: pLayout.attrib, +// }, +// primitive: { topology: 'triangle-strip', cullMode: 'back' }, +// // depthStencil: { +// // format: 'depth24plus', +// // depthWriteEnabled: true, +// // depthCompare: 'less', +// // }, +// }) +// // console.log(root.unwrap(pipeline)) +// return (props: { view: box2D, radius: number, highlight: number, count: number, ctx: GPUCanvasContext, columns: Record }) => { +// // write the unis... +// const { position, highlightBy } = props.columns; +// view.patch({ min: props.view.minCorner, max: props.view.maxCorner }); // ugh +// highlight.patch(props.highlight); +// radius.patch(props.radius); +// pipeline +// .with(bg) + +// .withColorAttachment({ view: props.ctx, loadOp: 'load', }) +// .with(pLayout, position.buffer) +// .with(hLayout, highlightBy.buffer) +// .draw(4, props.count); +// } +// } const tenx = @@ -275,17 +274,53 @@ const Class = 'FS00DXV0T9R1X9FJ4QE' async function loadRawJson() { return await (await fetch(tenx)).json(); } -function buildTest(device: GPUDevice) { +// function buildTest(device: GPUDevice) { + +// const yay = generate({ categoricalColumns: ['Class', 'subclass', 'cellId'], categoricalTable: 'lookupTexture', colorByColumn: 'Class', gradientTable: 'gradientTexture', highlightByColumn: 'cellId', mode: 'color', positionColumn: 'umapxy', quantitativeColumns: ['gaba'], samplerName: 'smpl', tableSize: [2, 40] }) +// beginValidate(device); +// const module = device.createShaderModule({ code: yay, label: 'test shader' }) +// endValidate(device); +// } +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; +}; - const yay = generate({ categoricalColumns: ['Class', 'subclass', 'cellId'], categoricalTable: 'lookupTexture', colorByColumn: 'Class', gradientTable: 'gradientTexture', highlightByColumn: 'cellId', mode: 'color', positionColumn: 'umapxy', quantitativeColumns: ['gaba'], samplerName: 'smpl', tableSize: [2, 40] }) - beginValidate(device); - const module = device.createShaderModule({ code: yay, label: 'test shader' }) - endValidate(device); -} export async function whatever() { - const root = await tgpu.init() - buildTest(root.device) + + 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 r = buildRenderFn(root.device); // const r = buildRenderFn(root) @@ -294,7 +329,7 @@ export async function whatever() { throw new Error('blerg this data is toast') } const cache = new SharedPriorityCache(new Map(), 1024 * 1024 * 2000); - const { render, connectToCache } = buildScatterbrainTGPU(root, { colorBy: { kind: 'metadata', column: Class }, highlightBy: { kind: 'metadata', column: Class }, dataset }) + const { render, connectToCache } = buildRenderFrameFn(device, { ...settings, dataset }) // const toGpuBuffer = (buffer: ArrayBuffer, _type: WebGLSafeBasicType) => { // const B = root.device.createBuffer({ size: buffer.byteLength, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX }); // root.device.queue.writeBuffer(B, 0, buffer); @@ -306,29 +341,48 @@ export async function whatever() { cnvs.height = 1500; const ctx = cnvs.getContext('webgpu') ctx?.configure({ - device: root.device, + 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(() => { - // console.log('re render!') - - render({ camera: { view, screenResolution: [1500, 1500] }, client, ctx: ctx!, dataset, visibilityThresholdPx: 10 }) - }) + 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, + } }) - render({ camera: { view, screenResolution: [1500, 1500] }, client, ctx: ctx!, dataset, visibilityThresholdPx: 10 }) - - // const position = toGpuBuffer(new Float32Array([3, 3, 40, 10, 25, 40]).buffer, 'float') - // const highlightBy = toGpuBuffer(new Uint32Array([0, 1, 3]).buffer, 'uint32') - // const columns = { - // position, - // highlightBy, - // } - // r({ ctx: ctx!, columns, count: 3, highlight: 3, radius: 4, view: { minCorner: [0, 0], maxCorner: [50, 50] } }) } whatever(); \ No newline at end of file diff --git a/packages/scatterbrain/src/wgpu-shader.ts b/packages/scatterbrain/src/wgpu-shader.ts index 9f13a922..0801b979 100644 --- a/packages/scatterbrain/src/wgpu-shader.ts +++ b/packages/scatterbrain/src/wgpu-shader.ts @@ -6,71 +6,61 @@ import * as wgh from 'webgpu-utils' -export function buildRenderFn(device: GPUDevice) { +// export function buildRenderFn(device: GPUDevice) { - const { binding, 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', - }); - const bg0 = pipeline.getBindGroupLayout(0); - const bg = device.createBindGroup({ - layout: bg0, - label: 'scatterplot bindgroup 0', - entries: [{ binding, resource: uniBuffer }] - }) - // can I re-use an encoder? ANSWER: NO! what the hell is the point of all these if you cant keep them? - return (props: { - view: box2D, radius: number, highlight: number, ctx: GPUCanvasContext, blocks: ReadonlyArray<{ - columns: Record, - count: number, - }> - }) => { - const { view, ctx, highlight, radius, blocks } = props; - uniformView.set({ - view: { min: view.minCorner, max: view.maxCorner }, - pointSize: [radius / 2, radius * 2], - highlight - }); - // now copy the typed array to the actual gpu buffer: - device.queue.writeBuffer(uniBuffer, 0, uniformView.arrayBuffer) - const enc = device.createCommandEncoder({ label: 'scatterbrain encoder' }) +// const { binding, 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', +// }); +// const bg0 = pipeline.getBindGroupLayout(0); +// const bg = device.createBindGroup({ +// layout: bg0, +// label: 'scatterplot bindgroup 0', +// entries: [{ binding, resource: uniBuffer }] +// }) +// // can I re-use an encoder? ANSWER: NO! what the hell is the point of all these if you cant keep them? +// return (props: { +// view: box2D, radius: number, highlight: number, ctx: GPUCanvasContext, blocks: ReadonlyArray<{ +// columns: Record, +// count: number, +// }> +// }) => { +// const { view, ctx, highlight, radius, blocks } = props; +// uniformView.set({ +// view: { min: view.minCorner, max: view.maxCorner }, +// pointSize: [radius / 2, radius * 2], +// highlight +// }); +// // now copy the typed array to the actual gpu buffer: +// device.queue.writeBuffer(uniBuffer, 0, uniformView.arrayBuffer) +// const enc = device.createCommandEncoder({ label: 'scatterbrain encoder' }) - const pass = enc.beginRenderPass({ - colorAttachments: [ - { - clearValue: [0, 0, 0.5, 1], - loadOp: 'clear', - storeOp: 'store', - view: ctx.getCurrentTexture().createView(), - } - ] - }); +// const pass = enc.beginRenderPass({ +// colorAttachments: [ +// { +// clearValue: [0, 0, 0.5, 1], +// loadOp: 'clear', +// storeOp: 'store', +// view: ctx.getCurrentTexture().createView(), +// } +// ] +// }); - pass.setPipeline(pipeline); - pass.setBindGroup(0, bg); +// pass.setPipeline(pipeline); +// pass.setBindGroup(0, bg); - for (const block of blocks) { - const { count, columns } = block - pass.setVertexBuffer(0, columns.position.buffer) - pass.setVertexBuffer(1, columns.colorBy.buffer) - pass.setVertexBuffer(2, columns.highlightBy.buffer) - pass.draw(4, count); - } +// for (const block of blocks) { +// const { count, columns } = block +// pass.setVertexBuffer(0, columns.position.buffer) +// pass.setVertexBuffer(1, columns.colorBy.buffer) +// pass.setVertexBuffer(2, columns.highlightBy.buffer) +// pass.draw(4, count); +// } - pass.end(); - device.queue.submit([enc.finish()]); - } -} -export class VBO implements Cacheable { - constructor(readonly buffer: GPUBuffer) { - } - destroy() { - this.buffer.destroy(); - } - sizeInBytes() { - return this.buffer.size; - } -} +// pass.end(); +// device.queue.submit([enc.finish()]); +// } +// } diff --git a/packages/scatterbrain/vite.config.ts b/packages/scatterbrain/vite.config.ts index eadcc68e..be27a805 100644 --- a/packages/scatterbrain/vite.config.ts +++ b/packages/scatterbrain/vite.config.ts @@ -1,7 +1,6 @@ import { defineConfig } from 'vite' import { resolve } from 'node:path'; import dts from 'vite-plugin-dts'; -import typegpuPlugin from 'unplugin-typegpu/vite'; export default defineConfig({ build: { @@ -17,7 +16,6 @@ export default defineConfig({ }, }, plugins: [ - typegpuPlugin(), dts({ rollupTypes: true, }), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8329446..b557351f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,9 +126,6 @@ importers: '@webgpu/types': specifier: 0.1.69 version: 0.1.69 - unplugin-typegpu: - specifier: 0.11.0 - version: 0.11.0(typegpu@0.11.2) vite: specifier: 8.0.8 version: 8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(yaml@2.8.1) @@ -211,6 +208,9 @@ importers: '@types/node': specifier: 22.1.0 version: 22.1.0 + '@webgpu/types': + specifier: 0.1.69 + version: 0.1.69 packages: @@ -2287,10 +2287,6 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-kit@2.2.0: - resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} - engines: {node: '>=20.19.0'} - astring@1.9.0: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true @@ -3225,10 +3221,6 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - magic-string-ast@1.0.3: - resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==} - engines: {node: '>=20.19.0'} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3954,10 +3946,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyest-for-wgsl@0.3.2: - resolution: {integrity: sha512-Y5IBd0In6btjdJ3Tv0PTWeDajDc9Phwd8G/3gW3p3hbbr02JTkwqjvN9JeJsvuFmnmAStU+t05Zu/8Fl5oIOvw==} - engines: {node: '>=12.20.0'} - tinyest@0.3.1: resolution: {integrity: sha512-SJNnjbvTEo5VmIjsMYpUFL34b9RyaI382r1v7gyVXZpd4VnjIZKMrGk1mphXM4zkhrs3hZfO1Xwv63DoZX50yw==} engines: {node: '>=12.20.0'} @@ -4098,15 +4086,6 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - unplugin-typegpu@0.11.0: - resolution: {integrity: sha512-5uEOrSbs4H+hFqlMd7PcAQAIxJy5uMl5gGdZurI6NeyQWaNBxO4jkHFo/yrxjXpSiQFiz431j7sou2eSIjjzCA==} - peerDependencies: - typegpu: ^0.11.0 - - unplugin@3.0.0: - resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} - engines: {node: ^20.19.0 || >=22.12.0} - unstorage@1.17.4: resolution: {integrity: sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==} peerDependencies: @@ -4438,9 +4417,6 @@ packages: webgpu-utils@2.0.2: resolution: {integrity: sha512-uoReAiZwl15ITelmp7hHL+eXg/E6VsRDFqWP4ZHkDruAO8pXS/cZGNY+vOWAc6LALJ8zefK1skUl9AVRuv5ijg==} - webpack-virtual-modules@0.6.2: - resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - which-pm-runs@1.1.0: resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} engines: {node: '>=4'} @@ -6705,11 +6681,6 @@ snapshots: assertion-error@2.0.1: {} - ast-kit@2.2.0: - dependencies: - '@babel/parser': 7.29.2 - pathe: 2.0.3 - astring@1.9.0: {} astro-expressive-code@0.41.3(astro@5.17.1(@types/node@22.1.0)(lightningcss@1.32.0)(rollup@4.60.0)(typescript@5.9.3)(yaml@2.8.1)): @@ -7778,10 +7749,6 @@ snapshots: dependencies: yallist: 4.0.0 - magic-string-ast@1.0.3: - dependencies: - magic-string: 0.30.21 - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -8965,10 +8932,6 @@ snapshots: tinybench@2.9.0: {} - tinyest-for-wgsl@0.3.2: - dependencies: - tinyest: 0.3.1 - tinyest@0.3.1: {} tinyexec@1.0.2: {} @@ -9103,29 +9066,6 @@ snapshots: universalify@2.0.1: {} - unplugin-typegpu@0.11.0(typegpu@0.11.2): - dependencies: - '@babel/parser': 7.29.2 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - ast-kit: 2.2.0 - defu: 6.1.4 - magic-string-ast: 1.0.3 - pathe: 2.0.3 - picomatch: 4.0.4 - tinyest: 0.3.1 - tinyest-for-wgsl: 0.3.2 - typegpu: 0.11.2 - unplugin: 3.0.0 - transitivePeerDependencies: - - supports-color - - unplugin@3.0.0: - dependencies: - '@jridgewell/remapping': 2.3.5 - picomatch: 4.0.4 - webpack-virtual-modules: 0.6.2 - unstorage@1.17.4: dependencies: anymatch: 3.1.3 @@ -9350,8 +9290,6 @@ snapshots: webgpu-utils@2.0.2: {} - webpack-virtual-modules@0.6.2: {} - which-pm-runs@1.1.0: {} which@2.0.2: From 8a7ab4f35e2d505630202c5aa5b22fd85b956865 Mon Sep 17 00:00:00 2001 From: noah Date: Fri, 24 Apr 2026 13:57:49 -0700 Subject: [PATCH 08/27] continue to fight with stuff to make the examples work... --- packages/scatterbrain/vite.config.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/scatterbrain/vite.config.ts b/packages/scatterbrain/vite.config.ts index be27a805..cf98f111 100644 --- a/packages/scatterbrain/vite.config.ts +++ b/packages/scatterbrain/vite.config.ts @@ -7,16 +7,17 @@ export default defineConfig({ lib: { entry: resolve(import.meta.dirname, 'src/index.ts'), formats: ['es'], - fileName: 'module', + fileName: 'main', }, }, resolve: { alias: { - '@': resolve(import.meta.dirname, 'src'), + '~': resolve(import.meta.dirname, './'), }, }, plugins: [ dts({ + tsconfigPath: "./tsconfig.json", rollupTypes: true, }), ], From 80532be8b88a4d3cbf1bbe82458d62fc1bc91ef9 Mon Sep 17 00:00:00 2001 From: noah Date: Fri, 24 Apr 2026 14:12:44 -0700 Subject: [PATCH 09/27] lots of cleanup --- packages/scatterbrain/package.json | 4 +- packages/scatterbrain/src/demo.html | 2 +- packages/scatterbrain/src/demo.ts | 121 ++++++ packages/scatterbrain/src/index.ts | 3 +- .../src/render/webgpu/generated.test.ts | 96 ----- .../src/render/webgpu/generated.ts | 337 --------------- .../src/render/webgpu/renderer.ts | 10 +- .../scatterbrain/src/render/webgpu/shader.ts | 385 +++++++++++------ packages/scatterbrain/src/tgpu-shader.ts | 388 ------------------ packages/scatterbrain/src/wgpu-shader.ts | 66 --- 10 files changed, 395 insertions(+), 1017 deletions(-) create mode 100644 packages/scatterbrain/src/demo.ts delete mode 100644 packages/scatterbrain/src/render/webgpu/generated.test.ts delete mode 100644 packages/scatterbrain/src/render/webgpu/generated.ts delete mode 100644 packages/scatterbrain/src/tgpu-shader.ts delete mode 100644 packages/scatterbrain/src/wgpu-shader.ts diff --git a/packages/scatterbrain/package.json b/packages/scatterbrain/package.json index 3c4aaa69..b6cbd2aa 100644 --- a/packages/scatterbrain/package.json +++ b/packages/scatterbrain/package.json @@ -37,8 +37,8 @@ ], "scripts": { "typecheck": "tsc --noEmit", - "vbuild": "vite build", - "build": "parcel build --no-cache", + "build": "vite build", + "oldbuild": "parcel build --no-cache", "dev": "parcel watch --port 1239", "demo": "vite", "test": "vitest --watch", diff --git a/packages/scatterbrain/src/demo.html b/packages/scatterbrain/src/demo.html index fd478c76..20b603c4 100644 --- a/packages/scatterbrain/src/demo.html +++ b/packages/scatterbrain/src/demo.html @@ -22,7 +22,7 @@ place-content: center center; } - + diff --git a/packages/scatterbrain/src/demo.ts b/packages/scatterbrain/src/demo.ts new file mode 100644 index 00000000..894d7868 --- /dev/null +++ b/packages/scatterbrain/src/demo.ts @@ -0,0 +1,121 @@ + +// lets try and make not a full-fledged scatterbrain shader, +// with all its fancy filtering, hovering, dot sizes, etc +// but instead, some subplot shaders - so we render the dots, +// but we have no fancy filtering, just a simple highlight value, +// and a color-by attribute + + +// and lets try it with typeGPU generating our shaders for us... which I must admit seems pretty good... + +import type { ScatterbrainDataset, SlideviewScatterbrainDataset } from './types'; +import { SharedPriorityCache } from '@alleninstitute/vis-core'; +import { loadDataset } from './dataset'; +import { Box2D, type vec4 } from '@alleninstitute/vis-geometry'; +import { buildRenderFrameFn, type ShaderSettings } from './render/webgpu/renderer'; + + +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(() => { + 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, + } + }) +} +whatever(); \ No newline at end of file diff --git a/packages/scatterbrain/src/index.ts b/packages/scatterbrain/src/index.ts index 874be366..6aa65b51 100644 --- a/packages/scatterbrain/src/index.ts +++ b/packages/scatterbrain/src/index.ts @@ -8,5 +8,4 @@ export { } from './render/webgpu/renderer'; export { buildScatterbrainCacheClient } from './cache-client' export * from './types'; -export { getVisibleItems, loadDataset as loadScatterbrainDataset } from './dataset'; -export { whatever } from './tgpu-shader' \ No newline at end of file +export { getVisibleItems, loadDataset as loadScatterbrainDataset } from './dataset'; \ No newline at end of file diff --git a/packages/scatterbrain/src/render/webgpu/generated.test.ts b/packages/scatterbrain/src/render/webgpu/generated.test.ts deleted file mode 100644 index c8c5c226..00000000 --- a/packages/scatterbrain/src/render/webgpu/generated.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import { generate } from './generated'; - - -describe('this shader is annoying', () => { - const good = /*wgsl*/` - // attribs // - struct Vertex { - @builtin(vertex_index) vIndex: u32, - @location(0) umapxy: vec2f, - @location(1) Class:u32, -@location(2) subClass:u32, -@location(3) cellId:u32, - @location(4) gaba:f32, - }; - // 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 - gaba_range:vec2f, - }; - - @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 lookupTexture: texture_2d; - @group(0) @binding(2) var gradientTexture: 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.umapxy; - let withinFilterBox = within(p.x,unis.spatialFilterBox.xz)*within(p.y,unis.spatialFilterBox.yw); - let filteredIn: f32 = withinFilterBox * - step(0.01,textureLoad(lookupTexture, vec2u(0,v.Class),0).a) * step(0.01,textureLoad(lookupTexture, vec2u(1,v.subClass),0).a) * step(0.01,textureLoad(lookupTexture, vec2u(2,v.cellId),0).a) - * within(v.gaba,unis.gaba_range); - - // highlighting - let highlighted = 1.0-step(0.1,abs(f32(v.cellId-unis.highlightValue))); - - // from filtering, we can compute color - let baseColor = - vec4(textureLoad(lookupTexture,vec2u(0,v.Class),0).rgb,1.0); - let clr = mix(unis.filteredOutColor, baseColor, filteredIn); - - // point size (todo make this a uniform...) - // todo: handle offset (slides) - let R = 2.0; - let dPos = clip[v.vIndex]*R + p; - out.color = clr; - out.position = applyCamera(dPos,unis.view); - return out; - }` - test('it looks good...', () => { - const shader = generate({ categoricalColumns: ['Class', 'subClass', 'cellId'], categoricalTable: 'lookupTexture', colorByColumn: 'Class', gradientTable: 'gradientTexture', highlightByColumn: 'cellId', mode: 'color', positionColumn: 'umapxy', quantitativeColumns: ['gaba'], samplerName: 'smpl', tableSize: [2, 40] }) - expect(shader).toEqual(good) - }) -}) \ No newline at end of file diff --git a/packages/scatterbrain/src/render/webgpu/generated.ts b/packages/scatterbrain/src/render/webgpu/generated.ts deleted file mode 100644 index a64e9809..00000000 --- a/packages/scatterbrain/src/render/webgpu/generated.ts +++ /dev/null @@ -1,337 +0,0 @@ -import { difference, isEqual, keys, map } from "lodash"; -import { beginValidate, endValidate } from "./validate"; -import * as wgh from 'webgpu-utils' -import type { vec2, vec4 } from "@alleninstitute/vis-geometry"; -import { setCategoricalLookupTableValues } from "./lookup-texture"; - -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, - 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, - }] - }, - ...map(categoricalColumns, (cat, i): GPUVertexBufferLayout => ({ - arrayStride: 4, - attributes: [{ - format: 'uint32', - offset: 0, - shaderLocation: catStart + i - }], - stepMode: 'instance' - })), - ...map(quantitativeColumns, (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', - }); - - const BGL = pipeline.getBindGroupLayout(0); - // const lookupBindGroupLayout = pipeline.getBindGroupLayout(1); - // const gradientBindGroupLayout = pipeline.getBindGroupLayout(2); - // gradientBindGroupLayout.label = 'gradient bgLayout' // can I do that? - - // const bg0 = device.createBindGroup({ - // layout: BGL, - // label: 'scatterplot uniform bind group', - // entries: [ - // { binding: 0, resource: uniBuffer }, - - // ] - // }) - // hmmm, if we want to re-create the texture... we'd need to build a new bindGroup... - // that is annoying! - 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 - console.warn('warning - not enough data to update gradient texture') - } - // const bg = device.createBindGroup({ - // label: 'gradient bg', - // layout: gradientBindGroupLayout, - // entries: [ - // { binding: 2, resource: gradientTexture } - // ] - // }) - 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 } - } - let 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 device.createBindGroup({ - // layout: lookupBindGroupLayout, - // entries: [ - return { binding: 1, resource: lookupTable }; - // ] - // }); - } - if (isEqual(keys(categories).toSorted(), 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!! - } - return { pipeline, updateGradient, updateUniforms, updateCategorical }; -} \ No newline at end of file diff --git a/packages/scatterbrain/src/render/webgpu/renderer.ts b/packages/scatterbrain/src/render/webgpu/renderer.ts index ae885ba4..59859130 100644 --- a/packages/scatterbrain/src/render/webgpu/renderer.ts +++ b/packages/scatterbrain/src/render/webgpu/renderer.ts @@ -1,6 +1,6 @@ import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset, WebGLSafeBasicType } from "~/src/types"; -import { buildPipeline, generate, type Config, type Uniforms } from "./generated"; -import { keys, map, omit, pick, reduce } from "lodash"; +import { buildPipeline, type Config } from "./shader"; +import { keys, map, omit, reduce } from "lodash"; import type { ShaderSettings as BaseSettings } from "../webgl/shader"; import { getVisibleItems, type NodeWithBounds } from "~/src/dataset"; import type { Cacheable, SharedPriorityCache } from "@alleninstitute/vis-core"; @@ -171,12 +171,6 @@ export function configureShader(settings: ShaderSettings): { // figure out the columns we care about // assign them names that are safe to use in the shader (A,B,C, whatever) const categories = keys(categoricalFilters).toSorted(); - // const numCategories = categories.length; - // const longestCategory = reduce( - // keys(categoricalFilters), - // (highest, cur) => Math.max(highest, categoricalFilters[cur]), - // 0, - // ); // the goal here is to associate column names with shader-safe names const initialQuantitativeAttrs: Record = diff --git a/packages/scatterbrain/src/render/webgpu/shader.ts b/packages/scatterbrain/src/render/webgpu/shader.ts index 25fb89b4..de59bbbb 100644 --- a/packages/scatterbrain/src/render/webgpu/shader.ts +++ b/packages/scatterbrain/src/render/webgpu/shader.ts @@ -1,56 +1,118 @@ +import { isEqual, keys, map } from "lodash"; import { beginValidate, endValidate } from "./validate"; import * as wgh from 'webgpu-utils' +import type { vec2, vec4 } from "@alleninstitute/vis-geometry"; +import { setCategoricalLookupTableValues } from "./lookup-texture"; -type Config = {} -export function buildHighlightShader(config: Config) { - return /*wgsl*/` +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 - struct View { - min: vec2f, - max: vec2f, - }; - struct Uniforms { - view: View, - pointSize: vec2f, // in data space - highlight: u32, - }; +export function generate(config: Config): string { + const { + 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) position: vec2f, - @location(1) colorBy: u32, - @location(2) highlightBy: 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; - - fn isHighlighted(v:Vertex,u:Uniforms)->bool{ - return v.highlightBy == u.highlight; - } - fn highlightMix(v:Vertex,u:Uniforms)->f32 { - return 1.0-step(0.01, clamp(0.0,1.0, f32(abs(v.highlightBy-u.highlight)))); + // 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); } - // get the clip-space position of this vertex - fn applyCamera(v:Vertex, u:Uniforms)->vec2f { - let clip = array( - vec2f(1, -1), - vec2f(1, 1), - vec2f(-1, -1), - vec2f(-1, 1) - ); - let view = u.view; - let pointSize = u.pointSize; - - let S = view.max-view.min; - let R = mix(pointSize.x,pointSize.y, highlightMix(v,u)); - let dPos = clip[v.vIndex]*R + v.position; - let uPos =(dPos-view.min)/S; - // now clip space - return (uPos*2.0)-1.0; + fn rangeParameter(v:f32,range:vec2f)->f32{ + return (v-range.x)/(range.y-range.x); } - fn getColor(v:Vertex, u:Uniforms, tex: texture_2d, smpl:sampler)->vec4f { - const clr = textureSample(tex, smpl, ); - return mix(vec4f(0.5,f32(v.colorBy)/40.0,0.5,1.0),vec4f(1.0,0.,0.,1.0),highlightMix(v,u)); + fn within( v:f32, range:vec2f)->f32{ + return step(range.x,v)*step(v,range.y); } struct VsOutput { @@ -58,108 +120,197 @@ export function buildHighlightShader(config: Config) { @location(0) color: vec4f, }; - @group(0) @binding(0) - var unis: Uniforms; - @group(0) @binding(1) var tex: sampler; - @group(0) @binding(2) var lookup: texture_2d; - + const clip = array( + vec2f(1, -1), + vec2f(1, 1), + vec2f(-1, -1), + vec2f(-1, 1) + ); + @vertex - fn vmain(vert: Vertex)->VsOutput{ + 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'}; - out.color = getColor(vert,unis,tex); - out.position = vec4f(applyCamera(vert,unis),0.5,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; + fn fmain(v:VsOutput)->@location(0) vec4f { + return v.color; // todo: round points with discard? } - ` + `; } - -// the shader is very directly connected to the pipeline, so lets export that too... -export function buildHighlightPipeline(device: GPUDevice, config: Config) { +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, + }] + }, + ...map(categoricalColumns, (cat, i): GPUVertexBufferLayout => ({ + arrayStride: 4, + attributes: [{ + format: 'uint32', + offset: 0, + shaderLocation: catStart + i + }], + stepMode: 'instance' + })), + ...map(quantitativeColumns, (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 prgm = buildHighlightShader({}); const module = device.createShaderModule({ - code: prgm, - label: 'simple scatterplot highlighting' - }) - endValidate(device); - - const defs = wgh.makeShaderDataDefinitions(prgm) - console.dir(defs) - beginValidate(device); + 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({ - label: 'scatterplot render pipe', layout: 'auto', vertex: { module, + buffers: vertexLayout, entryPoint: 'vmain', - // not using interleaved buffers, so we need 3 entries... - buffers: [{//position - stepMode: 'instance', - arrayStride: 8, - attributes: [{ - format: 'float32x2', - offset: 0, - shaderLocation: 0, - }] - }, {//colorBy - stepMode: 'instance', - arrayStride: 4, // the stride of an array may not be less than 4, so uint16 is basically only supported for interleaved arrays, which we cant really use! thanks WebGPU! - attributes: [{ - format: 'uint16', - offset: 0, - shaderLocation: 1, - }] - }, - { // highlightBy - stepMode: 'instance', - arrayStride: 4, - attributes: [{ - format: 'uint16', - offset: 0, - shaderLocation: 2, - }] - } - ] }, fragment: { module, entryPoint: 'fmain', targets: [{ format: 'bgra8unorm', - blend: { - alpha: { - operation: 'add', - srcFactor: 'one', - dstFactor: 'one', - }, - color: { - operation: 'add', - srcFactor: 'src-alpha', - dstFactor: 'one-minus-src-alpha', - }, - }, + blend }] }, primitive: { - topology: 'triangle-strip', - }, - }) + topology: 'triangle-strip' + } + }); endValidate(device); - // return the pipeline, plus hand-made, typesafe info to make it easy to bind: - // also - lets just go ahead and set up the buffer for the uniforms... - - return { pipeline, defs } -} - + // 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', + }); -export function buildScatterbrainRenderer(device: GPUDevice, config: Config) { + 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 + 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 } + } + let 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 device.createBindGroup({ + // layout: lookupBindGroupLayout, + // entries: [ + return { binding: 1, resource: lookupTable }; + // ] + // }); + } + if (isEqual(keys(categories).toSorted(), 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!! + } + return { pipeline, updateGradient, updateUniforms, updateCategorical }; } \ No newline at end of file diff --git a/packages/scatterbrain/src/tgpu-shader.ts b/packages/scatterbrain/src/tgpu-shader.ts deleted file mode 100644 index aa2d8df9..00000000 --- a/packages/scatterbrain/src/tgpu-shader.ts +++ /dev/null @@ -1,388 +0,0 @@ - -// lets try and make not a full-fledged scatterbrain shader, -// with all its fancy filtering, hovering, dot sizes, etc -// but instead, some subplot shaders - so we render the dots, -// but we have no fancy filtering, just a simple highlight value, -// and a color-by attribute - - -// and lets try it with typeGPU generating our shaders for us... which I must admit seems pretty good... - -import { buildScatterbrainCacheClient } from './cache-client'; -import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset } from './types'; -import { uniq } from 'lodash'; -import { SharedPriorityCache, type Cacheable } from '@alleninstitute/vis-core'; -import type { WebGLSafeBasicType } from './typed-array'; -import { getVisibleItems, loadDataset, type NodeWithBounds } from './dataset'; -import { Box2D, type box2D, type vec2, type vec4 } from '@alleninstitute/vis-geometry'; -import { pl } from 'zod/locales'; -import type { F32 } from 'typegpu/data'; -// import { buildRenderFn as OldSchool, VBO } from './wgpu-shader'; -import { generate } from './render/webgpu/generated'; -import { beginValidate, endValidate } from './render/webgpu/validate'; -import { buildRenderFrameFn, type ShaderSettings } from './render/webgpu/renderer'; - - - - -// it seems like we can define a shader (and it will get actually created lazily) -// without needing an upfront instance of a gpu device... thats handy. - -// except... realistically, main functions will probably need uniforms! -// we can pull those in, but you need a root to even have one... -// so - - -// uh ok this is hard to read, but tgpu.vertexFn({in,out}) -// returns a function which we call immediately, -// passing it another function, which is given the things in in, and must return the things in out -// type wtf = d.unstruct({clip:d.vec2f,position:d.vec2f,colorBy:d.u16,highlightBy:d.u16 }) -// const wtf = d.unstruct({clip:d.vec2f,position:d.vec2f,colorBy:d.u32,highlightBy:d.u32 }) -// const vmain = tgpu.vertexFn({ -// in: {clip:d.vec2f,position:d.vec2f,colorBy:d.u32,highlightBy:d.u32}, -// out: { pos: d.builtin.position, uv: d.vec2f }, -// uniform: {} -// })(({clip,position,highlightBy,colorBy}) => { -// const pos = [d.vec2f(0, 0.8), d.vec2f(-0.8, -0.8), d.vec2f(0.8, -0.8)]; -// const uv = [d.vec2f(0.5, 1), d.vec2f(0, 0), d.vec2f(1, 0)]; - -// return { -// pos: d.vec4f(1,0, 0, 1), -// uv: d.vec2f(0.5,0.5), -// }; -// }); - -// lets find this after the build step runs? -// export function whatever() { -// const View = d.struct({ min: d.vec2f, max: d.vec2f }) -// const myLayout = tgpu.bindGroupLayout({ view: { uniform: View }, highlight: { uniform: d.u32 } }); -// // function vmain() { -// // 'use gpu'; - -// // } -// const vmain = tgpu.vertexFn({ -// in: { clip: d.vec2f, position: d.vec2f, colorBy: d.u32, highlightBy: d.u32 }, -// out: { pos: d.builtin.position, color: d.vec4f }, -// })(function ({ clip, position, highlightBy, colorBy }) { -// 'use gpu'; -// const size = std.sub(myLayout.$.view.max, myLayout.$.view.min) -// const p = std.div(std.sub(position, myLayout.$.view.min), size); -// const clr = std.mix(d.vec4f(0, 0, 0, 1), d.vec4f(1, 0, 0, 1), std.abs(std.sub(highlightBy, myLayout.$.highlight))) -// return { -// pos: d.vec4f(p.xy, 0, 1), -// color: clr, -// }; -// }); - -// const { code, usedBindGroupLayouts, catchall } = tgpu.resolveWithContext({ -// externals: { vmain }, template: -// /*wgsl*/` -// @vertex -// vmain -// ` }) - -// console.log(code) -// console.log(usedBindGroupLayouts) -// console.log(catchall) -// } -// whatever(); - - -// lets build an oh-so-basic typeGPU renderer for scatterbrain data... -export type SimpleSettings = { - dataset: ScatterbrainDataset | SlideviewScatterbrainDataset; - highlightBy: { kind: 'metadata', column: string }; // the name of a categorical feature by which to highlight - colorBy: { kind: 'metadata', column: string } -} - -// const dType = { -// uint8: d.uint8, -// uint16: d.u16, -// uint32: d.u32, -// int8: d.i32, -// int16: d.sint16, -// int32: d.i32, -// float: d.f32, -// } as const; - -// annoying hurdle 1 - I dont know how to use root.createBuffer() correctly to upload my raw bytes -// type RenderProps = { -// camera: { view: box2D, screenResolution: vec2 } -// dataset: ScatterbrainDataset | SlideviewScatterbrainDataset; -// client: ReturnType>; -// visibilityThresholdPx: number; -// ctx: GPUCanvasContext -// }; - -// export function buildScatterbrainTGPU(root: TgpuRoot, settings: SimpleSettings) { -// 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... -// // this is probably why the typeGPU thing didnt work right either... -// const B = root.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)) -// root.device.queue.writeBuffer(B, 0, u32.buffer); -// return new VBO(B); -// } -// const B = root.device.createBuffer({ size: buffer.byteLength, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX }); -// root.device.queue.writeBuffer(B, 0, buffer, 0, buffer.byteLength); -// return new VBO(B); -// } -// const prepareQtCell = columnsForItem(settings); -// const drawQtCell = buildRenderFn(root); -// const drawQtCells = OldSchool(root.device); -// const render = (props: RenderProps) => { -// const { camera, dataset, client, visibilityThresholdPx } = props; -// const visibilityThreshold = (visibilityThresholdPx * Box2D.size(camera.view)[0]) / camera.screenResolution[0]; // (units*pixel)/pixel ==> units -// const visibleQtNodes = getVisibleItems(dataset, camera, visibilityThreshold).map(prepareQtCell); -// client.setPriorities(visibleQtNodes, []); -// // console.log("visible: ", visibleQtNodes.length) -// const blocks: Array<{ columns: Record, count: number }> = [] -// for (const node of visibleQtNodes) { -// if (client.has(node)) { -// const drawable = client.get(node); -// if (drawable) { -// // // console.log('draw it: ', node.node.file) -// // drawQtCell({ -// // count: node.node.numSpecimens, -// // columns: drawable, -// // ctx: props.ctx, -// // highlight: 4, -// // radius: .05, -// // view: props.camera.view, -// // }) -// blocks.push({ count: node.node.numSpecimens, columns: drawable }) -// } -// } -// } -// drawQtCells({ ctx: props.ctx, highlight: 4, radius: 0.05, view: props.camera.view, blocks }) -// } - -// const connectToCache = (cache: SharedPriorityCache, onDataArrived: () => void) => { -// return buildScatterbrainCacheClient(['position', 'colorBy', 'highlightBy'], cache, toGpuBuffer, onDataArrived); -// }; - -// return { render, connectToCache }; - -// } - -// function columnsForItem( -// config: SimpleSettings, -// ) { -// const columns: Record = { -// position: { type: 'METADATA', name: config.dataset.metadata.spatialColumn }, -// colorBy: { type: 'METADATA', name: config.colorBy.column }, -// highlightBy: { type: 'METADATA', name: config.highlightBy.column } -// }; - -// return (item: T) => { -// return { ...item, dataset: config.dataset, columns }; -// }; -// } - -// so this version has problems -// I cannot figure out for the life of me how to get uint16 buffer in as a vertex attribute.... -// I think I'm gonna try (again) to just make the meat of the shader, then feed it to a template via resolveWithContext -// that will mean I'll roll the pipeline by hand, but honestly I dont see how else to make this work... -// function buildRenderFn(root: TgpuRoot) { -// // ok - heres where we do the thing, which is to create a pipeline -// // and return a function that invokes it... - -// const View = d.struct({ min: d.vec2f, max: d.vec2f }) -// const Vertex = { vIndex: d.builtin.vertexIndex, position: d.vec2f, highlightBy: d.u32 } -// const unis = tgpu.bindGroupLayout({ view: { uniform: View }, highlight: { uniform: d.u32 }, radius: { uniform: d.f32 } }); - -// // ok because we dont use interleaved buffers -// // we have to hand-roll the layouts - do we need locations here? -// const pLayout = tgpu.vertexLayout(d.arrayOf(d.vec2f), 'instance') -// const hLayout = tgpu.vertexLayout(d.arrayOf(d.u32), 'instance') -// // const hLayout: GPUVertexBufferLayout = { -// // ...wLayout, -// // arrayStride: 0, -// // } - -// const vmain = tgpu.vertexFn({ -// in: Vertex, -// out: { pos: d.builtin.position, color: d.vec4f }, -// })(function ({ vIndex, position, highlightBy }) { -// 'use gpu'; -// const clip = [d.vec2f(1, -1), d.vec2f(1, 1), d.vec2f(-1, -1), d.vec2f(-1, 1)]; -// const size = std.sub(unis.$.view.max, unis.$.view.min); -// const dP = std.add(position, std.mul(clip[vIndex], unis.$.radius)); -// const p = std.sub(std.mul(std.div(std.sub(dP, unis.$.view.min), size), 2), 1); -// const clr = std.mix(d.vec4f(1, 0, 0, 1), d.vec4f(0.5, 0.5, 0.5, 1), std.step(0.001, std.abs(std.sub(highlightBy, unis.$.highlight)))) -// return { -// pos: d.vec4f(p.xy, 0, 1), -// color: clr, -// }; -// }); - -// const fmain = tgpu.fragmentFn({ in: { pos: d.builtin.position, color: d.vec4f }, out: d.vec4f })(function ({ color }) { -// return color; -// }); - -// // lets create some uniforms... -// const view = root.createUniform(View) -// const highlight = root.createUniform(d.u32) -// const radius = root.createUniform(d.f32) - -// const bg = root.createBindGroup(unis, { -// view: view.buffer, -// highlight: highlight.buffer, -// radius: radius.buffer -// }) -// // this next part - it does pick up the types from -// // the vertex shader... but some other stuff is mysterious -// // also raw createPipeline has a fair bit of type-safety, although its -// // true it cant line up named attribs, it only works by location index -// const pipeline = root.createRenderPipeline({ -// vertex: vmain, -// fragment: fmain, -// attribs: { -// highlightBy: hLayout.attrib, -// position: pLayout.attrib, -// }, -// primitive: { topology: 'triangle-strip', cullMode: 'back' }, -// // depthStencil: { -// // format: 'depth24plus', -// // depthWriteEnabled: true, -// // depthCompare: 'less', -// // }, -// }) -// // console.log(root.unwrap(pipeline)) -// return (props: { view: box2D, radius: number, highlight: number, count: number, ctx: GPUCanvasContext, columns: Record }) => { -// // write the unis... -// const { position, highlightBy } = props.columns; -// view.patch({ min: props.view.minCorner, max: props.view.maxCorner }); // ugh -// highlight.patch(props.highlight); -// radius.patch(props.radius); -// pipeline -// .with(bg) - -// .withColorAttachment({ view: props.ctx, loadOp: 'load', }) -// .with(pLayout, position.buffer) -// .with(hLayout, highlightBy.buffer) -// .draw(4, props.count); -// } -// } - - -const tenx = - 'https://bkp-2d-visualizations-stage.s3.amazonaws.com/wmb_tenx_01172024_stage-20240128193624/G4I4GFJXJB9ATZ3PTX1/ScatterBrain.json'; -const Class = 'FS00DXV0T9R1X9FJ4QE' - -async function loadRawJson() { - return await (await fetch(tenx)).json(); -} -// function buildTest(device: GPUDevice) { - -// const yay = generate({ categoricalColumns: ['Class', 'subclass', 'cellId'], categoricalTable: 'lookupTexture', colorByColumn: 'Class', gradientTable: 'gradientTexture', highlightByColumn: 'cellId', mode: 'color', positionColumn: 'umapxy', quantitativeColumns: ['gaba'], samplerName: 'smpl', tableSize: [2, 40] }) -// beginValidate(device); -// const module = device.createShaderModule({ code: yay, label: 'test shader' }) -// endValidate(device); -// } -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 r = buildRenderFn(root.device); - // const r = buildRenderFn(root) - 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 toGpuBuffer = (buffer: ArrayBuffer, _type: WebGLSafeBasicType) => { - // const B = root.device.createBuffer({ size: buffer.byteLength, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX }); - // root.device.queue.writeBuffer(B, 0, buffer); - // return new VBO(B); - // } - - 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(() => { - 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, - } - }) -} -whatever(); \ No newline at end of file diff --git a/packages/scatterbrain/src/wgpu-shader.ts b/packages/scatterbrain/src/wgpu-shader.ts deleted file mode 100644 index 0801b979..00000000 --- a/packages/scatterbrain/src/wgpu-shader.ts +++ /dev/null @@ -1,66 +0,0 @@ - -// like the webGL version, (shader.ts) but in wgsl (webGPU) -import type { Cacheable } from '@alleninstitute/vis-core'; -import type { box2D } from '@alleninstitute/vis-geometry'; -import * as wgh from 'webgpu-utils' - - - -// export function buildRenderFn(device: GPUDevice) { - -// const { binding, 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', -// }); -// const bg0 = pipeline.getBindGroupLayout(0); -// const bg = device.createBindGroup({ -// layout: bg0, -// label: 'scatterplot bindgroup 0', -// entries: [{ binding, resource: uniBuffer }] -// }) -// // can I re-use an encoder? ANSWER: NO! what the hell is the point of all these if you cant keep them? -// return (props: { -// view: box2D, radius: number, highlight: number, ctx: GPUCanvasContext, blocks: ReadonlyArray<{ -// columns: Record, -// count: number, -// }> -// }) => { -// const { view, ctx, highlight, radius, blocks } = props; -// uniformView.set({ -// view: { min: view.minCorner, max: view.maxCorner }, -// pointSize: [radius / 2, radius * 2], -// highlight -// }); -// // now copy the typed array to the actual gpu buffer: -// device.queue.writeBuffer(uniBuffer, 0, uniformView.arrayBuffer) -// const enc = device.createCommandEncoder({ label: 'scatterbrain encoder' }) - -// const pass = enc.beginRenderPass({ -// colorAttachments: [ -// { -// clearValue: [0, 0, 0.5, 1], -// loadOp: 'clear', -// storeOp: 'store', -// view: ctx.getCurrentTexture().createView(), -// } -// ] -// }); - -// pass.setPipeline(pipeline); -// pass.setBindGroup(0, bg); - -// for (const block of blocks) { -// const { count, columns } = block -// pass.setVertexBuffer(0, columns.position.buffer) -// pass.setVertexBuffer(1, columns.colorBy.buffer) -// pass.setVertexBuffer(2, columns.highlightBy.buffer) -// pass.draw(4, count); -// } - -// pass.end(); -// device.queue.submit([enc.finish()]); -// } -// } From 0051cd32d86f2658708698ea839aff6bca8d4cd6 Mon Sep 17 00:00:00 2001 From: noah Date: Fri, 24 Apr 2026 14:13:19 -0700 Subject: [PATCH 10/27] more cleanup --- packages/scatterbrain/src/render/webgpu/shader.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/scatterbrain/src/render/webgpu/shader.ts b/packages/scatterbrain/src/render/webgpu/shader.ts index de59bbbb..8f5cef34 100644 --- a/packages/scatterbrain/src/render/webgpu/shader.ts +++ b/packages/scatterbrain/src/render/webgpu/shader.ts @@ -292,12 +292,7 @@ export function buildPipeline(device: GPUDevice, config: Config) { // first - determine the diff what what needs to change if (categories === lastCategories || isEqual(categories, lastCategories)) { // no change - return early, change nothing - // return device.createBindGroup({ - // layout: lookupBindGroupLayout, - // entries: [ return { binding: 1, resource: lookupTable }; - // ] - // }); } if (isEqual(keys(categories).toSorted(), keys(lastCategories).toSorted())) { // the set of categories stayed the same - great From e93aa02a4f7317bb4d201047ff35081416a68990 Mon Sep 17 00:00:00 2001 From: noah Date: Fri, 1 May 2026 08:24:34 -0700 Subject: [PATCH 11/27] fmt --- packages/scatterbrain/package.json | 4 +- packages/scatterbrain/src/cache-client.ts | 9 +- packages/scatterbrain/src/demo.ts | 35 ++--- packages/scatterbrain/src/index.ts | 9 +- .../scatterbrain/src/render/webgl/renderer.ts | 11 +- .../scatterbrain/src/render/webgl/shader.ts | 8 +- .../src/render/webgpu/lookup-texture.ts | 37 +++-- .../src/render/webgpu/renderer.ts | 98 ++++++------ .../scatterbrain/src/render/webgpu/shader.ts | 148 ++++++++++-------- .../src/render/webgpu/validate.ts | 6 +- packages/scatterbrain/src/types.ts | 3 +- packages/scatterbrain/vite.config.ts | 6 +- site/src/examples/scatterbrain/demo.tsx | 76 ++++----- 13 files changed, 243 insertions(+), 207 deletions(-) diff --git a/packages/scatterbrain/package.json b/packages/scatterbrain/package.json index b6cbd2aa..b1458f39 100644 --- a/packages/scatterbrain/package.json +++ b/packages/scatterbrain/package.json @@ -37,7 +37,7 @@ ], "scripts": { "typecheck": "tsc --noEmit", - "build": "vite build", + "build": "parcel build", "oldbuild": "parcel build --no-cache", "dev": "parcel watch --port 1239", "demo": "vite", @@ -72,4 +72,4 @@ "vite": "8.0.8", "vite-plugin-dts": "4.5.4" } -} \ No newline at end of file +} diff --git a/packages/scatterbrain/src/cache-client.ts b/packages/scatterbrain/src/cache-client.ts index cc6202ed..c3b10d0e 100644 --- a/packages/scatterbrain/src/cache-client.ts +++ b/packages/scatterbrain/src/cache-client.ts @@ -1,11 +1,10 @@ import type { Cacheable, SharedPriorityCache } from '@alleninstitute/vis-core'; -import type { ColumnRequest, Item, } from './types'; +import type { ColumnRequest, Item } from './types'; import reduce from 'lodash/reduce'; import type { WebGLSafeBasicType } from './typed-array'; import { keys } from 'lodash'; - -type Content = Record +type Content = Record; export function buildScatterbrainCacheClient( allNeededColumns: readonly string[], @@ -45,7 +44,7 @@ export function buildScatterbrainCacheClient( ...getters, [key]: (signal) => fetch(url, { signal }).then((b) => - b.arrayBuffer().then((buff) => toCacheValue(buff, type)) + b.arrayBuffer().then((buff) => toCacheValue(buff, type)), ), }; }, @@ -64,4 +63,4 @@ export function buildScatterbrainCacheClient( onDataArrived, }); return client; -} \ No newline at end of file +} diff --git a/packages/scatterbrain/src/demo.ts b/packages/scatterbrain/src/demo.ts index 894d7868..e663b02d 100644 --- a/packages/scatterbrain/src/demo.ts +++ b/packages/scatterbrain/src/demo.ts @@ -1,11 +1,9 @@ - // lets try and make not a full-fledged scatterbrain shader, // with all its fancy filtering, hovering, dot sizes, etc // but instead, some subplot shaders - so we render the dots, // but we have no fancy filtering, just a simple highlight value, // and a color-by attribute - // and lets try it with typeGPU generating our shaders for us... which I must admit seems pretty good... import type { ScatterbrainDataset, SlideviewScatterbrainDataset } from './types'; @@ -14,7 +12,6 @@ import { loadDataset } from './dataset'; import { Box2D, type vec4 } from '@alleninstitute/vis-geometry'; import { buildRenderFrameFn, type ShaderSettings } from './render/webgpu/renderer'; - const tenx = 'https://bkp-2d-visualizations-stage.s3.amazonaws.com/wmb_tenx_01172024_stage-20240128193624/G4I4GFJXJB9ATZ3PTX1/ScatterBrain.json'; @@ -33,9 +30,9 @@ const makeFakeColors = (n: number) => { return stuff; }; - export async function whatever() { - + const x: any = 3; + let ohno: Array = Array.isArray(x) ? x : []; const gradientData = new Uint8Array(256 * 4); for (let i = 0; i < 256; i += 4) { gradientData[i * 4 + 0] = i; @@ -43,7 +40,7 @@ export async function whatever() { gradientData[i * 4 + 2] = i; gradientData[i * 4 + 3] = 255; } - const adapter = await navigator.gpu.requestAdapter() + const adapter = await navigator.gpu.requestAdapter(); const device = await adapter?.requestDevice()!; // buildTest(root.device) @@ -59,25 +56,25 @@ export async function whatever() { // colorBy: { kind: 'quantitative', column: '27683', gradient: 'viridis', range: { min: 0, max: 10 } }, mode: 'color', quantitativeFilters: [], - highlightByColumn: { kind: 'metadata', column: 'FS00DXV0T9R1X9FJ4QE' } + highlightByColumn: { kind: 'metadata', column: 'FS00DXV0T9R1X9FJ4QE' }, }; - const dataset = await loadDataset(await loadRawJson()) + const dataset = await loadDataset(await loadRawJson()); if (!dataset) { - throw new Error('blerg this data is toast') + 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 { 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') + const ctx = cnvs.getContext('webgpu'); ctx?.configure({ device: device, format: navigator.gpu.getPreferredCanvasFormat(), - alphaMode: 'premultiplied' - }) + alphaMode: 'premultiplied', + }); const bound = (dataset as ScatterbrainDataset).metadata.tightBoundingBox; const view = Box2D.create([bound.lx, bound.ly], [bound.ux, bound.uy]); @@ -85,7 +82,7 @@ export async function whatever() { // redraw? // console.log('new data arrived...') requestAnimationFrame(() => { - console.log('re render!') + console.log('re render!'); render({ categories, @@ -99,8 +96,8 @@ export async function whatever() { offset: [0, 0], quantitativeRangeFilters: {}, spatialFilterBox: view, - } - }) + }, + }); }); }); render({ @@ -115,7 +112,7 @@ export async function whatever() { offset: [0, 0], quantitativeRangeFilters: {}, spatialFilterBox: view, - } - }) + }, + }); } -whatever(); \ No newline at end of file +whatever(); diff --git a/packages/scatterbrain/src/index.ts b/packages/scatterbrain/src/index.ts index 6aa65b51..d916ead1 100644 --- a/packages/scatterbrain/src/index.ts +++ b/packages/scatterbrain/src/index.ts @@ -3,9 +3,8 @@ export { setCategoricalLookupTableValues, updateCategoricalValue, } from './render/webgl/renderer'; -export { - buildRenderFrameFn as buildWebGPUScatterbrainRenderFn, -} from './render/webgpu/renderer'; -export { buildScatterbrainCacheClient } from './cache-client' +export { buildRenderFrameFn as buildWebGPUScatterbrainRenderFn } from './render/webgpu/renderer'; +export { buildScatterbrainCacheClient } from './cache-client'; export * from './types'; -export { getVisibleItems, loadDataset as loadScatterbrainDataset } from './dataset'; \ No newline at end of file +export { getVisibleItems, loadDataset as loadScatterbrainDataset } from './dataset'; +export { whatever } from './demo'; diff --git a/packages/scatterbrain/src/render/webgl/renderer.ts b/packages/scatterbrain/src/render/webgl/renderer.ts index 162465f3..1a261025 100644 --- a/packages/scatterbrain/src/render/webgl/renderer.ts +++ b/packages/scatterbrain/src/render/webgl/renderer.ts @@ -3,11 +3,11 @@ import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset } import { Box2D, type vec4 } from '@alleninstitute/vis-geometry'; import keys from 'lodash/keys'; import reduce from 'lodash/reduce'; -import type REGL from 'regl' +import type REGL from 'regl'; import { getVisibleItems, type NodeWithBounds } from '../../dataset'; import { buildScatterbrainRenderCommand, type Config, configureShader, type ShaderSettings, VBO } from './shader'; import { buildScatterbrainCacheClient } from '../../cache-client'; -import { MakeTaggedBufferView } from '../../typed-array' +import { MakeTaggedBufferView } from '../../typed-array'; function columnsForItem( config: Config, col2shader: Record, @@ -142,7 +142,9 @@ export function buildRenderFrameFn(regl: REGL.Regl, settings: ShaderSettings) { }; const connectToCache = (cache: SharedPriorityCache, onDataArrived: () => void) => { const allColumns = [...config.categoricalColumns, ...config.quantitativeColumns, config.positionColumn]; - const client = buildScatterbrainCacheClient(allColumns, cache, + const client = buildScatterbrainCacheClient( + allColumns, + cache, (buff, type) => { const typed = MakeTaggedBufferView(type, buff); return new VBO({ @@ -151,7 +153,8 @@ export function buildRenderFrameFn(regl: REGL.Regl, settings: ShaderSettings) { type: 'buffer', }); }, - onDataArrived); + onDataArrived, + ); return client; }; return { render, connectToCache }; diff --git a/packages/scatterbrain/src/render/webgl/shader.ts b/packages/scatterbrain/src/render/webgl/shader.ts index 4ab2a54b..6bac778f 100644 --- a/packages/scatterbrain/src/render/webgl/shader.ts +++ b/packages/scatterbrain/src/render/webgl/shader.ts @@ -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/lookup-texture.ts b/packages/scatterbrain/src/render/webgpu/lookup-texture.ts index b82c089e..ff2ea5f2 100644 --- a/packages/scatterbrain/src/render/webgpu/lookup-texture.ts +++ b/packages/scatterbrain/src/render/webgpu/lookup-texture.ts @@ -1,5 +1,5 @@ -import type { vec4 } from "@alleninstitute/vis-geometry"; -import { reduce, keys } from "lodash"; +import type { vec4 } from '@alleninstitute/vis-geometry'; +import { reduce, keys } from 'lodash'; /** * a helper function that MUTATES ALL the values in the given @param texture @@ -27,7 +27,11 @@ export function setCategoricalLookupTableValues( texture.destroy(); } // create a texture! - texture = device.createTexture({ format: 'rgba8unorm', size: { width: columns, height: rows }, usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING }); + 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) { @@ -43,10 +47,15 @@ export function setCategoricalLookupTableValues( data.set(rgbf, rowIndex * columns * 4 + columnIndex * 4); } } - device.queue.writeTexture({ texture }, data, { bytesPerRow: columns * bytesPerPixel, rowsPerImage: rows }, { - width: columns, - height: rows, - }); + device.queue.writeTexture( + { texture }, + data, + { bytesPerRow: columns * bytesPerPixel, rowsPerImage: rows }, + { + width: columns, + height: rows, + }, + ); return texture; } @@ -79,8 +88,12 @@ export function updateCategoricalValue( 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, - }); -} \ No newline at end of file + { 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 index 59859130..62f6aa5c 100644 --- a/packages/scatterbrain/src/render/webgpu/renderer.ts +++ b/packages/scatterbrain/src/render/webgpu/renderer.ts @@ -1,20 +1,19 @@ -import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset, WebGLSafeBasicType } from "~/src/types"; -import { buildPipeline, type Config } from "./shader"; -import { keys, map, omit, reduce } from "lodash"; -import type { ShaderSettings as BaseSettings } from "../webgl/shader"; -import { getVisibleItems, type NodeWithBounds } from "~/src/dataset"; -import type { Cacheable, SharedPriorityCache } from "@alleninstitute/vis-core"; -import { buildScatterbrainCacheClient } from "~/src/cache-client"; -import { Box2D, type box2D, type vec2, type vec4 } from "@alleninstitute/vis-geometry"; -import { beginValidate, endValidate } from "./validate"; +import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset, WebGLSafeBasicType } from '~/src/types'; +import { buildPipeline, type Config } from './shader'; +import { keys, map, omit, reduce } from 'lodash'; +import type { ShaderSettings as BaseSettings } from '../webgl/shader'; +import { getVisibleItems, type NodeWithBounds } from '~/src/dataset'; +import type { Cacheable, SharedPriorityCache } from '@alleninstitute/vis-core'; +import { buildScatterbrainCacheClient } from '~/src/cache-client'; +import { Box2D, type box2D, type vec2, type vec4 } from '@alleninstitute/vis-geometry'; +import { beginValidate, endValidate } from './validate'; export type ShaderSettings = BaseSettings & { - highlightByColumn: { kind: 'quantitative' | 'metadata', column: string } -} + highlightByColumn: { kind: 'quantitative' | 'metadata'; column: string }; +}; export class VBO implements Cacheable { - constructor(readonly buffer: GPUBuffer) { - } + constructor(readonly buffer: GPUBuffer) {} destroy() { this.buffer.destroy(); } @@ -23,7 +22,6 @@ export class VBO implements Cacheable { } } - function columnsForItem( config: Config, col2shader: Record, @@ -48,7 +46,6 @@ function columnsForItem( }; } - export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) { const { dataset } = settings; const { config, columnNameToShaderName } = configureShader(settings); @@ -58,16 +55,22 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) 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 }); + 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)) + 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 }); + 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 allColumns = [...config.categoricalColumns, ...config.quantitativeColumns, config.positionColumn]; const client = buildScatterbrainCacheClient(allColumns, cache, toGpuBuffer, onDataArrived); @@ -83,45 +86,51 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) } const render = (props: RenderPassProps & { client: ReturnType> }) => { const { target, categories, uniforms, client } = props; - const { camera } = uniforms + const { camera } = uniforms; beginValidate(device); - const bg0 = updateUniforms({ ...omit(uniforms, 'camera', 'spatialFilterBox', 'quantitativeRangeFilters'), view: Box2D.toFlatArray(uniforms.camera.view), spatialFilterBox: Box2D.toFlatArray(uniforms.spatialFilterBox), ...uniforms.quantitativeRangeFilters }); + const bg0 = updateUniforms({ + ...omit(uniforms, 'camera', 'spatialFilterBox', 'quantitativeRangeFilters'), + view: Box2D.toFlatArray(uniforms.camera.view), + spatialFilterBox: Box2D.toFlatArray(uniforms.spatialFilterBox), + ...uniforms.quantitativeRangeFilters, + }); const bg1 = updateCategorical(categories); const bg2 = updateGradient(viridis); // todo - dont do this every frame... // so... the gad damn bindings - if you dont use a binding, it needs to be omitted from // the freaking bg.. that means our gradient texture shouldnt be added if we dont have any quant stuff... - let entries: GPUBindGroupEntry[] = [bg0] + let entries: GPUBindGroupEntry[] = [bg0]; if (keys(categories).length > 0) { - entries.push(bg1) + entries.push(bg1); } if (keys(uniforms.quantitativeRangeFilters).length > 0) { - entries.push(bg2) + entries.push(bg2); } const bg = device.createBindGroup({ label: 'single bg', entries, layout: pipeline.getBindGroupLayout(0), - }) - const enc = device.createCommandEncoder({ label: 'encoder for scatterbrain render pass' }) + }); + 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 - }] + 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, []) + const visible = getVisibleItems(dataset, camera, 0.1).map(prepareQtCell); + client.setPriorities(visible, []); // console.log('visible: ', visible.length) for (const node of visible) { - if (client.has(node)) { const drawable = client.get(node); if (drawable) { @@ -139,12 +148,12 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) pass.end(); device.queue.submit([enc.finish()]); endValidate(device); - } - return { render, connectToCache } + }; + return { render, connectToCache }; } export type RenderPassProps = { - target: GPUTextureView + target: GPUTextureView; uniforms: { camera: { view: box2D; screenResolution: vec2 }; offset: vec2; @@ -152,12 +161,11 @@ export type RenderPassProps = { spatialFilterBox: box2D; quantitativeRangeFilters: Record; highlightedValue: number; - } + }; // categoricalLookupTable: GPUTexture // gradient: GPUTexture; categories: Readonly>>>; gradient: Uint8Array; - }; export function configureShader(settings: ShaderSettings): { @@ -193,9 +201,9 @@ export function configureShader(settings: ShaderSettings): { const colToAttribute = { ...qAttrs, ...cAttrs, - [dataset.metadata.spatialColumn]: 'position' - } - const ordered = map([...categories, ...quantitativeFilters.toSorted()], (col) => colToAttribute[col]) + [dataset.metadata.spatialColumn]: 'position', + }; + const ordered = map([...categories, ...quantitativeFilters.toSorted()], (col) => colToAttribute[col]); const config: Config = { categoricalColumns: keys(cAttrs).map((columnName) => colToAttribute[columnName]), quantitativeColumns: keys(qAttrs).map((columnName) => colToAttribute[columnName]), @@ -205,7 +213,7 @@ export function configureShader(settings: ShaderSettings): { mode, positionColumn: 'position', highlightByColumn: { ...highlightByColumn, column: colToAttribute[highlightByColumn.column] }, - vertexLocationOrder: ['position', ...ordered] + vertexLocationOrder: ['position', ...ordered], }; return { config, columnNameToShaderName: colToAttribute }; -} \ No newline at end of file +} diff --git a/packages/scatterbrain/src/render/webgpu/shader.ts b/packages/scatterbrain/src/render/webgpu/shader.ts index 8f5cef34..6f47d179 100644 --- a/packages/scatterbrain/src/render/webgpu/shader.ts +++ b/packages/scatterbrain/src/render/webgpu/shader.ts @@ -1,8 +1,8 @@ -import { isEqual, keys, map } from "lodash"; -import { beginValidate, endValidate } from "./validate"; -import * as wgh from 'webgpu-utils' -import type { vec2, vec4 } from "@alleninstitute/vis-geometry"; -import { setCategoricalLookupTableValues } from "./lookup-texture"; +import { isEqual, keys, map } from 'lodash'; +import { beginValidate, endValidate } from './validate'; +import * as wgh from 'webgpu-utils'; +import type { vec2, vec4 } from '@alleninstitute/vis-geometry'; +import { setCategoricalLookupTableValues } from './lookup-texture'; function rangeFor(col: string): `${string}_range` { return `${col}_range`; @@ -14,10 +14,7 @@ function rangeFilterExpression(quantitativeColumns: readonly string[]) { 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)`, - ) + .map((attrib, i) => /*wgsl*/ `step(0.01,textureLoad(${tableName}, vec2u(${i.toFixed(0)},v.${attrib}),0).a)`) .join(' * '); } @@ -29,21 +26,20 @@ export type Config = { gradientTable: string; positionColumn: string; colorByColumn: string; - highlightByColumn: { kind: 'quantitative' | 'metadata', column: string }; - vertexLocationOrder: 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 - + view: vec4; + spatialFilterBox: vec4; + filteredOutColor: vec4; + highlightColor: vec4; + screenSize: vec2; + offset: vec2; + highlightValue: number; +} & QuantitativeFilterRanges; export function generate(config: Config): string { const { @@ -59,7 +55,6 @@ export function generate(config: Config): string { const catFilter = categoricalFilterExpression(categoricalColumns, categoricalTable); const rangeFilter = rangeFilterExpression(quantitativeColumns); - const categoryColumnIndex = categoricalColumns.indexOf(colorByColumn); const colorByCategorical = /*wgsl*/ ` @@ -174,31 +169,43 @@ function generateVertexBufferLayout(config: Config) { { arrayStride: 8, // xy floats stepMode: 'instance', - attributes: [{ - shaderLocation: 0, - format: 'float32x2', - offset: 0, - }] + attributes: [ + { + shaderLocation: 0, + format: 'float32x2', + offset: 0, + }, + ], }, - ...map(categoricalColumns, (cat, i): GPUVertexBufferLayout => ({ - arrayStride: 4, - attributes: [{ - format: 'uint32', - offset: 0, - shaderLocation: catStart + i - }], - stepMode: 'instance' - })), - ...map(quantitativeColumns, (q, i): GPUVertexBufferLayout => ({ - arrayStride: 4, - attributes: [{ - format: 'float32', - offset: 0, - shaderLocation: quantStart + i - }], - stepMode: 'instance' - })), - ] + ...map( + categoricalColumns, + (cat, i): GPUVertexBufferLayout => ({ + arrayStride: 4, + attributes: [ + { + format: 'uint32', + offset: 0, + shaderLocation: catStart + i, + }, + ], + stepMode: 'instance', + }), + ), + ...map( + quantitativeColumns, + (q, i): GPUVertexBufferLayout => ({ + arrayStride: 4, + attributes: [ + { + format: 'float32', + offset: 0, + shaderLocation: quantStart + i, + }, + ], + stepMode: 'instance', + }), + ), + ]; return what; } export function buildPipeline(device: GPUDevice, config: Config) { @@ -206,7 +213,7 @@ export function buildPipeline(device: GPUDevice, config: Config) { beginValidate(device); const module = device.createShaderModule({ code: shader, - label: 'scatterbrain shader mod' + label: 'scatterbrain shader mod', }); const defs = wgh.makeShaderDataDefinitions(shader); const vertexLayout = generateVertexBufferLayout(config); @@ -232,14 +239,16 @@ export function buildPipeline(device: GPUDevice, config: Config) { fragment: { module, entryPoint: 'fmain', - targets: [{ - format: 'bgra8unorm', - blend - }] + targets: [ + { + format: 'bgra8unorm', + blend, + }, + ], }, primitive: { - topology: 'triangle-strip' - } + topology: 'triangle-strip', + }, }); endValidate(device); @@ -256,8 +265,8 @@ export function buildPipeline(device: GPUDevice, config: Config) { let gradientTexture = device.createTexture({ format: 'rgba8unorm', size: { width: 256, height: 1 }, - usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING - }) + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, + }); const updateGradient = (data: Uint8Array) => { beginValidate(device); if (data.byteLength >= 256 * 4) { @@ -265,30 +274,37 @@ export function buildPipeline(device: GPUDevice, config: Config) { gradientTexture = device.createTexture({ format: 'rgba8unorm', size: { width: 256, height: 1 }, - usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, }); - device.queue.writeTexture({ texture: gradientTexture }, data, { bytesPerRow: 4 * 256, rowsPerImage: 1 }, { width: 256, height: 1 }) + device.queue.writeTexture( + { texture: gradientTexture }, + data, + { bytesPerRow: 4 * 256, rowsPerImage: 1 }, + { width: 256, height: 1 }, + ); } else { // warn - we didnt updat the gradient - console.warn('warning - not enough data to update gradient texture') + console.warn('warning - not enough data to update gradient texture'); } endValidate(device); return { binding: 2, resource: gradientTexture }; - } + }; const updateUniforms = (unis: Partial) => { - uniformView.set(unis) + uniformView.set(unis); // now we write that to the stashed buffer device.queue.writeBuffer(uniBuffer, 0, uniformView.arrayBuffer); - return { binding: 0, resource: uniBuffer } - } + return { binding: 0, resource: uniBuffer }; + }; let lastCategories = {}; let lookupTable = device.createTexture({ format: 'rgba8unorm', size: { width: 1, height: 1 }, - usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING - }) - const updateCategorical = (categories: Readonly>>>) => { + 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 @@ -306,6 +322,6 @@ export function buildPipeline(device: GPUDevice, config: Config) { } return { binding: 1, resource: lookupTable }; // bindGroups dont have a destroy() - so I'm assuming its totally fine to leak them!! - } + }; return { pipeline, updateGradient, updateUniforms, updateCategorical }; -} \ No newline at end of file +} diff --git a/packages/scatterbrain/src/render/webgpu/validate.ts b/packages/scatterbrain/src/render/webgpu/validate.ts index 20d3f4e0..09de89bc 100644 --- a/packages/scatterbrain/src/render/webgpu/validate.ts +++ b/packages/scatterbrain/src/render/webgpu/validate.ts @@ -1,15 +1,15 @@ const VALIDATE = true; // todo turn me off for prod... export function beginValidate(device: GPUDevice) { if (VALIDATE) { - device.pushErrorScope('validation') + device.pushErrorScope('validation'); } } export function endValidate(device: GPUDevice) { if (VALIDATE) { device.popErrorScope().then((errs) => { if (errs) { - console.error(errs) + console.error(errs); } - }) + }); } } diff --git a/packages/scatterbrain/src/types.ts b/packages/scatterbrain/src/types.ts index eedb6ac0..2ad4b942 100644 --- a/packages/scatterbrain/src/types.ts +++ b/packages/scatterbrain/src/types.ts @@ -1,7 +1,7 @@ /// Types describing the metadata that gets loaded from scatterbrain.json files /// // there are 2 variants, slideview and regular - they are distinguished at runtime -import type { box2D } from "@alleninstitute/vis-geometry"; +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'; @@ -85,7 +85,6 @@ export type SlideviewMetadata = CommonMetadata & { export type SlideviewScatterbrainDataset = { type: 'slideview'; metadata: SlideviewMetadata }; export type ScatterbrainDataset = { type: 'normal'; metadata: ScatterbrainMetadata }; - // renderer-specific types: export type Item = Readonly<{ diff --git a/packages/scatterbrain/vite.config.ts b/packages/scatterbrain/vite.config.ts index cf98f111..b19a2a79 100644 --- a/packages/scatterbrain/vite.config.ts +++ b/packages/scatterbrain/vite.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from 'vite' +import { defineConfig } from 'vite'; import { resolve } from 'node:path'; import dts from 'vite-plugin-dts'; @@ -17,8 +17,8 @@ export default defineConfig({ }, plugins: [ dts({ - tsconfigPath: "./tsconfig.json", + tsconfigPath: './tsconfig.json', rollupTypes: true, }), ], -}); \ No newline at end of file +}); diff --git a/site/src/examples/scatterbrain/demo.tsx b/site/src/examples/scatterbrain/demo.tsx index c5af213a..e590b67d 100644 --- a/site/src/examples/scatterbrain/demo.tsx +++ b/site/src/examples/scatterbrain/demo.tsx @@ -1,10 +1,9 @@ import type { vec2, vec4 } from '@alleninstitute/vis-geometry'; -import { SharedCacheContext, SharedCacheProvider } from '../common/react/priority-cache-provider'; +import { SharedCacheContext, SharedCacheProvider } from '../common/react/cache-provider'; import { useContext, useEffect, useRef, useState } from 'react'; import { - buildScatterbrainRenderFn, + buildWebGPUScatterbrainRenderFn, loadScatterbrainDataset, - setCategoricalLookupTableValues, type Dataset, type ShaderSettings, } from '@alleninstitute/vis-scatterbrain'; @@ -36,6 +35,7 @@ const categories = { '4MV7HA5DG2XJZ3UD8G9': makeFakeColors(40), // nt type FS00DXV0T9R1X9FJ4QE: makeFakeColors(40), // class }; + const settings: Omit = { categoricalFilters: { '4MV7HA5DG2XJZ3UD8G9': 40, FS00DXV0T9R1X9FJ4QE: 40 }, colorBy: { kind: 'metadata', column: 'FS00DXV0T9R1X9FJ4QE' }, @@ -51,7 +51,7 @@ type Props = { screenSize: vec2 }; function Demo(props: Props) { const { screenSize } = props; const cnvs = useRef(null); - const server = useContext(SharedCacheContext); + const cache = useContext(SharedCacheContext); const [dataset, setDataset] = useState(undefined); useEffect(() => { loadRawJson().then((raw) => setDataset(loadScatterbrainDataset(raw))); @@ -59,10 +59,7 @@ function Demo(props: Props) { // todo handlers, etc useEffect(() => { // build the renderer - if (server && dataset && cnvs.current) { - const ctx = cnvs.current.getContext('2d'); - const { cache, regl } = server; - const lookup = regl.texture({ width: 10, height: 10, format: 'rgba' }); + if (cache && dataset && cnvs.current) { const gradientData = new Uint8Array(256 * 4); for (let i = 0; i < 256; i += 4) { gradientData[i * 4 + 0] = i; @@ -70,38 +67,43 @@ function Demo(props: Props) { gradientData[i * 4 + 2] = i; gradientData[i * 4 + 3] = 255; } - const gradient = regl.texture({ width: 256, height: 1, format: 'rgba', data: gradientData }); - const tgt = regl.framebuffer(screenSize[0], screenSize[1]); + const ctx = cnvs.current.getContext('webgpu'); // make up random colors for the coloring, and add random filtering - - setCategoricalLookupTableValues(categories, lookup); - - const { render, connectToCache } = buildScatterbrainRenderFn( - regl, - { ...settings, dataset }, - ); - const renderOneFrame = () => { - render({ - client, - visibilityThresholdPx: 10, - camera: { view: { minCorner: [-17, -17], maxCorner: [26, 26] }, screenResolution: [800, 800] }, - categoricalLookupTable: lookup, + navigator.gpu.requestAdapter().then((adapter) => { + const device = adapter?.requestDevice(); + ctx!.configure({ + device: device, + format: navigator.gpu.getPreferredCanvasFormat(), + alphaMode: 'premultiplied', + }); + const { render, connectToCache } = buildWebGPUScatterbrainRenderFn(device, { + ...settings, dataset, - filteredOutColor: [0, 0, 0, 1], - gradient, - hoveredValue: 22, - offset: [0, 0], - quantitativeRangeFilters: {}, - spatialFilterBox: { minCorner: [-17, -17], maxCorner: [30, 30] }, - target: tgt, + highlightByColumn: { kind: 'metadata', column: '4MV7HA5DG2XJZ3UD8G9' }, }); - const bytes = regl.read({ framebuffer: tgt }); - const img = new ImageData(new Uint8ClampedArray(bytes), screenSize[0], screenSize[1]); - ctx!.putImageData(img, 0, 0); - }; - const client = connectToCache(cache, renderOneFrame); - renderOneFrame(); + const renderOneFrame = () => { + render({ + client, + categories, + gradient: gradientData, + target: ctx?.getCurrentTexture().createView(), + uniforms: { + camera: { + view: { minCorner: [-17, -17], maxCorner: [26, 26] }, + screenResolution: [800, 800], + }, + filteredOutColor: [0, 0, 0, 1], + highlightedValue: 22, + offset: [0, 0], + quantitativeRangeFilters: {}, + spatialFilterBox: { minCorner: [-17, -17], maxCorner: [30, 30] }, + }, + }); + }; + const client = connectToCache(cache, renderOneFrame); + renderOneFrame(); + }); } - }, [dataset, server, screenSize]); + }, [dataset, cache, screenSize]); return ; } From 3eaf74d4c004adfb14eedbdc60dc0ca9848d2768 Mon Sep 17 00:00:00 2001 From: noah Date: Fri, 1 May 2026 08:26:09 -0700 Subject: [PATCH 12/27] tsconfig webgpu types --- site/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/site/tsconfig.json b/site/tsconfig.json index c50435cf..e3766ee6 100644 --- a/site/tsconfig.json +++ b/site/tsconfig.json @@ -1,5 +1,6 @@ { "extends": "astro/tsconfigs/strict", + "types": ["@webgpu/types"], "include": [".astro/types.d.ts", "**/*"], "exclude": ["dist"], "compilerOptions": { From b04849f328c2aa595fb55d956f56ae5e120972cd Mon Sep 17 00:00:00 2001 From: noah Date: Fri, 1 May 2026 08:26:15 -0700 Subject: [PATCH 13/27] webgpu types --- site/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/site/package.json b/site/package.json index 308577e8..b72e7912 100644 --- a/site/package.json +++ b/site/package.json @@ -42,6 +42,7 @@ "@types/file-saver": "2.0.7", "@types/node": "22.1.0", "@types/react": "18.3.0", + "@webgpu/types": "0.1.69", "@types/react-dom": "18.3.0" }, "dependencies": { From 855c371b6351117185d8ab6d9431b1577a669cad Mon Sep 17 00:00:00 2001 From: noah Date: Fri, 1 May 2026 08:49:37 -0700 Subject: [PATCH 14/27] put everything back to regl until we can figure out what is going on with the webGPU stuff (startlight cant build wgpu-utils, site/ cant resolve webGPU types) --- packages/core/package.json | 3 +- packages/scatterbrain/src/demo.html | 2 +- packages/scatterbrain/src/demo.ts | 15 ++-- .../src/render/webgpu/lookup-texture.ts | 1 + .../src/render/webgpu/renderer.ts | 2 +- .../scatterbrain/src/render/webgpu/shader.ts | 9 ++- .../src/render/webgpu/validate.ts | 1 + pnpm-lock.yaml | 6 +- site/package.json | 3 +- .../examples/common/react/cache-provider.tsx | 20 +++++ site/src/examples/scatterbrain/demo.tsx | 78 +++++++++---------- site/tsconfig.json | 3 +- 12 files changed, 80 insertions(+), 63 deletions(-) create mode 100644 site/src/examples/common/react/cache-provider.tsx diff --git a/packages/core/package.json b/packages/core/package.json index b4ac9b30..50111a47 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -58,7 +58,8 @@ "@alleninstitute/vis-geometry": "workspace:*", "lodash": "4.17.23", "regl": "2.1.1", + "@webgpu/types": "0.1.69", "uuid": "13.0.0" }, "packageManager": "pnpm@9.14.2" -} +} \ No newline at end of file diff --git a/packages/scatterbrain/src/demo.html b/packages/scatterbrain/src/demo.html index 20b603c4..12405798 100644 --- a/packages/scatterbrain/src/demo.html +++ b/packages/scatterbrain/src/demo.html @@ -1,5 +1,5 @@ - + diff --git a/packages/scatterbrain/src/demo.ts b/packages/scatterbrain/src/demo.ts index e663b02d..c223a1d7 100644 --- a/packages/scatterbrain/src/demo.ts +++ b/packages/scatterbrain/src/demo.ts @@ -1,16 +1,12 @@ -// lets try and make not a full-fledged scatterbrain shader, -// with all its fancy filtering, hovering, dot sizes, etc -// but instead, some subplot shaders - so we render the dots, -// but we have no fancy filtering, just a simple highlight value, -// and a color-by attribute +/** biome-ignore-all lint/suspicious/noNonNullAssertedOptionalChain: */ +/** biome-ignore-all lint/style/noNonNullAssertion: */ -// and lets try it with typeGPU generating our shaders for us... which I must admit seems pretty good... -import type { ScatterbrainDataset, SlideviewScatterbrainDataset } from './types'; import { SharedPriorityCache } from '@alleninstitute/vis-core'; -import { loadDataset } from './dataset'; 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'; @@ -31,8 +27,6 @@ const makeFakeColors = (n: number) => { }; export async function whatever() { - const x: any = 3; - let ohno: Array = Array.isArray(x) ? x : []; const gradientData = new Uint8Array(256 * 4); for (let i = 0; i < 256; i += 4) { gradientData[i * 4 + 0] = i; @@ -82,6 +76,7 @@ export async function whatever() { // redraw? // console.log('new data arrived...') requestAnimationFrame(() => { + // biome-ignore lint/suspicious/noConsole: console.log('re render!'); render({ diff --git a/packages/scatterbrain/src/render/webgpu/lookup-texture.ts b/packages/scatterbrain/src/render/webgpu/lookup-texture.ts index ff2ea5f2..9399d88e 100644 --- a/packages/scatterbrain/src/render/webgpu/lookup-texture.ts +++ b/packages/scatterbrain/src/render/webgpu/lookup-texture.ts @@ -27,6 +27,7 @@ export function setCategoricalLookupTableValues( texture.destroy(); } // create a texture! + // biome-ignore lint/style/noParameterAssign: texture = device.createTexture({ format: 'rgba8unorm', size: { width: columns, height: rows }, diff --git a/packages/scatterbrain/src/render/webgpu/renderer.ts b/packages/scatterbrain/src/render/webgpu/renderer.ts index 62f6aa5c..134bd212 100644 --- a/packages/scatterbrain/src/render/webgpu/renderer.ts +++ b/packages/scatterbrain/src/render/webgpu/renderer.ts @@ -100,7 +100,7 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) // so... the gad damn bindings - if you dont use a binding, it needs to be omitted from // the freaking bg.. that means our gradient texture shouldnt be added if we dont have any quant stuff... - let entries: GPUBindGroupEntry[] = [bg0]; + const entries: GPUBindGroupEntry[] = [bg0]; if (keys(categories).length > 0) { entries.push(bg1); } diff --git a/packages/scatterbrain/src/render/webgpu/shader.ts b/packages/scatterbrain/src/render/webgpu/shader.ts index 6f47d179..9924572c 100644 --- a/packages/scatterbrain/src/render/webgpu/shader.ts +++ b/packages/scatterbrain/src/render/webgpu/shader.ts @@ -43,7 +43,7 @@ export type Uniforms = { export function generate(config: Config): string { const { - mode, + mode: _mode, quantitativeColumns, categoricalColumns, categoricalTable, @@ -179,7 +179,7 @@ function generateVertexBufferLayout(config: Config) { }, ...map( categoricalColumns, - (cat, i): GPUVertexBufferLayout => ({ + (_cat, i): GPUVertexBufferLayout => ({ arrayStride: 4, attributes: [ { @@ -193,7 +193,7 @@ function generateVertexBufferLayout(config: Config) { ), ...map( quantitativeColumns, - (q, i): GPUVertexBufferLayout => ({ + (_q, i): GPUVertexBufferLayout => ({ arrayStride: 4, attributes: [ { @@ -284,6 +284,7 @@ export function buildPipeline(device: GPUDevice, config: Config) { ); } else { // warn - we didnt updat the gradient + // biome-ignore lint/suspicious/noConsole: console.warn('warning - not enough data to update gradient texture'); } @@ -296,7 +297,7 @@ export function buildPipeline(device: GPUDevice, config: Config) { device.queue.writeBuffer(uniBuffer, 0, uniformView.arrayBuffer); return { binding: 0, resource: uniBuffer }; }; - let lastCategories = {}; + const lastCategories = {}; let lookupTable = device.createTexture({ format: 'rgba8unorm', size: { width: 1, height: 1 }, diff --git a/packages/scatterbrain/src/render/webgpu/validate.ts b/packages/scatterbrain/src/render/webgpu/validate.ts index 09de89bc..e389fe48 100644 --- a/packages/scatterbrain/src/render/webgpu/validate.ts +++ b/packages/scatterbrain/src/render/webgpu/validate.ts @@ -1,3 +1,4 @@ +/** 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) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b557351f..a4b4705c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: '@alleninstitute/vis-geometry': specifier: workspace:* version: link:../geometry + '@webgpu/types': + specifier: 0.1.69 + version: 0.1.69 lodash: specifier: 4.17.23 version: 4.17.23 @@ -208,9 +211,6 @@ importers: '@types/node': specifier: 22.1.0 version: 22.1.0 - '@webgpu/types': - specifier: 0.1.69 - version: 0.1.69 packages: diff --git a/site/package.json b/site/package.json index b72e7912..b9ba756a 100644 --- a/site/package.json +++ b/site/package.json @@ -42,7 +42,6 @@ "@types/file-saver": "2.0.7", "@types/node": "22.1.0", "@types/react": "18.3.0", - "@webgpu/types": "0.1.69", "@types/react-dom": "18.3.0" }, "dependencies": { @@ -70,4 +69,4 @@ "zarrita": "0.5.1" }, "packageManager": "pnpm@9.14.2" -} +} \ No newline at end of file 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/scatterbrain/demo.tsx b/site/src/examples/scatterbrain/demo.tsx index e590b67d..26c11f3c 100644 --- a/site/src/examples/scatterbrain/demo.tsx +++ b/site/src/examples/scatterbrain/demo.tsx @@ -1,9 +1,10 @@ import type { vec2, vec4 } from '@alleninstitute/vis-geometry'; -import { SharedCacheContext, SharedCacheProvider } from '../common/react/cache-provider'; +import { SharedCacheContext, SharedCacheProvider } from '../common/react/priority-cache-provider'; import { useContext, useEffect, useRef, useState } from 'react'; import { - buildWebGPUScatterbrainRenderFn, + buildScatterbrainRenderFn, loadScatterbrainDataset, + setCategoricalLookupTableValues, type Dataset, type ShaderSettings, } from '@alleninstitute/vis-scatterbrain'; @@ -35,7 +36,6 @@ const categories = { '4MV7HA5DG2XJZ3UD8G9': makeFakeColors(40), // nt type FS00DXV0T9R1X9FJ4QE: makeFakeColors(40), // class }; - const settings: Omit = { categoricalFilters: { '4MV7HA5DG2XJZ3UD8G9': 40, FS00DXV0T9R1X9FJ4QE: 40 }, colorBy: { kind: 'metadata', column: 'FS00DXV0T9R1X9FJ4QE' }, @@ -51,7 +51,7 @@ type Props = { screenSize: vec2 }; function Demo(props: Props) { const { screenSize } = props; const cnvs = useRef(null); - const cache = useContext(SharedCacheContext); + const server = useContext(SharedCacheContext); const [dataset, setDataset] = useState(undefined); useEffect(() => { loadRawJson().then((raw) => setDataset(loadScatterbrainDataset(raw))); @@ -59,7 +59,10 @@ function Demo(props: Props) { // todo handlers, etc useEffect(() => { // build the renderer - if (cache && dataset && cnvs.current) { + if (server && dataset && cnvs.current) { + const ctx = cnvs.current.getContext('2d'); + const { cache, regl } = server; + const lookup = regl.texture({ width: 10, height: 10, format: 'rgba' }); const gradientData = new Uint8Array(256 * 4); for (let i = 0; i < 256; i += 4) { gradientData[i * 4 + 0] = i; @@ -67,43 +70,40 @@ function Demo(props: Props) { gradientData[i * 4 + 2] = i; gradientData[i * 4 + 3] = 255; } - const ctx = cnvs.current.getContext('webgpu'); + 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 - navigator.gpu.requestAdapter().then((adapter) => { - const device = adapter?.requestDevice(); - ctx!.configure({ - device: device, - format: navigator.gpu.getPreferredCanvasFormat(), - alphaMode: 'premultiplied', - }); - const { render, connectToCache } = buildWebGPUScatterbrainRenderFn(device, { - ...settings, + + setCategoricalLookupTableValues(categories, lookup); + + const { render, connectToCache } = buildScatterbrainRenderFn( + // @ts-expect-error we'll deal with this later + regl, + { ...settings, dataset }, + ); + // this ts error is bogus, dont know why + const renderOneFrame = () => { + render({ + client, + visibilityThresholdPx: 10, + camera: { view: { minCorner: [-17, -17], maxCorner: [26, 26] }, screenResolution: [800, 800] }, + categoricalLookupTable: lookup, dataset, - highlightByColumn: { kind: 'metadata', column: '4MV7HA5DG2XJZ3UD8G9' }, + filteredOutColor: [0, 0, 0, 1], + gradient, + hoveredValue: 22, + offset: [0, 0], + quantitativeRangeFilters: {}, + spatialFilterBox: { minCorner: [-17, -17], maxCorner: [30, 30] }, + target: tgt, }); - const renderOneFrame = () => { - render({ - client, - categories, - gradient: gradientData, - target: ctx?.getCurrentTexture().createView(), - uniforms: { - camera: { - view: { minCorner: [-17, -17], maxCorner: [26, 26] }, - screenResolution: [800, 800], - }, - filteredOutColor: [0, 0, 0, 1], - highlightedValue: 22, - offset: [0, 0], - quantitativeRangeFilters: {}, - spatialFilterBox: { minCorner: [-17, -17], maxCorner: [30, 30] }, - }, - }); - }; - const client = connectToCache(cache, renderOneFrame); - renderOneFrame(); - }); + const bytes = regl.read({ framebuffer: tgt }); + const img = new ImageData(new Uint8ClampedArray(bytes), screenSize[0], screenSize[1]); + ctx!.putImageData(img, 0, 0); + }; + const client = connectToCache(cache, renderOneFrame); + renderOneFrame(); } - }, [dataset, cache, screenSize]); + }, [dataset, server, screenSize]); return ; } diff --git a/site/tsconfig.json b/site/tsconfig.json index e3766ee6..c96949c3 100644 --- a/site/tsconfig.json +++ b/site/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "astro/tsconfigs/strict", - "types": ["@webgpu/types"], - "include": [".astro/types.d.ts", "**/*"], + "include": [".astro/types.d.ts", "**/*", "**/*.tsx"], "exclude": ["dist"], "compilerOptions": { "baseUrl": "./", From c5bed4191f58f5b84bbc69d3b20abd0fdcf1b752 Mon Sep 17 00:00:00 2001 From: noah Date: Fri, 1 May 2026 09:10:34 -0700 Subject: [PATCH 15/27] clean it up --- packages/core/package.json | 1 - packages/scatterbrain/tsconfig.json | 3 ++- pnpm-lock.yaml | 3 --- tsconfig.base.json | 3 +-- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 50111a47..3591f4fe 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -58,7 +58,6 @@ "@alleninstitute/vis-geometry": "workspace:*", "lodash": "4.17.23", "regl": "2.1.1", - "@webgpu/types": "0.1.69", "uuid": "13.0.0" }, "packageManager": "pnpm@9.14.2" diff --git a/packages/scatterbrain/tsconfig.json b/packages/scatterbrain/tsconfig.json index 2095290d..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/**/*"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4b4705c..242a671c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,9 +44,6 @@ importers: '@alleninstitute/vis-geometry': specifier: workspace:* version: link:../geometry - '@webgpu/types': - specifier: 0.1.69 - version: 0.1.69 lodash: specifier: 4.17.23 version: 4.17.23 diff --git a/tsconfig.base.json b/tsconfig.base.json index 9254664c..ff280309 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -5,7 +5,6 @@ "esModuleInterop": true, "exactOptionalPropertyTypes": true, "forceConsistentCasingInFileNames": true, - "skipLibCheck": true, - "types": ["@webgpu/types"] + "skipLibCheck": true } } From 3f12b87893ca64909dcf42a2277c2805404eeaf0 Mon Sep 17 00:00:00 2001 From: noah Date: Fri, 1 May 2026 09:14:30 -0700 Subject: [PATCH 16/27] lint & fmt --- packages/core/package.json | 2 +- packages/scatterbrain/src/cache-client.ts | 1 - packages/scatterbrain/src/demo.ts | 1 - site/package.json | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 3591f4fe..b4ac9b30 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -61,4 +61,4 @@ "uuid": "13.0.0" }, "packageManager": "pnpm@9.14.2" -} \ No newline at end of file +} diff --git a/packages/scatterbrain/src/cache-client.ts b/packages/scatterbrain/src/cache-client.ts index c3b10d0e..e157b5fc 100644 --- a/packages/scatterbrain/src/cache-client.ts +++ b/packages/scatterbrain/src/cache-client.ts @@ -2,7 +2,6 @@ import type { Cacheable, SharedPriorityCache } from '@alleninstitute/vis-core'; import type { ColumnRequest, Item } from './types'; import reduce from 'lodash/reduce'; import type { WebGLSafeBasicType } from './typed-array'; -import { keys } from 'lodash'; type Content = Record; diff --git a/packages/scatterbrain/src/demo.ts b/packages/scatterbrain/src/demo.ts index c223a1d7..afa4375c 100644 --- a/packages/scatterbrain/src/demo.ts +++ b/packages/scatterbrain/src/demo.ts @@ -1,7 +1,6 @@ /** 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'; diff --git a/site/package.json b/site/package.json index b9ba756a..308577e8 100644 --- a/site/package.json +++ b/site/package.json @@ -69,4 +69,4 @@ "zarrita": "0.5.1" }, "packageManager": "pnpm@9.14.2" -} \ No newline at end of file +} From da81118a38ae01c91652060db0c093d8b8e358e3 Mon Sep 17 00:00:00 2001 From: noah Date: Fri, 1 May 2026 09:44:15 -0700 Subject: [PATCH 17/27] fix a very silly hack that was totally ruining astro's build and it was all my fault --- packages/scatterbrain/src/demo.ts | 1 - packages/scatterbrain/src/index.ts | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/scatterbrain/src/demo.ts b/packages/scatterbrain/src/demo.ts index afa4375c..89b3a245 100644 --- a/packages/scatterbrain/src/demo.ts +++ b/packages/scatterbrain/src/demo.ts @@ -109,4 +109,3 @@ export async function whatever() { }, }); } -whatever(); diff --git a/packages/scatterbrain/src/index.ts b/packages/scatterbrain/src/index.ts index d916ead1..d28a3e49 100644 --- a/packages/scatterbrain/src/index.ts +++ b/packages/scatterbrain/src/index.ts @@ -1,10 +1,10 @@ +export { buildScatterbrainCacheClient } from './cache-client'; +export { getVisibleItems, loadDataset as loadScatterbrainDataset } from './dataset'; export { buildRenderFrameFn as buildScatterbrainRenderFn, setCategoricalLookupTableValues, - updateCategoricalValue, + updateCategoricalValue } from './render/webgl/renderer'; export { buildRenderFrameFn as buildWebGPUScatterbrainRenderFn } from './render/webgpu/renderer'; -export { buildScatterbrainCacheClient } from './cache-client'; export * from './types'; -export { getVisibleItems, loadDataset as loadScatterbrainDataset } from './dataset'; -export { whatever } from './demo'; + From 47afe9a3272089f38d25e1ca27a6c65102a942c9 Mon Sep 17 00:00:00 2001 From: noah Date: Fri, 1 May 2026 09:49:15 -0700 Subject: [PATCH 18/27] oh also this has float16array so thats good --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index cf4ca967..2fb02d06 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,8 @@ "vitest": "4.1.1" }, "volta": { - "node": "22.11.0", + "node": "24.15.0", "pnpm": "10.33.0" }, "packageManager": "pnpm@10.33.0" -} +} \ No newline at end of file From 086a83820a489dd570a0845a49ec6209867a1132 Mon Sep 17 00:00:00 2001 From: noah Date: Fri, 1 May 2026 10:44:54 -0700 Subject: [PATCH 19/27] cleanup experimental stuff --- packages/scatterbrain/package.json | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/scatterbrain/package.json b/packages/scatterbrain/package.json index b1458f39..574b6157 100644 --- a/packages/scatterbrain/package.json +++ b/packages/scatterbrain/package.json @@ -37,10 +37,8 @@ ], "scripts": { "typecheck": "tsc --noEmit", - "build": "parcel build", - "oldbuild": "parcel build --no-cache", + "build": "parcel build --no-cache", "dev": "parcel watch --port 1239", - "demo": "vite", "test": "vitest --watch", "test:ci": "vitest run", "coverage": "vitest run --coverage", @@ -68,8 +66,6 @@ "devDependencies": { "@types/lodash": "4.17.24", "@types/node": "22.19.15", - "@webgpu/types": "0.1.69", - "vite": "8.0.8", - "vite-plugin-dts": "4.5.4" + "@webgpu/types": "0.1.69" } -} +} \ No newline at end of file From 24d78a0e759d1ed76d00f4a517dd681fc50d5727 Mon Sep 17 00:00:00 2001 From: noah Date: Fri, 1 May 2026 11:47:02 -0700 Subject: [PATCH 20/27] update lockfile --- pnpm-lock.yaml | 531 ------------------------------------------------- 1 file changed, 531 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 242a671c..aa9bb527 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,12 +126,6 @@ importers: '@webgpu/types': specifier: 0.1.69 version: 0.1.69 - vite: - specifier: 8.0.8 - version: 8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(yaml@2.8.1) - vite-plugin-dts: - specifier: 4.5.4 - version: 4.5.4(@types/node@22.19.15)(rollup@4.60.0)(typescript@5.9.3)(vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(yaml@2.8.1)) site: dependencies: @@ -998,19 +992,6 @@ packages: '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} - '@microsoft/api-extractor-model@7.33.6': - resolution: {integrity: sha512-E9iI4yGEVVusbTAqSLetVFxDuBVCVqCigcoQwdJuOjsLq5Hry3MkBgUQhSZNzLCu17pgjk58MI80GRDJLht/1A==} - - '@microsoft/api-extractor@7.58.2': - resolution: {integrity: sha512-qmqWa0Fx1xn3irQy8MyuAKUs8e3CdwMJOujaPkM8gx5v/V7RcLhTjBU0/uL2kdhmROpW+5WG1FD98O441kkvQQ==} - hasBin: true - - '@microsoft/tsdoc-config@0.18.1': - resolution: {integrity: sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==} - - '@microsoft/tsdoc@0.16.0': - resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} - '@mischnic/json-sourcemap@0.1.1': resolution: {integrity: sha512-iA7+tyVqfrATAIsIRWQG+a7ZLLD0VaOCKV2Wd/v4mqIU3J9c4jx9p7S0nw1XH3gJCKNBOOwACOPYYSUu9pgT+w==} engines: {node: '>=12.0.0'} @@ -1874,36 +1855,6 @@ packages: cpu: [x64] os: [win32] - '@rushstack/node-core-library@5.22.0': - resolution: {integrity: sha512-S/Dm/N+8tkbasS6yM5cF6q4iDFt14mQQniiVIwk1fd0zpPwWESspO4qtPyIl8szEaN86XOYC1HRRzZrOowxjtw==} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true - - '@rushstack/problem-matcher@0.2.1': - resolution: {integrity: sha512-gulfhBs6n+I5b7DvjKRfhMGyUejtSgOHTclF/eONr8hcgF1APEDjhxIsfdUYYMzC3rvLwGluqLjbwCFZ8nxrog==} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true - - '@rushstack/rig-package@0.7.2': - resolution: {integrity: sha512-9XbFWuqMYcHUso4mnETfhGVUSaADBRj6HUAAEYk50nMPn8WRICmBuCphycQGNB3duIR6EEZX3Xj3SYc2XiP+9A==} - - '@rushstack/terminal@0.22.5': - resolution: {integrity: sha512-umej8J6A+WRbfQV1G/uNfnz4bMa8CzFU9IJzQb/ZcH4j7Ybg3BQ8UBKOCF3o5U3/2yah1TDU/zE71ugg2JJv+Q==} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true - - '@rushstack/ts-command-line@5.3.5': - resolution: {integrity: sha512-ToJQu3+o6aEdDoApGrwb/RsbwDi/NSC7jIEaAezzWM470TRrsXfSHoYAm1eWkhh34xJ+kZxU1ZzKSHiOMlOFPA==} - '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -2020,9 +1971,6 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@types/argparse@1.0.38': - resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} - '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -2172,26 +2120,6 @@ packages: '@vscode/l10n@0.0.18': resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} - '@vue/compiler-core@3.5.32': - resolution: {integrity: sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==} - - '@vue/compiler-dom@3.5.32': - resolution: {integrity: sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==} - - '@vue/compiler-vue2@2.7.16': - resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} - - '@vue/language-core@2.2.0': - resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@vue/shared@3.5.32': - resolution: {integrity: sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==} - '@webgpu/types@0.1.69': resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==} @@ -2211,11 +2139,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - ajv-draft-04@1.0.0: resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} peerDependencies: @@ -2224,23 +2147,9 @@ packages: ajv: optional: true - ajv-formats@3.0.1: - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - ajv@8.18.0: - resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} - - alien-signals@0.4.14: - resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} - ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -2267,9 +2176,6 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2305,13 +2211,6 @@ packages: bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - base-64@1.0.0: resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} @@ -2338,13 +2237,6 @@ packages: resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} engines: {node: '>=18'} - brace-expansion@2.1.0: - resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} - - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} - engines: {node: 18 || 20 || >=22} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -2447,15 +2339,6 @@ packages: common-ancestor-path@1.0.1: resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} - compare-versions@6.1.1: - resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} - - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - - confbox@0.2.4: - resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -2503,9 +2386,6 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - de-indent@1.0.2: - resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2603,14 +2483,6 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - entities@7.0.1: - resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} - engines: {node: '>=0.12'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -2679,9 +2551,6 @@ packages: expressive-code@0.41.3: resolution: {integrity: sha512-YLnD62jfgBZYrXIPQcJ0a51Afv9h8VlWqEGK9uU2T5nL/5rb8SnA86+7+mgCZe5D34Tff5RNEA5hjNVJYHzrFg==} - exsolve@1.0.8: - resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} - extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -2725,18 +2594,11 @@ packages: resolution: {integrity: sha512-piJxbLnkD9Xcyi7dWJRnqszEURixe7CrF/efBfbffe2DPyabmuIuqraruY8cXTs19QoM8VJzx47BDRVNXETM7Q==} engines: {node: '>=20'} - fs-extra@11.3.4: - resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} - engines: {node: '>=14.14'} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -2799,9 +2661,6 @@ packages: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - h3@1.15.5: resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==} @@ -2809,10 +2668,6 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - hast-util-embedded@3.0.0: resolution: {integrity: sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==} @@ -2873,10 +2728,6 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} - he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2902,10 +2753,6 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - import-lazy@4.0.0: - resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} - engines: {node: '>=8'} - import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} @@ -2921,10 +2768,6 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -2988,9 +2831,6 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - jju@1.4.0: - resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3017,9 +2857,6 @@ packages: jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - jsonfile@6.2.0: - resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - kiwi-schema@0.5.0: resolution: {integrity: sha512-X+FpfU0yTEtc6aTHS7VwbOpvQwRt70+pXXWRI5fd6CvWhe7pSVC854TVo4Zo0x5/wwcWj+/9KUlXpdcP0dY9AA==} hasBin: true @@ -3036,9 +2873,6 @@ packages: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} - kolorist@1.8.0: - resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} - lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -3191,19 +3025,12 @@ packages: resolution: {integrity: sha512-9bMdFfc80S+vSldBmG3HOuLVHnxRdNTlpzR6QDnzqCQtCzGUEAGTzBKYMeIM+I/sU4oZfgbcbS7X7F65/z/oxQ==} hasBin: true - local-pkg@1.1.2: - resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} - engines: {node: '>=14'} - lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} - lodash@4.18.1: - resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} - longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -3214,10 +3041,6 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3410,17 +3233,6 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - minimatch@10.2.3: - resolution: {integrity: sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==} - engines: {node: 18 || 20 || >=22} - - minimatch@9.0.9: - resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} - engines: {node: '>=16 || 14 >=14.17'} - - mlly@1.8.2: - resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -3556,9 +3368,6 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -3580,12 +3389,6 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - - pkg-types@2.3.0: - resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - postcss-nested@6.2.0: resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} engines: {node: '>=12.0'} @@ -3634,9 +3437,6 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - quansync@0.2.11: - resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} - radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} @@ -3757,11 +3557,6 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - resolve@1.22.12: - resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} - engines: {node: '>= 0.4'} - hasBin: true - retext-latin@4.0.0: resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} @@ -3802,11 +3597,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} @@ -3856,10 +3646,6 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - source-map@0.7.6: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} @@ -3867,9 +3653,6 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -3879,10 +3662,6 @@ packages: stream-replace-string@2.0.0: resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==} - string-argv@0.3.2: - resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} - engines: {node: '>=0.6.19'} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -3906,10 +3685,6 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - style-to-js@1.1.17: resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} @@ -3920,14 +3695,6 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - svgo@4.0.0: resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==} engines: {node: '>=16'} @@ -4079,10 +3846,6 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - unstorage@1.17.4: resolution: {integrity: sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==} peerDependencies: @@ -4178,15 +3941,6 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-plugin-dts@4.5.4: - resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} - peerDependencies: - typescript: '*' - vite: '*' - peerDependenciesMeta: - vite: - optional: true - vite@6.4.1: resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -4450,9 +4204,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml-language-server@1.19.2: resolution: {integrity: sha512-9F3myNmJzUN/679jycdMxqtydPSDRAarSj3wPiF7pchEPnO9Dg07Oc+gIYLqXR4L+g+FSEVXXv2+mr54StLFOg==} hasBin: true @@ -5238,42 +4989,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@microsoft/api-extractor-model@7.33.6(@types/node@22.19.15)': - dependencies: - '@microsoft/tsdoc': 0.16.0 - '@microsoft/tsdoc-config': 0.18.1 - '@rushstack/node-core-library': 5.22.0(@types/node@22.19.15) - transitivePeerDependencies: - - '@types/node' - - '@microsoft/api-extractor@7.58.2(@types/node@22.19.15)': - dependencies: - '@microsoft/api-extractor-model': 7.33.6(@types/node@22.19.15) - '@microsoft/tsdoc': 0.16.0 - '@microsoft/tsdoc-config': 0.18.1 - '@rushstack/node-core-library': 5.22.0(@types/node@22.19.15) - '@rushstack/rig-package': 0.7.2 - '@rushstack/terminal': 0.22.5(@types/node@22.19.15) - '@rushstack/ts-command-line': 5.3.5(@types/node@22.19.15) - diff: 8.0.3 - lodash: 4.18.1 - minimatch: 10.2.3 - resolve: 1.22.12 - semver: 7.5.4 - source-map: 0.6.1 - typescript: 5.9.3 - transitivePeerDependencies: - - '@types/node' - - '@microsoft/tsdoc-config@0.18.1': - dependencies: - '@microsoft/tsdoc': 0.16.0 - ajv: 8.18.0 - jju: 1.4.0 - resolve: 1.22.12 - - '@microsoft/tsdoc@0.16.0': {} - '@mischnic/json-sourcemap@0.1.1': dependencies: '@lezer/common': 1.2.3 @@ -6213,45 +5928,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.0': optional: true - '@rushstack/node-core-library@5.22.0(@types/node@22.19.15)': - dependencies: - ajv: 8.18.0 - ajv-draft-04: 1.0.0(ajv@8.18.0) - ajv-formats: 3.0.1(ajv@8.18.0) - fs-extra: 11.3.4 - import-lazy: 4.0.0 - jju: 1.4.0 - resolve: 1.22.12 - semver: 7.5.4 - optionalDependencies: - '@types/node': 22.19.15 - - '@rushstack/problem-matcher@0.2.1(@types/node@22.19.15)': - optionalDependencies: - '@types/node': 22.19.15 - - '@rushstack/rig-package@0.7.2': - dependencies: - resolve: 1.22.12 - strip-json-comments: 3.1.1 - - '@rushstack/terminal@0.22.5(@types/node@22.19.15)': - dependencies: - '@rushstack/node-core-library': 5.22.0(@types/node@22.19.15) - '@rushstack/problem-matcher': 0.2.1(@types/node@22.19.15) - supports-color: 8.1.1 - optionalDependencies: - '@types/node': 22.19.15 - - '@rushstack/ts-command-line@5.3.5(@types/node@22.19.15)': - dependencies: - '@rushstack/terminal': 0.22.5(@types/node@22.19.15) - '@types/argparse': 1.0.38 - argparse: 1.0.10 - string-argv: 0.3.2 - transitivePeerDependencies: - - '@types/node' - '@sec-ant/readable-stream@0.4.1': {} '@shikijs/core@3.22.0': @@ -6353,8 +6029,6 @@ snapshots: tslib: 2.8.1 optional: true - '@types/argparse@1.0.38': {} - '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.2 @@ -6564,39 +6238,6 @@ snapshots: '@vscode/l10n@0.0.18': {} - '@vue/compiler-core@3.5.32': - dependencies: - '@babel/parser': 7.29.2 - '@vue/shared': 3.5.32 - entities: 7.0.1 - estree-walker: 2.0.2 - source-map-js: 1.2.1 - - '@vue/compiler-dom@3.5.32': - dependencies: - '@vue/compiler-core': 3.5.32 - '@vue/shared': 3.5.32 - - '@vue/compiler-vue2@2.7.16': - dependencies: - de-indent: 1.0.2 - he: 1.2.0 - - '@vue/language-core@2.2.0(typescript@5.9.3)': - dependencies: - '@volar/language-core': 2.4.28 - '@vue/compiler-dom': 3.5.32 - '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.32 - alien-signals: 0.4.14 - minimatch: 9.0.9 - muggle-string: 0.4.1 - path-browserify: 1.0.1 - optionalDependencies: - typescript: 5.9.3 - - '@vue/shared@3.5.32': {} - '@webgpu/types@0.1.69': {} '@zarrita/storage@0.1.3': @@ -6615,20 +6256,10 @@ snapshots: acorn@8.15.0: {} - acorn@8.16.0: {} - ajv-draft-04@1.0.0(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 - ajv-draft-04@1.0.0(ajv@8.18.0): - optionalDependencies: - ajv: 8.18.0 - - ajv-formats@3.0.1(ajv@8.18.0): - optionalDependencies: - ajv: 8.18.0 - ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -6636,15 +6267,6 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - ajv@8.18.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - - alien-signals@0.4.14: {} - ansi-align@3.0.1: dependencies: string-width: 4.2.3 @@ -6666,10 +6288,6 @@ snapshots: arg@5.0.2: {} - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - argparse@2.0.1: {} aria-query@5.3.2: {} @@ -6791,10 +6409,6 @@ snapshots: bail@2.0.2: {} - balanced-match@1.0.2: {} - - balanced-match@4.0.4: {} - base-64@1.0.0: {} base-x@3.0.11: @@ -6826,14 +6440,6 @@ snapshots: widest-line: 5.0.0 wrap-ansi: 9.0.2 - brace-expansion@2.1.0: - dependencies: - balanced-match: 1.0.2 - - brace-expansion@5.0.5: - dependencies: - balanced-match: 4.0.4 - braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -6914,12 +6520,6 @@ snapshots: common-ancestor-path@1.0.1: {} - compare-versions@6.1.1: {} - - confbox@0.1.8: {} - - confbox@0.2.4: {} - convert-source-map@2.0.0: {} cookie-es@1.2.2: {} @@ -6966,8 +6566,6 @@ snapshots: csstype@3.2.3: {} - de-indent@1.0.2: {} - debug@4.4.3: dependencies: ms: 2.1.3 @@ -7043,10 +6641,6 @@ snapshots: entities@6.0.1: {} - entities@7.0.1: {} - - es-errors@1.3.0: {} - es-module-lexer@1.7.0: {} es-module-lexer@2.0.0: {} @@ -7189,8 +6783,6 @@ snapshots: '@expressive-code/plugin-shiki': 0.41.3 '@expressive-code/plugin-text-markers': 0.41.3 - exsolve@1.0.8: {} - extend@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -7223,17 +6815,9 @@ snapshots: dependencies: tiny-inflate: 1.0.3 - fs-extra@11.3.4: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.0 - universalify: 2.0.1 - fsevents@2.3.3: optional: true - function-bind@1.1.2: {} - gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -7282,8 +6866,6 @@ snapshots: dependencies: type-fest: 0.20.2 - graceful-fs@4.2.11: {} - h3@1.15.5: dependencies: cookie-es: 1.2.2 @@ -7298,10 +6880,6 @@ snapshots: has-flag@4.0.0: {} - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - hast-util-embedded@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -7491,8 +7069,6 @@ snapshots: property-information: 7.1.0 space-separated-tokens: 2.0.2 - he@1.2.0: {} - html-escaper@2.0.2: {} html-escaper@3.0.3: {} @@ -7511,8 +7087,6 @@ snapshots: ieee754@1.2.1: {} - import-lazy@4.0.0: {} - import-meta-resolve@4.2.0: {} inline-style-parser@0.2.4: {} @@ -7526,10 +7100,6 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - is-decimal@2.0.1: {} is-docker@3.0.0: {} @@ -7575,8 +7145,6 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - jju@1.4.0: {} - js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -7593,12 +7161,6 @@ snapshots: jsonc-parser@3.3.1: {} - jsonfile@6.2.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - kiwi-schema@0.5.0: {} kleur@3.0.3: {} @@ -7607,8 +7169,6 @@ snapshots: klona@2.0.6: {} - kolorist@1.8.0: {} - lightningcss-android-arm64@1.30.2: optional: true @@ -7722,18 +7282,10 @@ snapshots: '@lmdb/lmdb-linux-x64': 2.8.5 '@lmdb/lmdb-win32-x64': 2.8.5 - local-pkg@1.1.2: - dependencies: - mlly: 1.8.2 - pkg-types: 2.3.0 - quansync: 0.2.11 - lodash@4.17.21: {} lodash@4.17.23: {} - lodash@4.18.1: {} - longest-streak@3.1.0: {} lru-cache@11.2.6: {} @@ -7742,10 +7294,6 @@ snapshots: dependencies: yallist: 3.1.1 - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -8236,21 +7784,6 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 - minimatch@10.2.3: - dependencies: - brace-expansion: 5.0.5 - - minimatch@9.0.9: - dependencies: - brace-expansion: 2.1.0 - - mlly@1.8.2: - dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.3 - mrmime@2.0.1: {} ms@2.1.3: {} @@ -8411,8 +7944,6 @@ snapshots: path-key@4.0.0: {} - path-parse@1.0.7: {} - pathe@2.0.3: {} piccolore@0.1.3: {} @@ -8425,18 +7956,6 @@ snapshots: picomatch@4.0.4: {} - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.2 - pathe: 2.0.3 - - pkg-types@2.3.0: - dependencies: - confbox: 0.2.4 - exsolve: 1.0.8 - pathe: 2.0.3 - postcss-nested@6.2.0(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -8480,8 +7999,6 @@ snapshots: property-information@7.1.0: {} - quansync@0.2.11: {} - radix3@1.1.2: {} react-dom@19.2.4(react@19.2.4): @@ -8653,13 +8170,6 @@ snapshots: require-from-string@2.0.2: {} - resolve@1.22.12: - dependencies: - es-errors: 1.3.0 - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - retext-latin@4.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -8777,10 +8287,6 @@ snapshots: semver@6.3.1: {} - semver@7.5.4: - dependencies: - lru-cache: 6.0.0 - semver@7.7.3: {} semver@7.7.4: {} @@ -8850,22 +8356,16 @@ snapshots: source-map-js@1.2.1: {} - source-map@0.6.1: {} - source-map@0.7.6: {} space-separated-tokens@2.0.2: {} - sprintf-js@1.0.3: {} - stackback@0.0.2: {} std-env@4.0.0: {} stream-replace-string@2.0.0: {} - string-argv@0.3.2: {} - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -8893,8 +8393,6 @@ snapshots: strip-final-newline@4.0.0: {} - strip-json-comments@3.1.1: {} - style-to-js@1.1.17: dependencies: style-to-object: 1.0.9 @@ -8907,12 +8405,6 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - svgo@4.0.0: dependencies: commander: 11.1.0 @@ -9061,8 +8553,6 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 - universalify@2.0.1: {} - unstorage@1.17.4: dependencies: anymatch: 3.1.3 @@ -9107,25 +8597,6 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-dts@4.5.4(@types/node@22.19.15)(rollup@4.60.0)(typescript@5.9.3)(vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(yaml@2.8.1)): - dependencies: - '@microsoft/api-extractor': 7.58.2(@types/node@22.19.15) - '@rollup/pluginutils': 5.3.0(rollup@4.60.0) - '@volar/typescript': 2.4.28 - '@vue/language-core': 2.2.0(typescript@5.9.3) - compare-versions: 6.1.1 - debug: 4.4.3 - kolorist: 1.8.0 - local-pkg: 1.1.2 - magic-string: 0.30.21 - typescript: 5.9.3 - optionalDependencies: - vite: 8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(yaml@2.8.1) - transitivePeerDependencies: - - '@types/node' - - rollup - - supports-color - vite@6.4.1(@types/node@22.1.0)(lightningcss@1.32.0)(yaml@2.8.1): dependencies: esbuild: 0.25.10 @@ -9320,8 +8791,6 @@ snapshots: yallist@3.1.1: {} - yallist@4.0.0: {} - yaml-language-server@1.19.2: dependencies: '@vscode/l10n': 0.0.18 From b582440d577db295e30750daab78cc907f69e051 Mon Sep 17 00:00:00 2001 From: noah Date: Mon, 4 May 2026 09:46:10 -0700 Subject: [PATCH 21/27] install --- pnpm-lock.yaml | 54 ++++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f024b74e..d562b538 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(@types/node@22.19.15)(vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(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 @@ -37,7 +37,7 @@ importers: version: 5.9.3 vitest: specifier: 4.1.1 - version: 4.1.1(@types/node@22.19.15)(vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(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: @@ -149,13 +149,13 @@ importers: version: 0.9.6(prettier@3.8.1)(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.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 @@ -167,7 +167,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 @@ -4336,12 +4336,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 @@ -4388,17 +4388,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 @@ -5772,6 +5772,8 @@ snapshots: '@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 @@ -6133,7 +6135,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@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.4)(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 @@ -6145,7 +6147,7 @@ snapshots: magicast: 0.5.2 obug: 2.1.1 tinyrainbow: 3.1.0 - vitest: 4.1.1(@types/node@22.19.15)(vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(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 @@ -6158,13 +6160,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.1(vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(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: 8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(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: @@ -6300,12 +6302,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 @@ -6718,6 +6720,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: {} @@ -8617,17 +8620,16 @@ snapshots: lightningcss: 1.32.0 yaml: 2.8.1 - vite@8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(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.4 + esbuild: 0.27.7 fsevents: 2.3.3 yaml: 2.8.1 @@ -8635,10 +8637,10 @@ snapshots: optionalDependencies: vite: 6.4.1(@types/node@22.1.0)(lightningcss@1.32.0)(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.4)(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@8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(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 @@ -8655,7 +8657,7 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 8.0.8(@types/node@22.19.15)(esbuild@0.27.4)(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 From 82d8fa3a6503cd22de94dd9faa7d87093cfb680c Mon Sep 17 00:00:00 2001 From: noah Date: Mon, 4 May 2026 10:44:19 -0700 Subject: [PATCH 22/27] use node 24 in ci actions, so that it doesnt choke on Float16Array in node-dependencies... --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66c8fc59..4fcfda59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '24' - name: Install dependencies run: pnpm install --frozen-lockfile @@ -46,7 +46,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '24' - name: Install dependencies run: pnpm install --frozen-lockfile @@ -69,7 +69,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '24' - name: Install dependencies run: pnpm install --frozen-lockfile @@ -92,7 +92,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '24' - name: Install dependencies run: pnpm install --frozen-lockfile @@ -118,7 +118,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '24' - name: Install dependencies run: pnpm install --frozen-lockfile From 43d3ae7821c2af4d9d4f6be0e96f7d761b4072e6 Mon Sep 17 00:00:00 2001 From: noah Date: Mon, 4 May 2026 16:26:50 -0700 Subject: [PATCH 23/27] for some fun reason lodash does not play nice with parcel... thinking through how to make better use of how webGPU works with regard to the nice way of thinking via REGL. --- .../src/render/webgpu/renderer.ts | 112 +++++++------ .../scatterbrain/src/render/webgpu/shader.ts | 148 +++++++++--------- pnpm-lock.yaml | 3 + site/package.json | 3 +- .../docs/examples/scatterbrain-webgpu.mdx | 8 + .../common/react/gpu-device-provider.tsx | 19 +++ .../src/examples/scatterbrain/webgpu-demo.tsx | 121 ++++++++++++++ site/tsconfig.json | 3 +- 8 files changed, 297 insertions(+), 120 deletions(-) create mode 100644 site/src/content/docs/examples/scatterbrain-webgpu.mdx create mode 100644 site/src/examples/common/react/gpu-device-provider.tsx create mode 100644 site/src/examples/scatterbrain/webgpu-demo.tsx diff --git a/packages/scatterbrain/src/render/webgpu/renderer.ts b/packages/scatterbrain/src/render/webgpu/renderer.ts index 134bd212..1fb7efd9 100644 --- a/packages/scatterbrain/src/render/webgpu/renderer.ts +++ b/packages/scatterbrain/src/render/webgpu/renderer.ts @@ -1,6 +1,6 @@ +/** biome-ignore-all lint/performance/noAccumulatingSpread: leave me be */ import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset, WebGLSafeBasicType } from '~/src/types'; import { buildPipeline, type Config } from './shader'; -import { keys, map, omit, reduce } from 'lodash'; import type { ShaderSettings as BaseSettings } from '../webgl/shader'; import { getVisibleItems, type NodeWithBounds } from '~/src/dataset'; import type { Cacheable, SharedPriorityCache } from '@alleninstitute/vis-core'; @@ -8,12 +8,26 @@ import { buildScatterbrainCacheClient } from '~/src/cache-client'; import { Box2D, type box2D, type vec2, type vec4 } from '@alleninstitute/vis-geometry'; import { beginValidate, endValidate } from './validate'; +export type Head> = T extends readonly [] ? never : T[0]; +export type Tail> = T extends readonly [infer _I, ...infer rest] ? rest : never; +export type Last> = T extends readonly [infer K] ? K : Last>; + +export type OR> = T extends readonly [infer K] ? K : Head | OR>; + +// todo figure out why parcel cant bundle lodash... +function omit, Drop extends ReadonlyArray>(obj: T, ...drop: Drop): Omit> { + const stuff = { ...obj }; + for (const d of drop) { + delete stuff[d] + } + return stuff; +} export type ShaderSettings = BaseSettings & { highlightByColumn: { kind: 'quantitative' | 'metadata'; column: string }; }; export class VBO implements Cacheable { - constructor(readonly buffer: GPUBuffer) {} + constructor(readonly buffer: GPUBuffer) { } destroy() { this.buffer.destroy(); } @@ -28,8 +42,7 @@ function columnsForItem( dataset: ScatterbrainDataset | SlideviewScatterbrainDataset, ) { const columns: Record = {}; - const s2c = reduce( - keys(col2shader), + const s2c = Object.keys(col2shader).reduce( (acc, col) => ({ ...acc, [col2shader[col]]: col }), {} as Record, ); @@ -49,7 +62,7 @@ function columnsForItem( export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) { const { dataset } = settings; const { config, columnNameToShaderName } = configureShader(settings); - const { pipeline, updateCategorical, updateGradient, updateUniforms } = buildPipeline(device, config); + const { pipeline, makeUniformBuffer, updateUniforms, uniformSize } = buildPipeline(device, config); const prepareQtCell = columnsForItem(config, columnNameToShaderName, dataset); const toGpuBuffer = (buffer: ArrayBuffer, type: WebGLSafeBasicType) => { @@ -84,28 +97,44 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) viridis[i * 4 + 2] = i; viridis[i * 4 + 3] = 255; } + 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, categories, uniforms, client } = props; - const { camera } = uniforms; + const { target, camera, offset, filteredOutColor, spatialFilterBox, quantitativeRangeFilters, highlightedValue, client, + categoricalLookupTable, gradient } = props; + const uniforms = { + camera: { ...camera, view: Box2D.toFlatArray(camera.view) }, offset, filteredOutColor, spatialFilterBox: Box2D.toFlatArray(spatialFilterBox), highlightedValue, quantitativeRangeFilters + } beginValidate(device); - const bg0 = updateUniforms({ - ...omit(uniforms, 'camera', 'spatialFilterBox', 'quantitativeRangeFilters'), - view: Box2D.toFlatArray(uniforms.camera.view), - spatialFilterBox: Box2D.toFlatArray(uniforms.spatialFilterBox), - ...uniforms.quantitativeRangeFilters, - }); - const bg1 = updateCategorical(categories); - const bg2 = updateGradient(viridis); // todo - dont do this every frame... - - // so... the gad damn bindings - if you dont use a binding, it needs to be omitted from - // the freaking bg.. that means our gradient texture shouldnt be added if we dont have any quant stuff... - const entries: GPUBindGroupEntry[] = [bg0]; - if (keys(categories).length > 0) { - entries.push(bg1); + // we know how many columns are gonna be filtered... because we have a closure over the settings + // thats "fine" because this renderer already cant be applied a different set of columns, nor a different dataset... + + // const bg0 = updateUniforms({ + // ...omit(uniforms, 'camera', 'spatialFilterBox', 'quantitativeRangeFilters'), + // view: Box2D.toFlatArray(uniforms.camera.view), + // spatialFilterBox: Box2D.toFlatArray(uniforms.spatialFilterBox), + // ...uniforms.quantitativeRangeFilters, + // }); + // const bg1 = updateCategorical(categories); + // const bg2 = updateGradient(viridis); // todo - dont do this every frame... + + // unlike the textures... we'd like to not make our user handle raw buffers... + // but perhaps that is silly + updateUniforms(uniforms, unis); + // it would be... very slightly better if we could not do this for every single node in the quadtree - but + // thats a future thing TODO + 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 (keys(uniforms.quantitativeRangeFilters).length > 0) { - entries.push(bg2); + if (Object.keys(uniforms.quantitativeRangeFilters).length > 0) { + entries.push({ binding: 2, resource: gradient }); } const bg = device.createBindGroup({ label: 'single bg', @@ -149,23 +178,20 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) device.queue.submit([enc.finish()]); endValidate(device); }; - return { render, connectToCache }; + return { render, connectToCache, makeUniformBuffer, pipeline, updateUniforms }; } export type RenderPassProps = { target: GPUTextureView; - uniforms: { - camera: { view: box2D; screenResolution: vec2 }; - offset: vec2; - filteredOutColor: vec4; - spatialFilterBox: box2D; - quantitativeRangeFilters: Record; - highlightedValue: number; - }; - // categoricalLookupTable: GPUTexture - // gradient: GPUTexture; - categories: Readonly>>>; - gradient: Uint8Array; + + camera: { view: box2D; screenResolution: vec2 }; + offset: vec2; + filteredOutColor: vec4; + spatialFilterBox: box2D; + quantitativeRangeFilters: Record; + highlightedValue: number; + categoricalLookupTable: GPUTextureView; + gradient: GPUTextureView; }; export function configureShader(settings: ShaderSettings): { @@ -178,7 +204,7 @@ export function configureShader(settings: ShaderSettings): { 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 = keys(categoricalFilters).toSorted(); + const categories = Object.keys(categoricalFilters).toSorted(); // the goal here is to associate column names with shader-safe names const initialQuantitativeAttrs: Record = @@ -186,14 +212,12 @@ export function configureShader(settings: ShaderSettings): { 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 = reduce( - quantitativeFilters.toSorted(), + 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 = reduce( - categories, + const cAttrs = categories.reduce( (catAttrs, categoricalFilter, i) => ({ ...catAttrs, [categoricalFilter]: `CATEGORY_${i.toFixed(0)}` }), initialCategoricalAttrs, ); @@ -203,10 +227,10 @@ export function configureShader(settings: ShaderSettings): { ...cAttrs, [dataset.metadata.spatialColumn]: 'position', }; - const ordered = map([...categories, ...quantitativeFilters.toSorted()], (col) => colToAttribute[col]); + const ordered = [...categories, ...quantitativeFilters.toSorted()].map((col) => colToAttribute[col]); const config: Config = { - categoricalColumns: keys(cAttrs).map((columnName) => colToAttribute[columnName]), - quantitativeColumns: keys(qAttrs).map((columnName) => colToAttribute[columnName]), + categoricalColumns: Object.keys(cAttrs).map((columnName) => colToAttribute[columnName]), + quantitativeColumns: Object.keys(qAttrs).map((columnName) => colToAttribute[columnName]), categoricalTable: 'lookup', gradientTable: 'gradient', colorByColumn: colToAttribute[colorBy.column], diff --git a/packages/scatterbrain/src/render/webgpu/shader.ts b/packages/scatterbrain/src/render/webgpu/shader.ts index 9924572c..39255230 100644 --- a/packages/scatterbrain/src/render/webgpu/shader.ts +++ b/packages/scatterbrain/src/render/webgpu/shader.ts @@ -1,4 +1,3 @@ -import { isEqual, keys, map } from 'lodash'; import { beginValidate, endValidate } from './validate'; import * as wgh from 'webgpu-utils'; import type { vec2, vec4 } from '@alleninstitute/vis-geometry'; @@ -7,7 +6,6 @@ import { setCategoricalLookupTableValues } from './lookup-texture'; 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(' * '); } @@ -177,8 +175,7 @@ function generateVertexBufferLayout(config: Config) { }, ], }, - ...map( - categoricalColumns, + ...categoricalColumns.map( (_cat, i): GPUVertexBufferLayout => ({ arrayStride: 4, attributes: [ @@ -191,8 +188,7 @@ function generateVertexBufferLayout(config: Config) { stepMode: 'instance', }), ), - ...map( - quantitativeColumns, + ...quantitativeColumns.map( (_q, i): GPUVertexBufferLayout => ({ arrayStride: 4, attributes: [ @@ -255,74 +251,78 @@ export function buildPipeline(device: GPUDevice, config: Config) { // 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', - }); + // 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'); - } + // 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(keys(categories).toSorted(), 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!! - }; - return { pipeline, updateGradient, updateUniforms, updateCategorical }; + // 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/pnpm-lock.yaml b/pnpm-lock.yaml index d562b538..85337d2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -202,6 +202,9 @@ importers: '@types/node': specifier: 22.1.0 version: 22.1.0 + '@webgpu/types': + specifier: 0.1.69 + version: 0.1.69 packages: 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/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/webgpu-demo.tsx b/site/src/examples/scatterbrain/webgpu-demo.tsx new file mode 100644 index 00000000..d82044a4 --- /dev/null +++ b/site/src/examples/scatterbrain/webgpu-demo.tsx @@ -0,0 +1,121 @@ +import type { vec2, vec4 } from '@alleninstitute/vis-geometry'; +import { GpuDeviceProvider } from '../common/react/gpu-device-provider'; +import { useContext, useEffect, useRef, useState } from 'react'; +import { + buildWebGPUScatterbrainRenderFn as buildScatterbrainRenderFn, + loadScatterbrainDataset, + type Dataset, + type ShaderSettings, +} 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' }); + 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 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 + + // setCategoricalLookupTableValues(categories, lookup); + + const { render, connectToCache } = buildScatterbrainRenderFn(device, { + categoricalFilters: { '4MV7HA5DG2XJZ3UD8G9': 40, FS00DXV0T9R1X9FJ4QE: 40 }, + colorBy: { kind: 'metadata', column: 'FS00DXV0T9R1X9FJ4QE' }, + dataset, + highlightByColumn: { kind: 'metadata', column: 'FS00DXV0T9R1X9FJ4QE' }, + mode: 'color', + quantitativeFilters: [], + }); + // this ts error is bogus, dont know why + const renderOneFrame = () => { + if (ctx) { + render({ + client, + categories, + gradient: gradientData, + target: ctx.getCurrentTexture().createView(), + uniforms: { + 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 c96949c3..a45b83cb 100644 --- a/site/tsconfig.json +++ b/site/tsconfig.json @@ -5,6 +5,7 @@ "compilerOptions": { "baseUrl": "./", "jsx": "react-jsx", - "jsxImportSource": "react" + "jsxImportSource": "react", + "types": ["@webgpu/types"] } } From 26b03dfe8dfdbdd71426374d1d9349d209724dc2 Mon Sep 17 00:00:00 2001 From: noah Date: Tue, 5 May 2026 10:56:45 -0700 Subject: [PATCH 24/27] TIL lodash-es, hooray! --- packages/scatterbrain/package.json | 4 ++-- packages/scatterbrain/src/cache-client.ts | 2 +- packages/scatterbrain/src/dataset.ts | 2 +- .../scatterbrain/src/render/webgl/renderer.ts | 4 ++-- .../scatterbrain/src/render/webgl/shader.ts | 10 ++++---- .../src/render/webgpu/lookup-texture.ts | 2 +- .../scatterbrain/src/render/webgpu/shader.ts | 1 - pnpm-lock.yaml | 24 ++++++++++++++----- 8 files changed, 30 insertions(+), 19 deletions(-) diff --git a/packages/scatterbrain/package.json b/packages/scatterbrain/package.json index 574b6157..6828c183 100644 --- a/packages/scatterbrain/package.json +++ b/packages/scatterbrain/package.json @@ -51,7 +51,7 @@ "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", @@ -64,7 +64,7 @@ }, "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" } diff --git a/packages/scatterbrain/src/cache-client.ts b/packages/scatterbrain/src/cache-client.ts index e157b5fc..50a9a4e0 100644 --- a/packages/scatterbrain/src/cache-client.ts +++ b/packages/scatterbrain/src/cache-client.ts @@ -1,6 +1,6 @@ import type { Cacheable, SharedPriorityCache } from '@alleninstitute/vis-core'; import type { ColumnRequest, Item } from './types'; -import reduce from 'lodash/reduce'; +import reduce from 'lodash-es/reduce' import type { WebGLSafeBasicType } from './typed-array'; type Content = Record; diff --git a/packages/scatterbrain/src/dataset.ts b/packages/scatterbrain/src/dataset.ts index 98d00044..563c3376 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/render/webgl/renderer.ts b/packages/scatterbrain/src/render/webgl/renderer.ts index 1a261025..eb35c06e 100644 --- a/packages/scatterbrain/src/render/webgl/renderer.ts +++ b/packages/scatterbrain/src/render/webgl/renderer.ts @@ -1,8 +1,8 @@ import type { SharedPriorityCache } from '@alleninstitute/vis-core'; import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset } from '../../types'; import { 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 { getVisibleItems, type NodeWithBounds } from '../../dataset'; import { buildScatterbrainRenderCommand, type Config, configureShader, type ShaderSettings, VBO } from './shader'; diff --git a/packages/scatterbrain/src/render/webgl/shader.ts b/packages/scatterbrain/src/render/webgl/shader.ts index 6bac778f..7f71ca74 100644 --- a/packages/scatterbrain/src/render/webgl/shader.ts +++ b/packages/scatterbrain/src/render/webgl/shader.ts @@ -4,7 +4,7 @@ import type REGL from 'regl'; 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 @@ -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/lookup-texture.ts b/packages/scatterbrain/src/render/webgpu/lookup-texture.ts index 9399d88e..5f19386e 100644 --- a/packages/scatterbrain/src/render/webgpu/lookup-texture.ts +++ b/packages/scatterbrain/src/render/webgpu/lookup-texture.ts @@ -1,5 +1,5 @@ import type { vec4 } from '@alleninstitute/vis-geometry'; -import { reduce, keys } from 'lodash'; +import { reduce, keys } from 'lodash-es'; /** * a helper function that MUTATES ALL the values in the given @param texture diff --git a/packages/scatterbrain/src/render/webgpu/shader.ts b/packages/scatterbrain/src/render/webgpu/shader.ts index 39255230..3a293907 100644 --- a/packages/scatterbrain/src/render/webgpu/shader.ts +++ b/packages/scatterbrain/src/render/webgpu/shader.ts @@ -1,7 +1,6 @@ import { beginValidate, endValidate } from './validate'; import * as wgh from 'webgpu-utils'; import type { vec2, vec4 } from '@alleninstitute/vis-geometry'; -import { setCategoricalLookupTableValues } from './lookup-texture'; function rangeFor(col: string): `${string}_range` { return `${col}_range`; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85337d2a..b2732d33 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,9 +98,9 @@ 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 @@ -117,9 +117,9 @@ importers: 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 @@ -2010,6 +2010,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==} @@ -3028,6 +3031,9 @@ packages: 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==} @@ -6082,6 +6088,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': {} @@ -7289,6 +7299,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: {} From 46c7a1a326b09b7ab83677c2370b3f15d48ca461 Mon Sep 17 00:00:00 2001 From: noah Date: Tue, 5 May 2026 11:46:29 -0700 Subject: [PATCH 25/27] beat stuff up until it works --- packages/scatterbrain/src/index.ts | 8 +-- .../scatterbrain/src/render/webgl/index.ts | 8 +++ .../scatterbrain/src/render/webgl/renderer.ts | 2 + .../scatterbrain/src/render/webgpu/index.ts | 8 +++ .../src/render/webgpu/lookup-texture.ts | 8 +-- .../src/render/webgpu/renderer.ts | 35 ++++++++--- site/src/examples/scatterbrain/demo.tsx | 13 ++-- .../src/examples/scatterbrain/webgpu-demo.tsx | 60 ++++++++++--------- 8 files changed, 85 insertions(+), 57 deletions(-) create mode 100644 packages/scatterbrain/src/render/webgl/index.ts create mode 100644 packages/scatterbrain/src/render/webgpu/index.ts diff --git a/packages/scatterbrain/src/index.ts b/packages/scatterbrain/src/index.ts index d28a3e49..b9ef1d6d 100644 --- a/packages/scatterbrain/src/index.ts +++ b/packages/scatterbrain/src/index.ts @@ -1,10 +1,6 @@ export { buildScatterbrainCacheClient } from './cache-client'; export { getVisibleItems, loadDataset as loadScatterbrainDataset } from './dataset'; -export { - buildRenderFrameFn as buildScatterbrainRenderFn, - setCategoricalLookupTableValues, - updateCategoricalValue -} from './render/webgl/renderer'; -export { buildRenderFrameFn as buildWebGPUScatterbrainRenderFn } from './render/webgpu/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/render/webgl/renderer.ts b/packages/scatterbrain/src/render/webgl/renderer.ts index eb35c06e..0ae27aa7 100644 --- a/packages/scatterbrain/src/render/webgl/renderer.ts +++ b/packages/scatterbrain/src/render/webgl/renderer.ts @@ -8,6 +8,8 @@ import { getVisibleItems, type NodeWithBounds } from '../../dataset'; import { buildScatterbrainRenderCommand, type Config, configureShader, type ShaderSettings, VBO } from './shader'; import { buildScatterbrainCacheClient } from '../../cache-client'; import { MakeTaggedBufferView } from '../../typed-array'; + + function columnsForItem( config: Config, col2shader: Record, 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 index 5f19386e..01057898 100644 --- a/packages/scatterbrain/src/render/webgpu/lookup-texture.ts +++ b/packages/scatterbrain/src/render/webgpu/lookup-texture.ts @@ -1,5 +1,4 @@ import type { vec4 } from '@alleninstitute/vis-geometry'; -import { reduce, keys } from 'lodash-es'; /** * a helper function that MUTATES ALL the values in the given @param texture @@ -16,9 +15,10 @@ export function setCategoricalLookupTableValues( texture: GPUTexture, ) { const bytesPerPixel = 4; // rgba8 - const categoryKeys = keys(categories).toSorted(); + const categoryKeys = Object.keys(categories).toSorted(); const columns = categoryKeys.length; - const rows = reduce(categoryKeys, (highest, category) => Math.max(highest, keys(categories[category]).length), 1); + 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; @@ -37,7 +37,7 @@ export function setCategoricalLookupTableValues( // 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 = keys(category).length; + 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; diff --git a/packages/scatterbrain/src/render/webgpu/renderer.ts b/packages/scatterbrain/src/render/webgpu/renderer.ts index 1fb7efd9..755fea27 100644 --- a/packages/scatterbrain/src/render/webgpu/renderer.ts +++ b/packages/scatterbrain/src/render/webgpu/renderer.ts @@ -1,13 +1,15 @@ /** biome-ignore-all lint/performance/noAccumulatingSpread: leave me be */ -import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset, WebGLSafeBasicType } from '~/src/types'; -import { buildPipeline, type Config } from './shader'; -import type { ShaderSettings as BaseSettings } from '../webgl/shader'; -import { getVisibleItems, type NodeWithBounds } from '~/src/dataset'; import type { Cacheable, SharedPriorityCache } from '@alleninstitute/vis-core'; -import { buildScatterbrainCacheClient } from '~/src/cache-client'; 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 Last> = T extends readonly [infer K] ? K : Last>; @@ -22,7 +24,14 @@ function omit, Drop extends ReadonlyArray; // 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 }; }; @@ -106,8 +115,15 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) const render = (props: RenderPassProps & { client: ReturnType> }) => { const { target, camera, offset, filteredOutColor, spatialFilterBox, quantitativeRangeFilters, highlightedValue, client, categoricalLookupTable, gradient } = props; - const uniforms = { - camera: { ...camera, view: Box2D.toFlatArray(camera.view) }, offset, filteredOutColor, spatialFilterBox: Box2D.toFlatArray(spatialFilterBox), highlightedValue, quantitativeRangeFilters + 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); @@ -133,7 +149,7 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) if (Object.keys(Object.keys(settings.categoricalFilters)).length > 0) { entries.push({ binding: 1, resource: categoricalLookupTable }); } - if (Object.keys(uniforms.quantitativeRangeFilters).length > 0) { + if (Object.keys(quantitativeRangeFilters).length > 0) { entries.push({ binding: 2, resource: gradient }); } const bg = device.createBindGroup({ @@ -155,6 +171,7 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) pass.setPipeline(pipeline); pass.setBindGroup(0, bg); + // now - actually start submitting stuff const visible = getVisibleItems(dataset, camera, 0.1).map(prepareQtCell); client.setPriorities(visible, []); diff --git a/site/src/examples/scatterbrain/demo.tsx b/site/src/examples/scatterbrain/demo.tsx index 26c11f3c..50c7c606 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 index d82044a4..a3858f8b 100644 --- a/site/src/examples/scatterbrain/webgpu-demo.tsx +++ b/site/src/examples/scatterbrain/webgpu-demo.tsx @@ -1,12 +1,7 @@ import type { vec2, vec4 } from '@alleninstitute/vis-geometry'; import { GpuDeviceProvider } from '../common/react/gpu-device-provider'; import { useContext, useEffect, useRef, useState } from 'react'; -import { - buildWebGPUScatterbrainRenderFn as buildScatterbrainRenderFn, - loadScatterbrainDataset, - type Dataset, - type ShaderSettings, -} from '@alleninstitute/vis-scatterbrain'; +import { WebGPU, loadScatterbrainDataset, type Dataset } from '@alleninstitute/vis-scatterbrain'; import { GpuContext } from '../common/react/gpu-device-provider'; import { SharedPriorityCache } from '@alleninstitute/vis-core'; @@ -37,14 +32,14 @@ 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: [], -}; +// 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(); } @@ -70,6 +65,11 @@ function Demo(props: Props) { } // 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; @@ -77,13 +77,18 @@ function Demo(props: Props) { 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 - // setCategoricalLookupTableValues(categories, lookup); + lookup = WebGPU.setCategoricalLookupTableValues(categories, device, lookup); - const { render, connectToCache } = buildScatterbrainRenderFn(device, { + const { render, connectToCache } = WebGPU.buildRenderFrameFn(device, { categoricalFilters: { '4MV7HA5DG2XJZ3UD8G9': 40, FS00DXV0T9R1X9FJ4QE: 40 }, colorBy: { kind: 'metadata', column: 'FS00DXV0T9R1X9FJ4QE' }, dataset, @@ -91,25 +96,22 @@ function Demo(props: Props) { mode: 'color', quantitativeFilters: [], }); - // this ts error is bogus, dont know why const renderOneFrame = () => { if (ctx) { render({ client, - categories, - gradient: gradientData, + gradient: gradientTexture.createView(), + categoricalLookupTable: lookup.createView(), target: ctx.getCurrentTexture().createView(), - uniforms: { - 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] }, + 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] }, }); } }; From 8a4c235796c40040a6107756188254293aa10b67 Mon Sep 17 00:00:00 2001 From: noah Date: Tue, 5 May 2026 12:33:29 -0700 Subject: [PATCH 26/27] make it build, confirm working webgpu example... discover little bug... --- packages/scatterbrain/package.json | 2 +- .../src/render/webgpu/renderer.ts | 45 ++++--------------- 2 files changed, 10 insertions(+), 37 deletions(-) diff --git a/packages/scatterbrain/package.json b/packages/scatterbrain/package.json index 6828c183..32805cda 100644 --- a/packages/scatterbrain/package.json +++ b/packages/scatterbrain/package.json @@ -38,7 +38,7 @@ "scripts": { "typecheck": "tsc --noEmit", "build": "parcel build --no-cache", - "dev": "parcel watch --port 1239", + "dev": "parcel watch --no-cache --port 1239", "test": "vitest --watch", "test:ci": "vitest run", "coverage": "vitest run --coverage", diff --git a/packages/scatterbrain/src/render/webgpu/renderer.ts b/packages/scatterbrain/src/render/webgpu/renderer.ts index 755fea27..ef762519 100644 --- a/packages/scatterbrain/src/render/webgpu/renderer.ts +++ b/packages/scatterbrain/src/render/webgpu/renderer.ts @@ -12,18 +12,9 @@ export { setCategoricalLookupTableValues, updateCategoricalValue } from './looku export type Head> = T extends readonly [] ? never : T[0]; export type Tail> = T extends readonly [infer _I, ...infer rest] ? rest : never; -export type Last> = T extends readonly [infer K] ? K : Last>; export type OR> = T extends readonly [infer K] ? K : Head | OR>; -// todo figure out why parcel cant bundle lodash... -function omit, Drop extends ReadonlyArray>(obj: T, ...drop: Drop): Omit> { - const stuff = { ...obj }; - for (const d of drop) { - delete stuff[d] - } - return stuff; -} export type ShaderSettings = { dataset: ScatterbrainDataset | SlideviewScatterbrainDataset; categoricalFilters: Record; // category name -> maximum # of distinct values in that category @@ -98,14 +89,6 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) const client = buildScatterbrainCacheClient(allColumns, cache, toGpuBuffer, onDataArrived); return client; }; - const viridis = new Uint8Array(256 * 4); - // ugh todo - for (let i = 0; i < 256; i += 1) { - viridis[i * 4 + 0] = i; - viridis[i * 4 + 1] = i; - viridis[i * 4 + 2] = i; - viridis[i * 4 + 3] = 255; - } const unis = makeUniformBuffer(); const ubo = device.createBuffer({ size: uniformSize, @@ -115,6 +98,8 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) 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, @@ -127,23 +112,13 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) } beginValidate(device); - // we know how many columns are gonna be filtered... because we have a closure over the settings - // thats "fine" because this renderer already cant be applied a different set of columns, nor a different dataset... - - // const bg0 = updateUniforms({ - // ...omit(uniforms, 'camera', 'spatialFilterBox', 'quantitativeRangeFilters'), - // view: Box2D.toFlatArray(uniforms.camera.view), - // spatialFilterBox: Box2D.toFlatArray(uniforms.spatialFilterBox), - // ...uniforms.quantitativeRangeFilters, - // }); - // const bg1 = updateCategorical(categories); - // const bg2 = updateGradient(viridis); // todo - dont do this every frame... - - // unlike the textures... we'd like to not make our user handle raw buffers... - // but perhaps that is silly updateUniforms(uniforms, unis); - // it would be... very slightly better if we could not do this for every single node in the quadtree - but - // thats a future thing TODO + // 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)) device.queue.writeBuffer(ubo, 0, unis.arrayBuffer); const entries: GPUBindGroupEntry[] = [{ binding: 0, resource: ubo }]; if (Object.keys(Object.keys(settings.categoricalFilters)).length > 0) { @@ -157,6 +132,7 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) entries, layout: pipeline.getBindGroupLayout(0), }); + const enc = device.createCommandEncoder({ label: 'encoder for scatterbrain render pass' }); const pass = enc.beginRenderPass({ colorAttachments: [ @@ -175,7 +151,6 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) // now - actually start submitting stuff const visible = getVisibleItems(dataset, camera, 0.1).map(prepareQtCell); client.setPriorities(visible, []); - // console.log('visible: ', visible.length) for (const node of visible) { if (client.has(node)) { const drawable = client.get(node); @@ -185,8 +160,6 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) for (let i = 0; i < config.vertexLocationOrder.length; i++) { pass.setVertexBuffer(i, columns[config.vertexLocationOrder[i]].buffer); } - //bind all the vbo... - // with the correct dang locations, ugh pass.draw(4, count); } } From 88045912823c2928a123c0383a0c7a5f21b96252 Mon Sep 17 00:00:00 2001 From: noah Date: Tue, 5 May 2026 13:59:18 -0700 Subject: [PATCH 27/27] a note about per-draw uniform data... --- packages/scatterbrain/src/render/webgpu/renderer.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/scatterbrain/src/render/webgpu/renderer.ts b/packages/scatterbrain/src/render/webgpu/renderer.ts index ef762519..09fd9c8c 100644 --- a/packages/scatterbrain/src/render/webgpu/renderer.ts +++ b/packages/scatterbrain/src/render/webgpu/renderer.ts @@ -119,6 +119,12 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) // 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) { @@ -161,6 +167,7 @@ export function buildRenderFrameFn(device: GPUDevice, settings: ShaderSettings) pass.setVertexBuffer(i, columns[config.vertexLocationOrder[i]].buffer); } pass.draw(4, count); + } } }