Skip to content

Latest commit

 

History

History
135 lines (117 loc) · 11.1 KB

File metadata and controls

135 lines (117 loc) · 11.1 KB

Deep Dive

jsColorEngine docs: ← Project README · Bench · Performance · Roadmap · Examples · API: Profile · Transform · Loader


This folder is the "how it works and why it's fast" layer of the docs. The project README tells you what it does; Bench.md and BenchResults.md are the current numbers. Performance.md is the measurement retrospective — what we learned measuring it. This folder answers why.

If you're here you probably:

  • Want to know whether to trust the numbers
  • Are considering using (or writing) a JavaScript CMS and want to see what "hand-tuned for the JIT" actually means in practice
  • Are curious why jsColorEngine's native-JS hot path can keep up with an Emscripten wasm32 build of LittleCMS
  • Are reviewing a PR against the kernel code and want the rationale for a design decision

Shallow dive — the cheeky TL;DR

Four things we learned writing this that we didn't expect going in. Each one has a deep-dive page behind it if you want the receipts.

  • Modern JS JITs emit wicked good assembly when the loop is well-shaped. TurboFan lowered our hand-unrolled tetrahedral interpolator to inline x64 SSE/AVX with no spills, no boxing, no allocations, and zero int↔float conversions. If you blanked the filenames you couldn't tell the int kernel's hot body from C. The reputation that numeric JS is slow is about a decade out of date on monomorphic typed-array loops.JIT inspection

  • Dedicated, unrolled hot loops are still the win. The "tidy it up into a helper function" instinct is wrong here. Our unrolled 6-branch kernels fit in 24–34 % of a 32 KB L1i; re-rolling them would save 5 KB but cost 5–10 cycles per pixel in branch mispredicts. The counter-intuitive code is the fast code, and there's a 200-line comment block at the top of src/Transform.js to make sure nobody "cleans it up". → LUT modes, Architecture

  • WASM SIMD does 4× the work per cycle and it feels like cheating. Channel-parallel v128 lanes + v128.load64_zero instead of a gather got us 3.0-3.5× over JS 'int' on the 3D tetrahedral kernel, bit-exact, in 1.3 KB of .wasm. The hardware really does have this much headroom — you just have to pick the right axis to vectorise along. (We got the axis wrong the first time. See the page.) → WASM kernels

  • Plain-optimised JS can beat an Emscripten-wasm32 port of a battle-hardened C library. jsColorEngine's 'int' JS kernel beats lcms-wasm on every direction we benchmarked. That isn't "JS vs C"; it's "one specialised kernel per LUT shape tuned for V8" vs "a general-purpose C codebase compiled through Emscripten, carrying all of lcms2's dispatcher generality, with no SIMD and no Fast Float plugin". The comparison is really about specialisation, not language choice. → LcmsComparison, historical notes in Performance

  • And the answers match too — jsCE's float pipeline agrees with lcms's float pipeline within visual-noise levels across a 130-file ICC oracle suite (worst case 0.06 ΔE76 on Lab outputs, 1.24 LSB on RGB, 0.04 % ink on CMYK). The one structural divergence (~17 LSB on a niche grey-1c Perceptual path) is fully diagnosed and parked with a documented reason. We're a faithful float-precision peer, not a port. → Accuracy

The rest of this folder is the evidence. If any of these claims smell wrong, the detail pages carry the asm dumps, bench scripts, and repro recipes.

Contents

