Count the sensor's own numbers, so a cull can see headroom the render hides

FR-CULL-3's remaining two bullets. What existed was a *display* histogram
tagged FR-DSP-7: it binds AdjustPass's Rgba8Unorm output, recovers an 8-bit
code value, and counts clipping as `r == 255`. Its own documentation says a
clipped bin means "a highlight that is actually gone rather than one the
transform might still recover", which is the opposite of what a culling
decision needs. FR-CULL-3 asks for the histogram of the sensor data, on the
explicit grounds that a rendered image "systematically lies about what is
recoverable in the raw", and a readout that measures the render cannot answer
that however it is presented.

So this is a second instrument beside the first rather than a setting on it.
Both are true; they are true about different things; the panel offers both
behind a chip row and the words travel with the numbers, because a raw
saturation figure drawn under a heading saying Highlights would be mislabelled
exactly where the difference matters.

**What is reduced over, and what it cost to decide.** ARCH §5.5 specified the
pre-demosaic CFA samples. This reduces over the demosaiced scene-linear
texture instead, and §5.5 is amended to record the choice rather than let the
specification and the code disagree in silence. The texture is camera-native —
unbalanced, unmatrixed, uncurved — and normalised by the sensor's own black
and white levels, so 1.0 is saturation by construction and the distribution
below it is the headroom question with no calibration to carry. Retaining the
CFA samples would mean keeping the packed u32 buffer Demosaicer::run currently
drops: 48 MB at 24 MP, 120 MB at 60 MP, resident per open photograph whether
or not anyone looks at the histogram, on a platform §6.2 exists because memory
is scarce on.

Three things it therefore cannot say, written into the module docs and into
§5.5 rather than left to be discovered: it counts pixels not photosites, so a
saturated site drags its interpolated neighbours up and per-channel clipping
is smeared by about a demosaic kernel; it cannot see above white, because
demosaic.wgsl clamps each photosite at 1.0 for its own good reasons (a Canon
6D reads to 16383 against a declared 15070) so "at saturation" and "a stop
past it" share a bin; and it is measured after the CFA pattern is gone, so it
can name which colour clipped in the reconstructed image but not which
photosite went first.

The axis is stops below saturation, 16 bins per stop over 256 bins — the same
bin count the display reduction uses, so the fold into drawable columns is
shared and a divergence between the two plots would have to be deliberate. A
linear axis spends half its width on the top stop, which is why nobody has
ever drawn a useful linear raw histogram. The fourth series is the brightest
channel rather than luma: these values are unbalanced, so any weighted sum of
them is a number about nothing, and the brightest channel is the one that
saturates first and so the one the headroom question is actually about.

It is a property of the file and not of the render, which has two
consequences. It is computed once per photograph and cached — nothing
downstream of the demosaic can move a count in it — so a cull does not pay the
display histogram's per-frame cost three thousand times. And it describes the
whole frame rather than the visible region, deliberately opposite to
DevelopSession::histogram: a crop changes what is on screen and changes
nothing about what the sensor recorded.

