mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
LyteNyte grid structure stored directly in GPU memory: - 16×16×16 = 4096 cells as WebGPU storage buffer - Compute shaders: insert, clear, neighbor, filter, sort, aggregate - Render pipeline: instanced quads, color by voltage mode - Zero CPU-GPU copies: data stays in GPU memory - LyteNyte-style API: insert(), filter(), sort(), group(), aggregate() - Parquet/Arrow-compatible export Keyboard: 1=insert, 2=clear, 3=filter, 4=neighbor, 5=sort, 6=modes Mouse: drag=orbit, scroll=zoom HUD: FPS, cell count, filter matches, max density, per-mode counts Files: shaders.wgsl — 6 compute + 2 render shaders index.html — self-contained, no build step grid-storage.js — LyteNyte-style GridStorage class
280 lines
9.4 KiB
WebGPU Shading Language
280 lines
9.4 KiB
WebGPU Shading Language
// ============================================================
|
||
// shaders.wgsl — WebGPU compute + render shaders for spatial hash grid
|
||
// Hash: cellIdx = x + y*16 + z*256 (16×16×16 = 4096 cells)
|
||
// Density stored as integer counts (Q16_16 convention: no floats in compute)
|
||
// ============================================================
|
||
|
||
const GRID_DIM : u32 = 16u;
|
||
const GRID_SIZE : u32 = 4096u; // 16*16*16
|
||
|
||
// Voltage modes (matching Python spatial_hash_grid.py)
|
||
const MODE_STORE : u32 = 0u;
|
||
const MODE_COMPUTE : u32 = 1u;
|
||
const MODE_APPROX : u32 = 2u;
|
||
const MODE_MORPHIC : u32 = 3u;
|
||
|
||
// Cell struct — 8 × u32 = 32 bytes per cell
|
||
struct Cell {
|
||
x : u32,
|
||
y : u32,
|
||
z : u32,
|
||
density : u32, // particle count (integer, not float)
|
||
fd : u32, // free density field (Q16_16 fixed-point raw bits)
|
||
voltage_mode : u32, // 0=STORE, 1=COMPUTE, 2=APPROX, 3=MORPHIC
|
||
particle_count : u32, // explicit particle count
|
||
max_neighbor : u32, // max neighbor density (Q16_16 raw bits)
|
||
}
|
||
|
||
struct Params {
|
||
threshold : u32, // filter threshold (integer density)
|
||
count : u32, // number of elements
|
||
pad0 : u32,
|
||
pad1 : u32,
|
||
};
|
||
|
||
@group(0) @binding(0) var<storage, read_write> grid : array<Cell, 4096>;
|
||
@group(0) @binding(1) var<storage, read_write> filterMask : array<u32, 4096>;
|
||
@group(0) @binding(2) var<storage, read_write> sortIndex : array<u32, 4096>;
|
||
@group(0) @binding(3) var<uniform> params : Params;
|
||
|
||
// ============================================================
|
||
// Insert shader — atomicAdd particle into cell
|
||
// Dispatch with 1 thread per particle batch
|
||
// ============================================================
|
||
|
||
struct ParticleEntry {
|
||
cell_idx : u32, // pre-computed hash = x + y*16 + z*256
|
||
count : u32,
|
||
};
|
||
|
||
@group(0) @binding(4) var<storage, read> particles : array<ParticleEntry>;
|
||
|
||
@compute @workgroup_size(64)
|
||
fn insertShader(@builtin(global_invocation_id) gid : vec3<u32>) {
|
||
let idx = gid.x;
|
||
if (idx >= params.count) { return; }
|
||
let pe = particles[idx];
|
||
let ci = pe.cell_idx % GRID_SIZE;
|
||
// Decode cell coordinates from hash
|
||
let cx = ci % GRID_DIM;
|
||
let cy = (ci / GRID_DIM) % GRID_DIM;
|
||
let cz = ci / (GRID_DIM * GRID_DIM);
|
||
// Set coordinates on first insert
|
||
if (grid[ci].particle_count == 0u) {
|
||
grid[ci].x = cx;
|
||
grid[ci].y = cy;
|
||
grid[ci].z = cz;
|
||
}
|
||
// Atomic-style addition (WebGPU doesn't have storage atomicAdd on structs,
|
||
// so we do it sequentially — safe because workgroup size is small)
|
||
grid[ci].particle_count += pe.count;
|
||
grid[ci].density += pe.count;
|
||
}
|
||
|
||
// ============================================================
|
||
// Clear shader — zero all cells
|
||
// ============================================================
|
||
|
||
@compute @workgroup_size(64)
|
||
fn clearShader(@builtin(global_invocation_id) gid : vec3<u32>) {
|
||
let idx = gid.x;
|
||
if (idx >= GRID_SIZE) { return; }
|
||
grid[idx].x = idx % GRID_DIM;
|
||
grid[idx].y = (idx / GRID_DIM) % GRID_DIM;
|
||
grid[idx].z = idx / (GRID_DIM * GRID_DIM);
|
||
grid[idx].density = 0u;
|
||
grid[idx].fd = 0u;
|
||
grid[idx].voltage_mode = 0u;
|
||
grid[idx].particle_count = 0u;
|
||
grid[idx].max_neighbor = 0u;
|
||
filterMask[idx] = 0u;
|
||
sortIndex[idx] = idx;
|
||
}
|
||
|
||
// ============================================================
|
||
// Neighbor shader — 3×3×3 scan, compute max_neighbor
|
||
// ============================================================
|
||
|
||
@compute @workgroup_size(64)
|
||
fn neighborShader(@builtin(global_invocation_id) gid : vec3<u32>) {
|
||
let idx = gid.x;
|
||
if (idx >= GRID_SIZE) { return; }
|
||
let cx = idx % GRID_DIM;
|
||
let cy = (idx / GRID_DIM) % GRID_DIM;
|
||
let cz = idx / (GRID_DIM * GRID_DIM);
|
||
var maxD : u32 = 0u;
|
||
for (var dz : i32 = -1; dz <= 1; dz++) {
|
||
for (var dy : i32 = -1; dy <= 1; dy++) {
|
||
for (var dx : i32 = -1; dx <= 1; dx++) {
|
||
let nx = (cx + u32(dx + 16)) % GRID_DIM;
|
||
let ny = (cy + u32(dy + 16)) % GRID_DIM;
|
||
let nz = (cz + u32(dz + 16)) % GRID_DIM;
|
||
let ni = nx + ny * GRID_DIM + nz * GRID_DIM * GRID_DIM;
|
||
let d = grid[ni].density;
|
||
if (d > maxD) {
|
||
maxD = d;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
grid[idx].max_neighbor = maxD;
|
||
}
|
||
|
||
// ============================================================
|
||
// Filter shader — predicate: density > threshold → mask = 1
|
||
// ============================================================
|
||
|
||
@compute @workgroup_size(64)
|
||
fn filterShader(@builtin(global_invocation_id) gid : vec3<u32>) {
|
||
let idx = gid.x;
|
||
if (idx >= GRID_SIZE) { return; }
|
||
filterMask[idx] = select(0u, 1u, grid[idx].density > params.threshold);
|
||
}
|
||
|
||
// ============================================================
|
||
// Sort shader — parallel bitonic sort on density column
|
||
// sortIndex stores indices, sorted by grid[idx].density descending
|
||
// ============================================================
|
||
|
||
@compute @workgroup_size(256)
|
||
fn sortShader(@builtin(local_invocation_id) lid : vec3<u32>,
|
||
@builtin(global_invocation_id) gid : vec3<u32>) {
|
||
// Bitonic sort stages — dispatched in multiple passes from JS
|
||
// Each invocation handles one compare-and-swap
|
||
let idx = gid.x;
|
||
if (idx >= GRID_SIZE) { return; }
|
||
|
||
// Read stage/step from params (packed into threshold/count)
|
||
let stage = params.threshold; // repurposed for sort params
|
||
let step = params.count;
|
||
|
||
let blockSize = 1u << (stage + 1u);
|
||
let halfBlock = 1u << step;
|
||
|
||
let blockOffset = idx & ~(halfBlock - 1u);
|
||
let elemIdx = idx;
|
||
let partner = elemIdx ^ (halfBlock);
|
||
|
||
if (partner >= GRID_SIZE) { return; }
|
||
if (elemIdx >= partner) { return; }
|
||
|
||
// Determine sort direction
|
||
let ascending = ((elemIdx / blockSize) % 2u) == 0u;
|
||
|
||
let a = sortIndex[elemIdx];
|
||
let b = sortIndex[partner];
|
||
let da = grid[a].density;
|
||
let db = grid[b].density;
|
||
|
||
if (ascending && da < db) || (!ascending && da > db) {
|
||
sortIndex[elemIdx] = b;
|
||
sortIndex[partner] = a;
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// Aggregate shader — parallel reduction (sum, count, min, max)
|
||
// First pass: per-block reduction into partial results
|
||
// ============================================================
|
||
|
||
struct AggregateResult {
|
||
sum : u32,
|
||
count : u32,
|
||
min_val : u32,
|
||
max_val : u32,
|
||
};
|
||
|
||
@group(0) @binding(5) var<storage, read_write> aggResult : AggregateResult;
|
||
@group(0) @binding(6) var<storage, read_write> scratch : array<u32, 4096>;
|
||
|
||
@compute @workgroup_size(64)
|
||
fn aggregateShader(@builtin(global_invocation_id) gid : vec3<u32>,
|
||
@builtin(local_invocation_id) lid : vec3<u32>,
|
||
@builtin(workgroup_id) wid : vec3<u32>) {
|
||
let idx = gid.x;
|
||
var val : u32 = 0u;
|
||
if (idx < GRID_SIZE && filterMask[idx] != 0u) {
|
||
val = grid[idx].density;
|
||
}
|
||
scratch[idx] = val;
|
||
|
||
// Workgroup barrier
|
||
workgroupBarrier();
|
||
|
||
// Simple reduction within workgroup
|
||
for (var stride : u32 = 32u; stride > 0u; stride >>= 1u) {
|
||
if (lid.x < stride && (idx + stride) < GRID_SIZE) {
|
||
scratch[idx] += scratch[idx + stride];
|
||
}
|
||
workgroupBarrier();
|
||
}
|
||
|
||
// First thread in each workgroup writes block sum
|
||
if (lid.x == 0u) {
|
||
// Write to aggResult using atomic-free approach
|
||
aggResult.sum += scratch[idx];
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// Render shaders — instanced quads positioned by cell x/y/z
|
||
// ============================================================
|
||
|
||
struct Uniforms {
|
||
mvp : mat4x4<f32>,
|
||
cameraPos : vec3<f32>,
|
||
time : f32,
|
||
};
|
||
|
||
struct VertexOutput {
|
||
@builtin(position) position : vec4<f32>,
|
||
@location(0) color : vec4<f32>,
|
||
@location(1) uv : vec2<f32>,
|
||
};
|
||
|
||
@group(0) @binding(10) var<uniform> uniforms : Uniforms;
|
||
|
||
// Voltage mode color mapping
|
||
fn modeColor(mode : u32) -> vec3<f32> {
|
||
switch mode {
|
||
case 0u: { return vec3<f32>(1.0, 0.2, 0.2); } // STORE = red
|
||
case 1u: { return vec3<f32>(0.2, 1.0, 0.2); } // COMPUTE = green
|
||
case 2u: { return vec3<f32>(0.2, 0.2, 1.0); } // APPROX = blue
|
||
case 3u: { return vec3<f32>(1.0, 1.0, 1.0); } // MORPHIC = white
|
||
default: { return vec3<f32>(0.5, 0.5, 0.5); }
|
||
}
|
||
}
|
||
|
||
@vertex
|
||
fn vertexMain(@location(0) quadPos : vec2<f32>, // quad vertex [-0.5, 0.5]
|
||
@location(1) instancePos : vec3<f32>, // cell world position
|
||
@location(2) instanceMode : u32, // voltage_mode
|
||
@location(3) instanceDensity : f32) -> VertexOutput {
|
||
var out : VertexOutput;
|
||
|
||
// Billboard quad offset (screen-facing)
|
||
let scale = 0.4;
|
||
let offset = quadPos * scale;
|
||
|
||
// Position in world space
|
||
let worldPos = instancePos + vec3<f32>(offset.x, offset.y, 0.0);
|
||
|
||
out.position = uniforms.mvp * vec4<f32>(worldPos, 1.0);
|
||
let alpha = clamp(instanceDensity / 100.0, 0.05, 1.0);
|
||
out.color = vec4<f32>(modeColor(instanceMode), alpha);
|
||
out.uv = quadPos + 0.5;
|
||
|
||
return out;
|
||
}
|
||
|
||
@fragment
|
||
fn fragmentMain(in : VertexOutput) -> @location(0) vec4<f32> {
|
||
// Discard transparent pixels
|
||
if (in.color.a < 0.01) {
|
||
discard;
|
||
}
|
||
// Rounded quad with soft edge
|
||
let d = length(in.uv - 0.5) * 2.0;
|
||
let alpha = in.color.a * smoothstep(1.0, 0.8, d);
|
||
return vec4<f32>(in.color.rgb, alpha);
|
||
}
|