UNPKG

kinetic-slider

Version:

A WebGL-powered kinetic slider component using PIXI.js

361 lines (358 loc) 14.6 kB
import { Filter, Shader } from 'pixi.js'; /** * @file ShaderResourceManager.ts * @description Manages shader resources with pooling, caching, and performance monitoring. * Optimizes GPU resource usage by sharing shader programs across filters. */ /** * Manager for shader program resources with pooling and performance metrics * * Provides: * - Shader program pooling to share across filter instances * - Performance metrics tracking for shader compilation and usage * - Automatic cleanup of unused shaders */ class ShaderResourceManager { /** Singleton instance of the shader manager */ static instance; /** Pool of shader programs indexed by hash */ shaderPool = new Map(); /** Map of filters to their associated shader hashes */ filterShaderMap = new Map(); /** Manager configuration */ options; /** Is debug logging enabled */ debug = false; /** * Creates a new ShaderResourceManager instance * * @param options - Configuration options */ constructor(options = {}) { this.options = { enableMetrics: options.enableMetrics ?? true, debug: options.debug ?? false, maxPoolSize: options.maxPoolSize ?? 100 }; this.debug = this.options.debug ?? false; this.log('ShaderResourceManager initialized'); } /** * Get the singleton instance of the ShaderResourceManager * * @param options - Optional configuration options * @returns The singleton instance */ static getInstance(options = {}) { if (!ShaderResourceManager.instance) { ShaderResourceManager.instance = new ShaderResourceManager(options); } return ShaderResourceManager.instance; } /** * Log a message if debug is enabled * * @param message - Message to log */ log(message) { if (this.debug) { console.log(`[ShaderResourceManager] ${message}`); } } /** * Generate a hash for a shader program based on its source code * * @param vertexSrc - Vertex shader source code * @param fragmentSrc - Fragment shader source code * @returns A hash string uniquely identifying the shader program */ generateShaderHash(vertexSrc, fragmentSrc) { // Simple hashing function - could be improved for production const combinedSrc = `${vertexSrc}:${fragmentSrc}`; let hash = 0; for (let i = 0; i < combinedSrc.length; i++) { const char = combinedSrc.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return `shader_${Math.abs(hash).toString(16)}`; } getShaderProgram(keyOrFilter, vertexSrcOrFilter, fragmentSrc, filterType) { // Handle the simplified overload if (typeof keyOrFilter === 'string' && vertexSrcOrFilter instanceof Filter) { const key = keyOrFilter; const filter = vertexSrcOrFilter; // Check if we already have a mapping for this filter if (this.filterShaderMap.has(filter)) { const hash = this.filterShaderMap.get(filter); if (hash) { const entry = this.shaderPool.get(hash); if (entry) { // Update usage stats entry.stats.usageCount++; entry.stats.lastUsed = Date.now(); this.log(`Using already mapped shader for ${key}`); return entry.program; } } } // Try to extract shader information from the filter try { // Most PixiJS filters use a program or shader property let program = null; let vertexSrc = ''; let fragmentSrc = ''; const extractedFilterType = filter.constructor.name; // First attempt: using a 'shader' property if it exists if ('shader' in filter && filter.shader) { if (filter.shader instanceof Shader && filter.shader.glProgram) { program = filter.shader.glProgram; // For newer versions of PixiJS, we might not have direct access to shader source // Instead, we'll use the key as a unique identifier const hash = `key_${key}_${Date.now()}`; // Create a new entry const entry = { program, filterType: extractedFilterType, vertexSrc: 'extracted', // Placeholder fragmentSrc: 'extracted', // Placeholder stats: { compilationCount: 0, // Not compiled by us usageCount: 1, instanceCount: 1, compilationTime: 0, lastUsed: Date.now() } }; this.shaderPool.set(hash, entry); this.filterShaderMap.set(filter, hash); this.log(`Registered existing shader for ${key} (${extractedFilterType})`); return program; } } // If we couldn't extract properly, just map the filter to key for tracking // but don't try to manage the shader program const fallbackHash = `key_${key}_${Date.now()}`; this.log(`Could not extract shader source for ${key}, using basic tracking`); this.filterShaderMap.set(filter, fallbackHash); // If filter has a program property, use it directly if ('program' in filter && typeof filter.program === 'object' && filter.program !== null) { return filter.program; } // Try to access shader.glProgram if ('shader' in filter && filter.shader && typeof filter.shader === 'object' && 'glProgram' in filter.shader && filter.shader.glProgram) { return filter.shader.glProgram; } this.log(`Unable to access shader program for ${key}`); return undefined; } catch (error) { this.log(`Error extracting shader from filter: ${error}`); return undefined; } } // Handle the original implementation with full parameters if (typeof keyOrFilter === 'object' && typeof vertexSrcOrFilter === 'string' && typeof fragmentSrc === 'string' && typeof filterType === 'string') { return this.getShaderProgramInternal(keyOrFilter, vertexSrcOrFilter, fragmentSrc, filterType); } this.log('Invalid parameters for getShaderProgram'); return undefined; } /** * Internal implementation of getShaderProgram with all parameters */ getShaderProgramInternal(filter, vertexSrc, fragmentSrc, filterType) { performance.now(); const hash = this.generateShaderHash(vertexSrc, fragmentSrc); // Check if we already have this shader in the pool let entry = this.shaderPool.get(hash); if (entry) { // Update statistics entry.stats.usageCount++; entry.stats.instanceCount++; entry.stats.lastUsed = Date.now(); // Map this filter to the shader hash this.filterShaderMap.set(filter, hash); this.log(`Reusing shader program ${hash} for ${filterType} filter`); return entry.program; } // For newer versions of PixiJS, we may not have the ability to create GlProgram directly // Instead, we'll track the filter and return undefined this.log(`Cannot directly create shader program in this PixiJS version`); // Create a fallback entry for tracking purposes const fallbackHash = `key_manual_${Date.now()}`; this.filterShaderMap.set(filter, fallbackHash); return undefined; } releaseShader(filter) { if (typeof filter === 'string') { // Find all filters using this key pattern const filtersToRelease = []; for (const [filterInstance, hash] of this.filterShaderMap.entries()) { if (hash.includes(`key_${filter}_`)) { filtersToRelease.push(filterInstance); } } // Release each filter for (const filterToRelease of filtersToRelease) { this.releaseShaderByFilter(filterToRelease); } return; } this.releaseShaderByFilter(filter); } /** * Release a shader program associated with a filter * * @param filter - The filter instance */ releaseShaderByFilter(filter) { const hash = this.filterShaderMap.get(filter); if (!hash) { return; // No shader associated with this filter } const entry = this.shaderPool.get(hash); if (entry) { // Decrease instance count entry.stats.instanceCount--; // Remove only if there are no instances left if (entry.stats.instanceCount <= 0) { this.log(`Removing unused shader program ${hash}`); this.shaderPool.delete(hash); } } // Remove the filter mapping this.filterShaderMap.delete(filter); } /** * Prune the shader pool by removing the least recently used shaders * when the pool exceeds the maximum size */ pruneShaderPool() { if (this.shaderPool.size <= (this.options.maxPoolSize || 100)) { return; // No need to prune } // Sort entries by last used time (oldest first) const entries = Array.from(this.shaderPool.entries()) .sort(([, a], [, b]) => a.stats.lastUsed - b.stats.lastUsed); // Determine how many to remove const removeCount = Math.ceil(this.shaderPool.size * 0.2); // Remove 20% // Remove oldest entries for (let i = 0; i < removeCount && i < entries.length; i++) { const [hash, entry] = entries[i]; // Only remove if not in use if (entry.stats.instanceCount <= 0) { this.log(`Pruning shader ${hash} (last used: ${new Date(entry.stats.lastUsed).toISOString()})`); this.shaderPool.delete(hash); } } } /** * Get statistics about shader usage and the shader pool * * @returns Statistics object */ getStats() { const stats = { totalShaders: this.shaderPool.size, activeShaders: 0, totalCompilationTime: 0, avgCompilationTime: 0, totalUsage: 0, oldestShader: 0, newestShader: 0, shaderTypes: {} }; if (this.shaderPool.size > 0) { for (const [, entry] of this.shaderPool.entries()) { // Count active shaders (those with instances > 0) if (entry.stats.instanceCount > 0) { stats.activeShaders++; } // Add to total compilation time stats.totalCompilationTime += entry.stats.compilationTime; // Add to total usage stats.totalUsage += entry.stats.usageCount; // Track by filter type if (!stats.shaderTypes[entry.filterType]) { stats.shaderTypes[entry.filterType] = 0; } stats.shaderTypes[entry.filterType]++; // Track oldest and newest if (stats.oldestShader === 0 || entry.stats.lastUsed < stats.oldestShader) { stats.oldestShader = entry.stats.lastUsed; } if (stats.newestShader === 0 || entry.stats.lastUsed > stats.newestShader) { stats.newestShader = entry.stats.lastUsed; } } // Calculate average compilation time stats.avgCompilationTime = stats.totalCompilationTime / this.shaderPool.size; } return stats; } /** * Registers a filter with the shader manager for tracking and optimization * * @param filter - The filter to register * @param key - Optional unique key to identify this filter's shader * @returns True if registration was successful */ registerFilter(filter, key) { if (!filter) { this.log('Cannot register null or undefined filter'); return false; } try { // If no key provided, we'll generate one from the filter's shader when needed if (key) { this.filterShaderMap.set(filter, key); this.log(`Filter registered with key: ${key}`); } else { // The key will be generated when getShaderProgram is called this.log('Filter registered without key (will be auto-generated)'); } return true; } catch (error) { this.log(`Error registering filter: ${error}`); return false; } } /** * Releases a filter from the shader manager * * @param filter - The filter to release * @param key - Optional key that was used to register the filter */ releaseFilter(filter, key) { if (!filter) { this.log('Cannot release null or undefined filter'); return; } try { if (key) { this.filterShaderMap.delete(filter); this.log(`Filter with key ${key} released`); } else { this.releaseShaderByFilter(filter); } } catch (error) { this.log(`Error releasing filter: ${error}`); } } } export { ShaderResourceManager }; //# sourceMappingURL=ShaderResourceManager.js.map