Format the tree the way the gate asks for it
`cargo fmt --check` is a required step and had drifted across 45 files. Most of it arrived this week: several operations were written in parallel worktrees and merged by hand, and a hand-merge resolves conflicts without ever running the formatter over the result. No behaviour changes — this is `cargo fmt --all` and nothing else, kept as its own commit so the next reader can skip it wholesale rather than search it for one that matters. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,9 +26,7 @@
|
||||
//! mask pinned to the viewport, a slice read from the wrong layer — shows up
|
||||
//! here as an adjustment in the wrong place, and nowhere else.
|
||||
|
||||
use dr_gpu::{
|
||||
AdjustPass, DemosaicedImage, Demosaicer, GpuContext, MaskPass, SubjectMasks,
|
||||
};
|
||||
use dr_gpu::{AdjustPass, DemosaicedImage, Demosaicer, GpuContext, MaskPass, SubjectMasks};
|
||||
use dr_pipeline::descriptor::ParamId;
|
||||
use dr_pipeline::mask::{MaskLayer, MaskSource, MaskStack, Morphology};
|
||||
use dr_pipeline::operation::compose_full;
|
||||
@@ -152,7 +150,18 @@ fn main() {
|
||||
drain.feather = 0.02;
|
||||
pop.push(drain);
|
||||
|
||||
render_stack(&ctx, &source, &mut masks, &mut adjust, &pop, &alpha, pw, ph, ow, oh);
|
||||
render_stack(
|
||||
&ctx,
|
||||
&source,
|
||||
&mut masks,
|
||||
&mut adjust,
|
||||
&pop,
|
||||
&alpha,
|
||||
pw,
|
||||
ph,
|
||||
ow,
|
||||
oh,
|
||||
);
|
||||
write(&format!("{prefix}-colour-pop.ppm"), &adjust);
|
||||
|
||||
// ---- 2. lift the subject out of its background ------------------------
|
||||
@@ -170,7 +179,18 @@ fn main() {
|
||||
darker.feather = 0.03;
|
||||
lift.push(darker);
|
||||
|
||||
render_stack(&ctx, &source, &mut masks, &mut adjust, &lift, &alpha, pw, ph, ow, oh);
|
||||
render_stack(
|
||||
&ctx,
|
||||
&source,
|
||||
&mut masks,
|
||||
&mut adjust,
|
||||
&lift,
|
||||
&alpha,
|
||||
pw,
|
||||
ph,
|
||||
ow,
|
||||
oh,
|
||||
);
|
||||
write(&format!("{prefix}-subject-lift.ppm"), &adjust);
|
||||
|
||||
// ---- 3. the same edit, grown and shrunk -------------------------------
|
||||
@@ -192,7 +212,18 @@ fn main() {
|
||||
layer.morph_radius = radius;
|
||||
stack.push(layer);
|
||||
|
||||
render_stack(&ctx, &source, &mut masks, &mut adjust, &stack, &alpha, pw, ph, ow, oh);
|
||||
render_stack(
|
||||
&ctx,
|
||||
&source,
|
||||
&mut masks,
|
||||
&mut adjust,
|
||||
&stack,
|
||||
&alpha,
|
||||
pw,
|
||||
ph,
|
||||
ow,
|
||||
oh,
|
||||
);
|
||||
write(&format!("{prefix}-{name}.ppm"), &adjust);
|
||||
}
|
||||
|
||||
@@ -292,12 +323,18 @@ fn render_stack(
|
||||
|
||||
fn fit(w: u32, h: u32, longest: u32) -> (u32, u32) {
|
||||
let s = (longest as f32 / w.max(h) as f32).min(1.0);
|
||||
(((w as f32 * s) as u32).max(1), ((h as f32 * s) as u32).max(1))
|
||||
(
|
||||
((w as f32 * s) as u32).max(1),
|
||||
((h as f32 * s) as u32).max(1),
|
||||
)
|
||||
}
|
||||
|
||||
fn write(path: &str, adjust: &AdjustPass) {
|
||||
let (rgba, w, h) = adjust.export_pixels().expect("readback");
|
||||
let rgb: Vec<u8> = rgba.chunks_exact(4).flat_map(|p| [p[0], p[1], p[2]]).collect();
|
||||
let rgb: Vec<u8> = rgba
|
||||
.chunks_exact(4)
|
||||
.flat_map(|p| [p[0], p[1], p[2]])
|
||||
.collect();
|
||||
write_ppm(path, &rgb, w, h);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
//! PPM for the same reason `develop` uses it: no encoder dependency, and
|
||||
//! every viewer reads it. This is a diagnostic, not an export path.
|
||||
|
||||
use dr_segment::{MergeTree, RegionField};
|
||||
use dr_gpu::{DemosaicedImage, Demosaicer, GpuContext, SegmentOptions, SegmentPass};
|
||||
use dr_segment::{MergeTree, RegionField};
|
||||
|
||||
/// The ladder the example dumps. Chosen to span "far too fine to be useful"
|
||||
/// through "one or two objects", because both ends are informative: if no rung
|
||||
|
||||
@@ -589,14 +589,14 @@ impl AdjustPass {
|
||||
});
|
||||
|
||||
if !reuse {
|
||||
let params_buf = self
|
||||
.ctx
|
||||
.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("adjust-params"),
|
||||
contents: bytemuck::cast_slice(&uniforms),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
let params_buf =
|
||||
self.ctx
|
||||
.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("adjust-params"),
|
||||
contents: bytemuck::cast_slice(&uniforms),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
let bind_group = self
|
||||
.ctx
|
||||
.device
|
||||
|
||||
@@ -123,8 +123,7 @@ impl Intermediates {
|
||||
// output these are never handed to a compositor, and no
|
||||
// COPY_SRC, because nothing reads them back — that is the
|
||||
// point (ARCH §6.1).
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = texture.create_view(&Default::default());
|
||||
|
||||
@@ -30,8 +30,8 @@ pub use adjust::AdjustPass;
|
||||
// rather than an implementation detail: a detail pass is guaranteed linear,
|
||||
// unclipped, full internal precision (FR-DEV-2), and anyone reasoning about
|
||||
// VRAM at 24 MP needs to know what an intermediate costs.
|
||||
pub use detail::INTERMEDIATE_FORMAT as DETAIL_INTERMEDIATE_FORMAT;
|
||||
pub use demosaic::{DemosaicedImage, Demosaicer};
|
||||
pub use detail::INTERMEDIATE_FORMAT as DETAIL_INTERMEDIATE_FORMAT;
|
||||
pub use error::GpuError;
|
||||
// Renamed on the way out: `BINS` says enough inside `histogram`, and nothing
|
||||
// at all at a crate root shared with demosaic and segmentation.
|
||||
|
||||
+33
-22
@@ -582,10 +582,7 @@ impl MaskPass {
|
||||
MaskSource::Subject { .. } => match subjects.filter(|s| slot < s.len()) {
|
||||
Some(s) => (s, slot),
|
||||
None => {
|
||||
log::warn!(
|
||||
"mask layer {} has no distance field; skipping",
|
||||
layer.id
|
||||
);
|
||||
log::warn!("mask layer {} has no distance field; skipping", layer.id);
|
||||
continue;
|
||||
}
|
||||
},
|
||||
@@ -599,7 +596,14 @@ impl MaskPass {
|
||||
}
|
||||
_ => {
|
||||
let selected = self.selection_buffer(layer, field);
|
||||
self.draw(&mut encoder, slot as u32, ¶ms, field, &selected, subject);
|
||||
self.draw(
|
||||
&mut encoder,
|
||||
slot as u32,
|
||||
¶ms,
|
||||
field,
|
||||
&selected,
|
||||
subject,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -730,14 +734,14 @@ impl MaskPass {
|
||||
// Still worth beginning the pass: the slice has to be cleared, or an
|
||||
// unpainted layer would show whatever the last edit left in it.
|
||||
let bind_group = (!batch.is_empty()).then(|| {
|
||||
let params_buf = self
|
||||
.ctx
|
||||
.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("mask-brush-params"),
|
||||
contents: bytemuck::bytes_of(params),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
let params_buf =
|
||||
self.ctx
|
||||
.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("mask-brush-params"),
|
||||
contents: bytemuck::bytes_of(params),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
// Rebuilt per rasterisation rather than kept and patched. This runs
|
||||
// when a mask's shape changes, not per frame, and a few kilobytes
|
||||
// of stroke geometry is cheaper to upload than a residency scheme
|
||||
@@ -901,12 +905,9 @@ impl MaskPass {
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: wgpu::BindingResource::TextureView(
|
||||
subject
|
||||
.0
|
||||
.view(subject.1)
|
||||
.unwrap_or_else(|| {
|
||||
self.empty_subject.view(0).expect("placeholder exists")
|
||||
}),
|
||||
subject.0.view(subject.1).unwrap_or_else(|| {
|
||||
self.empty_subject.view(0).expect("placeholder exists")
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -949,7 +950,11 @@ impl MaskPass {
|
||||
}
|
||||
|
||||
fn ensure_array(&mut self, width: u32, height: u32, layers: u32) -> Result<(), GpuError> {
|
||||
if self.array.as_ref().is_some_and(|a| a.matches(width, height, layers)) {
|
||||
if self
|
||||
.array
|
||||
.as_ref()
|
||||
.is_some_and(|a| a.matches(width, height, layers))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1111,7 +1116,9 @@ mod tests {
|
||||
}));
|
||||
|
||||
let mut pass = MaskPass::new(&ctx).expect("mask pass");
|
||||
let array = pass.render(&stack, Some(&field), None, w, h).expect("render");
|
||||
let array = pass
|
||||
.render(&stack, Some(&field), None, w, h)
|
||||
.expect("render");
|
||||
assert_eq!(array.size(), (w, h));
|
||||
assert_eq!(array.layers(), 1);
|
||||
}
|
||||
@@ -1201,7 +1208,11 @@ mod tests {
|
||||
assert_eq!(batch.headers.len(), 1);
|
||||
|
||||
let h = &batch.headers[0];
|
||||
assert!(h.hi[0] - h.lo[0] > 0.2, "wide enough for the dab: {h:?}", h = (h.lo, h.hi));
|
||||
assert!(
|
||||
h.hi[0] - h.lo[0] > 0.2,
|
||||
"wide enough for the dab: {h:?}",
|
||||
h = (h.lo, h.hi)
|
||||
);
|
||||
assert!(
|
||||
h.hi[1] - h.lo[1] > h.hi[0] - h.lo[0],
|
||||
"and taller than it is wide in normalised units, since the radius \
|
||||
|
||||
@@ -62,9 +62,7 @@ fn rendered_level(ctx: &GpuContext, level: u16, curve: BaseCurve) -> u8 {
|
||||
.expect("demosaic");
|
||||
let shader = EditGraph::default_chain().compose();
|
||||
let mut adjust = AdjustPass::new(ctx);
|
||||
adjust
|
||||
.render(&source, &shader, SIZE, SIZE)
|
||||
.expect("render");
|
||||
adjust.render(&source, &shader, SIZE, SIZE).expect("render");
|
||||
let (pixels, _, _) = adjust.export_pixels().expect("readback");
|
||||
let centre = ((SIZE / 2) * SIZE + SIZE / 2) * 4;
|
||||
pixels[centre as usize]
|
||||
|
||||
@@ -84,12 +84,7 @@ fn sharpened(amount: f32, radius: f32, threshold: f32) -> EditGraph {
|
||||
/// The whole calling convention a frontend adopts, in five lines: compose both
|
||||
/// halves from one graph at one output space, ask the graph for the scale, and
|
||||
/// pass the invalidation key through.
|
||||
fn render(
|
||||
pass: &mut AdjustPass,
|
||||
graph: &EditGraph,
|
||||
source: &DemosaicedImage,
|
||||
out: u32,
|
||||
) -> Vec<u8> {
|
||||
fn render(pass: &mut AdjustPass, graph: &EditGraph, source: &DemosaicedImage, out: u32) -> Vec<u8> {
|
||||
let shader = graph.compose_for(ColourSpace::Srgb);
|
||||
let scale = graph.render_scale(source.size(), (out, out));
|
||||
let detail = graph.compose_detail_for(scale, ColourSpace::Srgb);
|
||||
@@ -281,7 +276,10 @@ fn a_proxy_and_an_export_sharpen_the_same_photograph() {
|
||||
proxy_width > 0.05 && export_width > 0.05,
|
||||
"{proxy_width:.3} / {export_width:.3}"
|
||||
);
|
||||
assert!(proxy_area > 1.0 && export_area > 1.0, "{proxy_area} / {export_area}");
|
||||
assert!(
|
||||
proxy_area > 1.0 && export_area > 1.0,
|
||||
"{proxy_area} / {export_area}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -80,9 +80,7 @@ fn expected_profile(size: u32, r: i32) -> Vec<u8> {
|
||||
let edge = (size / 2) as i32;
|
||||
(0..size as i32)
|
||||
.map(|x| {
|
||||
let white = (-r..=r)
|
||||
.filter(|i| (x + i).clamp(0, last) >= edge)
|
||||
.count();
|
||||
let white = (-r..=r).filter(|i| (x + i).clamp(0, last) >= edge).count();
|
||||
srgb_encode(white as f32 / (2 * r + 1) as f32)
|
||||
})
|
||||
.collect()
|
||||
@@ -232,7 +230,10 @@ fn an_inactive_detail_operation_costs_exactly_nothing() {
|
||||
let mut without = AdjustPass::new(&ctx);
|
||||
let b = render(&ctx, &mut without, &plain, &source, SIZE);
|
||||
|
||||
assert_eq!(a, b, "an operation at its defaults must not touch the image");
|
||||
assert_eq!(
|
||||
a, b,
|
||||
"an operation at its defaults must not touch the image"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -252,7 +253,11 @@ fn moving_a_detail_parameter_does_not_re_run_the_colour_pass() {
|
||||
graph.set_param(PROBE, RADIUS, 0.0625);
|
||||
render(&ctx, &mut pass, &graph, &source, SIZE);
|
||||
assert_eq!(pass.colour_dispatches(), 1);
|
||||
assert_eq!(pass.detail_dispatches(), 2, "a separable blur is two passes");
|
||||
assert_eq!(
|
||||
pass.detail_dispatches(),
|
||||
2,
|
||||
"a separable blur is two passes"
|
||||
);
|
||||
|
||||
// Drag the sharpening slider. The colour chain is untouched, so the linear
|
||||
// intermediate it wrote is still exactly right.
|
||||
|
||||
@@ -65,12 +65,7 @@ fn render_at(
|
||||
h: u32,
|
||||
) -> Vec<u8> {
|
||||
let source = grey_at(ctx, w, h);
|
||||
let shader = compose_full(
|
||||
&ops::chain(),
|
||||
&Framing::new(),
|
||||
ColourSpace::Srgb,
|
||||
stack,
|
||||
);
|
||||
let shader = compose_full(&ops::chain(), &Framing::new(), ColourSpace::Srgb, stack);
|
||||
|
||||
let mut masks = MaskPass::new(ctx).expect("mask pass");
|
||||
let array = masks.render(stack, field, None, w, h).expect("rasterise");
|
||||
@@ -245,11 +240,14 @@ fn stacked_layers_use_their_own_masks() {
|
||||
ids: vec![0],
|
||||
}));
|
||||
// Right half down.
|
||||
let mut darken = MaskLayer::new("m2", MaskSource::Regions {
|
||||
signature: 1,
|
||||
level: 2,
|
||||
ids: vec![1],
|
||||
});
|
||||
let mut darken = MaskLayer::new(
|
||||
"m2",
|
||||
MaskSource::Regions {
|
||||
signature: 1,
|
||||
level: 2,
|
||||
ids: vec![1],
|
||||
},
|
||||
);
|
||||
darken.set_param("exposure", ParamId("exposure"), -2.0);
|
||||
stack.push(darken);
|
||||
|
||||
@@ -412,12 +410,7 @@ fn a_stroke_that_doubles_back_does_not_build_up() {
|
||||
return;
|
||||
};
|
||||
|
||||
let once = stack_of(painted(&[(
|
||||
false,
|
||||
0.15,
|
||||
0.5,
|
||||
vec![(0.1, 0.5), (0.9, 0.5)],
|
||||
)]));
|
||||
let once = stack_of(painted(&[(false, 0.15, 0.5, vec![(0.1, 0.5), (0.9, 0.5)])]));
|
||||
let twice = stack_of(painted(&[(
|
||||
false,
|
||||
0.15,
|
||||
@@ -473,10 +466,17 @@ fn a_dab_is_round_on_a_frame_that_is_not_square() {
|
||||
let pixels = render_at(&ctx, &stack, None, W, H);
|
||||
|
||||
let lit = |v: u8| v > 160;
|
||||
let across = (0..W).filter(|&x| lit(luma_in(&pixels, W, x, H / 2))).count();
|
||||
let down = (0..H).filter(|&y| lit(luma_in(&pixels, W, W / 2, y))).count();
|
||||
let across = (0..W)
|
||||
.filter(|&x| lit(luma_in(&pixels, W, x, H / 2)))
|
||||
.count();
|
||||
let down = (0..H)
|
||||
.filter(|&y| lit(luma_in(&pixels, W, W / 2, y)))
|
||||
.count();
|
||||
|
||||
assert!(across > 4 && down > 4, "the dab should exist: {across}x{down}");
|
||||
assert!(
|
||||
across > 4 && down > 4,
|
||||
"the dab should exist: {across}x{down}"
|
||||
);
|
||||
assert!(
|
||||
across.abs_diff(down) <= 2,
|
||||
"a dab must be as wide as it is tall, got {across} across and {down} down"
|
||||
|
||||
@@ -158,7 +158,12 @@ fn clarity_lifts_local_contrast_and_leaves_the_flat_regions_alone() {
|
||||
|
||||
// Far outside the kernel's reach the base equals the pixel, the detail
|
||||
// signal is zero, and the output must be the input to the last code value.
|
||||
for x in [0, reach / 2, SIZE as usize - 1 - reach / 2, SIZE as usize - 1] {
|
||||
for x in [
|
||||
0,
|
||||
reach / 2,
|
||||
SIZE as usize - 1 - reach / 2,
|
||||
SIZE as usize - 1,
|
||||
] {
|
||||
assert!(
|
||||
edited[x].abs_diff(plain[x]) <= 1,
|
||||
"column {x} moved by {} away from any edge",
|
||||
@@ -246,7 +251,10 @@ fn the_soft_limit_bounds_the_halo_at_a_hard_edge() {
|
||||
an unlimited unsharp mask would give"
|
||||
);
|
||||
// But it is still a real effect, not a control that does nothing.
|
||||
assert!(worst > 0.1, "clarity moved almost nothing: {worst:.3} stops");
|
||||
assert!(
|
||||
worst > 0.1,
|
||||
"clarity moved almost nothing: {worst:.3} stops"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -476,7 +484,11 @@ fn dragging_the_slider_re_runs_the_detail_stage_and_nothing_else() {
|
||||
let mut graph = graph_with(CLARITY, 40.0);
|
||||
render(&mut pass, &graph, &source, 256);
|
||||
assert_eq!(pass.colour_dispatches(), 1);
|
||||
assert_eq!(pass.detail_dispatches(), 2, "a separable mask is two passes");
|
||||
assert_eq!(
|
||||
pass.detail_dispatches(),
|
||||
2,
|
||||
"a separable mask is two passes"
|
||||
);
|
||||
let pipelines = pass.cached_detail_pipelines();
|
||||
|
||||
for amount in [50.0, 60.0, 70.0] {
|
||||
|
||||
@@ -25,7 +25,9 @@ fn ctx() -> Option<GpuContext> {
|
||||
}
|
||||
|
||||
fn grey(ctx: &GpuContext, size: u32) -> DemosaicedImage {
|
||||
let data: Vec<u8> = (0..size * size).flat_map(|_| [128, 128, 128, 255]).collect();
|
||||
let data: Vec<u8> = (0..size * size)
|
||||
.flat_map(|_| [128, 128, 128, 255])
|
||||
.collect();
|
||||
DemosaicedImage::from_rgba8(ctx, &data, size, size).expect("upload")
|
||||
}
|
||||
|
||||
|
||||
@@ -112,11 +112,7 @@ fn split(pixels: &[u8], width: u32, x: u32, y: u32) -> (f32, [f32; 3]) {
|
||||
}
|
||||
|
||||
/// Upload an image built from a per-pixel closure.
|
||||
fn upload(
|
||||
ctx: &GpuContext,
|
||||
size: u32,
|
||||
f: impl Fn(u32, u32) -> [u8; 3],
|
||||
) -> DemosaicedImage {
|
||||
fn upload(ctx: &GpuContext, size: u32, f: impl Fn(u32, u32) -> [u8; 3]) -> DemosaicedImage {
|
||||
let data: Vec<u8> = (0..size * size)
|
||||
.flat_map(|i| {
|
||||
let (x, y) = (i % size, i / size);
|
||||
@@ -248,7 +244,11 @@ fn modulation(
|
||||
let mut count = 0.0;
|
||||
for x in margin..(width - margin) {
|
||||
let value = sample(linear(pixels, width, x, row));
|
||||
let sign = if (x / half_period) % 2 == 0 { 1.0 } else { -1.0 };
|
||||
let sign = if (x / half_period) % 2 == 0 {
|
||||
1.0
|
||||
} else {
|
||||
-1.0
|
||||
};
|
||||
total += value * sign;
|
||||
count += 1.0;
|
||||
}
|
||||
@@ -305,7 +305,13 @@ fn chroma_noise_reduction_never_moves_lightness() {
|
||||
let mut off = AdjustPass::new(&ctx);
|
||||
let plain = render(&ctx, &mut off, &graph_with(0.0, 0.0), &source, (SIZE, SIZE));
|
||||
let mut on = AdjustPass::new(&ctx);
|
||||
let chroma = render(&ctx, &mut on, &graph_with(0.0, 100.0), &source, (SIZE, SIZE));
|
||||
let chroma = render(
|
||||
&ctx,
|
||||
&mut on,
|
||||
&graph_with(0.0, 100.0),
|
||||
&source,
|
||||
(SIZE, SIZE),
|
||||
);
|
||||
|
||||
let colour_before = modulation(&plain, SIZE, 4, chroma_r);
|
||||
let colour_after = modulation(&chroma, SIZE, 4, chroma_r);
|
||||
@@ -338,7 +344,13 @@ fn luminance_noise_reduction_never_moves_colour() {
|
||||
let mut off = AdjustPass::new(&ctx);
|
||||
let plain = render(&ctx, &mut off, &graph_with(0.0, 0.0), &source, (SIZE, SIZE));
|
||||
let mut on = AdjustPass::new(&ctx);
|
||||
let luma = render(&ctx, &mut on, &graph_with(100.0, 0.0), &source, (SIZE, SIZE));
|
||||
let luma = render(
|
||||
&ctx,
|
||||
&mut on,
|
||||
&graph_with(100.0, 0.0),
|
||||
&source,
|
||||
(SIZE, SIZE),
|
||||
);
|
||||
|
||||
// The colour difference — not the raw channel, which follows lightness.
|
||||
let row = SIZE / 2;
|
||||
@@ -413,10 +425,10 @@ fn the_same_edit_denoises_the_same_at_two_resolutions() {
|
||||
let Some(ctx) = ctx() else { return };
|
||||
const SOURCE: u32 = 256;
|
||||
const HALF_PERIOD: u32 = 8; // in source pixels
|
||||
// Six code values of swing. Small on purpose: the colour difference has to
|
||||
// land near the filter's threshold, because a larger one is a colour
|
||||
// boundary and the whole point of a bilateral is that it refuses to cross
|
||||
// those. There would be nothing to measure at either resolution.
|
||||
// Six code values of swing. Small on purpose: the colour difference has to
|
||||
// land near the filter's threshold, because a larger one is a colour
|
||||
// boundary and the whole point of a bilateral is that it refuses to cross
|
||||
// those. There would be nothing to measure at either resolution.
|
||||
let source = chroma_pattern(&ctx, SOURCE, HALF_PERIOD, 6);
|
||||
|
||||
// Sixty percent is an eight-source-pixel radius, which halves to exactly
|
||||
@@ -430,14 +442,26 @@ fn the_same_edit_denoises_the_same_at_two_resolutions() {
|
||||
|
||||
let proxy_size = SOURCE / 2;
|
||||
let mut proxy_pass = AdjustPass::new(&ctx);
|
||||
let proxy = render(&ctx, &mut proxy_pass, &graph, &source, (proxy_size, proxy_size));
|
||||
let proxy = render(
|
||||
&ctx,
|
||||
&mut proxy_pass,
|
||||
&graph,
|
||||
&source,
|
||||
(proxy_size, proxy_size),
|
||||
);
|
||||
let proxy_amp = modulation(&proxy, proxy_size, HALF_PERIOD / 2, chroma_r);
|
||||
|
||||
// Both must be doing something: two flat images would agree perfectly and
|
||||
// prove nothing. About 0.0098 of 0.0188, by the derivation above.
|
||||
let untouched = {
|
||||
let mut pass = AdjustPass::new(&ctx);
|
||||
let plain = render(&ctx, &mut pass, &graph_with(0.0, 0.0), &source, (SOURCE, SOURCE));
|
||||
let plain = render(
|
||||
&ctx,
|
||||
&mut pass,
|
||||
&graph_with(0.0, 0.0),
|
||||
&source,
|
||||
(SOURCE, SOURCE),
|
||||
);
|
||||
modulation(&plain, SOURCE, HALF_PERIOD, chroma_r)
|
||||
};
|
||||
assert!(
|
||||
@@ -486,7 +510,13 @@ fn each_amount_costs_only_the_dispatches_it_needs() {
|
||||
|
||||
for (luminance, chroma, expected) in [(60.0, 0.0, 1), (0.0, 60.0, 2), (60.0, 60.0, 3)] {
|
||||
let mut pass = AdjustPass::new(&ctx);
|
||||
render(&ctx, &mut pass, &graph_with(luminance, chroma), &source, (SIZE, SIZE));
|
||||
render(
|
||||
&ctx,
|
||||
&mut pass,
|
||||
&graph_with(luminance, chroma),
|
||||
&source,
|
||||
(SIZE, SIZE),
|
||||
);
|
||||
assert_eq!(
|
||||
pass.detail_dispatches(),
|
||||
expected,
|
||||
@@ -496,7 +526,13 @@ fn each_amount_costs_only_the_dispatches_it_needs() {
|
||||
}
|
||||
|
||||
let mut neutral = AdjustPass::new(&ctx);
|
||||
let a = render(&ctx, &mut neutral, &graph_with(0.0, 0.0), &source, (SIZE, SIZE));
|
||||
let a = render(
|
||||
&ctx,
|
||||
&mut neutral,
|
||||
&graph_with(0.0, 0.0),
|
||||
&source,
|
||||
(SIZE, SIZE),
|
||||
);
|
||||
assert_eq!(neutral.detail_dispatches(), 0);
|
||||
assert_eq!(neutral.detail_allocations(), 0, "nothing was allocated");
|
||||
|
||||
@@ -504,7 +540,13 @@ fn each_amount_costs_only_the_dispatches_it_needs() {
|
||||
// touch the image, and a stage that ran and wrote back the same values
|
||||
// would still have quantised twice.
|
||||
let mut absent = AdjustPass::new(&ctx);
|
||||
let b = render(&ctx, &mut absent, &EditGraph::default_chain(), &source, (SIZE, SIZE));
|
||||
let b = render(
|
||||
&ctx,
|
||||
&mut absent,
|
||||
&EditGraph::default_chain(),
|
||||
&source,
|
||||
(SIZE, SIZE),
|
||||
);
|
||||
assert_eq!(a, b, "a neutral denoiser changed the picture");
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@ fn ctx() -> Option<GpuContext> {
|
||||
/// The centre pixel's red, green and blue, after `graph` has run over flat
|
||||
/// mid-grey.
|
||||
fn rendered(ctx: &GpuContext, graph: &EditGraph) -> (u8, u8, u8) {
|
||||
let data: Vec<u8> = (0..SIZE * SIZE).flat_map(|_| [128, 128, 128, 255]).collect();
|
||||
let data: Vec<u8> = (0..SIZE * SIZE)
|
||||
.flat_map(|_| [128, 128, 128, 255])
|
||||
.collect();
|
||||
let source = DemosaicedImage::from_rgba8(ctx, &data, SIZE, SIZE).expect("upload");
|
||||
|
||||
// Composed the way the display path composes it. A curve that generates
|
||||
@@ -44,11 +46,7 @@ fn rendered(ctx: &GpuContext, graph: &EditGraph) -> (u8, u8, u8) {
|
||||
/// an edit.
|
||||
fn lifted(channel: Channel) -> EditGraph {
|
||||
let mut graph = EditGraph::default_chain();
|
||||
graph.set_param(
|
||||
curve::ID,
|
||||
curve::coordinate(channel, 2, Axis::Y),
|
||||
0.75,
|
||||
);
|
||||
graph.set_param(curve::ID, curve::coordinate(channel, 2, Axis::Y), 0.75);
|
||||
graph
|
||||
}
|
||||
|
||||
@@ -62,7 +60,10 @@ fn the_master_curve_lifts_every_component_together() {
|
||||
let (r0, g0, b0) = rendered(&ctx, &EditGraph::default_chain());
|
||||
let (r, g, b) = rendered(&ctx, &lifted(Channel::Master));
|
||||
|
||||
assert!(r > r0, "the master curve did not lift the image: {r} vs {r0}");
|
||||
assert!(
|
||||
r > r0,
|
||||
"the master curve did not lift the image: {r} vs {r0}"
|
||||
);
|
||||
// Grey in, grey out: the master curve is applied as a ratio over
|
||||
// luminance, so it changes tone and not hue. A tolerance of one code
|
||||
// value, because the components travel through the ratio separately and
|
||||
|
||||
Reference in New Issue
Block a user