Merge branch 'worktree-agent-a75c901968abfa183' into integration

# Conflicts:
#	core/dr-gpu/src/adjust.rs
#	core/dr-pipeline/ops/README.md
#	core/dr-pipeline/src/lib.rs
#	core/dr-pipeline/src/ops/mod.rs
#	ui/dr-ui/src/develop.rs
This commit is contained in:
2026-08-22 19:29:39 +02:00
10 changed files with 1783 additions and 77 deletions
+23 -10
View File
@@ -1056,13 +1056,23 @@ impl DevelopSession {
/// through the framing map, so one array is correct at every output size:
/// a 256px thumbnail and a 24 MP export bind the same texture.
///
/// `space` is the output space `shader` was composed for, and it has to be
/// passed rather than assumed because the **detail stage** is composed
/// here too and the two halves must agree. When an edit has an active
/// neighbourhood operation the fused pass stops at unclipped linear
/// working values and the last detail pass performs the output transform;
/// composing the fused half for Display P3 and the detail half for sRGB
/// would encode the export in the wrong space, with nothing to notice it.
/// **And the detail stage with it.** The neighbourhood operations — noise
/// reduction, capture sharpening, and the rest of FR-DEV-3's kernels —
/// cannot be fused into the single dispatch, so an edit using one composes
/// a fused pass that hands on *linear* values and a chain of passes that
/// finishes the job (see `dr_pipeline::detail`). Those two halves must be
/// composed from one graph and dispatched together, or the fused shader's
/// storage format does not match the texture bound to it; going through
/// `render_detailed` here is what makes that true of every path at once.
/// It falls through to the plain render when the chain is empty, which is
/// almost every edit, so this costs nothing to the frames that do not
/// need it.
///
/// `space` has to be the space `shader` was composed for. It is the last
/// pass of the detail chain that performs the output transform when there
/// is one, so the two would otherwise be free to disagree about which
/// primaries the file is in — and the result would be a correctly
/// labelled file with the wrong colours in it (FR-EXP-2).
fn render_with_masks(
&mut self,
shader: &dr_pipeline::operation::ComposedShader,
@@ -1085,15 +1095,18 @@ impl DevelopSession {
// scale-free: a sharpening radius is stated in source pixels and the
// develop view renders at whatever the viewport needs (FR-DSP-1), so
// the conversion is different for the canvas, the thumbnail and the
// export. `render_scale` works the ratio out from the framing, which
// is also what makes zooming to 1:1 restore an exact preview with no
// second render path to maintain.
// export. `render_scale` works the ratio out from the framing, so a
// crop and a zoom are already accounted for, and zooming to 1:1
// restores an exact preview with no second render path to maintain.
//
// Empty for every edit with no active neighbourhood operation — which
// is almost all of them — and `render_detailed` then falls straight
// through to the single masked dispatch this used to call.
let scale = self.graph.render_scale(self.demosaiced.size(), (w, h));
let detail = self.graph.compose_detail_for(scale, space);
// Detail passes read what the colour pass wrote, so the key they are
// cached against is the colour key: moving a sharpening slider re-runs
// this stage and not the fused one (FR-DEV-3d).
let colour_key = self
.graph
.invalidation()