UNPKG

@luma.gl/core

Version:

The luma.gl core Device API

270 lines 12.5 kB
// luma.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import { ComputePipeline } from "../adapter/resources/compute-pipeline.js"; import { RenderPipeline } from "../adapter/resources/render-pipeline.js"; import { log } from "../utils/log.js"; import { uid } from "../utils/uid.js"; /** * Efficiently creates / caches pipelines */ export class PipelineFactory { static defaultProps = { ...RenderPipeline.defaultProps }; /** Get the singleton default pipeline factory for the specified device */ static getDefaultPipelineFactory(device) { const moduleData = device.getModuleData('@luma.gl/core'); moduleData.defaultPipelineFactory ||= new PipelineFactory(device); return moduleData.defaultPipelineFactory; } device; _hashCounter = 0; _hashes = {}; _renderPipelineCache = {}; _computePipelineCache = {}; _sharedRenderPipelineCache = {}; get [Symbol.toStringTag]() { return 'PipelineFactory'; } toString() { return `PipelineFactory(${this.device.id})`; } constructor(device) { this.device = device; } /** * WebGL has two cache layers with different priorities: * - `_sharedRenderPipelineCache` owns `WEBGLSharedRenderPipeline` / `WebGLProgram` reuse. * - `_renderPipelineCache` owns `RenderPipeline` wrapper reuse. * * Shared WebGL program reuse is the hard requirement. Wrapper reuse is beneficial, * but wrapper cache misses are acceptable if that keeps the cache logic simple and * prevents incorrect cache hits. * * In particular, wrapper hash logic must never force program creation or linked-program * introspection just to decide whether a shared WebGL program can be reused. */ /** Return a RenderPipeline matching supplied props. Reuses an equivalent pipeline if already created. */ createRenderPipeline(props) { if (!this.device.props._cachePipelines) { return this.device.createRenderPipeline(props); } const allProps = { ...RenderPipeline.defaultProps, ...props }; const cache = this._renderPipelineCache; const hash = this._hashRenderPipeline(allProps); let pipeline = cache[hash]?.resource; if (!pipeline) { const sharedRenderPipeline = this.device.type === 'webgl' && this.device.props._sharePipelines ? this.createSharedRenderPipeline(allProps) : undefined; pipeline = this.device.createRenderPipeline({ ...allProps, id: allProps.id ? `${allProps.id}-cached` : uid('unnamed-cached'), _sharedRenderPipeline: sharedRenderPipeline }); pipeline.hash = hash; cache[hash] = { resource: pipeline, useCount: 1 }; if (this.device.props.debugFactories) { log.log(3, `${this}: ${pipeline} created, count=${cache[hash].useCount}`)(); } } else { cache[hash].useCount++; if (this.device.props.debugFactories) { log.log(3, `${this}: ${cache[hash].resource} reused, count=${cache[hash].useCount}, (id=${props.id})`)(); } } return pipeline; } /** Return a ComputePipeline matching supplied props. Reuses an equivalent pipeline if already created. */ createComputePipeline(props) { if (!this.device.props._cachePipelines) { return this.device.createComputePipeline(props); } const allProps = { ...ComputePipeline.defaultProps, ...props }; const cache = this._computePipelineCache; const hash = this._hashComputePipeline(allProps); let pipeline = cache[hash]?.resource; if (!pipeline) { pipeline = this.device.createComputePipeline({ ...allProps, id: allProps.id ? `${allProps.id}-cached` : undefined }); pipeline.hash = hash; cache[hash] = { resource: pipeline, useCount: 1 }; if (this.device.props.debugFactories) { log.log(3, `${this}: ${pipeline} created, count=${cache[hash].useCount}`)(); } } else { cache[hash].useCount++; if (this.device.props.debugFactories) { log.log(3, `${this}: ${cache[hash].resource} reused, count=${cache[hash].useCount}, (id=${props.id})`)(); } } return pipeline; } release(pipeline) { if (!this.device.props._cachePipelines) { pipeline.destroy(); return; } const cache = this._getCache(pipeline); const hash = pipeline.hash; cache[hash].useCount--; if (cache[hash].useCount === 0) { this._destroyPipeline(pipeline); if (this.device.props.debugFactories) { log.log(3, `${this}: ${pipeline} released and destroyed`)(); } } else if (cache[hash].useCount < 0) { log.error(`${this}: ${pipeline} released, useCount < 0, resetting`)(); cache[hash].useCount = 0; } else if (this.device.props.debugFactories) { log.log(3, `${this}: ${pipeline} released, count=${cache[hash].useCount}`)(); } } createSharedRenderPipeline(props) { const sharedPipelineHash = this._hashSharedRenderPipeline(props); let sharedCacheItem = this._sharedRenderPipelineCache[sharedPipelineHash]; if (!sharedCacheItem) { const sharedRenderPipeline = this.device._createSharedRenderPipelineWebGL(props); sharedCacheItem = { resource: sharedRenderPipeline, useCount: 0 }; this._sharedRenderPipelineCache[sharedPipelineHash] = sharedCacheItem; } sharedCacheItem.useCount++; return sharedCacheItem.resource; } releaseSharedRenderPipeline(pipeline) { if (!pipeline.sharedRenderPipeline) { return; } const sharedPipelineHash = this._hashSharedRenderPipeline(pipeline.sharedRenderPipeline.props); const sharedCacheItem = this._sharedRenderPipelineCache[sharedPipelineHash]; if (!sharedCacheItem) { return; } sharedCacheItem.useCount--; if (sharedCacheItem.useCount === 0) { sharedCacheItem.resource.destroy(); delete this._sharedRenderPipelineCache[sharedPipelineHash]; } } // PRIVATE /** Destroy a cached pipeline, removing it from the cache if configured to do so. */ _destroyPipeline(pipeline) { const cache = this._getCache(pipeline); if (!this.device.props._destroyPipelines) { return false; } delete cache[pipeline.hash]; pipeline.destroy(); if (pipeline instanceof RenderPipeline) { this.releaseSharedRenderPipeline(pipeline); } return true; } /** Get the appropriate cache for the type of pipeline */ _getCache(pipeline) { let cache; if (pipeline instanceof ComputePipeline) { cache = this._computePipelineCache; } if (pipeline instanceof RenderPipeline) { cache = this._renderPipelineCache; } if (!cache) { throw new Error(`${this}`); } if (!cache[pipeline.hash]) { throw new Error(`${this}: ${pipeline} matched incorrect entry`); } return cache; } /** Calculate a hash based on all the inputs for a compute pipeline */ _hashComputePipeline(props) { const { type } = this.device; const shaderHash = this._getHash(props.shader.source); const shaderLayoutHash = this._getHash(JSON.stringify(props.shaderLayout)); return `${type}/C/${shaderHash}SL${shaderLayoutHash}`; } /** Calculate a hash based on all the inputs for a render pipeline */ _hashRenderPipeline(props) { // Backend-specific hashing requirements: // - WebGPU hash keys must include every immutable descriptor-shaping input that can // change the created `GPURenderPipeline`, including attachment formats. // - WebGL hash keys only govern wrapper reuse. They must remain compatible with // shared-program reuse and must not depend on linked-program introspection just // to decide whether a shared `WebGLProgram` can be reused. // // General exclusions: // - `id`, `handle`: resource identity / caller-supplied handles, not cache shape // - `bindings`: mutable per-pipeline compatibility state // - `disableWarnings`: logging only, no rendering impact // - `vsConstants`, `fsConstants`: currently unused by pipeline creation const vsHash = props.vs ? this._getHash(props.vs.source) : 0; const fsHash = props.fs ? this._getHash(props.fs.source) : 0; const varyingHash = this._getWebGLVaryingHash(props); const shaderLayoutHash = this._getHash(JSON.stringify(props.shaderLayout)); const bufferLayoutHash = this._getHash(JSON.stringify(props.bufferLayout)); const { type } = this.device; switch (type) { case 'webgl': // WebGL wrappers preserve default topology and parameter semantics for direct // callers, even though the underlying linked program may be shared separately. // Future WebGL-only additions here must not turn wrapper reuse into a prerequisite // for shared `WebGLProgram` reuse. const webglParameterHash = this._getHash(JSON.stringify(props.parameters)); return `${type}/R/${vsHash}/${fsHash}V${varyingHash}T${props.topology}P${webglParameterHash}SL${shaderLayoutHash}BL${bufferLayoutHash}`; case 'webgpu': default: // On WebGPU we need to rebuild the pipeline if topology, entry points, // shader/layout data, parameters, bufferLayout or attachment formats change. // Attachment formats must stay in the key so screen and offscreen passes do not // accidentally alias the same cached `GPURenderPipeline`. const entryPointHash = this._getHash(JSON.stringify({ vertexEntryPoint: props.vertexEntryPoint, fragmentEntryPoint: props.fragmentEntryPoint })); const parameterHash = this._getHash(JSON.stringify(props.parameters)); const attachmentHash = this._getWebGPUAttachmentHash(props); // TODO - Can json.stringify() generate different strings for equivalent objects if order of params is different? // create a deepHash() to deduplicate? return `${type}/R/${vsHash}/${fsHash}V${varyingHash}T${props.topology}EP${entryPointHash}P${parameterHash}SL${shaderLayoutHash}BL${bufferLayoutHash}A${attachmentHash}`; } } // This is the only gate for shared `WebGLProgram` reuse. // Only include inputs that affect program linking or transform-feedback linkage. // Wrapper-only concerns such as topology, parameters, attachment formats and layout // overrides must not be added here. _hashSharedRenderPipeline(props) { const vsHash = props.vs ? this._getHash(props.vs.source) : 0; const fsHash = props.fs ? this._getHash(props.fs.source) : 0; const varyingHash = this._getWebGLVaryingHash(props); return `webgl/S/${vsHash}/${fsHash}V${varyingHash}`; } _getHash(key) { if (this._hashes[key] === undefined) { this._hashes[key] = this._hashCounter++; } return this._hashes[key]; } _getWebGLVaryingHash(props) { const { varyings = [], bufferMode = null } = props; return this._getHash(JSON.stringify({ varyings, bufferMode })); } _getWebGPUAttachmentHash(props) { const colorAttachmentFormats = props.colorAttachmentFormats ?? [ this.device.preferredColorFormat ]; const depthStencilAttachmentFormat = props.parameters?.depthWriteEnabled ? props.depthStencilAttachmentFormat || this.device.preferredDepthFormat : null; return this._getHash(JSON.stringify({ colorAttachmentFormats, depthStencilAttachmentFormat })); } } //# sourceMappingURL=pipeline-factory.js.map