UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

632 lines (629 loc) 24 kB
import { DEVICETYPE_WEBGPU, UNUSED_UNIFORM_NAME, PIXELFORMAT_SRGBA8, PIXELFORMAT_RGBA8, PIXELFORMAT_SBGRA8, PIXELFORMAT_BGRA8, DISPLAYFORMAT_LDR_SRGB, DISPLAYFORMAT_HDR, PIXELFORMAT_RGBA16F, BUFFERUSAGE_INDIRECT, BUFFERUSAGE_COPY_DST, BUFFERUSAGE_READ, semanticToLocation } from '../constants.js'; import { BindGroupFormat } from '../bind-group-format.js'; import { BindGroup } from '../bind-group.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 { 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'; const _uniqueLocations = new Map(); const _indirectEntryByteSize = 5 * 4; class WebgpuGraphicsDevice extends GraphicsDevice { constructor(canvas, options = {}){ super(canvas, options), this.renderPipeline = new WebgpuRenderPipeline(this), this.computePipeline = new WebgpuComputePipeline(this), this._indirectDrawBuffer = null, this._indirectDrawBufferCount = 0, this._indirectDrawNextIndex = 0, this.pipeline = null, this.bindGroupFormats = [], this.commandEncoder = null, this.commandBuffers = [], this.glslang = null, this.twgsl = null; options = this.initOptions; options.alpha = options.alpha ?? true; this.backBufferAntialias = options.antialias ?? false; this.isWebGPU = true; this._deviceType = DEVICETYPE_WEBGPU; this.scope.resolve(UNUSED_UNIFORM_NAME).setValue(0); } destroy() { this.clearRenderer.destroy(); this.clearRenderer = null; this.mipmapRenderer.destroy(); this.mipmapRenderer = null; this.resolver.destroy(); this.resolver = null; super.destroy(); } 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.initCapsDefines(); } async initWebGpu(glslangUrl, twgslUrl) { if (!window.navigator.gpu) { throw new Error('Unable to retrieve GPU. Ensure you are using a browser that supports WebGPU rendering.'); } if (glslangUrl && twgslUrl) { const buildUrl = (srcPath)=>{ return new URL(srcPath, window.location.href).toString(); }; const results = await Promise.all([ import(/* @vite-ignore */ /* webpackIgnore: true */ `${buildUrl(twgslUrl)}`).then((module)=>twgsl(twgslUrl.replace('.js', '.wasm'))), import(/* @vite-ignore */ /* webpackIgnore: true */ `${buildUrl(glslangUrl)}`).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 : undefined, xrCompatible: this.initOptions.xrCompatible }; this.gpuAdapter = await window.navigator.gpu.requestAdapter(adapterOptions); const requiredFeatures = []; const requireFeature = (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.extCompressedTextureETC = requireFeature('texture-compression-etc2'); this.extCompressedTextureASTC = requireFeature('texture-compression-astc'); 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'); const adapterLimits = this.gpuAdapter?.limits; const requiredLimits = {}; 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' } }; this.wgpu = await this.gpuAdapter.requestDevice(deviceDescr); this.wgpu.lost?.then(this.handleDeviceLost.bind(this)); 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 : 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', format: preferredCanvasFormat, toneMapping: { mode: canvasToneMapping }, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST, 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') { super.loseContext(); await this.createDevice(); super.restoreContext(); } } 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(); const outColorBuffer = this.gpuContext?.getCurrentTexture?.() ?? this.externalBackbuffer?.impl.gpuTexture; 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; wrt.setColorAttachment(0, undefined, this.backBufferViewFormat); this.initRenderTarget(rt); wrt.assignColorTexture(this, outColorBuffer); } frameEnd() { super.frameEnd(); this.gpuProfiler.frameEnd(); this.submit(); if (!this.contextLost) { this.gpuProfiler.request(); } this._indirectDrawNextIndex = 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); } createTextureImpl(texture) { return new WebgpuTexture(texture); } createRenderTargetImpl(renderTarget) { return new WebgpuRenderTarget(renderTarget); } 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 * 4, BUFFERUSAGE_INDIRECT | BUFFERUSAGE_COPY_DST); this._indirectDrawBufferCount = this.maxIndirectDrawCount; } } getIndirectDrawSlot() { this.allocateIndirectDrawBuffer(); const slot = this._indirectDrawNextIndex; this._indirectDrawNextIndex++; return slot; } 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)) ; _uniqueLocations.set(location, name); } }; validateVB(vb0); validateVB(vb1); _uniqueLocations.clear(); } draw(primitive, indexBuffer, numInstances = 1, indirectSlot, first = true, last = true) { if (this.shader.ready && !this.shader.failed) { const passEncoder = this.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) { this.submitVertexBuffer(vb1, vbSlot); } } 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); if (this.pipeline !== pipeline) { this.pipeline = pipeline; passEncoder.setPipeline(pipeline); } } if (indexBuffer) { passEncoder.setIndexBuffer(indexBuffer.impl.buffer, indexBuffer.impl.format); } if (indirectSlot !== undefined) { const indirectOffset = indirectSlot * _indirectEntryByteSize; const indirectBuffer = this.indirectDrawBuffer.impl.buffer; 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); } } } if (last) { this.clearVertexBuffer(); this.pipeline = null; } } setShader(shader, asyncCompile = false) { if (shader !== this.shader) { this.shader = shader; } } 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; } setAlphaToCoverage(state) {} initializeContextCaches() { super.initializeContextCaches(); } setupPassEncoderDefaults() { this.pipeline = null; this.stencilRef = 0; this.blendColor.set(0, 0, 0, 0); } _uploadDirtyTextures() { this.textures.forEach((texture)=>{ if (texture._needsUpload || texture._needsMipmaps) { texture.upload(); } }); } setupTimeStampWrites(passDesc, name) { if (this.gpuProfiler._enabled) { if (this.gpuProfiler.timestampQueriesSet) { const slot = this.gpuProfiler.getSlot(name); passDesc = passDesc ?? {}; passDesc.timestampWrites = { querySet: this.gpuProfiler.timestampQueriesSet.querySet, beginningOfPassWriteIndex: slot * 2, endOfPassWriteIndex: slot * 2 + 1 }; } } return passDesc; } startRenderPass(renderPass) { this._uploadDirtyTextures(); const rt = renderPass.renderTarget || this.backBuffer; this.renderTarget = 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}`; this.setupPassEncoderDefaults(); const { width, height } = rt; this.setViewport(0, 0, width, height); this.setScissor(0, 0, width, height); this.insideRenderPass = true; } endRenderPass(renderPass) { 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); } } } startComputePass(name) { this.pipeline = null; const computePassDesc = this.setupTimeStampWrites(undefined, name); const commandEncoder = this.getCommandEncoder(); this.passEncoder = commandEncoder.beginComputePass(computePassDesc); this.insideRenderPass = true; } endComputePass() { this.passEncoder.end(); this.passEncoder = null; this.insideRenderPass = false; this.bindGroupFormats.length = 0; } 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(); this.commandEncoder = commandEncoder; } return commandEncoder; } endCommandEncoder() { const { commandEncoder } = this; if (commandEncoder) { const cb = commandEncoder.finish(); this.addCommandBuffer(cb); this.commandEncoder = null; } } addCommandBuffer(commandBuffer, front = false) { if (front) { this.commandBuffers.unshift(commandBuffer); } else { this.commandBuffers.push(commandBuffer); } } submit() { this.endCommandEncoder(); if (this.commandBuffers.length > 0) { this.dynamicBuffers.submit(); this.wgpu.queue.submit(this.commandBuffers); this.commandBuffers.length = 0; this.dynamicBuffers.onCommandBuffersSubmitted(); } } clear(options) { if (options.flags) { this.clearRenderer.clear(this, this.renderTarget, options, this.defaultClearOptions); } } setViewport(x, y, w, h) { if (this.passEncoder) { 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.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); } } clearStorageBuffer(storageBuffer, offset = 0, size = storageBuffer.byteSize) { const commandEncoder = this.getCommandEncoder(); commandEncoder.clearBuffer(storageBuffer.buffer, offset, size); } 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 ??= 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(); }); } }); } writeStorageBuffer(storageBuffer, bufferOffset = 0, data, dataOffset = 0, size) { this.wgpu.queue.writeBuffer(storageBuffer.buffer, bufferOffset, data, dataOffset, size); } 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(); if (color) { const copySrc = { texture: source ? source.colorBuffer.impl.gpuTexture : this.backBuffer.impl.assignedColorTexture, mipLevel: 0 }; const copyDst = { texture: dest ? dest.colorBuffer.impl.gpuTexture : this.backBuffer.impl.assignedColorTexture, mipLevel: 0 }; commandEncoder.copyTextureToTexture(copySrc, copyDst, copySize); } if (depth) { const sourceRT = source ? source : this.renderTarget; const sourceTexture = sourceRT.impl.depthAttachment.depthTexture; 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 copySrc = { texture: sourceTexture, mipLevel: 0 }; const copyDst = { texture: destTexture, mipLevel: 0 }; commandEncoder.copyTextureToTexture(copySrc, copyDst, copySize); } } return true; } } export { WebgpuGraphicsDevice };