more workers

This commit is contained in:
2025-07-05 23:13:13 +02:00
parent f7054d8300
commit b0f650f243
2 changed files with 203 additions and 32 deletions

View File

@ -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<number, ImageData> = 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<WorkerResponse>) => 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<WorkerResponse>) => 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,6 +180,10 @@ export class FakeShader {
break;
case 'rendered':
if (this.workerCount > 1) {
this.handleTileResult(response, workerIndex);
} else {
// Single worker mode
this.isRendering = false;
if (response.success && response.imageData) {
// Put ImageData on adaptive resolution canvas
@ -167,6 +198,7 @@ export class FakeShader {
console.error('Render failed:', response.error);
this.fillBlack();
}
}
// Process pending renders
if (this.pendingRenders.length > 0) {
@ -196,11 +228,15 @@ export class FakeShader {
private compile(): void {
this.isCompiled = false;
const id = `compile_${Date.now()}`;
this.worker.postMessage({
// 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',

View File

@ -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,