2257 lines
46 KiB
JavaScript
2257 lines
46 KiB
JavaScript
|
||
|
||
|
||
if( typeof document != "undefined" ) {
|
||
|
||
document.shaders = new Array();
|
||
|
||
document.bufferMap = {};
|
||
|
||
}
|
||
|
||
|
||
export default class Shader {
|
||
|
||
sampleCount = 1;
|
||
|
||
device;
|
||
|
||
path;
|
||
|
||
wgslSource;
|
||
|
||
bindGroupLayout;
|
||
|
||
bindGroupLayouts = new Map();
|
||
|
||
vertexBuffersDescriptors = new Map();
|
||
|
||
textures = new Map();
|
||
|
||
pipeline;
|
||
|
||
bindGroups = new Map();
|
||
|
||
entryPoint = "main";
|
||
|
||
bindings = new Array();
|
||
|
||
buffers = new Map();
|
||
|
||
bufferUsages = new Map();
|
||
|
||
bufferSizes = new Map();
|
||
|
||
attributeBuffers ;
|
||
|
||
bufferUsages = new Map(); // store usage per buffer to recreate correctly
|
||
|
||
_externalBindGroupLayout = null;
|
||
|
||
hasLoaded = false;
|
||
|
||
stage = GPUShaderStage.COMPUTE;
|
||
|
||
isInWorker = typeof document === "undefined";
|
||
|
||
topologyValue = "triangle-strip";
|
||
|
||
binded = false;
|
||
|
||
clearOnRender = true;
|
||
|
||
|
||
textures = new Map();
|
||
|
||
samplers = new Map();
|
||
|
||
|
||
|
||
constructor( device, path = false ) {
|
||
|
||
if( !this.isInWorker ) {
|
||
|
||
document.shaders.push( this );
|
||
|
||
}
|
||
|
||
console.log("\n\n \n Creating New Shader", path, "\n\n");
|
||
|
||
console.log("device", typeof device);
|
||
|
||
this.device = device;
|
||
|
||
if( path ) {
|
||
|
||
this.path = path;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
set topology(value) {
|
||
|
||
if (this.binded) {
|
||
|
||
console.error("Define topology before setup", this);
|
||
}
|
||
|
||
this.topologyValue = value;
|
||
|
||
}
|
||
|
||
get topology() {
|
||
|
||
return this.topologyValue;
|
||
|
||
}
|
||
_createAllBuffers() {
|
||
|
||
for (const b of this.bindings) {
|
||
|
||
if (this.buffers.has(b.varName)) continue;
|
||
|
||
let size;
|
||
|
||
if (b.type === "uniform") {
|
||
size = 256; // uniforms must be 256 bytes
|
||
}
|
||
else if (b.type === "read-only-storage" || b.type === "storage") {
|
||
size = 16; // minimum valid storage buffer size
|
||
}
|
||
else {
|
||
size = 16; // safe fallback
|
||
}
|
||
|
||
let usage;
|
||
|
||
switch (b.type) {
|
||
case "uniform":
|
||
usage = GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST;
|
||
break;
|
||
|
||
case "read-only-storage":
|
||
usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
|
||
break;
|
||
|
||
case "storage":
|
||
default:
|
||
usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
|
||
break;
|
||
}
|
||
|
||
const buffer = this.device.createBuffer({
|
||
size,
|
||
usage,
|
||
label: b.varName
|
||
});
|
||
|
||
this.buffers.set(b.varName, buffer);
|
||
this.bufferUsages.set(b.varName, usage);
|
||
this.bufferSizes.set(b.varName, size);
|
||
}
|
||
}
|
||
|
||
_parseStructs(wgsl) {
|
||
|
||
this.structs = {};
|
||
|
||
const structRegex =
|
||
/struct\s+(\w+)\s*\{([^}]+)\}/g;
|
||
|
||
let match;
|
||
|
||
while ((match = structRegex.exec(wgsl)) !== null) {
|
||
|
||
const structName = match[1];
|
||
const body = match[2].trim();
|
||
|
||
const fields = [];
|
||
|
||
const fieldRegex =
|
||
/(\w+)\s*:\s*([\w<>\[\]]+)\s*,?/g;
|
||
|
||
let f;
|
||
|
||
while ((f = fieldRegex.exec(body)) !== null) {
|
||
|
||
const fieldName = f[1];
|
||
const fieldType = f[2];
|
||
|
||
fields.push({
|
||
name: fieldName,
|
||
type: fieldType
|
||
});
|
||
}
|
||
|
||
this.structs[structName] = {
|
||
name: structName,
|
||
fields: fields,
|
||
size: 0 // computed later
|
||
};
|
||
}
|
||
|
||
for (const name of Object.keys(this.structs)) {
|
||
|
||
this._computeStructSize(name);
|
||
|
||
}
|
||
|
||
console.log("structs reflection created:", this.structs);
|
||
}
|
||
|
||
_computeStructSize(structName) {
|
||
|
||
const struct = this.structs[structName];
|
||
if (!struct) return 0;
|
||
|
||
let offset = 0;
|
||
|
||
for (const f of struct.fields) {
|
||
|
||
const align = 4;
|
||
const size = 4;
|
||
|
||
// align field offset
|
||
offset = Math.ceil(offset / align) * align;
|
||
|
||
f.offset = offset;
|
||
f.size = size;
|
||
f.align = align;
|
||
|
||
offset += size;
|
||
}
|
||
|
||
struct.size = offset;
|
||
|
||
return offset;
|
||
}
|
||
|
||
|
||
_computeTypeSize(wgslType) {
|
||
|
||
if (wgslType === "f32" || wgslType === "i32" || wgslType === "u32") {
|
||
return 4;
|
||
}
|
||
|
||
// vecN<T>
|
||
const vecMatch = wgslType.match(/vec(\d+)<(\w+)>/);
|
||
if (vecMatch) {
|
||
const n = Number(vecMatch[1]);
|
||
const elemType = vecMatch[2];
|
||
return n * this._computeTypeSize(elemType);
|
||
}
|
||
|
||
// matCxR<T> (e.g. mat4x4<f32>)
|
||
const matMatch = wgslType.match(/mat(\d+)x(\d+)<(\w+)>/);
|
||
if (matMatch) {
|
||
const c = Number(matMatch[1]);
|
||
const r = Number(matMatch[2]);
|
||
const elemType = matMatch[3];
|
||
return c * r * this._computeTypeSize(elemType);
|
||
}
|
||
|
||
// array<T, N>
|
||
const arrFix = wgslType.match(/array<(\w+),\s*(\d+)>/);
|
||
if (arrFix) {
|
||
const element = arrFix[1];
|
||
const count = Number(arrFix[2]);
|
||
return this._computeTypeSize(element) * count;
|
||
}
|
||
|
||
// array<T> runtime sized
|
||
const arrDyn = wgslType.match(/array<(\w+)>/);
|
||
if (arrDyn) {
|
||
return 4;
|
||
}
|
||
|
||
// struct
|
||
if (this.structs[wgslType]) {
|
||
return this.structs[wgslType].size;
|
||
}
|
||
|
||
return 4;
|
||
}
|
||
|
||
_computeBindingSizes() {
|
||
|
||
for (const b of this.bindings) {
|
||
|
||
const t = b.varType;
|
||
|
||
// -----------------------------------------
|
||
// 1. Struct?
|
||
// -----------------------------------------
|
||
if (this.structs[t]) {
|
||
b.size = this.structs[t].size;
|
||
continue;
|
||
}
|
||
|
||
// -----------------------------------------
|
||
// 4. Texture / sampler (no buffer)
|
||
// -----------------------------------------
|
||
if (b.type === "texture" || b.type === "sampler") {
|
||
b.size = 0;
|
||
continue;
|
||
}
|
||
|
||
// -----------------------------------------
|
||
// 5. Use computed WGSL type size (scalars, vectors, matrices, arrays)
|
||
// -----------------------------------------
|
||
b.size = this._computeTypeSize(t);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
_createAllBuffers() {
|
||
|
||
for (const b of this.bindings) {
|
||
|
||
if (this.buffers.has(b.varName))
|
||
continue;
|
||
|
||
let size = b.size;
|
||
if (size < 4) size = 4;
|
||
|
||
let usage;
|
||
switch (b.type) {
|
||
case "uniform":
|
||
usage = GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST;
|
||
break;
|
||
case "read-only-storage":
|
||
usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
|
||
break;
|
||
default:
|
||
usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
|
||
}
|
||
|
||
const buffer = this.device.createBuffer({
|
||
size,
|
||
usage,
|
||
label: b.varName
|
||
});
|
||
|
||
this.buffers.set(b.varName, buffer);
|
||
this.bufferSizes.set(b.varName, size);
|
||
this.bufferUsages.set(b.varName, usage);
|
||
}
|
||
}
|
||
|
||
|
||
_createDefaultTexturesAndSamplers() {
|
||
|
||
for ( const b of this.bindings ) {
|
||
|
||
if ( b.type === "texture" && !this.textures.has( b.varName ) ) {
|
||
|
||
// Detect if the type is a texture array
|
||
const isTextureArray = b.varType.startsWith("texture_2d_array");
|
||
|
||
// Create 1x1 texture with 1 layer (depthOrArrayLayers = 1)
|
||
const defaultTexture = this.device.createTexture({
|
||
|
||
size: isTextureArray ? [1, 1, 2] : [1, 1, 1],
|
||
|
||
format: "rgba8unorm",
|
||
|
||
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
||
|
||
label: b.varName + "_defaultTexture"
|
||
|
||
});
|
||
|
||
|
||
console.log("create texture ", isTextureArray, isTextureArray ? "2d-array" : "2d");
|
||
// Create texture view with correct dimension
|
||
const defaultTextureView = defaultTexture.createView({
|
||
|
||
dimension: isTextureArray ? "2d-array" : "2d"
|
||
|
||
});
|
||
|
||
console.log("create default texture?", b, defaultTextureView, isTextureArray ? "2d-array" : "2d");
|
||
|
||
this.textures.set(b.varName, defaultTextureView);
|
||
|
||
} else if ( b.type === "sampler" && !this.samplers.has( b.varName ) ) {
|
||
|
||
// Create default sampler
|
||
|
||
const defaultSampler = this.device.createSampler( {
|
||
|
||
magFilter: "linear",
|
||
|
||
minFilter: "linear",
|
||
|
||
label: b.varName + "_defaultSampler"
|
||
|
||
} );
|
||
|
||
this.samplers.set( b.varName, defaultSampler );
|
||
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
createBindGroups() {
|
||
|
||
this.bindGroups = new Map();
|
||
|
||
for ( const [ group, layout ] of this.bindGroupLayouts.entries() ) {
|
||
|
||
const bindings = this.bindings.filter( b => b.group === group );
|
||
|
||
const entries = new Array( bindings.length );
|
||
|
||
for ( let i = 0; i < bindings.length; i++ ) {
|
||
|
||
const b = bindings[ i ];
|
||
|
||
let resource;
|
||
|
||
if ( b.type === "uniform" || b.type === "read-only-storage" || b.type === "storage" ) {
|
||
|
||
const gpuBuffer = this.buffers.get( b.varName );
|
||
|
||
if ( !gpuBuffer ) {
|
||
|
||
throw new Error( `Buffer missing for ${b.varName} when creating bind group.` );
|
||
|
||
}
|
||
|
||
resource = { buffer: gpuBuffer };
|
||
|
||
} else if ( b.type === "texture" ) {
|
||
|
||
const gpuTextureView = this.textures.get( b.varName );
|
||
|
||
//console.log("this.textures", this.textures);
|
||
|
||
if ( !gpuTextureView ) {
|
||
|
||
console.warn( `Texture missing for ${b.varName} when creating bind group.` );
|
||
|
||
} else {
|
||
|
||
//console.log( `Binding texture ${b.varName} with dimension:`, gpuTextureView.dimension );
|
||
|
||
}
|
||
|
||
resource = gpuTextureView;
|
||
|
||
} else if ( b.type === "sampler" ) {
|
||
|
||
const gpuSampler = this.samplers.get( b.varName );
|
||
|
||
if ( !gpuSampler ) {
|
||
|
||
//throw new Error( `Sampler missing for ${b.varName} when creating bind group.` );
|
||
|
||
}
|
||
|
||
resource = gpuSampler;
|
||
|
||
} else {
|
||
|
||
throw new Error( `Unknown binding type '${b.type}' for ${b.varName}` );
|
||
|
||
}
|
||
|
||
entries[ i ] = {
|
||
|
||
binding: b.binding,
|
||
|
||
resource: resource
|
||
|
||
};
|
||
|
||
}
|
||
|
||
const bindGroup = this.device.createBindGroup( {
|
||
|
||
layout: layout,
|
||
|
||
entries: entries
|
||
|
||
} );
|
||
|
||
this.bindGroups.set( group, bindGroup );
|
||
|
||
}
|
||
|
||
}
|
||
extractEntryPoints( wgslCode ) {
|
||
const entryPoints = [];
|
||
|
||
// Regex to match lines like: @vertex fn functionName(...) { ... }
|
||
const regex =
|
||
/@(\w+)(?:\s*@[\w_]+(?:\([^)]*\))?)*\s*fn\s+([\w_]+)\s*\(/gm;
|
||
|
||
let match;
|
||
|
||
while ((match = regex.exec(wgslCode)) !== null) {
|
||
|
||
|
||
const stage = match[1];
|
||
const name = match[2];
|
||
|
||
let type;
|
||
|
||
switch (stage) {
|
||
case "vertex":
|
||
type = GPUShaderStage.VERTEX;
|
||
break;
|
||
case "fragment":
|
||
type = GPUShaderStage.FRAGMENT;
|
||
break;
|
||
case "compute":
|
||
type = GPUShaderStage.COMPUTE;
|
||
break;
|
||
default:
|
||
type = "Unknown";
|
||
}
|
||
|
||
entryPoints.push({ name: name, type: type });
|
||
}
|
||
|
||
return entryPoints;
|
||
|
||
}
|
||
|
||
async setup( path ) {
|
||
|
||
|
||
|
||
this.path = path;
|
||
|
||
this.wgslSource = await this._loadShaderSource( this.path );
|
||
|
||
// 2. Parse metadata from WGSL
|
||
this._parseVertexAttributes(this.wgslSource);
|
||
this._parseBindings(this.wgslSource);
|
||
this._parseStructs(this.wgslSource);
|
||
|
||
// 3. Compute struct sizes BEFORE creating buffers
|
||
this._computeBindingSizes(); // if you implemented this
|
||
|
||
// ----------------------------------------------------------
|
||
// 4. CREATE BUFFERS BEFORE EVERYTHING ELSE
|
||
// ----------------------------------------------------------
|
||
this._createAllBuffers();
|
||
|
||
|
||
//console.log(this.wgslSource);
|
||
var entryPoints = this.extractEntryPoints( this.wgslSource );
|
||
console.log(entryPoints);
|
||
|
||
|
||
for (var i = 0; i < entryPoints.length; i++) {
|
||
|
||
var entryPoint = entryPoints[i];
|
||
|
||
await this.addStage( entryPoint.name, entryPoint.type );
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
//await this.addStage("computeMain", 4 );
|
||
|
||
//console.log( "load shader", this.wgslSource, entryPoints )
|
||
|
||
}
|
||
|
||
async _loadShaderSource( pathName ) {
|
||
|
||
const response = await fetch( pathName );
|
||
|
||
if ( !response.ok ) throw new Error( `Failed to load shader: ${ pathName }` );
|
||
|
||
return await response.text();
|
||
|
||
}
|
||
|
||
setVertexBuffer( name, dataArray ) {
|
||
|
||
const buffer = this.device.createBuffer({
|
||
size: dataArray.byteLength,
|
||
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
||
mappedAtCreation: true
|
||
});
|
||
|
||
const mapping = new Float32Array( buffer.getMappedRange() );
|
||
|
||
mapping.set( dataArray );
|
||
|
||
buffer.unmap();
|
||
|
||
|
||
|
||
this.buffers.set( name, buffer );
|
||
|
||
}
|
||
|
||
extractVertexFunctionParameters(wgsl) {
|
||
|
||
let vertexFnIndex = wgsl.indexOf('@vertex');
|
||
|
||
if (vertexFnIndex === -1) return '';
|
||
|
||
let fnIndex = wgsl.indexOf('fn', vertexFnIndex);
|
||
|
||
if (fnIndex === -1) return '';
|
||
|
||
let parenStart = wgsl.indexOf('(', fnIndex);
|
||
|
||
if (parenStart === -1) return '';
|
||
|
||
let parenCount = 1;
|
||
let i = parenStart + 1;
|
||
|
||
while (i < wgsl.length && parenCount > 0) {
|
||
const c = wgsl[i];
|
||
|
||
if (c === '(') parenCount++;
|
||
|
||
else if (c === ')') parenCount--;
|
||
|
||
i++;
|
||
}
|
||
|
||
if (parenCount !== 0) {
|
||
// unbalanced parentheses
|
||
return '';
|
||
}
|
||
|
||
// Extract parameter substring between parenStart+1 and i-1
|
||
let paramsStr = wgsl.substring(parenStart + 1, i - 1);
|
||
|
||
return paramsStr.trim();
|
||
}
|
||
|
||
extractVertexParamsLocations(wgsl) {
|
||
|
||
const paramsStr = this.extractVertexFunctionParameters(wgsl)
|
||
|
||
const paramRegex = /@location\s*\(\s*(\d+)\s*\)\s+(\w+)\s*:\s*([\w<>\d]+(\s*<[^>]+>)?)/g;
|
||
|
||
const results = new Array();
|
||
|
||
let paramMatch;
|
||
|
||
while ((paramMatch = paramRegex.exec(paramsStr)) !== null) {
|
||
|
||
const location = Number(paramMatch[1]);
|
||
|
||
const varName = paramMatch[2];
|
||
|
||
const type = paramMatch[3].trim();
|
||
|
||
results.push({ location, varName, type });
|
||
}
|
||
|
||
|
||
|
||
return results;
|
||
}
|
||
|
||
getFormatSize( format ) {
|
||
|
||
switch ( format ) {
|
||
|
||
case "float32": return 4;
|
||
|
||
case "float32x2": return 8;
|
||
|
||
case "float32x3": return 12;
|
||
|
||
case "float32x4": return 16;
|
||
|
||
case "uint32": return 4;
|
||
|
||
case "sint32": return 4;
|
||
|
||
default: throw new Error("Unsupported format size for: " + format);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
mapTypeToVertexFormat( type ) {
|
||
|
||
// Map WGSL types to GPUVertexFormat strings (example subset)
|
||
|
||
switch ( type ) {
|
||
|
||
case "f32": return "float32";
|
||
|
||
case "vec2<f32>": return "float32x2";
|
||
|
||
case "vec3<f32>": return "float32x3";
|
||
|
||
case "vec4<f32>": return "float32x4";
|
||
|
||
case "u32": return "uint32";
|
||
|
||
case "i32": return "sint32";
|
||
|
||
default: throw new Error("Unsupported vertex attribute type: " + type);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
_parseVertexAttributes( wgsl ) {
|
||
|
||
const locations = this.extractVertexParamsLocations( wgsl );
|
||
|
||
|
||
if (locations.length === 0) {
|
||
// No vertex attributes = no vertex buffer descriptors needed
|
||
return;
|
||
}
|
||
|
||
let offset = 0;
|
||
|
||
const attributes = new Array();
|
||
|
||
for ( let attr of locations ) {
|
||
|
||
const format = this.mapTypeToVertexFormat( attr.type );
|
||
|
||
attributes.push({
|
||
shaderLocation : attr.location,
|
||
offset : offset,
|
||
format : format
|
||
});
|
||
|
||
offset += this.getFormatSize( format );
|
||
|
||
}
|
||
|
||
const vertexBufferDescriptor = {
|
||
arrayStride: offset,
|
||
attributes: attributes,
|
||
stepMode: "vertex"
|
||
};
|
||
|
||
console.log("vertexBufferDescriptor", vertexBufferDescriptor);
|
||
|
||
|
||
this.vertexBuffersDescriptors.set("vertexBuffer0", vertexBufferDescriptor);
|
||
|
||
console.log("vertex buffer descriptors updated:", this.vertexBuffersDescriptors);
|
||
|
||
}
|
||
|
||
_parseBindings( wgsl ) {
|
||
|
||
console.log("parse bindings");
|
||
|
||
this.bindings = [];
|
||
|
||
// Make the `<...>` part optional by wrapping it in a non-capturing group with `?`
|
||
const regex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var(?:<(\w+)(?:,\s*(\w+))?>)?\s+(\w+)\s*:\s*([^;]+);/g;
|
||
|
||
let match;
|
||
|
||
while ((match = regex.exec(wgsl)) !== null) {
|
||
|
||
const group = Number(match[1]);
|
||
|
||
const binding = Number(match[2]);
|
||
|
||
const storageClass = match[3]; // e.g. "uniform", "storage", or undefined for textures/samplers
|
||
|
||
const access = match[4]; // e.g. "read", "write", or undefined
|
||
|
||
const varName = match[5];
|
||
|
||
const varType = match[6]; // WGSL type string like "mat4x4<f32>", "texture_2d<f32>", "sampler", etc.
|
||
|
||
let type;
|
||
|
||
if (storageClass === "storage") {
|
||
|
||
type = access === "read" ? "read-only-storage" : "storage";
|
||
|
||
} else if (storageClass === "uniform") {
|
||
|
||
type = "uniform";
|
||
|
||
} else if (varType.startsWith("texture_")) {
|
||
|
||
type = "texture";
|
||
|
||
} else if (varType === "sampler" || varType === "sampler_comparison") {
|
||
|
||
type = "sampler";
|
||
|
||
} else {
|
||
|
||
type = "uniform"; // fallback
|
||
|
||
}
|
||
|
||
this.bindings.push({ group, binding, type, varName, varType });
|
||
|
||
}
|
||
|
||
console.log("bindings", this.bindings);
|
||
|
||
this.bindings.sort((a, b) => a.binding - b.binding);
|
||
|
||
}
|
||
|
||
_createBindGroupLayoutsFromBindings() {
|
||
|
||
const groups = new Map();
|
||
|
||
for ( let i = 0; i < this.bindings.length; i++ ) {
|
||
|
||
const b = this.bindings[ i ];
|
||
|
||
if ( !groups.has( b.group ) ) groups.set( b.group, [] );
|
||
|
||
groups.get( b.group ).push( b );
|
||
|
||
}
|
||
|
||
const layouts = new Map();
|
||
|
||
for ( const groupEntry of groups.entries() ) {
|
||
|
||
const group = groupEntry[ 0 ];
|
||
|
||
const bindings = groupEntry[ 1 ];
|
||
|
||
let visibility;
|
||
|
||
if ( this.stage === GPUShaderStage.COMPUTE ) {
|
||
|
||
visibility = GPUShaderStage.COMPUTE;
|
||
|
||
} else {
|
||
|
||
visibility = GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT;
|
||
|
||
}
|
||
|
||
const entries = new Array( bindings.length );
|
||
|
||
for ( let j = 0; j < bindings.length; j++ ) {
|
||
|
||
const b = bindings[ j ];
|
||
|
||
let entry;
|
||
|
||
if ( b.type === "uniform" || b.type === "read-only-storage" || b.type === "storage" ) {
|
||
|
||
entry = {
|
||
|
||
binding: b.binding,
|
||
|
||
visibility: visibility,
|
||
|
||
buffer: { type: b.type }
|
||
|
||
};
|
||
|
||
} else if ( b.type === "texture" ) {
|
||
|
||
|
||
let viewDimension = "2d"; // default
|
||
|
||
if ( b.varType.startsWith("texture_2d_array") ) {
|
||
|
||
viewDimension = "2d-array";
|
||
|
||
} else if ( b.varType.startsWith("texture_cube") ) {
|
||
|
||
viewDimension = "cube";
|
||
|
||
} else if ( b.varType.startsWith("texture_3d") ) {
|
||
|
||
viewDimension = "3d";
|
||
|
||
}
|
||
|
||
console.log("viewDimension", viewDimension);
|
||
|
||
entry = {
|
||
binding: b.binding,
|
||
visibility: visibility,
|
||
texture: {
|
||
sampleType: "float",
|
||
viewDimension: viewDimension,
|
||
multisampled: false
|
||
}
|
||
};
|
||
|
||
} else if ( b.type === "sampler" ) {
|
||
|
||
entry = {
|
||
|
||
binding: b.binding,
|
||
|
||
visibility: visibility,
|
||
|
||
sampler: { type: "filtering" }
|
||
|
||
};
|
||
|
||
} else {
|
||
|
||
entry = {
|
||
|
||
binding: b.binding,
|
||
|
||
visibility: visibility,
|
||
|
||
buffer: { type: "uniform" }
|
||
|
||
};
|
||
|
||
}
|
||
|
||
entries[ j ] = entry;
|
||
|
||
}
|
||
|
||
console.log("createBindGroupLayout ", entries);
|
||
|
||
const layout = this.device.createBindGroupLayout( { entries } );
|
||
|
||
layouts.set( group, layout );
|
||
|
||
}
|
||
|
||
return layouts;
|
||
|
||
}
|
||
|
||
async _createPipeline() {
|
||
|
||
const layoutsArray = this.bindGroupLayouts && this.bindGroupLayouts.size > 0
|
||
|
||
? Array.from( this.bindGroupLayouts.values() )
|
||
|
||
: [ this.bindGroupLayout ];
|
||
|
||
const shaderModule = this.device.createShaderModule( {
|
||
code: this.wgslSource,
|
||
|
||
label: this.path,
|
||
|
||
} );
|
||
|
||
const info = await shaderModule.compilationInfo();
|
||
|
||
if (info.messages.length > 0) {
|
||
for (const msg of info.messages) {
|
||
console[msg.type === "error" ? "error" : "warn"](
|
||
`[${msg.lineNum}:${msg.linePos}] ${msg.message}`
|
||
);
|
||
}
|
||
}
|
||
|
||
//console.log("this.bindGroupLayouts.values()", this.bindGroupLayouts.values());
|
||
|
||
this.pipeline = this.device.createComputePipeline({
|
||
|
||
layout: this.device.createPipelineLayout({
|
||
|
||
bindGroupLayouts: Array.from( this.bindGroupLayouts.values() )
|
||
|
||
}),
|
||
|
||
compute: {
|
||
|
||
module: shaderModule,
|
||
|
||
entryPoint: this.entryPoint,
|
||
|
||
},
|
||
|
||
} );
|
||
|
||
}
|
||
|
||
setBindingGroupLayout( bindGroupLayout ) {
|
||
|
||
this._externalBindGroupLayout = bindGroupLayout;
|
||
|
||
}
|
||
|
||
getBindingGroupLayout( bindingGroupIndex = 0 ) {
|
||
|
||
if ( this.bindGroupLayouts ) {
|
||
|
||
return this.bindGroupLayouts.get( bindingGroupIndex );
|
||
|
||
}
|
||
|
||
return this.bindGroupLayout;
|
||
|
||
}
|
||
|
||
getTypedArrayByVariableName( name, flatData ) {
|
||
|
||
let typedArray;
|
||
|
||
const bindingInfo = this.bindings.find( b => b.varName === name );
|
||
|
||
if ( bindingInfo ) {
|
||
|
||
const wgslType = bindingInfo.varType;
|
||
|
||
switch( wgslType ) {
|
||
|
||
case "vec3<f32>":
|
||
|
||
// flatData.push( 0 );
|
||
|
||
break;
|
||
|
||
}
|
||
|
||
switch ( wgslType ) {
|
||
|
||
case "f32":
|
||
case "vec2<f32>":
|
||
case "vec3<f32>":
|
||
case "vec4<f32>":
|
||
case "array<f32>":
|
||
|
||
typedArray = new Float32Array( flatData );
|
||
|
||
break;
|
||
|
||
case "u32":
|
||
case "vec2<u32>":
|
||
case "vec3<u32>":
|
||
case "vec4<u32>":
|
||
case "array<u32>":
|
||
|
||
typedArray = new Uint32Array( flatData );
|
||
|
||
break;
|
||
|
||
case "i32":
|
||
case "vec2<i32>":
|
||
case "vec3<i32>":
|
||
case "vec4<i32>":
|
||
case "array<i32>":
|
||
|
||
typedArray = new Int32Array( flatData );
|
||
|
||
break;
|
||
|
||
default:
|
||
|
||
// find struct and then the datatype
|
||
typedArray = new Float32Array( flatData );
|
||
//console.error( "Unknown WGSL type in setVariable: " + wgslType );
|
||
}
|
||
|
||
//console.log("wgslType", wgslType);
|
||
|
||
}
|
||
|
||
return typedArray;
|
||
|
||
}
|
||
|
||
createTextureFromData(width, height, data = null) {
|
||
|
||
// 1. Infer format based on TypedArray
|
||
let format;
|
||
let bytesPerComponent;
|
||
|
||
if (data instanceof Float32Array) {
|
||
format = "rgba32float";
|
||
bytesPerComponent = 4;
|
||
}
|
||
else if (data instanceof Uint8Array) {
|
||
format = "rgba8unorm";
|
||
bytesPerComponent = 1;
|
||
}
|
||
else if (data instanceof Uint32Array) {
|
||
format = "rgba32uint";
|
||
bytesPerComponent = 4;
|
||
}
|
||
else if (data instanceof Int32Array) {
|
||
format = "rgba32sint";
|
||
bytesPerComponent = 4;
|
||
}
|
||
else {
|
||
throw new Error("Unsupported texture data type");
|
||
}
|
||
|
||
// 2. Compute expected size
|
||
const channels = 4;
|
||
const expected = width * height * channels;
|
||
|
||
if (data && data.length !== expected) {
|
||
throw new Error(
|
||
`Texture data length mismatch. Expected ${expected}, got ${data.length}`
|
||
);
|
||
}
|
||
|
||
// 3. Create texture
|
||
const texture = this.device.createTexture({
|
||
size: { width, height, depthOrArrayLayers: 1 },
|
||
format,
|
||
usage:
|
||
GPUTextureUsage.TEXTURE_BINDING |
|
||
GPUTextureUsage.STORAGE_BINDING |
|
||
GPUTextureUsage.COPY_DST
|
||
});
|
||
|
||
// 4. Upload if data provided
|
||
if (data) {
|
||
|
||
this.device.queue.writeTexture(
|
||
{
|
||
texture: texture
|
||
},
|
||
data,
|
||
{
|
||
bytesPerRow: width * channels * bytesPerComponent
|
||
},
|
||
{
|
||
width,
|
||
height,
|
||
depthOrArrayLayers: 1
|
||
}
|
||
);
|
||
}
|
||
|
||
return texture;
|
||
}
|
||
|
||
|
||
setVariable( name, dataObject ) {
|
||
|
||
|
||
if (dataObject instanceof GPUBuffer) {
|
||
// Direct buffer binding – do NOT recreate, resize, or write.
|
||
this.setBuffer( name, dataObject );
|
||
return;
|
||
}
|
||
|
||
|
||
// TEXTURES -----------------------------------------------------
|
||
if (dataObject instanceof GPUTexture) {
|
||
this.textures.set(name, dataObject.createView());
|
||
this._invalidateBindGroups();
|
||
return;
|
||
}
|
||
|
||
// SAMPLERS -----------------------------------------------------
|
||
if (dataObject instanceof GPUSampler) {
|
||
this.samplers.set(name, dataObject);
|
||
this._invalidateBindGroups();
|
||
return;
|
||
}
|
||
|
||
// STRUCT CHECK -------------------------------------------------
|
||
const binding = this.bindings.find(b => b.varName === name);
|
||
const structInfo = binding ? this.structs[binding.varType] : undefined;
|
||
|
||
let typed;
|
||
|
||
// --------------------------------------------------------------
|
||
// 1. WGSL STRUCT → Uint32Array (using struct reflection)
|
||
// --------------------------------------------------------------
|
||
if (structInfo) {
|
||
|
||
const out = new Uint32Array(structInfo.size / 4); // aligned struct size in bytes
|
||
|
||
for (let i = 0; i < structInfo.fields.length; i++) {
|
||
|
||
const f = structInfo.fields[i];
|
||
let v = dataObject[f.name];
|
||
|
||
if (v === undefined) {
|
||
v = 0;
|
||
}
|
||
|
||
// only scalar u32/f32 fields for now
|
||
out[f.offset / 4] = v >>> 0;
|
||
}
|
||
|
||
typed = out;
|
||
}
|
||
|
||
// --------------------------------------------------------------
|
||
// 2. Already typed arrays
|
||
// --------------------------------------------------------------
|
||
else if (
|
||
dataObject instanceof Float32Array ||
|
||
dataObject instanceof Uint32Array ||
|
||
dataObject instanceof Int32Array
|
||
) {
|
||
typed = dataObject;
|
||
}
|
||
|
||
// --------------------------------------------------------------
|
||
// 3. Number or JS array → detect type
|
||
// --------------------------------------------------------------
|
||
else if (typeof dataObject === "number" || Array.isArray(dataObject)) {
|
||
const flat = this.flattenStructure(dataObject);
|
||
typed = this.getTypedArrayByVariableName(name, flat);
|
||
}
|
||
|
||
// --------------------------------------------------------------
|
||
// 4. Raw ArrayBuffer
|
||
// --------------------------------------------------------------
|
||
else if (dataObject instanceof ArrayBuffer) {
|
||
typed = new Float32Array(dataObject);
|
||
}
|
||
|
||
// --------------------------------------------------------------
|
||
// 5. Fallback
|
||
// --------------------------------------------------------------
|
||
else {
|
||
const flat = this.flattenStructure(dataObject);
|
||
typed = new Float32Array(flat);
|
||
}
|
||
|
||
// BUFFER CREATION / RESIZE -------------------------------------
|
||
const bytes = typed.byteLength;
|
||
const oldBuffer = this.buffers.get(name);
|
||
const oldUsage = this.bufferUsages.get(name);
|
||
const oldSize = oldBuffer ? oldBuffer.size : 0;
|
||
|
||
if (typeof oldUsage !== "number") {
|
||
throw new Error("Invalid buffer usage for '" + name + "'");
|
||
}
|
||
|
||
let buffer = oldBuffer;
|
||
|
||
// Resize if needed
|
||
if (!buffer || oldSize !== bytes) {
|
||
|
||
buffer = this.device.createBuffer({
|
||
label: name,
|
||
size: bytes,
|
||
usage: oldUsage,
|
||
mappedAtCreation: false
|
||
});
|
||
|
||
this.buffers.set(name, buffer);
|
||
this.bufferSizes.set(name, bytes);
|
||
|
||
this._invalidateBindGroups(); // <— IMPORTANT FIX
|
||
}
|
||
|
||
// BUFFER UPLOAD -------------------------------------------------
|
||
this.device.queue.writeBuffer(
|
||
buffer,
|
||
0,
|
||
typed.buffer,
|
||
typed.byteOffset,
|
||
typed.byteLength
|
||
);
|
||
}
|
||
|
||
|
||
_invalidateBindGroups() {
|
||
this.bindGroups = false;
|
||
}
|
||
|
||
|
||
|
||
computeUsage(name) {
|
||
|
||
const binding = this.bindings.find(b => b.varName === name);
|
||
|
||
if (!binding) {
|
||
// No binding found → use safe fallback
|
||
return GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
|
||
}
|
||
|
||
switch (binding.type) {
|
||
|
||
case "uniform":
|
||
return GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST;
|
||
|
||
case "read-only-storage":
|
||
return GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST ;
|
||
|
||
case "storage":
|
||
return GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
|
||
|
||
default:
|
||
return GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
|
||
}
|
||
}
|
||
|
||
|
||
flattenStructure( input, output = [] ) {
|
||
|
||
if ( Array.isArray( input ) ) {
|
||
|
||
for ( const element of input ) {
|
||
|
||
this.flattenStructure( element, output );
|
||
|
||
}
|
||
|
||
} else if ( typeof input === "object" && input !== null ) {
|
||
|
||
for ( const key of Object.keys( input ) ) {
|
||
|
||
this.flattenStructure( input[ key ], output );
|
||
|
||
}
|
||
|
||
} else if ( typeof input === "number" ) {
|
||
|
||
output.push( input );
|
||
|
||
} else if ( typeof input === "boolean" ) {
|
||
|
||
output.push( input ? 1 : 0 );
|
||
|
||
} else {
|
||
|
||
throw new Error( "Unsupported data type in shader variable: " + typeof input );
|
||
|
||
}
|
||
|
||
return output;
|
||
|
||
}
|
||
|
||
copyBuffersFromShader( sourceShader ) {
|
||
|
||
for ( const [ name, buffer ] of sourceShader.buffers.entries() ) {
|
||
|
||
this.buffers.set( name, buffer );
|
||
|
||
}
|
||
|
||
}
|
||
|
||
updateUniforms( data ) {
|
||
const arrayBuffer = new ArrayBuffer(12); // 3 x u32 = 3 * 4 bytes
|
||
const dataView = new DataView(arrayBuffer);
|
||
|
||
dataView.setUint32(0, data.k, true); // offset 0
|
||
dataView.setUint32(4, data.j, true); // offset 4
|
||
dataView.setUint32(8, data.totalCount, true); // offset 8
|
||
|
||
this.device.queue.writeBuffer(
|
||
this.uniformBuffer, // your GPUBuffer holding uniforms
|
||
0, // offset in buffer
|
||
arrayBuffer
|
||
);
|
||
}
|
||
|
||
createCommandEncoder( x, y, z ) {
|
||
|
||
// AUTO-REBUILD BIND GROUPS IF NEEDED
|
||
if (!this.bindGroups || this.bindGroups.size === 0) {
|
||
this.createBindGroups();
|
||
}
|
||
|
||
|
||
const commandEncoder = this.device.createCommandEncoder({
|
||
label: this.path
|
||
});
|
||
|
||
const passEncoder = commandEncoder.beginComputePass({
|
||
label: this.path
|
||
});
|
||
|
||
|
||
if( !this.pipeline ) {
|
||
|
||
console.log("Pipeline missing, Maybe you forget to call addStage( ) ", this);
|
||
|
||
}
|
||
|
||
passEncoder.setPipeline( this.pipeline );
|
||
|
||
for ( const [bindingGroupIndex, bindGroup] of this.bindGroups.entries() ) {
|
||
|
||
passEncoder.setBindGroup( bindingGroupIndex, bindGroup );
|
||
|
||
}
|
||
|
||
passEncoder.dispatchWorkgroups( x, y, z );
|
||
|
||
passEncoder.end();
|
||
|
||
return commandEncoder;
|
||
|
||
}
|
||
|
||
async execute( x = 1, y = 1, z = 1 ) {
|
||
|
||
const commandEncoder = this.createCommandEncoder( x, y, z );
|
||
|
||
const commandBuffer = commandEncoder.finish();
|
||
|
||
this.device.queue.submit( [ commandBuffer ] );
|
||
|
||
await this.device.queue.onSubmittedWorkDone(); // critical!
|
||
|
||
}
|
||
|
||
registerBuffer( buffer, name, type ) {
|
||
|
||
if( !this.isInWorker ) {
|
||
|
||
if( !document.bufferMap[name] ) {
|
||
|
||
document.bufferMap[name] = new Array();
|
||
|
||
}
|
||
|
||
var bufferHistoryPoint = { buffer: buffer, shader: this, label : buffer.label, type: type };
|
||
|
||
document.bufferMap[name].push( bufferHistoryPoint );
|
||
|
||
}
|
||
|
||
}
|
||
|
||
getBuffer( name ) {
|
||
|
||
|
||
// If no bind groups yet, create them now
|
||
if (!this.bindGroups || this.bindGroups.size === 0) {
|
||
this.createBindGroups();
|
||
}
|
||
|
||
var buffer = this.buffers.get( name );
|
||
|
||
this.registerBuffer( buffer, name, "get" );
|
||
|
||
return buffer;
|
||
|
||
}
|
||
|
||
async setBuffer( name, buffer ) {
|
||
|
||
this.registerBuffer( buffer, name, "set" );
|
||
|
||
if( !this.isInWorker ) {
|
||
|
||
//var bufferOwner = document.bufferOwners.get( buffer );
|
||
|
||
//console.log("setBuffer Origin:", bufferOwner, " this shader", this);
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
const bindingInfo = this.bindings.find( b => b.varName === name );
|
||
|
||
if( !bindingInfo ) {
|
||
|
||
console.error("No binding to buffer ", name, " in shader ", this );
|
||
|
||
}
|
||
|
||
//console.log("set buffer", name, buffer);
|
||
|
||
this.buffers.set(name, buffer);
|
||
|
||
await this.createBindGroups(); // always refresh bindings
|
||
|
||
}
|
||
|
||
|
||
|
||
async addStage( entryPoint, stage ) {
|
||
|
||
if ( !this.path ) {
|
||
|
||
throw new Error("Shader path is not set");
|
||
|
||
}
|
||
this.entryPoint = entryPoint;
|
||
|
||
if( stage != GPUShaderStage.COMPUTE ) {
|
||
|
||
this.stage = GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT;
|
||
|
||
}
|
||
|
||
|
||
if ( !this.binded ) {
|
||
|
||
if( stage != GPUShaderStage.COMPUTE ) {
|
||
|
||
if( this.sampleCount == 1 ) {
|
||
|
||
this.depthTexture = this.device.createTexture({
|
||
size: {
|
||
width: this.canvas.width,
|
||
height: this.canvas.height,
|
||
depthOrArrayLayers: 1
|
||
},
|
||
sampleCount: 1, // Must be 1 for non-multisampled rendering
|
||
format: "depth24plus", // Or "depth24plus-stencil8" if stencil is needed
|
||
usage: GPUTextureUsage.RENDER_ATTACHMENT
|
||
});
|
||
|
||
} else {
|
||
|
||
this.multisampledColorTexture = this.device.createTexture({
|
||
size: [this.canvas.width, this.canvas.height],
|
||
sampleCount: this.sampleCount,
|
||
format: "bgra8unorm",
|
||
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
||
});
|
||
|
||
this.multisampledDepthTexture = this.device.createTexture({
|
||
size: [this.canvas.width, this.canvas.height],
|
||
sampleCount: this.sampleCount, // must match multisample count of color texture and pipeline
|
||
format: "depth24plus", // or "depth24plus-stencil8" if you need stencil
|
||
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
||
});
|
||
|
||
}
|
||
}
|
||
|
||
this.wgslSource = await this._loadShaderSource( this.path );
|
||
|
||
//this._parseVertexAttributes( this.wgslSource );
|
||
|
||
//this._parseBindings( this.wgslSource );
|
||
|
||
//this._parseStructs( this.wgslSource );
|
||
|
||
if ( this._externalBindGroupLayout ) {
|
||
|
||
this.bindGroupLayouts.set( 0, this._externalBindGroupLayout );
|
||
|
||
} else {
|
||
|
||
this.bindGroupLayouts = this._createBindGroupLayoutsFromBindings();
|
||
|
||
}
|
||
|
||
|
||
this._createDefaultTexturesAndSamplers();
|
||
|
||
await this.createBindGroups();
|
||
|
||
this.binded = true;
|
||
|
||
}
|
||
|
||
const shaderModule = this.device.createShaderModule( {
|
||
|
||
code: this.wgslSource,
|
||
|
||
label: this.path
|
||
|
||
} );
|
||
|
||
if ( stage === GPUShaderStage.COMPUTE ) {
|
||
|
||
//console.log("this.bindGroupLayouts", this.bindGroupLayouts);
|
||
|
||
//console.log("create compute shader pipeline.");
|
||
|
||
this.computeStage = { module: shaderModule, entryPoint: this.entryPoint };
|
||
|
||
this.pipeline = this.device.createComputePipeline( {
|
||
|
||
layout: this.device.createPipelineLayout( {
|
||
|
||
bindGroupLayouts: Array.from( this.bindGroupLayouts.values() )
|
||
|
||
} ),
|
||
|
||
compute: this.computeStage
|
||
|
||
} );
|
||
|
||
await this.finalizeShader();
|
||
|
||
} else if ( stage === GPUShaderStage.VERTEX ) {
|
||
|
||
//this.vertexStage = { module: shaderModule, entryPoint: this.entryPoint };
|
||
console.log("Vertex buffers descriptors:", Array.from(this.vertexBuffersDescriptors.values()));
|
||
|
||
const vertexBuffers = this.vertexBuffersDescriptors.size > 0
|
||
? [...this.vertexBuffersDescriptors.values()]
|
||
: [];
|
||
|
||
this.vertexStage = {
|
||
module: shaderModule,
|
||
entryPoint: this.entryPoint,
|
||
buffers: vertexBuffers
|
||
};
|
||
|
||
} else if ( stage === GPUShaderStage.FRAGMENT ) {
|
||
|
||
this.fragmentStage = { module: shaderModule, entryPoint: this.entryPoint };
|
||
|
||
}
|
||
|
||
// If both vertex and fragment stages exist, create render pipeline
|
||
if ( this.vertexStage && this.fragmentStage ) {
|
||
|
||
this._createAllBuffers();
|
||
|
||
console.log( this.device );
|
||
|
||
console.log("create fragment and vertex shader pipeline.");
|
||
|
||
console.log("setup createRenderPipeline", this.vertexStage);
|
||
|
||
console.log("this.device.createPipelineLayout", this.bindGroupLayouts.get(0) );
|
||
|
||
|
||
|
||
|
||
this.pipeline = this.device.createRenderPipeline( {
|
||
|
||
layout: this.device.createPipelineLayout( {
|
||
|
||
bindGroupLayouts: Array.from( this.bindGroupLayouts.values() )
|
||
|
||
} ),
|
||
|
||
vertex: this.vertexStage,
|
||
|
||
fragment: {
|
||
...this.fragmentStage,
|
||
|
||
targets: [
|
||
{
|
||
format: "bgra8unorm",
|
||
// No blending here for opaque rendering
|
||
blend: undefined,
|
||
writeMask: GPUColorWrite.ALL
|
||
}
|
||
] // Specify the color target format here
|
||
|
||
|
||
},
|
||
multisample: {
|
||
|
||
count: this.sampleCount, // number of samples per pixel, typical values: 4 or 8
|
||
|
||
},
|
||
primitive: {
|
||
|
||
topology: this.topologyValue,
|
||
|
||
frontFace: 'ccw' ,
|
||
|
||
cullMode: 'none',
|
||
|
||
},
|
||
|
||
depthStencil: {
|
||
|
||
format: "depth24plus", // <-- Add this to match render pass depthStencil
|
||
|
||
depthWriteEnabled: true,
|
||
|
||
depthCompare: "less",
|
||
|
||
},
|
||
|
||
|
||
|
||
} );
|
||
|
||
this._createAllBuffers();
|
||
|
||
await this.finalizeShader();
|
||
|
||
}
|
||
|
||
}
|
||
|
||
convertToTypedArrayWithPadding(array, preferredType = null) {
|
||
|
||
const isTypedArray = (
|
||
array instanceof Uint16Array ||
|
||
array instanceof Uint32Array ||
|
||
array instanceof Float32Array
|
||
);
|
||
|
||
let typedArray;
|
||
let arrayType = preferredType;
|
||
|
||
if (isTypedArray) {
|
||
if (preferredType === null) {
|
||
if (array instanceof Uint16Array) arrayType = "uint16";
|
||
else if (array instanceof Uint32Array) arrayType = "uint32";
|
||
else if (array instanceof Float32Array) arrayType = "float32";
|
||
}
|
||
|
||
const remainder = array.byteLength % 4;
|
||
|
||
if (remainder !== 0) {
|
||
const elementSize = (arrayType === "uint16") ? 2 : 4;
|
||
const paddedByteLength = array.byteLength + (4 - remainder);
|
||
const paddedElementCount = paddedByteLength / elementSize;
|
||
|
||
if (arrayType === "uint16") {
|
||
const paddedArray = new Uint16Array(paddedElementCount);
|
||
paddedArray.set(array);
|
||
typedArray = paddedArray;
|
||
} else if (arrayType === "uint32") {
|
||
const paddedArray = new Uint32Array(paddedElementCount);
|
||
paddedArray.set(array);
|
||
typedArray = paddedArray;
|
||
} else if (arrayType === "float32") {
|
||
const paddedArray = new Float32Array(paddedElementCount);
|
||
paddedArray.set(array);
|
||
typedArray = paddedArray;
|
||
} else {
|
||
throw new Error("Unsupported typed array type for padding");
|
||
}
|
||
} else {
|
||
typedArray = array;
|
||
}
|
||
} else if (Array.isArray(array)) {
|
||
if (preferredType === null) {
|
||
// Default to Float32Array if no preferred type
|
||
arrayType = "float32";
|
||
} else {
|
||
arrayType = preferredType;
|
||
}
|
||
|
||
if (arrayType === "uint16") {
|
||
typedArray = new Uint16Array(array);
|
||
} else if (arrayType === "uint32") {
|
||
typedArray = new Uint32Array(array);
|
||
} else if (arrayType === "float32") {
|
||
typedArray = new Float32Array(array);
|
||
} else {
|
||
throw new Error("Unsupported preferred type");
|
||
}
|
||
|
||
const remainder = typedArray.byteLength % 4;
|
||
|
||
if (remainder !== 0) {
|
||
const elementSize = (arrayType === "uint16") ? 2 : 4;
|
||
const paddedByteLength = typedArray.byteLength + (4 - remainder);
|
||
const paddedElementCount = paddedByteLength / elementSize;
|
||
|
||
if (arrayType === "uint16") {
|
||
const paddedArray = new Uint16Array(paddedElementCount);
|
||
paddedArray.set(typedArray);
|
||
typedArray = paddedArray;
|
||
} else if (arrayType === "uint32") {
|
||
const paddedArray = new Uint32Array(paddedElementCount);
|
||
paddedArray.set(typedArray);
|
||
typedArray = paddedArray;
|
||
} else if (arrayType === "float32") {
|
||
const paddedArray = new Float32Array(paddedElementCount);
|
||
paddedArray.set(typedArray);
|
||
typedArray = paddedArray;
|
||
} else {
|
||
throw new Error("Unsupported typed array type for padding");
|
||
}
|
||
}
|
||
} else {
|
||
throw new Error("Input must be a TypedArray or Array of numbers");
|
||
}
|
||
|
||
return { typedArray, arrayType };
|
||
}
|
||
|
||
setIndices( indices ) {
|
||
console.log("indices", indices);
|
||
const isTypedArray = indices instanceof Uint16Array || indices instanceof Uint32Array;
|
||
|
||
let typedArray;
|
||
let arrayType;
|
||
|
||
console.log("isTypedArray", isTypedArray);
|
||
|
||
if ( isTypedArray ) {
|
||
|
||
arrayType = (indices instanceof Uint16Array) ? "uint16" : "uint32";
|
||
|
||
({ typedArray } = this.convertToTypedArrayWithPadding( indices, arrayType ) );
|
||
|
||
} else if (Array.isArray(indices)) {
|
||
|
||
let maxIndex = 0;
|
||
|
||
for (let i = 0; i < indices.length; i++) {
|
||
if (indices[i] > maxIndex) maxIndex = indices[i];
|
||
}
|
||
|
||
arrayType = (maxIndex < 65536) ? "uint16" : "uint32";
|
||
|
||
|
||
({ typedArray } = this.convertToTypedArrayWithPadding(indices, arrayType));
|
||
|
||
|
||
} else {
|
||
|
||
throw new Error("Indices must be Uint16Array, Uint32Array, or Array of numbers");
|
||
|
||
}
|
||
|
||
this.indexFormat = arrayType;
|
||
|
||
this.indexBufferData = typedArray;
|
||
|
||
if (this.indexBuffer) {
|
||
|
||
this.indexBuffer.destroy();
|
||
|
||
}
|
||
|
||
this.indexBuffer = this.device.createBuffer({
|
||
size: this.indexBufferData.byteLength,
|
||
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
||
});
|
||
|
||
this.device.queue.writeBuffer(
|
||
this.indexBuffer,
|
||
0,
|
||
this.indexBufferData.buffer,
|
||
this.indexBufferData.byteOffset,
|
||
this.indexBufferData.byteLength
|
||
);
|
||
|
||
this.indexCount = this.indexBufferData.length;
|
||
|
||
|
||
}
|
||
|
||
setAttribute( name, array ) {
|
||
|
||
if ( this.attributeArrays === undefined ) {
|
||
|
||
this.attributeArrays = new Map();
|
||
|
||
}
|
||
|
||
const { typedArray, arrayType } = this.convertToTypedArrayWithPadding( array );
|
||
|
||
let attributeSize = 3; // Default size (vec3)
|
||
|
||
// this is not proper
|
||
if ( name === "uv" ) {
|
||
|
||
attributeSize = 2;
|
||
|
||
} else if ( name === "position" || name === "normal" ) {
|
||
|
||
attributeSize = 3;
|
||
|
||
} else {
|
||
|
||
// You can add more attribute names and sizes here
|
||
|
||
}
|
||
|
||
this.attributeArrays.set( name, { typedArray: typedArray, size: attributeSize } );
|
||
|
||
}
|
||
|
||
getAllAttributeBuffers() {
|
||
|
||
if ( !this.attributeBuffers ) {
|
||
|
||
return new Map();
|
||
|
||
}
|
||
|
||
return this.attributeBuffers;
|
||
|
||
}
|
||
|
||
vertexBuffers( passEncoder ) {
|
||
|
||
if ( this.attributeArrays === undefined ) {
|
||
|
||
console.warn("No attribute arrays to bind.");
|
||
|
||
return;
|
||
|
||
}
|
||
|
||
if ( this.mergedAttributeBuffer === undefined ) {
|
||
|
||
const attributeNames = Array.from( this.attributeArrays.keys() );
|
||
|
||
const arrays = attributeNames.map( name => this.attributeArrays.get( name ).typedArray );
|
||
|
||
const attributeSizes = attributeNames.map( name => (name === "uv") ? 2 : 3 );
|
||
|
||
const vertexCount = arrays[0].length / attributeSizes[0];
|
||
|
||
const strideFloats = attributeSizes.reduce( (sum, size) => sum + size, 0 );
|
||
|
||
const mergedArray = new Float32Array( vertexCount * strideFloats );
|
||
|
||
|
||
for ( let i = 0; i < vertexCount; i++ ) {
|
||
|
||
let offset = i * strideFloats;
|
||
|
||
for ( let attrIndex = 0; attrIndex < arrays.length; attrIndex++ ) {
|
||
|
||
const arr = arrays[attrIndex];
|
||
|
||
const size = attributeSizes[attrIndex];
|
||
|
||
const baseIndex = i * size;
|
||
|
||
for ( let c = 0; c < size; c++ ) {
|
||
|
||
mergedArray[ offset++ ] = arr[ baseIndex + c ];
|
||
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
const byteLength = mergedArray.byteLength;
|
||
|
||
const buffer = this.device.createBuffer({
|
||
|
||
size : byteLength,
|
||
|
||
usage : GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
||
|
||
mappedAtCreation : true,
|
||
|
||
label : "mergedAttributes"
|
||
|
||
});
|
||
|
||
const mapping = new Float32Array( buffer.getMappedRange() );
|
||
|
||
mapping.set( mergedArray );
|
||
|
||
buffer.unmap();
|
||
|
||
this.device.queue.writeBuffer(
|
||
|
||
buffer,
|
||
|
||
0,
|
||
|
||
mergedArray.buffer,
|
||
|
||
mergedArray.byteOffset,
|
||
|
||
mergedArray.byteLength
|
||
|
||
);
|
||
|
||
this.mergedAttributeBuffer = buffer;
|
||
|
||
// Store for pipeline setup
|
||
this.attributeStrideFloats = strideFloats;
|
||
|
||
this.attributeSizes = attributeSizes;
|
||
|
||
this.attributeNames = attributeNames;
|
||
|
||
}
|
||
|
||
// Bind the merged buffer at slot 0
|
||
|
||
passEncoder.setVertexBuffer( 0, this.mergedAttributeBuffer );
|
||
|
||
}
|
||
|
||
setCanvas( canvas ) {
|
||
|
||
this.canvas = canvas;
|
||
|
||
}
|
||
|
||
|
||
encodeRender( passEncoder, x, y = 0, z = 0 ) {
|
||
|
||
if ( !this.bindGroups || this.bindGroups.size === 0 ) {
|
||
this.createBindGroups();
|
||
}
|
||
|
||
passEncoder.setPipeline( this.pipeline );
|
||
|
||
this.vertexBuffers( passEncoder );
|
||
|
||
for ( const [ bindingGroupIndex, bindGroup ] of this.bindGroups.entries() ) {
|
||
|
||
passEncoder.setBindGroup( bindingGroupIndex, bindGroup );
|
||
|
||
}
|
||
|
||
if ( this.indexBuffer ) {
|
||
|
||
passEncoder.setIndexBuffer( this.indexBuffer, this.indexFormat );
|
||
|
||
passEncoder.drawIndexed( this.indexCount, y, z );
|
||
|
||
} else {
|
||
|
||
passEncoder.draw( x, y, z );
|
||
|
||
}
|
||
|
||
}
|
||
|
||
async renderToCanvas( x, y = 0, z = 0 ) {
|
||
|
||
// AUTO-REBUILD BIND GROUPS IF NEEDED
|
||
if ( !this.bindGroups || this.bindGroups.size === 0 ) {
|
||
this.createBindGroups();
|
||
}
|
||
|
||
const canvas = this.canvas;
|
||
|
||
const context = canvas.getContext( "webgpu" );
|
||
|
||
const format = navigator.gpu.getPreferredCanvasFormat();
|
||
|
||
const depthTexture = this.device.createTexture( {
|
||
size: [ this.canvas.width, this.canvas.height, 1 ],
|
||
sampleCount: this.sampleCount,
|
||
format: "depth24plus",
|
||
usage: GPUTextureUsage.RENDER_ATTACHMENT
|
||
} );
|
||
|
||
const commandEncoder = this.device.createCommandEncoder();
|
||
|
||
let renderPassDescriptor;
|
||
|
||
const colorLoadOp = this.clearOnRender ? "clear" : "load";
|
||
const depthLoadOp = this.clearOnRender ? "clear" : "load";
|
||
const clearColor = { r: 0.3, g: 0.3, b: 0.3, a: 1 };
|
||
|
||
if ( this.sampleCount === 1 ) {
|
||
|
||
renderPassDescriptor = {
|
||
colorAttachments: [ {
|
||
view: context.getCurrentTexture().createView(),
|
||
loadOp: colorLoadOp,
|
||
storeOp: "store",
|
||
clearValue: clearColor
|
||
} ],
|
||
depthStencilAttachment: {
|
||
view: this.depthTexture.createView(),
|
||
depthLoadOp: depthLoadOp,
|
||
depthStoreOp: "store",
|
||
depthClearValue: 1.0
|
||
}
|
||
};
|
||
|
||
} else {
|
||
|
||
renderPassDescriptor = {
|
||
colorAttachments: [ {
|
||
view: this.multisampledColorTexture.createView(),
|
||
resolveTarget: context.getCurrentTexture().createView(),
|
||
loadOp: colorLoadOp,
|
||
storeOp: "store",
|
||
clearValue: { r: 0, g: 0, b: 0, a: 1 }
|
||
} ],
|
||
depthStencilAttachment: {
|
||
view: this.multisampledDepthTexture.createView(),
|
||
depthLoadOp: depthLoadOp,
|
||
depthStoreOp: "store",
|
||
depthClearValue: 1.0
|
||
}
|
||
};
|
||
}
|
||
|
||
const passEncoder = commandEncoder.beginRenderPass( renderPassDescriptor );
|
||
|
||
this.encodeRender( passEncoder, x, y, z );
|
||
|
||
passEncoder.end();
|
||
|
||
this.device.queue.submit( [ commandEncoder.finish() ] );
|
||
|
||
}
|
||
|
||
async finalizeShader() {
|
||
|
||
//await this._createPipeline();
|
||
|
||
await this.createBindGroups();
|
||
|
||
}
|
||
|
||
async debugBuffer( bufferName, bufferType ) {
|
||
|
||
|
||
|
||
const buffer = this.getBuffer( bufferName );
|
||
|
||
const dataSize = buffer.size;
|
||
|
||
const debugReadBuffer = this.device.createBuffer( {
|
||
size: dataSize,
|
||
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
||
} );
|
||
|
||
const debugEncoder = this.device.createCommandEncoder();
|
||
|
||
//console.log( "print buffer", buffer );
|
||
|
||
debugEncoder.copyBufferToBuffer(
|
||
buffer,
|
||
0,
|
||
debugReadBuffer,
|
||
0,
|
||
dataSize
|
||
);
|
||
|
||
this.device.queue.submit( [ debugEncoder.finish() ] );
|
||
|
||
await this.device.queue.onSubmittedWorkDone();
|
||
|
||
await debugReadBuffer.mapAsync( GPUMapMode.READ );
|
||
|
||
const copyArrayBuffer = debugReadBuffer.getMappedRange();
|
||
|
||
let result;
|
||
|
||
const bindingInfo = this.bindings.find( b => b.varName === bufferName );
|
||
|
||
if ( bindingInfo ) {
|
||
|
||
const wgslType = bindingInfo.varType;
|
||
|
||
//console.log("wgslType", bindingInfo, wgslType, bufferName);
|
||
|
||
// === STRUCT SUPPORT ========================================================
|
||
if (this.structs && this.structs[wgslType]) {
|
||
|
||
const structInfo = this.structs[wgslType];
|
||
const view = new DataView(copyArrayBuffer);
|
||
const resultObj = {};
|
||
|
||
for (const field of structInfo.fields) {
|
||
|
||
const off = field.offset;
|
||
|
||
switch (field.type) {
|
||
case "u32":
|
||
resultObj[field.name] = view.getUint32(off, true);
|
||
break;
|
||
case "i32":
|
||
resultObj[field.name] = view.getInt32(off, true);
|
||
break;
|
||
case "f32":
|
||
resultObj[field.name] = view.getFloat32(off, true);
|
||
break;
|
||
|
||
default:
|
||
console.warn("Unhandled struct field type:", field.type);
|
||
resultObj[field.name] = null;
|
||
break;
|
||
}
|
||
}
|
||
|
||
debugReadBuffer.unmap();
|
||
return resultObj;
|
||
}
|
||
|
||
|
||
|
||
switch ( wgslType ) {
|
||
|
||
case "array<f32>":
|
||
result = new Float32Array( copyArrayBuffer );
|
||
break;
|
||
case "array<u32>":
|
||
result = new Uint32Array( copyArrayBuffer );
|
||
break;
|
||
case "vec3<f32>":
|
||
|
||
break;
|
||
case "vec4<f32>":
|
||
result = new Float32Array( copyArrayBuffer );
|
||
break;
|
||
case "u32":
|
||
case "vec2<u32>":
|
||
case "vec3<u32>":
|
||
case "vec4<u32>":
|
||
result = new Uint32Array( copyArrayBuffer );
|
||
break;
|
||
case "Tensor":
|
||
result = new Float32Array(copyArrayBuffer);
|
||
break;
|
||
|
||
case "array<vec4<f32>>":
|
||
case "array<Vector>":
|
||
case "array<array<f32, 64>>":
|
||
case "array<vec3<f32>>":
|
||
result = new Float32Array( copyArrayBuffer );
|
||
break;
|
||
case "vec3<i32>":
|
||
case "vec4<i32>":
|
||
result = new Int32Array( copyArrayBuffer );
|
||
break;
|
||
|
||
default:
|
||
console.error( "Unknown WGSL type in setVariable: " + wgslType, "Using Float32Array as fallback." );
|
||
|
||
result = new Float32Array( copyArrayBuffer );
|
||
}
|
||
|
||
// use wgslType here to determine appropriate TypedArray
|
||
}
|
||
const length = result.length;
|
||
var regularArray = new Array();
|
||
|
||
for (var i = 0; i < length; i++) {
|
||
|
||
regularArray.push(result[i]);
|
||
|
||
}
|
||
|
||
/*
|
||
console.log("Debugging array");
|
||
console.log("");
|
||
console.log("Shader: ", this.path);
|
||
console.log("Variable Name: ", bufferName);
|
||
|
||
console.log( "Buffer Size: ", this.getBuffer(bufferName).size );
|
||
console.log( "TypedArray Size: ", length );
|
||
*/
|
||
|
||
if ( bindingInfo ) {
|
||
|
||
const wgslType = bindingInfo.varType;
|
||
|
||
//console.log( "type: ", wgslType );
|
||
|
||
}
|
||
|
||
//console.log(regularArray);
|
||
|
||
debugReadBuffer.unmap();
|
||
|
||
return regularArray;
|
||
|
||
}
|
||
|
||
|
||
}
|