Stop describing a base curve the pipeline no longer has

D19 retired the per-body base curve, moved the matrix ahead of the
edits and the film into the view transform's place, but a dozen doc
comments still listed the curve among what a pixel passes through, or
said the film skipped it. The detail stage's module doc still drew the
matrix after the edits and the last detail pass encoding, which the
view pass took over. The film crate's README gave the base curves as
its reason for being data, and the ops README's list of hand-written
nodes had neither the view transform nor three of the five kernels.

FR-MRG-2 gave the base curve as why the merge cuts below the profile;
the view transform is why now. The decision table still said colour
defaults were a per-body curve, and FR-DEV-3j said only the default
view transform skips a JPEG, where the node skips one whatever its
sliders say. frame-budget.md records the view pass as unmeasured.
This commit is contained in:
2026-09-27 19:42:41 -04:00
parent 31bcc3a462
commit affdaecaee
19 changed files with 107 additions and 61 deletions
+2 -2
View File
@@ -67,8 +67,8 @@
//! FR-DEV-3e defers full `.dcp` support — `HueSatDeltas` and //! FR-DEV-3e defers full `.dcp` support — `HueSatDeltas` and
//! `ProfileLookTable` — and requires that they arrive as *additions* rather //! `ProfileLookTable` — and requires that they arrive as *additions* rather
//! than as a pipeline reordering. They would: both are lookups applied to a //! than as a pipeline reordering. They would: both are lookups applied to a
//! colour after this matrix and before, or alongside, the base curve, so they //! colour at this matrix, before any edit reaches it, so they extend
//! extend [`CameraProfile`] with more calibration data and extend the shader's //! [`CameraProfile`] with more calibration data and extend the shader's
//! camera-profile stage with more work. Nothing above would move. //! camera-profile stage with more work. Nothing above would move.
use crate::{cam_to_srgb_from, invert3}; use crate::{cam_to_srgb_from, invert3};
+9 -3
View File
@@ -1,9 +1,8 @@
# Film stocks # Film stocks
One file per stock in [`profiles/`](profiles/). Adding a stock is adding a One file per stock in [`profiles/`](profiles/). Adding a stock is adding a
file — no code change, no shader, no new operation — for the same reason file — no code change, no shader, no new operation — because under the GPLv3
`dr-decode`'s base curves work that way: under the GPLv3 a stock should be a stock should be contributable without a release.
contributable without a release.
## What a profile is ## What a profile is
@@ -51,6 +50,13 @@ matters — see [`src/bake.rs`](src/bake.rs) for the argument:
log exposure through the negative, the enlarger's exposure is added there, log exposure through the negative, the enlarger's exposure is added there,
and the paper's own curve row and cube take it to linear sRGB. and the paper's own curve row and cube take it to linear sRGB.
The stock is the last thing that happens to the picture. It runs in the view
transform's place (D19): handed linear sRGB, scene-referred, after every other
adjustment and after sharpening and noise reduction, and handing back the
rendering the output transform encodes. So every other slider decides the
exposure the negative receives, and the default tone mapping is not applied
on top.
Per pixel that is a matrix multiply, a handful of curve taps and one texture Per pixel that is a matrix multiply, a handful of curve taps and one texture
fetch — two for a print. Splitting 2 from 3, rather than baking one LUT over exposure, is measured fetch — two for a print. 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 rather than assumed: the curve carries all the sharp shape and the dye mixing
+3 -3
View File
@@ -1520,9 +1520,9 @@ impl AdjustPass {
/// one: the storage format is in the layout. The profile uniforms are /// one: the storage format is in the layout. The profile uniforms are
/// filled neutral here rather than from the source, which is the whole /// filled neutral here rather than from the source, which is the whole
/// point of the mode (`OutputMode::CameraLinear`): unit white balance, /// point of the mode (`OutputMode::CameraLinear`): unit white balance,
/// identity matrix, base curve off. The non-linear flag is kept, so a /// identity matrix, and no view transform composed. The non-linear flag
/// JPEG source is still linearised — camera space for a JPEG is the /// is kept, so a JPEG source is still linearised — camera space for a
/// decoded values made linear, which is the best that exists. /// JPEG is the decoded values made linear, which is the best that exists.
/// ///
/// The texture stays on the device for a merge's warp to sample; see /// The texture stays on the device for a merge's warp to sample; see
/// [`Self::camera_texture`] and [`Self::read_camera_linear`]. /// [`Self::camera_texture`] and [`Self::read_camera_linear`].
+6 -5
View File
@@ -320,10 +320,11 @@ impl DemosaicedImage {
color_matrix: IDENTITY_3X3, color_matrix: IDENTITY_3X3,
as_shot_wb: [1.0, 1.0, 1.0], as_shot_wb: [1.0, 1.0, 1.0],
// **The identity, and this is the whole reason the field is here // **The identity, and this is the whole reason the field is here
// rather than resolved further down.** A JPEG has already had its // rather than resolved further down.** A JPEG has already been
// camera's base curve baked in by the camera; applying one again // rendered by the camera; the view transform skips a source
// would render the rendering, crushing the shadows and flattening // flagged non-linear, since rendering the rendering would crush
// the highlights of an image that was already finished. // the shadows and flatten the highlights of an image that was
// already finished.
non_linear: true, non_linear: true,
id: next_image_id(), id: next_image_id(),
frame: (width, height), frame: (width, height),
@@ -338,7 +339,7 @@ impl DemosaicedImage {
/// what a merge writes. No demosaic; the samples are normalised by the /// what a merge writes. No demosaic; the samples are normalised by the
/// file's black and white levels exactly as the demosaic kernel would /// file's black and white levels exactly as the demosaic kernel would
/// normalise a photosite, and everything else — the matrix, the /// normalise a photosite, and everything else — the matrix, the
/// balance, the body's base curve — is carried through as for a CFA /// balance, the view transform — is carried through as for a CFA
/// file, because the composite is developed as one photograph from the /// file, because the composite is developed as one photograph from the
/// body that took its sources. /// body that took its sources.
pub fn from_linear_rgb16(ctx: &GpuContext, raw: &RawImage) -> Result<Self, GpuError> { pub fn from_linear_rgb16(ctx: &GpuContext, raw: &RawImage) -> Result<Self, GpuError> {
+2 -2
View File
@@ -28,8 +28,8 @@
//! than leaving the specification and the code silently disagreeing. //! than leaving the specification and the code silently disagreeing.
//! //!
//! That texture is the right one on the merits. It is camera-native: no white //! That texture is the right one on the merits. It is camera-native: no white
//! balance has been applied, no camera matrix, no base curve, no tone curve, //! balance has been applied, no camera matrix, no tone curve, no view
//! no output transform. It is normalised by the sensor's own black and white //! transform, no output transform. It is normalised by the sensor's own black and white
//! levels, so 1.0 is saturation by construction and the distribution below it //! levels, so 1.0 is saturation by construction and the distribution below it
//! *is* the headroom question, with no calibration to carry and no origin to //! *is* the headroom question, with no calibration to carry and no origin to
//! choose. //! choose.
+1 -1
View File
@@ -3,7 +3,7 @@
// //
// The shader beside this one, `histogram.wgsl`, counts the frame the display // The shader beside this one, `histogram.wgsl`, counts the frame the display
// is about to show: an 8-bit code value, after white balance, the camera // is about to show: an 8-bit code value, after white balance, the camera
// matrix, the base curve, the tone curve and the output transform. This one // matrix, the tone curve, the view transform and the output transform. This one
// counts the texture the demosaic wrote, before any of that. The two differ in // counts the texture the demosaic wrote, before any of that. The two differ in
// exactly one place — the axis — and everything else here is deliberately the // exactly one place — the axis — and everything else here is deliberately the
// same construction, because the two reductions have the same shape and any // same construction, because the two reductions have the same shape and any
+7 -4
View File
@@ -228,9 +228,11 @@ interpolated points — master, red, green, blue — each reaching the shader on
when it has been moved), `colour_mixer` (thirty-six faceted parameters from when it has been moved), `colour_mixer` (thirty-six faceted parameters from
twelve computed hue bands), `film_sim` (a stock's measured tables, which are twelve computed hue bands), `film_sim` (a stock's measured tables, which are
not parameters, and the one node that declares `Operation::renders` — see not parameters, and the one node that declares `Operation::renders` — see
below), `capture_sharpen` (a separable convolution) and `noise_reduction` (a below), `view_transform` (composed at its defaults, which a declaration cannot
kernel, and one that decides how many dispatches to emit at each resolution) — say — see [What is not a node](#what-is-not-a-node-and-why)), and the five
the last two for the reason the next section gives. `vignetting` is kernels — `capture_sharpen` (a separable convolution), `noise_reduction` (one
that decides how many dispatches to emit at each resolution), `clarity`,
`texture` and `dehaze` — for the reason the next section gives. `vignetting` is
hand-written too but is not in the develop chain — it carries lens-profile hand-written too but is not in the develop chain — it carries lens-profile
coefficients that are not parameters. coefficients that are not parameters.
@@ -262,7 +264,8 @@ clarity, texture, dehaze and spot removal are all defined by what the
of `c` at any price. of `c` at any price.
They go in the **detail stage**, which runs after the fused pass, in linear They go in the **detail stage**, which runs after the fused pass, in linear
light, at render resolution, before the output transform — see light, at render resolution, before the view transform and the output
transform — see
[`../src/detail.rs`](../src/detail.rs) for why each of those is a decision [`../src/detail.rs`](../src/detail.rs) for why each of those is a decision
rather than a convenience. A node of this kind: rather than a convenience. A node of this kind:
+2 -2
View File
@@ -11,8 +11,8 @@ why_rust: |
characteristic curves and a density lookup — which are not parameters and characteristic curves and a density lookup — which are not parameters and
which no `uniforms:` expression could produce. Its neutral is "no stock which no `uniforms:` expression could produce. Its neutral is "no stock
loaded" rather than a set of values, and it is the one node that declares loaded" rather than a set of values, and it is the one node that declares
`Operation::renders`, so the composer omits the camera profile's base curve `Operation::renders`, so while a stock is loaded the composer emits it in
and the conversion out of camera space on its behalf. the view transform's place instead of the default sigmoid.
placement: | placement: |
Last, in the view transform's place (D19, FR-DEV-3j), after every other Last, in the view transform's place (D19, FR-DEV-3j), after every other
+4 -4
View File
@@ -659,10 +659,10 @@ impl Attribute {
/// ///
/// `Effect` after `Colour` is a look laid over a settled picture — and is /// `Effect` after `Colour` is a look laid over a settled picture — and is
/// the one arguable slot. A spectral film simulation declares /// the one arguable slot. A spectral film simulation declares
/// [`crate::Operation::renders`] and replaces the base curve, which is an /// [`crate::Operation::renders`] and takes the view transform's place at
/// argument for treating it as foundational rather than final; an array of /// the very end of the chain (D19), which is an argument for treating it as
/// six cannot say "last, except when it is first". The tension is recorded /// the rendering rather than one effect among others; an array of six
/// here rather than settled. /// cannot say that. The tension is recorded here rather than settled.
/// ///
/// Both ends were wrong for as long as this list only fed a row of chips /// Both ends were wrong for as long as this list only fed a row of chips
/// nobody reads in order. It stopped being harmless when the same list /// nobody reads in order. It stopped being harmless when the same list
+21 -13
View File
@@ -24,9 +24,10 @@
//! v //! v
//! +------------------------------------------+ //! +------------------------------------------+
//! | the fused point-operation pass | one dispatch //! | the fused point-operation pass | one dispatch
//! | white balance, exposure, tone, colour | //! | white balance (camera RGB) |
//! | the mask layers |
//! | camera RGB -> linear sRGB | //! | camera RGB -> linear sRGB |
//! | exposure, tone, colour |
//! | the mask layers |
//! +------------------------------------------+ //! +------------------------------------------+
//! | rgba16float, linear, **unclipped**, at render resolution //! | rgba16float, linear, **unclipped**, at render resolution
//! v //! v
@@ -34,7 +35,13 @@
//! | the detail stage - this module | one dispatch per pass //! | the detail stage - this module | one dispatch per pass
//! | sharpen, NR, clarity, texture, spots | //! | sharpen, NR, clarity, texture, spots |
//! +------------------------------------------+ //! +------------------------------------------+
//! | the last pass applies the output transform //! | rgba16float, still scene-linear and unclipped
//! v
//! +------------------------------------------+
//! | the view pass | one dispatch
//! | view transform, or the film stock |
//! | output transform, mask reveal |
//! +------------------------------------------+
//! v //! v
//! rgba8unorm display or export texture //! rgba8unorm display or export texture
//! ``` //! ```
@@ -52,14 +59,13 @@
//! texture, clarity, spot removal and sharpen/NR sit below the tone curve and //! texture, clarity, spot removal and sharpen/NR sit below the tone curve and
//! the colour mixer. //! the colour mixer.
//! //!
//! **In linear light, after the camera matrix.** The fused pass works in //! **In linear light, after the camera matrix.** A detail pass wants a
//! *camera* space, because white balance and exposure are physically //! luminance, and camera RGB has no luminance — the three channels are
//! meaningful there and nowhere else. A detail pass is the opposite case: it
//! wants a luminance, and camera RGB has no luminance — the three channels are
//! whatever the CFA's dyes passed, and weighting them 0.2126/0.7152/0.0722 //! whatever the CFA's dyes passed, and weighting them 0.2126/0.7152/0.0722
//! would be numerology. So the split is taken *after* the `cam_to_srgb` //! would be numerology. Since D19 only white balance runs in camera RGB; the
//! multiply, where the working space is linear sRGB and a luminance is a //! `cam_to_srgb` multiply follows it, so every point operation, and every
//! luminance. //! detail pass after them, works in linear sRGB primaries, where a luminance
//! is a luminance.
//! //!
//! **Before the output transform, and before the clip.** FR-DEV-2 allows //! **Before the output transform, and before the clip.** FR-DEV-2 allows
//! exactly one quantisation, at the display or export stage. A detail pass //! exactly one quantisation, at the display or export stage. A detail pass
@@ -70,9 +76,11 @@
//! therefore `rgba16float` and holds linear values that have **not** been //! therefore `rgba16float` and holds linear values that have **not** been
//! clamped to `0..=1`: a recovered highlight is still above one at this point, //! clamped to `0..=1`: a recovered highlight is still above one at this point,
//! and clipping it before the sharpener sees it would put a hard edge exactly //! and clipping it before the sharpener sees it would put a hard edge exactly
//! where the sharpener is most visible. The last detail pass performs the //! where the sharpener is most visible. Every detail pass writes such an
//! primaries conversion, the clip and the encode, so the single quantisation //! intermediate, the last one included, and the view pass after them — the
//! stays single. //! view transform, then the output transform's primaries, clip and encode —
//! is the one place the scene is fitted to a display (D19, ARCH §6.14), so
//! the single quantisation stays single.
//! //!
//! **After framing, at render resolution.** The alternative — running detail //! **After framing, at render resolution.** The alternative — running detail
//! on the demosaiced source before the framing prologue — is superficially //! on the demosaiced source before the framing prologue — is superficially
+2 -2
View File
@@ -2068,8 +2068,8 @@ pub(crate) struct LayerShader {
/// ///
/// Kept apart from the rest because it belongs at the other end of the /// Kept apart from the rest because it belongs at the other end of the
/// shader. Everything else runs on scene-referred colour in the working /// shader. Everything else runs on scene-referred colour in the working
/// space, where a flat tint would then be pushed through the base curve /// space, where a flat tint would then be pushed through the view
/// and the camera matrix and arrive as some other colour, and a /// transform and arrive as some other colour, and a
/// white-on-black alpha would arrive as neither. This runs after the /// white-on-black alpha would arrive as neither. This runs after the
/// output transform, so what is written is what is seen. /// output transform, so what is written is what is seen.
pub reveal: String, pub reveal: String,
+2 -2
View File
@@ -82,8 +82,8 @@ const FLOOR: f32 = 1e-4;
/// Move the graph so that `sample` renders neutral. /// Move the graph so that `sample` renders neutral.
/// ///
/// `sample` is the linear triple the operation's own gains multiply — camera /// `sample` is the linear triple the operation's own gains multiply — camera
/// RGB with the camera's as-shot balance on, *before* the body's base curve /// RGB with the camera's as-shot balance on, *before* the camera matrix and
/// and matrix, and with the sampling operation at its defaults. Not the /// the view transform, and with the sampling operation at its defaults. Not the
/// pixel on the screen: the matrix mixes the channels on the way there, so /// pixel on the screen: the matrix mixes the channels on the way there, so
/// a colour read after it does not answer to these gains, and a solve over /// a colour read after it does not answer to these gains, and a solve over
/// one lands somewhere no sample asked for. Returns whether the graph was /// one lands somewhere no sample asked for. Returns whether the graph was
+1 -1
View File
@@ -518,7 +518,7 @@ pub enum OutputMode {
/// ///
/// What a merge stitches (FR-MRG-2). The shader is the linear tail with /// What a merge stitches (FR-MRG-2). The shader is the linear tail with
/// no operations, and the caller fills the reserved uniforms neutral — /// no operations, and the caller fills the reserved uniforms neutral —
/// unit white balance, identity matrix, base curve off — so what is /// unit white balance, identity matrix, no view transform — so what is
/// stored is the sensor's own numbers, demosaiced and undistorted. Only /// stored is the sensor's own numbers, demosaiced and undistorted. Only
/// [`compose_camera_probe`] produces it — for a merge through /// [`compose_camera_probe`] produces it — for a merge through
/// [`compose_camera_linear`], and for the white balance picker under the /// [`compose_camera_linear`], and for the white balance picker under the
+4 -4
View File
@@ -706,10 +706,10 @@ mod tests {
#[test] #[test]
fn it_declares_itself_a_rendering_transform() { fn it_declares_itself_a_rendering_transform() {
// The whole reason the composer skips the base curve and the camera // The whole reason the composer emits the stock in the view
// matrix. If this ever returned false the picture would be rendered // transform's place rather than beside it. If this ever returned
// twice and converted twice, which looks like a colour management bug // false the picture would be rendered twice, which looks like a
// a long way from here. // colour management bug a long way from here.
assert!(FilmSim::new().renders()); assert!(FilmSim::new().renders());
} }
+23
View File
@@ -515,3 +515,26 @@ texture and dehaze; the scenes without dehaze moved within ±2%. The
picture is the same bits: a minimum is exact in any order, the window is picture is the same bits: a minimum is exact in any order, the window is
the one the split passes covered, and the rgba8 output hashed identically the one the split passes covered, and the rgba8 output hashed identically
before and after in all 64 scene, view and size combinations measured. before and after in all 64 scene, view and size combinations measured.
## The view pass after the detail stage — 2026-09-27
**Status:** Not measured. Every figure above predates it.
**A chain with a detail stage is now one dispatch longer** (`c07f81e`,
D19). The fused pass used to end in the rendering — the base curve, then
the output transform — before it stored, so every detail pass convolved
display-referred values, and the last detail pass encoded. Now the fused
pass stops before the view transform, every detail pass writes a
scene-linear `rgba16float` intermediate, the last one included, and a view
pass composed from the same inputs reads the result and runs the view
transform (or the film stock), the output transform and the mask reveal.
What that adds, per frame with a detail stage: one render-sized read and
write, and a third intermediate for a one-pass chain. By the dehaze
section's own figure that is about 4 ms at 2560 × 1600 on the laptop
RTX 3050 under its power cap. What it removed: a capture sharpening too
fine to draw at the current scale no longer emits a pass-through, and
there is no resolve pass for an active kernel with nothing to draw. A
chain with no detail operation is unchanged, one fused dispatch with the
view transform at its tail. The rows above that name a detail operation
should be re-run before they are quoted.
+5 -3
View File
@@ -143,7 +143,9 @@ composer already makes that a matter of uniforms rather than structure: the
white balance, the matrix and the curve's active flag are all in the reserved white balance, the matrix and the curve's active flag are all in the reserved
uniform block, and a fused pass with no operations, `as_shot_wb = 1`, uniform block, and a fused pass with no operations, `as_shot_wb = 1`,
`cam_to_srgb = I` and `base_curve_last.z = 0` stores exactly camera-linear `cam_to_srgb = I` and `base_curve_last.z = 0` stores exactly camera-linear
RGB after the warp. So the tap is: RGB after the warp. *(Since D19 there is no curve flag: the base curve is
gone, and the tap composes no view transform, so the white balance and the
matrix are the only uniforms it fills neutral.)* So the tap is:
- `EditGraph::compose_camera_linear()` — the `LinearWorking` tail with an - `EditGraph::compose_camera_linear()` — the `LinearWorking` tail with an
empty operation list and identity framing, paired by name with empty operation list and identity framing, paired by name with
@@ -158,8 +160,8 @@ be re-developed deserves the sensor's precision. The cost is 2× on buffers
FR-MRG-11 already bounds. FR-MRG-11 already bounds.
**What the DNG carries as a consequence:** the first source's `Make`, **What the DNG carries as a consequence:** the first source's `Make`,
`Model` and `UniqueCameraModel` — so `base_curve::for_body` finds the 6D's `Model` and `UniqueCameraModel` — so `base_curve::for_body` found the 6D's
curve (retired with the base curves, D19) — its `ColorMatrix1`/`2` with illuminants, and its `AsShotNeutral`. The curve, until D19 retired the per-body curves — its `ColorMatrix1`/`2` with illuminants, and its `AsShotNeutral`. The
composite then develops through the same profile as its sources, applied composite then develops through the same profile as its sources, applied
once. The spike's 64 × 48 file (§8) already carries the matrix and neutral; once. The spike's 64 × 48 file (§8) already carries the matrix and neutral;
the body name is a string. the body name is a string.
+10 -7
View File
@@ -565,8 +565,9 @@ display 0.18. A photograph with the view transform at its defaults is **unedited
is always composed, and "active" keeps meaning "moved from the defaults", so an untouched image 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. 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 An already-rendered source — a JPEG — is not rendered again: the view transform is skipped for it,
for it, as the base curve was. A film stock is not, because choosing one is an edit. as the base curve was, so its two sliders do not move a JPEG. A film stock is not skipped, because
choosing one is an edit.
*Acceptance:* monotone in each channel; a neutral stays neutral; middle grey lands within 0.01 of *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 0.18; between scene 0.03 and 1.0, the default is within 0.3 EV of the retired default curve; the
@@ -1911,7 +1912,7 @@ with no depth to recover — so it is where the shared machinery is built.
**FR-MRG-2 — What is stitched.** Each source enters the merge in **camera space**: after black **FR-MRG-2 — What is stitched.** Each source enters the merge in **camera space**: after black
and white levels, demosaic and lens distortion correction, and before everything else — no white and white levels, demosaic and lens distortion correction, and before everything else — no white
balance, no base curve, no camera matrix, no edit. The composite carries the first source's body, balance, no camera matrix, no edit, no view transform. The composite carries the first source's body,
colour matrix and as-shot neutral, so that it is developed afterwards exactly as one of its colour matrix and as-shot neutral, so that it is developed afterwards exactly as one of its
sources would be: the camera profile, the white balance and every operation in §3.3 are applied sources would be: the camera profile, the white balance and every operation in §3.3 are applied
once, to the composite, in its own develop. once, to the composite, in its own develop.
@@ -1920,9 +1921,11 @@ This is the clause that decides what the output *is*. Stitching the rendered edi
stitcher does; the result cannot be re-developed, and any difference between the frames' edits stitcher does; the result cannot be re-developed, and any difference between the frames' edits
becomes a seam. Stitching camera-space pixels produces a photograph the camera could have taken, becomes a seam. Stitching camera-space pixels produces a photograph the camera could have taken,
and nothing is applied twice. The cut sits *below* the profile, not above it, for a reason S15.3 and nothing is applied twice. The cut sits *below* the profile, not above it, for a reason S15.3
found in the pipeline: the base curve is part of the profile (FR-DEV-3e) and is applied to every found in the pipeline: the profile's rendering was applied to every frame of a known body, so a
frame of a known body, so a composite that baked it in and then developed as one would render the composite that baked it in and then developed as one would render it twice. *(Amended 2026-09-27,
curve twice. Lens correction alone sits above the cut, because a distorted frame does not align. D19: that rendering was the per-body base curve, retired; the view transform that replaces it is
applied to every develop, so the reason stands.)* Lens correction alone sits above the cut, because
a distorted frame does not align.
White balance sits below it because the sensor saw the same light in every frame: un-balanced White balance sits below it because the sensor saw the same light in every frame: un-balanced
camera RGB agrees across the overlaps whether or not the camera's auto white balance drifted, and camera RGB agrees across the overlaps whether or not the camera's auto white balance drifted, and
the balanced values would not. the balanced values would not.
@@ -2429,7 +2432,7 @@ Settled by requirements calibration, 2026-08-08.
| Culling | **The core differentiator** (§3.9) | | Culling | **The core differentiator** (§3.9) |
| Focus checking | Peaking *and* zoom | | Focus checking | Peaking *and* zoom |
| Ingest | Full workflow — template rename, checksum verify, dual-destination | | Ingest | Full workflow — template rename, checksum verify, dual-destination |
| Colour defaults | Good, not obsessive — matrices plus per-body base curve | | Colour defaults | Good, not obsessive — matrices plus one scene-referred view transform for every body (D19; the per-body base curves are retired) |
| Film simulation | Fujifilm explicitly targeted | | Film simulation | Fujifilm explicitly targeted |
| AI | Denoise in v1; masking deferred. Per-face eye state and head pose are in v1 **as culling evidence, not AI** (FR-CULL-8a, FR-CULL-13); gaze deferred (§7) | | AI | Denoise in v1; masking deferred. Per-face eye state and head pose are in v1 **as culling evidence, not AI** (FR-CULL-8a, FR-CULL-13); gaze deferred (§7) |
| Local adjustments | Full masking, GPU-rasterised | | Local adjustments | Full masking, GPU-rasterised |
+1 -1
View File
@@ -416,7 +416,7 @@ impl DevelopSession {
/// **This is the other histogram, not a variant of the one above**, and /// **This is the other histogram, not a variant of the one above**, and
/// the two answer questions that a culling decision needs kept apart. /// the two answer questions that a culling decision needs kept apart.
/// [`Self::histogram`] counts the frame on the canvas, after white /// [`Self::histogram`] counts the frame on the canvas, after white
/// balance, the camera matrix, the base curve, the tone curve and the /// balance, the camera matrix, the tone curve, the view transform and the
/// output transform: a clipped bin there is a highlight that is gone as /// output transform: a clipped bin there is a highlight that is gone as
/// the image currently stands. This counts the demosaiced scene-linear /// the image currently stands. This counts the demosaiced scene-linear
/// texture, before any of that, on an axis of stops below sensor /// texture, before any of that, on an axis of stops below sensor
+2 -2
View File
@@ -49,8 +49,8 @@ impl DevelopSession {
/// ///
/// **Measured where the operation acts, not where the photographer /// **Measured where the operation acts, not where the photographer
/// looks.** The white balance node runs first in the chain, on camera /// looks.** The white balance node runs first in the chain, on camera
/// RGB, before the body's base curve and its matrix; the canvas shows /// RGB, before the camera matrix and the view transform; the canvas
/// the pixel after all three. The probe used to be read off a display /// shows the pixel after all three. The probe used to be read off a display
/// render with the adjustments stripped, and the solve then treated an /// render with the adjustments stripped, and the solve then treated an
/// sRGB triple as if the gains multiplied it directly. On a JPEG the two /// sRGB triple as if the gains multiplied it directly. On a JPEG the two
/// spaces coincide, so it worked; on a raw file from any real body the /// spaces coincide, so it worked; on a raw file from any real body the