playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
1,128 lines (1,127 loc) • 44.6 kB
JavaScript
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
};