Files
DarkRoom/core/dr-film
dtourolleandClaude Opus 5 3b5952769b Emit floats an f32 can hold, and drop the format! that formats nothing
CI runs cargo fmt --check and clippy -D warnings, and this branch had
never been through either. Both would have failed it.

The bulk was the generated colour tables: eight significant figures where
an f32 carries about 7.2, so the eighth is noise that rounds away at
compile time and clippy's excessive_precision says so 109 times over.
Fixed in the generator rather than only in the file, so it stays fixed --
and the file is trimmed in place rather than re-derived, because
regenerating it needs a colour-science stack that has nothing to do with
the defect.

The format! in the composer is mine too, from extracting the rendering
tail: the braces in it were escaped because the text used to live inside a
larger template, and once extracted the escapes are noise and the call
formats nothing.

Also here, and clearly not mine: an unused import and a shadowed binding
in dr-gpu, and an unused import in a test. They are pre-existing --
clippy has been failing on master before this branch existed, on lints
like is_multiple_of that arrived with a toolchain rather than with
anyone's code. Fixed because CI cannot go green around them, and called
out because a merge commit is a bad place to quietly edit someone else's
crate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 22:28:14 +02:00
..

Film stocks

One file per stock in profiles/. Adding a stock is adding a file — no code change, no shader, no new operation — for the same reason dr-decode's base curves work that way: under the GPLv3 a stock should be contributable without a release.

What a profile is

Three measured tables, all of them published in the manufacturer's datasheet:

Field What it decides
log_sensitivity what each emulsion layer sees, per wavelength
density_curves contrast, latitude, and where the stock clips
dye_density what the developed stock looks like, per wavelength
base_density the support: film base, and a colour negative's orange mask

Plus kind (negative or positive), support (film or paper), and the two illuminants the data is referenced to. A print paper is a stock like any other; support exists so an interface can offer papers separately, not because the renderer treats them differently.

Why it is not a LUT

Because the parameters stay physical. Opening up a stop moves the picture along the film's own characteristic curve — toe, shoulder and all — instead of scaling a number somebody baked at one exposure. A scanned negative comes out orange and inverted because that is what a negative is, and it becomes a photograph when a paper profile prints it, exactly as it would in a darkroom.

The data cost runs the other way from a LUT collection too: a stock is about 17 kB of measurements, where one HaldCLUT is roughly 800 kB of one person's grade.

How it runs

The spectral chain reduces to three tables, and the reduction is exact where it matters — see src/bake.rs for the argument:

  1. A 3×3 matrix, linear sRGB to the three layers' exposure. Exact, not an approximation: the reconstructed scene spectrum is linear in the sRGB triple, so the integral collapses into nine numbers.
  2. Three 1D curves, log exposure to density, sampled at 256 points.
  3. One 32³ lookup, density to linear sRGB — dye absorption, the print through the negative, the paper, the viewing illuminant and the chromatic adaptation, all of which take exactly three numbers in.

Per pixel that is a matrix multiply, three curve taps and one texture fetch. Splitting 2 from 3, rather than baking one LUT over exposure, is measured rather than assumed: the curve carries all the sharp shape and the dye mixing is smooth, so folding the curve into the 3D lookup would need it three times larger for the same error. At 32³ the worst interpolation error is about 0.003 in linear sRGB, below one 8-bit code value, and there is a test that says so.

Adding a stock

If spektrafilm has it, add its name to STOCKS in tools/film-profiles/convert.py and re-run it. Otherwise write the YAML by hand from the datasheet; the loader validates the table lengths and says which file and field is wrong.

Either way, list it in BUILT_IN in src/lib.rs to compile it in — or drop it in the profile directory at runtime, which is the path meant for stocks that ship separately from the binary.

Provenance

The shipped profiles are converted from spektrafilm by Andrea Volpato, licensed CC BY-SA 4.0. See profiles/LICENSE-PROFILES.txt for the licence and profiles/CHANGELOG.txt for what the conversion changed and what it deliberately did not.

The sRGB reflectance basis is Mallett & Yuksel (2019); the observer is the CIE 1931 2°.