Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ export {
export { WorkerPool, type WorkerInit } from './workers/worker-pool';

export * from './rendering/webgpu';
export { given, FilterTable } from './rendering/webgpu/filter/query';
export { given } from './rendering/webgpu/filter/query';
215 changes: 46 additions & 169 deletions packages/core/src/rendering/webgpu/filter/gen.ts
Original file line number Diff line number Diff line change
@@ -1,165 +1,15 @@
// generate the interesting bits of the filter-shader

import {
type Elem,
type FilterShaderQueryContext,
type WgslType,
type ITable,
type OP,
type PredLst,
type Sel,
type SimplePredExpr,
type Tables,
type VOP,
} from './types';

function isVecOp(s: OP | VOP): s is VOP {
return s.startsWith('a');
}
function parseVecOp(op: VOP) {
const aggregation = op.substring(0, 3);
const sop = op.substring(4).split(')')[0];
return [aggregation, sop] as ['any' | 'all', OP];
}

// dont export this - its only legit if we know a bunch of stuff about the string
function fieldType(s: string, table: ITable) {
return table[s];
}
export function indexExprType(s: string, tables: Tables, from: string) {
const ext = looksLikeIndexExpr(s, tables, from);
return ext ? ext.type : undefined;
}
export function looksLikeIndexExpr(s: string, tables: Tables, from: string) {
// the goal here is to parse an expr that looks like:
// someTable[someColumn].someOtherColumn
// could I have used Regex? yes, and perhaps that would be more elegant!
// for now, this works, and its not too long

const [tbl, rest] = s.split('[');
if (tbl && rest) {
const [index_field, selection] = rest.split('].');
if (index_field && selection) {
if (tables[tbl] && tables[from] && tables[from][index_field] && tables[tbl][selection]) {
// return the expected type
return {
fTable: tbl,
selection,
index_field,
from,
type: tables[tbl][selection],
};
}
}
}
return false;
}
function genRef(ctx: FilterShaderQueryContext<Tables>, operand: string, indexing: string = 'element') {
if (operand === '$index') {
return indexing;
}
const indexed = looksLikeIndexExpr(operand, ctx.tables, ctx.from);
if (indexed) {
return `${indexed.fTable}_${indexed.selection}[${indexed.from}_${indexed.index_field}[${indexing}]]`;
} else if (operand in ctx.tables[ctx.from]!) {
return `${ctx.from}_${operand}[${indexing}]`;
}
return operand;
}
function genPred(ctx: FilterShaderQueryContext<Tables>, p: SimplePredExpr) {
const [lhs, op, rhs] = p.split(' ') as [string, OP | VOP, string];
// TODO! handle non-PARAMETER rhs exprs
const param = `${ctx.uniformName}.${rhs}`;
if (isVecOp(op)) {
const [agg, sop] = parseVecOp(op);
const str = `${agg}(${genRef(ctx, lhs)} ${sop} ${param})`; // any / all are built-in wgsl fns over vectors of booleans
return str;
}
return `${genRef(ctx, lhs)} ${op} ${param}`;
}
function handlePredElem(ctx: FilterShaderQueryContext<Tables>, e: Elem<PredLst>) {
switch (e.OP) {
case 'and (':
return `&& (${genPred(ctx, e.pred)}`;
case 'or (':
return `|| (${genPred(ctx, e.pred)}`;
case ')':
return ')';
case 'and':
return `&& ${genPred(ctx, e.pred)}`;
case 'or':
return `|| ${genPred(ctx, e.pred)}`;
}
}
export function generatePredicateExpr(ctx: FilterShaderQueryContext<Tables>, exprs: [SimplePredExpr, ...PredLst]) {
return exprs.map((e) => (typeof e === 'string' ? genPred(ctx, e) : handlePredElem(ctx, e))).join('\n');
}
function extractPred(p: SimplePredExpr | Elem<PredLst>): SimplePredExpr | undefined {
return typeof p === 'string' ? p : 'pred' in p ? p.pred : undefined;
}
export function genUniformParameterStruct(tables: Tables, from: string, exprs: [SimplePredExpr, ...PredLst]) {
const fields = exprs.reduce(
(acc, cur: SimplePredExpr | Elem<PredLst>) => {
const info = extractPredicateInfo(tables, from, extractPred(cur));
return info === undefined ? acc : { ...acc, [info[0]]: info[1] };
},
{} as Record<string, WgslType>
);

const decls = Object.entries(fields)
.map(([param, type]) => `${param}:${type}`)
.join(',\n');

return `struct Parameters {
${decls}
};
`;
}
function extractPredicateInfo(tables: Tables, from: string, expr: SimplePredExpr | undefined) {
if (expr === undefined) {
return undefined;
}
const [lhs, _op, rhs] = expr.split(' ') as [string, OP | VOP, string];
let lhsType = indexExprType(lhs, tables, from) || fieldType(lhs, tables[from]!);
if (lhsType) {
return [rhs, lhsType] as const;
}
return undefined;
}

// we also need to generate a storage buffer per field per table...
// todo - someday support row-major tables - structs vs. parallel arrays
export function generateTableBindings(
tableName: string,
table: Record<string, WgslType>,
group: number,
bindingStart: number = 0
) {
const cols = Object.entries(table);
let bindingLookup = cols.reduce(
(acc, [f, _t], index) => ({ ...acc, [f]: index + bindingStart }),
{} as Record<string, number>
);
const decls = cols
.map(
([f, t], index) =>
`@group(${group}) @binding(${index + bindingStart}) var<storage,read> ${tableName}_${f}: array<${t}>;`
)
.join('\n');
return { decls, numBindings: cols.length, bindingLookup };
}

import { type WgslType, type Sel, type Tables } from './types';
const entries = <T extends {}>(r: T): ReadonlyArray<[keyof T, T[keyof T]]> => Object.entries(r) as any;

Check failure on line 2 in packages/core/src/rendering/webgpu/filter/gen.ts

View workflow job for this annotation

GitHub Actions / Lint

typescript(no-explicit-any)

Unexpected `any`. Specify a different type.
function generateOutputStructure(selections: ReadonlyArray<Sel>) {
// the names of the values in the structure dont matter at all -
const structName = `OutputStruct`;
const fields = selections.map((s, i) => `field_${i.toFixed(0)}: ${s.type},`).join('\n');
const decl = `struct ${structName} {\n ${fields} \n };`;
const initializers = selections.map((s) => s.selection).join(', '); // === '$index' ? 'tmp - 1' : `${s.selection}[tmp - 1]`).join(', ');
const initializers = selections.map((s) => s.selection).join(', ');
const construct = `${structName}(${initializers})`;

return { structName, decl, construct };
}

export function generateShader(params: {
workgroupSize: number;
inputBindings: string;
Expand All @@ -169,11 +19,11 @@
selections: ReadonlyArray<Sel>;
}) {
const { inputBindings, predicateExpr, uniformStruct, selections, workgroupSize, indexed } = params;
const { structName, decl: outputStructDecl, construct } = generateOutputStructure(selections);
const { structName, decl, construct } = generateOutputStructure(selections);
const host = /*wgsl*/ `

${uniformStruct.decl}
${outputStructDecl}
${decl}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks like this got reverted somehow 😅

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.

fixed, sorry about that...

var<workgroup> results: array<u32,${workgroupSize}>;
var<workgroup> count: atomic<u32>;

Expand All @@ -194,8 +44,7 @@
@builtin(local_invocation_id) local_id: vec3<u32>,
){
let element = ${indexed ? 'elements[global_id.x]' : 'global_id.x'};
let inbounds = global_id.x < arrayLength(&passing);
let predicateResult = inbounds && ${predicateExpr};
let predicateResult = ${predicateExpr};
if(predicateResult){
atomicAdd(&count,1u);
results[local_id.x]=(element+1);
Expand All @@ -208,6 +57,10 @@
if(local_id.x==0){
let c = atomicLoad(&count);
let start = atomicAdd(&used[0],c);
// early return - if the start is past the last result:
if(start >= arrayLength(&passing)){
return;
}
var p = start;
for(var i = 0;i < ${workgroupSize};i++){
if(results[i]>0){
Expand All @@ -222,16 +75,39 @@
return host;
}

export function genQuery<Ts extends Tables>(
ctx: FilterShaderQueryContext<Ts>,
predicates: PredLst,
export function generateTableBindings(
tableName: string,
table: Record<string, WgslType>,
group: number,
bindingStart: number = 0
) {
const cols = entries(table);
let bindingLookup = cols.reduce(
(acc, [f, _t], index) => ({ ...acc, [f]: index + bindingStart }),
{} as Record<string, number>
);
const decls = cols
.map(
([f, t], index) =>
`@group(${group}) @binding(${index + bindingStart}) var<storage,read> ${tableName}_${f}: array<${t}>;`
)
.join('\n');
return { decls, numBindings: cols.length, bindingLookup };
}
export type FilterCtx<Ts extends Tables> = {
from: string;
tables: Ts;
selections: ReadonlyArray<Sel>;
uniformName: string;
uniformTypeName: string;
};
export function assembleQuery<Ts extends Tables>(
ctx: FilterCtx<Ts>,
predExpr: string,
paramsDecl: string,
wgSize: number,
indexed: boolean
) {
// const bindings = generateTableBindings(ctx.from as string, ctx.tables[ctx.from]!, 1)
// for now, assume all tables will be used somehow
// future - allow specification of which tables go in which groups
// const bindings = map(keys(ctx.tables), (t) => generateTableBindings(t, ctx.tables[t]!, 1))
let bindingStart = 3;
let bindings: string = '';
const bindingLookups: Record<string, Record<string, number>> = {};
Expand All @@ -241,16 +117,17 @@
bindingStart += binding.numBindings;
bindingLookups[t] = binding.bindingLookup;
}
const predicate = generatePredicateExpr(ctx, [ctx.firstPred, ...predicates]);
const paramsDecl = genUniformParameterStruct(ctx.tables, ctx.from, [ctx.firstPred, ...predicates]);
const uniStructDecl = `struct ${ctx.uniformTypeName} {
${paramsDecl}
};`;
return {
shader: generateShader({
workgroupSize: wgSize,
inputBindings: bindings,
predicateExpr: predicate,
predicateExpr: predExpr,
indexed,
selections: ctx.selections.map((s) => ({ selection: genRef(ctx, s.selection, 'tmp - 1'), type: s.type })),
uniformStruct: { name: ctx.uniformName, typeName: ctx.uniformTypeName, decl: paramsDecl },
selections: ctx.selections.map((s) => ({ selection: s.selection, type: s.type })),
uniformStruct: { name: ctx.uniformName, typeName: ctx.uniformTypeName, decl: uniStructDecl },
}),
bindingLookups,
};
Expand Down
Loading
Loading