diff --git a/src/FakeShader.ts b/src/FakeShader.ts index d69b492..e1f50ad 100644 --- a/src/FakeShader.ts +++ b/src/FakeShader.ts @@ -476,21 +476,36 @@ export class FakeShader { } } - private compositeTiles(): void { - const width = this.canvas.width; + private async compositeTiles(): Promise { const height = this.canvas.height; const tileHeight = Math.ceil(height / this.workerCount); - // Clear main canvas - this.ctx.clearRect(0, 0, width, height); - - // Composite all tiles directly on main canvas - for (let i = 0; i < this.workerCount; i++) { - const tileData = this.tileResults.get(i); - if (tileData) { - const startY = i * tileHeight; - this.ctx.putImageData(tileData, 0, startY); + // Use ImageBitmap for faster compositing if available + if (typeof createImageBitmap !== 'undefined') { + try { + const bitmapPromises: Promise[] = []; + const positions: number[] = []; + + for (let i = 0; i < this.workerCount; i++) { + const tileData = this.tileResults.get(i); + if (tileData) { + bitmapPromises.push(createImageBitmap(tileData)); + positions.push(i * tileHeight); + } + } + + const bitmaps = await Promise.all(bitmapPromises); + + for (let i = 0; i < bitmaps.length; i++) { + this.ctx.drawImage(bitmaps[i], 0, positions[i]); + bitmaps[i].close(); // Free memory + } + } catch (error) { + // Fallback to putImageData if ImageBitmap fails + this.fallbackCompositeTiles(); } + } else { + this.fallbackCompositeTiles(); } // Clear tile results @@ -510,6 +525,18 @@ export class FakeShader { } } + private fallbackCompositeTiles(): void { + const tileHeight = Math.ceil(this.canvas.height / this.workerCount); + + for (let i = 0; i < this.workerCount; i++) { + const tileData = this.tileResults.get(i); + if (tileData) { + const startY = i * tileHeight; + this.ctx.putImageData(tileData, 0, startY); + } + } + } + // 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 diff --git a/src/ShaderWorker.ts b/src/ShaderWorker.ts index ecfe55f..0c0780b 100644 --- a/src/ShaderWorker.ts +++ b/src/ShaderWorker.ts @@ -46,7 +46,7 @@ interface WorkerResponse { import { LRUCache } from './utils/LRUCache'; import { calculateColorDirect } from './utils/colorModes'; -import { PERFORMANCE, COLOR_TABLE_SIZE } from './utils/constants'; +import { PERFORMANCE, COLOR_TABLE_SIZE, RENDER_MODES, RENDER_MODE_INDEX } from './utils/constants'; type ShaderFunction = (...args: number[]) => number; @@ -59,7 +59,7 @@ class ShaderWorker { private compilationCache: LRUCache = new LRUCache( PERFORMANCE.COMPILATION_CACHE_SIZE ); - private colorTables: Map = new Map(); + private colorTables: Uint8Array[] = []; private feedbackBuffer: Float32Array | null = null; constructor() { @@ -73,19 +73,11 @@ class ShaderWorker { private initializeColorTables(): void { const tableSize = COLOR_TABLE_SIZE; - // Pre-compute color tables for each render mode - const modes = [ - 'classic', - 'grayscale', - 'red', - 'green', - 'blue', - 'rgb', - 'hsv', - 'rainbow', - ]; + // Pre-compute color tables for each render mode using array indexing + this.colorTables = new Array(RENDER_MODES.length); - for (const mode of modes) { + for (let modeIndex = 0; modeIndex < RENDER_MODES.length; modeIndex++) { + const mode = RENDER_MODES[modeIndex]; const colorTable = new Uint8Array(tableSize * 3); // RGB triplets for (let i = 0; i < tableSize; i++) { @@ -95,7 +87,7 @@ class ShaderWorker { colorTable[i * 3 + 2] = b; } - this.colorTables.set(mode, colorTable); + this.colorTables[modeIndex] = colorTable; } } @@ -228,44 +220,10 @@ 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; + // Check if code contains any variables using regex for better accuracy + const variablePattern = /\b(x|y|t|i|r|a|u|v|c|f|d|n|b|mouse[XY]|mousePressed|mouseV[XY]|mouseClickTime|touchCount|touch[01][XY]|pinchScale|pinchRotation|accel[XYZ]|gyro[XYZ]|audioLevel|bassLevel|midLevel|trebleLevel)\b/; + + return !variablePattern.test(code); } private evaluateStaticExpression(code: string): number { @@ -836,14 +794,15 @@ class ShaderWorker { break; } - // Use pre-computed color table if available - const colorTable = this.colorTables.get(renderMode); - if (colorTable) { + // Use pre-computed color table with O(1) array indexing + const modeIndex = RENDER_MODE_INDEX[renderMode]; + if (modeIndex !== undefined && this.colorTables[modeIndex]) { + const colorTable = this.colorTables[modeIndex]; const index = Math.floor(processedValue) * 3; return [colorTable[index], colorTable[index + 1], colorTable[index + 2]]; } - // Fallback to direct calculation + // Fallback to direct calculation for unknown render modes return calculateColorDirect(processedValue, renderMode); } diff --git a/src/stores/input.ts b/src/stores/input.ts index f966dff..d1b3268 100644 --- a/src/stores/input.ts +++ b/src/stores/input.ts @@ -56,6 +56,16 @@ export const defaultInputState: InputState = { export const $input = atom(defaultInputState); +let mouseUpdatePending = false; +let pendingMouseData = { + x: 0, + y: 0, + pressed: false, + vx: 0, + vy: 0, + clickTime: 0, +}; + export function updateMousePosition( x: number, y: number, @@ -64,17 +74,37 @@ export function updateMousePosition( vy: number, clickTime: number ) { - $input.set({ - ...$input.get(), - mouseX: x, - mouseY: y, - mousePressed: pressed, - mouseVX: vx, - mouseVY: vy, - mouseClickTime: clickTime, - }); + pendingMouseData = { x, y, pressed, vx, vy, clickTime }; + + if (!mouseUpdatePending) { + mouseUpdatePending = true; + requestAnimationFrame(() => { + const current = $input.get(); + $input.set({ + ...current, + mouseX: pendingMouseData.x, + mouseY: pendingMouseData.y, + mousePressed: pendingMouseData.pressed, + mouseVX: pendingMouseData.vx, + mouseVY: pendingMouseData.vy, + mouseClickTime: pendingMouseData.clickTime, + }); + mouseUpdatePending = false; + }); + } } +let touchUpdatePending = false; +let pendingTouchData = { + count: 0, + x0: 0, + y0: 0, + x1: 0, + y1: 0, + scale: 1, + rotation: 0, +}; + export function updateTouchPosition( count: number, x0: number, @@ -84,16 +114,25 @@ export function updateTouchPosition( scale: number, rotation: number ) { - $input.set({ - ...$input.get(), - touchCount: count, - touch0X: x0, - touch0Y: y0, - touch1X: x1, - touch1Y: y1, - pinchScale: scale, - pinchRotation: rotation, - }); + pendingTouchData = { count, x0, y0, x1, y1, scale, rotation }; + + if (!touchUpdatePending) { + touchUpdatePending = true; + requestAnimationFrame(() => { + const current = $input.get(); + $input.set({ + ...current, + touchCount: pendingTouchData.count, + touch0X: pendingTouchData.x0, + touch0Y: pendingTouchData.y0, + touch1X: pendingTouchData.x1, + touch1Y: pendingTouchData.y1, + pinchScale: pendingTouchData.scale, + pinchRotation: pendingTouchData.rotation, + }); + touchUpdatePending = false; + }); + } } export function updateDeviceMotion( diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 18326c0..41cad5f 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -7,18 +7,47 @@ export const UI_HEIGHTS = { // Performance Constants export const PERFORMANCE = { - DEFAULT_TILE_SIZE: 64, + DEFAULT_TILE_SIZE: 128, MAX_RENDER_TIME_MS: 50, MAX_SHADER_TIMEOUT_MS: 5, TIMEOUT_CHECK_INTERVAL: 1000, MAX_SAVED_SHADERS: 50, - IMAGE_DATA_CACHE_SIZE: 5, - COMPILATION_CACHE_SIZE: 20, + IMAGE_DATA_CACHE_SIZE: 10, + COMPILATION_CACHE_SIZE: 30, } as const; // Color Constants export const COLOR_TABLE_SIZE = 256; +// Render Mode Constants - Keep in sync with color modes +export const RENDER_MODES = [ + 'classic', + 'grayscale', + 'red', + 'green', + 'blue', + 'rgb', + 'hsv', + 'rainbow', + 'thermal', + 'neon', + 'cyberpunk', + 'vaporwave', + 'dithered', + 'palette', +] as const; + +export type RenderMode = (typeof RENDER_MODES)[number]; + +// Create a mapping from render mode to index for O(1) lookups +export const RENDER_MODE_INDEX: Record = RENDER_MODES.reduce( + (acc, mode, index) => { + acc[mode] = index; + return acc; + }, + {} as Record +); + // Storage Keys export const STORAGE_KEYS = { SHADERS: 'bitfielder_shaders',