Convert to the working space before the edits, not after

Every point operation ran in camera RGB and the camera matrix came after
them all, against the order ARCH §5.2 draws. So `luminance()` applied
Rec.709 weights to a body's own primaries, a band in the colour mixer
was a different hue on every make of sensor, and the vibrance skin guard
tested channel order in a space where skin does not have one.

Operations now declare a `Stage`. White balance is the only camera-stage
node — its multipliers scale the sensor's channels, and after a matrix
that mixes them the same numbers are a different correction — and says
so with `stage: camera` in its YAML, a key both the build-time generator
and the load-time declared op read. The composer emits the camera nodes,
then the matrix, then the rest, each group in graph order; an empty
chain still gets the matrix.

Film simulation stops converting out of camera space itself, since it is
now handed working-space colour like every other scene node. The base
curve stays where it was, after the operations, and so now acts on
working-space colour; the next commit replaces it (D19).
This commit is contained in:
2026-09-27 16:52:53 -04:00
parent e8898a5c38
commit db7b84795c
9 changed files with 234 additions and 123 deletions
+10 -12
View File
@@ -396,14 +396,10 @@ impl Operation for FilmSim {
// sampler binding, and adding one to interpolate two lookups would
// cost a binding in every shader whether or not a film is loaded.
"\
// Camera RGB to linear sRGB. The film's exposure matrix is defined against
// sRGB primaries, and this node has taken over the conversion the composer
// would otherwise have emitted at the end — see `Operation::renders`.
let scene = vec3<f32>(
dot(u.cam_to_srgb_0.rgb, c),
dot(u.cam_to_srgb_1.rgb, c),
dot(u.cam_to_srgb_2.rgb, c),
);
// Working-space colour, which is linear sRGB primaries — what the film's
// exposure matrix is defined against. The composer converted out of camera
// RGB before any scene-stage operation ran (D19).
let scene = c;
// What each emulsion layer was exposed to. A matrix, exactly: the scene
// spectrum reconstructed from an sRGB triple is linear in that triple, so the
@@ -727,13 +723,15 @@ mod tests {
}
#[test]
fn the_fragment_converts_out_of_camera_space_itself() {
// It has to: it has taken over the conversion the composer would
// otherwise emit at the end.
fn the_fragment_is_handed_working_space_colour() {
// TRACES: FR-DEV-3f
// D19: the composer leaves camera space before any scene-stage
// operation, so a film converting again would apply the camera
// matrix twice.
let mut op = FilmSim::new();
op.set_tables(Some(tables()));
let wgsl = op.wgsl_body();
assert!(wgsl.contains("cam_to_srgb_0"), "{wgsl}");
assert!(!wgsl.contains("cam_to_srgb"), "{wgsl}");
}
#[test]