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:
2026-08-22 21:16:34 +02:00
co-authored by Claude Opus 5
parent d04087af83
commit c75849040c
45 changed files with 545 additions and 257 deletions
+4 -15
View File
@@ -487,7 +487,8 @@ mod tests {
// And the last span is shallower than the one before it, which is
// what a shoulder *is*. Without one the curve clips highlights
// harder than the linear rendering did.
let slope = |i: usize| (curve.ys[i + 1] - curve.ys[i]) / (curve.xs[i + 1] - curve.xs[i]);
let slope =
|i: usize| (curve.ys[i + 1] - curve.ys[i]) / (curve.xs[i + 1] - curve.xs[i]);
assert!(
slope(POINTS - 2) < slope(POINTS - 3),
"{name} has no highlight shoulder"
@@ -502,13 +503,7 @@ mod tests {
// as a dark halo in a gradient, which reads as a rendering fault
// rather than as a bad profile.
assert_eq!(
BaseCurve::from_points(&[
[0.0, 0.0],
[0.25, 0.4],
[0.5, 0.3],
[0.75, 0.8],
[1.0, 1.0]
]),
BaseCurve::from_points(&[[0.0, 0.0], [0.25, 0.4], [0.5, 0.3], [0.75, 0.8], [1.0, 1.0]]),
None
);
}
@@ -540,13 +535,7 @@ mod tests {
// point above 1.0 would put the shoulder outside the range the curve
// is defined over and silently flatten everything below it.
assert_eq!(
BaseCurve::from_points(&[
[0.0, 0.0],
[0.25, 0.3],
[0.5, 1.4],
[0.75, 1.5],
[1.0, 1.6]
]),
BaseCurve::from_points(&[[0.0, 0.0], [0.25, 0.3], [0.5, 1.4], [0.75, 1.5], [1.0, 1.6]]),
None
);
}
+8 -2
View File
@@ -375,7 +375,10 @@ fn rawler_location(gps: &rawler::exif::ExifGPS) -> Option<Location> {
(r.d != 0).then(|| r.n as f64 / r.d as f64)
}
fn degrees(dms: &[rawler::formats::tiff::Rational; 3], reference: Option<&String>) -> Option<f64> {
fn degrees(
dms: &[rawler::formats::tiff::Rational; 3],
reference: Option<&String>,
) -> Option<f64> {
let d = ratio(&dms[0])? + ratio(&dms[1])? / 60.0 + ratio(&dms[2])? / 3600.0;
// South and west are stored as positive magnitudes with a letter.
let negative = matches!(
@@ -534,7 +537,10 @@ pub fn decode(bytes: &[u8]) -> Result<RawImage, DecodeError> {
// both callers ask the same object for it.
let profile = profile::CameraProfile::extract(&image, &dng);
let color_matrix = profile.as_ref().and_then(|p| p.cam_to_srgb());
let wb_coeffs = sane_wb(image.wb_coeffs, profile.as_ref().map(|p| p.xyz_to_cam()).as_ref());
let wb_coeffs = sane_wb(
image.wb_coeffs,
profile.as_ref().map(|p| p.xyz_to_cam()).as_ref(),
);
// The rendering half of the profile (FR-DEV-3e). rawler's cleaned strings
// are preferred where it has them — they are what the shipped database is
+5 -2
View File
@@ -678,7 +678,8 @@ fn read_gps_entries(r: &TiffReader, entries: &[Entry]) -> Option<dr_types::Locat
// still writes the entry.
let degrees = |tag: u16, ref_tag: u16| -> Option<f64> {
let e = find(tag)?;
let d = r.rational(e, 0)? + r.rational(e, 1).unwrap_or(0.0) / 60.0
let d = r.rational(e, 0)?
+ r.rational(e, 1).unwrap_or(0.0) / 60.0
+ r.rational(e, 2).unwrap_or(0.0) / 3600.0;
// The magnitude is unsigned; the hemisphere is a letter beside it.
let south_or_west = find(ref_tag)
@@ -1412,7 +1413,9 @@ pub fn defects(tiff_data: &[u8]) -> Defects {
/// skip one this build does not implement rather than abandoning the list —
/// and lists mixing a warp with a defect map are ordinary.
fn read_opcode_list(bytes: &[u8], out: &mut Defects) {
let Some(count) = be_u32(bytes, 0) else { return };
let Some(count) = be_u32(bytes, 0) else {
return;
};
// A sensor has a handful of opcodes, not thousands. A huge count is a
// corrupt or hostile file.
if count > 256 {
+5 -1
View File
@@ -1048,7 +1048,11 @@ mod tests {
// would render black, which is much harder to diagnose than
// uncalibrated.
assert!(!usable(&[[0.0; 3]; 3]));
assert!(!usable(&[[f32::NAN, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]));
assert!(!usable(&[
[f32::NAN, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0]
]));
assert!(usable(&SIX_D_D65));
}
+17 -8
View File
@@ -77,9 +77,7 @@ pub fn encode(
.map(|m| m.sanitised(settings.strip_location));
// JPEG and PNG take a finished block; TIFF writes the tags into its own
// directory and needs the fields.
let block = carried
.as_ref()
.and_then(|m| exif::block(m, width, height));
let block = carried.as_ref().and_then(|m| exif::block(m, width, height));
match settings.format {
ExportFormat::Jpeg => jpeg(
@@ -303,8 +301,11 @@ where
W: std::io::Write + std::io::Seek,
K: tiff::encoder::TiffKind,
{
dir.write_tag(tiff::tags::Tag::Unknown(TAG_ICC_PROFILE), Undefined(profile))
.map_err(|e| ExportError::Encode(e.to_string()))
dir.write_tag(
tiff::tags::Tag::Unknown(TAG_ICC_PROFILE),
Undefined(profile),
)
.map_err(|e| ExportError::Encode(e.to_string()))
}
/// TRACES: FR-EXP-8
@@ -376,7 +377,10 @@ where
)?;
}
if let Some(f) = md.aperture.filter(|f| *f > 0.0) {
dir.write_tag(Tag::Unknown(exif::tag::FNUMBER), Rationals(&[exif::tenths(f)]))?;
dir.write_tag(
Tag::Unknown(exif::tag::FNUMBER),
Rationals(&[exif::tenths(f)]),
)?;
}
if let Some(f) = md.focal_length.filter(|f| *f > 0.0) {
dir.write_tag(
@@ -935,11 +939,16 @@ mod tests {
assert_eq!(md.aperture, Some(2.8), "{format:?}");
assert_eq!(md.focal_length, Some(85.0), "{format:?}");
let loc = md.location.unwrap_or_else(|| panic!("{format:?} lost the fix"));
let loc = md
.location
.unwrap_or_else(|| panic!("{format:?} lost the fix"));
// Within a metre of where it started, which is finer than any
// consumer receiver and far finer than the tag's own rounding.
assert!((loc.latitude - LATITUDE).abs() < 1e-5, "{format:?} {loc:?}");
assert!((loc.longitude - LONGITUDE).abs() < 1e-5, "{format:?} {loc:?}");
assert!(
(loc.longitude - LONGITUDE).abs() < 1e-5,
"{format:?} {loc:?}"
);
assert_eq!(loc.altitude, Some(35.0), "{format:?}");
}
}
+21 -3
View File
@@ -250,7 +250,13 @@ mod tests {
#[test]
fn png_export_produces_a_png() {
let out = export(&frame(32, 32), &settings(ExportFormat::Png), "a.png".into(), None).unwrap();
let out = export(
&frame(32, 32),
&settings(ExportFormat::Png),
"a.png".into(),
None,
)
.unwrap();
assert_eq!(&out.bytes[..8], b"\x89PNG\r\n\x1a\n");
}
@@ -270,8 +276,20 @@ mod tests {
fn a_sixteen_bit_tiff_is_larger_than_an_eight_bit_one() {
// Both are uncompressed RGB; the only difference is the sample width,
// so this is what proves the 16-bit path is not quietly writing 8.
let eight = export(&frame(16, 16), &settings(ExportFormat::Tiff8), "a".into(), None).unwrap();
let sixteen = export(&frame(16, 16), &settings(ExportFormat::Tiff16), "a".into(), None).unwrap();
let eight = export(
&frame(16, 16),
&settings(ExportFormat::Tiff8),
"a".into(),
None,
)
.unwrap();
let sixteen = export(
&frame(16, 16),
&settings(ExportFormat::Tiff16),
"a".into(),
None,
)
.unwrap();
assert!(sixteen.bytes.len() > eight.bytes.len());
}
+45 -8
View File
@@ -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);
}
+1 -1
View File
@@ -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
+8 -8
View File
@@ -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
+1 -2
View File
@@ -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());
+1 -1
View File
@@ -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
View File
@@ -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, &params, field, &selected, subject);
self.draw(
&mut encoder,
slot as u32,
&params,
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 \
+1 -3
View File
@@ -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]
+5 -7
View File
@@ -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]
+10 -5
View File
@@ -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.
+20 -20
View File
@@ -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"
+15 -3
View File
@@ -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] {
+3 -1
View File
@@ -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")
}
+59 -17
View File
@@ -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");
}
+8 -7
View File
@@ -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
+4 -1
View File
@@ -1532,7 +1532,10 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> {
.iter()
.map(|a| {
let mut c = a.chars();
let head = c.next().expect("attribute names are non-empty").to_uppercase();
let head = c
.next()
.expect("attribute names are non-empty")
.to_uppercase();
format!("Attribute::{head}{}", c.as_str())
})
.collect();
+1 -5
View File
@@ -499,11 +499,7 @@ pub fn compose_detail(
// case where a photographer has a kernel switched on at a scale that
// cannot draw it — against the alternative of the preview failing outright
// or `compose_full` growing a resolution argument it has no other use for.
if planned.is_empty()
&& ops
.iter()
.any(|o| o.is_active() && o.detail().is_some())
{
if planned.is_empty() && ops.iter().any(|o| o.is_active() && o.detail().is_some()) {
return ComposedDetail {
passes: vec![compose_one(
RESOLVE_ID,
+10 -4
View File
@@ -8,8 +8,7 @@
//! so reordering the pipeline needs no code change.
use crate::descriptor::{
Attribute,
Facet, LocalizedKey, OpDescriptor, OpId, ParamId, ParamKind, Presentation,
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamId, ParamKind, Presentation,
};
use crate::framing::{CropRect, Framing};
use crate::mask::MaskStack;
@@ -378,7 +377,11 @@ impl EditGraph {
/// Only the detail stage needs this. Every point operation is scale-free
/// — a multiply is a multiply at any resolution — which is why nothing in
/// the pipeline had to know its own size until a kernel arrived.
pub fn render_scale(&self, source: (u32, u32), render: (u32, u32)) -> crate::detail::RenderScale {
pub fn render_scale(
&self,
source: (u32, u32),
render: (u32, u32),
) -> crate::detail::RenderScale {
let (fw, fh) = self.framing.output_size(source.0, source.1);
let view = self.framing.view();
// The *viewed* part of the framed image, at source resolution. Zoom
@@ -397,7 +400,10 @@ impl EditGraph {
/// Empty for every edit with no active neighbourhood operation, which is
/// almost all of them — and in that case [`Self::compose`] emits the
/// single encoded dispatch it always has.
pub fn compose_detail(&self, scale: crate::detail::RenderScale) -> crate::detail::ComposedDetail {
pub fn compose_detail(
&self,
scale: crate::detail::RenderScale,
) -> crate::detail::ComposedDetail {
self.compose_detail_for(scale, dr_types::ColourSpace::Srgb)
}
+2 -2
View File
@@ -44,8 +44,8 @@ pub mod preset;
pub mod sidecar;
pub use descriptor::{
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, ParamKind, Presentation,
Scale, Unit, WidgetDemand, WidgetKind,
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, ParamKind,
Presentation, Scale, Unit, WidgetDemand, WidgetKind,
};
pub use detail::{
compose_detail, ComposedDetail, ComposedDetailPass, DetailPass, DetailStage, RenderScale,
+30 -10
View File
@@ -856,7 +856,11 @@ impl MaskLayer {
pub fn begin_stroke(&mut self, erase: bool, radius: f32, hardness: f32, flow: f32) -> bool {
let full = self.stroke_points() >= MAX_LAYER_POINTS;
let MaskSource::Brush { strokes } = &mut self.source else {
log::warn!("layer {} is a {} mask, not a brush", self.id, self.source.kind());
log::warn!(
"layer {} is a {} mask, not a brush",
self.id,
self.source.kind()
);
return false;
};
if full {
@@ -1121,7 +1125,10 @@ impl MaskStack {
/// Generate a fresh layer id that does not collide with an existing one.
pub fn next_id(&self) -> String {
(1..).map(|n| format!("m{n}")).find(|id| self.get(id).is_none()).expect("infinite range")
(1..)
.map(|n| format!("m{n}"))
.find(|id| self.get(id).is_none())
.expect("infinite range")
}
}
@@ -1178,10 +1185,7 @@ pub(crate) fn compose_layers(stack: &MaskStack) -> LayerShader {
// place. A second copy here would be a second thing to keep in step
// with `Framing::wgsl_prologue`, and the failure would be a mask that
// is subtly wrong only when straightened.
let _ = writeln!(
out.body,
" var m = sample_mask(uv_src, {slot});"
);
let _ = writeln!(out.body, " var m = sample_mask(uv_src, {slot});");
let _ = writeln!(
out.body,
" m = select(m, 1.0 - m, u.{prefix}_invert > 0.5);"
@@ -1403,7 +1407,11 @@ mod tests {
assert!(shader.uniform_fields.contains("mask1_exposure_"));
assert_eq!(
shader.uniform_values.len(),
shader.uniform_fields.lines().filter(|l| l.trim_start().starts_with("mask")).count(),
shader
.uniform_fields
.lines()
.filter(|l| l.trim_start().starts_with("mask"))
.count(),
"one value per emitted field"
);
}
@@ -1448,7 +1456,11 @@ mod tests {
// A gradient has no region ids, so nothing can go stale about it.
let grad = MaskLayer::new(
"m2",
MaskSource::Linear { centre: (0.5, 0.5), angle: 0.0, width: 0.2 },
MaskSource::Linear {
centre: (0.5, 0.5),
angle: 0.0,
width: 0.2,
},
);
assert!(!grad.is_stale(999));
}
@@ -1458,8 +1470,16 @@ mod tests {
let base = segmentation_signature(1600, 1067, 6730, 2);
assert_eq!(base, segmentation_signature(1600, 1067, 6730, 2));
assert_ne!(base, segmentation_signature(1600, 1067, 6730, 5), "tuning");
assert_ne!(base, segmentation_signature(800, 1067, 6730, 2), "proxy size");
assert_ne!(base, segmentation_signature(1600, 1067, 42, 2), "region count");
assert_ne!(
base,
segmentation_signature(800, 1067, 6730, 2),
"proxy size"
);
assert_ne!(
base,
segmentation_signature(1600, 1067, 42, 2),
"region count"
);
}
#[test]
+5 -5
View File
@@ -472,10 +472,7 @@ pub fn compose_full(
// than from a flag the caller sets, because a caller that got the flag
// wrong would produce a shader whose storage format does not match the
// texture bound to it.
let output_mode = if ops
.iter()
.any(|o| o.is_active() && o.detail().is_some())
{
let output_mode = if ops.iter().any(|o| o.is_active() && o.detail().is_some()) {
OutputMode::LinearWorking
} else {
OutputMode::Encoded
@@ -1389,7 +1386,10 @@ mod tests {
// how a shoulder rolls off.
let mut curve = crate::ops::ToneCurve::new();
curve.set_param(crate::ops::curve::P2_Y, 0.7);
assert!(curve.is_active(), "the fixture must actually reach the shader");
assert!(
curve.is_active(),
"the fixture must actually reach the shader"
);
let source = compose(&[Box::new(curve)]).source;
assert_eq!(
+3 -1
View File
@@ -32,7 +32,9 @@
//! least to perceived sharpness. Scaling all three about a virtual reference
//! would soften the image even when the correction is right.
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit};
use crate::descriptor::{
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
};
use crate::lens::Warp;
use crate::operation::{Helper, Uniform};
+42 -8
View File
@@ -764,9 +764,11 @@ mod tests {
};
assert_eq!(gate_of(0.0), 0.0);
assert!((gate_of(1.0) - 0.25).abs() < 1e-6);
assert!(chain_at(&sharpening(50.0, 1.0), RenderScale::full((512, 512))).passes[0]
.source
.contains("gate > 0.0"));
assert!(
chain_at(&sharpening(50.0, 1.0), RenderScale::full((512, 512))).passes[0]
.source
.contains("gate > 0.0")
);
}
#[test]
@@ -783,7 +785,11 @@ mod tests {
for scale in scales {
for pass in chain_at(&sharpening(75.0, 1.0), scale).passes {
assert_eq!(pass.uniforms.len() % 4, 0, "{}", pass.label);
assert!(pass.uniforms.iter().all(|v| v.is_finite()), "{}", pass.label);
assert!(
pass.uniforms.iter().all(|v| v.is_finite()),
"{}",
pass.label
);
}
}
}
@@ -805,10 +811,38 @@ mod tests {
// word be safe in one stage and not in the other, which is the sort of
// difference nobody discovers until a shader fails to compile.
const RESERVED: &[&str] = &[
"target", "sample", "filter", "texture", "buffer", "binding", "const", "enum", "mat",
"vec", "ptr", "ref", "shared", "static", "typedef", "union", "unless", "handle",
"layout", "packed", "premerge", "regardless", "active", "do", "input", "output",
"private", "resource", "restrict", "self", "std", "where",
"target",
"sample",
"filter",
"texture",
"buffer",
"binding",
"const",
"enum",
"mat",
"vec",
"ptr",
"ref",
"shared",
"static",
"typedef",
"union",
"unless",
"handle",
"layout",
"packed",
"premerge",
"regardless",
"active",
"do",
"input",
"output",
"private",
"resource",
"restrict",
"self",
"std",
"where",
];
for pass in chain_at(&sharpening(100.0, 2.0), RenderScale::full((512, 512))).passes {
+3 -1
View File
@@ -30,7 +30,9 @@
//! adjusted, each one's share depends on what the other is set to, so turning
//! up one colour's saturation quietly weakened its neighbour's hue shift.
use crate::descriptor::{Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
use crate::descriptor::{
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId,
};
use crate::operation::{Helper, Operation, Uniform};
use crate::ops::helpers;
+8 -8
View File
@@ -121,12 +121,7 @@ impl Channel {
/// Master first because it runs first — see the module documentation for
/// why that is the composition order — and because a list showing the
/// grade before the tone would be describing a different operation.
pub const ALL: [Channel; 4] = [
Channel::Master,
Channel::Red,
Channel::Green,
Channel::Blue,
];
pub const ALL: [Channel; 4] = [Channel::Master, Channel::Red, Channel::Green, Channel::Blue];
/// Position in [`Self::ALL`], and so in every array keyed by channel.
pub const fn index(self) -> usize {
@@ -771,7 +766,10 @@ impl Operation for ToneCurve {
// declared from, so a call and its uniform block cannot disagree
// about a name.
let args = UNIFORM_NAMES[channel.index()].join(", ");
let _ = writeln!(body, "c.{component} = channel_curve(c.{component}, {args});");
let _ = writeln!(
body,
"c.{component} = channel_curve(c.{component}, {args});"
);
}
// Whichever curves ran, the result has to be a colour: the spline's
@@ -1345,7 +1343,9 @@ mod tests {
let body = c.wgsl_body();
let master = body.find("apply_tone_gain").expect("the master curve runs");
let red = body.find("c.r = channel_curve").expect("the red curve runs");
let red = body
.find("c.r = channel_curve")
.expect("the red curve runs");
assert!(master < red, "the master curve must run first:\n{body}");
}
+3 -1
View File
@@ -26,7 +26,9 @@
//! profile is a "make the horizon straight" task, which one term does well.
//! The full triple is reachable by loading a profile.
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit};
use crate::descriptor::{
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
};
use crate::lens::Warp;
use crate::operation::{Helper, Uniform};
+15 -4
View File
@@ -619,7 +619,10 @@ mod tests {
let scale = RenderScale::full((2000, 1500));
let only_clarity = composed(50.0, 0.0, scale);
assert_eq!(only_clarity.len(), 2, "one operation, two passes");
assert!(only_clarity.passes.iter().all(|p| p.label.starts_with("clarity/")));
assert!(only_clarity
.passes
.iter()
.all(|p| p.label.starts_with("clarity/")));
let both = composed(50.0, 50.0, scale);
assert_eq!(both.len(), 4);
@@ -701,7 +704,10 @@ mod tests {
// acutance family's unit.
let proxy = RenderScale::new((2000, 1500), (6000, 4500));
let clarity = Clarity::with_amount(60.0);
assert_eq!(clarity.kernel(proxy), clarity.kernel(RenderScale::full((2000, 1500))));
assert_eq!(
clarity.kernel(proxy),
clarity.kernel(RenderScale::full((2000, 1500)))
);
assert!(
proxy.source_pixels(96.0) < 40.0,
"the same length in the other unit would have collapsed"
@@ -786,7 +792,9 @@ mod tests {
// slider can produce is `gain * threshold` stops however violent the
// edge — a bound that holds by construction rather than by tuning.
let combine = &Clarity::with_amount(100.0).passes(RenderScale::full((2000, 1500)))[1];
assert!(combine.wgsl.contains("threshold * tanh(detail / threshold)"));
assert!(combine
.wgsl
.contains("threshold * tanh(detail / threshold)"));
let bound = |u: &[Uniform]| {
let get = |n| u.iter().find(|x| x.name == n).unwrap().value;
@@ -819,7 +827,10 @@ mod tests {
// And texture does not carry the helpers it would need for one, so its
// shader says what it does rather than merely not calling it.
assert!(Texture::new().helpers().iter().any(|h| h.name == "log_luma"));
assert!(Texture::new()
.helpers()
.iter()
.any(|h| h.name == "log_luma"));
assert!(!Texture::new()
.helpers()
.iter()
+7 -1
View File
@@ -820,7 +820,13 @@ mod tests {
// Everything else about the two must match, or one axis is filtered
// harder than the other and a round blotch comes out oval.
assert_eq!(passes[0].radius, passes[1].radius);
for name in ["radius", "inv_spatial", "guide_k", "chroma_k", "noise_floor"] {
for name in [
"radius",
"inv_spatial",
"guide_k",
"chroma_k",
"noise_floor",
] {
let of = |p: &DetailPass| {
p.uniforms
.iter()
+6 -5
View File
@@ -67,9 +67,7 @@ use std::fmt;
use std::fmt::Write as _;
use crate::graph::EditGraph;
use crate::mask::{
Falloff, MaskLayer, MaskSource, MaskStack, Morphology, Stroke, DEFAULT_FEATHER,
};
use crate::mask::{Falloff, MaskLayer, MaskSource, MaskStack, Morphology, Stroke, DEFAULT_FEATHER};
use crate::preset::{resolve, Preset};
/// Format version of the document itself.
@@ -662,7 +660,6 @@ impl Sidecar {
}
}
/// Write one mask layer as its own block.
///
/// The version uuid is repeated in the header rather than relying on the
@@ -1292,7 +1289,11 @@ mod tests {
// else.
assert_eq!(g.param(curve::ID, curve::P1_Y), Some(0.15));
assert_eq!(g.param(curve::ID, curve::P3_Y), Some(0.85));
for channel in [curve::Channel::Red, curve::Channel::Green, curve::Channel::Blue] {
for channel in [
curve::Channel::Red,
curve::Channel::Green,
curve::Channel::Blue,
] {
for point in 0..curve::POINTS {
for axis in [curve::Axis::X, curve::Axis::Y] {
let id = curve::coordinate(channel, point, axis);
+4 -1
View File
@@ -40,7 +40,10 @@ fn the_groups_are_derivable_from_the_chain() {
present.sort();
present.dedup();
assert!(present.contains(&Attribute::Tone), "the chain has tonal work");
assert!(
present.contains(&Attribute::Tone),
"the chain has tonal work"
);
assert!(present.contains(&Attribute::Colour));
assert!(
present.contains(&Attribute::Geometry),
+23 -4
View File
@@ -55,7 +55,14 @@ fn a_region_mask_survives_a_round_trip() {
assert_eq!(layer.id, "m1");
assert_eq!(layer.name, "Subject");
assert_eq!(layer.source, regions(&[3, 7, 12]));
assert_eq!(layer.ops.iter().find(|o| o.descriptor().id.0 == "exposure").map(|o| o.param(ParamId("exposure"))), Some(0.75));
assert_eq!(
layer
.ops
.iter()
.find(|o| o.descriptor().id.0 == "exposure")
.map(|o| o.param(ParamId("exposure"))),
Some(0.75)
);
}
#[test]
@@ -120,7 +127,10 @@ fn flags_and_opacity_survive() {
let layer = &restored.masks().layers()[0];
assert!(layer.invert);
assert!((layer.opacity - 0.35).abs() < 1e-6);
assert!(!layer.enabled, "a disabled layer must stay disabled, not vanish");
assert!(
!layer.enabled,
"a disabled layer must stay disabled, not vanish"
);
}
/// A selection's identity is the *set*. Two files naming the same regions in
@@ -272,7 +282,10 @@ fn unknown_top_level_keys_still_round_trip_alongside_masks() {
exposure.exposure = 1\n";
let out = Sidecar::parse(text).expect("parse").to_text();
assert!(out.contains("future.thing"), "unknown keys are still preserved");
assert!(
out.contains("future.thing"),
"unknown keys are still preserved"
);
assert!(out.contains("[mask default m1]"));
}
@@ -361,7 +374,13 @@ fn a_brush_layer_writes_one_line_per_stroke() {
.lines()
.filter(|l| l.starts_with("stroke = "))
.collect();
assert_eq!(lines, ["stroke = add 0.05 0.5 1 0.2,0.5 0.8,0.5", "stroke = erase 0.05 0.5 1 0.5,0.5"]);
assert_eq!(
lines,
[
"stroke = add 0.05 0.5 1 0.2,0.5 0.8,0.5",
"stroke = erase 0.05 0.5 1 0.5,0.5"
]
);
}
#[test]
+2 -2
View File
@@ -12,7 +12,8 @@ use dr_pipeline::{EditGraph, Sidecar};
/// A version block carrying `params`, in the on-disk spelling.
fn sidecar_with(params: &[(&str, f32)]) -> String {
let mut text = String::from("drsc 1\n\n[version u1]\nname = Default\nrevision = 2\nmodified = 0\n");
let mut text =
String::from("drsc 1\n\n[version u1]\nname = Default\nrevision = 2\nmodified = 0\n");
for (key, value) in params {
text.push_str(&format!("{key} = {value}\n"));
}
@@ -199,4 +200,3 @@ fn every_point_of_every_curve_round_trips_through_a_sidecar() {
}
}
}
+7 -9
View File
@@ -46,7 +46,11 @@ fn main() {
};
println!(
"tiling {}",
if tiled { "grid, 25% overlap" } else { "whole frame" }
if tiled {
"grid, 25% overlap"
} else {
"whole frame"
}
);
let t0 = std::time::Instant::now();
@@ -57,10 +61,7 @@ fn main() {
let instances = model
.detect(&rgb, width, height, &options)
.expect("inference");
println!(
"detect {:.0} ms",
t1.elapsed().as_secs_f32() * 1000.0
);
println!("detect {:.0} ms", t1.elapsed().as_secs_f32() * 1000.0);
println!("found {} instances", instances.len());
for (i, inst) in instances.iter().enumerate() {
@@ -131,10 +132,7 @@ fn load_jpeg(path: &str) -> (Vec<f32>, usize, usize) {
// where *not* linearising is the correct thing to do.
let rgb = match pixels.len() / (w * h) {
3 => pixels.iter().map(|&v| v as f32 / 255.0).collect(),
1 => pixels
.iter()
.flat_map(|&v| [v as f32 / 255.0; 3])
.collect(),
1 => pixels.iter().flat_map(|&v| [v as f32 / 255.0; 3]).collect(),
n => panic!("unexpected {n} components per pixel"),
};
+11 -4
View File
@@ -346,8 +346,8 @@ impl Shaped {
radius_px: f32,
) -> Self {
let distance = signed_distance(coverage, width, height, threshold);
let distance = apply_morphology(&distance, width, height, morphology, radius_px)
.unwrap_or(distance);
let distance =
apply_morphology(&distance, width, height, morphology, radius_px).unwrap_or(distance);
Self {
distance,
width,
@@ -403,7 +403,11 @@ mod tests {
// Distance from the boundary, so the half-pixel comes back off to
// compare against the centre-to-centre figures.
let at = |x: usize, y: usize| -d[y * 5 + x] + 0.5;
assert!((at(1, 1) - std::f32::consts::SQRT_2).abs() < 1e-4, "{}", at(1, 1));
assert!(
(at(1, 1) - std::f32::consts::SQRT_2).abs() < 1e-4,
"{}",
at(1, 1)
);
assert!((at(0, 0) - (8.0f32).sqrt()).abs() < 1e-4, "{}", at(0, 0));
assert_eq!(at(2, 0), 2.0, "straight up is exactly two");
}
@@ -489,7 +493,10 @@ mod tests {
m[10 * w + 10] = 0;
let before = signed_distance(&m, w, h, 128);
assert!(before[10 * w + 10] < 0.0, "the hole starts outside the mask");
assert!(
before[10 * w + 10] < 0.0,
"the hole starts outside the mask"
);
let after = apply_morphology(&before, w, h, Morphology::Close, 2.0).expect("recomputed");
assert!(after[10 * w + 10] >= 0.0, "closing should have filled it");
+37 -18
View File
@@ -142,7 +142,11 @@ impl Membership {
/// The instance a region belongs to, if any.
pub fn instance_of(&self, region: u32) -> Option<usize> {
self.of.get(region as usize).copied().flatten().map(|(i, _)| i)
self.of
.get(region as usize)
.copied()
.flatten()
.map(|(i, _)| i)
}
/// How two regions relate semantically, in `-1.0..=1.0`.
@@ -233,11 +237,7 @@ pub fn apply_semantic_prior(
///
/// The returned ids are sorted, so the same click always produces the same
/// mask — which is what lets it be a cache key.
pub fn regions_for_instance(
field: &RegionField,
mask: &[f32],
options: &PriorOptions,
) -> Vec<u32> {
pub fn regions_for_instance(field: &RegionField, mask: &[f32], options: &PriorOptions) -> Vec<u32> {
let mut coverage = vec![0.0f32; field.region_count];
let mut sizes = vec![0u32; field.region_count];
@@ -278,10 +278,26 @@ mod tests {
labels: vec![0, 0, 2, 2, 1, 1, 3, 3],
region_count: 4,
adjacency: vec![
Edge { a: 0, b: 1, saddle: 1.0 },
Edge { a: 0, b: 2, saddle: 1.0 },
Edge { a: 1, b: 3, saddle: 1.0 },
Edge { a: 2, b: 3, saddle: 1.0 },
Edge {
a: 0,
b: 1,
saddle: 1.0,
},
Edge {
a: 0,
b: 2,
saddle: 1.0,
},
Edge {
a: 1,
b: 3,
saddle: 1.0,
},
Edge {
a: 2,
b: 3,
saddle: 1.0,
},
],
}
}
@@ -361,7 +377,10 @@ mod tests {
#[test]
fn zero_strength_is_arm_a_exactly() {
let f = field();
let opts = PriorOptions { strength: 0.0, ..PriorOptions::default() };
let opts = PriorOptions {
strength: 0.0,
..PriorOptions::default()
};
let m = Membership::compute(&f, &[&left_half()], &opts).unwrap();
assert_eq!(apply_semantic_prior(&f, &m, &opts).adjacency, f.adjacency);
}
@@ -394,7 +413,11 @@ mod tests {
let err = Membership::compute(&f, &[&vec![0.0; 3]], &PriorOptions::default()).unwrap_err();
assert_eq!(
err,
MembershipError::MaskSize { instance: 0, expected: 8, got: 3 }
MembershipError::MaskSize {
instance: 0,
expected: 8,
got: 3
}
);
}
@@ -403,12 +426,8 @@ mod tests {
fn different_instances_repel() {
let f = field();
let right_half = vec![0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0];
let m = Membership::compute(
&f,
&[&left_half(), &right_half],
&PriorOptions::default(),
)
.unwrap();
let m = Membership::compute(&f, &[&left_half(), &right_half], &PriorOptions::default())
.unwrap();
assert_eq!(m.instance_of(0), Some(0));
assert_eq!(m.instance_of(2), Some(1));
+11 -3
View File
@@ -357,7 +357,6 @@ impl SemanticModel {
options,
))
}
}
/// Turn one forward pass into instances in source-image space.
@@ -543,7 +542,11 @@ fn assemble_mask(
let (gx1, gy1) = ((gx0 + 1).min(pw - 1), (gy0 + 1).min(ph - 1));
let mut acc = 0.0;
for (k, &c) in coeffs.iter().enumerate().take(PROTOTYPES.min(protos.shape()[0])) {
for (k, &c) in coeffs
.iter()
.enumerate()
.take(PROTOTYPES.min(protos.shape()[0]))
{
if c == 0.0 {
continue;
}
@@ -643,7 +646,12 @@ mod tests {
#[test]
fn letterbox_round_trips_a_landscape_window() {
let window = Window { x: 0.0, y: 0.0, w: 1600.0, h: 1067.0 };
let window = Window {
x: 0.0,
y: 0.0,
w: 1600.0,
h: 1067.0,
};
let lb = Letterbox::fit(window.w, window.h);
// A source point maps into the input and back to where it started.
+4 -2
View File
@@ -548,8 +548,10 @@ pub fn cancel_press(window: &AppWindow, ctl: &Rc<CollectionsController>, ids: &[
let Some(undo) = ctl.press_undo.borrow_mut().take() else {
return;
};
let (previous_anchor, cursor) =
undo.restore(&mut ctl.selection.borrow_mut(), &mut ctl.anchor.borrow_mut());
let (previous_anchor, cursor) = undo.restore(
&mut ctl.selection.borrow_mut(),
&mut ctl.anchor.borrow_mut(),
);
ctl.previous_anchor.set(previous_anchor);
ctl.set_cursor(cursor);
sync_selection(window, ctl, ids);
+35 -19
View File
@@ -206,7 +206,13 @@ impl SegmentationJob {
// few places in this codebase where not linearising is correct.
let rgb = rgba
.chunks_exact(4)
.flat_map(|p| [p[0] as f32 / 255.0, p[1] as f32 / 255.0, p[2] as f32 / 255.0])
.flat_map(|p| {
[
p[0] as f32 / 255.0,
p[1] as f32 / 255.0,
p[2] as f32 / 255.0,
]
})
.collect();
Ok((rgb, pw as usize, ph as usize))
}
@@ -1458,10 +1464,7 @@ impl DevelopSession {
/// What kind of mask a layer is — "regions", "linear", "radial".
pub fn mask_kind(&self, id: &str) -> &'static str {
self.graph
.masks()
.get(id)
.map_or("", |l| l.source.kind())
self.graph.masks().get(id).map_or("", |l| l.source.kind())
}
pub fn mask_inverted(&self, id: &str) -> bool {
@@ -1625,7 +1628,11 @@ impl DevelopSession {
width: 0.3,
}
};
if !self.graph.masks_mut().push(MaskLayer::new(id.clone(), source)) {
if !self
.graph
.masks_mut()
.push(MaskLayer::new(id.clone(), source))
{
return None;
}
self.active_mask = Some(id.clone());
@@ -1660,7 +1667,8 @@ impl DevelopSession {
pub fn set_mask_feather(&mut self, id: &str, feather: f32) {
if let Some(layer) = self.graph.masks_mut().get_mut(id) {
layer.feather = feather.clamp(0.0, 1.0);
self.history.record(&self.graph, Edit::Op(OpId("mask-feather")));
self.history
.record(&self.graph, Edit::Op(OpId("mask-feather")));
}
}
@@ -1694,7 +1702,8 @@ impl DevelopSession {
pub fn set_mask_morph_radius(&mut self, id: &str, radius: f32) {
if let Some(layer) = self.graph.masks_mut().get_mut(id) {
layer.morph_radius = radius.clamp(0.0, 1.0);
self.history.record(&self.graph, Edit::Op(OpId("mask-morph")));
self.history
.record(&self.graph, Edit::Op(OpId("mask-morph")));
}
}
@@ -1702,7 +1711,10 @@ impl DevelopSession {
pub fn mask_falloff(&self, id: &str) -> usize {
use dr_pipeline::mask::Falloff;
self.graph.masks().get(id).map_or(0, |l| {
Falloff::ALL.iter().position(|&f| f == l.falloff).unwrap_or(0)
Falloff::ALL
.iter()
.position(|&f| f == l.falloff)
.unwrap_or(0)
})
}
@@ -1732,7 +1744,10 @@ impl DevelopSession {
pub fn mask_is_shapeable(&self, id: &str) -> bool {
use dr_pipeline::mask::MaskSource;
self.graph.masks().get(id).is_some_and(|l| {
matches!(l.source, MaskSource::Subject { .. } | MaskSource::Regions { .. })
matches!(
l.source,
MaskSource::Subject { .. } | MaskSource::Regions { .. }
)
})
}
@@ -1742,7 +1757,8 @@ impl DevelopSession {
// `Op` rather than `Discrete`: opacity is dragged, and a drag is
// one decision however many values it passes through. `Discrete`
// would put every intermediate position on the undo stack.
self.history.record(&self.graph, Edit::Op(OpId("mask-opacity")));
self.history
.record(&self.graph, Edit::Op(OpId("mask-opacity")));
}
}
@@ -2510,9 +2526,8 @@ mod tests {
fn a_job_carries_the_session_it_was_taken_from() {
let Some(ctx) = headless() else { return };
let rgba: Vec<u8> = (0..16 * 16).flat_map(|_| [128, 128, 128, 255]).collect();
let session =
DevelopSession::open_rgb(&ctx, &rgba, 16, 16, dr_types::Orientation::NORMAL)
.expect("session");
let session = DevelopSession::open_rgb(&ctx, &rgba, 16, 16, dr_types::Orientation::NORMAL)
.expect("session");
assert_eq!(session.segmentation_job().session(), session.id());
}
@@ -2523,9 +2538,8 @@ mod tests {
fn an_abandoned_job_does_no_work() {
let Some(ctx) = headless() else { return };
let rgba: Vec<u8> = (0..16 * 16).flat_map(|_| [128, 128, 128, 255]).collect();
let session =
DevelopSession::open_rgb(&ctx, &rgba, 16, 16, dr_types::Orientation::NORMAL)
.expect("session");
let session = DevelopSession::open_rgb(&ctx, &rgba, 16, 16, dr_types::Orientation::NORMAL)
.expect("session");
let job = session.segmentation_job();
job.abandon().now();
@@ -3993,7 +4007,10 @@ mod tests {
let narrowed = session.rows().len();
assert!(narrowed > 0, "a tab must show something");
assert!(narrowed < all, "and less than everything: {narrowed} of {all}");
assert!(
narrowed < all,
"and less than everything: {narrowed} of {all}"
);
}
/// The trap: `op_index` counts over *every* capability, so a row that
@@ -4054,7 +4071,6 @@ mod tests {
session.set_active_tab(99);
assert_eq!(session.rows().len(), all);
}
}
/// `dr_pipeline`'s morphology, as `dr_segment` names it.
-1
View File
@@ -844,7 +844,6 @@ mod tests {
);
}
/// The ordinary case: the answer comes back to the photograph that asked.
#[test]
fn a_result_for_the_open_photograph_is_applied() {
+2 -2
View File
@@ -318,8 +318,8 @@ fn refresh_thumbnail_for(
session: &Rc<RefCell<Option<DevelopSession>>>,
library: &Rc<library_ui::LibraryController>,
) {
let worth_it = window.get_can_undo()
|| session.borrow().as_ref().is_some_and(|s| !s.is_neutral());
let worth_it =
window.get_can_undo() || session.borrow().as_ref().is_some_and(|s| !s.is_neutral());
if !worth_it {
return;
}