-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathlcms-runner.js
More file actions
274 lines (254 loc) · 11.8 KB
/
Copy pathlcms-runner.js
File metadata and controls
274 lines (254 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
/*
* samples/bench/lcms-runner.js
* ============================
*
* Thin browser-side wrapper around lcms-wasm. Provides the same
* "pinned heap buffers + cmsDoTransform in a tight loop" pattern that
* bench/lcms-comparison/bench.js uses on Node, so the numbers are
* directly comparable.
*
* Exports:
* loadLcms() - initialise the wasm runtime
* buildProfiles(lcms, gracolBytes) - open the working profiles
* freeProfiles(lcms, profiles) - cleanup
* makeLcmsRunner(lcms, wf, flags, input) - returns { run, free, lutBuildMs, kernel }
*
* The runner allocates wasm-heap input/output buffers once, copies the
* input into the heap once, and then each `run()` call is a single
* `_cmsDoTransform` invocation - the production-realistic hot path.
*
* Flag-set choices match bench/lcms-comparison/bench.js so we can compare
* apples-to-apples:
* - cmsFLAGS_HIGHRESPRECALC : forces a large-grid baked precalc LUT.
* Mirrors jsColorEngine `buildLut: true`.
* - cmsFLAGS_NOOPTIMIZE : disables the precalc LUT entirely.
* Each call walks the full per-pixel
* pipeline. Mirrors jsColorEngine
* `buildLut: false` (the "accuracy" path).
*/
const LCMS_DIST = '../lcms-wasm-dist/';
let cachedLcms = null;
let cachedConsts = null;
let cachedBuildInfo = null;
/**
* Inspect lcms.wasm bytes to report whether the build uses WebAssembly
* SIMD. Run once, on demand, for the engine-info panel. Purely
* informational - doesn't affect the bench path.
*
* Heuristic: the SIMD prefix byte 0xfd appears in EVERY v128 opcode
* encoding. If the density is above ~1% of the module size, the build
* is compiled with `-msimd128`. If it is below 0.1% (coincidental
* occurrences in imm data / LEB128 stretches), the build is scalar.
*
* Measured densities:
* - lcms-wasm 1.0.5 stock build: 85 / 315910 = 0.027%
* - jsColorEngine's own tetra3d_simd.wasm: 101 / 1391 = 7.3%
* So the heuristic has a comfortable ~100x margin between the two
* regimes and a single-shot byte scan is all we need.
*/
export async function probeLcmsBuild() {
if (cachedBuildInfo) return cachedBuildInfo;
try {
const r = await fetch(LCMS_DIST + 'lcms.wasm');
if (!r.ok) throw new Error('HTTP ' + r.status);
const bytes = new Uint8Array(await r.arrayBuffer());
let fd = 0;
for (let i = 0; i < bytes.length; i++) if (bytes[i] === 0xfd) fd++;
const density = fd / bytes.length;
cachedBuildInfo = {
size: bytes.length,
fdCount: fd,
density,
hasSimd: density > 0.01,
};
} catch (err) {
cachedBuildInfo = { error: String(err && err.message || err) };
}
return cachedBuildInfo;
}
/**
* Dynamically import lcms-wasm and instantiate the WebAssembly runtime.
* Idempotent - caches both the runtime and the named-constant exports.
*/
export async function loadLcms() {
if (cachedLcms) return { lcms: cachedLcms, consts: cachedConsts };
// Dynamic import - keeps page load light, only fetches when bench is run
const mod = await import(LCMS_DIST + 'lcms.js');
// Tell Emscripten where to find the .wasm sibling. Default behaviour
// resolves it relative to the importing module URL, but we make it
// explicit so the locateFile-style hook works under any base path.
const lcms = await mod.instantiate({
locateFile: (name) => LCMS_DIST + name,
});
cachedLcms = lcms;
cachedConsts = {
TYPE_RGB_8: mod.TYPE_RGB_8,
TYPE_CMYK_8: mod.TYPE_CMYK_8,
TYPE_Lab_8: mod.TYPE_Lab_8,
// 16-bit formats (added in v1.3 for the int16 path comparison).
// lcms's u16 Lab uses the canonical ICC v2/v4 encoding:
// L : [0..0xFFFF] -> [0..100]
// a : [0..0xFFFF] -> [-128..+128] (offset binary)
// b : [0..0xFFFF] -> [-128..+128]
TYPE_RGB_16: mod.TYPE_RGB_16,
TYPE_CMYK_16: mod.TYPE_CMYK_16,
TYPE_Lab_16: mod.TYPE_Lab_16,
INTENT_RELATIVE_COLORIMETRIC: mod.INTENT_RELATIVE_COLORIMETRIC,
cmsFLAGS_HIGHRESPRECALC: mod.cmsFLAGS_HIGHRESPRECALC,
cmsFLAGS_LOWRESPRECALC: mod.cmsFLAGS_LOWRESPRECALC,
cmsFLAGS_NOOPTIMIZE: mod.cmsFLAGS_NOOPTIMIZE,
cmsInfoDescription: mod.cmsInfoDescription || 0,
LCMS_VERSION: mod.LCMS_VERSION,
};
return { lcms, consts: cachedConsts };
}
/**
* Open the four working profiles we need.
*
* `gracolBytes` - raw .icc bytes for the CMYK profile (GRACoL2006_Coated1v2).
* `adobeBytes` - raw .icc bytes for AdobeRGB1998.
*
* AdobeRGB is loaded from bytes rather than synthesised because the
* lcms-wasm build doesn't export cmsCreateRGBProfile / cmsBuildGamma
* (see node_modules/lcms-wasm/lib/export.txt). Critically, we MUST use a
* non-sRGB RGB profile as the output of the RGB->RGB direction - otherwise
* cmsCreateTransform detects the identical sRGB<->sRGB wiring, resolves
* it to an identity at optimisation time, and the resulting MPx/s number
* is ~30% higher than any real RGB->RGB conversion. Measured in Node:
* sRGB -> sRGB: 78.1 MPx/s (identity-optimised)
* sRGB -> AdobeRGB: 59.6 MPx/s (real matrix + curves)
*
* Returns { srgb, adobe, cmyk, lab, cmykName, adobeName } with pointers
* you must free via freeProfiles().
*/
export function buildProfiles(lcms, gracolBytes, adobeBytes) {
const cmyk = lcms.cmsOpenProfileFromMem(gracolBytes, gracolBytes.byteLength);
if (!cmyk) throw new Error('lcms-wasm: cmsOpenProfileFromMem(GRACoL) failed');
const srgb = lcms.cmsCreate_sRGBProfile();
if (!srgb) throw new Error('lcms-wasm: cmsCreate_sRGBProfile failed');
const lab = lcms.cmsCreateLab4Profile(null);
if (!lab) throw new Error('lcms-wasm: cmsCreateLab4Profile failed');
if (!adobeBytes || !adobeBytes.byteLength) {
throw new Error('lcms-wasm: buildProfiles requires AdobeRGB bytes (for a real RGB->RGB, not identity)');
}
const adobe = lcms.cmsOpenProfileFromMem(adobeBytes, adobeBytes.byteLength);
if (!adobe) throw new Error('lcms-wasm: cmsOpenProfileFromMem(AdobeRGB) failed');
// Grab friendly profile descriptions for the engine-info panel. Not all
// builds expose cmsGetProfileInfoASCII the same way - guard it.
const readName = (p, fallback) => {
try {
if (typeof lcms.cmsGetProfileInfoASCII === 'function') {
const desc = lcms.cmsGetProfileInfoASCII(p, 0 /* description */, 'en', 'US');
if (desc && typeof desc === 'string' && desc.length) return desc;
}
} catch (_) { /* fall through */ }
return fallback;
};
const cmykName = readName(cmyk, 'GRACoL2006');
const adobeName = readName(adobe, 'AdobeRGB (1998)');
return { srgb, adobe, cmyk, lab, cmykName, adobeName };
}
export function freeProfiles(lcms, profiles) {
if (!profiles) return;
if (profiles.srgb) lcms.cmsCloseProfile(profiles.srgb);
if (profiles.adobe) lcms.cmsCloseProfile(profiles.adobe);
if (profiles.cmyk) lcms.cmsCloseProfile(profiles.cmyk);
if (profiles.lab) lcms.cmsCloseProfile(profiles.lab);
}
/**
* Build a "runner" object that owns the pinned heap buffers and the
* cmsTransform handle. Matches the shape returned by makeJsceRunner()
* in main.js so the timing harness is identical for both.
*
* @param {object} lcms instantiated lcms-wasm runtime
* @param {object} consts named constants from loadLcms()
* @param {object} wf { pIn, fIn, pOut, fOut, inCh, outCh, outBytesPerChannel? }
* @param {number} flags lcms flag mask (e.g. HIGHRESPRECALC | NOOPTIMIZE)
* @param {Uint8Array|Uint16Array} input pixel buffer (length = pixels * inCh).
* u8 -> 8-bit lcms TYPE_*_8 path,
* u16 -> 16-bit lcms TYPE_*_16 path.
* Bytes-per-channel for the input is
* taken from input.BYTES_PER_ELEMENT.
* @param {number} pixelCount number of pixels to transform per call
*
* For asymmetric workflows (u16 input -> u16 output, but caller passes a u8
* input array somehow) wf.outBytesPerChannel can override the output stride.
* Default behaviour: output stride matches input stride.
*
* Returns:
* {
* run: () => void // hot-path: single _cmsDoTransform call
* free: () => void // free heap buffers + transform handle
* lutBuildMs: number // wall-clock of cmsCreateTransform
* outputBytes: () => Uint8Array // result buffer (raw bytes, heap-backed)
* }
*
* If cmsCreateTransform fails (e.g. unsupported flag combo on this profile),
* throws with a descriptive message - the caller marks the cell as errored
* and continues with the next config.
*/
export function makeLcmsRunner(lcms, consts, wf, flags, input, pixelCount) {
const inBpC = input.BYTES_PER_ELEMENT || 1;
const outBpC = wf.outBytesPerChannel || inBpC;
const inBytes = pixelCount * wf.inCh * inBpC;
const outBytes = pixelCount * wf.outCh * outBpC;
const inPtr = lcms._malloc(inBytes);
const outPtr = lcms._malloc(outBytes);
if (!inPtr || !outPtr) {
if (inPtr) lcms._free(inPtr);
if (outPtr) lcms._free(outPtr);
throw new Error('lcms-wasm: _malloc failed for ' + (inBytes + outBytes) + ' bytes');
}
// Copy input into the wasm heap. For u8 we treat it as a byte stream;
// for u16 we use the heap's u16 view aligned at the same pointer so
// endianness matches lcms (both are little-endian on every supported
// host).
if (inBpC === 2) {
new Uint16Array(lcms.HEAPU8.buffer, inPtr, pixelCount * wf.inCh)
.set(input.subarray(0, pixelCount * wf.inCh));
} else {
lcms.HEAPU8.set(input.subarray(0, inBytes), inPtr);
}
// ---- LUT build time: cmsCreateTransform is the lcms equivalent of
// jsColorEngine's "Transform.create(...)" + buildIntLut. With
// HIGHRESPRECALC it bakes a high-grid precalc LUT here; with
// NOOPTIMIZE it does almost nothing (kernel is built lazily).
const t0 = performance.now();
const xf = lcms.cmsCreateTransform(
wf.pIn, wf.fIn,
wf.pOut, wf.fOut,
consts.INTENT_RELATIVE_COLORIMETRIC,
flags
);
const lutBuildMs = performance.now() - t0;
if (!xf) {
lcms._free(inPtr);
lcms._free(outPtr);
throw new Error('lcms-wasm: cmsCreateTransform failed (flags=0x' +
flags.toString(16) + ')');
}
// The hot-path closure - pinned ptrs, captured in scope, no allocs per call.
function run() {
lcms._cmsDoTransform(xf, inPtr, outPtr, pixelCount);
}
function free() {
lcms.cmsDeleteTransform(xf);
lcms._free(inPtr);
lcms._free(outPtr);
}
// outputView is for sanity-checks; do NOT hold across resize of HEAPU8.
function outputBytes() {
return new Uint8Array(lcms.HEAPU8.buffer, outPtr, outBytes).slice();
}
return { run, free, lutBuildMs, outputBytes };
}
/**
* Pretty short label for an lcms flag set, for the results table.
*/
export function lcmsFlagLabel(consts, flags) {
if (flags === consts.cmsFLAGS_HIGHRESPRECALC) return 'lcms-wasm HIGHRES';
if (flags === consts.cmsFLAGS_LOWRESPRECALC) return 'lcms-wasm LOWRES';
if (flags === consts.cmsFLAGS_NOOPTIMIZE) return 'lcms-wasm NOOPT';
if (flags === 0) return 'lcms-wasm default';
return 'lcms-wasm 0x' + flags.toString(16);
}