Tags are on the reduction, the type, its constructor and the presentation
arithmetic, each of which has a test that fails if the behaviour goes. The
Slint panel and the push from lib.rs keep their reasoning as prose: nothing
asserts them, and a tag would claim coverage the assertions are not making.
This commit is contained in:
2026-08-29 23:36:21 +02:00
parent a2a0131693
commit 4b6c110816
10 changed files with 1793 additions and 47 deletions
+33 -2
View File
@@ -369,6 +369,10 @@ RawImage (sensor data, CPU)
Working precision is f16 in a linear wide-gamut space, quantising once at the output transform.
There is a second reduction that does not hang off the bottom of this chain. The raw histogram
(FR-CULL-3) taps the demosaiced scene-linear texture directly — the box four rows from the top —
because what it measures is the file rather than the render. See §5.5.
### 5.3 Tiling and scheduling
Work decomposes into tiles (default 256×256) scheduled by priority class:
@@ -402,8 +406,35 @@ The histogram is a compute-shader reduction into a small storage buffer, read on
most, and only the *bins* — never image data. A per-frame CPU readback of pixels would reintroduce
exactly the stall §6.1 exists to prevent.
Raw-domain histograms for culling (FR-CULL-3) reduce over the pre-demosaic texture, which is why
they can report headroom the embedded JPEG's histogram cannot.
**Two reductions, not one.** The display histogram (FR-DSP-7) counts the frame the output transform
produced: its axis is the output code value, and a clipped bin means a highlight that is gone as the
image currently stands. The raw histogram for culling (FR-CULL-3) counts the **demosaiced
scene-linear texture** — before white balance, the camera matrix, the base curve and the tone chain
— on an axis of stops below sensor saturation, which is how it reports headroom the embedded JPEG's
histogram cannot. A culling decision needs the second, an export decision needs the first, and
neither answers for the other. Both are drawn by the same panel and chosen between.
**The raw reduction runs after the demosaic, not before it.** This section previously specified the
pre-demosaic CFA samples, and that is the more complete instrument: it counts photosites rather than
pixels, so no interpolation smears a clipped site across its neighbours, and it can name which
channel of the mosaic saturated first. It was not worth its cost. `Demosaicer::run` uploads the
packed sample buffer and drops it the moment the dispatch is encoded; retaining it is 48 MB at 24 MP
and 120 MB at 60 MP, resident per open photograph whether or not anyone looks at the histogram, on a
platform §6.2 exists because memory is scarce on. The record here is of the choice, not of the
intent — a specification the code contradicts is worse than either of the two things it could say.
The texture that is reduced over instead is camera-native, unbalanced, unmatrixed and uncurved, and
is normalised by the sensor's own black and white levels: 1.0 is saturation by construction, so the
distribution below it is the headroom question with no calibration to carry and no origin to choose.
What it cannot answer, and the CFA reduction could, is **which photosite** clipped rather than which
pixel, and **how far above** the white level a sample reached — the demosaic clamps there, for its
own good reasons, so "at saturation" and "a stop past it" share a bin. Both limits are stated again
where the code is, in `core/dr-gpu/src/raw_histogram.rs`.
It is also the one reduction on this path that is not per frame. Nothing downstream of the demosaic
can move a count in it, so it is computed once per photograph and cached — which is what makes it
affordable during a cull, where the display histogram's per-frame cost would be paid three thousand
times.
### 5.6 Device loss
+21 -18
View File
@@ -21,10 +21,10 @@ percentage. Where the honest answer is "this requirement should be amended rathe
so — an unbuilt requirement that nobody intends to build is worse than a deferred one, because it
keeps costing attention.
**Four of these are being built right now**, in parallel worktrees, and are marked **⟳ in
progress** where they appear: focus peaking (part of FR-CULL-3), burst grouping (FR-CULL-5),
Flatpak packaging (FR-PLAT-LIN-3), and Android platform integration (FR-PLAT-AND-2/4/5/6). Strike
those lines as they land rather than rewriting around them.
**Three of these are being built right now**, in parallel worktrees, and are marked **⟳ in
progress** where they appear: burst grouping (FR-CULL-5), Flatpak packaging (FR-PLAT-LIN-3), and
Android platform integration (FR-PLAT-AND-2/4/5/6). Strike those lines as they land rather than
rewriting around them.
---
@@ -70,23 +70,26 @@ somebody reads the matrix.
## 2. Culling — the stated differentiator, half built
[D11](requirements.md) names culling "the core differentiator". FR-CULL-1, -2, -4 and -8 through -12
are built. Four are not.
[D11](requirements.md) names culling "the core differentiator". FR-CULL-1, -2, -3, -4 and -8 through
-12 are built. Three are not.
**FR-CULL-3 — Raw-truth overlays. All three bullets, unbuilt.** Focus peaking does not exist
anywhere; the string appears zero times in the tree.
**FR-CULL-3 — Raw-truth overlays. Built, all three bullets.** Focus peaking is
`core/dr-gpu/src/focus.rs` and `ui/dr-ui/src/peaking.rs`; the raw histogram and the raw clipping
indicators are `core/dr-gpu/src/raw_histogram.rs` and the second reading of the panel in
`histogram.slint`.
The other two are easy to mistake for present, and are not. A histogram and clipping indicators do
exist — `dr-gpu/src/histogram.rs`, `ui/dr-ui/src/histogram.rs`, the panel in `histogram.slint` —
but they are tagged FR-DSP-7 and they answer the opposite question. They read `AdjustPass`'s 8-bit
output and count clipping as `r == 255`, which is to say they describe **the frame the display is
about to show**, after the whole develop chain has run. FR-CULL-3 asks for the histogram of the
*sensor data*, on the explicit grounds that a rendered image "systematically lies about what is
recoverable in the raw". A readout that measures the render cannot answer that however it is
presented, so this is not a matter of moving an existing widget into the culling view.
Worth recording, because it is the thing this entry previously got wrong and the next reader will
have to check again. The display histogram — `dr-gpu/src/histogram.rs`, `ui/dr-ui/src/histogram.rs`
— is **not** this requirement and never was: it is tagged FR-DSP-7, it reads `AdjustPass`'s 8-bit
output, it counts clipping as `r == 255`, and so it describes the frame the display is about to
show, after the whole develop chain. FR-CULL-3 asks for the *sensor data*, on the explicit grounds
that a rendered image "systematically lies about what is recoverable in the raw". The two now sit in
one panel behind a chip row, which is the arrangement that keeps them from being mistaken for each
other: they answer different questions and both are true.
The requirement exists because a culling decision made against a rendered preview is a decision made
against the wrong image, and the whole of it is still to build. **⟳ in progress** (focus peaking).
What the raw reduction cannot answer is written down rather than left to be discovered —
[architecture.md §5.5](architecture.md) records why it reduces over the demosaiced texture instead
of the CFA samples §5.5 originally specified, and what that costs in what it can say.
**FR-CULL-5 — Burst and near-duplicate grouping.** Absent. Worth knowing before it is built:
`core/dr-face/src/calibrate.rs` already *assumes* it exists — "since FR-CULL-5 already groups