// luma.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors // A lot of imports, but then Model is where it all comes together... import {type TypedArray} from '@math.gl/types'; import { type RenderPipelineProps, type RenderPipelineParameters, type BufferLayout, type Shader, type VertexArray, type TransformFeedback, type AttributeInfo, type Binding, type BindingsByGroup, type PrimitiveTopology, Device, DeviceFeature, Buffer, Texture, TextureView, Sampler, RenderPipeline, RenderPass, PipelineFactory, ShaderFactory, UniformStore, log, dataTypeDecoder, getAttributeInfosFromLayouts, normalizeBindingsByGroup } from '@luma.gl/core'; import type {ShaderBindingDebugRow, ShaderModule, PlatformInfo} from '@luma.gl/shadertools'; import {ShaderAssembler} from '@luma.gl/shadertools'; import type {Geometry} from '../geometry/geometry'; import {GPUGeometry, makeGPUGeometry} from '../geometry/gpu-geometry'; import {getDebugTableForShaderLayout} from '../debug/debug-shader-layout'; import {debugFramebuffer} from '../debug/debug-framebuffer'; import {deepEqual} from '../utils/deep-equal'; import {BufferLayoutHelper} from '../utils/buffer-layout-helper'; import {sortedBufferLayoutByShaderSourceLocations} from '../utils/buffer-layout-order'; import { mergeShaderModuleBindingsIntoLayout, shaderModuleHasUniforms } from '../utils/shader-module-utils'; import {uid} from '../utils/uid'; import {ShaderInputs} from '../shader-inputs'; import {DynamicTexture} from '../dynamic-texture/dynamic-texture'; import {Material} from '../material/material'; const LOG_DRAW_PRIORITY = 2; const LOG_DRAW_TIMEOUT = 10000; const PIPELINE_INITIALIZATION_FAILED = 'render pipeline initialization failed'; export type ModelProps = Omit & { source?: string; vs?: string | null; fs?: string | null; /** shadertool shader modules (added to shader code) */ modules?: ShaderModule[]; /** Shadertool module defines (configures shader code)*/ defines?: Record; // TODO - injections, hooks etc? /** Shader inputs, used to generated uniform buffers and bindings */ shaderInputs?: ShaderInputs; /** Material-owned group-3 bindings */ material?: Material; /** Bindings */ bindings?: Record; /** WebGL-only uniforms */ uniforms?: Record; /** Parameters that are built into the pipeline */ parameters?: RenderPipelineParameters; /** Geometry */ geometry?: GPUGeometry | Geometry | null; /** @deprecated Use instanced rendering? Will be auto-detected in 9.1 */ isInstanced?: boolean; /** instance count */ instanceCount?: number; /** Vertex count */ vertexCount?: number; indexBuffer?: Buffer | null; /** @note this is really a map of buffers, not a map of attributes */ attributes?: Record; /** */ constantAttributes?: Record; /** Some applications intentionally supply unused attributes and bindings, and want to disable warnings */ disableWarnings?: boolean; /** @internal For use with {@link TransformFeedback}, WebGL only. */ varyings?: string[]; transformFeedback?: TransformFeedback; /** Show shader source in browser? */ debugShaders?: 'never' | 'errors' | 'warnings' | 'always'; /** Factory used to create a {@link RenderPipeline}. Defaults to {@link Device} default factory. */ pipelineFactory?: PipelineFactory; /** Factory used to create a {@link Shader}. Defaults to {@link Device} default factory. */ shaderFactory?: ShaderFactory; /** Shader assembler. Defaults to the ShaderAssembler.getShaderAssembler() */ shaderAssembler?: ShaderAssembler; }; /** * High level draw API for luma.gl. * * A `Model` encapsulates shaders, geometry attributes, bindings and render * pipeline state into a single object. It automatically reuses and rebuilds * pipelines as render parameters change and exposes convenient hooks for * updating uniforms and attributes. * * Features: * - Reuses and lazily recompiles {@link RenderPipeline | pipelines} as needed. * - Integrates with `@luma.gl/shadertools` to assemble GLSL or WGSL from shader modules. * - Manages geometry attributes and buffer bindings. * - Accepts textures, samplers and uniform buffers as bindings, including `DynamicTexture`. * - Provides detailed debug logging and optional shader source inspection. */ export class Model { static defaultProps: Required = { ...RenderPipeline.defaultProps, source: undefined!, vs: null, fs: null, id: 'unnamed', handle: undefined, userData: {}, defines: {}, modules: [], geometry: null, indexBuffer: null, attributes: {}, constantAttributes: {}, bindings: {}, uniforms: {}, varyings: [], isInstanced: undefined!, instanceCount: 0, vertexCount: 0, shaderInputs: undefined!, material: undefined!, pipelineFactory: undefined!, shaderFactory: undefined!, transformFeedback: undefined!, shaderAssembler: ShaderAssembler.getDefaultShaderAssembler(), debugShaders: undefined!, disableWarnings: undefined! }; /** Device that created this model */ readonly device: Device; /** Application provided identifier */ readonly id: string; /** WGSL shader source when using unified shader */ // @ts-expect-error assigned in function called from constructor readonly source: string; /** GLSL vertex shader source */ // @ts-expect-error assigned in function called from constructor readonly vs: string; /** GLSL fragment shader source */ // @ts-expect-error assigned in function called from constructor readonly fs: string; /** Factory used to create render pipelines */ readonly pipelineFactory: PipelineFactory; /** Factory used to create shaders */ readonly shaderFactory: ShaderFactory; /** User-supplied per-model data */ userData: {[key: string]: any} = {}; // Fixed properties (change can trigger pipeline rebuild) /** The render pipeline GPU parameters, depth testing etc */ parameters: RenderPipelineParameters; /** The primitive topology */ topology: PrimitiveTopology; /** Buffer layout */ bufferLayout: BufferLayout[]; // Dynamic properties /** Use instanced rendering */ isInstanced: boolean | undefined = undefined; /** instance count. `undefined` means not instanced */ instanceCount: number = 0; /** Vertex count */ vertexCount: number; /** Index buffer */ indexBuffer: Buffer | null = null; /** Buffer-valued attributes */ bufferAttributes: Record = {}; /** Constant-valued attributes */ constantAttributes: Record = {}; /** Bindings (textures, samplers, uniform buffers) */ bindings: Record = {}; /** * VertexArray * @note not implemented: if bufferLayout is updated, vertex array has to be rebuilt! * @todo - allow application to define multiple vertex arrays? * */ vertexArray: VertexArray; /** TransformFeedback, WebGL 2 only. */ transformFeedback: TransformFeedback | null = null; /** The underlying GPU "program". @note May be recreated if parameters change */ pipeline: RenderPipeline; /** ShaderInputs instance */ // @ts-expect-error Assigned in function called by constructor shaderInputs: ShaderInputs; material: Material | null = null; // @ts-expect-error Assigned in function called by constructor _uniformStore: UniformStore; _attributeInfos: Record = {}; _gpuGeometry: GPUGeometry | null = null; private props: Required; _pipelineNeedsUpdate: string | false = 'newly created'; private _needsRedraw: string | false = 'initializing'; private _destroyed = false; /** "Time" of last draw. Monotonically increasing timestamp */ _lastDrawTimestamp: number = -1; private _bindingTable: ShaderBindingDebugRow[] = []; get [Symbol.toStringTag](): string { return 'Model'; } toString(): string { return `Model(${this.id})`; } constructor(device: Device, props: ModelProps) { this.props = {...Model.defaultProps, ...props}; props = this.props; this.id = props.id || uid('model'); this.device = device; Object.assign(this.userData, props.userData); this.material = props.material || null; // Setup shader module inputs const moduleMap = Object.fromEntries( this.props.modules?.map(module => [module.name, module]) || [] ); const shaderInputs = props.shaderInputs || new ShaderInputs(moduleMap, {disableWarnings: this.props.disableWarnings}); // @ts-ignore this.setShaderInputs(shaderInputs); // Setup shader assembler const platformInfo = getPlatformInfo(device); // Extract modules from shader inputs if not supplied const modules = // @ts-ignore shaderInputs is assigned in setShaderInputs above. (this.props.modules?.length > 0 ? this.props.modules : this.shaderInputs?.getModules()) || []; this.props.shaderLayout = mergeShaderModuleBindingsIntoLayout(this.props.shaderLayout, modules) || null; const isWebGPU = this.device.type === 'webgpu'; // WebGPU // TODO - hack to support unified WGSL shader // TODO - this is wrong, compile a single shader if (isWebGPU && this.props.source) { // WGSL const {source, getUniforms, bindingTable} = this.props.shaderAssembler.assembleWGSLShader({ platformInfo, ...this.props, modules }); this.source = source; // @ts-expect-error this._getModuleUniforms = getUniforms; this._bindingTable = bindingTable; // Extract shader layout after modules have been added to WGSL source, to include any bindings added by modules const inferredShaderLayout = ( device as Device & {getShaderLayout?: (source: string) => any} ).getShaderLayout?.(this.source); this.props.shaderLayout = mergeShaderModuleBindingsIntoLayout( this.props.shaderLayout || inferredShaderLayout || null, modules ) || null; } else { // GLSL const {vs, fs, getUniforms} = this.props.shaderAssembler.assembleGLSLShaderPair({ platformInfo, ...this.props, modules }); this.vs = vs; this.fs = fs; // @ts-expect-error this._getModuleUniforms = getUniforms; this._bindingTable = []; } this.vertexCount = this.props.vertexCount; this.instanceCount = this.props.instanceCount; this.topology = this.props.topology; this.bufferLayout = this.props.bufferLayout; this.parameters = this.props.parameters; // Geometry, if provided, sets topology and vertex cound if (props.geometry) { this.setGeometry(props.geometry); } this.pipelineFactory = props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device); this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device); // Create the pipeline // @note order is important this.pipeline = this._updatePipeline(); this.vertexArray = device.createVertexArray({ shaderLayout: this.pipeline.shaderLayout, bufferLayout: this.pipeline.bufferLayout }); // Now we can apply geometry attributes if (this._gpuGeometry) { this._setGeometryAttributes(this._gpuGeometry); } // Apply any dynamic settings that will not trigger pipeline change if ('isInstanced' in props) { this.isInstanced = props.isInstanced; } if (props.instanceCount) { this.setInstanceCount(props.instanceCount); } if (props.vertexCount) { this.setVertexCount(props.vertexCount); } if (props.indexBuffer) { this.setIndexBuffer(props.indexBuffer); } if (props.attributes) { this.setAttributes(props.attributes); } if (props.constantAttributes) { this.setConstantAttributes(props.constantAttributes); } if (props.bindings) { this.setBindings(props.bindings); } if (props.transformFeedback) { this.transformFeedback = props.transformFeedback; } } destroy(): void { if (!this._destroyed) { // Release pipeline before we destroy the shaders used by the pipeline this.pipelineFactory.release(this.pipeline); // Release the shaders this.shaderFactory.release(this.pipeline.vs); if (this.pipeline.fs && this.pipeline.fs !== this.pipeline.vs) { this.shaderFactory.release(this.pipeline.fs); } this._uniformStore.destroy(); // TODO - mark resource as managed and destroyIfManaged() ? this._gpuGeometry?.destroy(); this._destroyed = true; } } // Draw call /** Query redraw status. Clears the status. */ needsRedraw(): false | string { // Catch any writes to already bound resources if (this._getBindingsUpdateTimestamp() > this._lastDrawTimestamp) { this.setNeedsRedraw('contents of bound textures or buffers updated'); } const needsRedraw = this._needsRedraw; this._needsRedraw = false; return needsRedraw; } /** Mark the model as needing a redraw */ setNeedsRedraw(reason: string): void { this._needsRedraw ||= reason; } /** Returns WGSL binding debug rows for the assembled shader. Returns an empty array for GLSL models. */ getBindingDebugTable(): readonly ShaderBindingDebugRow[] { return this._bindingTable; } /** Update uniforms and pipeline state prior to drawing. */ predraw(): void { // Update uniform buffers if needed this.updateShaderInputs(); // Check if the pipeline is invalidated this.pipeline = this._updatePipeline(); } /** * Issue one draw call. * @param renderPass - render pass to draw into * @returns `true` if the draw call was executed, `false` if resources were not ready. */ draw(renderPass: RenderPass): boolean { const loadingBinding = this._areBindingsLoading(); if (loadingBinding) { log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${loadingBinding} not loaded`)(); return false; } try { renderPass.pushDebugGroup(`${this}.predraw(${renderPass})`); this.predraw(); } finally { renderPass.popDebugGroup(); } let drawSuccess: boolean; let pipelineErrored = this.pipeline.isErrored; try { renderPass.pushDebugGroup(`${this}.draw(${renderPass})`); this._logDrawCallStart(); // Update the pipeline if invalidated // TODO - inside RenderPass is likely the worst place to do this from performance perspective. // Application can call Model.predraw() to avoid this. this.pipeline = this._updatePipeline(); pipelineErrored = this.pipeline.isErrored; if (pipelineErrored) { log.info( LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${PIPELINE_INITIALIZATION_FAILED}` )(); drawSuccess = false; } else { const syncBindings = this._getBindings(); const syncBindGroups = this._getBindGroups(); const {indexBuffer} = this.vertexArray; const indexCount = indexBuffer ? indexBuffer.byteLength / (indexBuffer.indexType === 'uint32' ? 4 : 2) : undefined; drawSuccess = this.pipeline.draw({ renderPass, vertexArray: this.vertexArray, isInstanced: this.isInstanced, vertexCount: this.vertexCount, instanceCount: this.instanceCount, indexCount, transformFeedback: this.transformFeedback || undefined, // Pipelines may be shared across models when caching is enabled, so bindings // and WebGL uniforms must be supplied on every draw instead of being stored // on the pipeline instance. bindings: syncBindings, bindGroups: syncBindGroups, _bindGroupCacheKeys: this._getBindGroupCacheKeys(), uniforms: this.props.uniforms, // WebGL shares underlying cached pipelines even for models that have different parameters and topology, // so we must provide our unique parameters to each draw // (In WebGPU most parameters are encoded in the pipeline and cannot be changed per draw call) parameters: this.parameters, topology: this.topology }); } } finally { renderPass.popDebugGroup(); this._logDrawCallEnd(); } this._logFramebuffer(renderPass); // Update needsRedraw flag if (drawSuccess) { this._lastDrawTimestamp = this.device.timestamp; this._needsRedraw = false; } else if (pipelineErrored) { this._needsRedraw = PIPELINE_INITIALIZATION_FAILED; } else { this._needsRedraw = 'waiting for resource initialization'; } return drawSuccess; } // Update fixed fields (can trigger pipeline rebuild) /** * Updates the optional geometry * Geometry, set topology and bufferLayout * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU */ setGeometry(geometry: GPUGeometry | Geometry | null): void { this._gpuGeometry?.destroy(); const gpuGeometry = geometry && makeGPUGeometry(this.device, geometry); if (gpuGeometry) { this.setTopology(gpuGeometry.topology || 'triangle-list'); const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout); this.bufferLayout = bufferLayoutHelper.mergeBufferLayouts( gpuGeometry.bufferLayout, this.bufferLayout ); if (this.vertexArray) { this._setGeometryAttributes(gpuGeometry); } } this._gpuGeometry = gpuGeometry; } /** * Updates the primitive topology ('triangle-list', 'triangle-strip' etc). * @note Triggers a pipeline rebuild / pipeline cache fetch on WebGPU */ setTopology(topology: PrimitiveTopology): void { if (topology !== this.topology) { this.topology = topology; this._setPipelineNeedsUpdate('topology'); } } /** * Updates the buffer layout. * @note Triggers a pipeline rebuild / pipeline cache fetch */ setBufferLayout(bufferLayout: BufferLayout[]): void { const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout); this.bufferLayout = this._gpuGeometry ? bufferLayoutHelper.mergeBufferLayouts(bufferLayout, this._gpuGeometry.bufferLayout) : bufferLayout; this._setPipelineNeedsUpdate('bufferLayout'); // Recreate the pipeline this.pipeline = this._updatePipeline(); // vertex array needs to be updated if we update buffer layout, // but not if we update parameters this.vertexArray = this.device.createVertexArray({ shaderLayout: this.pipeline.shaderLayout, bufferLayout: this.pipeline.bufferLayout }); // Reapply geometry attributes to the new vertex array if (this._gpuGeometry) { this._setGeometryAttributes(this._gpuGeometry); } } /** * Set GPU parameters. * @note Can trigger a pipeline rebuild / pipeline cache fetch. * @param parameters */ setParameters(parameters: RenderPipelineParameters) { if (!deepEqual(parameters, this.parameters, 2)) { this.parameters = parameters; this._setPipelineNeedsUpdate('parameters'); } } // Update dynamic fields /** * Updates the instance count (used in draw calls) * @note Any attributes with stepMode=instance need to be at least this big */ setInstanceCount(instanceCount: number): void { this.instanceCount = instanceCount; // luma.gl examples don't set props.isInstanced and rely on auto-detection // but deck.gl sets instanceCount even for models that are not instanced. if (this.isInstanced === undefined && instanceCount > 0) { this.isInstanced = true; } this.setNeedsRedraw('instanceCount'); } /** * Updates the vertex count (used in draw calls) * @note Any attributes with stepMode=vertex need to be at least this big */ setVertexCount(vertexCount: number): void { this.vertexCount = vertexCount; this.setNeedsRedraw('vertexCount'); } /** Set the shader inputs */ setShaderInputs(shaderInputs: ShaderInputs): void { this.shaderInputs = shaderInputs; this._uniformStore = new UniformStore(this.device, this.shaderInputs.modules); // Create uniform buffer bindings for all modules that actually have uniforms for (const [moduleName, module] of Object.entries(this.shaderInputs.modules)) { if (shaderModuleHasUniforms(module) && !this.material?.ownsModule(moduleName)) { const uniformBuffer = this._uniformStore.getManagedUniformBuffer(moduleName); this.bindings[`${moduleName}Uniforms`] = uniformBuffer; } } this.setNeedsRedraw('shaderInputs'); } setMaterial(material: Material | null): void { this.material = material; this.setNeedsRedraw('material'); } /** Update uniform buffers from the model's shader inputs */ updateShaderInputs(): void { this._uniformStore.setUniforms(this.shaderInputs.getUniformValues()); this.setBindings(this._getNonMaterialBindings(this.shaderInputs.getBindingValues())); // TODO - this is already tracked through buffer/texture update times? this.setNeedsRedraw('shaderInputs'); } /** * Sets bindings (textures, samplers, uniform buffers) */ setBindings(bindings: Record): void { Object.assign(this.bindings, bindings); this.setNeedsRedraw('bindings'); } /** * Updates optional transform feedback. WebGL only. */ setTransformFeedback(transformFeedback: TransformFeedback | null): void { this.transformFeedback = transformFeedback; this.setNeedsRedraw('transformFeedback'); } /** * Sets the index buffer * @todo - how to unset it if we change geometry? */ setIndexBuffer(indexBuffer: Buffer | null): void { this.vertexArray.setIndexBuffer(indexBuffer); this.setNeedsRedraw('indexBuffer'); } /** * Sets attributes (buffers) * @note Overrides any attributes previously set with the same name */ setAttributes(buffers: Record, options?: {disableWarnings?: boolean}): void { const disableWarnings = options?.disableWarnings ?? this.props.disableWarnings; if (buffers['indices']) { log.warn( `Model:${this.id} setAttributes() - indexBuffer should be set using setIndexBuffer()` )(); } // ensure bufferLayout order matches source layout so we bind // the correct buffers to the correct indices in webgpu. this.bufferLayout = sortedBufferLayoutByShaderSourceLocations( this.pipeline.shaderLayout, this.bufferLayout ); const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout); // Check if all buffers have a layout for (const [bufferName, buffer] of Object.entries(buffers)) { const bufferLayout = bufferLayoutHelper.getBufferLayout(bufferName); if (!bufferLayout) { if (!disableWarnings) { log.warn(`Model(${this.id}): Missing layout for buffer "${bufferName}".`)(); } continue; // eslint-disable-line no-continue } // In WebGL, for an interleaved attribute we may need to set multiple attributes // but in WebGPU, we set it according to the buffer's position in the vertexArray const attributeNames = bufferLayoutHelper.getAttributeNamesForBuffer(bufferLayout); let set = false; for (const attributeName of attributeNames) { const attributeInfo = this._attributeInfos[attributeName]; if (attributeInfo) { const location = this.device.type === 'webgpu' ? bufferLayoutHelper.getBufferIndex(attributeInfo.bufferName) : attributeInfo.location; this.vertexArray.setBuffer(location, buffer); set = true; } } if (!set && !disableWarnings) { log.warn( `Model(${this.id}): Ignoring buffer "${buffer.id}" for unknown attribute "${bufferName}"` )(); } } this.setNeedsRedraw('attributes'); } /** * Sets constant attributes * @note Overrides any attributes previously set with the same name * Constant attributes are only supported in WebGL, not in WebGPU * Any attribute that is disabled in the current vertex array object * is read from the context's global constant value for that attribute location. * @param constantAttributes */ setConstantAttributes( attributes: Record, options?: {disableWarnings?: boolean} ): void { for (const [attributeName, value] of Object.entries(attributes)) { const attributeInfo = this._attributeInfos[attributeName]; if (attributeInfo) { this.vertexArray.setConstantWebGL(attributeInfo.location, value); } else if (!(options?.disableWarnings ?? this.props.disableWarnings)) { log.warn( `Model "${this.id}: Ignoring constant supplied for unknown attribute "${attributeName}"` )(); } } this.setNeedsRedraw('constants'); } // INTERNAL METHODS /** Check that bindings are loaded. Returns id of first binding that is still loading. */ _areBindingsLoading(): string | false { for (const binding of Object.values(this.bindings)) { if (binding instanceof DynamicTexture && !binding.isReady) { return binding.id; } } for (const binding of Object.values(this.material?.bindings || {})) { if (binding instanceof DynamicTexture && !binding.isReady) { return binding.id; } } return false; } /** Extracts texture view from loaded async textures. Returns null if any textures have not yet been loaded. */ _getBindings(): Record { const validBindings: Record = {}; for (const [name, binding] of Object.entries(this.bindings)) { if (binding instanceof DynamicTexture) { // Check that async textures are loaded if (binding.isReady) { validBindings[name] = binding.texture; } } else { validBindings[name] = binding; } } return validBindings; } _getBindGroups(): BindingsByGroup { const shaderLayout = this.pipeline?.shaderLayout || this.props.shaderLayout || {bindings: []}; const bindGroups = shaderLayout.bindings.length ? normalizeBindingsByGroup(shaderLayout, this._getBindings()) : {0: this._getBindings()}; if (!this.material) { return bindGroups; } for (const [groupKey, groupBindings] of Object.entries(this.material.getBindingsByGroup())) { const group = Number(groupKey); bindGroups[group] = { ...(bindGroups[group] || {}), ...groupBindings }; } return bindGroups; } _getBindGroupCacheKeys(): Partial> { const bindGroupCacheKey = this.material?.getBindGroupCacheKey(3); return bindGroupCacheKey ? {3: bindGroupCacheKey} : {}; } /** Get the timestamp of the latest updated bound GPU memory resource (buffer/texture). */ _getBindingsUpdateTimestamp(): number { let timestamp = 0; for (const binding of Object.values(this.bindings)) { if (binding instanceof TextureView) { timestamp = Math.max(timestamp, binding.texture.updateTimestamp); } else if (binding instanceof Buffer || binding instanceof Texture) { timestamp = Math.max(timestamp, binding.updateTimestamp); } else if (binding instanceof DynamicTexture) { timestamp = binding.texture ? Math.max(timestamp, binding.texture.updateTimestamp) : // The texture will become available in the future Infinity; } else if (!(binding instanceof Sampler)) { timestamp = Math.max(timestamp, binding.buffer.updateTimestamp); } } return Math.max(timestamp, this.material?.getBindingsUpdateTimestamp() || 0); } /** * Updates the optional geometry attributes * Geometry, sets several attributes, indexBuffer, and also vertex count * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU */ _setGeometryAttributes(gpuGeometry: GPUGeometry): void { // Filter geometry attribute so that we don't issue warnings for unused attributes const attributes = {...gpuGeometry.attributes}; for (const [attributeName] of Object.entries(attributes)) { if ( !this.pipeline.shaderLayout.attributes.find(layout => layout.name === attributeName) && attributeName !== 'positions' ) { delete attributes[attributeName]; } } // TODO - delete previous geometry? this.vertexCount = gpuGeometry.vertexCount; this.setIndexBuffer(gpuGeometry.indices || null); this.setAttributes(gpuGeometry.attributes, {disableWarnings: true}); this.setAttributes(attributes, {disableWarnings: this.props.disableWarnings}); this.setNeedsRedraw('geometry attributes'); } /** Mark pipeline as needing update */ _setPipelineNeedsUpdate(reason: string): void { this._pipelineNeedsUpdate ||= reason; this.setNeedsRedraw(reason); } /** Update pipeline if needed */ _updatePipeline(): RenderPipeline { if (this._pipelineNeedsUpdate) { let prevShaderVs: Shader | null = null; let prevShaderFs: Shader | null = null; if (this.pipeline) { log.log( 1, `Model ${this.id}: Recreating pipeline because "${this._pipelineNeedsUpdate}".` )(); prevShaderVs = this.pipeline.vs; prevShaderFs = this.pipeline.fs; } this._pipelineNeedsUpdate = false; const vs = this.shaderFactory.createShader({ id: `${this.id}-vertex`, stage: 'vertex', source: this.source || this.vs, debugShaders: this.props.debugShaders }); let fs: Shader | null = null; if (this.source) { fs = vs; } else if (this.fs) { fs = this.shaderFactory.createShader({ id: `${this.id}-fragment`, stage: 'fragment', source: this.source || this.fs, debugShaders: this.props.debugShaders }); } this.pipeline = this.pipelineFactory.createRenderPipeline({ ...this.props, bindings: undefined, bufferLayout: this.bufferLayout, topology: this.topology, parameters: this.parameters, bindGroups: this._getBindGroups(), vs, fs }); this._attributeInfos = getAttributeInfosFromLayouts( this.pipeline.shaderLayout, this.bufferLayout ); if (prevShaderVs) this.shaderFactory.release(prevShaderVs); if (prevShaderFs && prevShaderFs !== prevShaderVs) { this.shaderFactory.release(prevShaderFs); } } return this.pipeline; } /** Throttle draw call logging */ _lastLogTime = 0; _logOpen = false; _logDrawCallStart(): void { // IF level is 4 or higher, log every frame. const logDrawTimeout = log.level > 3 ? 0 : LOG_DRAW_TIMEOUT; if (log.level < 2 || Date.now() - this._lastLogTime < logDrawTimeout) { return; } this._lastLogTime = Date.now(); this._logOpen = true; log.group(LOG_DRAW_PRIORITY, `>>> DRAWING MODEL ${this.id}`, {collapsed: log.level <= 2})(); } _logDrawCallEnd(): void { if (this._logOpen) { const shaderLayoutTable = getDebugTableForShaderLayout(this.pipeline.shaderLayout, this.id); // log.table(logLevel, attributeTable)(); // log.table(logLevel, uniformTable)(); log.table(LOG_DRAW_PRIORITY, shaderLayoutTable)(); const uniformTable = this.shaderInputs.getDebugTable(); log.table(LOG_DRAW_PRIORITY, uniformTable)(); const attributeTable = this._getAttributeDebugTable(); log.table(LOG_DRAW_PRIORITY, this._attributeInfos)(); log.table(LOG_DRAW_PRIORITY, attributeTable)(); log.groupEnd(LOG_DRAW_PRIORITY)(); this._logOpen = false; } } protected _drawCount = 0; _logFramebuffer(renderPass: RenderPass): void { const debugFramebuffers = this.device.props.debugFramebuffers; this._drawCount++; // Update first 3 frames and then every 60 frames if (!debugFramebuffers) { // } || (this._drawCount++ > 3 && this._drawCount % 60)) { return; } const framebuffer = renderPass.props.framebuffer; debugFramebuffer(renderPass, framebuffer, { id: framebuffer?.id || `${this.id}-framebuffer`, minimap: true }); } _getAttributeDebugTable(): Record> { const table: Record> = {}; for (const [name, attributeInfo] of Object.entries(this._attributeInfos)) { const values = this.vertexArray.attributes[attributeInfo.location]; table[attributeInfo.location] = { name, type: attributeInfo.shaderType, values: values ? this._getBufferOrConstantValues(values, attributeInfo.bufferDataType) : 'null' }; } if (this.vertexArray.indexBuffer) { const {indexBuffer} = this.vertexArray; const values = indexBuffer.indexType === 'uint32' ? new Uint32Array(indexBuffer.debugData) : new Uint16Array(indexBuffer.debugData); table['indices'] = { name: 'indices', type: indexBuffer.indexType, values: values.toString() }; } return table; } // TODO - fix typing of luma data types _getBufferOrConstantValues(attribute: Buffer | TypedArray, dataType: any): string { const TypedArrayConstructor = dataTypeDecoder.getTypedArrayConstructor(dataType); const typedArray = attribute instanceof Buffer ? new TypedArrayConstructor(attribute.debugData) : attribute; return typedArray.toString(); } private _getNonMaterialBindings( bindings: Record ): Record { if (!this.material) { return bindings; } const filteredBindings: Record = {}; for (const [name, binding] of Object.entries(bindings)) { if (!this.material.ownsBinding(name)) { filteredBindings[name] = binding; } } return filteredBindings; } } // HELPERS /** Create a shadertools platform info from the Device */ export function getPlatformInfo(device: Device): PlatformInfo { return { type: device.type, shaderLanguage: device.info.shadingLanguage, shaderLanguageVersion: device.info.shadingLanguageVersion as 100 | 300, gpu: device.info.gpu, // HACK - we pretend that the DeviceFeatures is a Set, it has a similar API features: device.features as unknown as Set }; }