This commit is contained in:
Andras Schmelczer 2026-05-13 21:07:10 +01:00
commit 39b0160064
136 changed files with 7129 additions and 1950 deletions

View file

@ -1,15 +1,19 @@
import { appConfig } from '../config';
import { clamp } from './clamp';
import { exponentialDecay } from './exponential-decay';
export class DeltaTimeCalculator {
private static FPS_EXPONENTIAL_DECAY_STRENGTH = 0.01;
private static FPS_EXPONENTIAL_DECAY_STRENGTH =
appConfig.deltaTime.fpsExponentialDecayStrength;
private previousTime: DOMHighResTimeStamp | null = null;
private deltaTimeAccumulator: number | null = null;
constructor(
private readonly maxDeltaTimeInSeconds: number = 1 / 30,
private readonly minDeltaTimeInSeconds: number = 1 / 240
private readonly maxDeltaTimeInSeconds: number =
appConfig.deltaTime.maxDeltaTimeSeconds,
private readonly minDeltaTimeInSeconds: number =
appConfig.deltaTime.minDeltaTimeSeconds
) {
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
}

View file

@ -0,0 +1,58 @@
import { describe, expect, it, vi } from 'vitest';
import {
createCachedFloat32BufferWrite,
writeFloat32BufferIfChanged,
} from './cached-buffer-write';
const createGpuWriteStub = () => {
const writeBuffer = vi.fn();
const device = {
queue: {
writeBuffer,
},
} as unknown as GPUDevice;
return { device, writeBuffer };
};
describe('cached float32 buffer writes', () => {
it('writes the first value set and skips unchanged values', () => {
const { device, writeBuffer } = createGpuWriteStub();
const buffer = {} as GPUBuffer;
const cache = createCachedFloat32BufferWrite(3);
const values = new Float32Array([1, 2, 3]);
expect(writeFloat32BufferIfChanged(device, buffer, values, cache)).toBe(true);
expect(writeBuffer).toHaveBeenCalledTimes(1);
expect(writeBuffer).toHaveBeenLastCalledWith(buffer, 0, values);
expect(writeFloat32BufferIfChanged(device, buffer, values, cache)).toBe(false);
expect(writeBuffer).toHaveBeenCalledTimes(1);
});
it('writes again when any float changes', () => {
const { device, writeBuffer } = createGpuWriteStub();
const buffer = {} as GPUBuffer;
const cache = createCachedFloat32BufferWrite(3);
expect(
writeFloat32BufferIfChanged(device, buffer, new Float32Array([1, 2, 3]), cache)
).toBe(true);
expect(
writeFloat32BufferIfChanged(device, buffer, new Float32Array([1, 2, 4]), cache)
).toBe(true);
expect(writeBuffer).toHaveBeenCalledTimes(2);
});
it('rejects cache length mismatches before writing', () => {
const { device, writeBuffer } = createGpuWriteStub();
const buffer = {} as GPUBuffer;
const cache = createCachedFloat32BufferWrite(2);
expect(() =>
writeFloat32BufferIfChanged(device, buffer, new Float32Array([1]), cache)
).toThrow('Cached buffer write length mismatch');
expect(writeBuffer).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,36 @@
export interface CachedFloat32BufferWrite {
hasValue: boolean;
previous: Float32Array;
}
export const createCachedFloat32BufferWrite = (
length: number
): CachedFloat32BufferWrite => ({
hasValue: false,
previous: new Float32Array(length),
});
export const writeFloat32BufferIfChanged = (
device: GPUDevice,
buffer: GPUBuffer,
values: Float32Array,
cache: CachedFloat32BufferWrite
): boolean => {
if (values.length !== cache.previous.length) {
throw new Error('Cached buffer write length mismatch');
}
let hasChanged = !cache.hasValue;
for (let i = 0; i < values.length && !hasChanged; i++) {
hasChanged = !Object.is(values[i], cache.previous[i]);
}
if (!hasChanged) {
return false;
}
cache.previous.set(values);
cache.hasValue = true;
device.queue.writeBuffer(buffer, 0, values);
return true;
};

View file

@ -1,39 +0,0 @@
export const persist = <T extends Record<string, number>>(wrapee: T): T => {
const keys = Object.keys(wrapee);
keys.sort();
const keysToShortKeys = Object.fromEntries(keys.map((key) => [key, key]));
const params = new URLSearchParams(window.location.search);
const newParams = new URLSearchParams();
keys.forEach((key) => {
if (params.has(keysToShortKeys[key])) {
(wrapee as any)[key] = Number(params.get(keysToShortKeys[key]));
newParams.set(keysToShortKeys[key], params.get(keysToShortKeys[key])!);
}
});
window.history.replaceState(
{},
'',
`${window.location.pathname}?${newParams.toString()}`
);
return new Proxy(wrapee, {
set: (target, key: string, value: number) => {
const params = new URLSearchParams(window.location.search);
params.set(keysToShortKeys[key], value.toString());
(target as any)[key] = value;
window.history.replaceState(
{},
'',
`${window.location.pathname}?${params.toString()}`
);
return true;
},
});
};