WIP: noise reduction

Checkpoint committed by the coordinator, not by the authoring agent: the
session hit its API limit mid-task and left this work uncommitted. Committed
so it survives, NOT because it is finished - expect failing tests and
half-applied changes. The agent resumes from here.
This commit is contained in:
2026-08-22 19:01:18 +02:00
parent c963dafd09
commit b4e55b47c1
6 changed files with 1469 additions and 5 deletions
+45 -4
View File
@@ -891,11 +891,30 @@ impl DevelopSession {
/// The mask array is rasterised in source space at proxy size and sampled
/// 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.
///
/// **And the detail stage with it.** The neighbourhood operations — noise
/// reduction, 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,
w: u32,
h: u32,
space: dr_types::ColourSpace,
) -> Result<(), String> {
let ctx = self.ctx.clone();
self.ensure_subject_fields(&ctx);
@@ -905,8 +924,30 @@ impl DevelopSession {
.then(|| self.masks.as_ref().and_then(|p| p.array()))
.flatten();
// The scale a kernel's radius is converted through. Worked out from
// the framing, so a crop and a zoom are already accounted for: what
// matters to a sensor-sized radius is how many source pixels one
// render pixel stands for, here and now (FR-DSP-1).
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()
.through(dr_pipeline::Affects::Colour);
self.adjust
.render_masked(&self.demosaiced, shader, w, h, masks)
.render_detailed(
&self.demosaiced,
shader,
w,
h,
masks,
&detail,
colour_key,
)
.map(|_| ())
.map_err(|e| e.to_string())
}
@@ -1605,7 +1646,7 @@ impl DevelopSession {
// Rasterise the masks first: the shader addresses array slices by
// index, so the array has to describe *this* stack before it is bound.
self.render_with_masks(&shader, w, h)?;
self.render_with_masks(&shader, w, h, dr_types::ColourSpace::Srgb)?;
let texture = self.adjust.output().ok_or("nothing was rendered")?;
// The import is fallible on format and usage only, and both are fixed
@@ -1729,7 +1770,7 @@ impl DevelopSession {
let (w, h) = self.graph.output_size(sw, sh);
let shader = self.graph.compose_for(space);
self.render_with_masks(&shader, w, h)?;
self.render_with_masks(&shader, w, h, space)?;
let (pixels, rw, rh) = self.adjust.export_pixels().map_err(|e| e.to_string())?;
dr_export::Frame::in_space(rw, rh, pixels, space).map_err(|e| e.to_string())
@@ -1756,7 +1797,7 @@ impl DevelopSession {
let (w, h) = fit(fw, fh, edge.max(1), edge.max(1));
let shader = self.graph.compose_for(dr_types::ColourSpace::Srgb);
self.render_with_masks(&shader, w, h)?;
self.render_with_masks(&shader, w, h, dr_types::ColourSpace::Srgb)?;
let (pixels, rw, rh) = self.adjust.export_pixels().map_err(|e| e.to_string())?;
Ok((rw, rh, pixels))