UNPKG

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

621 lines (620 loc) 27.7 kB
// 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 { Bindgroup } from "../shader/Bindgroup"; import { FragmentShader } from "../shader/FragmentShader"; import { ShaderStruct } from "../shader/shaderParts/ShaderStruct"; import { VertexShader } from "../shader/VertexShader"; import { Pipeline } from "./Pipeline"; import { DepthStencilTexture } from "./resources/textures/DepthStencilTexture"; import { MultiSampleTexture } from "./resources/textures/MultiSampleTexture"; import { RenderPassTexture } from "./resources/textures/RenderPassTexture"; import { Bindgroups } from "../shader/Bindgroups"; import { DrawConfig } from "./resources/DrawConfig"; import { HighLevelParser } from "../HighLevelParser"; import { VertexShaderDebuggerPipeline } from "./VertexShaderDebuggerPipeline"; export class RenderPipeline extends Pipeline { static ON_ADDED_TO_RENDERER = "ON_ADDED_TO_RENDERER"; static ON_REMOVED_FROM_RENDERER = "ON_REMOVED_FROM_RENDERER"; static ON_DRAW_BEGIN = "ON_DRAW_BEGIN"; static ON_DRAW_END = "ON_DRAW_END"; static ON_DRAW = "ON_DRAW"; static ON_GPU_PIPELINE_BUILT = "ON_GPU_PIPELINE_BUILT"; static ON_LOG = "ON_LOG"; static ON_VERTEX_SHADER_CODE_BUILT = "ON_VERTEX_SHADER_CODE_BUILT"; static ON_FRAGMENT_SHADER_CODE_BUILT = "ON_FRAGMENT_SHADER_CODE_BUILT"; static ON_INIT_FROM_OBJECT = "ON_INIT_FROM_OBJECT"; static ON_DEVICE_LOST = "ON_DEVICE_LOST"; static ON_UPDATE_RESOURCES = "ON_UPDATE_RESOURCES"; _renderer; get renderer() { return this._renderer; } set renderer(renderer) { if (this._renderer != renderer) { this._renderer = renderer; if (renderer) { if (this.waitingMultisampleTexture) { this.setupMultiSampleView(this.multiSampleTextureDescriptor); this.waitingMultisampleTexture = false; } if (this.waitingDepthStencilTexture) { this.setupDepthStencilView(this.depthStencilTextureDescriptor); this.waitingDepthStencilTexture = false; } //console.log("dispatch") this.dispatchEvent(RenderPipeline.ON_ADDED_TO_RENDERER); } else { this.dispatchEvent(RenderPipeline.ON_REMOVED_FROM_RENDERER); } } } drawConfig; multiSampleTextureDescriptor; waitingMultisampleTexture = false; multisampleTexture; waitingDepthStencilTexture = false; depthStencilTextureDescriptor; _depthStencilTexture; renderPassTexture; outputColor; renderPassDescriptor = { colorAttachments: [] }; vertexShaderDebuggerPipeline = null; gpuPipeline; debug = "renderPipeline"; constructor(bgColor = { r: 0, g: 0, b: 0, a: 1 }) { super(); this.type = "render"; this.drawConfig = new DrawConfig(this); this.vertexShader = new VertexShader(); this.fragmentShader = new FragmentShader(); this.description.primitive = { topology: "triangle-list", cullMode: "none", frontFace: "ccw" }; if (bgColor !== null) { this.outputColor = this.createColorAttachment(bgColor); } } /* public get canvas(): any { if (!this.renderer) return null; return this.renderer.canvas; } */ get depthStencilTexture() { return this._depthStencilTexture; } destroy() { this.bindGroups.destroy(); if (this.multisampleTexture) this.multisampleTexture.destroy(); if (this.renderPassTexture) this.renderPassTexture.destroyGpuResource(); if (this.depthStencilTexture) this.depthStencilTexture.destroy(); for (let z in this.description) this.description[z] = null; for (let z in this) { try { this[z].destroy(); } catch (e) { try { this[z].destroyGpuResource(); } catch (e) { } } this[z] = null; } } initFromObject(descriptor) { this._resources = {}; this.vertexShader = null; this.fragmentShader = null; this.gpuPipeline = null; this.bindGroups.destroy(); this.bindGroups = new Bindgroups(this, "pipeline"); descriptor = HighLevelParser.parse(descriptor, "render", this.drawConfig); super.initFromObject(descriptor); if (!descriptor.cullMode) this.description.primitive.cullMode = "none"; else this.description.primitive.cullMode = descriptor.cullMode; if (!descriptor.topology) this.description.primitive.topology = "triangle-list"; else { this.description.primitive.topology = descriptor.topology; if (descriptor.topology === "line-strip" || descriptor.topology === "triangle-strip") { if (!descriptor.stripIndexFormat) { throw new Error("You must define a 'stripIndexFormat' in order to use a topology 'triangle-strip' or 'line-strip'. See https://www.w3.org/TR/webgpu/#enumdef-gpuindexformat for more details"); } else { this.description.primitive.stripIndexFormat = descriptor.stripIndexFormat; } } } if (!descriptor.frontFace) this.description.primitive.frontFace = "ccw"; else this.description.primitive.frontFace = descriptor.frontFace; if (descriptor.indexBuffer) { this.drawConfig.indexBuffer = descriptor.indexBuffer; } if (this.outputColor) { if (descriptor.clearColor) this.outputColor.clearValue = descriptor.clearColor; else descriptor.clearColor = this.outputColor.clearValue; } if (descriptor.blendMode) this.blendMode = descriptor.blendMode; if (descriptor.antiAliasing) this.setupMultiSampleView(); if (descriptor.useDepthTexture) { let depthTextureSize = 1024; if (descriptor.depthTextureSize) depthTextureSize = descriptor.depthTextureSize; this.setupDepthStencilView({ size: [depthTextureSize, depthTextureSize, 1], format: "depth32float", usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING }); } else if (descriptor.depthTest) this.setupDepthStencilView(); if (descriptor.bindgroups) { let group; let resourcesGroups = []; let k = 0; for (let z in descriptor.bindgroups) { if (descriptor.bindgroups[z] instanceof Bindgroup) { const elements = descriptor.bindgroups[z].elements; const resources = []; for (let i = 0; i < elements.length; i++) { resources[i] = elements[i].resource; } resourcesGroups[k++] = resources; descriptor.bindgroups[z].name = z; this.bindGroups.add(descriptor.bindgroups[z]); } else { group = new Bindgroup(); group.name = z; const g = group.initFromObject(descriptor.bindgroups[z]); resourcesGroups[k++] = g; this.bindGroups.add(group); } } if (descriptor.bindgroups.default) { if (descriptor.bindgroups.default.buffer) { const attributes = descriptor.bindgroups.default.buffer.attributes; for (let z in attributes) { if (descriptor[z]) descriptor[z] = attributes[z]; } } } } const createArrayOfObjects = (obj) => { const result = []; let o; for (let z in obj) { o = obj[z]; result.push({ name: z, ...o }); } return result; }; this.vertexShader = new VertexShader(); if (typeof descriptor.vertexShader === "string") { this.vertexShader.main.text = descriptor.vertexShader; } else { if (descriptor.vertexShader instanceof VertexShader) { this.vertexShader = descriptor.vertexShader; } else { if (descriptor.vertexShader.constants) this.vertexShader.constants.text = descriptor.vertexShader.constants; this.vertexShader.main.text = descriptor.vertexShader.main; } this.vertexShader.inputs = createArrayOfObjects(descriptor.vertexShader.inputs); this.vertexShader.outputs = createArrayOfObjects(descriptor.vertexShader.outputs); } if (descriptor.fragmentShader) { this.fragmentShader = new FragmentShader(); if (typeof descriptor.fragmentShader === "string") { this.fragmentShader.main.text = descriptor.fragmentShader; } else { if (descriptor.fragmentShader instanceof FragmentShader) { this.fragmentShader = descriptor.fragmentShader; } else { if (descriptor.fragmentShader.constants) this.fragmentShader.constants.text = descriptor.fragmentShader.constants; this.fragmentShader.main.text = descriptor.fragmentShader.main; } this.fragmentShader.inputs = createArrayOfObjects(descriptor.fragmentShader.inputs); ; this.fragmentShader.outputs = createArrayOfObjects(descriptor.fragmentShader.outputs); ; } } this.dispatchEvent(RenderPipeline.ON_INIT_FROM_OBJECT, descriptor); return descriptor; } _clearValue = null; get clearValue() { return this._clearValue; //if (!this.renderPassDescriptor.colorAttachment) return null; //return this.renderPassDescriptor.colorAttachment.clearValue; } createColorAttachment(rgba, view = undefined) { const colorAttachment = { view: view, clearValue: rgba, loadOp: "clear", storeOp: "store" }; this.renderPassDescriptor.colorAttachments.push(colorAttachment); return colorAttachment; } //----------------- setupDraw(o) { if (o.instanceCount !== undefined) this.drawConfig.instanceCount = o.instanceCount; if (o.vertexCount !== undefined) this.drawConfig.vertexCount = o.vertexCount; if (o.firstVertexId !== undefined) this.drawConfig.firstVertexId = o.firstVertexId; if (o.firstInstanceId !== undefined) this.drawConfig.firstInstanceId = o.firstInstanceId; if (o.indexBuffer !== undefined) this.drawConfig.indexBuffer = o.indexBuffer; if (o.baseVertex !== undefined) this.drawConfig.baseVertex = o.baseVertex; } get debugVertexCount() { return this.resources.debugVertexCount; } set debugVertexCount(n) { this.resources.debugVertexCount = n; } get vertexCount() { return this.drawConfig.vertexCount; } set vertexCount(n) { this.drawConfig.vertexCount = n; } get instanceCount() { return this.drawConfig.instanceCount; } set instanceCount(n) { this.drawConfig.instanceCount = n; } get firstVertexId() { return this.drawConfig.firstVertexId; } set firstVertexId(n) { this.drawConfig.firstVertexId = n; } get firstInstanceId() { return this.drawConfig.firstInstanceId; } set firstInstanceId(n) { this.drawConfig.firstInstanceId = n; } get baseVertex() { return this.drawConfig.baseVertex; } set baseVertex(n) { this.drawConfig.baseVertex = n; } //------------------------------------------------ setupMultiSampleView(descriptor) { if (!this.renderer) { this.waitingMultisampleTexture = true; this.multiSampleTextureDescriptor = descriptor; return; } if (this.multisampleTexture) this.multisampleTexture.destroy(); if (!descriptor) descriptor = {}; if (!descriptor.size) descriptor.size = [this.renderer.width, this.renderer.height]; this.multisampleTexture = new MultiSampleTexture(descriptor); this.description.multisample = { count: this.multisampleTexture.description.count }; if (this._depthStencilTexture) { this.renderPassDescriptor.description.sampleCount = 4; this._depthStencilTexture.create(); } } //--------------------------- setupDepthStencilView(descriptor, depthStencilDescription, depthStencilAttachmentOptions) { if (!this.renderer) { this.waitingDepthStencilTexture = true; this.depthStencilTextureDescriptor = descriptor; return; } if (!depthStencilAttachmentOptions) depthStencilAttachmentOptions = {}; if (!descriptor) descriptor = {}; if (!descriptor.size) descriptor.size = [this.renderer.width, this.renderer.height]; if (this.multisampleTexture) descriptor.sampleCount = 4; else descriptor.sampleCount = 1; if (this._depthStencilTexture) this._depthStencilTexture.destroy(); this._depthStencilTexture = new DepthStencilTexture(descriptor, depthStencilDescription, depthStencilAttachmentOptions); this.renderPassDescriptor.depthStencilAttachment = this.depthStencilTexture.attachment; this.description.depthStencil = this.depthStencilTexture.description; } //---------------------------------------- get renderPassView() { return this.renderPass.view; } get renderPass() { if (!this.renderPassTexture) { this.renderPassTexture = new RenderPassTexture(this); } return this.renderPassTexture; } get useRenderPassTexture() { return !!this.renderPassTexture; } cleanInputs( /*initIO: boolean = false*/) { const _inputs = []; const t = this.vertexShader.inputs; for (let z in t) _inputs.push({ name: z, ...t[z] }); this.vertexShader.inputs = _inputs; return _inputs; } blendMode; getFragmentShaderColorOptions() { const o = { format: XGPU.getPreferredCanvasFormat(), }; if (this.blendMode) o.blend = this.blendMode; return o; } rebuildingAfterDeviceLost = false; onRebuildStartAfterDeviceLost; clearAfterDeviceLostAndRebuild() { this.dispatchEvent(RenderPipeline.ON_DEVICE_LOST); if (this.onRebuildStartAfterDeviceLost) this.onRebuildStartAfterDeviceLost(); this.gpuPipeline = null; if (this.drawConfig.indexBuffer) this.drawConfig.indexBuffer.createGpuResource(); if (this.multisampleTexture) this.multisampleTexture.resize(this.renderer.width, this.renderer.height); if (this.depthStencilTexture) this.depthStencilTexture.resize(this.renderer.width, this.renderer.height); if (this.renderPassTexture) this.renderPassTexture.resize(this.renderer.width, this.renderer.height); this.rebuildingAfterDeviceLost = true; super.clearAfterDeviceLostAndRebuild(); } buildingPipeline = false; buildGpuPipeline() { if (this.gpuPipeline || this.buildingPipeline) return this.gpuPipeline; this.buildingPipeline = true; //console.warn("BUILD GPUPIPELINE") this.bindGroups.handleRenderPipelineResourceIOs(); this.initPipelineResources(this); const o = this.bindGroups.build(); if (o.description.layout) this.description.layout = o.description.layout; else this.description.layout = "auto"; if (!this.rebuildingAfterDeviceLost) { const buffers = o.buffers; this.description.vertex = o.description.vertex; //setup vertexShader inputs ------ const vertexInput = new ShaderStruct("Input", this.cleanInputs()); ; if (buffers.length) { let buffer; let arrays; let builtin = 0; for (let i = 0; i < buffers.length; i++) { buffer = buffers[i]; arrays = buffer.vertexArrays; for (let j = 0; j < arrays.length; j++) { vertexInput.addProperty({ name: arrays[j].name, type: arrays[j].varType, builtin: "@location(" + builtin + ")" }); builtin++; } } } //--------------------------------- const vertexShader = this.vertexShader.build(this, vertexInput); this.dispatchEvent(RenderPipeline.ON_VERTEX_SHADER_CODE_BUILT, vertexShader); let fragmentShader; if (this.fragmentShader) { fragmentShader = this.fragmentShader.build(this, vertexShader.output.getInputFromOutput()); this.dispatchEvent(RenderPipeline.ON_FRAGMENT_SHADER_CODE_BUILT, fragmentShader); } this.description.vertex = { code: vertexShader.code, entryPoint: "main", buffers: o.description.vertex.buffers }; if (this.fragmentShader) { this.description.fragment = { code: fragmentShader.code, entryPoint: "main", targets: [ this.getFragmentShaderColorOptions() ] }; } } this.description.vertex.module = XGPU.device.createShaderModule({ code: this.description.vertex.code }); if (this.description.fragment) { this.description.fragment.module = XGPU.device.createShaderModule({ code: this.description.fragment.code }); } this.rebuildingAfterDeviceLost = false; this.gpuPipeline = XGPU.createRenderPipeline(this.description); let resources = this.bindGroups.resources.types; for (let type in resources) { resources[type].forEach((o) => { if (o.resource.gpuResource) { o.resource.gpuResource.label = o.name; } }); } if (this.resources.__DEBUG__) { this.vertexShaderDebuggerPipeline = new VertexShaderDebuggerPipeline(); this.vertexShaderDebuggerPipeline.init(this, this.debugVertexCount); this.vertexShaderDebuggerPipeline.onLog = (o) => { this.dispatchEvent(RenderPipeline.ON_LOG, o); //this._onLog(o); }; } this.buildingPipeline = false; this.dispatchEvent(RenderPipeline.ON_GPU_PIPELINE_BUILT); return this.gpuPipeline; } //------------------------------------------- clearOpReady = false; rendererUseSinglePipeline = true; beginRenderPass(commandEncoder, outputView, drawCallId, usingRenderPassTexture = false) { if (!this.resourceDefined) return null; if (this.vertexShaderDebuggerPipeline) this.vertexShaderDebuggerPipeline.nextFrame(); this.dispatchEvent(RenderPipeline.ON_DRAW_BEGIN); if (usingRenderPassTexture) { //console.log("usingRenderPassTexture") this.renderPassDescriptor.colorAttachments[0].loadOp = "clear"; } else { if (this.renderPassDescriptor.colorAttachments[0]) { this._clearValue = this.renderPassDescriptor.colorAttachments[0].clearValue; } //console.log("not usingRenderPassTexture") let rendererUseSinglePipeline = this.renderer.renderPipelines.length == 1 && this.pipelineCount === 1; if (this.rendererUseSinglePipeline !== rendererUseSinglePipeline) { this.clearOpReady = false; this.rendererUseSinglePipeline = rendererUseSinglePipeline; } if (this.clearOpReady === false && this.renderPassDescriptor.colorAttachments[0] || this.pipelineCount > 1) { this.clearOpReady = true; /*if (this.useRenderPassTexture && this.renderPassDescriptor.colorAttachments[0]) { this.renderPassDescriptor.colorAttachments[0].loadOp = "clear" } else {*/ if (rendererUseSinglePipeline && this.pipelineCount == 1) this.renderPassDescriptor.colorAttachments[0].loadOp = "clear"; else { if (this.pipelineCount === 1) { if (this.renderer.renderPipelines[0] === this) this.renderPassDescriptor.colorAttachments[0].loadOp = "clear"; else this.renderPassDescriptor.colorAttachments[0].loadOp = "load"; } else { this.renderPassDescriptor.colorAttachments[0].loadOp = "clear"; if (drawCallId === 0) { } else this.renderPassDescriptor.colorAttachments[0].loadOp = "load"; } } //} } } if (!this.gpuPipeline) this.buildGpuPipeline(); if (outputView && this.outputColor) this.handleOutputColor(outputView); return commandEncoder.beginRenderPass(this.renderPassDescriptor); } handleOutputColor(outputView) { if (this.outputColor) { if (this.multisampleTexture) { if (!this.multisampleTexture.view) this.multisampleTexture.create(); this.outputColor.view = this.multisampleTexture.view; if (this.multisampleTexture.resolveTarget) this.outputColor.resolveTarget = this.multisampleTexture.resolveTarget; else this.outputColor.resolveTarget = outputView; } else { this.outputColor.view = outputView; } } } //---------------------------------------------------------------------- update() { //console.log("renderPipeline.update start gpuPipeline = ", this.gpuPipeline) if (!this.gpuPipeline) return; if (this.renderPassTexture) this.renderPassTexture.update(); this.bindGroups.update(); this.dispatchEvent(RenderPipeline.ON_UPDATE_RESOURCES); //console.log("renderPipeline.update end") } draw(renderPass) { if (!this.resourceDefined) return; //console.log("draw") renderPass.setPipeline(this.gpuPipeline); this.bindGroups.apply(renderPass); } //------------------------------- end(commandEncoder, renderPass) { if (!this.resourceDefined) return; renderPass.end(); //console.log("end") //------ the arrays of textures may contains GPUTexture so I must use commandEncoder.copyTextureToTexture // to update the content from a GPUTexture to the texture_array_2d const types = this.bindGroups.resources.types; if (types) { if (!types.textureArrays) { let textureArrays = []; if (types.imageTextureArrays) textureArrays = textureArrays.concat(types.imageTextureArrays); if (types.cubeMapTextureArrays) textureArrays = textureArrays.concat(types.cubeMapTextureArrays); if (types.cubeMapTexture) textureArrays = textureArrays.concat(types.cubeMapTexture); types.textureArrays = textureArrays; } for (let i = 0; i < types.textureArrays.length; i++) { types.textureArrays[i].resource.updateInnerGpuTextures(commandEncoder); } } //---------------------------------------------------------------------------------------- const { width, height } = this.renderer; if (this.renderer.resized) { if (this.multisampleTexture) { this.multisampleTexture.resize(width, height); } if (this.depthStencilTexture) { this.depthStencilTexture.resize(width, height); } if (this.renderPassTexture) { this.renderPassTexture.resize(width, height); } } if (this.renderPassTexture && this.renderPassTexture.mustUseCopyTextureToTexture) { if (!this.renderPassTexture.gpuResource) this.renderPassTexture.createGpuResource(); commandEncoder.copyTextureToTexture({ texture: this.renderer.texture }, { texture: this.renderPassTexture.gpuResource }, [width, height]); } if (this.multisampleTexture) this.multisampleTexture.update(); if (this.depthStencilTexture) this.depthStencilTexture.update(); if (this.renderPassTexture) this.renderPassTexture.update(); this.dispatchEvent(RenderPipeline.ON_DRAW_END); } get resourceDefined() { const bool = !!this.bindGroups.resources.all; if (!bool) { //some very basic shader can run without any resource if (this.drawConfig.vertexCount > 0) { if (this.vertexShader.main.text != "" && this.fragmentShader.main.text != "") { return true; } } return false; } return true; } get pipeline() { return this.gpuPipeline; } get cullMode() { return this.description.primitive.cullMode; } set cullMode(s) { this.description.primitive.cullMode = s; } get topology() { return this.description.primitive.topology; } set topology(s) { this.description.primitive.topology = s; } get frontFace() { return this.description.primitive.frontFace; } set frontFace(s) { this.description.primitive.frontFace = s; } get stripIndexFormat() { return this.description.primitive.stripIndexFormat; } set stripIndexFormat(s) { this.description.primitive.stripIndexFormat = s; } }