|
| 1 | +# radiantlab/LibRaw |
| 2 | + |
| 3 | +A fork of [LibRaw/LibRaw](https://github.com/LibRaw/LibRaw) that adds a |
| 4 | +**WebAssembly** build of `dcraw_emu`, the RAW converter. |
| 5 | + |
| 6 | +It exists to serve |
| 7 | +[radiantlab/HDRICalibrationTool#237](https://github.com/radiantlab/HDRICalibrationTool/issues/237), |
| 8 | +which ports that app's image pipeline off locally-installed binaries so it can |
| 9 | +run fully client-side in a browser. RAW is the workflow the app was built for: |
| 10 | +without this, a browser build would handle JPEG and TIFF only. |
| 11 | + |
| 12 | +## Why a fork |
| 13 | + |
| 14 | +Not because LibRaw's sources need patching. **They do not.** Every source file |
| 15 | +here is upstream's, unmodified, and an `emcmake` configure succeeds on the first |
| 16 | +attempt. LibRaw is unusually clean in this respect: its optional dependencies |
| 17 | +self-guard behind preprocessor macros, so simply not defining them is enough. |
| 18 | + |
| 19 | +The fork exists because **LibRaw ships no CMake build at all**. `README.cmake` |
| 20 | +redirects to [LibRaw/LibRaw-cmake](https://github.com/LibRaw/LibRaw-cmake), a |
| 21 | +separate repository upstream stopped supporting in October 2014, which has to be |
| 22 | +copied into the tree before it works. So the build file has to live somewhere, |
| 23 | +and a fork is where it lives. |
| 24 | + |
| 25 | +## The change |
| 26 | + |
| 27 | +One file: **`CMakeLists.txt`**, about 120 lines. Nothing else in the tree is |
| 28 | +touched. |
| 29 | + |
| 30 | +It replaces LibRaw-cmake rather than vendoring it. Roughly 700 of |
| 31 | +LibRaw-cmake's 737 lines are `find_package()` probes for LCMS, JasPer, libjpeg, |
| 32 | +zlib, OpenMP and RawSpeed, every one of which this build turns off. Vendoring |
| 33 | +would mean carrying precisely the code most likely to misbehave under |
| 34 | +`emcmake`, for nothing. What remains once the probes are gone is a source glob |
| 35 | +and one executable. |
| 36 | + |
| 37 | +`CMakeLists.txt` is offered under the same terms as LibRaw itself (LGPL-2.1 or |
| 38 | +CDDL-1.0, at your option), so the fork stays coherent for anyone else who wants |
| 39 | +it. |
| 40 | + |
| 41 | +### The one glob exclusion |
| 42 | + |
| 43 | +`src/**/*_ph.cpp` are placeholder translation units whose every function returns |
| 44 | +`LIBRAW_NOT_IMPLEMENTED`; they exist so LibRaw can be built with no |
| 45 | +postprocessing at all, and they define the same symbols as the real |
| 46 | +implementations. Left in, the link fails on ten duplicate symbols. That is the |
| 47 | +good outcome: a link that happened to resolve in favour of the stubs would |
| 48 | +produce a `dcraw_emu` whose `dcraw_process()` silently does nothing. |
| 49 | + |
| 50 | +### Every optional dependency is off |
| 51 | + |
| 52 | +Off technically, because nothing needs them: |
| 53 | + |
| 54 | +| Off | What it would add | Why a CR2 does not need it | |
| 55 | +|---|---|---| |
| 56 | +| libjpeg | lossy DNG | CR2's lossless-JPEG payload is decoded by `src/decompressors/losslessjpeg.cpp` | |
| 57 | +| zlib | deflate DNG | not used by CR2 | |
| 58 | +| JasPer | RedCine | not used by CR2 | |
| 59 | +| LCMS2 | ICC profile application | the pipeline does its colour work in Radiance | |
| 60 | +| libtiff | — | `dcraw_emu -T` writes TIFF through `src/write/`, not libtiff | |
| 61 | + |
| 62 | +And **OpenMP**, whose absence is load-bearing rather than incidental. |
| 63 | +LibRaw-cmake enables it by default. Threads in a wasm build mean |
| 64 | +`SharedArrayBuffer`, which means whoever serves the `.wasm` must send COOP/COEP |
| 65 | +headers. Keeping this build single-threaded is what lets it be hosted on any |
| 66 | +static host with no header configuration, which is the point of the port. This |
| 67 | +matches the choice made in |
| 68 | +[radiantlab/hdrgen](https://github.com/radiantlab/hdrgen) for the same reason. |
| 69 | + |
| 70 | +Off for **licensing** reasons as well as technical ones, and these should stay |
| 71 | +off unless deliberately revisited: **RawSpeed**, the **Adobe DNG SDK**, |
| 72 | +**GoPro/GPR**, **X3FTOOLS**, and the abandoned **demosaic packs** (GPL-2 and |
| 73 | +GPL-3, and not in this tree). See "Licensing" below. |
| 74 | + |
| 75 | +## Branches |
| 76 | + |
| 77 | +`main` only, matching the other radiantlab forks. Upstream's working branch is |
| 78 | +`master`; the `*-stable` release branches are not carried here. |
| 79 | + |
| 80 | +```sh |
| 81 | +git remote add upstream https://github.com/LibRaw/LibRaw.git # once |
| 82 | +git fetch upstream master |
| 83 | +git merge upstream/master |
| 84 | +``` |
| 85 | + |
| 86 | +Nothing in `CMakeLists.txt` enumerates source files, so an upstream sync that |
| 87 | +adds or removes a `src/*.cpp` needs no reconciliation. The one thing to re-check |
| 88 | +after a sync is whether a new `*_ph.cpp` appeared, and to re-run the comparison |
| 89 | +below. |
| 90 | + |
| 91 | +## Building |
| 92 | + |
| 93 | +Requires Emscripten. Validated with `emscripten/emsdk:6.0.4`. |
| 94 | + |
| 95 | +```sh |
| 96 | +# Native. Also the reference for the comparison below. |
| 97 | +cmake -S . -B build-native -DCMAKE_BUILD_TYPE=Release |
| 98 | +cmake --build build-native --target dcraw_emu -j8 |
| 99 | + |
| 100 | +# wasm, node -- real filesystem via NODERAWFS. Same argv and same real paths as |
| 101 | +# the native binary, which is what makes native-vs-wasm a direct A/B. |
| 102 | +emcmake cmake -S . -B build-wasm -DCMAKE_BUILD_TYPE=Release |
| 103 | +cmake --build build-wasm --target dcraw_emu -j8 |
| 104 | + |
| 105 | +# wasm, browser -- ES module, virtual filesystem, FS and callMain exported. |
| 106 | +emcmake cmake -S . -B build-web -DCMAKE_BUILD_TYPE=Release -DLIBRAW_WASM_NODERAWFS=OFF |
| 107 | +cmake --build build-web --target dcraw_emu -j8 |
| 108 | +``` |
| 109 | + |
| 110 | +Both wasm variants produce a 786 KB `.wasm`. |
| 111 | + |
| 112 | +## Status |
| 113 | + |
| 114 | +`dcraw_emu` builds for all three targets and the wasm output is **exact except |
| 115 | +for one demosaic**. |
| 116 | + |
| 117 | +### Validation against native, same tree |
| 118 | + |
| 119 | +The 10-frame reference CR2 bracket (Canon EOS 5D Mark III, 8 mm fisheye, |
| 120 | +5796x3870) converted with the exact argument list HDRICalibrationTool's pipeline |
| 121 | +constructs: |
| 122 | + |
| 123 | +``` |
| 124 | +dcraw_emu -T -o 1 -W -j -q 3 -g 2 0 -t 0 -b 1.1 -Z <out.tiff> <in.CR2> |
| 125 | +``` |
| 126 | + |
| 127 | +Each output is a 67,293,432-byte 8-bit RGB TIFF. wasm runs at roughly half |
| 128 | +native speed (~2.0 s vs ~1.0 s per frame). |
| 129 | + |
| 130 | +| Comparison | Result | |
| 131 | +|---|---| |
| 132 | +| wasm vs **stock native** (arm64, FMA on) | frames 1-5 identical; 6-10 differ by 16 to 704 bytes | |
| 133 | +| wasm vs native **`-ffp-contract=off`** | frames 1-5 identical; 6-10 differ by 1 to 28 bytes | |
| 134 | +| wasm run 1 vs wasm run 2 | **identical** | |
| 135 | + |
| 136 | +So most of the gap is arm64 FMA contraction, exactly as found for the Radiance |
| 137 | +tools: arm64 fuses `a*b+c` into one instruction with a single rounding step and |
| 138 | +wasm MVP has no FMA, so it must round twice. The control confirms it -- stock |
| 139 | +native vs `-ffp-contract=off` native reproduces 676 of the 704 differing bytes |
| 140 | +on frame 10 on its own. |
| 141 | + |
| 142 | +### The residual is AHD's cube-root table, and no flag fixes it |
| 143 | + |
| 144 | +Unlike the Radiance tools, `-ffp-contract=off` does **not** get all the way to |
| 145 | +byte-identical. The remaining 1 to 28 bytes per frame are entirely attributable |
| 146 | +to the demosaic: |
| 147 | + |
| 148 | +| `dcraw_emu -q` | Algorithm | wasm vs native `-ffp-contract=off` | |
| 149 | +|---|---|---| |
| 150 | +| `-q 0` | bilinear | **0 bytes differ** | |
| 151 | +| `-q 1` | VNG | **0 bytes differ** | |
| 152 | +| `-q 3` | AHD | 28 bytes differ (frame 10) | |
| 153 | + |
| 154 | +Everything else was ruled out by the same method. Forcing linear gamma |
| 155 | +(`-g 1 1`) leaves the count unchanged at 28, and skipping the colour matrix |
| 156 | +(`-o 0`) leaves 23 -- so the RAW decode, white balance, colour conversion, |
| 157 | +gamma curve and TIFF writer are all bit-exact. Only AHD is not. |
| 158 | + |
| 159 | +AHD is also the only path that calls `pow()`. |
| 160 | +`src/demosaic/ahd_demosaic.cpp:46` builds a 65,536-entry cube-root lookup table |
| 161 | +for the CIELAB conversion behind its homogeneity test: |
| 162 | + |
| 163 | +```c |
| 164 | +cbrt[i] = r > 0.008856f ? pow(r, 1.f / 3.0f) : 7.787f * r + 16.f / 116.0f; |
| 165 | +``` |
| 166 | + |
| 167 | +macOS libm and Emscripten's musl libm agree on `pow` to within an ulp, not |
| 168 | +exactly. A handful of the 65,536 entries land on the other side of a rounding |
| 169 | +boundary, which flips AHD's choice of interpolation direction at the pixels |
| 170 | +where the two directions are near enough to tied. Roughly 14 pixels of 22.4 |
| 171 | +million. |
| 172 | + |
| 173 | +**This is a different kind of difference from FMA and it is worth being clear |
| 174 | +about.** FMA is a code-generation choice and a compiler flag removes it. A libm |
| 175 | +difference is not reachable by any flag; removing it would mean bundling a |
| 176 | +correctly-rounded `pow`. The wasm build is nonetheless **reproducible**, which |
| 177 | +is the property that actually matters: it is deterministic run to run, and |
| 178 | +because wasm MVP has no FMA it gives the same answer on every architecture and |
| 179 | +in every browser. See |
| 180 | +[radiantlab/HDRICalibrationTool#235](https://github.com/radiantlab/HDRICalibrationTool/issues/235) |
| 181 | +for the same argument applied to hdrgen's JPEG IDCT. |
| 182 | + |
| 183 | +### It does not survive into the result |
| 184 | + |
| 185 | +The ten TIFFs from each build merged with `hdrgen -m 1000 ... -a -e -f -g -F`: |
| 186 | + |
| 187 | +``` |
| 188 | +resolution 5796x3870 |
| 189 | +mantissa bytes differ 44 of 67,291,560 (0.000065%) |
| 190 | +exponent bytes differ 0 of 22,430,520 |
| 191 | +max abs difference 6.1e-05 |
| 192 | +mean rel difference 9.5e-09 |
| 193 | +mean luminance 0.135740 vs 0.135740 (relative 2.3e-09) |
| 194 | +``` |
| 195 | + |
| 196 | +No pixel changes exponent, and the mean luminance -- which is what becomes |
| 197 | +`COMPUTED_VERTICAL_ILLUMINANCE` downstream -- agrees to nine significant |
| 198 | +figures. |
| 199 | + |
| 200 | +### Not yet done |
| 201 | + |
| 202 | +Browser-tab execution. `build-web` links, and its embedding mechanism is the |
| 203 | +same one verified for the Radiance tools, but `dcraw_emu.wasm` has never been |
| 204 | +instantiated in a browser. Memory has not been measured against the wasm32 |
| 205 | +ceiling either; a 5796x3870 CR2 at `-q 3` should be a few hundred MB, but that |
| 206 | +is an estimate rather than a measurement. |
| 207 | + |
| 208 | +## Licensing |
| 209 | + |
| 210 | +LibRaw is dual-licensed: **LGPL-2.1 or CDDL-1.0**, at the user's option |
| 211 | +(`COPYRIGHT`). This fork changes neither, and deliberately does not alter any |
| 212 | +licence notice. |
| 213 | + |
| 214 | +HDRICalibrationTool is **GPL-3.0**, which settles which of the two applies: |
| 215 | +CDDL-1.0 is incompatible with the GPL, so the LGPL is the one that can be used. |
| 216 | +LGPL-2.1 §3 permits applying ordinary GPL terms to a given copy of the library, |
| 217 | +"version 2... or any later version", which is what brings the statically-linked |
| 218 | +`dcraw_emu.wasm` inside a GPL-3 work. |
| 219 | + |
| 220 | +The conversion is recorded on the consuming side, not here, so this fork stays |
| 221 | +dual-licensed and useful to anyone. See |
| 222 | +[`licenses/DECISIONS.md`](https://github.com/radiantlab/HDRICalibrationTool/blob/main/licenses/DECISIONS.md) |
| 223 | +in HDRICalibrationTool. |
| 224 | + |
| 225 | +The optional components listed above stay off partly for this reason. The |
| 226 | +demosaic packs in particular are GPL-2 and GPL-3 licensed, and enabling them |
| 227 | +would change the analysis. Core LibRaw's third-party pieces are all |
| 228 | +GPL-compatible: DCB and FBDD are BSD-3, the X3F reader is BSD, and the DNG SDK |
| 229 | +fragments are MIT. |
0 commit comments