UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

1,128 lines (1,127 loc) 44.6 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { TRACEID_RENDER_QUEUE } from "../../../core/constants.js"; import { Debug, DebugHelper } from "../../../core/debug.js"; import { warnInsecureContext } from "../../../core/secure-context-warning.js"; import { PIXELFORMAT_RGBA8, PIXELFORMAT_BGRA8, DEVICETYPE_WEBGPU, BUFFERUSAGE_READ, BUFFERUSAGE_COPY_DST, semanticToLocation, PIXELFORMAT_SRGBA8, DISPLAYFORMAT_LDR_SRGB, PIXELFORMAT_SBGRA8, DISPLAYFORMAT_HDR, PIXELFORMAT_RGBA16F, UNUSED_UNIFORM_NAME, BUFFERUSAGE_INDIRECT } from "../constants.js"; import { BindGroupFormat } from "../bind-group-format.js"; import { BindGroup } from "../bind-group.js"; import { DebugGraphics } from "../debug-graphics.js"; import { GraphicsDevice } from "../graphics-device.js"; import { RenderTarget } from "../render-target.js"; import { StencilParameters } from "../stencil-parameters.js"; import { WebgpuBindGroup } from "./webgpu-bind-group.js"; import { WebgpuBindGroupFormat } from "./webgpu-bind-group-format.js"; import { WebgpuIndexBuffer } from "./webgpu-index-buffer.js"; import { WebgpuRenderPipeline } from "./webgpu-render-pipeline.js"; import { WebgpuComputePipeline } from "./webgpu-compute-pipeline.js"; import { WebgpuRenderTarget } from "./webgpu-render-target.js"; import { WebgpuShader } from "./webgpu-shader.js"; import { WebgpuTexture } from "./webgpu-texture.js"; import { WebgpuUniformBuffer } from "./webgpu-uniform-buffer.js"; import { WebgpuVertexBuffer } from "./webgpu-vertex-buffer.js"; import { WebgpuClearRenderer } from "./webgpu-clear-renderer.js"; import { WebgpuMipmapRenderer } from "./webgpu-mipmap-renderer.js"; import { WebgpuDebug } from "./webgpu-debug.js"; import { WebgpuDynamicBuffers } from "./webgpu-dynamic-buffers.js"; import { WebgpuGpuProfiler } from "./webgpu-gpu-profiler.js"; import { WebgpuResolver } from "./webgpu-resolver.js"; import { WebgpuCompute } from "./webgpu-compute.js"; import { WebgpuBuffer } from "./webgpu-buffer.js"; import { StorageBuffer } from "../storage-buffer.js"; import { WebgpuDrawCommands } from "./webgpu-draw-commands.js"; import { WebgpuUploadStream } from "./webgpu-upload-stream.js"; import { WebgpuXrBridge } from "./webgpu-xr-bridge.js"; const _uniqueLocations = /* @__PURE__ */ new Map(); const _indirectEntryByteSize = 5 * 4; const _indirectDispatchEntryByteSize = 3 * 4; class WebgpuGraphicsDevice extends GraphicsDevice { constructor(canvas, options = {}) { super(canvas, options); /** * Array of GPU resources pending destruction. Resources are destroyed after the current * command buffers are submitted to ensure they're not in use. * * @type {Array<GPUTexture|GPUBuffer|GPUQuerySet>} * @private */ __publicField(this, "_deferredDestroys", []); /** * Object responsible for caching and creation of render pipelines. */ __publicField(this, "renderPipeline", new WebgpuRenderPipeline(this)); /** * Object responsible for caching and creation of compute pipelines. */ __publicField(this, "computePipeline", new WebgpuComputePipeline(this)); /** * Buffer used to store arguments for indirect draw calls. * * @type {StorageBuffer|null} * @private */ __publicField(this, "_indirectDrawBuffer", null); /** * Number of indirect draw slots allocated. * * @private */ __publicField(this, "_indirectDrawBufferCount", 0); /** * Next unused index in indirectDrawBuffer. * * @private */ __publicField(this, "_indirectDrawNextIndex", 0); /** * Buffer used to store arguments for indirect dispatch calls. * * @type {StorageBuffer|null} * @private */ __publicField(this, "_indirectDispatchBuffer", null); /** * Number of indirect dispatch slots allocated. * * @private */ __publicField(this, "_indirectDispatchBufferCount", 0); /** * Next unused index in indirectDispatchBuffer. * * @private */ __publicField(this, "_indirectDispatchNextIndex", 0); /** * Object responsible for clearing the rendering surface by rendering a quad. * * @type { WebgpuClearRenderer } */ __publicField(this, "clearRenderer"); /** * Object responsible for mipmap generation. * * @type { WebgpuMipmapRenderer } */ __publicField(this, "mipmapRenderer"); /** * Render pipeline currently set on the device. * * @type {GPURenderPipeline|null} * @private */ __publicField(this, "pipeline", null); /** * An array of bind group formats, based on currently assigned bind groups * * @type {WebgpuBindGroupFormat[]} */ __publicField(this, "bindGroupFormats", []); /** * An empty bind group, used when the draw call is using a typical bind group layout based on * BINDGROUP_*** constants but some bind groups are not needed, for example clear renderer. * * @type {BindGroup} */ __publicField(this, "emptyBindGroup"); /** * Monotonically increasing counter incremented each time queue.submit() is called. * * @ignore */ __publicField(this, "submitVersion", 0); /** * When set, immersive XR writes color to this texture instead of the canvas swapchain. * @type {any} // `GPUTexture | null`; using `any` to avoid exporting WebGPU types in published typings. * @ignore */ __publicField(this, "xrColorTexture", null); /** * View format of {@link WebgpuGraphicsDevice#xrColorTexture} for render pass attachment views. * @type {any} // `GPUTextureFormat | null`; using `any` to avoid exporting WebGPU types in published typings. * @ignore */ __publicField(this, "xrColorTextureViewFormat", null); /** * Optional `GPUTextureViewDescriptor` describing how the framebuffer's color attachment view * should be created from {@link WebgpuGraphicsDevice#xrColorTexture}. Used to pick the right * array layer / mip when XR provides a layered (texture array) projection layer. Set per eye * by {@link FramePassMultiView}; cleared back to `null` outside the per-view loop. * * @type {any} // `GPUTextureViewDescriptor | null`; using `any` to avoid exporting WebGPU types in published typings. * @ignore */ __publicField(this, "xrColorTextureViewDescriptor", null); /** * Per-view XR sub-image entries populated each frame by the WebGPU XR bridge. Each entry * describes one XR view: the underlying GPU color texture, the view descriptor that selects the * right slice, the viewport, and the view's GPU format. Empty outside immersive WebGPU XR. * * @type {{ colorTexture: any, viewDescriptor: any, viewport: any, viewFormat: any }[]} * @ignore */ __publicField(this, "xrSubImages", []); /** * Active XR view index for the multi-view rendering wrapper, or `-1` when not iterating views. * Read by the forward renderer's per-view inner loop to render only the active eye. * * @type {number} * @ignore */ __publicField(this, "xrCurrentViewIndex", -1); /** * When set, used as the main color attachment in {@link WebgpuGraphicsDevice#frameStart} if there is * no XR color texture and no canvas {@link GPUCanvasContext#getCurrentTexture} (for example headless * or custom-surface hosts). Must be a WebGPU-backed {@link Texture}; {@link Texture#impl} must expose * {@link WebgpuTexture#gpuTexture}. * * @type {Texture|null} * @ignore */ __publicField(this, "externalBackbuffer", null); /** * Current command buffer encoder. * * @type {GPUCommandEncoder|null} * @private */ __publicField(this, "commandEncoder", null); /** * Command buffers scheduled for execution on the GPU. * * @type {GPUCommandBuffer[]} * @private */ __publicField(this, "commandBuffers", []); /** * @type {GPUSupportedLimits} * @private */ __publicField(this, "limits"); /** GLSL to SPIR-V transpiler */ __publicField(this, "glslang", null); /** SPIR-V to WGSL transpiler */ __publicField(this, "twgsl", null); options = this.initOptions; options.alpha = options.alpha ?? true; this.backBufferAntialias = options.antialias ?? false; this.isWebGPU = true; this._deviceType = DEVICETYPE_WEBGPU; this.featureLevel = options.featureLevel; this.scope.resolve(UNUSED_UNIFORM_NAME).setValue(0); } /** * Destroy the graphics device. */ destroy() { this.clearRenderer.destroy(); this.clearRenderer = null; this.mipmapRenderer.destroy(); this.mipmapRenderer = null; this.resolver.destroy(); this.resolver = null; this._clearXrState(); this.externalBackbuffer = null; super.destroy(); } /** * Reset all per-frame WebGPU XR render state to its inactive defaults. Called by the XR bridge * at the end of each XR frame and on session teardown, and by the graphics device on destroy. * * @ignore */ _clearXrState() { this.xrColorTexture = null; this.xrColorTextureViewFormat = null; this.xrColorTextureViewDescriptor = null; this.xrSubImages.length = 0; this.xrCurrentViewIndex = -1; } initDeviceCaps() { const limits = this.wgpu?.limits; this.limits = limits; this.precision = "highp"; this.maxPrecision = "highp"; this.maxSamples = 4; this.maxTextures = 16; this.maxTextureSize = limits.maxTextureDimension2D; this.maxCubeMapSize = limits.maxTextureDimension2D; this.maxVolumeSize = limits.maxTextureDimension3D; this.maxColorAttachments = limits.maxColorAttachments; this.maxPixelRatio = 1; this.maxAnisotropy = 16; this.fragmentUniformsCount = limits.maxUniformBufferBindingSize / 16; this.vertexUniformsCount = limits.maxUniformBufferBindingSize / 16; this.supportsUniformBuffers = true; this.supportsAreaLights = true; this.supportsGpuParticles = true; this.supportsCompute = true; this.textureFloatRenderable = true; this.textureHalfFloatRenderable = true; this.supportsImageBitmap = true; this.samples = this.backBufferAntialias ? 4 : 1; const wgslFeatures = window.navigator.gpu.wgslLanguageFeatures; this.supportsStorageTextureRead = wgslFeatures?.has("readonly_and_readwrite_storage_textures"); this.supportsSubgroupUniformity = wgslFeatures?.has("subgroup_uniformity"); this.supportsSubgroupId = wgslFeatures?.has("subgroup_id"); this.supportsLinearIndexing = wgslFeatures?.has("linear_indexing"); this.supportsUnrestrictedPointerParameters = wgslFeatures?.has("unrestricted_pointer_parameters"); this.supportsPointerCompositeAccess = wgslFeatures?.has("pointer_composite_access"); this.supportsPacked4x8IntegerDotProduct = wgslFeatures?.has("packed_4x8_integer_dot_product"); this.supportsTextureAndSamplerLet = wgslFeatures?.has("texture_and_sampler_let"); this.initCapsDefines(); } async initWebGpu(glslangUrl, twgslUrl) { if (!window.navigator.gpu) { warnInsecureContext("WebGPU"); throw new Error("Unable to retrieve GPU. Ensure you are using a browser that supports WebGPU rendering."); } Debug.log("WebgpuGraphicsDevice initialization .."); if (glslangUrl && twgslUrl) { const baseUrl = window.document?.baseURI ?? window.location.href; const buildUrl = (srcPath) => { return new URL(srcPath, baseUrl).toString(); }; const twgslScriptUrl = buildUrl(twgslUrl); const twgslWasmUrl = buildUrl(twgslUrl.replace(".js", ".wasm")); const glslangScriptUrl = buildUrl(glslangUrl); const results = await Promise.all([ import( /* @vite-ignore */ /* webpackIgnore: true */ `${twgslScriptUrl}` ).then(() => twgsl(twgslWasmUrl)), import( /* @vite-ignore */ /* webpackIgnore: true */ `${glslangScriptUrl}` ).then((module) => module.default()) ]); this.twgsl = results[0]; this.glslang = results[1]; } return this.createDevice(); } async createDevice() { const adapterOptions = { powerPreference: this.initOptions.powerPreference !== "default" ? this.initOptions.powerPreference : void 0, // Required for WebXR sessions using WebGPU xrCompatible: !!this.initOptions.xrCompatible }; this.gpuAdapter = await window.navigator.gpu.requestAdapter(adapterOptions); const featureLevel = this.initOptions.featureLevel; const bare = featureLevel === "bare"; const requiredFeatures = []; const requireFeature = bare ? () => false : (feature) => { const supported = this.gpuAdapter.features.has(feature); if (supported) { requiredFeatures.push(feature); } return supported; }; this.textureFloatFilterable = requireFeature("float32-filterable"); this.textureFloatBlendable = requireFeature("float32-blendable"); this.extCompressedTextureS3TC = requireFeature("texture-compression-bc"); this.extCompressedTextureS3TCSliced3D = requireFeature("texture-compression-bc-sliced-3d"); this.extCompressedTextureETC = requireFeature("texture-compression-etc2"); this.extCompressedTextureASTC = requireFeature("texture-compression-astc"); this.extCompressedTextureASTCSliced3D = requireFeature("texture-compression-astc-sliced-3d"); this.supportsTimestampQuery = requireFeature("timestamp-query"); this.supportsDepthClip = requireFeature("depth-clip-control"); this.supportsDepth32Stencil = requireFeature("depth32float-stencil8"); this.supportsIndirectFirstInstance = requireFeature("indirect-first-instance"); this.supportsShaderF16 = requireFeature("shader-f16"); this.supportsStorageRGBA8 = requireFeature("bgra8unorm-storage"); this.textureRG11B10Renderable = requireFeature("rg11b10ufloat-renderable"); this.supportsClipDistances = requireFeature("clip-distances"); this.supportsTextureFormatTier1 = requireFeature("texture-format-tier1"); this.supportsTextureFormatTier2 = requireFeature("texture-format-tier2"); this.supportsTextureFormatTier1 || (this.supportsTextureFormatTier1 = this.supportsTextureFormatTier2); this.supportsPrimitiveIndex = requireFeature("primitive-index"); this.supportsSubgroups = requireFeature("subgroups"); this.maxSubgroupSize = this.supportsSubgroups ? this.gpuAdapter?.info?.subgroupMaxSize ?? 0 : 0; this.minSubgroupSize = this.supportsSubgroups ? this.gpuAdapter?.info?.subgroupMinSize ?? 0 : 0; const wgslFeatureNames = window.navigator.gpu.wgslLanguageFeatures ? Array.from(window.navigator.gpu.wgslLanguageFeatures) : []; Debug.log( `WEBGPU${this.gpuAdapter?.info ? ` (${this.gpuAdapter.info.vendor || "?"} / ${this.gpuAdapter.info.architecture || this.gpuAdapter.info.device || "?"})` : ""} features [${bare ? "bare" : "full"}]: ${requiredFeatures.join(", ") || "none"}, wgslFeatures(${wgslFeatureNames.join(", ") || "none"})` ); const requiredLimits = {}; if (!bare) { const adapterLimits = this.gpuAdapter?.limits; if (adapterLimits) { for (const limitName in adapterLimits) { if (limitName === "minSubgroupSize" || limitName === "maxSubgroupSize") { continue; } requiredLimits[limitName] = adapterLimits[limitName]; } } } const deviceDescr = { requiredFeatures, requiredLimits, defaultQueue: { label: "Default Queue" } }; DebugHelper.setLabel(deviceDescr, "PlayCanvasWebGPUDevice"); this.wgpu = await this.gpuAdapter.requestDevice(deviceDescr); this.wgpu.lost?.then(this.handleDeviceLost.bind(this)); this.wgpu.addEventListener?.("uncapturederror", (ev) => { const e = ( /** @type {any} */ ev.error ); Debug.error(`WebGPU uncaptured ${e?.constructor?.name ?? "Error"}: ${e?.message ?? e}`); }); this.initDeviceCaps(); this.gpuContext = this.canvas.getContext("webgpu"); let canvasToneMapping = "standard"; let preferredCanvasFormat = window.navigator.gpu.getPreferredCanvasFormat(); const displayFormat = this.initOptions.displayFormat; this.backBufferFormat = preferredCanvasFormat === "rgba8unorm" ? displayFormat === DISPLAYFORMAT_LDR_SRGB ? PIXELFORMAT_SRGBA8 : PIXELFORMAT_RGBA8 : ( // (S)RGBA displayFormat === DISPLAYFORMAT_LDR_SRGB ? PIXELFORMAT_SBGRA8 : PIXELFORMAT_BGRA8 ); this.backBufferViewFormat = displayFormat === DISPLAYFORMAT_LDR_SRGB ? `${preferredCanvasFormat}-srgb` : preferredCanvasFormat; if (displayFormat === DISPLAYFORMAT_HDR && this.textureFloatFilterable) { const hdrMediaQuery = window.matchMedia("(dynamic-range: high)"); if (hdrMediaQuery?.matches) { this.backBufferFormat = PIXELFORMAT_RGBA16F; this.backBufferViewFormat = "rgba16float"; preferredCanvasFormat = "rgba16float"; this.isHdr = true; canvasToneMapping = "extended"; } } this.canvasConfig = { device: this.wgpu, colorSpace: "srgb", alphaMode: this.initOptions.alpha ? "premultiplied" : "opaque", // use preferred format for optimal performance on mobile format: preferredCanvasFormat, toneMapping: { mode: canvasToneMapping }, // RENDER_ATTACHMENT is required, COPY_SRC allows scene grab to copy out from it usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST, // formats that views created from textures returned by getCurrentTexture may use // (this allows us to view the preferred format as srgb) viewFormats: displayFormat === DISPLAYFORMAT_LDR_SRGB ? [this.backBufferViewFormat] : [] }; this.gpuContext?.configure(this.canvasConfig); this.createBackbuffer(); this.clearRenderer = new WebgpuClearRenderer(this); this.mipmapRenderer = new WebgpuMipmapRenderer(this); this.resolver = new WebgpuResolver(this); this.postInit(); return this; } async handleDeviceLost(info) { if (info.reason !== "destroyed") { Debug.warn(`WebGPU device was lost: ${info.message}, this needs to be handled`); super.loseContext(); this.fire("devicelost"); await this.createDevice(); super.restoreContext(); this.fire("devicerestored"); } } postInit() { super.postInit(); this.initializeRenderState(); this.setupPassEncoderDefaults(); this.gpuProfiler = new WebgpuGpuProfiler(this); this.dynamicBuffers = new WebgpuDynamicBuffers(this, 100 * 1024, this.limits.minUniformBufferOffsetAlignment); this.emptyBindGroup = new BindGroup(this, new BindGroupFormat(this, [])); this.emptyBindGroup.update(); } createBackbuffer() { this.supportsStencil = this.initOptions.stencil; this.backBuffer = new RenderTarget({ name: "WebgpuFramebuffer", graphicsDevice: this, depth: this.initOptions.depth, stencil: this.supportsStencil, samples: this.samples }); this.backBuffer.impl.isBackbuffer = true; } frameStart() { super.frameStart(); this.gpuProfiler.frameStart(); this.submit(); WebgpuDebug.memory(this); WebgpuDebug.validate(this); const outColorBuffer = this.xrColorTexture ?? this.gpuContext?.getCurrentTexture?.() ?? this.externalBackbuffer?.impl.gpuTexture; Debug.assert(outColorBuffer, "WebGPU frameStart requires an XR color texture, canvas swapchain texture, or externalBackbuffer."); DebugHelper.setLabel(outColorBuffer, `${this.backBuffer.name}`); if (this.backBufferSize.x !== outColorBuffer.width || this.backBufferSize.y !== outColorBuffer.height) { this.backBufferSize.set(outColorBuffer.width, outColorBuffer.height); this.backBuffer.destroy(); this.backBuffer = null; this.createBackbuffer(); } const rt = this.backBuffer; const wrt = rt.impl; const attachmentViewFormat = outColorBuffer === this.xrColorTexture && this.xrColorTextureViewFormat ? this.xrColorTextureViewFormat : this.backBufferViewFormat; wrt.setColorAttachment(0, void 0, attachmentViewFormat); rt._width = outColorBuffer.width; rt._height = outColorBuffer.height; this.initRenderTarget(rt); wrt.assignColorTexture(outColorBuffer, attachmentViewFormat); WebgpuDebug.end(this, "frameStart"); WebgpuDebug.end(this, "frameStart"); } frameEnd() { super.frameEnd(); this.gpuProfiler.frameEnd(); this.submit(); if (!this.contextLost) { this.gpuProfiler.request(); } this._indirectDrawNextIndex = 0; this._indirectDispatchNextIndex = 0; } createBufferImpl(usageFlags) { return new WebgpuBuffer(usageFlags); } createUniformBufferImpl(uniformBuffer) { return new WebgpuUniformBuffer(uniformBuffer); } createVertexBufferImpl(vertexBuffer, format, options) { return new WebgpuVertexBuffer(vertexBuffer, format, options); } createIndexBufferImpl(indexBuffer, options) { return new WebgpuIndexBuffer(indexBuffer, options); } createShaderImpl(shader) { return new WebgpuShader(shader); } createDrawCommandImpl(drawCommands) { return new WebgpuDrawCommands(this); } createTextureImpl(texture) { this.textures.add(texture); return new WebgpuTexture(texture); } createXrBridgeImpl(xrBridge) { return new WebgpuXrBridge(xrBridge); } createRenderTargetImpl(renderTarget) { return new WebgpuRenderTarget(renderTarget); } createUploadStreamImpl(uploadStream) { return new WebgpuUploadStream(uploadStream); } createBindGroupFormatImpl(bindGroupFormat) { return new WebgpuBindGroupFormat(bindGroupFormat); } createBindGroupImpl(bindGroup) { return new WebgpuBindGroup(); } createComputeImpl(compute) { return new WebgpuCompute(compute); } get indirectDrawBuffer() { this.allocateIndirectDrawBuffer(); return this._indirectDrawBuffer; } allocateIndirectDrawBuffer() { if (this._indirectDrawNextIndex === 0 && this._indirectDrawBufferCount < this.maxIndirectDrawCount) { this._indirectDrawBuffer?.destroy(); this._indirectDrawBuffer = null; } if (this._indirectDrawBuffer === null) { this._indirectDrawBuffer = new StorageBuffer(this, this.maxIndirectDrawCount * _indirectEntryByteSize, BUFFERUSAGE_INDIRECT | BUFFERUSAGE_COPY_DST); DebugHelper.setName(this._indirectDrawBuffer, "WebgpuGraphicsDevice.indirectDraw"); this._indirectDrawBufferCount = this.maxIndirectDrawCount; } } getIndirectDrawSlot(count = 1) { this.allocateIndirectDrawBuffer(); const slot = this._indirectDrawNextIndex; const nextIndex = this._indirectDrawNextIndex + count; Debug.assert(nextIndex <= this.maxIndirectDrawCount, `Insufficient indirect draw slots per frame (requested ${count}, currently ${nextIndex}), please adjust GraphicsDevice#maxIndirectDrawCount`); this._indirectDrawNextIndex = nextIndex; return slot; } get indirectDispatchBuffer() { this.allocateIndirectDispatchBuffer(); return this._indirectDispatchBuffer; } allocateIndirectDispatchBuffer() { if (this._indirectDispatchNextIndex === 0 && this._indirectDispatchBufferCount < this.maxIndirectDispatchCount) { this._indirectDispatchBuffer?.destroy(); this._indirectDispatchBuffer = null; } if (this._indirectDispatchBuffer === null) { this._indirectDispatchBuffer = new StorageBuffer(this, this.maxIndirectDispatchCount * _indirectDispatchEntryByteSize, BUFFERUSAGE_INDIRECT | BUFFERUSAGE_COPY_DST); DebugHelper.setName(this._indirectDispatchBuffer, "WebgpuGraphicsDevice.indirectDispatch"); this._indirectDispatchBufferCount = this.maxIndirectDispatchCount; } } getIndirectDispatchSlot(count = 1) { this.allocateIndirectDispatchBuffer(); const slot = this._indirectDispatchNextIndex; const nextIndex = this._indirectDispatchNextIndex + count; Debug.assert(nextIndex <= this.maxIndirectDispatchCount, `Insufficient indirect dispatch slots per frame (requested ${count}, currently ${nextIndex}), please adjust GraphicsDevice#maxIndirectDispatchCount`); this._indirectDispatchNextIndex = nextIndex; return slot; } /** * @param {number} index - Index of the bind group slot * @param {BindGroup} bindGroup - Bind group to attach * @param {number[]} [offsets] - Byte offsets for all uniform buffers in the bind group. */ setBindGroup(index, bindGroup, offsets) { if (this.passEncoder) { this.passEncoder.setBindGroup(index, bindGroup.impl.bindGroup, offsets ?? bindGroup.uniformBufferOffsets); this.bindGroupFormats[index] = bindGroup.format.impl; } } submitVertexBuffer(vertexBuffer, slot) { const format = vertexBuffer.format; const { interleaved, elements } = format; const elementCount = elements.length; const vbBuffer = vertexBuffer.impl.buffer; if (interleaved) { this.passEncoder.setVertexBuffer(slot, vbBuffer); return 1; } for (let i = 0; i < elementCount; i++) { this.passEncoder.setVertexBuffer(slot + i, vbBuffer, elements[i].offset); } return elementCount; } validateVBLocations(vb0, vb1) { const validateVB = (vb) => { const { elements } = vb.format; for (let i = 0; i < elements.length; i++) { const name = elements[i].name; const location = semanticToLocation[name]; if (_uniqueLocations.has(location)) { Debug.errorOnce(`Vertex buffer element location ${location} used by [${name}] is already used by element [${_uniqueLocations.get(location)}], while rendering [${DebugGraphics.toString()}]`); } _uniqueLocations.set(location, name); } }; validateVB(vb0); validateVB(vb1); _uniqueLocations.clear(); } draw(primitive, indexBuffer, numInstances = 1, drawCommands, first = true, last = true) { if (this.shader.ready && !this.shader.failed) { WebgpuDebug.validate(this); const passEncoder = this.passEncoder; Debug.assert(passEncoder); let pipeline = this.pipeline; const vb0 = this.vertexBuffers[0]; const vb1 = this.vertexBuffers[1]; if (first) { if (vb0) { const vbSlot = this.submitVertexBuffer(vb0, 0); if (vb1) { Debug.call(() => this.validateVBLocations(vb0, vb1)); this.submitVertexBuffer(vb1, vbSlot); } } Debug.call(() => this.validateAttributes(this.shader, vb0?.format, vb1?.format)); pipeline = this.renderPipeline.get( primitive, vb0?.format, vb1?.format, indexBuffer?.format, this.shader, this.renderTarget, this.bindGroupFormats, this.blendState, this.depthState, this.cullMode, this.stencilEnabled, this.stencilFront, this.stencilBack, this.frontFace ); Debug.assert(pipeline); if (this.pipeline !== pipeline) { this.pipeline = pipeline; passEncoder.setPipeline(pipeline); } } if (indexBuffer) { passEncoder.setIndexBuffer(indexBuffer.impl.buffer, indexBuffer.impl.format); } if (drawCommands) { const storage = drawCommands.impl?.storage ?? this.indirectDrawBuffer; const indirectBuffer = storage.impl.buffer; const drawsCount = drawCommands.count; for (let d = 0; d < drawsCount; d++) { const indirectOffset = (drawCommands.slotIndex + d) * _indirectEntryByteSize; if (indexBuffer) { passEncoder.drawIndexedIndirect(indirectBuffer, indirectOffset); } else { passEncoder.drawIndirect(indirectBuffer, indirectOffset); } } } else { if (indexBuffer) { passEncoder.drawIndexed(primitive.count, numInstances, primitive.base, primitive.baseVertex ?? 0, 0); } else { passEncoder.draw(primitive.count, numInstances, primitive.base, 0); } } this._drawCallsPerFrame++; if (drawCommands) { this._primsPerFrame[primitive.type] += drawCommands.primitiveCount; } else { const primCount = primitive.count * (numInstances > 1 ? numInstances : 1); this._primsPerFrame[primitive.type] += primCount; } WebgpuDebug.end(this, "Drawing", { vb0, vb1, indexBuffer, primitive, numInstances, pipeline }); } if (last) { this.clearVertexBuffer(); this.pipeline = null; } } setShader(shader, asyncCompile = false) { if (shader !== this.shader) { this.shader = shader; this._shaderSwitchesPerFrame++; } } setBlendState(blendState) { this.blendState.copy(blendState); } setDepthState(depthState) { this.depthState.copy(depthState); } setStencilState(stencilFront, stencilBack) { if (stencilFront || stencilBack) { this.stencilEnabled = true; this.stencilFront.copy(stencilFront ?? StencilParameters.DEFAULT); this.stencilBack.copy(stencilBack ?? StencilParameters.DEFAULT); const ref = this.stencilFront.ref; if (this.stencilRef !== ref) { this.stencilRef = ref; this.passEncoder.setStencilReference(ref); } } else { this.stencilEnabled = false; } } setBlendColor(r, g, b, a) { const c = this.blendColor; if (r !== c.r || g !== c.g || b !== c.b || a !== c.a) { c.set(r, g, b, a); this.passEncoder.setBlendConstant(c); } } setCullMode(cullMode) { this.cullMode = cullMode; } setFrontFace(frontFace) { this.frontFace = frontFace; } setAlphaToCoverage(state) { } initializeContextCaches() { super.initializeContextCaches(); } /** * Set up default values for the render pass encoder. */ setupPassEncoderDefaults() { this.pipeline = null; this.stencilRef = 0; this.blendColor.set(0, 0, 0, 0); } _uploadDirtyTextures() { this.texturesToUpload.forEach((texture) => { if (texture._needsUpload || texture._needsMipmapsUpload) { texture.upload(); } }); this.texturesToUpload.clear(); } setupTimeStampWrites(passDesc, name) { if (this.gpuProfiler._enabled) { if (this.gpuProfiler.timestampQueriesSet) { const slot = this.gpuProfiler.getSlot(name); if (slot === -1) { Debug.warnOnce("Too many GPU profiler slots allocated during the frame, ignoring timestamp writes"); } else { passDesc = passDesc ?? {}; passDesc.timestampWrites = { querySet: this.gpuProfiler.timestampQueriesSet.querySet, beginningOfPassWriteIndex: slot * 2, endOfPassWriteIndex: slot * 2 + 1 }; } } } return passDesc; } /** * Start a render pass. * * @param {RenderPass} renderPass - The render pass to start. * @ignore */ startRenderPass(renderPass) { this._uploadDirtyTextures(); WebgpuDebug.internal(this); WebgpuDebug.validate(this); const rt = renderPass.renderTarget || this.backBuffer; this.renderTarget = rt; Debug.assert(rt); const wrt = rt.impl; if (rt !== this.backBuffer) { this.initRenderTarget(rt); } wrt.setupForRenderPass(renderPass, rt); const renderPassDesc = wrt.renderPassDescriptor; this.setupTimeStampWrites(renderPassDesc, renderPass.name); const commandEncoder = this.getCommandEncoder(); this.passEncoder = commandEncoder.beginRenderPass(renderPassDesc); this.passEncoder.label = `${renderPass.name}-PassEncoder RT:${rt.name}`; DebugGraphics.pushGpuMarker(this, `Pass:${renderPass.name} RT:${rt.name}`); this.setupPassEncoderDefaults(); const { width, height } = rt; this.setViewport(0, 0, width, height); this.setScissor(0, 0, width, height); Debug.assert(!this.insideRenderPass, "RenderPass cannot be started while inside another render pass."); this.insideRenderPass = true; } /** * End a render pass. * * @param {RenderPass} renderPass - The render pass to end. * @ignore */ endRenderPass(renderPass) { DebugGraphics.popGpuMarker(this); this.passEncoder.end(); this.passEncoder = null; this.insideRenderPass = false; this.bindGroupFormats.length = 0; const target = this.renderTarget; if (target) { if (target.depthBuffer && renderPass.depthStencilOps.resolveDepth) { if (renderPass.samples > 1 && target.autoResolve) { const depthAttachment = target.impl.depthAttachment; const destTexture = target.depthBuffer.impl.gpuTexture; if (depthAttachment && destTexture) { this.resolver.resolveDepth(this.commandEncoder, depthAttachment.multisampledDepthBuffer, destTexture); } } } } for (let i = 0; i < renderPass.colorArrayOps.length; i++) { const colorOps = renderPass.colorArrayOps[i]; if (colorOps.genMipmaps) { this.mipmapRenderer.generate(renderPass.renderTarget._colorBuffers[i].impl); } } WebgpuDebug.end(this, "RenderPass", { renderPass }); WebgpuDebug.end(this, "RenderPass", { renderPass }); } startComputePass(name) { this._uploadDirtyTextures(); WebgpuDebug.internal(this); WebgpuDebug.validate(this); this.pipeline = null; const computePassDesc = this.setupTimeStampWrites(void 0, name); DebugHelper.setLabel(computePassDesc, `ComputePass-${name}`); const commandEncoder = this.getCommandEncoder(); this.passEncoder = commandEncoder.beginComputePass(computePassDesc); DebugHelper.setLabel(this.passEncoder, `ComputePass-${name}`); Debug.assert(!this.insideRenderPass, "ComputePass cannot be started while inside another pass."); this.insideRenderPass = true; } endComputePass() { this.passEncoder.end(); this.passEncoder = null; this.insideRenderPass = false; this.bindGroupFormats.length = 0; WebgpuDebug.end(this, "ComputePass"); WebgpuDebug.end(this, "ComputePass"); } computeDispatch(computes, name = "Unnamed") { this.startComputePass(name); for (let i = 0; i < computes.length; i++) { const compute = computes[i]; compute.applyParameters(); compute.impl.updateBindGroup(); } for (let i = 0; i < computes.length; i++) { const compute = computes[i]; compute.impl.dispatch(compute.countX, compute.countY, compute.countZ); } this.endComputePass(); } getCommandEncoder() { let commandEncoder = this.commandEncoder; if (!commandEncoder) { commandEncoder = this.wgpu.createCommandEncoder(); DebugHelper.setLabel(commandEncoder, "CommandEncoder-Shared"); this.commandEncoder = commandEncoder; } return commandEncoder; } endCommandEncoder() { Debug.assert(!this.insideRenderPass, 'Attempted to finish GPUCommandEncoder while inside a pass. This will invalidate the current pass encoder and cause "Parent encoder is already finished" validation errors.'); const { commandEncoder } = this; if (commandEncoder) { const cb = commandEncoder.finish(); DebugHelper.setLabel(cb, "CommandBuffer-Shared"); this.addCommandBuffer(cb); this.commandEncoder = null; } } addCommandBuffer(commandBuffer, front = false) { if (front) { this.commandBuffers.unshift(commandBuffer); } else { this.commandBuffers.push(commandBuffer); } } submit() { Debug.assert(!this.insideRenderPass, 'Attempted to submit command buffers while inside a pass. This finishes the parent command encoder and invalidates the active pass ("Parent encoder is already finished") .'); this.endCommandEncoder(); if (this.commandBuffers.length > 0) { this.dynamicBuffers.submit(); Debug.call(() => { if (this.commandBuffers.length > 0) { Debug.trace(TRACEID_RENDER_QUEUE, `SUBMIT (${this.commandBuffers.length})`); for (let i = 0; i < this.commandBuffers.length; i++) { Debug.trace(TRACEID_RENDER_QUEUE, ` CB: ${this.commandBuffers[i].label}`); } } }); this.wgpu.queue.submit(this.commandBuffers); this.commandBuffers.length = 0; this.submitVersion++; this.dynamicBuffers.onCommandBuffersSubmitted(); } const deferredDestroys = this._deferredDestroys; if (deferredDestroys.length > 0) { for (let i = 0; i < deferredDestroys.length; i++) { deferredDestroys[i].destroy(); } deferredDestroys.length = 0; } } /** * Defer destruction of a GPU resource until after the current command buffers are submitted. * This ensures the resource is not destroyed while still referenced by pending GPU commands. * * @param {GPUTexture|GPUBuffer|GPUQuerySet} gpuResource - The GPU resource to destroy. * @private */ deferDestroy(gpuResource) { if (gpuResource) { this._deferredDestroys.push(gpuResource); } } clear(options) { if (options.flags) { this.clearRenderer.clear(this, this.renderTarget, options, this.defaultClearOptions); } } setViewport(x, y, w, h) { if (this.passEncoder) { if (this.xrColorTexture) { return; } if (!this.renderTarget.flipY) { y = this.renderTarget.height - y - h; } this.vx = x; this.vy = y; this.vw = w; this.vh = h; this.passEncoder.setViewport(x, y, w, h, 0, 1); } } setScissor(x, y, w, h) { if (this.passEncoder) { if (this.xrColorTexture) { return; } if (!this.renderTarget.flipY) { y = this.renderTarget.height - y - h; } this.sx = x; this.sy = y; this.sw = w; this.sh = h; this.passEncoder.setScissorRect(x, y, w, h); } } /** * Clear the content of a storage buffer to 0. * * @param {WebgpuBuffer} storageBuffer - The storage buffer. * @param {number} [offset] - The offset of data to clear. Defaults to 0. * @param {number} [size] - The size of data to clear. Defaults to the full size of the buffer. * @ignore */ clearStorageBuffer(storageBuffer, offset = 0, size = storageBuffer.byteSize) { const commandEncoder = this.getCommandEncoder(); commandEncoder.clearBuffer(storageBuffer.buffer, offset, size); } /** * Read a content of a storage buffer. * * @param {WebgpuBuffer} storageBuffer - The storage buffer. * @param {number} [offset] - The byte offset of data to read. Defaults to 0. * @param {number} [size] - The byte size of data to read. Defaults to the full size of the * buffer minus the offset. * @param {ArrayBufferView} [data] - Typed array to populate with the data read from the storage * buffer. When typed array is supplied, enough space needs to be reserved, otherwise only * partial data is copied. If not specified, the data is returned in an Uint8Array. Defaults to * null. * @param {boolean} [immediate] - If true, the read operation will be executed as soon as * possible. This has a performance impact, so it should be used only when necessary. Defaults * to false. * @returns {Promise<ArrayBufferView>} A promise that resolves with the data read from the storage * buffer. * @ignore */ readStorageBuffer(storageBuffer, offset = 0, size = storageBuffer.byteSize - offset, data = null, immediate = false) { const stagingBuffer = this.createBufferImpl(BUFFERUSAGE_READ | BUFFERUSAGE_COPY_DST); stagingBuffer.allocate(this, size); const destBuffer = stagingBuffer.buffer; const commandEncoder = this.getCommandEncoder(); commandEncoder.copyBufferToBuffer(storageBuffer.buffer, offset, destBuffer, 0, size); return this.readBuffer(stagingBuffer, size, data, immediate); } readBuffer(stagingBuffer, size, data = null, immediate = false) { const destBuffer = stagingBuffer.buffer; return new Promise((resolve, reject) => { const read = () => { destBuffer?.mapAsync(GPUMapMode.READ).then(() => { data ?? (data = new Uint8Array(size)); const copySrc = destBuffer.getMappedRange(0, size); const srcType = data.constructor; data.set(new srcType(copySrc)); destBuffer.unmap(); stagingBuffer.destroy(this); resolve(data); }); }; if (immediate) { this.submit(); read(); } else { setTimeout(() => { read(); }); } }); } /** * Issues a write operation of the provided data into a storage buffer. * * @param {WebgpuBuffer} storageBuffer - The storage buffer. * @param {number} bufferOffset - The offset in bytes to start writing to the storage buffer. * @param {ArrayBufferView} data - The data to write to the storage buffer. * @param {number} dataOffset - Offset in data to begin writing from. Given in elements if data * is a TypedArray and bytes otherwise. * @param {number} size - Size of content to write from data to buffer. Given in elements if * data is a TypedArray and bytes otherwise. */ writeStorageBuffer(storageBuffer, bufferOffset = 0, data, dataOffset = 0, size) { Debug.assert(storageBuffer.buffer); Debug.assert(data); this.wgpu.queue.writeBuffer(storageBuffer.buffer, bufferOffset, data, dataOffset, size); } /** * Copies source render target into destination render target. Mostly used by post-effects. * * @param {RenderTarget} [source] - The source render target. Defaults to frame buffer. * @param {RenderTarget} [dest] - The destination render target. Defaults to frame buffer. * @param {boolean} [color] - If true, will copy the color buffer. Defaults to false. * @param {boolean} [depth] - If true, will copy the depth buffer. Defaults to false. * @returns {boolean} True if the copy was successful, false otherwise. */ copyRenderTarget(source, dest, color, depth) { const copySize = { width: source ? source.width : dest.width, height: source ? source.height : dest.height, depthOrArrayLayers: 1 }; const commandEncoder = this.getCommandEncoder(); DebugGraphics.pushGpuMarker(this, "COPY-RT"); if (color) { const copySrc = { texture: source ? source.colorBuffer.impl.gpuTexture : this.backBuffer.impl.assignedColorTexture, mipLevel: source ? source.mipLevel : 0 }; const copyDst = { texture: dest ? dest.colorBuffer.impl.gpuTexture : this.backBuffer.impl.assignedColorTexture, mipLevel: dest ? dest.mipLevel : 0 }; Debug.assert(copySrc.texture !== null && copyDst.texture !== null); commandEncoder.copyTextureToTexture(copySrc, copyDst, copySize); } if (depth) { const sourceRT = source ? source : this.renderTarget; const sourceTexture = sourceRT.impl.depthAttachment.depthTexture; const sourceMipLevel = sourceRT.mipLevel; if (source.samples > 1) { const destTexture = dest.colorBuffer.impl.gpuTexture; this.resolver.resolveDepth(commandEncoder, sourceTexture, destTexture); } else { const destTexture = dest ? dest.depthBuffer.impl.gpuTexture : this.renderTarget.impl.depthAttachment.depthTexture; const destMipLevel = dest ? dest.mipLevel : this.renderTarget.mipLevel; const copySrc = { texture: sourceTexture, mipLevel: sourceMipLevel }; const copyDst = { texture: destTexture, mipLevel: destMipLevel }; Debug.assert(copySrc.texture !== null && copyDst.texture !== null); commandEncoder.copyTextureToTexture(copySrc, copyDst, copySize); } } DebugGraphics.popGpuMarker(this); return true; } get hasTranspilers() { return this.glslang && this.twgsl; } pushMarker(name) { this.passEncoder?.pushDebugGroup(name); } popMarker() { this.passEncoder?.popDebugGroup(); } } export { WebgpuGraphicsDevice };