fix(web): искажение цветов в предпросмотре правок
Таблицы тональных кривых в WASM-модуле лежали в статическом массиве, который линковщик помещал в сегмент .data — такой сегмент в wasm доступен только для чтения, поэтому запись в него не срабатывала, и модуль считал по мусорным данным (отсюда «каша» вместо плавного градиента). Теперь таблицы считает и передаёт JS: модуль не хранит состояния и только применяет правки к пикселям. Дополнительно клиент сверяет модуль с эталонной JS-реализацией на маленьком образце и, если они расходятся, доверяет JS. Service worker больше не кэширует .wasm: старый модуль в кэше ломал предпросмотр даже после обновления сборки.
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { applyLightToPixels, emptyEditParams, type LightParams } from './edit';
|
||||
import { applyLightToPixels, buildCurveLUT, emptyEditParams, type LightParams } from './edit';
|
||||
|
||||
interface LightExports {
|
||||
memory: WebAssembly.Memory;
|
||||
build_lut(points: number, count: number, channel: number): void;
|
||||
apply(
|
||||
data: number,
|
||||
len: number,
|
||||
luts: number,
|
||||
exposureMul: number,
|
||||
contrastK: number,
|
||||
highlights: number,
|
||||
@@ -19,6 +19,9 @@ interface LightExports {
|
||||
|
||||
let wasm: LightExports;
|
||||
|
||||
const LUTS_OFFSET = 0;
|
||||
const PIXELS_OFFSET = 4096;
|
||||
|
||||
beforeAll(async () => {
|
||||
const bytes = await readFile(new URL('../../static/light.wasm', import.meta.url));
|
||||
const { instance } = await WebAssembly.instantiate(bytes, {});
|
||||
@@ -27,24 +30,27 @@ beforeAll(async () => {
|
||||
|
||||
/** Применяет правки модулем WASM к копии пикселей. */
|
||||
function applyWasm(pixels: Uint8ClampedArray, light: LightParams): Uint8ClampedArray {
|
||||
const points = light.curve?.rgb ?? [];
|
||||
const floats = new Float32Array(points.length * 2);
|
||||
points.forEach((p, i) => {
|
||||
floats[i * 2] = p.x;
|
||||
floats[i * 2 + 1] = p.y;
|
||||
});
|
||||
const mem = wasm.memory;
|
||||
const need = Math.ceil((4096 + pixels.length) / 65536);
|
||||
if (need > mem.buffer.byteLength / 65536) mem.grow(need - mem.buffer.byteLength / 65536);
|
||||
new Float32Array(mem.buffer, 0, floats.length).set(floats);
|
||||
wasm.build_lut(0, points.length, 0);
|
||||
wasm.build_lut(0, 0, 1);
|
||||
wasm.build_lut(0, 0, 2);
|
||||
wasm.build_lut(0, 0, 3);
|
||||
new Uint8ClampedArray(mem.buffer, 4096, pixels.length).set(pixels);
|
||||
const need = Math.ceil((PIXELS_OFFSET + pixels.length) / 65536);
|
||||
const have = mem.buffer.byteLength / 65536;
|
||||
if (need > have) mem.grow(need - have);
|
||||
|
||||
const curve = light.curve ?? {};
|
||||
const tables = [
|
||||
buildCurveLUT(curve.rgb),
|
||||
buildCurveLUT(curve.red),
|
||||
buildCurveLUT(curve.green),
|
||||
buildCurveLUT(curve.blue)
|
||||
];
|
||||
const luts = new Uint8Array(256 * tables.length);
|
||||
tables.forEach((table, i) => luts.set(table, i * 256));
|
||||
new Uint8Array(mem.buffer, LUTS_OFFSET, luts.length).set(luts);
|
||||
|
||||
new Uint8ClampedArray(mem.buffer, PIXELS_OFFSET, pixels.length).set(pixels);
|
||||
wasm.apply(
|
||||
4096,
|
||||
PIXELS_OFFSET,
|
||||
pixels.length,
|
||||
LUTS_OFFSET,
|
||||
Math.pow(2, light.exposure),
|
||||
1 + light.contrast / 100,
|
||||
(light.highlights / 100) * 80,
|
||||
@@ -52,7 +58,7 @@ function applyWasm(pixels: Uint8ClampedArray, light: LightParams): Uint8ClampedA
|
||||
(light.whites / 100) * 80,
|
||||
(light.blacks / 100) * 80
|
||||
);
|
||||
return new Uint8ClampedArray(mem.buffer, 4096, pixels.length).slice();
|
||||
return new Uint8ClampedArray(mem.buffer, PIXELS_OFFSET, pixels.length).slice();
|
||||
}
|
||||
|
||||
describe('WASM-модуль фильтров', () => {
|
||||
@@ -86,4 +92,10 @@ describe('WASM-модуль фильтров', () => {
|
||||
const out = applyWasm(pixels, light);
|
||||
expect(Array.from(out)).toEqual([10, 20, 30, 0]);
|
||||
});
|
||||
|
||||
it('осветляет при положительной экспозиции', () => {
|
||||
const light: LightParams = { ...emptyEditParams().light, exposure: 1 };
|
||||
const out = applyWasm(new Uint8ClampedArray([100, 100, 100, 255]), light);
|
||||
expect(out[0]).toBeGreaterThan(100);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user