From 7558053932276f7f5c680fb399566d222371c9a9 Mon Sep 17 00:00:00 2001
From: Duncan Tourolle
Date: Sun, 27 Sep 2026 07:59:29 -0400
Subject: [PATCH] Say that a mask layer's settings add to the photograph's
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
fc54523 (0.18.1) stopped running a layer as a second chain after every
global operation and made its settings offsets applied at each
operation's own place, and nothing outside the code said so. The
manual still read as though a mask's slider were a setting of its own,
frame-budget.md described the masks as "a separate chain per layer",
and no document said how a layer is blended at all.
architecture.md §5.2 now says it: the offset, the blend by the layer's
weighted difference from the global result, what a photograph with no
layers composes to, and the film as the one operation whose settings
are averaged instead (6b99f67). FR-DEV-3 records the change as resolved
beside its local-adjustments bullet, frame-budget.md's note on what it
does not measure describes the cost as it now is, and the manual gives
the arithmetic in one sentence: -20 in a mask over -30 is -50 there.
The bundled manual is regenerated to match.
---
docs/dev/architecture.md | 12 ++++++++++++
docs/dev/frame-budget.md | 9 ++++++---
docs/dev/requirements.md | 7 +++++++
docs/manual/README.md | 5 ++++-
docs/manual/index.html | 5 ++++-
5 files changed, 33 insertions(+), 5 deletions(-)
diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md
index f83ed78..f298d44 100644
--- a/docs/dev/architecture.md
+++ b/docs/dev/architecture.md
@@ -390,6 +390,18 @@ keeps a star: real light arrives through a lens and lights a patch, so its neigh
6×6 sensor-anchored colour tile serves Bayer and X-Trans alike, every path that demosaics gets it,
and there is no setting.
+**A mask layer runs inside this chain, not after it.** Its settings are offsets to the global ones
+(`mask::offset_onto`), and each operation a layer touches is composed at the operation's own place:
+the global fragment and each layer's combined fragment read the same input, and the pixel moves by
+each layer's weighted difference, `c_g + Σ wᵢ(cᵢ − c_g)`. At full weight that is the combined
+setting exactly and at zero the global result exactly, so global contrast −30 under a layer at −20
+is contrast −50 where contrast runs, never −30 now and −20 again later — which is what the layer
+chain did before 0.18.1, and how the shadows of a night shot went magenta. A photograph with no
+layers composes to the same shader byte for byte. The film is the one exception, because it is a
+rendering rather than an adjustment and cross-fading two developments is not what a region of a
+pushed negative looks like: an operation that `blends_settings` has its uniforms averaged by mask
+weight instead, and runs once (FR-DEV-3f).
+
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 demosaic box's output —
because what it measures is the file rather than the render. See §5.5.
diff --git a/docs/dev/frame-budget.md b/docs/dev/frame-budget.md
index 64340bc..0d0511b 100644
--- a/docs/dev/frame-budget.md
+++ b/docs/dev/frame-budget.md
@@ -325,9 +325,12 @@ without measuring them:
Stated because §7 of [display-and-extension.md](display-and-extension.md) asks
for it, and because each of these could move the numbers.
-- **Local adjustments.** The mask stack is a separate chain per layer and is not
- in any row above. `render_masked` takes them and the fused shader addresses
- them per layer, so a heavily masked edit costs more than `all`.
+- **Local adjustments.** Not in any row above. Since 0.18.1 a layer is no longer
+ a separate chain after the global one: each operation a layer touches runs a
+ second fragment at its own place in the chain, blended by the layer's mask
+ ([architecture.md §5.2](architecture.md)), and `render_masked` binds the mask
+ array the fused shader samples. A heavily masked edit therefore costs more
+ than `all`, by roughly one fragment per touched operation per layer.
- **Spot repairs.** These add detail passes, and their cost is per spot.
- **Lens corrections.** Not part of `EditGraph::default_chain` — they are built
from a matched profile — so the `point` row does not include the warp chain.
diff --git a/docs/dev/requirements.md b/docs/dev/requirements.md
index 5f801a6..49a3e20 100644
--- a/docs/dev/requirements.md
+++ b/docs/dev/requirements.md
@@ -326,6 +326,13 @@ once, at the final export or display stage.
- Crop, straighten, rotate, flip
- Local adjustments: linear gradient, radial gradient, and brush masks
+*Resolved 2026-09-26:* a mask layer's settings are **offsets to the photograph's**, applied at each
+operation's own place in the chain — global contrast −30 under a layer at −20 is −50 inside the
+mask, applied once. A moved switch or choice replaces the global one, and an offset that brings an
+operation back to neutral undoes the global setting inside the mask. Layers used to run as a second
+chain after every global operation, which compounded the two edits in ways neither slider showed
+([architecture.md §5.2](architecture.md); `core/dr-gpu/tests/local_adjustments.rs`).
+
**FR-DEV-3a — Self-describing operations.** Every processing operation shall declare its own
parameters through a descriptor, so that adding an operation requires no changes to frontend code.
An operation declares *what* its parameters are; the frontend decides *how* to present them.
diff --git a/docs/manual/README.md b/docs/manual/README.md
index 2998ffc..429ed96 100644
--- a/docs/manual/README.md
+++ b/docs/manual/README.md
@@ -235,7 +235,10 @@ take the crop back or keep it.
`Local` in the rail turns the column into a mask stack. `Find subjects` runs a
segmentation model over the photograph; what it recognises appears as a list
of categories with how much of the frame each covers. Click one and it is a
-mask — then every slider below edits only that region.
+mask — then every slider below edits only that region. A mask's slider adds to
+the photograph's own rather than repeating it: contrast −20 in the mask over
+−30 on the whole frame is −50 there, and +30 in the mask cancels the frame's
+−30 inside it.

diff --git a/docs/manual/index.html b/docs/manual/index.html
index 852250b..617db0f 100644
--- a/docs/manual/index.html
+++ b/docs/manual/index.html
@@ -306,7 +306,10 @@ take the crop back or keep it.
Local in the rail turns the column into a mask stack. Find subjects runs a
segmentation model over the photograph; what it recognises appears as a list
of categories with how much of the frame each covers. Click one and it is a
-mask — then every slider below edits only that region.
+mask — then every slider below edits only that region. A mask's slider adds to
+the photograph's own rather than repeating it: contrast −20 in the mask over
+−30 on the whole frame is −50 there, and +30 in the mask cancels the frame's
+−30 inside it.
What the model found in an urban scene: ground, architecture, sky, vegetationThe sky chosen: tinted on the photograph, and the column now scoped to it
A mask is a stack of parts. Paint into it, subtract a gradient from it, grow