// luma.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import type {RenderPipelineProps, ComputePipelineProps} from '@luma.gl/core'; import {Device, RenderPipeline, ComputePipeline} from '@luma.gl/core'; export type PipelineFactoryProps = RenderPipelineProps; /** * Efficiently creates / caches pipelines */ export class PipelineFactory { static defaultProps: Required = {...RenderPipeline.defaultProps}; readonly device: Device; private _hashCounter: number = 0; private readonly _hashes: Record = {}; private readonly _renderPipelineCache: Record< string, {pipeline: RenderPipeline; useCount: number} > = {}; private readonly _computePipelineCache: Record< string, {pipeline: ComputePipeline; useCount: number} > = {}; /** Get the singleton default pipeline factory for the specified device */ static getDefaultPipelineFactory(device: Device): PipelineFactory { device._lumaData.defaultPipelineFactory = device._lumaData.defaultPipelineFactory || new PipelineFactory(device); return device._lumaData.defaultPipelineFactory as PipelineFactory; } constructor(device: Device) { this.device = device; } /** Return a RenderPipeline matching props. Reuses a similar pipeline if already created. */ createRenderPipeline(props: RenderPipelineProps): RenderPipeline { const allProps: Required = {...RenderPipeline.defaultProps, ...props}; const hash = this._hashRenderPipeline(allProps); if (!this._renderPipelineCache[hash]) { const pipeline = this.device.createRenderPipeline({ ...allProps, id: allProps.id ? `${allProps.id}-cached` : undefined }); pipeline.hash = hash; this._renderPipelineCache[hash] = {pipeline, useCount: 0}; } this._renderPipelineCache[hash].useCount++; return this._renderPipelineCache[hash].pipeline; } createComputePipeline(props: ComputePipelineProps): ComputePipeline { const allProps: Required = {...ComputePipeline.defaultProps, ...props}; const hash = this._hashComputePipeline(allProps); if (!this._computePipelineCache[hash]) { const pipeline = this.device.createComputePipeline({ ...allProps, id: allProps.id ? `${allProps.id}-cached` : undefined }); pipeline.hash = hash; this._computePipelineCache[hash] = {pipeline, useCount: 0}; } this._computePipelineCache[hash].useCount++; return this._computePipelineCache[hash].pipeline; } release(pipeline: RenderPipeline | ComputePipeline): void { const hash = pipeline.hash; const cache = pipeline instanceof ComputePipeline ? this._computePipelineCache : this._renderPipelineCache; cache[hash].useCount--; if (cache[hash].useCount === 0) { cache[hash].pipeline.destroy(); delete cache[hash]; } } // PRIVATE private _hashComputePipeline(props: ComputePipelineProps): string { const shaderHash = this._getHash(props.shader.source); return `${shaderHash}`; } /** Calculate a hash based on all the inputs for a render pipeline */ private _hashRenderPipeline(props: RenderPipelineProps): string { const vsHash = this._getHash(props.vs.source); const fsHash = props.fs ? this._getHash(props.fs.source) : 0; // WebGL specific // const {varyings = [], bufferMode = {}} = props; // const varyingHashes = varyings.map((v) => this._getHash(v)); const varyingHash = '-'; // `${varyingHashes.join('/')}B${bufferMode}` const bufferLayoutHash = this._getHash(JSON.stringify(props.bufferLayout)); switch (this.device.type) { case 'webgl': // WebGL is more dynamic return `${vsHash}/${fsHash}V${varyingHash}BL${bufferLayoutHash}`; default: // On WebGPU we need to rebuild the pipeline if topology, parameters or bufferLayout change const parameterHash = this._getHash(JSON.stringify(props.parameters)); // TODO - Can json.stringify() generate different strings for equivalent objects if order of params is different? // create a deepHash() to deduplicate? return `${vsHash}/${fsHash}V${varyingHash}T${props.topology}P${parameterHash}BL${bufferLayoutHash}`; } } private _getHash(key: string): number { if (this._hashes[key] === undefined) { this._hashes[key] = this._hashCounter++; } return this._hashes[key]; } }