Files
bitfielder/src/shader/worker/ShaderWorker.ts
2025-07-14 21:08:21 +02:00

317 lines
8.4 KiB
TypeScript

import { WorkerMessage, WorkerResponse, createDefaultShaderContext } from '../types';
import { ShaderCompiler } from '../core/ShaderCompiler';
import { ShaderCache } from '../core/ShaderCache';
import { FeedbackSystem } from '../rendering/FeedbackSystem';
import { PixelRenderer } from '../rendering/PixelRenderer';
import { PERFORMANCE } from '../../utils/constants';
/**
* Main shader worker class - handles compilation and rendering
*/
class ShaderWorker {
private compiledFunction: any = null;
private lastCode: string = '';
private cache: ShaderCache;
private feedbackSystem: FeedbackSystem;
private pixelRenderer: PixelRenderer;
private shaderContext = createDefaultShaderContext();
private lastFrameTime: number = 0;
constructor() {
this.cache = new ShaderCache();
this.feedbackSystem = new FeedbackSystem();
this.pixelRenderer = new PixelRenderer(this.feedbackSystem, this.shaderContext);
self.onmessage = (e: MessageEvent<WorkerMessage>) => {
this.handleMessage(e.data);
};
}
private handleMessage(message: WorkerMessage): void {
try {
switch (message.type) {
case 'compile':
this.compileShader(message.id, message.code!);
break;
case 'render':
this.renderShader(
message.id,
message.width!,
message.height!,
message.time!,
message.renderMode || 'classic',
message.valueMode || 'integer',
message,
message.startY || 0
);
break;
}
} catch (error) {
this.postError(
message.id,
error instanceof Error ? error.message : 'Unknown error'
);
}
}
private compileShader(id: string, code: string): void {
const codeHash = ShaderCompiler.hashCode(code);
if (code === this.lastCode && this.compiledFunction) {
this.postMessage({ id, type: 'compiled', success: true });
return;
}
// Check compilation cache
const cachedFunction = this.cache.getCompiledShader(codeHash);
if (cachedFunction) {
this.compiledFunction = cachedFunction;
this.lastCode = code;
this.postMessage({ id, type: 'compiled', success: true });
return;
}
try {
this.compiledFunction = ShaderCompiler.compile(code);
// Cache the compiled function
if (this.compiledFunction) {
this.cache.setCompiledShader(codeHash, this.compiledFunction);
}
this.lastCode = code;
this.postMessage({ id, type: 'compiled', success: true });
} catch (error) {
this.compiledFunction = null;
this.postError(
id,
error instanceof Error ? error.message : 'Compilation failed'
);
}
}
private renderShader(
id: string,
width: number,
height: number,
time: number,
renderMode: string,
valueMode: string,
message: WorkerMessage,
startY: number = 0
): void {
if (!this.compiledFunction) {
this.postError(id, 'No compiled shader');
return;
}
const imageData = this.cache.getOrCreateImageData(width, height);
const data = imageData.data;
const startTime = performance.now();
const maxRenderTime = PERFORMANCE.MAX_RENDER_TIME_MS;
// Initialize feedback buffers if needed
this.feedbackSystem.initializeBuffers(width, height);
// Update frame timing for frame rate independence
const deltaTime = time - this.lastFrameTime;
this.lastFrameTime = time;
try {
// Use tiled rendering for better timeout handling
this.renderTiled(
data,
width,
height,
time,
renderMode,
valueMode,
message,
startTime,
maxRenderTime,
startY,
deltaTime
);
// Finalize frame processing
this.feedbackSystem.finalizeFrame();
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,
valueMode: string,
message: WorkerMessage,
startTime: number,
maxRenderTime: number,
yOffset: number = 0,
deltaTime: number = 0.016
): void {
const tileSize = PERFORMANCE.DEFAULT_TILE_SIZE;
const tilesX = Math.ceil(width / tileSize);
const tilesY = Math.ceil(height / tileSize);
// Pre-calculate constants outside the loop for performance
const fullWidth = message.fullWidth || width;
const fullHeight = message.fullHeight || message.height! + yOffset;
const centerX = fullWidth / 2;
const centerY = fullHeight / 2;
const maxDistance = Math.sqrt(centerX ** 2 + centerY ** 2);
const invMaxDistance = 1 / maxDistance;
const invFullWidth = 1 / fullWidth;
const invFullHeight = 1 / fullHeight;
const frameCount = Math.floor(time * 60);
const goldenRatio = 1.618033988749;
const phase = (time * Math.PI * 2) % (Math.PI * 2);
const timeTwoPi = time * 2 * Math.PI;
const fullWidthHalf = fullWidth >> 1;
const fullHeightHalf = fullHeight >> 1;
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,
valueMode,
message,
yOffset,
deltaTime,
// Pre-calculated constants
centerX,
centerY,
maxDistance,
invMaxDistance,
invFullWidth,
invFullHeight,
frameCount,
goldenRatio,
phase,
timeTwoPi,
fullWidthHalf,
fullHeightHalf
);
}
}
}
private renderTile(
data: Uint8ClampedArray,
width: number,
startX: number,
startY: number,
endX: number,
endY: number,
time: number,
renderMode: string,
valueMode: string,
message: WorkerMessage,
yOffset: number,
deltaTime: number,
// Pre-calculated constants
centerX: number,
centerY: number,
maxDistance: number,
invMaxDistance: number,
invFullWidth: number,
invFullHeight: number,
frameCount: number,
goldenRatio: number,
phase: number,
timeTwoPi: number,
fullWidthHalf: number,
fullHeightHalf: number
): void {
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
const actualY = y + yOffset;
this.pixelRenderer.renderPixel(
data,
x,
y,
actualY,
width,
time,
renderMode,
valueMode,
message,
this.compiledFunction,
// Pre-calculated constants
centerX,
centerY,
maxDistance,
invMaxDistance,
invFullWidth,
invFullHeight,
frameCount,
goldenRatio,
phase,
timeTwoPi,
fullWidthHalf,
fullHeightHalf,
deltaTime
);
}
}
}
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 postMessage(response: WorkerResponse): void {
self.postMessage(response);
}
private postError(id: string, error: string): void {
this.postMessage({ id, type: 'error', success: false, error });
}
}
// Initialize worker
new ShaderWorker();