xgpu
Version:
XGPU is an extendable library for WebGPU that provides a higher-level, easy-to-use interface for building rendering engines or processing numeric data. It handles automatic data binding, buffer alignment, variable declarations in shaders, and more. XGPU i
631 lines (619 loc) • 24.9 kB
JavaScript
// Copyright (c) 2023 Thomas Le Coz. All rights reserved.
// This code is governed by an MIT license that can be found in the LICENSE file.
import { XGPU } from "../../XGPU";
import { GPUType } from "../../GPUType";
import { ShaderStruct } from "../shaderParts/ShaderStruct";
import { VertexAttribute } from "./VertexAttribute";
import { StageableBuffer } from "./StageableBuffer";
export class VertexBuffer extends StageableBuffer {
bufferId; //the id used in renderPass.setVertexBuffer
io = 0;
resourceIO = null;
mustBeTransfered = false;
vertexArrays = [];
attributes = {};
gpuResource;
descriptor;
_nbComponent = 0;
_datas;
nbComponentData;
attributeChanged = false;
constructor(attributes, descriptor) {
super();
//console.warn("VERTEX BUFFER 2", attributes)
if (!descriptor)
descriptor = {};
else
descriptor = { ...descriptor };
if (!descriptor.stepMode)
descriptor.stepMode = "vertex";
this.descriptor = descriptor;
const items = attributes;
let buffer, offset, datas;
let attribute;
let nbComponent = 0;
for (let name in items) {
buffer = items[name];
offset = buffer.offset;
datas = buffer.datas;
nbComponent += buffer.nbComponent;
//console.log("=> ", name, offset,buffer.nbComponent)
if (!this.attributes[name]) {
attribute = this.createArray(name, buffer.type, offset);
//console.log(name, offset)
if (datas)
attribute.datas = datas;
}
else {
//console.log(this.attributes[name])
}
}
if (descriptor.datas)
this.datas = descriptor.datas;
if (descriptor.gpuUpdateMode == "manual") {
const accessMode = descriptor.accessMode ? descriptor.accessMode : "read";
if (!descriptor.usage) {
throw new Error("VertexBuffer constructor : you must set the property 'usage' in the descriptor if 'gpuUpdateMode' is set to 'manual' ");
}
const usage = descriptor.usage;
this.createLowLevelBuffer(nbComponent * 4, accessMode, usage);
}
}
/*
public pushDatas(datas: Float32Array | Int32Array | Uint32Array | Uint16Array) {
this.mustBeTransfered = true;
if (!extraBufferSize) extraBufferSize = 1000;
//if (this.datas) console.log(this.datas.length + " VS " + (offset + len))
if (!this._datas || this._datas.length < offset + len) {
if (!this._datas) {
this._datas = datas;
this.createGpuResource();
} else if ((offset + len) - this._datas.length >= extraBufferSize) {
this._datas = datas;
this.createGpuResource();
} else {
//console.log("B")
if (indices instanceof Uint16Array) this._datas = new Uint16Array(this._datas.length + extraBufferSize);
else this._datas = new Uint32Array(this._datas.length + extraBufferSize);
this._datas.set(indices);
this.createGpuResource();
}
} else {
//console.log("A ", indices.slice(offset, offset + len))
if (offset && len) this._datas.set(indices.slice(offset, offset + len), offset)
else this._datas.set(indices);
}
this.update();
}
*/
clone() {
const vb = new VertexBuffer(this.attributeDescriptor, this.descriptor);
vb.bufferId = this.bufferId;
let datas;
if (this.datas instanceof Float32Array)
datas = new Float32Array(this.datas.length);
else if (this.datas instanceof Int32Array)
datas = new Int32Array(this.datas.length);
else if (this.datas instanceof Uint32Array)
datas = new Uint32Array(this.datas.length);
//const data = new Float32Array(this.datas.length);
datas.set(this.datas);
vb.datas = datas;
//console.log("clone")
return vb;
}
gpuBufferIOs;
gpuBufferIO_index = 1;
initBufferIO(buffers) {
this.gpuBufferIOs = buffers;
}
get buffer() {
if (this.gpuBufferIOs) {
const buf = this.gpuBufferIOs[this.gpuBufferIO_index++ % 2];
return buf;
}
return this.gpuResource;
}
getCurrentBuffer() {
if (this.gpuBufferIOs)
return this.gpuBufferIOs[(this.gpuBufferIO_index + 1) % 2];
if (!this.gpuResource)
this.createGpuResource();
return this.gpuResource;
}
get stepMode() { return this.descriptor.stepMode; }
get length() { return this.vertexArrays.length; }
get nbComponent() { return this._nbComponent; }
get nbVertex() {
if (!this.datas)
return 0;
if (this.nbComponentData)
return this.datas.length / this.nbComponentData;
return this.datas.length / this._nbComponent;
}
get datas() { return this._datas; }
set datas(f) {
this._datas = f;
this.mustBeTransfered = true;
}
setComplexDatas(datas, nbComponentTotal) {
this._nbComponent = nbComponentTotal;
this.datas = datas;
}
get attributeDescriptor() {
const result = {};
let o;
for (let name in this.attributes) {
o = this.attributes[name];
result[name] = {
type: o.format,
offset: o.dataOffset
};
}
return result;
}
_byteCount = 0;
createArray(name, dataType, offset) {
if (this.attributes[name] && this.attributes[name].vertexBuffer) {
return this.attributes[name];
}
let v = this.attributes[name];
//console.warn("createArray ", name, dataType, offset, this.attributes[name]);
if (!v)
v = this.attributes[name] = new VertexAttribute(name, dataType, offset);
v.vertexBuffer = this;
const nbCompo = v.nbComponent;
const _offset = v.dataOffset === undefined ? 0 : v.dataOffset;
this._nbComponent += nbCompo;
//console.log("v.dataOffset = ", name, v.dataOffset)
if (v.dataOffset === undefined)
this._byteCount += nbCompo * new GPUType(v.varType).byteValue;
else
this._byteCount = Math.max(this._byteCount, (_offset + v.nbComponent) * new GPUType(v.varType).byteValue);
this.vertexArrays.push(v);
return v;
}
getAttributeByName(name) {
return this.attributes[name];
}
//----------------------------- USED WITH COMPUTE PIPELINE ----------------------------------------
createDeclaration(vertexBufferName, bindingId, groupId = 0, isInput = true) {
//console.warn("VB.createDeclaration ", vertexBufferName, isInput)
if (isInput) { }
this.stackAttributes();
let structName = vertexBufferName.substring(0, 1).toUpperCase() + vertexBufferName.slice(1);
const varName = vertexBufferName.substring(0, 1).toLowerCase() + vertexBufferName.slice(1);
//console.log("XGPU||| structName = ",structName);
let result = "";
let type = "storage, read";
let structType = "array<" + structName + ">";
if (this.descriptor.accessMode == "read") { //this.io === 1 || this.io === 0) {
result += "struct " + structName + "{\n";
let a;
for (let i = 0; i < this.vertexArrays.length; i++) {
a = this.vertexArrays[i];
result += " " + a.name + ":" + a.varType + ",\n";
}
result += "}\n\n";
structType = "array<" + structName + ">";
}
else {
type = "storage, read_write";
if (this.io == 2) {
//remove the '_out'
structName = structName.slice(0, structName.length - 4);
}
else {
result += "struct " + structName + "{\n";
let a;
for (let i = 0; i < this.vertexArrays.length; i++) {
a = this.vertexArrays[i];
result += " " + a.name + ":" + a.varType + ",\n";
}
result += "}\n\n";
structType = "array<" + structName + ">";
}
structType = "array<" + structName + ">";
}
result += "@binding(" + bindingId + ") @group(" + groupId + ") var<" + type + "> " + varName + ":" + structType + ";\n";
return result;
}
debug = false;
createBindGroupLayoutEntry(bindingId) {
return {
binding: bindingId,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: this.descriptor.accessMode === "read" ? "read-only-storage" : "storage"
},
};
}
createBindGroupEntry(bindingId) {
if (!this.gpuResource)
this.createGpuResource();
//console.log("VertexBuffer.createBindgroupEntry size = ", this.datas.byteLength)
let size = 0;
if (this.datas)
size = this.datas.byteLength;
if (this.lowLevelBuffer)
size = this._bufferSize;
return {
binding: bindingId,
resource: {
buffer: this.gpuResource,
offset: 0,
size
}
};
}
pipelineType;
setPipelineType(pipelineType) {
if (this.pipelineType || pipelineType == "compute" && this.lowLevelBuffer == true)
return;
this.pipelineType = pipelineType;
//use to handle particular cases in descriptor relative to the nature of pipeline
if (pipelineType === "render") {
if (!this.descriptor.accessMode)
this.descriptor.accessMode = "read";
if (!this.descriptor.usage)
this.descriptor.usage = GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST;
}
else if (pipelineType === "compute_mixed") {
if (this.io === 1 || this.io === 0) { //VertexBufferIO output , usable in a renderPipeline
if (!this.descriptor.usage)
this.descriptor.usage = GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
if (!this.descriptor.accessMode)
this.descriptor.accessMode = "read";
}
else if (this.io === 2) { //VertexBufferIO input
if (!this.descriptor.usage)
this.descriptor.usage = GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
if (!this.descriptor.accessMode)
this.descriptor.accessMode = "read_write";
}
}
else if (pipelineType === "compute") {
if (this.io === 1 || this.io == 0) { //VertexBufferIO output || VertexBuffer in computeShader
if (!this.descriptor.usage)
this.descriptor.usage = GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
if (!this.descriptor.accessMode)
this.descriptor.accessMode = "read";
}
else if (this.io === 2) { //VertexBufferIO input
if (!this.descriptor.usage)
this.descriptor.usage = GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
if (!this.descriptor.accessMode)
this.descriptor.accessMode = "read_write";
}
}
}
//------------------------------------------------------------------------------------------------------
createStruct(name) {
const structName = name.substring(0, 1).toUpperCase() + name.slice(1);
const properties = [];
let a;
for (let i = 0; i < this.vertexArrays.length; i++) {
a = this.vertexArrays[i];
properties[i] = { name: a.name, type: a.varType, builtin: "" };
}
return new ShaderStruct(structName, properties);
}
arrayStride;
stackAttributes(builtinOffset = 0) {
//console.log("---------- STACK ATTRIBUTES ------------");
//console.log(this.descriptor.stepMode)
const result = [];
let bound = 1;
var floats = [];
var vec2s = [];
var vec3s = [];
let v;
let offset = 0;
for (let i = 0; i < this.vertexArrays.length; i++) {
v = this.vertexArrays[i];
if (v.nbComponent === 1)
floats.push(v);
else if (v.nbComponent === 2) {
if (bound < 2)
bound = 2;
vec2s.push(v);
}
else if (v.nbComponent === 3) {
bound = 4;
vec3s.push(v);
}
else if (v.nbComponent === 4) {
bound = 4;
v.dataOffset = offset;
offset += 4;
result.push(v);
}
}
//------------------------
const addVec3 = () => {
v = vec3s.shift();
v.dataOffset = offset;
offset += 3;
result.push(v);
if (floats.length) {
const f = floats.shift();
f.dataOffset = offset;
result.push(f);
}
offset++;
};
let nb = vec3s.length;
for (let i = 0; i < nb; i++)
addVec3();
//--------------------------
nb = vec2s.length;
for (let i = 0; i < nb; i++) {
v = vec2s.shift();
v.dataOffset = offset;
offset += 2;
result.push(v);
}
//--------------------------
nb = floats.length;
for (let i = 0; i < nb; i++) {
v = floats.shift();
v.dataOffset = offset;
offset++;
result.push(v);
}
//--------------------------
if (offset % bound !== 0) {
offset += bound - (offset % bound);
}
//--------------------------
this.vertexArrays = result;
const attributes = [];
for (let i = 0; i < result.length; i++) {
attributes[i] = {
shaderLocation: builtinOffset + i,
offset: result[i].dataOffset * Float32Array.BYTES_PER_ELEMENT,
format: this.vertexArrays[i].format
};
}
//-----------------
this.arrayStride = offset;
//console.log("this.arrayStride = ", offset);
//console.log(this.descriptor.stepMode)
const res = {
stepMode: this.descriptor.stepMode,
arrayStride: Float32Array.BYTES_PER_ELEMENT * this.arrayStride,
attributes
};
//console.log("res = ",res)
return res;
}
addVertexInstance(instanceId, o) {
const start = instanceId * this.arrayStride;
const datas = this._datas;
let attribute;
for (let z in o) {
attribute = this.getAttributeByName(z);
if (attribute) {
datas[start + attribute.dataOffset] = o[z];
}
}
}
layout;
createVertexBufferLayout(builtinOffset = 0) {
//console.log(this.io, this.descriptor.stepMode)
if (this.gpuBufferIOs) {
//console.log("GPUBufferIOs = ",this.gpuBufferIOs)
return this.stackAttributes(builtinOffset);
}
let nb = this._nbComponent;
if (this.nbComponentData)
nb = this.nbComponentData;
const obj = {
stepMode: this.descriptor.stepMode,
arrayStride: Float32Array.BYTES_PER_ELEMENT * nb,
attributes: []
};
let componentId = 0;
let offset;
for (let i = 0; i < this.vertexArrays.length; i++) {
offset = componentId;
if (this.vertexArrays[i].dataOffset !== undefined)
offset = componentId = this.vertexArrays[i].dataOffset;
obj.attributes[i] = {
shaderLocation: builtinOffset + i,
offset: offset * Float32Array.BYTES_PER_ELEMENT,
format: this.vertexArrays[i].format
};
componentId += this.vertexArrays[i].nbComponent;
}
//console.log("vb layout = ", obj)
obj.arrayStride = Math.max(this._byteCount, nb * Float32Array.BYTES_PER_ELEMENT);
this.layout = obj;
return obj;
}
lowLevelBuffer = false;
createLowLevelBuffer(bufferSize, accessMode = "read", usage = GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST) {
this.lowLevelBuffer = true;
if (this.gpuResource)
this.gpuResource.destroy();
this.descriptor.usage = usage;
this.descriptor.accessMode = accessMode;
this.deviceId = XGPU.deviceId;
this._bufferSize = bufferSize;
this.gpuResource = XGPU.device.createBuffer({
size: bufferSize,
usage: this.descriptor.usage,
mappedAtCreation: false,
});
this.destroyed = false;
this.mustBeTransfered = true;
}
resizeLowLevelBuffer(byteLength, copyPreviousDataWithin = false) {
if (!this.lowLevelBuffer)
return;
const oldBuffer = this.gpuResource;
this._bufferSize = byteLength;
this.gpuResource = XGPU.device.createBuffer({
size: byteLength,
usage: this.descriptor.usage,
mappedAtCreation: false,
});
if (copyPreviousDataWithin) {
const command = XGPU.device.createCommandEncoder();
command.copyBufferToBuffer(oldBuffer, 0, this.gpuResource, 0, oldBuffer.size);
const end = command.finish();
XGPU.device.queue.submit([end]);
XGPU.device.queue.onSubmittedWorkDone().then(() => {
//console.log("DESTROY BUFFER")
oldBuffer.destroy();
});
}
else {
oldBuffer.destroy();
}
}
updateLowLevelBuffer(buffer, bufferOffset, dataOffset, size) {
//console.log({dataOffset,size,buffer})
if (size == undefined)
size = buffer.byteLength;
if (dataOffset == undefined)
dataOffset = 0;
if (!this.gpuResource)
throw new Error("you must create a GPUBuffer using VertexBuffer.createGPUResource(bufferSize)");
//if(bufferOffset + size > this._bufferSize) throw new Error("incorrect gpu buffer length, max length = "+this._bufferSize+" vs "+(bufferOffset + size));
if (dataOffset + size > buffer.byteLength)
throw new Error("incorrect datas length");
XGPU.device.queue.writeBuffer(this.gpuResource, bufferOffset, buffer, dataOffset, size);
}
_bufferSize;
deviceId;
get bufferSize() { return this._bufferSize; }
createGpuResource() {
if (this.lowLevelBuffer)
return;
if (this.attributeChanged)
this.updateAttributes();
if (!this.datas || this.gpuBufferIOs)
return;
if (this.gpuResource)
this.gpuResource.destroy();
const bufferSize = this.datas.byteLength;
//console.warn("VB.createGPUResource ", this.io, this.pipelineType, XGPU.debugUsage(this.descriptor.usage))
this.deviceId = XGPU.deviceId;
this._bufferSize = bufferSize;
this.gpuResource = XGPU.device.createBuffer({
size: bufferSize,
usage: this.descriptor.usage,
mappedAtCreation: false,
});
this.destroyed = false;
this.mustBeTransfered = true;
}
time;
destroyed = true;
destroyGpuResource() {
if (this.destroyed)
return;
if (this.time && new Date().getTime() - this.time < 100 && XGPU.loseDeviceRecently) {
return;
}
this.time = new Date().getTime();
if (this.io && XGPU.loseDeviceRecently) {
if (this.io === 1) {
const vbio = this.resourceIO;
const vbs = vbio.buffers;
this.setPipelineType(this.pipelineType);
const currentDatas = vbio.currentDatas ? vbio.currentDatas : vbs[0]._datas;
if (vbs[0]._datas instanceof Float32Array)
vbs[0]._datas = vbs[1]._datas = new Float32Array(currentDatas);
else if (vbs[0]._datas instanceof Int32Array)
vbs[0]._datas = vbs[1]._datas = new Int32Array(currentDatas);
else if (vbs[0]._datas instanceof Uint32Array)
vbs[0]._datas = vbs[1]._datas = new Uint32Array(currentDatas);
//vbs[0]._datas =
let temp = vbs[0].gpuBufferIOs;
vbs[0].gpuBufferIOs = null;
vbs[0].createGpuResource();
vbs[0].gpuBufferIOs = temp;
temp = vbs[1].gpuBufferIOs;
vbs[1].gpuBufferIOs = null;
vbs[1].createGpuResource();
vbs[1].gpuBufferIOs = temp;
vbs[0].gpuBufferIOs[0] = vbs[0].gpuResource;
vbs[0].gpuBufferIOs[1] = vbs[1].gpuResource;
}
return;
}
this.destroyed = true;
if (this.resourceIO) {
this.resourceIO.destroy();
this.resourceIO = null;
}
if (this.gpuResource) {
this.gpuResource.destroy();
this.gpuResource = null;
}
}
updateBuffer() {
if (this.lowLevelBuffer)
return;
if (!this.datas)
return;
if (!this.gpuResource)
this.createGpuResource();
if (this.datas.byteLength != this._bufferSize)
this.createGpuResource();
XGPU.device.queue.writeBuffer(this.gpuResource, 0, this.datas.buffer);
}
getVertexArrayById(id) { return this.vertexArrays[id]; }
updateAttributes() {
let attribute;
attribute = this.vertexArrays[0];
const nbAttributes = this.vertexArrays.length;
let offset = 0;
if (this.vertexArrays[0] && this.vertexArrays[0].useByVertexData) {
const nbVertex = attribute.datas.length;
if (!this._datas)
this._datas = new Float32Array(nbVertex * this.nbComponent);
for (let i = 0; i < nbVertex; i++) {
for (let j = 0; j < nbAttributes; j++) {
attribute = this.vertexArrays[j];
if (attribute.mustBeTransfered) {
//console.log(nbVertex, nbAttributes, offset, attribute)
this._datas.set(attribute.datas[i], offset);
}
offset += attribute.nbComponent;
}
}
}
else {
const nbVertex = attribute.datas.length / attribute.nbComponent;
if (!this._datas)
this._datas = new Float32Array(nbVertex * this.nbComponent);
for (let j = 0; j < nbAttributes; j++) {
attribute = this.vertexArrays[j];
if (attribute.mustBeTransfered) {
this._datas.set(attribute.datas, offset);
}
offset += attribute.nbComponent;
}
}
for (let j = 0; j < nbAttributes; j++)
this.vertexArrays[j].mustBeTransfered = false;
this.attributeChanged = false;
this.mustBeTransfered = true;
}
update() {
if (this.lowLevelBuffer)
return true;
if (this.vertexArrays.length === 0)
return false;
if (this.attributeChanged)
this.updateAttributes();
if (this.mustBeTransfered) {
this.mustBeTransfered = false;
this.updateBuffer();
}
return true;
}
}