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
645 lines (629 loc) • 25.6 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 { HighLevelParser } from "../HighLevelParser";
import { IndexBuffer } from "../pipelines/resources/IndexBuffer";
import { XGPU } from "../XGPU";
import { ImageTexture } from "./resources/ImageTexture";
import { ImageTextureIO } from "./resources/ImageTextureIO";
import { UniformBuffer } from "./resources/UniformBuffer";
import { VertexBuffer } from "./resources/VertexBuffer";
import { VertexBufferIO } from "./resources/VertexBufferIO";
import { VideoTexture } from "./resources/VideoTexture";
export class Bindgroup {
bindgroupId; //the id used in renderPass.setBindgroup
parent;
entries = [];
elements = [];
mustRefreshBindgroup = false;
applyDraw = false;
_layout;
_group;
name = "";
_pingPongBindgroup = null; //used in ComputePipeline and VertexBufferIO
vertexBufferIO;
textureIO;
resourcesIOs = [];
constructor(descriptor) {
if (descriptor)
this.initFromObject(descriptor);
}
add(name, resource) {
if (resource instanceof VideoTexture)
this.mustRefreshBindgroup = true;
else if (resource instanceof ImageTexture && (resource.source instanceof GPUTexture || !resource.source)) {
this.mustRefreshBindgroup = true;
}
if (resource instanceof IndexBuffer) {
this.indexBuffer = resource;
this.elementByName[name] = resource;
return resource;
}
//console.log("bindgroup.add ", resource)
if (resource instanceof VertexBufferIO) {
this.resourcesIOs.push(resource);
this.mustRefreshBindgroup = true;
this.vertexBufferIO = resource;
//console.log("group vertexBufferIO ", this.vertexBufferIO)
this.elements.push({ name: name, resource: resource.buffers[0] });
this.elements.push({ name: name + "_out", resource: resource.buffers[1] });
if (this.parent)
this.parent.add(this);
return resource;
}
if (resource instanceof ImageTextureIO) {
this.resourcesIOs.push(resource);
this.mustRefreshBindgroup = true;
this.textureIO = resource;
this.elements.push({ name: name, resource: resource.textures[0] });
this.elements.push({ name: name + "_out", resource: resource.textures[1] });
if (this.parent)
this.parent.add(this);
return resource;
}
if (resource instanceof VideoTexture) {
resource.addBindgroup(this);
}
this.elements.push({ name, resource });
if (this.parent)
this.parent.add(this);
return resource;
}
set(name, resource) {
for (let i = 0; i < this.elements.length; i++) {
if (this.elements[i].name === name) {
this.elements[i].resource = resource;
}
}
}
remove(name) {
for (let i = 0; i < this.elements.length; i++) {
if (this.elements[i].name === name) {
this.elements.splice(i, 1);
}
}
}
getResourceName(resource) {
for (let i = 0; i < this.elements.length; i++) {
if (resource === this.elements[i].resource) {
return this.elements[i].name;
}
}
return null;
}
get(name) {
for (let i = 0; i < this.elements.length; i++) {
if (this.elements[i].name === name)
return this.elements[i].resource;
}
for (let i = 0; i < this.elements.length; i++) {
if (this.elements[i].resource instanceof UniformBuffer) {
if (this.elements[i].resource.items[name]) {
return this.elements[i].resource;
}
}
}
return null;
}
initFromObject(descriptor) {
//console.log("group.initFromObject ", object)
let object = descriptor;
let isArray = false;
if (descriptor instanceof Array) {
isArray = true;
object = descriptor[0];
}
HighLevelParser.parse(object, "bindgroup");
const result = [];
let k = 0;
let o;
for (let z in object) {
o = object[z];
if (!o)
continue;
//console.log(z + " : " + o.create)
if (o.createGpuResource || o instanceof VertexBufferIO || o instanceof ImageTextureIO) { //if it's a shader resource
result[k++] = this.add(z, o);
}
}
//console.log("object = ", object)
if (isArray) {
for (let i = 0; i < descriptor.length; i++) {
this.createInstance(descriptor[i]);
}
}
//console.warn("bindgroup.initFromObject result = ", result)
return result;
}
//---------------------------------------------------------------------------
clearAfterDeviceLost() {
this._layout = null;
this._group = null;
this.setupApplyCompleted = false;
for (let i = 0; i < this.elements.length; i++) {
this.elements[i].resource.destroyGpuResource();
}
if (this.instances) {
let elements, resource;
let instance;
for (let i = 0; i < this.instances.length; i++) {
instance = this.instances[i];
instance.group = undefined;
elements = instance.elements;
if (instance.indexBuffer)
instance.indexBuffer.createGpuResource();
for (let j = 0; j < elements.length; j++) {
resource = elements[j].resource;
//console.log(j, resource)
if (resource.gpuResource) {
resource.destroyGpuResource();
}
}
}
}
}
;
deviceId = 0;
buildLayout() {
this.deviceId = XGPU.deviceId;
this.io_index = 0;
const layout = { entries: [] };
let bindingId = 0;
//console.warn(this.elements)
let resource;
for (let i = 0; i < this.elements.length; i++) {
resource = this.elements[i].resource;
//console.log(i, resource)
if (resource instanceof VertexBuffer && !resource.io && this.parent.pipeline.type != "compute")
continue;
let bgl = resource.createBindGroupLayoutEntry(bindingId++);
//console.log("bindgroupLayout entry ", (bindingId - 1), bgl);
layout.entries.push(bgl);
}
//console.log("BINDGROUP LAYOUT ENTRIES ", layout)
this._layout = XGPU.device.createBindGroupLayout(layout);
}
setupApplyCompleted = false;
build() {
if (!this._layout || (this.deviceId != XGPU.deviceId && this.ioGroups))
this.buildLayout();
this.deviceId = XGPU.deviceId;
let entries = [];
let bindingId = 0;
let resource;
for (let i = 0; i < this.elements.length; i++) {
if (!this.elements[i])
continue;
resource = this.elements[i].resource;
resource.update();
if (resource instanceof VertexBuffer && !resource.io && this.parent.pipeline.type != "compute")
continue;
let entry = resource.createBindGroupEntry(bindingId++);
//console.warn("BINDGROUP.BUILD ::: bindgroup entry ", this.elements[i].name, (bindingId - 1), entry);
entries.push(entry);
}
this._group = XGPU.device.createBindGroup({ layout: this._layout, entries });
if (!this.setupApplyCompleted && this.parent) {
this.setupApplyCompleted = true;
this.setupApply();
if (this.instanceResourcesArray) {
for (let i = 0; i < this.instanceResourcesArray.length; i++) {
this._createInstance(this.instanceResourcesArray[i]);
}
this.instanceResourcesArray = undefined;
}
}
return this._group;
}
indexBuffer;
vertexBuffers;
vertexBufferReferenceByName;
elementByName = {};
setupApply() {
this.bindgroupId = this.parent.groups.indexOf(this);
const types = this.parent.resources.types;
if (!types)
return;
const allVertexBuffers = types.vertexBuffers;
if (!allVertexBuffers)
return;
const getBufferId = (o) => {
if (!this.instances) {
for (let i = 0; i < allVertexBuffers.length; i++) {
if (allVertexBuffers[i].resource === o)
return i;
}
}
else {
for (let i = 0; i < allVertexBuffers.length; i++) {
if (allVertexBuffers[i].resource.nane === o.name)
return i;
}
}
return -1;
};
this.vertexBuffers = [];
this.vertexBufferReferenceByName = {};
let k = 0;
let element;
let resource;
for (let i = 0; i < this.elements.length; i++) {
element = this.elements[i];
resource = element.resource;
if (resource instanceof VertexBuffer) {
if (!resource.io) {
resource.bufferId = getBufferId(resource);
this.elementByName[element.name] = resource;
this.vertexBufferReferenceByName[element.name] = { bufferId: resource.bufferId, resource };
this.vertexBuffers[k++] = resource;
continue;
}
}
else {
this.elementByName[element.name] = resource;
}
}
}
setupDrawCompleted = false;
setupDraw(force = false) {
if (this.vertexBuffers) {
for (let i = 0; i < this.vertexBuffers.length; i++) {
if (!this.vertexBuffers[i].gpuResource) {
this.vertexBuffers[i].createGpuResource();
}
}
}
if (this.parent.drawConfig) { //may be undefined with a computePipeline
this.indexBuffer = this.parent.drawConfig.indexBuffer;
if (force || (!this.indexBuffer && this.parent.drawConfig.vertexCount <= 0)) {
if (!force && !this.parent.resources.types.vertexBuffers) {
throw new Error("a renderPipeline require a vertexBuffer or a drawConfig object in order to draw. You must add a vertexBuffer or call RenderPipeline.setupDraw");
}
const buffers = this.parent.resources.types.vertexBuffers;
let buf;
for (let i = 0; i < buffers.length; i++) {
buf = buffers[i].resource;
if (buf.descriptor.stepMode === "vertex") {
this.parent.drawConfig.vertexCount = this.parent.resources.types.vertexBuffers[i].resource.nbVertex;
break;
}
}
}
}
}
apply(renderPass) {
if (!this.setupDrawCompleted) {
this.setupDrawCompleted = true;
if (undefined === this.bindgroupId) {
this.bindgroupId = this.parent.groups.indexOf(this);
}
this.setupDraw();
}
if (renderPass instanceof GPUComputePassEncoder) {
this.update(this.parent.pipeline);
renderPass.setBindGroup(this.bindgroupId, this.group);
return;
}
const instances = this.instances ? this.instances : [{ group: this.group, update: () => { } }];
const applyDraw = this.applyDraw;
//console.log("instances.length = ", instances.length)
for (let i = 0; i < instances.length; i++) {
instances[i].update();
this.update(this.parent.pipeline);
renderPass.setBindGroup(this.bindgroupId, instances[i].group);
if (this.vertexBuffers) {
let buf;
for (let j = 0; j < this.vertexBuffers.length; j++) {
buf = this.vertexBuffers[j].getCurrentBuffer();
renderPass.setVertexBuffer(this.vertexBuffers[j].bufferId, buf);
}
}
if (applyDraw) {
this.parent.drawConfig.draw(renderPass);
}
}
}
instances;
instanceResourcesArray;
get useInstances() { return !!this.instances || !!this.instanceResourcesArray; }
;
createInstance(instanceResources) {
if (!this.instanceResourcesArray)
this.instanceResourcesArray = [];
this.instanceResourcesArray.push(instanceResources);
}
_createInstance(resourcePerInstance) {
resourcePerInstance = HighLevelParser.parse(resourcePerInstance, "bindgroup");
if (!this.instances)
this.instances = [];
let indexBuffer;
let vertexBuffers = [];
let result = {
elements: this.elements.concat()
};
let element, resource;
//console.log("---- ", vertexBuffers.length)
for (let i = 0; i < this.elements.length; i++) {
element = this.elements[i];
for (let z in resourcePerInstance) {
resource = resourcePerInstance[z];
if (resource instanceof IndexBuffer) {
indexBuffer = resourcePerInstance[z];
continue;
}
if (element.name === z) {
if (resource instanceof VideoTexture || resource instanceof ImageTexture) {
//keep source descriptor (the "source" option in the descriptor refeer to the media, we)
}
else {
//use "model" descriptor (some config options are applyed on VertexBuffer/UniformBuffer/... and we want to keep it for all the instances)
//(whzt I call "model" is the source bindgroup used to call 'createInstance')
resource.descriptor = element.resource.descriptor;
}
if (!resource.gpuResource) {
resource.createGpuResource();
}
if (element.resource instanceof VertexBuffer) {
resource.bufferId = element.resource.bufferId;
if (vertexBuffers.indexOf(resource) === -1) {
vertexBuffers.push(resource);
}
}
result.elements[i] = { name: z, resource: resource };
}
}
}
if (indexBuffer)
result.indexBuffer = indexBuffer;
result.update = () => {
let bool = false;
for (let i = 0; i < this.elements.length; i++) {
if (this.elements[i].resource.mustBeTransfered) {
this.elements[i].resource.update(this.parent.pipeline);
bool = true;
break;
}
}
this.elements = result.elements;
this.vertexBuffers = vertexBuffers;
if (bool || !result.group) {
result.group = this.build();
}
if (result.indexBuffer)
this.parent.drawConfig.indexBuffer = result.indexBuffer;
};
resourcePerInstance._object = result;
this.instances.push(result);
}
handleComputePipelineResourceIOs() {
if (this.resourcesIOs.length) {
let buf0 = [];
let buf1 = [];
for (let i = 0; i < this.resourcesIOs.length; i++) {
if (this.resourcesIOs[i] instanceof VertexBufferIO) {
buf0[i] = this.resourcesIOs[i].buffers[0];
buf1[i] = this.resourcesIOs[i].buffers[1];
}
else {
buf0[i] = this.resourcesIOs[i].textures[0];
buf1[i] = this.resourcesIOs[i].textures[1];
}
buf0[i].createGpuResource();
buf1[i].createGpuResource();
}
this.createPingPongBindgroup(buf0, buf1);
}
}
swapElements() {
let result = this.elements.concat();
let temp;
for (let i = 0; i < this.elements.length; i += 2) {
temp = result[i];
result[i] = result[i + 1];
result[i + 1] = temp;
}
return result;
}
createPingPongBindgroup(resource1, resource2) {
const group = new Bindgroup();
group.name = this.name;
group.mustRefreshBindgroup = this.mustRefreshBindgroup = true;
group._layout = this.layout;
//console.log("createPingPong")
group.elements = this.swapElements();
let res1, res2;
for (let i = 0; i < resource1.length; i++) {
res1 = resource1[i];
res2 = resource2[i];
if (res1 instanceof VertexBuffer) {
const buffers = [res1.buffer, res2.buffer];
buffers[0].debug = 1;
buffers[1].debug = 2;
res1.initBufferIO(buffers);
}
else if (res1 instanceof ImageTexture) {
if (!res1.gpuResource)
res1.createGpuResource();
if (!res2.gpuResource)
res2.createGpuResource();
const textures = [res1.gpuResource, res2.gpuResource];
try {
textures[0].debug = 1;
textures[1].debug = 2;
}
catch (e) {
}
res1.initTextureIO(textures);
}
}
this.ioGroups = [group, this];
//this.ioGroups = [this, group];
this.debug = 1;
group.debug = 2;
return group;
}
/*
-------------OLD VERSION-----------
public createPingPongBindgroup(resource1: IShaderResource, resource2: IShaderResource): Bindgroup {
const group = new Bindgroup(this.name);
group.mustRefreshBindgroup = this.mustRefreshBindgroup = true;
group._layout = this.layout;
group.elements = this.getSwappedElements(resource1, resource2);
//console.log("=> group.elements ", group.elements)
if (resource1 instanceof VertexBuffer) {
const buffers = [resource1.buffer, (resource2 as VertexBuffer).buffer];
(buffers[0] as any).debug = 1;
(buffers[1] as any).debug = 2;
resource1.initBufferIO(buffers);
} else if (resource1 instanceof ImageTexture) {
if (!resource1.gpuResource) resource1.createGpuResource();
if (!resource2.gpuResource) resource2.createGpuResource();
//console.log(resource1, resource2)
//console.log(resource1.gpuResource === resource2.gpuResource)
const textures = [resource1.gpuResource, resource2.gpuResource];
try {
(textures[0] as any).debug = 1;
(textures[1] as any).debug = 2;
} catch (e) {
}
resource1.initTextureIO(textures);
}
this.ioGroups = [this, group];
//console.log(this.ioGroups)
this.debug = 1;
group.debug = 2;
return group;
}
*/
renderPipelineimageIO;
renderPipelineBufferIO;
handleRenderPipelineResourceIOs() {
//console.warn("handleRenderPipelineResourceIOs ", this.elements.length, this.textureIO)
if (this.renderPipelineimageIO) {
this.renderPipelineimageIO.initIO();
return;
}
else if (this.renderPipelineBufferIO) {
this.renderPipelineBufferIO.initIO();
return;
}
//a vertexBufferIO uses 2 vertexBuffers in a computePipeline
//but a single one is required in a renderPipeline (same for textures)
let resource;
let name;
let bufferIOs = [];
let textureIOs = [];
let parentResources = this.parent.resources;
let foundVertexIO = false;
let foundTextureIO = false;
for (let i = 0; i < this.elements.length; i++) {
//console.log(i, this.elements[i], this.parent.pipeline);
resource = this.elements[i].resource;
if (resource instanceof VertexBuffer) {
if (resource.io === 1) {
name = this.elements[i].name;
parentResources[name] = undefined;
parentResources[name + "_out"] = undefined;
bufferIOs.push(resource);
bufferIOs.push(this.elements[i + 1].resource);
this.elements.splice(i, 2);
foundVertexIO = true;
break;
}
}
else if (resource instanceof ImageTexture) {
if (resource.io === 1) {
name = this.elements[i].name;
parentResources[name] = undefined;
parentResources[name + "_out"] = undefined;
//console.log("this.elements[i + 1] = ", this.elements[i + 1])
textureIOs.push(resource);
textureIOs.push(this.elements[i + 1].resource);
this.elements.splice(i, 2);
foundTextureIO = true;
break;
}
}
}
if (foundVertexIO) {
//console.log("foundVertexIO")
//console.log("bufferIOs = ", bufferIOs)
const attributes = bufferIOs[0].attributeDescriptor;
const stepMode = bufferIOs[0].descriptor.stepMode;
const vb = new VertexBuffer(attributes, { stepMode });
this.elements.push({ name, resource: vb });
let vertexBuffers = parentResources.types.vertexBuffers;
let buffers = [];
for (let i = 0; i < vertexBuffers.length; i++) {
if (!vertexBuffers[i].resource.io) {
buffers.push(vertexBuffers[i]);
}
}
buffers.push({ name, resource: vb });
parentResources[name] = vb;
parentResources.types.vertexBuffers = buffers;
vb.initIO = () => {
vb.initBufferIO([bufferIOs[0].buffer, bufferIOs[1].buffer]);
};
vb.initIO();
this.renderPipelineBufferIO = vb;
}
else if (foundTextureIO) {
//console.log("foundTextureIO")
const img = new ImageTexture({ source: textureIOs[0].gpuResource });
this.elements.push({ name, resource: img });
let images = parentResources.types.imageTextures;
let textures = [];
for (let i = 0; i < images.length; i++) {
if (!images[i].resource.io) {
textures.push(images[i]);
}
}
textures.push({ name, resource: img });
parentResources[name] = img;
parentResources.types.imageTextures = images;
img.initIO = () => {
//console.log("initIO ", textureIOs[0].deviceId, textureIOs[1].deviceId)
img.source = textureIOs[0].texture;
img.initTextureIO([textureIOs[0].texture, textureIOs[1].texture]);
};
img.initIO();
this.renderPipelineimageIO = img;
}
}
ioGroups;
io_index = 0;
debug;
get pingPongBindgroup() {
return this._pingPongBindgroup;
}
get layout() {
if (!this._layout)
this.buildLayout();
return this._layout;
}
get group() {
if (!this._group || this.mustRefreshBindgroup) {
this.build();
}
if (this.ioGroups) {
const group = this.ioGroups[this.io_index++ % 2];
if (!group._group)
group.build();
return group._group;
}
return this._group;
}
update(pipeline) {
for (let i = 0; i < this.elements.length; i++) {
this.elements[i].resource.update(pipeline);
}
}
destroy() {
//console.log("bindgroup.destroy")
for (let i = 0; i < this.elements.length; i++) {
//console.log("this.elements[i] = ", this.elements[i].resource)
this.elements[i].resource.destroyGpuResource();
}
this.elements = [];
}
}