317 lines
8.4 KiB
TypeScript
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(); |