Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6b57bba
thinking about what it would be like to use webGPU here for scatterbr…
froyo-np Apr 22, 2026
0b0d594
switch to vite (library mode) because it supports typeGPU's wgsl tran…
froyo-np Apr 22, 2026
a64a9d8
minor import cleanup
froyo-np Apr 23, 2026
71db373
try typegpu until I go crazy, but it does (almost) work
froyo-np Apr 23, 2026
8dd215c
fix the uint16 mystery. implement the raw webGPU scatterplot renderer…
froyo-np Apr 23, 2026
376e96c
WIP total reorg
froyo-np Apr 23, 2026
b04f894
fix oh so many mistakes
froyo-np Apr 24, 2026
8a7ab4f
continue to fight with stuff to make the examples work...
froyo-np Apr 24, 2026
80532be
lots of cleanup
froyo-np Apr 24, 2026
0051cd3
more cleanup
froyo-np Apr 24, 2026
e93aa02
fmt
froyo-np May 1, 2026
3eaf74d
tsconfig webgpu types
froyo-np May 1, 2026
b04849f
webgpu types
froyo-np May 1, 2026
855c371
put everything back to regl until we can figure out what is going on …
froyo-np May 1, 2026
c5bed41
clean it up
froyo-np May 1, 2026
3f12b87
lint & fmt
froyo-np May 1, 2026
da81118
fix a very silly hack that was totally ruining astro's build and it w…
froyo-np May 1, 2026
47afe9a
oh also this has float16array so thats good
froyo-np May 1, 2026
086a838
cleanup experimental stuff
froyo-np May 1, 2026
24d78a0
update lockfile
froyo-np May 1, 2026
9b3cda9
Merge remote-tracking branch 'origin/main' into noah/webgpu-scatterbr…
froyo-np May 4, 2026
b582440
install
froyo-np May 4, 2026
82d8fa3
use node 24 in ci actions, so that it doesnt choke on Float16Array in…
froyo-np May 4, 2026
43d3ae7
for some fun reason lodash does not play nice with parcel...
froyo-np May 4, 2026
26b03df
TIL lodash-es, hooray!
froyo-np May 5, 2026
46c7a1a
beat stuff up until it works
froyo-np May 5, 2026
8a4c235
make it build, confirm working webgpu example... discover little bug...
froyo-np May 5, 2026
8804591
a note about per-draw uniform data...
froyo-np May 5, 2026
c66c468
Merge remote-tracking branch 'origin/main' into noah/webgpu-scatterbr…
froyo-np May 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions packages/scatterbrain/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@alleninstitute/vis-scatterbrain",
"version": "0.0.2",
"version": "0.0.3",
"contributors": [
{
"name": "Lane Sawyer",
Expand Down Expand Up @@ -37,7 +37,8 @@
],
"scripts": {
"typecheck": "tsc --noEmit",
"build": "parcel build --no-cache",
"build": "vite build",
"oldbuild": "parcel build --no-cache",
"dev": "parcel watch --port 1239",
"demo": "vite",
"test": "vitest --watch",
Expand All @@ -55,6 +56,8 @@
"lodash": "4.17.23",
"regl": "2.1.0",
"ts-pattern": "5.9.0",
"typegpu": "0.11.2",
"webgpu-utils": "2.0.2",
"zod": "4.3.6"
},
"publishConfig": {
Expand All @@ -63,6 +66,10 @@
},
"packageManager": "pnpm@9.14.2",
"devDependencies": {
"@types/lodash": "4.17.24"
"@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"
}
}
}
67 changes: 67 additions & 0 deletions packages/scatterbrain/src/cache-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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<V extends Cacheable> = Record<string, V>

export function buildScatterbrainCacheClient<V extends Cacheable>(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file got moved - its mostly the same as the old webGL cache client! it did get a tiny tweak to make it agnostic to webGL / webGPU - you can use the same function (this one) to create either a webGL or webGPU cache-client, and they can even share the same cache, if you wanted

allNeededColumns: readonly string[],
cache: SharedPriorityCache,
toCacheValue: (buffer: ArrayBuffer, type: WebGLSafeBasicType) => V,
onDataArrived: () => void,
) {
const client = cache.registerClient<Item, Content<V>>({
cacheKeys: (item) => {
const { dataset, node, columns } = item;
return reduce<Record<string, ColumnRequest>, Record<string, string>>(
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<string, ColumnRequest>, Record<string, (signal: AbortSignal) => Promise<V>>>(
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<V> => {
for (const column of allNeededColumns) {
if (!(column in v)) {
return false;
}
}
return true;
},
onDataArrived,
});
return client;
}
32 changes: 32 additions & 0 deletions packages/scatterbrain/src/demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!DOCTYPE html>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the demo I used to debug my webGPU implementation - we should remove it, but for now starlight seems to have trouble with our webGPU utils (a 3rd party helper)

<html>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=yes">
<title>hello webgpu</title>
<style>
:root {
color-scheme: light dark;
}

html,
body {
margin: 0;
/* remove default margin */
height: 100%;
overflow: hidden;
/* make body fill the browser window */
display: flex;
touch-action: none;
place-content: center center;
}
</style>
<script defer src="../dist/main.js" type="module"></script>
</head>

<body>
<canvas id="canvas"></canvas>
</body>

</html>
121 changes: 121 additions & 0 deletions packages/scatterbrain/src/demo.ts
Original file line number Diff line number Diff line change
@@ -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<number, { color: vec4; filteredIn: boolean }> = {};
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() {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the bootstrap function for my demo - remote eventually


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<ShaderSettings, 'dataset'> = {
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();
9 changes: 6 additions & 3 deletions packages/scatterbrain/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
export {
buildRenderFrameFn as buildScatterbrainRenderFn,
buildScatterbrainCacheClient,
setCategoricalLookupTableValues,
updateCategoricalValue,
} from './renderer';
} 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 { getVisibleItems, loadDataset as loadScatterbrainDataset } from './dataset';
Loading
Loading