some easy wins
This commit is contained in:
@ -50,6 +50,21 @@ export class FakeShader {
|
||||
private isRendering: boolean = false;
|
||||
private pendingRenders: string[] = [];
|
||||
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;
|
||||
|
||||
private mouseX: number = 0;
|
||||
private mouseY: number = 0;
|
||||
private mousePressed: boolean = false;
|
||||
@ -84,6 +99,12 @@ export class FakeShader {
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
this.code = code;
|
||||
|
||||
// Initialize adaptive resolution canvas
|
||||
this.initializeAdaptiveCanvas();
|
||||
|
||||
// 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);
|
||||
@ -92,6 +113,35 @@ export class FakeShader {
|
||||
this.compile();
|
||||
}
|
||||
|
||||
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 handleWorkerMessage(response: WorkerResponse): void {
|
||||
switch (response.type) {
|
||||
case 'compiled':
|
||||
@ -105,7 +155,14 @@ export class FakeShader {
|
||||
case 'rendered':
|
||||
this.isRendering = false;
|
||||
if (response.success && response.imageData) {
|
||||
this.ctx.putImageData(response.imageData, 0, 0);
|
||||
// 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();
|
||||
@ -151,27 +208,37 @@ export class FakeShader {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update adaptive canvas size based on current scale
|
||||
this.updateAdaptiveCanvasSize();
|
||||
|
||||
// Start performance timing
|
||||
this.renderStartTime = performance.now();
|
||||
|
||||
this.isRendering = true;
|
||||
const currentTime = (Date.now() - this.startTime) / 1000;
|
||||
|
||||
// Scale mouse coordinates to match render resolution
|
||||
const scaledMouseX = this.mouseX * this.currentScale;
|
||||
const scaledMouseY = this.mouseY * this.currentScale;
|
||||
|
||||
this.worker.postMessage({
|
||||
id,
|
||||
type: 'render',
|
||||
width: this.canvas.width,
|
||||
height: this.canvas.height,
|
||||
width: this.adaptiveCanvas.width,
|
||||
height: this.adaptiveCanvas.height,
|
||||
time: currentTime,
|
||||
renderMode: this.renderMode,
|
||||
mouseX: this.mouseX,
|
||||
mouseY: this.mouseY,
|
||||
mouseX: scaledMouseX,
|
||||
mouseY: scaledMouseY,
|
||||
mousePressed: this.mousePressed,
|
||||
mouseVX: this.mouseVX,
|
||||
mouseVY: this.mouseVY,
|
||||
mouseVX: this.mouseVX * this.currentScale,
|
||||
mouseVY: this.mouseVY * this.currentScale,
|
||||
mouseClickTime: this.mouseClickTime,
|
||||
touchCount: this.touchCount,
|
||||
touch0X: this.touch0X,
|
||||
touch0Y: this.touch0Y,
|
||||
touch1X: this.touch1X,
|
||||
touch1Y: this.touch1Y,
|
||||
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,
|
||||
@ -295,6 +362,82 @@ export class FakeShader {
|
||||
this.worker.terminate();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static generateRandomCode(): string {
|
||||
const presets = [
|
||||
'x^y',
|
||||
|
||||
@ -43,11 +43,68 @@ interface WorkerResponse {
|
||||
class ShaderWorker {
|
||||
private compiledFunction: Function | null = null;
|
||||
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() {
|
||||
self.onmessage = (e: MessageEvent<WorkerMessage>) => {
|
||||
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 {
|
||||
@ -66,30 +123,60 @@ class ShaderWorker {
|
||||
}
|
||||
|
||||
private compileShader(id: string, code: string): void {
|
||||
const codeHash = this.hashCode(code);
|
||||
|
||||
if (code === this.lastCode && this.compiledFunction) {
|
||||
this.postMessage({ id, type: 'compiled', success: true });
|
||||
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 {
|
||||
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() {
|
||||
iterations++;
|
||||
if (iterations % 1000 === 0 && performance.now() - startTime > 5) {
|
||||
throw new Error('Shader timeout');
|
||||
|
||||
// Check if expression is static (contains no variables)
|
||||
const isStatic = this.isStaticExpression(safeCode);
|
||||
|
||||
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() {
|
||||
checkTimeout();
|
||||
return ${safeCode};
|
||||
})();
|
||||
`);
|
||||
|
||||
return (function() {
|
||||
checkTimeout();
|
||||
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.postMessage({ id, type: 'compiled', success: true });
|
||||
@ -99,78 +186,277 @@ class ShaderWorker {
|
||||
}
|
||||
}
|
||||
|
||||
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): void {
|
||||
if (!this.compiledFunction) {
|
||||
this.postError(id, 'No compiled shader');
|
||||
return;
|
||||
}
|
||||
|
||||
const imageData = new ImageData(width, height);
|
||||
const imageData = this.getOrCreateImageData(width, height);
|
||||
const data = imageData.data;
|
||||
const startTime = performance.now();
|
||||
const maxRenderTime = 50; // 50ms max render time
|
||||
|
||||
try {
|
||||
for (let y = 0; y < height; y++) {
|
||||
// Check timeout every row
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use tiled rendering for better timeout handling
|
||||
this.renderTiled(data, width, height, time, renderMode, message, startTime, maxRenderTime);
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderTile(data: Uint8ClampedArray, width: number, startX: number, startY: number, endX: number, endY: number, time: number, renderMode: string, message: WorkerMessage): 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;
|
||||
|
||||
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 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] {
|
||||
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) {
|
||||
case 'classic':
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user