Page What it covers
Architecture Pipeline model: how an ICC profile becomes a kernel. Stages, LUT build, kernel dispatch, accuracy vs image paths
LUT modes float / int / int-wasm-scalar / int-wasm-simd — what each mode is, when it's picked, how it's bit-exact vs the reference
JIT inspection V8 emitted x64 assembly walked line-by-line. Working-set size, instruction mix, move classification, the "named temps" micro-test. Why the scalar JS kernel is as fast as it is
WASM kernels Hand-written .wat for 3D and 4D tetrahedral interp. SIMD channel-parallel layout, rolling-shutter pack, the V8 inliner lesson. Reproduction recipes
Compiled pipeline (POC) transform.compile() — turning the runtime stage walker into one straight-line JS function per profile chain. 1.75× on sRGB→CMYK, three measurement methods, and the path to getSource() / toModule()
Accuracy jsColorEngine vs Little CMS — the bench/lcms_compat harness, methodology, headline numbers (130/150 files sub-LSB), the one localised divergence we found, and the design philosophy that keeps jsCE an independent engine rather than an lcms reimplementation
Custom LUT Builder Custom LUT creation, TIFF-based visual editing, lcms-wasm bridge, portable JSON serialisation format, and the architecture for CMS-agnostic LUT capture and redistribution. Companion how-to: samples/LutBuilder/lutbuilder.md.
Identity / NOP detection How same-profile pairs are detected (binary hash, virtual name, matrix comparison), how multi-stage chains are collapsed, the identity pipeline and kernelCopy path, and the connection to kernel binding.
The kernel contract As-built v1.6. Transform owns the pipeline; the kernel owns both surfaces (floatFor + array()). Dense registry kernels[0..15] (Identity at 0); Kernel3D may yield the matrix shaper; batch-size dispatch is the kernel's secret. Includes the journey from the v1.5 file split, a “do not reinvent” table, coverage, and the N-channel oracle gap.
Kernel modules Stub. v1.5 snapshot retired; redirects to KernelContract.md.
Synthetic profiles Three of five kernels had no second opinion, because real ICC profiles are licensed and this repo ships two. So the engine writes its own: encodeICC.js, fifteen dual-table profiles at 1-15 channels, handed to Little CMS. Also: gridFor is a fixture budget, not an ICC rule — real 5/6/7 A2B tables are typically 9 / 7–9 / 5 pts/axis; 2-pt at 11+ is ours.
Matrix-shaper WASM kernel The fused curve + 3x3 + curve path in WebAssembly, int8 and int16, with a bit-identical scalar fallback and five alpha entry points. 328 MPx/s on photographs against ~119 for the CLUT (see BenchResults for current numbers), and more accurate than it — the CLUT's interpolation error is what the kernel removes. (shipped in v1.5.5)
Why MPE is not supported Why multiProcessElementsType (mpet) / DToB/BToD tags are not decoded: spec-mandated fallback to AToB/BToA, near-zero real-world prevalence, and why ICC's own answer to HDR (iccMAX) made MPE a dead end.
Why named colour profiles are not supported Why ncl2 profiles are outside transform scope: they are pure name→Lab/XYZ lookup tables with no pipeline, and real-world spot colour workflows use RIP-internal databases, PANTONE-licensed app libraries, or CxF/X-4 instead.
The Good / Bad / Ugly (archived) Failed Schrödinger's Bench reproduction. Off the samples index; pixels are the old 256-colour LCG. Lesson in benchmark.md §20.
LUT Accuracy Baseline interpolation error for standard LUT grid sizes, measured against the full f64 accuracy pipeline. (In progress — not yet published.)

Learn more (external)

If jsColorEngine is your first brush with colour management, or you want to go deeper on the topics we build on:

  • LittleCMSlittlecms.com · source (mm2/Little-CMS) · API manual PDF. LittleCMS is the reference open-source CMS in C. A lot of jsColorEngine's core concepts — pipeline stages, tetrahedral interpolation, rendering intents — are lifted from LittleCMS. This is not a port, but reading lcms2/src/cmsintrp.c is time well spent.
  • International Color Consortium (ICC)color.org — the standards body. Good starting point for "what even is an ICC profile".
  • ICC specificationscolor.org/specification · direct: ICC.1:2010 (v4.3) PDF · ICC.1:2001-04 (v2.4) PDF. The authoritative specs. v4 is the current standard; v2 is still overwhelmingly dominant in the wild. jsColorEngine decodes both.
  • CIE colour scienceCIE publications. Reference for the Lab, XYZ, chromatic adaptation and ΔE formulas the engine uses internally.
  • WebAssembly SIMDWebAssembly SIMD proposal · v128 opcode table. Relevant if you want to read the .wat in src/kernels/{3d,4d}/ and understand the instruction choices in WASM kernels.