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:
@@ -355,11 +355,12 @@ RawImage (sensor data, CPU)
|
||||
├─────────────────────┤
|
||||
│ AI denoise │ optional; raw-domain, joint with demosaic where possible
|
||||
├─────────────────────┤
|
||||
│ camera profile │ matrices + per-body base curve (FR-DEV-3e)
|
||||
│ white balance │ camera RGB: as-shot, then the operation
|
||||
├─────────────────────┤
|
||||
│ → working space │ linear, wide-gamut, f16
|
||||
│ camera profile │ the matrix (FR-DEV-3e) — no curve (D19)
|
||||
├─────────────────────┤
|
||||
│ → working space │ linear, unbounded, f16
|
||||
├─────────────────────┤
|
||||
│ white balance │
|
||||
│ exposure/contrast │
|
||||
│ highlights/shadows │ ← masks apply per-op from here down
|
||||
│ tone curve │
|
||||
@@ -368,10 +369,11 @@ RawImage (sensor data, CPU)
|
||||
│ spot removal │
|
||||
│ sharpen / NR │
|
||||
│ lens corrections │
|
||||
│ look (HaldCLUT) │ FR-DEV-3f
|
||||
├─────────────────────┤
|
||||
│ geometry │ crop, straighten, rotate
|
||||
├─────────────────────┤
|
||||
│ view transform │ sigmoid, or the film stock (FR-DEV-3j)
|
||||
├─────────────────────┤
|
||||
│ output transform │ → display or export profile
|
||||
└─────────────────────┘
|
||||
│
|
||||
@@ -381,6 +383,14 @@ RawImage (sensor data, CPU)
|
||||
|
||||
Working precision is f16 in a linear wide-gamut space, quantising once at the output transform.
|
||||
|
||||
**Scene-referred until the view transform (D19, §6.14).** Everything between the matrix and the
|
||||
view transform is linear and unbounded. The view transform is the one stage allowed to compress
|
||||
the scene into a display range. With a detail stage it runs as a dispatch of its own after the
|
||||
detail passes, generated by the same composer as the fused pass so that it gets the mask layers,
|
||||
the film tables and the grain's source position. Without one it is the fused pass's tail. Both
|
||||
the view transform and the output transform (primaries, gamut clip, encode) come after
|
||||
everything that reads a neighbourhood.
|
||||
|
||||
**A hot or dead photosite is repaired before the demosaic, not after.** Past it, one photosite of
|
||||
nonsense is a coloured cross three pixels wide that no later stage can tell from detail. The pass
|
||||
(`shaders/hot_pixels.wgsl`, run by `Demosaicer::run` into a second buffer) replaces a photosite that
|
||||
@@ -1255,6 +1265,17 @@ differently, f16 rounding varies. Cache keys and graph hashes are computed over
|
||||
state, which is exactly deterministic. Cross-platform *rendering* equality is a bounded tolerance
|
||||
(R1), not a checksum.
|
||||
|
||||
### 6.14 Scene-referred until the view transform
|
||||
|
||||
Added 2026-09-27 (D19). Between the camera matrix and the view transform, values are scene-linear
|
||||
and unbounded, and no operation clamps above 1.0, applies a transfer function or maps to a display
|
||||
gamut. The view transform (FR-DEV-3j) is the one stage that does, and the output transform after
|
||||
it clips and encodes. This is the constraint the base curve broke and ARCH §5.2 had drawn all
|
||||
along: a display-referred curve in the middle of the chain throws away what every later stage,
|
||||
the neighbourhood ones above all, needs. A test (`scene_referred_until_the_view`) runs every point
|
||||
operation over a ramp to 16.0 so that a fragment that clips fails the build rather than the
|
||||
photograph.
|
||||
|
||||
---
|
||||
|
||||
## 13. Decisions
|
||||
@@ -1278,6 +1299,7 @@ Full rationale in [requirements.md §8](requirements.md). Summary:
|
||||
| D13 | Face inference runtime and model licensing | **Runtime answered**, reopened for per-device backends (docs/inference.md); licensing open |
|
||||
| D14 | Segmentation source for local masking | Decided — arm C (docs/segmentation.md §14) |
|
||||
| D15 | Target devices — 12-inch tablet and desktop, no phone | Decided (requirements D15) |
|
||||
| D19 | Scene-referred pipeline, one view transform last | Decided (requirements D19) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+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
|
||||
|
||||
@@ -11,16 +11,16 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
|
||||
|---|---|
|
||||
| Source files scanned | 470 |
|
||||
| TRACES tags found | 1911 |
|
||||
| Requirements defined | 192 |
|
||||
| Requirements defined | 193 |
|
||||
| Requirements deferred (post-v1) | 24 |
|
||||
| Requirements covered | 163 |
|
||||
| **Coverage** | **84.9%** (163/192) |
|
||||
| **Coverage** | **84.5%** (163/193) |
|
||||
|
||||
### By type
|
||||
|
||||
| Type | Covered | Defined |
|
||||
|---|---|---|
|
||||
| FR | 123 | 134 |
|
||||
| FR | 123 | 135 |
|
||||
| NFR | 35 | 51 |
|
||||
| R | 5 | 7 |
|
||||
|
||||
@@ -229,7 +229,7 @@ Defined in `requirements.md` and marked `(post-v1)` on the defining line. Not in
|
||||
|
||||
## Not yet tagged
|
||||
|
||||
29 of 192 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
|
||||
30 of 193 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
|
||||
|
||||
<details><summary>Show untagged requirements</summary>
|
||||
|
||||
@@ -237,6 +237,7 @@ Defined in `requirements.md` and marked `(post-v1)` on the defining line. Not in
|
||||
- FR-CULL-6
|
||||
- FR-CULL-7
|
||||
- FR-DEV-3g
|
||||
- FR-DEV-3j
|
||||
- FR-DSP-2
|
||||
- FR-INF-2
|
||||
- FR-INF-3
|
||||
|
||||
Reference in New Issue
Block a user