UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

900 lines (897 loc) 41.5 kB
import { TRACEID_RENDER_QUEUE } from '../../../core/constants.js'; import { DebugHelper, Debug } from '../../../core/debug.js'; import { BUFFERUSAGE_READ, BUFFERUSAGE_COPY_DST, DEVICETYPE_WEBGPU, UNUSED_UNIFORM_NAME, semanticToLocation, PIXELFORMAT_SRGBA8, PIXELFORMAT_RGBA8, PIXELFORMAT_SBGRA8, PIXELFORMAT_BGRA8, DISPLAYFORMAT_LDR_SRGB, DISPLAYFORMAT_HDR, PIXELFORMAT_RGBA16F } 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'; function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } function _async_to_generator(fn) { return function() { var self = this, args = arguments; return new Promise(function(resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } /** * @import { BindGroup } from '../bind-group.js' * @import { RenderPass } from '../render-pass.js' * @import { WebgpuBuffer } from './webgpu-buffer.js' */ var _uniqueLocations = new Map(); class WebgpuGraphicsDevice extends GraphicsDevice { /** * Destroy the graphics device. */ destroy() { this.clearRenderer.destroy(); this.clearRenderer = null; this.mipmapRenderer.destroy(); this.mipmapRenderer = null; this.resolver.destroy(); this.resolver = null; super.destroy(); } initDeviceCaps() { var _this_wgpu; var limits = (_this_wgpu = this.wgpu) == null ? void 0 : _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; // WebGPU currently only supports 1 and 4 samples this.samples = this.backBufferAntialias ? 4 : 1; // WGSL features var wgslFeatures = navigator.gpu.wgslLanguageFeatures; this.supportsStorageTextureRead = wgslFeatures == null ? void 0 : wgslFeatures.has('readonly_and_readwrite_storage_textures'); this.initCapsDefines(); } initWebGpu(glslangUrl, twgslUrl) { var _this = this; return _async_to_generator(function*() { if (!window.navigator.gpu) { throw new Error('Unable to retrieve GPU. Ensure you are using a browser that supports WebGPU rendering.'); } // temporary message to confirm Webgpu is being used Debug.log('WebgpuGraphicsDevice initialization ..'); // build a full URL from a relative or absolute path var buildUrl = (srcPath)=>{ return new URL(srcPath, window.location.href).toString(); }; var results = yield 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]; // create the device return _this.createDevice(); })(); } createDevice() { var _this = this; return _async_to_generator(function*() { var _this_gpuAdapter, // handle lost device _this_wgpu_lost; /** @type {GPURequestAdapterOptions} */ var adapterOptions = { powerPreference: _this.initOptions.powerPreference !== 'default' ? _this.initOptions.powerPreference : undefined }; /** * @type {GPUAdapter} * @private */ _this.gpuAdapter = yield window.navigator.gpu.requestAdapter(adapterOptions); // request optional features var requiredFeatures = []; var requireFeature = (feature)=>{ var 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'); Debug.log("WEBGPU features: " + requiredFeatures.join(', ')); // copy all adapter limits to the requiredLimits object - to created a device with the best feature sets available var adapterLimits = (_this_gpuAdapter = _this.gpuAdapter) == null ? void 0 : _this_gpuAdapter.limits; var requiredLimits = {}; if (adapterLimits) { for(var limitName in adapterLimits){ // skip these as they fail on Windows Chrome and are not part of spec currently if (limitName === 'minSubgroupSize' || limitName === 'maxSubgroupSize') { continue; } requiredLimits[limitName] = adapterLimits[limitName]; } } /** @type {GPUDeviceDescriptor} */ var deviceDescr = { requiredFeatures, requiredLimits, defaultQueue: { label: 'Default Queue' } }; DebugHelper.setLabel(deviceDescr, 'PlayCanvasWebGPUDevice'); /** * @type {GPUDevice} * @private */ _this.wgpu = yield _this.gpuAdapter.requestDevice(deviceDescr); (_this_wgpu_lost = _this.wgpu.lost) == null ? void 0 : _this_wgpu_lost.then(_this.handleDeviceLost.bind(_this)); _this.initDeviceCaps(); _this.gpuContext = _this.canvas.getContext('webgpu'); // tonemapping, used when the backbuffer is HDR var canvasToneMapping = 'standard'; // pixel format of the framebuffer that is the most efficient one on the system var preferredCanvasFormat = navigator.gpu.getPreferredCanvasFormat(); // display format the user asked for var displayFormat = _this.initOptions.displayFormat; // combine requested display format with the preferred format _this.backBufferFormat = preferredCanvasFormat === 'rgba8unorm' ? displayFormat === DISPLAYFORMAT_LDR_SRGB ? PIXELFORMAT_SRGBA8 : PIXELFORMAT_RGBA8 : displayFormat === DISPLAYFORMAT_LDR_SRGB ? PIXELFORMAT_SBGRA8 : PIXELFORMAT_BGRA8; // (S)BGRA // view format for the backbuffer. Backbuffer is always allocated without srgb conversion, and // the view we create specifies srgb is needed to handle the conversion. _this.backBufferViewFormat = displayFormat === DISPLAYFORMAT_LDR_SRGB ? "" + preferredCanvasFormat + "-srgb" : preferredCanvasFormat; // optional HDR display format if (displayFormat === DISPLAYFORMAT_HDR && _this.textureFloatFilterable) { // if supported by the system var hdrMediaQuery = window.matchMedia('(dynamic-range: high)'); if (hdrMediaQuery == null ? void 0 : hdrMediaQuery.matches) { // configure the backbuffer to be 16 bit float _this.backBufferFormat = PIXELFORMAT_RGBA16F; _this.backBufferViewFormat = 'rgba16float'; preferredCanvasFormat = 'rgba16float'; _this.isHdr = true; // use extended tonemapping for HDR to avoid clipping canvasToneMapping = 'extended'; } } /** * Configuration of the main colorframebuffer we obtain using getCurrentTexture * * @type {GPUCanvasConfiguration} * @private */ _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; })(); } handleDeviceLost(info) { var _this = this, _superprop_get_loseContext = ()=>super.loseContext, _superprop_get_restoreContext = ()=>super.restoreContext; return _async_to_generator(function*() { // reason is 'destroyed' if we intentionally destroy the device if (info.reason !== 'destroyed') { Debug.warn("WebGPU device was lost: " + info.message + ", this needs to be handled"); _superprop_get_loseContext().call(_this); // 'super' works correctly here yield _this.createDevice(); // Ensure this method is defined in your class _superprop_get_restoreContext().call(_this); // 'super' works correctly here } })(); } postInit() { super.postInit(); this.initializeRenderState(); this.setupPassEncoderDefaults(); this.gpuProfiler = new WebgpuGpuProfiler(this); // init dynamic buffer using 100kB allocation this.dynamicBuffers = new WebgpuDynamicBuffers(this, 100 * 1024, this.limits.minUniformBufferOffsetAlignment); // empty bind group 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(); // submit any commands collected before the frame rendering this.submit(); WebgpuDebug.memory(this); WebgpuDebug.validate(this); // current frame color output buffer var outColorBuffer = this.gpuContext.getCurrentTexture(); DebugHelper.setLabel(outColorBuffer, "" + this.backBuffer.name); // reallocate framebuffer if dimensions change, to match the output texture 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(); } var rt = this.backBuffer; var wrt = rt.impl; // assign the format, allowing following init call to use it to allocate matching multisampled buffer wrt.setColorAttachment(0, undefined, this.backBufferViewFormat); this.initRenderTarget(rt); // assign current frame's render texture wrt.assignColorTexture(this, outColorBuffer); WebgpuDebug.end(this, 'frameStart'); WebgpuDebug.end(this, 'frameStart'); } frameEnd() { super.frameEnd(); this.gpuProfiler.frameEnd(); // submit scheduled command buffers this.submit(); if (!this.contextLost) { this.gpuProfiler.request(); } } 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); } /** * @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) { // TODO: this condition should be removed, it's here to handle fake grab pass, which should be refactored instead if (this.passEncoder) { // set it on the device this.passEncoder.setBindGroup(index, bindGroup.impl.bindGroup, offsets != null ? offsets : bindGroup.uniformBufferOffsets); // store the active formats, used by the pipeline creation this.bindGroupFormats[index] = bindGroup.format.impl; } } submitVertexBuffer(vertexBuffer, slot) { var format = vertexBuffer.format; var { interleaved, elements } = format; var elementCount = elements.length; var vbBuffer = vertexBuffer.impl.buffer; if (interleaved) { // for interleaved buffers, we use a single vertex buffer, and attributes are specified using the layout this.passEncoder.setVertexBuffer(slot, vbBuffer); return 1; } // non-interleaved - vertex buffer per attribute for(var i = 0; i < elementCount; i++){ this.passEncoder.setVertexBuffer(slot + i, vbBuffer, elements[i].offset); } return elementCount; } validateVBLocations(vb0, vb1) { // in case of multiple VBs, validate all elements use unique locations var validateVB = (vb)=>{ var { elements } = vb.format; for(var i = 0; i < elements.length; i++){ var name = elements[i].name; var 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, numInstances, keepBuffers) { if (numInstances === void 0) numInstances = 1; if (this.shader.ready && !this.shader.failed) { WebgpuDebug.validate(this); var passEncoder = this.passEncoder; Debug.assert(passEncoder); // vertex buffers var vb0 = this.vertexBuffers[0]; var vb1 = this.vertexBuffers[1]; if (vb0) { var vbSlot = this.submitVertexBuffer(vb0, 0); if (vb1) { Debug.call(()=>this.validateVBLocations(vb0, vb1)); this.submitVertexBuffer(vb1, vbSlot); } } Debug.call(()=>this.validateAttributes(this.shader, vb0 == null ? void 0 : vb0.format, vb1 == null ? void 0 : vb1.format)); // render pipeline var pipeline = this.renderPipeline.get(primitive, vb0 == null ? void 0 : vb0.format, vb1 == null ? void 0 : vb1.format, this.shader, this.renderTarget, this.bindGroupFormats, this.blendState, this.depthState, this.cullMode, this.stencilEnabled, this.stencilFront, this.stencilBack); Debug.assert(pipeline); if (this.pipeline !== pipeline) { this.pipeline = pipeline; passEncoder.setPipeline(pipeline); } // draw var ib = this.indexBuffer; if (ib) { passEncoder.setIndexBuffer(ib.impl.buffer, ib.impl.format); passEncoder.drawIndexed(primitive.count, numInstances, primitive.base, 0, 0); } else { passEncoder.draw(primitive.count, numInstances, primitive.base, 0); } WebgpuDebug.end(this, 'Drawing', { vb0, vb1, ib, primitive, numInstances, pipeline }); } this.vertexBuffers.length = 0; this.indexBuffer = null; } setShader(shader, asyncCompile) { if (shader !== this.shader) { this.shader = shader; // TODO: we should probably track other stats instead, like pipeline switches 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 != null ? stencilFront : StencilParameters.DEFAULT); this.stencilBack.copy(stencilBack != null ? stencilBack : StencilParameters.DEFAULT); // ref value - based on stencil front var ref = this.stencilFront.ref; if (this.stencilRef !== ref) { this.stencilRef = ref; this.passEncoder.setStencilReference(ref); } } else { this.stencilEnabled = false; } } setBlendColor(r, g, b, a) { var 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(); } /** * 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.textures.forEach((texture)=>{ if (texture._needsUpload || texture._needsMipmaps) { texture.upload(); } }); } setupTimeStampWrites(passDesc, name) { if (this.gpuProfiler._enabled) { if (this.gpuProfiler.timestampQueriesSet) { var slot = this.gpuProfiler.getSlot(name); passDesc = passDesc != null ? 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) { // upload textures that need it, to avoid them being uploaded / their mips generated during the pass // TODO: this needs a better solution this._uploadDirtyTextures(); WebgpuDebug.internal(this); WebgpuDebug.validate(this); var rt = renderPass.renderTarget || this.backBuffer; this.renderTarget = rt; Debug.assert(rt); /** @type {WebgpuRenderTarget} */ var wrt = rt.impl; // framebuffer is initialized at the start of the frame if (rt !== this.backBuffer) { this.initRenderTarget(rt); } // set up clear / store / load settings wrt.setupForRenderPass(renderPass, rt); var renderPassDesc = wrt.renderPassDescriptor; // timestamp this.setupTimeStampWrites(renderPassDesc, renderPass.name); // start the pass var commandEncoder = this.getCommandEncoder(); this.passEncoder = commandEncoder.beginRenderPass(renderPassDesc); this.passEncoder.label = renderPass.name + "-PassEncoder RT:" + rt.name; // push marker to the passEncoder DebugGraphics.pushGpuMarker(this, "Pass:" + renderPass.name + " RT:" + rt.name); this.setupPassEncoderDefaults(); // the pass always clears full target // TODO: avoid this setting the actual viewport/scissor on webgpu as those are automatically reset to full // render target. We just need to update internal state, for the get functionality to return it. var { 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) { // pop the marker from the passEncoder DebugGraphics.popGpuMarker(this); // end the render pass this.passEncoder.end(); this.passEncoder = null; this.insideRenderPass = false; // each render pass can use different number of bind groups this.bindGroupFormats.length = 0; // resolve depth if needed after the pass has finished var target = this.renderTarget; if (target) { // resolve depth buffer (stencil resolve is not yet implemented) if (target.depthBuffer && renderPass.depthStencilOps.resolveDepth) { if (renderPass.samples > 1 && target.autoResolve) { var depthAttachment = target.impl.depthAttachment; var destTexture = target.depthBuffer.impl.gpuTexture; if (depthAttachment && destTexture) { this.resolver.resolveDepth(this.commandEncoder, depthAttachment.multisampledDepthBuffer, destTexture); } } } } // generate mipmaps using the same command buffer encoder for(var i = 0; i < renderPass.colorArrayOps.length; i++){ var 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) { WebgpuDebug.internal(this); WebgpuDebug.validate(this); // clear cached encoder state this.pipeline = null; // timestamp var computePassDesc = this.setupTimeStampWrites(undefined, name); // start the pass var 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() { // end the compute pass this.passEncoder.end(); this.passEncoder = null; this.insideRenderPass = false; // each render pass can use different number of bind groups this.bindGroupFormats.length = 0; WebgpuDebug.end(this, 'ComputePass'); WebgpuDebug.end(this, 'ComputePass'); } computeDispatch(computes, name) { if (name === void 0) name = 'Unnamed'; this.startComputePass(name); // update uniform buffers and bind groups for(var i = 0; i < computes.length; i++){ var compute = computes[i]; compute.applyParameters(); compute.impl.updateBindGroup(); } // dispatch for(var i1 = 0; i1 < computes.length; i1++){ var compute1 = computes[i1]; compute1.impl.dispatch(compute1.countX, compute1.countY, compute1.countZ); } this.endComputePass(); } getCommandEncoder() { // use existing or create new encoder var commandEncoder = this.commandEncoder; if (!commandEncoder) { commandEncoder = this.wgpu.createCommandEncoder(); DebugHelper.setLabel(commandEncoder, 'CommandEncoder-Shared'); this.commandEncoder = commandEncoder; } return commandEncoder; } endCommandEncoder() { var { commandEncoder } = this; if (commandEncoder) { var cb = commandEncoder.finish(); DebugHelper.setLabel(cb, 'CommandBuffer-Shared'); this.addCommandBuffer(cb); this.commandEncoder = null; } } addCommandBuffer(commandBuffer, front) { if (front === void 0) front = false; if (front) { this.commandBuffers.unshift(commandBuffer); } else { this.commandBuffers.push(commandBuffer); } } submit() { // end the current encoder this.endCommandEncoder(); if (this.commandBuffers.length > 0) { // copy dynamic buffers data to the GPU (this schedules the copy CB to run before all other CBs) this.dynamicBuffers.submit(); // trace all scheduled command buffers Debug.call(()=>{ if (this.commandBuffers.length > 0) { Debug.trace(TRACEID_RENDER_QUEUE, "SUBMIT (" + this.commandBuffers.length + ")"); for(var 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; // notify dynamic buffers this.dynamicBuffers.onCommandBuffersSubmitted(); } } clear(options) { if (options.flags) { this.clearRenderer.clear(this, this.renderTarget, options, this.defaultClearOptions); } } setViewport(x, y, w, h) { // TODO: only execute when it changes. Also, the viewport of encoder matches the rendering attachments, // so we can skip this if fullscreen // TODO: this condition should be removed, it's here to handle fake grab pass, which should be refactored instead 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) { // TODO: only execute when it changes. Also, the viewport of encoder matches the rendering attachments, // so we can skip this if fullscreen // TODO: this condition should be removed, it's here to handle fake grab pass, which should be refactored instead 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); } } /** * 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, size) { if (offset === void 0) offset = 0; if (size === void 0) size = storageBuffer.byteSize; var 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, size, data, immediate) { if (offset === void 0) offset = 0; if (size === void 0) size = storageBuffer.byteSize - offset; if (data === void 0) data = null; if (immediate === void 0) immediate = false; // create a temporary staging buffer var stagingBuffer = this.createBufferImpl(BUFFERUSAGE_READ | BUFFERUSAGE_COPY_DST); stagingBuffer.allocate(this, size); var destBuffer = stagingBuffer.buffer; // copy the GPU buffer to the staging buffer var commandEncoder = this.getCommandEncoder(); commandEncoder.copyBufferToBuffer(storageBuffer.buffer, offset, destBuffer, 0, size); return this.readBuffer(stagingBuffer, size, data, immediate); } readBuffer(stagingBuffer, size, data, immediate) { if (data === void 0) data = null; if (immediate === void 0) immediate = false; var destBuffer = stagingBuffer.buffer; // return a promise that resolves with the data return new Promise((resolve, reject)=>{ var read = ()=>{ destBuffer == null ? void 0 : destBuffer.mapAsync(GPUMapMode.READ).then(()=>{ // copy data to a buffer data != null ? data : data = new Uint8Array(size); var copySrc = destBuffer.getMappedRange(0, size); // use the same type as the target var srcType = data.constructor; data.set(new srcType(copySrc)); // release staging buffer destBuffer.unmap(); stagingBuffer.destroy(this); resolve(data); }); }; if (immediate) { // submit the command buffer immediately this.submit(); read(); } else { // map the buffer during the next event handling cycle, when the command buffer is submitted 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, data, dataOffset, size) { if (bufferOffset === void 0) bufferOffset = 0; if (dataOffset === void 0) dataOffset = 0; 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) { /** @type {GPUExtent3D} */ var copySize = { width: source ? source.width : dest.width, height: source ? source.height : dest.height, depthOrArrayLayers: 1 }; var commandEncoder = this.getCommandEncoder(); DebugGraphics.pushGpuMarker(this, 'COPY-RT'); if (color) { // read from supplied render target, or from the framebuffer /** @type {GPUImageCopyTexture} */ var copySrc = { texture: source ? source.colorBuffer.impl.gpuTexture : this.backBuffer.impl.assignedColorTexture, mipLevel: 0 }; // write to supplied render target, or to the framebuffer /** @type {GPUImageCopyTexture} */ var copyDst = { texture: dest ? dest.colorBuffer.impl.gpuTexture : this.backBuffer.impl.assignedColorTexture, mipLevel: 0 }; Debug.assert(copySrc.texture !== null && copyDst.texture !== null); commandEncoder.copyTextureToTexture(copySrc, copyDst, copySize); } if (depth) { // read from supplied render target, or from the framebuffer var sourceRT = source ? source : this.renderTarget; var sourceTexture = sourceRT.impl.depthAttachment.depthTexture; if (source.samples > 1) { // resolve the depth to a color buffer of destination render target var destTexture = dest.colorBuffer.impl.gpuTexture; this.resolver.resolveDepth(commandEncoder, sourceTexture, destTexture); } else { // write to supplied render target, or to the framebuffer var destTexture1 = dest ? dest.depthBuffer.impl.gpuTexture : this.renderTarget.impl.depthAttachment.depthTexture; /** @type {GPUImageCopyTexture} */ var copySrc1 = { texture: sourceTexture, mipLevel: 0 }; /** @type {GPUImageCopyTexture} */ var copyDst1 = { texture: destTexture1, mipLevel: 0 }; Debug.assert(copySrc1.texture !== null && copyDst1.texture !== null); commandEncoder.copyTextureToTexture(copySrc1, copyDst1, copySize); } } DebugGraphics.popGpuMarker(this); return true; } pushMarker(name) { var _this_passEncoder; (_this_passEncoder = this.passEncoder) == null ? void 0 : _this_passEncoder.pushDebugGroup(name); } popMarker() { var _this_passEncoder; (_this_passEncoder = this.passEncoder) == null ? void 0 : _this_passEncoder.popDebugGroup(); } constructor(canvas, options = {}){ super(canvas, options), /** * Object responsible for caching and creation of render pipelines. */ this.renderPipeline = new WebgpuRenderPipeline(this), /** * Object responsible for caching and creation of compute pipelines. */ this.computePipeline = new WebgpuComputePipeline(this), /** * An array of bind group formats, based on currently assigned bind groups * * @type {WebgpuBindGroupFormat[]} */ this.bindGroupFormats = [], /** * Current command buffer encoder. * * @type {GPUCommandEncoder|null} * @private */ this.commandEncoder = null, /** * Command buffers scheduled for execution on the GPU. * * @type {GPUCommandBuffer[]} * @private */ this.commandBuffers = []; options = this.initOptions; var _options_alpha; // alpha defaults to true options.alpha = (_options_alpha = options.alpha) != null ? _options_alpha : true; var _options_antialias; this.backBufferAntialias = (_options_antialias = options.antialias) != null ? _options_antialias : false; this.isWebGPU = true; this._deviceType = DEVICETYPE_WEBGPU; this.scope.resolve(UNUSED_UNIFORM_NAME).setValue(0); } } export { WebgpuGraphicsDevice };