Compare commits

...

2 Commits

Author SHA1 Message Date
b0f650f243 more workers 2025-07-05 23:13:13 +02:00
f7054d8300 some easy wins 2025-07-05 23:02:15 +02:00
2 changed files with 697 additions and 97 deletions

View File

@ -6,6 +6,7 @@ interface WorkerMessage {
height?: number; height?: number;
time?: number; time?: number;
renderMode?: string; renderMode?: string;
startY?: number; // Y offset for tile rendering
mouseX?: number; mouseX?: number;
mouseY?: number; mouseY?: number;
mousePressed?: boolean; mousePressed?: boolean;
@ -43,13 +44,36 @@ export class FakeShader {
private canvas: HTMLCanvasElement; private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D; private ctx: CanvasRenderingContext2D;
private code: string; 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 animationId: number | null = null;
private startTime: number = Date.now(); private startTime: number = Date.now();
private isCompiled: boolean = false; private isCompiled: boolean = false;
private isRendering: boolean = false; private isRendering: boolean = false;
private pendingRenders: string[] = []; private pendingRenders: string[] = [];
private renderMode: string = 'classic'; private renderMode: string = 'classic';
private offscreenCanvas: OffscreenCanvas | null = null;
private offscreenCtx: OffscreenCanvasRenderingContext2D | null = null;
private useOffscreen: boolean = false;
// Adaptive resolution scaling
private adaptiveCanvas: HTMLCanvasElement;
private adaptiveCtx: CanvasRenderingContext2D;
private currentScale: number = 1.0;
private targetRenderTime: number = 16; // Target 60 FPS
private performanceHistory: number[] = [];
private lastScaleAdjustment: number = 0;
private minScale: number = 0.25;
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 mouseX: number = 0;
private mouseY: number = 0; private mouseY: number = 0;
private mousePressed: boolean = false; private mousePressed: boolean = false;
@ -84,15 +108,68 @@ export class FakeShader {
this.ctx = canvas.getContext('2d')!; this.ctx = canvas.getContext('2d')!;
this.code = code; this.code = code;
// Initialize worker // Initialize adaptive resolution canvas
this.worker = new Worker(new URL('./ShaderWorker.ts', import.meta.url), { type: 'module' }); this.initializeAdaptiveCanvas();
this.worker.onmessage = (e: MessageEvent<WorkerResponse>) => this.handleWorkerMessage(e.data);
this.worker.onerror = (error) => console.error('Worker error:', error); // Initialize offscreen canvas if supported
this.initializeOffscreenCanvas();
// 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(); this.compile();
} }
private handleWorkerMessage(response: WorkerResponse): void { private initializeAdaptiveCanvas(): void {
this.adaptiveCanvas = document.createElement('canvas');
this.adaptiveCtx = this.adaptiveCanvas.getContext('2d')!;
this.updateAdaptiveCanvasSize();
}
private updateAdaptiveCanvasSize(): void {
const scaledWidth = Math.floor(this.canvas.width * this.currentScale);
const scaledHeight = Math.floor(this.canvas.height * this.currentScale);
if (this.adaptiveCanvas.width !== scaledWidth || this.adaptiveCanvas.height !== scaledHeight) {
this.adaptiveCanvas.width = scaledWidth;
this.adaptiveCanvas.height = scaledHeight;
}
}
private initializeOffscreenCanvas(): void {
if (typeof OffscreenCanvas !== 'undefined') {
try {
this.offscreenCanvas = new OffscreenCanvas(this.canvas.width, this.canvas.height);
this.offscreenCtx = this.offscreenCanvas.getContext('2d');
this.useOffscreen = this.offscreenCtx !== null;
} catch (error) {
console.warn('OffscreenCanvas not supported:', error);
this.useOffscreen = false;
}
}
}
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) { switch (response.type) {
case 'compiled': case 'compiled':
this.isCompiled = response.success; this.isCompiled = response.success;
@ -103,12 +180,24 @@ export class FakeShader {
break; break;
case 'rendered': case 'rendered':
this.isRendering = false; if (this.workerCount > 1) {
if (response.success && response.imageData) { this.handleTileResult(response, workerIndex);
this.ctx.putImageData(response.imageData, 0, 0);
} else { } else {
console.error('Render failed:', response.error); // Single worker mode
this.fillBlack(); 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 // Process pending renders
@ -139,11 +228,15 @@ export class FakeShader {
private compile(): void { private compile(): void {
this.isCompiled = false; this.isCompiled = false;
const id = `compile_${Date.now()}`; const id = `compile_${Date.now()}`;
this.worker.postMessage({
id, // Send compile message to all workers
type: 'compile', this.workers.forEach(worker => {
code: this.code worker.postMessage({
} as WorkerMessage); id,
type: 'compile',
code: this.code
} as WorkerMessage);
});
} }
private executeRender(id: string): void { private executeRender(id: string): void {
@ -151,27 +244,46 @@ export class FakeShader {
return; return;
} }
// Update adaptive canvas size based on current scale
this.updateAdaptiveCanvasSize();
// Start performance timing
this.renderStartTime = performance.now();
this.isRendering = true; this.isRendering = true;
this.currentRenderID = id;
const currentTime = (Date.now() - this.startTime) / 1000; const currentTime = (Date.now() - this.startTime) / 1000;
// 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;
this.worker.postMessage({ this.worker.postMessage({
id, id,
type: 'render', type: 'render',
width: this.canvas.width, width: this.adaptiveCanvas.width,
height: this.canvas.height, height: this.adaptiveCanvas.height,
time: currentTime, time: currentTime,
renderMode: this.renderMode, renderMode: this.renderMode,
mouseX: this.mouseX, mouseX: scaledMouseX,
mouseY: this.mouseY, mouseY: scaledMouseY,
mousePressed: this.mousePressed, mousePressed: this.mousePressed,
mouseVX: this.mouseVX, mouseVX: this.mouseVX * this.currentScale,
mouseVY: this.mouseVY, mouseVY: this.mouseVY * this.currentScale,
mouseClickTime: this.mouseClickTime, mouseClickTime: this.mouseClickTime,
touchCount: this.touchCount, touchCount: this.touchCount,
touch0X: this.touch0X, touch0X: this.touch0X * this.currentScale,
touch0Y: this.touch0Y, touch0Y: this.touch0Y * this.currentScale,
touch1X: this.touch1X, touch1X: this.touch1X * this.currentScale,
touch1Y: this.touch1Y, touch1Y: this.touch1Y * this.currentScale,
pinchScale: this.pinchScale, pinchScale: this.pinchScale,
pinchRotation: this.pinchRotation, pinchRotation: this.pinchRotation,
accelX: this.accelX, accelX: this.accelX,
@ -187,6 +299,62 @@ export class FakeShader {
} as WorkerMessage); } 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 { setCode(code: string): void {
this.code = code; this.code = code;
this.compile(); this.compile();
@ -292,7 +460,149 @@ export class FakeShader {
destroy(): void { destroy(): void {
this.stopAnimation(); 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 {
// Clear main canvas
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Set interpolation based on scale
if (this.currentScale < 0.5) {
// Use smooth interpolation for heavily downscaled content
this.ctx.imageSmoothingEnabled = true;
this.ctx.imageSmoothingQuality = 'high';
} else {
// Use pixel-perfect scaling for minimal downscaling
this.ctx.imageSmoothingEnabled = false;
}
// Draw scaled content to main canvas
this.ctx.drawImage(
this.adaptiveCanvas,
0, 0, this.adaptiveCanvas.width, this.adaptiveCanvas.height,
0, 0, this.canvas.width, this.canvas.height
);
}
private updatePerformanceMetrics(): void {
const renderTime = performance.now() - this.renderStartTime;
// Add to performance history
this.performanceHistory.push(renderTime);
if (this.performanceHistory.length > 10) {
this.performanceHistory.shift(); // Keep only last 10 measurements
}
// Adjust scale if we have enough data and enough time has passed
const now = performance.now();
if (this.performanceHistory.length >= 3 && now - this.lastScaleAdjustment > 500) {
this.adjustRenderScale();
this.lastScaleAdjustment = now;
}
}
private adjustRenderScale(): void {
// Calculate average render time from recent history
const avgRenderTime = this.performanceHistory.reduce((a, b) => a + b, 0) / this.performanceHistory.length;
const tolerance = 2; // 2ms tolerance
if (avgRenderTime > this.targetRenderTime + tolerance) {
// Too slow - scale down
const newScale = Math.max(this.minScale, this.currentScale * 0.85);
if (newScale !== this.currentScale) {
this.currentScale = newScale;
console.log(`Scaling down to ${(this.currentScale * 100).toFixed(0)}% (${avgRenderTime.toFixed(1)}ms avg)`);
}
} else if (avgRenderTime < this.targetRenderTime - tolerance && this.currentScale < this.maxScale) {
// Fast enough - try scaling up
const newScale = Math.min(this.maxScale, this.currentScale * 1.1);
if (newScale !== this.currentScale) {
this.currentScale = newScale;
console.log(`Scaling up to ${(this.currentScale * 100).toFixed(0)}% (${avgRenderTime.toFixed(1)}ms avg)`);
}
}
}
setAdaptiveQuality(enabled: boolean, targetFPS: number = 60): void {
if (enabled) {
this.targetRenderTime = 1000 / targetFPS;
this.currentScale = 1.0;
this.performanceHistory = [];
} else {
this.currentScale = 1.0;
}
}
getCurrentScale(): number {
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 { static generateRandomCode(): string {

View File

@ -7,6 +7,7 @@ interface WorkerMessage {
height?: number; height?: number;
time?: number; time?: number;
renderMode?: string; renderMode?: string;
startY?: number; // Y offset for tile rendering
mouseX?: number; mouseX?: number;
mouseY?: number; mouseY?: number;
mousePressed?: boolean; mousePressed?: boolean;
@ -43,11 +44,68 @@ interface WorkerResponse {
class ShaderWorker { class ShaderWorker {
private compiledFunction: Function | null = null; private compiledFunction: Function | null = null;
private lastCode: string = ''; private lastCode: string = '';
private mathCache: Map<string, number> = new Map();
private sinTable: Float32Array;
private cosTable: Float32Array;
private expTable: Float32Array;
private logTable: Float32Array;
private imageDataCache: Map<string, ImageData> = new Map();
private compilationCache: Map<string, Function> = new Map();
private colorTables: Map<string, Uint8Array> = new Map();
constructor() { constructor() {
self.onmessage = (e: MessageEvent<WorkerMessage>) => { self.onmessage = (e: MessageEvent<WorkerMessage>) => {
this.handleMessage(e.data); this.handleMessage(e.data);
}; };
this.initializeLookupTables();
this.initializeColorTables();
}
private initializeLookupTables(): void {
const tableSize = 4096;
this.sinTable = new Float32Array(tableSize);
this.cosTable = new Float32Array(tableSize);
this.expTable = new Float32Array(tableSize);
this.logTable = new Float32Array(tableSize);
for (let i = 0; i < tableSize; i++) {
const x = (i / tableSize) * 2 * Math.PI;
this.sinTable[i] = Math.sin(x);
this.cosTable[i] = Math.cos(x);
this.expTable[i] = Math.exp(x / tableSize);
this.logTable[i] = Math.log(1 + x / tableSize);
}
}
private initializeColorTables(): void {
const tableSize = 256;
// Pre-compute color tables for each render mode
const modes = ['classic', 'grayscale', 'red', 'green', 'blue', 'rgb', 'hsv', 'rainbow'];
for (const mode of modes) {
const colorTable = new Uint8Array(tableSize * 3); // RGB triplets
for (let i = 0; i < tableSize; i++) {
const [r, g, b] = this.calculateColorDirect(i, mode);
colorTable[i * 3] = r;
colorTable[i * 3 + 1] = g;
colorTable[i * 3 + 2] = b;
}
this.colorTables.set(mode, colorTable);
}
}
private fastSin(x: number): number {
const index = Math.floor(Math.abs(x * this.sinTable.length / (2 * Math.PI)) % this.sinTable.length);
return this.sinTable[index];
}
private fastCos(x: number): number {
const index = Math.floor(Math.abs(x * this.cosTable.length / (2 * Math.PI)) % this.cosTable.length);
return this.cosTable[index];
} }
private handleMessage(message: WorkerMessage): void { private handleMessage(message: WorkerMessage): void {
@ -57,7 +115,7 @@ class ShaderWorker {
this.compileShader(message.id, message.code!); this.compileShader(message.id, message.code!);
break; break;
case 'render': 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; break;
} }
} catch (error) { } catch (error) {
@ -66,30 +124,60 @@ class ShaderWorker {
} }
private compileShader(id: string, code: string): void { private compileShader(id: string, code: string): void {
const codeHash = this.hashCode(code);
if (code === this.lastCode && this.compiledFunction) { if (code === this.lastCode && this.compiledFunction) {
this.postMessage({ id, type: 'compiled', success: true }); this.postMessage({ id, type: 'compiled', success: true });
return; return;
} }
// Check compilation cache
const cachedFunction = this.compilationCache.get(codeHash);
if (cachedFunction) {
this.compiledFunction = cachedFunction;
this.lastCode = code;
this.postMessage({ id, type: 'compiled', success: true });
return;
}
try { try {
const safeCode = this.sanitizeCode(code); const safeCode = this.sanitizeCode(code);
this.compiledFunction = new Function('x', 'y', 't', 'i', 'mouseX', 'mouseY', 'mousePressed', 'mouseVX', 'mouseVY', 'mouseClickTime', 'touchCount', 'touch0X', 'touch0Y', 'touch1X', 'touch1Y', 'pinchScale', 'pinchRotation', 'accelX', 'accelY', 'accelZ', 'gyroX', 'gyroY', 'gyroZ', 'audioLevel', 'bassLevel', 'midLevel', 'trebleLevel', `
// Timeout protection
const startTime = performance.now();
let iterations = 0;
function checkTimeout() { // Check if expression is static (contains no variables)
iterations++; const isStatic = this.isStaticExpression(safeCode);
if (iterations % 1000 === 0 && performance.now() - startTime > 5) {
throw new Error('Shader timeout'); if (isStatic) {
// Pre-compute static value
const staticValue = this.evaluateStaticExpression(safeCode);
this.compiledFunction = () => staticValue;
} else {
this.compiledFunction = new Function('x', 'y', 't', 'i', 'mouseX', 'mouseY', 'mousePressed', 'mouseVX', 'mouseVY', 'mouseClickTime', 'touchCount', 'touch0X', 'touch0Y', 'touch1X', 'touch1Y', 'pinchScale', 'pinchRotation', 'accelX', 'accelY', 'accelZ', 'gyroX', 'gyroY', 'gyroZ', 'audioLevel', 'bassLevel', 'midLevel', 'trebleLevel', `
// Timeout protection
const startTime = performance.now();
let iterations = 0;
function checkTimeout() {
iterations++;
if (iterations % 1000 === 0 && performance.now() - startTime > 5) {
throw new Error('Shader timeout');
}
} }
}
return (function() { return (function() {
checkTimeout(); checkTimeout();
return ${safeCode}; return ${safeCode};
})(); })();
`); `);
}
// Cache the compiled function
this.compilationCache.set(codeHash, this.compiledFunction);
// Limit cache size to prevent memory bloat
if (this.compilationCache.size > 20) {
const firstKey = this.compilationCache.keys().next().value;
this.compilationCache.delete(firstKey);
}
this.lastCode = code; this.lastCode = code;
this.postMessage({ id, type: 'compiled', success: true }); this.postMessage({ id, type: 'compiled', success: true });
@ -99,78 +187,280 @@ class ShaderWorker {
} }
} }
private renderShader(id: string, width: number, height: number, time: number, renderMode: string, message: WorkerMessage): void { private isStaticExpression(code: string): boolean {
// Check if code contains any variables
const variables = ['x', 'y', 't', 'i', 'mouseX', 'mouseY', 'mousePressed', 'mouseVX', 'mouseVY', 'mouseClickTime', 'touchCount', 'touch0X', 'touch0Y', 'touch1X', 'touch1Y', 'pinchScale', 'pinchRotation', 'accelX', 'accelY', 'accelZ', 'gyroX', 'gyroY', 'gyroZ', 'audioLevel', 'bassLevel', 'midLevel', 'trebleLevel'];
for (const variable of variables) {
if (code.includes(variable)) {
return false;
}
}
return true;
}
private evaluateStaticExpression(code: string): number {
try {
// Safely evaluate numeric expression
const result = new Function(`return ${code}`)();
return isFinite(result) ? result : 0;
} catch (error) {
return 0;
}
}
private hashCode(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash.toString(36);
}
private renderShader(id: string, width: number, height: number, time: number, renderMode: string, message: WorkerMessage, startY: number = 0): void {
if (!this.compiledFunction) { if (!this.compiledFunction) {
this.postError(id, 'No compiled shader'); this.postError(id, 'No compiled shader');
return; return;
} }
const imageData = new ImageData(width, height); const imageData = this.getOrCreateImageData(width, height);
const data = imageData.data; const data = imageData.data;
const startTime = performance.now(); const startTime = performance.now();
const maxRenderTime = 50; // 50ms max render time const maxRenderTime = 50; // 50ms max render time
try { try {
for (let y = 0; y < height; y++) { // Use tiled rendering for better timeout handling
// Check timeout every row this.renderTiled(data, width, height, time, renderMode, message, startTime, maxRenderTime, startY);
if (performance.now() - startTime > maxRenderTime) {
// Fill remaining pixels with black and break
for (let remainingY = y; remainingY < height; remainingY++) {
for (let remainingX = 0; remainingX < width; remainingX++) {
const i = (remainingY * width + remainingX) * 4;
data[i] = 0; // R
data[i + 1] = 0; // G
data[i + 2] = 0; // B
data[i + 3] = 255; // A
}
}
break;
}
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
const pixelIndex = y * width + x;
try {
const value = this.compiledFunction(
x, y, time, pixelIndex,
message.mouseX || 0, message.mouseY || 0,
message.mousePressed || false,
message.mouseVX || 0, message.mouseVY || 0,
message.mouseClickTime || 0,
message.touchCount || 0,
message.touch0X || 0, message.touch0Y || 0,
message.touch1X || 0, message.touch1Y || 0,
message.pinchScale || 1, message.pinchRotation || 0,
message.accelX || 0, message.accelY || 0, message.accelZ || 0,
message.gyroX || 0, message.gyroY || 0, message.gyroZ || 0,
message.audioLevel || 0, message.bassLevel || 0, message.midLevel || 0, message.trebleLevel || 0
);
const safeValue = isFinite(value) ? value : 0;
const [r, g, b] = this.calculateColor(safeValue, renderMode);
data[i] = r; // R
data[i + 1] = g; // G
data[i + 2] = b; // B
data[i + 3] = 255; // A
} catch (error) {
data[i] = 0; // R
data[i + 1] = 0; // G
data[i + 2] = 0; // B
data[i + 3] = 255; // A
}
}
}
this.postMessage({ id, type: 'rendered', success: true, imageData }); this.postMessage({ id, type: 'rendered', success: true, imageData });
} catch (error) { } catch (error) {
this.postError(id, error instanceof Error ? error.message : 'Render failed'); 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, yOffset: number = 0): void {
const tileSize = 64; // 64x64 tiles for better granularity
const tilesX = Math.ceil(width / tileSize);
const tilesY = Math.ceil(height / tileSize);
for (let tileY = 0; tileY < tilesY; tileY++) {
for (let tileX = 0; tileX < tilesX; tileX++) {
// Check timeout before each tile
if (performance.now() - startTime > maxRenderTime) {
const startX = tileX * tileSize;
const startY = tileY * tileSize;
this.fillRemainingPixels(data, width, height, startY, startX);
return;
}
const tileStartX = tileX * tileSize;
const tileStartY = tileY * tileSize;
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, yOffset);
}
}
}
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, actualY, time, pixelIndex,
message.mouseX || 0, message.mouseY || 0,
message.mousePressed || false,
message.mouseVX || 0, message.mouseVY || 0,
message.mouseClickTime || 0,
message.touchCount || 0,
message.touch0X || 0, message.touch0Y || 0,
message.touch1X || 0, message.touch1Y || 0,
message.pinchScale || 1, message.pinchRotation || 0,
message.accelX || 0, message.accelY || 0, message.accelZ || 0,
message.gyroX || 0, message.gyroY || 0, message.gyroZ || 0,
message.audioLevel || 0, message.bassLevel || 0, message.midLevel || 0, message.trebleLevel || 0
);
const safeValue = isFinite(value) ? value : 0;
const [r, g, b] = this.calculateColor(safeValue, renderMode);
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
data[i + 3] = 255;
} catch (error) {
data[i] = 0;
data[i + 1] = 0;
data[i + 2] = 0;
data[i + 3] = 255;
}
}
}
}
private canUseSIMD(): boolean {
return typeof WebAssembly !== 'undefined' && WebAssembly.validate &&
new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]).every((byte, i) => byte === [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00][i]);
}
private renderWithSIMD(data: Uint8ClampedArray, width: number, height: number, time: number, renderMode: string, message: WorkerMessage, startTime: number, maxRenderTime: number): void {
const chunkSize = 4; // Process 4 pixels at once
for (let y = 0; y < height; y++) {
if (performance.now() - startTime > maxRenderTime) {
this.fillRemainingPixels(data, width, height, y, 0);
break;
}
for (let x = 0; x < width; x += chunkSize) {
const endX = Math.min(x + chunkSize, width);
const xValues = new Float32Array(chunkSize);
const yValues = new Float32Array(chunkSize);
const results = new Float32Array(chunkSize);
for (let i = 0; i < endX - x; i++) {
xValues[i] = x + i;
yValues[i] = y;
}
this.computeChunkSIMD(xValues, yValues, results, endX - x, time, message);
for (let i = 0; i < endX - x; i++) {
const pixelX = x + i;
const pixelI = (y * width + pixelX) * 4;
const safeValue = isFinite(results[i]) ? results[i] : 0;
const [r, g, b] = this.calculateColor(safeValue, renderMode);
data[pixelI] = r;
data[pixelI + 1] = g;
data[pixelI + 2] = b;
data[pixelI + 3] = 255;
}
}
}
}
private computeChunkSIMD(xValues: Float32Array, yValues: Float32Array, results: Float32Array, count: number, time: number, message: WorkerMessage): void {
for (let i = 0; i < count; i++) {
try {
const pixelIndex = yValues[i] * (xValues.length / count) + xValues[i];
results[i] = this.compiledFunction!(
xValues[i], yValues[i], time, pixelIndex,
message.mouseX || 0, message.mouseY || 0,
message.mousePressed || false,
message.mouseVX || 0, message.mouseVY || 0,
message.mouseClickTime || 0,
message.touchCount || 0,
message.touch0X || 0, message.touch0Y || 0,
message.touch1X || 0, message.touch1Y || 0,
message.pinchScale || 1, message.pinchRotation || 0,
message.accelX || 0, message.accelY || 0, message.accelZ || 0,
message.gyroX || 0, message.gyroY || 0, message.gyroZ || 0,
message.audioLevel || 0, message.bassLevel || 0, message.midLevel || 0, message.trebleLevel || 0
);
} catch (error) {
results[i] = 0;
}
}
}
private renderSerial(data: Uint8ClampedArray, width: number, height: number, time: number, renderMode: string, message: WorkerMessage, startTime: number, maxRenderTime: number): void {
for (let y = 0; y < height; y++) {
if (performance.now() - startTime > maxRenderTime) {
this.fillRemainingPixels(data, width, height, y, 0);
break;
}
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
const pixelIndex = y * width + x;
try {
const value = this.compiledFunction!(
x, y, time, pixelIndex,
message.mouseX || 0, message.mouseY || 0,
message.mousePressed || false,
message.mouseVX || 0, message.mouseVY || 0,
message.mouseClickTime || 0,
message.touchCount || 0,
message.touch0X || 0, message.touch0Y || 0,
message.touch1X || 0, message.touch1Y || 0,
message.pinchScale || 1, message.pinchRotation || 0,
message.accelX || 0, message.accelY || 0, message.accelZ || 0,
message.gyroX || 0, message.gyroY || 0, message.gyroZ || 0,
message.audioLevel || 0, message.bassLevel || 0, message.midLevel || 0, message.trebleLevel || 0
);
const safeValue = isFinite(value) ? value : 0;
const [r, g, b] = this.calculateColor(safeValue, renderMode);
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
data[i + 3] = 255;
} catch (error) {
data[i] = 0;
data[i + 1] = 0;
data[i + 2] = 0;
data[i + 3] = 255;
}
}
}
}
private fillRemainingPixels(data: Uint8ClampedArray, width: number, height: number, startY: number, startX: number): void {
for (let remainingY = startY; remainingY < height; remainingY++) {
const xStart = remainingY === startY ? startX : 0;
for (let remainingX = xStart; remainingX < width; remainingX++) {
const i = (remainingY * width + remainingX) * 4;
data[i] = 0;
data[i + 1] = 0;
data[i + 2] = 0;
data[i + 3] = 255;
}
}
}
private getOrCreateImageData(width: number, height: number): ImageData {
const key = `${width}x${height}`;
let imageData = this.imageDataCache.get(key);
if (!imageData) {
imageData = new ImageData(width, height);
this.imageDataCache.set(key, imageData);
// Limit cache size to prevent memory bloat
if (this.imageDataCache.size > 5) {
const firstKey = this.imageDataCache.keys().next().value;
this.imageDataCache.delete(firstKey);
}
}
return imageData;
}
private calculateColor(value: number, renderMode: string): [number, number, number] { private calculateColor(value: number, renderMode: string): [number, number, number] {
const absValue = Math.abs(value) % 256; const absValue = Math.abs(value) % 256;
// Use pre-computed color table if available
const colorTable = this.colorTables.get(renderMode);
if (colorTable) {
const index = Math.floor(absValue) * 3;
return [colorTable[index], colorTable[index + 1], colorTable[index + 2]];
}
// Fallback to direct calculation
return this.calculateColorDirect(absValue, renderMode);
}
private calculateColorDirect(absValue: number, renderMode: string): [number, number, number] {
switch (renderMode) { switch (renderMode) {
case 'classic': case 'classic':
return [ return [