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. ![What the model found in an urban scene: ground, architecture, sky, vegetation](media/local-categories.png) 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, vegetation
What the model found in an urban scene: ground, architecture, sky, vegetation
The sky chosen: tinted on the photograph, and the column now scoped to it
The 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