diff --git a/src/FakeShader.ts b/src/FakeShader.ts index 8e2ae47..5db5c7c 100644 --- a/src/FakeShader.ts +++ b/src/FakeShader.ts @@ -6,6 +6,7 @@ interface WorkerMessage { height?: number; time?: number; renderMode?: string; + startY?: number; // Y offset for tile rendering mouseX?: number; mouseY?: number; mousePressed?: boolean; @@ -43,7 +44,9 @@ export class FakeShader { private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; private code: string; - private worker: Worker; + private worker: Worker; // Single worker for backwards compatibility + private workers: Worker[] = []; + private workerCount: number; private animationId: number | null = null; private startTime: number = Date.now(); private isCompiled: boolean = false; @@ -65,6 +68,12 @@ export class FakeShader { private maxScale: number = 1.0; private renderStartTime: number = 0; + // Multi-worker state + private tileResults: Map = new Map(); + private tilesCompleted: number = 0; + private totalTiles: number = 0; + private currentRenderID: string = ''; + private mouseX: number = 0; private mouseY: number = 0; private mousePressed: boolean = false; @@ -105,10 +114,18 @@ export class FakeShader { // Initialize offscreen canvas if supported this.initializeOffscreenCanvas(); - // Initialize worker - this.worker = new Worker(new URL('./ShaderWorker.ts', import.meta.url), { type: 'module' }); - this.worker.onmessage = (e: MessageEvent) => this.handleWorkerMessage(e.data); - this.worker.onerror = (error) => console.error('Worker error:', error); + // Always use maximum available cores + this.workerCount = navigator.hardwareConcurrency || 4; + // Some browsers report logical processors (hyperthreading), which is good + // But cap at a reasonable maximum to avoid overhead + this.workerCount = Math.min(this.workerCount, 32); + console.log(`Auto-detected ${this.workerCount} CPU cores, using all for maximum performance`); + + // Initialize workers + this.initializeWorkers(); + + // Keep single worker reference for backwards compatibility + this.worker = this.workers[0]; this.compile(); } @@ -142,7 +159,17 @@ export class FakeShader { } } - private handleWorkerMessage(response: WorkerResponse): void { + private initializeWorkers(): void { + // Create worker pool + for (let i = 0; i < this.workerCount; i++) { + const worker = new Worker(new URL('./ShaderWorker.ts', import.meta.url), { type: 'module' }); + worker.onmessage = (e: MessageEvent) => this.handleWorkerMessage(e.data, i); + worker.onerror = (error) => console.error(`Worker ${i} error:`, error); + this.workers.push(worker); + } + } + + private handleWorkerMessage(response: WorkerResponse, workerIndex: number = 0): void { switch (response.type) { case 'compiled': this.isCompiled = response.success; @@ -153,19 +180,24 @@ export class FakeShader { break; case 'rendered': - this.isRendering = false; - if (response.success && response.imageData) { - // Put ImageData on adaptive resolution canvas - this.adaptiveCtx.putImageData(response.imageData, 0, 0); - - // Upscale to main canvas with proper interpolation - this.upscaleToMainCanvas(); - - // Monitor performance and adjust scale - this.updatePerformanceMetrics(); + if (this.workerCount > 1) { + this.handleTileResult(response, workerIndex); } else { - console.error('Render failed:', response.error); - this.fillBlack(); + // Single worker mode + this.isRendering = false; + if (response.success && response.imageData) { + // Put ImageData on adaptive resolution canvas + this.adaptiveCtx.putImageData(response.imageData, 0, 0); + + // Upscale to main canvas with proper interpolation + this.upscaleToMainCanvas(); + + // Monitor performance and adjust scale + this.updatePerformanceMetrics(); + } else { + console.error('Render failed:', response.error); + this.fillBlack(); + } } // Process pending renders @@ -196,11 +228,15 @@ export class FakeShader { private compile(): void { this.isCompiled = false; const id = `compile_${Date.now()}`; - this.worker.postMessage({ - id, - type: 'compile', - code: this.code - } as WorkerMessage); + + // Send compile message to all workers + this.workers.forEach(worker => { + worker.postMessage({ + id, + type: 'compile', + code: this.code + } as WorkerMessage); + }); } private executeRender(id: string): void { @@ -215,9 +251,18 @@ export class FakeShader { this.renderStartTime = performance.now(); this.isRendering = true; + this.currentRenderID = id; const currentTime = (Date.now() - this.startTime) / 1000; - // Scale mouse coordinates to match render resolution + // Always use multiple workers if available + if (this.workerCount > 1) { + this.renderWithMultipleWorkers(id, currentTime); + } else { + this.renderWithSingleWorker(id, currentTime); + } + } + + private renderWithSingleWorker(id: string, currentTime: number): void { const scaledMouseX = this.mouseX * this.currentScale; const scaledMouseY = this.mouseY * this.currentScale; @@ -254,6 +299,62 @@ export class FakeShader { } as WorkerMessage); } + private renderWithMultipleWorkers(id: string, currentTime: number): void { + // Reset tile tracking + this.tileResults.clear(); + this.tilesCompleted = 0; + this.totalTiles = this.workerCount; + + const width = this.adaptiveCanvas.width; + const height = this.adaptiveCanvas.height; + const tileHeight = Math.ceil(height / this.workerCount); + + // Distribute tiles to workers + this.workers.forEach((worker, index) => { + const startY = index * tileHeight; + const endY = Math.min((index + 1) * tileHeight, height); + + if (startY >= height) return; // Skip if tile is outside canvas + + const scaledMouseX = this.mouseX * this.currentScale; + const scaledMouseY = this.mouseY * this.currentScale; + + worker.postMessage({ + id: `${id}_tile_${index}`, + type: 'render', + width: width, + height: endY - startY, + // Pass the Y offset for correct coordinate calculation + startY: startY, + time: currentTime, + renderMode: this.renderMode, + mouseX: scaledMouseX, + mouseY: scaledMouseY, + mousePressed: this.mousePressed, + mouseVX: this.mouseVX * this.currentScale, + mouseVY: this.mouseVY * this.currentScale, + mouseClickTime: this.mouseClickTime, + touchCount: this.touchCount, + touch0X: this.touch0X * this.currentScale, + touch0Y: this.touch0Y * this.currentScale, + touch1X: this.touch1X * this.currentScale, + touch1Y: this.touch1Y * this.currentScale, + pinchScale: this.pinchScale, + pinchRotation: this.pinchRotation, + accelX: this.accelX, + accelY: this.accelY, + accelZ: this.accelZ, + gyroX: this.gyroX, + gyroY: this.gyroY, + gyroZ: this.gyroZ, + audioLevel: this.audioLevel, + bassLevel: this.bassLevel, + midLevel: this.midLevel, + trebleLevel: this.trebleLevel + } as WorkerMessage); + }); + } + setCode(code: string): void { this.code = code; this.compile(); @@ -359,7 +460,63 @@ export class FakeShader { destroy(): void { this.stopAnimation(); - this.worker.terminate(); + this.workers.forEach(worker => worker.terminate()); + } + + private handleTileResult(response: WorkerResponse, workerIndex: number): void { + if (!response.success || !response.imageData) { + console.error(`Tile render failed for worker ${workerIndex}:`, response.error); + return; + } + + // Store tile result + this.tileResults.set(workerIndex, response.imageData); + this.tilesCompleted++; + + // Check if all tiles are complete + if (this.tilesCompleted === this.totalTiles) { + this.compositeTiles(); + } + } + + private compositeTiles(): void { + const width = this.adaptiveCanvas.width; + const height = this.adaptiveCanvas.height; + const tileHeight = Math.ceil(height / this.workerCount); + + // Clear adaptive canvas + this.adaptiveCtx.clearRect(0, 0, width, height); + + // Composite all tiles + for (let i = 0; i < this.workerCount; i++) { + const tileData = this.tileResults.get(i); + if (tileData) { + const startY = i * tileHeight; + this.adaptiveCtx.putImageData(tileData, 0, startY); + } + } + + // Clear tile results + this.tileResults.clear(); + + // Mark rendering as complete + this.isRendering = false; + + // Upscale to main canvas + this.upscaleToMainCanvas(); + + // Monitor performance + this.updatePerformanceMetrics(); + + // Process pending renders + if (this.pendingRenders.length > 0) { + this.pendingRenders.shift(); + if (this.pendingRenders.length > 0) { + const latestId = this.pendingRenders[this.pendingRenders.length - 1]; + this.pendingRenders = [latestId]; + this.executeRender(latestId); + } + } } private upscaleToMainCanvas(): void { @@ -438,6 +595,16 @@ export class FakeShader { return this.currentScale; } + // Simplified method - kept for backward compatibility but always uses all cores + setMultiWorkerMode(enabled: boolean, workerCount?: number): void { + // Always use all available cores, ignore the enabled parameter + console.log(`Multi-worker mode is always enabled, using ${this.workerCount} cores for maximum performance`); + } + + getWorkerCount(): number { + return this.workerCount; + } + static generateRandomCode(): string { const presets = [ 'x^y', diff --git a/src/ShaderWorker.ts b/src/ShaderWorker.ts index d895b24..f6a0e7f 100644 --- a/src/ShaderWorker.ts +++ b/src/ShaderWorker.ts @@ -7,6 +7,7 @@ interface WorkerMessage { height?: number; time?: number; renderMode?: string; + startY?: number; // Y offset for tile rendering mouseX?: number; mouseY?: number; mousePressed?: boolean; @@ -114,7 +115,7 @@ class ShaderWorker { this.compileShader(message.id, message.code!); break; case 'render': - this.renderShader(message.id, message.width!, message.height!, message.time!, message.renderMode || 'classic', message); + this.renderShader(message.id, message.width!, message.height!, message.time!, message.renderMode || 'classic', message, message.startY || 0); break; } } catch (error) { @@ -219,7 +220,7 @@ class ShaderWorker { return hash.toString(36); } - private renderShader(id: string, width: number, height: number, time: number, renderMode: string, message: WorkerMessage): void { + private renderShader(id: string, width: number, height: number, time: number, renderMode: string, message: WorkerMessage, startY: number = 0): void { if (!this.compiledFunction) { this.postError(id, 'No compiled shader'); return; @@ -232,14 +233,14 @@ class ShaderWorker { try { // Use tiled rendering for better timeout handling - this.renderTiled(data, width, height, time, renderMode, message, startTime, maxRenderTime); + this.renderTiled(data, width, height, time, renderMode, message, startTime, maxRenderTime, startY); this.postMessage({ id, type: 'rendered', success: true, imageData }); } catch (error) { this.postError(id, error instanceof Error ? error.message : 'Render failed'); } } - private renderTiled(data: Uint8ClampedArray, width: number, height: number, time: number, renderMode: string, message: WorkerMessage, startTime: number, maxRenderTime: number): void { + private renderTiled(data: Uint8ClampedArray, width: number, height: number, time: number, renderMode: string, message: WorkerMessage, startTime: number, maxRenderTime: number, yOffset: number = 0): void { const tileSize = 64; // 64x64 tiles for better granularity const tilesX = Math.ceil(width / tileSize); const tilesY = Math.ceil(height / tileSize); @@ -259,20 +260,23 @@ class ShaderWorker { const tileEndX = Math.min(tileStartX + tileSize, width); const tileEndY = Math.min(tileStartY + tileSize, height); - this.renderTile(data, width, tileStartX, tileStartY, tileEndX, tileEndY, time, renderMode, message); + this.renderTile(data, width, tileStartX, tileStartY, tileEndX, tileEndY, time, renderMode, message, yOffset); } } } - private renderTile(data: Uint8ClampedArray, width: number, startX: number, startY: number, endX: number, endY: number, time: number, renderMode: string, message: WorkerMessage): void { + private renderTile(data: Uint8ClampedArray, width: number, startX: number, startY: number, endX: number, endY: number, time: number, renderMode: string, message: WorkerMessage, yOffset: number = 0): void { for (let y = startY; y < endY; y++) { for (let x = startX; x < endX; x++) { const i = (y * width + x) * 4; const pixelIndex = y * width + x; + // Adjust y coordinate to account for tile offset + const actualY = y + yOffset; + try { const value = this.compiledFunction!( - x, y, time, pixelIndex, + x, actualY, time, pixelIndex, message.mouseX || 0, message.mouseY || 0, message.mousePressed || false, message.mouseVX || 0, message.mouseVY || 0,