small optimizations

This commit is contained in:
2025-07-06 17:11:35 +02:00
parent bf5085431a
commit 8aad6554ed
4 changed files with 144 additions and 90 deletions

View File

@ -476,21 +476,36 @@ export class FakeShader {
}
}
private compositeTiles(): void {
const width = this.canvas.width;
private async compositeTiles(): Promise<void> {
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<ImageBitmap>[] = [];
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

View File

@ -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<string, ShaderFunction> = new LRUCache(
PERFORMANCE.COMPILATION_CACHE_SIZE
);
private colorTables: Map<string, Uint8Array> = 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);
}

View File

@ -56,6 +56,16 @@ export const defaultInputState: InputState = {
export const $input = atom<InputState>(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(

View File

@ -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<string, number> = RENDER_MODES.reduce(
(acc, mode, index) => {
acc[mode] = index;
return acc;
},
{} as Record<string, number>
);
// Storage Keys
export const STORAGE_KEYS = {
SHADERS: 'bitfielder_shaders',