Specify a scene-referred pipeline (D19)
The spec missed Ansel, and with it Aurélien Pierre's argument that a display-referred curve early in the pipeline throws away what every later stage needs. Read against that argument, the code broke it four ways: the base curve was flat past its last point and so clipped every recovered highlight; the detail stage was handed curved, clipped values while its comments promised linear ones; the edits ran in camera RGB because the matrix had drifted to after them, against ARCH §5.2; and the tone curve clamped to [0, 1] around a 2.2 gamma mid-chain. D19 records the decision: unbounded scene-linear colour from the matrix to one view transform, last, after the detail stage. FR-DEV-3j specifies that view transform as an adjustable operation (contrast, white) with a default fitted to where the retired default curve put middle grey. FR-DEV-3e retires the per-body base curves, whose own file called them hand-tuned shapes of unknown provenance. FR-DEV-3f makes a film stock the view transform when one is chosen, instead of a rendering at order 25 that the later edits acted on. FR-DEV-2 gains the acceptance test that holds the rule, and ARCH gains §6.14 and the corrected §5.2 order.
This commit is contained in:
+123
-9
@@ -306,6 +306,19 @@ from source + graph.
|
||||
per channel in a wide-gamut linear working space. Quantisation to the output bit depth happens
|
||||
once, at the final export or display stage.
|
||||
|
||||
*Amended 2026-09-27 (D19):* quantisation is one of three things deferred to the end, not the only
|
||||
one. Until the view transform (FR-DEV-3j), values are **scene-linear and unbounded**: nothing
|
||||
clamps above 1.0, nothing applies a transfer function, and nothing maps to a display gamut. Every
|
||||
operation between the camera matrix and the view transform receives and returns that. The
|
||||
working space's primaries are linear Rec.709, carried unbounded, so a colour outside sRGB is a
|
||||
negative component rather than a clipped one. That is wide-gamut in range, not in the primaries
|
||||
the operations measure hue against; moving the primaries to Rec.2020 is deferred (D19).
|
||||
|
||||
*Acceptance:* every point operation at non-neutral settings, handed a ramp to 16.0, returns values
|
||||
that are still monotone in the ramp and still above 1.0 where the ramp is, before the view
|
||||
transform (`scene_referred_until_the_view` in `dr-pipeline`). The view transform itself and film
|
||||
simulation are excluded, because clipping into a display range is their job.
|
||||
|
||||
**FR-DEV-3 — Adjustment set (v1).**
|
||||
|
||||
- White balance (temperature/tint, and picker)
|
||||
@@ -408,21 +421,31 @@ demosaic and the working-space conversion.
|
||||
**v1 scope** (per D11 — good defaults rather than exhaustive colour science):
|
||||
|
||||
1. Embedded DNG `ColorMatrix1/2` and `ForwardMatrix1/2` tags
|
||||
2. A hand-tuned base curve per launch camera body, shipped with the app
|
||||
2. ~~A hand-tuned base curve per launch camera body, shipped with the app~~ — **retired
|
||||
2026-09-27 (D19).** The tone half of "the camera's look" is the view transform's (FR-DEV-3j),
|
||||
one for every body and adjustable. The colour half stays here, in the matrix and later the DCP.
|
||||
3. HaldCLUT import (FR-DEV-3f)
|
||||
|
||||
The camera profile ends at the matrix, and the matrix runs **first**: white balance is applied in
|
||||
camera RGB, where its multipliers are defined, and every other operation receives working-space
|
||||
colour. Before D19 the edits ran in camera RGB and the matrix came after them, so a hue in the
|
||||
colour mixer and the weights in `luminance()` meant something different on every body.
|
||||
|
||||
**Deferred but not foreclosed:** full `.dcp` support with `HueSatDeltas`, `ProfileLookTable`, and
|
||||
dual-illuminant interpolation. The stage shall be structured so these are additions rather than a
|
||||
pipeline reordering.
|
||||
|
||||
Rationale for the reduced scope: a bare 3×3 matrix produces the flat, poor-skin-tone rendering
|
||||
characteristic of dcraw defaults, which is the documented reason people abandon darktable in the
|
||||
first hour. A per-body base curve fixes most of that at a fraction of the cost of a full DCP
|
||||
implementation. The profile database ships **versioned independently of the app binary** so bodies
|
||||
and curves can be added without a release — and, under D8's GPLv3, contributed by users.
|
||||
first hour. ~~A per-body base curve fixes most of that at a fraction of the cost of a full DCP
|
||||
implementation.~~ The flat render is a missing *view transform*, not a missing per-body curve:
|
||||
darktable's own answer to the first-hour complaint was a scene-referred default, and Ansel's is
|
||||
the same. The per-body curves this clause shipped described themselves as hand-tuned shapes, not
|
||||
measurements, and their provenance was not known well enough to keep them as defaults (D19).
|
||||
|
||||
*Acceptance:* for each launch body, the default render is subjectively comparable to the camera's
|
||||
own JPEG. ΔE2000 validation against ColorChecker references applies once DCP support lands.
|
||||
*Acceptance:* the default render is subjectively comparable to the camera's own JPEG — through
|
||||
FR-DEV-3j's default, for every body. ΔE2000 validation against ColorChecker references applies
|
||||
once DCP support lands.
|
||||
|
||||
**FR-DEV-3f — Look emulation.** Support HaldCLUT import, which inherits the existing free film
|
||||
simulation ecosystem at near-zero implementation cost, plus reading the in-RAF film simulation tag
|
||||
@@ -439,9 +462,13 @@ the picture along the film's own curve, shoulder and all, rather than scaling a
|
||||
exposure. And the **data cost inverts**: a stock is ~17 kB of published measurements where one
|
||||
HaldCLUT is ~800 kB of one person's grade.
|
||||
|
||||
A film simulation is a *rendering*, not an adjustment, so it replaces the camera profile's base
|
||||
curve and the conversion out of camera space (`Operation::renders`) — applying both would render
|
||||
the scene twice.
|
||||
A film simulation is a *rendering*, not an adjustment, so it **is** the view transform when a
|
||||
stock is chosen (FR-DEV-3j): it runs last, after every adjustment and after the detail stage, in
|
||||
place of the default sigmoid, and never in addition to it. *Amended 2026-09-27 (D19):* it ran at
|
||||
order 25 before this, after exposure and before everything else, so the edits below it acted on
|
||||
the film's output. They now act on the scene the film is shown: an edit is a decision about the
|
||||
exposure the negative receives, and the film is the last thing that happens to the picture.
|
||||
Existing edits that combine a stock with tone or colour operations render differently.
|
||||
|
||||
*Acceptance:* a neutral scene printed through a colour negative's own paper renders neutral to
|
||||
within 0.06 in linear sRGB; the baked lookup's interpolation error stays under one 8-bit code
|
||||
@@ -518,6 +545,33 @@ also written to the sidecar, run-length coded beside the layer, because a stored
|
||||
that never runs a model. It is a materialisation of the identity, not the edit: it takes no part
|
||||
in equality or merge, and the identity remains what the part means.
|
||||
|
||||
**FR-DEV-3j — View transform.** The last stage of the develop pipeline maps scene-linear
|
||||
colour to a display range, and it is the only stage that may. By default it is a log-logistic
|
||||
sigmoid applied per channel, with the middle channel's position between the other two restored
|
||||
afterwards so a hue survives the shoulder, and the result clipped only by the output transform. A
|
||||
stock chosen under FR-DEV-3f replaces it.
|
||||
|
||||
It is an operation with two parameters, persisted in the sidecar, adjustable in the develop panel,
|
||||
and held per mask layer like any other:
|
||||
|
||||
- **Contrast** — the sigmoid's slope. Default 1.4.
|
||||
- **White** — how far above middle grey, in stops, the scene reaches display white. Default 4.0,
|
||||
so a highlight a stop past sensor saturation still rolls into white rather than clipping at it.
|
||||
|
||||
Scene middle grey is 0.13, where the retired default curve placed it (FR-DEV-3e), and it maps to
|
||||
display 0.18. A photograph with the view transform at its defaults is **unedited**: the operation
|
||||
is always composed, and "active" keeps meaning "moved from the defaults", so an untouched image
|
||||
writes no parameters and every other operation's neutral is still the image.
|
||||
|
||||
An already-rendered source — a JPEG — is not rendered again: the default view transform is skipped
|
||||
for it, as the base curve was. A film stock is not, because choosing one is an edit.
|
||||
|
||||
*Acceptance:* monotone in each channel; a neutral stays neutral; middle grey lands within 0.01 of
|
||||
0.18; between scene 0.03 and 1.0, the default is within 0.3 EV of the retired default curve; the
|
||||
scene value `0.13 · 2^white` reaches 1.0; and the shader agrees with the CPU reference.
|
||||
|
||||
*Added 2026-09-27 (D19).*
|
||||
|
||||
**FR-DEV-4 — Ordered, GPU-resident execution.** The pipeline executes as a sequence of GPU
|
||||
compute stages. Intermediate results remain in GPU memory between stages. **Processed pixels
|
||||
shall reach the display without a CPU round-trip.** *(This is a hard architectural constraint —
|
||||
@@ -2346,6 +2400,7 @@ Rationale, evidence, and the eliminated alternatives are recorded in
|
||||
| D11 | Product positioning | Culling-first differentiator; see below |
|
||||
| D12 | Scope versus pace | **DECIDED 2026-09-19** — settled by events; full scope stands, no v1 date |
|
||||
| D18 | Derived images | **DECIDED 2026-09-19** — a merge writes a new source file; no multi-source Version |
|
||||
| D19 | Scene-referred pipeline | **DECIDED 2026-09-27** — edits on unbounded scene-linear colour; one view transform, last; per-body base curves retired |
|
||||
|
||||
### D11 — product positioning
|
||||
|
||||
@@ -2592,6 +2647,65 @@ file, not an edit to the old one; and the composite occupies disk — a five-fra
|
||||
where the output is still frame A, and inherits nothing from this decision but the provenance
|
||||
rule.
|
||||
|
||||
### D19 — scene-referred pipeline · **DECIDED 2026-09-27**
|
||||
|
||||
**Edits operate on scene-linear, unbounded colour in the working space, and one view transform,
|
||||
last, maps it to a display range.** Range, encoding and gamut are all deferred to that point, as
|
||||
quantisation already was (FR-DEV-2).
|
||||
|
||||
*Why now.* The spec missed [Ansel](https://ansel.photos/), Aurélien Pierre's fork of darktable 4.0,
|
||||
and with it the argument he spent years making in darktable: a display-referred curve early in the
|
||||
pipeline throws away what every later stage needs. Reading the code against that argument found
|
||||
four places it applied:
|
||||
|
||||
1. **The base curve clipped.** It was a five-point spline on the unit square, flat past its last
|
||||
point, so every value above 1.0 — every recovered highlight — left it at the same number, per
|
||||
channel.
|
||||
2. **The detail stage was handed non-linear data.** The fused pass stops at "linear working
|
||||
values" when a sharpener or a blur follows, but it stopped *after* the base curve, so the
|
||||
neighbourhood operations convolved curved, clipped values while their comments promised the
|
||||
opposite.
|
||||
3. **The edits ran in camera RGB.** The matrix came after them, so `luminance()`'s Rec.709 weights
|
||||
were applied to camera primaries and a hue in the colour mixer was a different hue on each
|
||||
body. ARCH §5.2 had always drawn the matrix first; the code had drifted.
|
||||
4. **The tone curve clamped** to [0, 1] and applied a 2.2 gamma around its spline, mid-chain.
|
||||
|
||||
*What changes.* The order becomes: demosaic → as-shot white balance and the white balance
|
||||
operation, in camera RGB → the camera matrix → every other point operation and every mask layer →
|
||||
the detail stage → the view transform (FR-DEV-3j), or the film stock (FR-DEV-3f) when one is chosen
|
||||
→ the output transform. With a detail stage the view transform is a dispatch of its own after it,
|
||||
composed by the same generator as the fused pass. Nothing before the view transform clamps above
|
||||
1.0 or display-encodes, and a test says so (FR-DEV-2).
|
||||
|
||||
*What is retired.* The per-body base curves and their database (FR-DEV-3e). Their own file called
|
||||
them hand-tuned shapes rather than measurements, and not enough was known about where the shapes
|
||||
came from to keep them as defaults behind sliders. Body character is the matrix's, and the DCP's
|
||||
when it lands.
|
||||
|
||||
*What it costs.*
|
||||
|
||||
- **Every photograph renders differently.** The default view transform was fitted so middle grey
|
||||
lands where the retired default curve put it and midtones stay within 0.3 EV of it, but the
|
||||
upper midtones are darker and the highlights roll off over two more stops. Previews rendered
|
||||
before the change keep the old look until they are rendered again.
|
||||
- **Film edits change meaning.** A tone or colour operation beside a stock used to act on the
|
||||
film's output; it now acts on the scene the film receives.
|
||||
- **Tablet and desktop must be released together.** No schema changes and the sidecar gains only
|
||||
ordinary parameters, but two peers on different builds render the same edit differently.
|
||||
- **One more dispatch with a detail stage**, for the view transform after it.
|
||||
|
||||
*Rejected.* Keeping the per-body curves as the view transform's per-body defaults, for the
|
||||
provenance reason above. Leaving the film at order 25 and having it suppress the view transform:
|
||||
simpler, and it kept existing film edits' meaning, but it left a display-referred rendering in the
|
||||
middle of the chain, which is the thing this decision removes. A fixed view transform with no
|
||||
controls: it would have been smaller, but a scene-referred pipeline whose white point cannot be
|
||||
moved hands the photographer a shoulder they cannot place.
|
||||
|
||||
*Deferred.* Working-space primaries of Rec.2020 rather than Rec.709. The range is already
|
||||
unbounded, but several fragments floor at zero, which clips a colour outside sRGB, and the colour
|
||||
mixer's bands and the colour grading wheel would need their hues re-measured. Gamut compression
|
||||
beyond the output transform's clip goes with it.
|
||||
|
||||
### D16 — plugin licensing · **OPEN, post-v1**
|
||||
|
||||
> Deferred with §3.10 on 2026-09-19. Still to be answered before the format is published as
|
||||
|
||||
Reference in New Issue
Block a user