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 // And the last span is shallower than the one before it, which is
// what a shoulder *is*. Without one the curve clips highlights // what a shoulder *is*. Without one the curve clips highlights
// harder than the linear rendering did. // 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!( assert!(
slope(POINTS - 2) < slope(POINTS - 3), slope(POINTS - 2) < slope(POINTS - 3),
"{name} has no highlight shoulder" "{name} has no highlight shoulder"
@@ -502,13 +503,7 @@ mod tests {
// as a dark halo in a gradient, which reads as a rendering fault // as a dark halo in a gradient, which reads as a rendering fault
// rather than as a bad profile. // rather than as a bad profile.
assert_eq!( assert_eq!(
BaseCurve::from_points(&[ BaseCurve::from_points(&[[0.0, 0.0], [0.25, 0.4], [0.5, 0.3], [0.75, 0.8], [1.0, 1.0]]),
[0.0, 0.0],
[0.25, 0.4],
[0.5, 0.3],
[0.75, 0.8],
[1.0, 1.0]
]),
None None
); );
} }
@@ -540,13 +535,7 @@ mod tests {
// point above 1.0 would put the shoulder outside the range the curve // point above 1.0 would put the shoulder outside the range the curve
// is defined over and silently flatten everything below it. // is defined over and silently flatten everything below it.
assert_eq!( assert_eq!(
BaseCurve::from_points(&[ BaseCurve::from_points(&[[0.0, 0.0], [0.25, 0.3], [0.5, 1.4], [0.75, 1.5], [1.0, 1.6]]),
[0.0, 0.0],
[0.25, 0.3],
[0.5, 1.4],
[0.75, 1.5],
[1.0, 1.6]
]),
None 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) (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; 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. // South and west are stored as positive magnitudes with a letter.
let negative = matches!( let negative = matches!(
@@ -534,7 +537,10 @@ pub fn decode(bytes: &[u8]) -> Result<RawImage, DecodeError> {
// both callers ask the same object for it. // both callers ask the same object for it.
let profile = profile::CameraProfile::extract(&image, &dng); let profile = profile::CameraProfile::extract(&image, &dng);
let color_matrix = profile.as_ref().and_then(|p| p.cam_to_srgb()); 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 // 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 // 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. // still writes the entry.
let degrees = |tag: u16, ref_tag: u16| -> Option<f64> { let degrees = |tag: u16, ref_tag: u16| -> Option<f64> {
let e = find(tag)?; 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; + r.rational(e, 2).unwrap_or(0.0) / 3600.0;
// The magnitude is unsigned; the hemisphere is a letter beside it. // The magnitude is unsigned; the hemisphere is a letter beside it.
let south_or_west = find(ref_tag) 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 — /// skip one this build does not implement rather than abandoning the list —
/// and lists mixing a warp with a defect map are ordinary. /// and lists mixing a warp with a defect map are ordinary.
fn read_opcode_list(bytes: &[u8], out: &mut Defects) { 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 // A sensor has a handful of opcodes, not thousands. A huge count is a
// corrupt or hostile file. // corrupt or hostile file.
if count > 256 { if count > 256 {
+5 -1
View File
@@ -1048,7 +1048,11 @@ mod tests {
// would render black, which is much harder to diagnose than // would render black, which is much harder to diagnose than
// uncalibrated. // uncalibrated.
assert!(!usable(&[[0.0; 3]; 3])); 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)); assert!(usable(&SIX_D_D65));
} }
+17 -8
View File
@@ -77,9 +77,7 @@ pub fn encode(
.map(|m| m.sanitised(settings.strip_location)); .map(|m| m.sanitised(settings.strip_location));
// JPEG and PNG take a finished block; TIFF writes the tags into its own // JPEG and PNG take a finished block; TIFF writes the tags into its own
// directory and needs the fields. // directory and needs the fields.
let block = carried let block = carried.as_ref().and_then(|m| exif::block(m, width, height));
.as_ref()
.and_then(|m| exif::block(m, width, height));
match settings.format { match settings.format {
ExportFormat::Jpeg => jpeg( ExportFormat::Jpeg => jpeg(
@@ -303,8 +301,11 @@ where
W: std::io::Write + std::io::Seek, W: std::io::Write + std::io::Seek,
K: tiff::encoder::TiffKind, K: tiff::encoder::TiffKind,
{ {
dir.write_tag(tiff::tags::Tag::Unknown(TAG_ICC_PROFILE), Undefined(profile)) dir.write_tag(
.map_err(|e| ExportError::Encode(e.to_string())) tiff::tags::Tag::Unknown(TAG_ICC_PROFILE),
Undefined(profile),
)
.map_err(|e| ExportError::Encode(e.to_string()))
} }
/// TRACES: FR-EXP-8 /// TRACES: FR-EXP-8
@@ -376,7 +377,10 @@ where
)?; )?;
} }
if let Some(f) = md.aperture.filter(|f| *f > 0.0) { 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) { if let Some(f) = md.focal_length.filter(|f| *f > 0.0) {
dir.write_tag( dir.write_tag(
@@ -935,11 +939,16 @@ mod tests {
assert_eq!(md.aperture, Some(2.8), "{format:?}"); assert_eq!(md.aperture, Some(2.8), "{format:?}");
assert_eq!(md.focal_length, Some(85.0), "{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 // Within a metre of where it started, which is finer than any
// consumer receiver and far finer than the tag's own rounding. // consumer receiver and far finer than the tag's own rounding.
assert!((loc.latitude - LATITUDE).abs() < 1e-5, "{format:?} {loc:?}"); 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:?}"); assert_eq!(loc.altitude, Some(35.0), "{format:?}");
} }
} }
+21 -3
View File
@@ -250,7 +250,13 @@ mod tests {
#[test] #[test]
fn png_export_produces_a_png() { 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"); 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() { fn a_sixteen_bit_tiff_is_larger_than_an_eight_bit_one() {
// Both are uncompressed RGB; the only difference is the sample width, // 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. // 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 eight = export(
let sixteen = export(&frame(16, 16), &settings(ExportFormat::Tiff16), "a".into(), None).unwrap(); &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()); 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 //! 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. //! here as an adjustment in the wrong place, and nowhere else.
use dr_gpu::{ use dr_gpu::{AdjustPass, DemosaicedImage, Demosaicer, GpuContext, MaskPass, SubjectMasks};
AdjustPass, DemosaicedImage, Demosaicer, GpuContext, MaskPass, SubjectMasks,
};
use dr_pipeline::descriptor::ParamId; use dr_pipeline::descriptor::ParamId;
use dr_pipeline::mask::{MaskLayer, MaskSource, MaskStack, Morphology}; use dr_pipeline::mask::{MaskLayer, MaskSource, MaskStack, Morphology};
use dr_pipeline::operation::compose_full; use dr_pipeline::operation::compose_full;
@@ -152,7 +150,18 @@ fn main() {
drain.feather = 0.02; drain.feather = 0.02;
pop.push(drain); 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); write(&format!("{prefix}-colour-pop.ppm"), &adjust);
// ---- 2. lift the subject out of its background ------------------------ // ---- 2. lift the subject out of its background ------------------------
@@ -170,7 +179,18 @@ fn main() {
darker.feather = 0.03; darker.feather = 0.03;
lift.push(darker); 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); write(&format!("{prefix}-subject-lift.ppm"), &adjust);
// ---- 3. the same edit, grown and shrunk ------------------------------- // ---- 3. the same edit, grown and shrunk -------------------------------
@@ -192,7 +212,18 @@ fn main() {
layer.morph_radius = radius; layer.morph_radius = radius;
stack.push(layer); 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); write(&format!("{prefix}-{name}.ppm"), &adjust);
} }
@@ -292,12 +323,18 @@ fn render_stack(
fn fit(w: u32, h: u32, longest: u32) -> (u32, u32) { fn fit(w: u32, h: u32, longest: u32) -> (u32, u32) {
let s = (longest as f32 / w.max(h) as f32).min(1.0); 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) { fn write(path: &str, adjust: &AdjustPass) {
let (rgba, w, h) = adjust.export_pixels().expect("readback"); 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); 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 //! PPM for the same reason `develop` uses it: no encoder dependency, and
//! every viewer reads it. This is a diagnostic, not an export path. //! 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_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" /// 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 /// 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 { if !reuse {
let params_buf = self let params_buf =
.ctx self.ctx
.device .device
.create_buffer_init(&wgpu::util::BufferInitDescriptor { .create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("adjust-params"), label: Some("adjust-params"),
contents: bytemuck::cast_slice(&uniforms), contents: bytemuck::cast_slice(&uniforms),
usage: wgpu::BufferUsages::UNIFORM, usage: wgpu::BufferUsages::UNIFORM,
}); });
let bind_group = self let bind_group = self
.ctx .ctx
.device .device
+1 -2
View File
@@ -123,8 +123,7 @@ impl Intermediates {
// output these are never handed to a compositor, and no // output these are never handed to a compositor, and no
// COPY_SRC, because nothing reads them back — that is the // COPY_SRC, because nothing reads them back — that is the
// point (ARCH §6.1). // point (ARCH §6.1).
usage: wgpu::TextureUsages::STORAGE_BINDING usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[], view_formats: &[],
}); });
let view = texture.create_view(&Default::default()); 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, // rather than an implementation detail: a detail pass is guaranteed linear,
// unclipped, full internal precision (FR-DEV-2), and anyone reasoning about // unclipped, full internal precision (FR-DEV-2), and anyone reasoning about
// VRAM at 24 MP needs to know what an intermediate costs. // 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 demosaic::{DemosaicedImage, Demosaicer};
pub use detail::INTERMEDIATE_FORMAT as DETAIL_INTERMEDIATE_FORMAT;
pub use error::GpuError; pub use error::GpuError;
// Renamed on the way out: `BINS` says enough inside `histogram`, and nothing // Renamed on the way out: `BINS` says enough inside `histogram`, and nothing
// at all at a crate root shared with demosaic and segmentation. // 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()) { MaskSource::Subject { .. } => match subjects.filter(|s| slot < s.len()) {
Some(s) => (s, slot), Some(s) => (s, slot),
None => { None => {
log::warn!( log::warn!("mask layer {} has no distance field; skipping", layer.id);
"mask layer {} has no distance field; skipping",
layer.id
);
continue; continue;
} }
}, },
@@ -599,7 +596,14 @@ impl MaskPass {
} }
_ => { _ => {
let selected = self.selection_buffer(layer, field); 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 // Still worth beginning the pass: the slice has to be cleared, or an
// unpainted layer would show whatever the last edit left in it. // unpainted layer would show whatever the last edit left in it.
let bind_group = (!batch.is_empty()).then(|| { let bind_group = (!batch.is_empty()).then(|| {
let params_buf = self let params_buf =
.ctx self.ctx
.device .device
.create_buffer_init(&wgpu::util::BufferInitDescriptor { .create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("mask-brush-params"), label: Some("mask-brush-params"),
contents: bytemuck::bytes_of(params), contents: bytemuck::bytes_of(params),
usage: wgpu::BufferUsages::UNIFORM, usage: wgpu::BufferUsages::UNIFORM,
}); });
// Rebuilt per rasterisation rather than kept and patched. This runs // Rebuilt per rasterisation rather than kept and patched. This runs
// when a mask's shape changes, not per frame, and a few kilobytes // when a mask's shape changes, not per frame, and a few kilobytes
// of stroke geometry is cheaper to upload than a residency scheme // of stroke geometry is cheaper to upload than a residency scheme
@@ -901,12 +905,9 @@ impl MaskPass {
wgpu::BindGroupEntry { wgpu::BindGroupEntry {
binding: 3, binding: 3,
resource: wgpu::BindingResource::TextureView( resource: wgpu::BindingResource::TextureView(
subject subject.0.view(subject.1).unwrap_or_else(|| {
.0 self.empty_subject.view(0).expect("placeholder exists")
.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> { 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(()); return Ok(());
} }
@@ -1111,7 +1116,9 @@ mod tests {
})); }));
let mut pass = MaskPass::new(&ctx).expect("mask pass"); 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.size(), (w, h));
assert_eq!(array.layers(), 1); assert_eq!(array.layers(), 1);
} }
@@ -1201,7 +1208,11 @@ mod tests {
assert_eq!(batch.headers.len(), 1); assert_eq!(batch.headers.len(), 1);
let h = &batch.headers[0]; 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!( assert!(
h.hi[1] - h.lo[1] > h.hi[0] - h.lo[0], h.hi[1] - h.lo[1] > h.hi[0] - h.lo[0],
"and taller than it is wide in normalised units, since the radius \ "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"); .expect("demosaic");
let shader = EditGraph::default_chain().compose(); let shader = EditGraph::default_chain().compose();
let mut adjust = AdjustPass::new(ctx); let mut adjust = AdjustPass::new(ctx);
adjust adjust.render(&source, &shader, SIZE, SIZE).expect("render");
.render(&source, &shader, SIZE, SIZE)
.expect("render");
let (pixels, _, _) = adjust.export_pixels().expect("readback"); let (pixels, _, _) = adjust.export_pixels().expect("readback");
let centre = ((SIZE / 2) * SIZE + SIZE / 2) * 4; let centre = ((SIZE / 2) * SIZE + SIZE / 2) * 4;
pixels[centre as usize] 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 /// 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 /// halves from one graph at one output space, ask the graph for the scale, and
/// pass the invalidation key through. /// pass the invalidation key through.
fn render( fn render(pass: &mut AdjustPass, graph: &EditGraph, source: &DemosaicedImage, out: u32) -> Vec<u8> {
pass: &mut AdjustPass,
graph: &EditGraph,
source: &DemosaicedImage,
out: u32,
) -> Vec<u8> {
let shader = graph.compose_for(ColourSpace::Srgb); let shader = graph.compose_for(ColourSpace::Srgb);
let scale = graph.render_scale(source.size(), (out, out)); let scale = graph.render_scale(source.size(), (out, out));
let detail = graph.compose_detail_for(scale, ColourSpace::Srgb); 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 > 0.05 && export_width > 0.05,
"{proxy_width:.3} / {export_width:.3}" "{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] #[test]
+10 -5
View File
@@ -80,9 +80,7 @@ fn expected_profile(size: u32, r: i32) -> Vec<u8> {
let edge = (size / 2) as i32; let edge = (size / 2) as i32;
(0..size as i32) (0..size as i32)
.map(|x| { .map(|x| {
let white = (-r..=r) let white = (-r..=r).filter(|i| (x + i).clamp(0, last) >= edge).count();
.filter(|i| (x + i).clamp(0, last) >= edge)
.count();
srgb_encode(white as f32 / (2 * r + 1) as f32) srgb_encode(white as f32 / (2 * r + 1) as f32)
}) })
.collect() .collect()
@@ -232,7 +230,10 @@ fn an_inactive_detail_operation_costs_exactly_nothing() {
let mut without = AdjustPass::new(&ctx); let mut without = AdjustPass::new(&ctx);
let b = render(&ctx, &mut without, &plain, &source, SIZE); 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] #[test]
@@ -252,7 +253,11 @@ fn moving_a_detail_parameter_does_not_re_run_the_colour_pass() {
graph.set_param(PROBE, RADIUS, 0.0625); graph.set_param(PROBE, RADIUS, 0.0625);
render(&ctx, &mut pass, &graph, &source, SIZE); render(&ctx, &mut pass, &graph, &source, SIZE);
assert_eq!(pass.colour_dispatches(), 1); 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 // Drag the sharpening slider. The colour chain is untouched, so the linear
// intermediate it wrote is still exactly right. // intermediate it wrote is still exactly right.
+20 -20
View File
@@ -65,12 +65,7 @@ fn render_at(
h: u32, h: u32,
) -> Vec<u8> { ) -> Vec<u8> {
let source = grey_at(ctx, w, h); let source = grey_at(ctx, w, h);
let shader = compose_full( let shader = compose_full(&ops::chain(), &Framing::new(), ColourSpace::Srgb, stack);
&ops::chain(),
&Framing::new(),
ColourSpace::Srgb,
stack,
);
let mut masks = MaskPass::new(ctx).expect("mask pass"); let mut masks = MaskPass::new(ctx).expect("mask pass");
let array = masks.render(stack, field, None, w, h).expect("rasterise"); 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], ids: vec![0],
})); }));
// Right half down. // Right half down.
let mut darken = MaskLayer::new("m2", MaskSource::Regions { let mut darken = MaskLayer::new(
signature: 1, "m2",
level: 2, MaskSource::Regions {
ids: vec![1], signature: 1,
}); level: 2,
ids: vec![1],
},
);
darken.set_param("exposure", ParamId("exposure"), -2.0); darken.set_param("exposure", ParamId("exposure"), -2.0);
stack.push(darken); stack.push(darken);
@@ -412,12 +410,7 @@ fn a_stroke_that_doubles_back_does_not_build_up() {
return; return;
}; };
let once = stack_of(painted(&[( let once = stack_of(painted(&[(false, 0.15, 0.5, vec![(0.1, 0.5), (0.9, 0.5)])]));
false,
0.15,
0.5,
vec![(0.1, 0.5), (0.9, 0.5)],
)]));
let twice = stack_of(painted(&[( let twice = stack_of(painted(&[(
false, false,
0.15, 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 pixels = render_at(&ctx, &stack, None, W, H);
let lit = |v: u8| v > 160; let lit = |v: u8| v > 160;
let across = (0..W).filter(|&x| lit(luma_in(&pixels, W, x, H / 2))).count(); let across = (0..W)
let down = (0..H).filter(|&y| lit(luma_in(&pixels, W, W / 2, y))).count(); .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!( assert!(
across.abs_diff(down) <= 2, across.abs_diff(down) <= 2,
"a dab must be as wide as it is tall, got {across} across and {down} down" "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 // 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. // 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!( assert!(
edited[x].abs_diff(plain[x]) <= 1, edited[x].abs_diff(plain[x]) <= 1,
"column {x} moved by {} away from any edge", "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" an unlimited unsharp mask would give"
); );
// But it is still a real effect, not a control that does nothing. // 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] #[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); let mut graph = graph_with(CLARITY, 40.0);
render(&mut pass, &graph, &source, 256); render(&mut pass, &graph, &source, 256);
assert_eq!(pass.colour_dispatches(), 1); 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(); let pipelines = pass.cached_detail_pipelines();
for amount in [50.0, 60.0, 70.0] { 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 { 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") 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. /// Upload an image built from a per-pixel closure.
fn upload( fn upload(ctx: &GpuContext, size: u32, f: impl Fn(u32, u32) -> [u8; 3]) -> DemosaicedImage {
ctx: &GpuContext,
size: u32,
f: impl Fn(u32, u32) -> [u8; 3],
) -> DemosaicedImage {
let data: Vec<u8> = (0..size * size) let data: Vec<u8> = (0..size * size)
.flat_map(|i| { .flat_map(|i| {
let (x, y) = (i % size, i / size); let (x, y) = (i % size, i / size);
@@ -248,7 +244,11 @@ fn modulation(
let mut count = 0.0; let mut count = 0.0;
for x in margin..(width - margin) { for x in margin..(width - margin) {
let value = sample(linear(pixels, width, x, row)); 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; total += value * sign;
count += 1.0; count += 1.0;
} }
@@ -305,7 +305,13 @@ fn chroma_noise_reduction_never_moves_lightness() {
let mut off = AdjustPass::new(&ctx); let mut off = AdjustPass::new(&ctx);
let plain = render(&ctx, &mut off, &graph_with(0.0, 0.0), &source, (SIZE, SIZE)); let plain = render(&ctx, &mut off, &graph_with(0.0, 0.0), &source, (SIZE, SIZE));
let mut on = AdjustPass::new(&ctx); 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_before = modulation(&plain, SIZE, 4, chroma_r);
let colour_after = modulation(&chroma, 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 mut off = AdjustPass::new(&ctx);
let plain = render(&ctx, &mut off, &graph_with(0.0, 0.0), &source, (SIZE, SIZE)); let plain = render(&ctx, &mut off, &graph_with(0.0, 0.0), &source, (SIZE, SIZE));
let mut on = AdjustPass::new(&ctx); 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. // The colour difference — not the raw channel, which follows lightness.
let row = SIZE / 2; let row = SIZE / 2;
@@ -413,10 +425,10 @@ fn the_same_edit_denoises_the_same_at_two_resolutions() {
let Some(ctx) = ctx() else { return }; let Some(ctx) = ctx() else { return };
const SOURCE: u32 = 256; const SOURCE: u32 = 256;
const HALF_PERIOD: u32 = 8; // in source pixels const HALF_PERIOD: u32 = 8; // in source pixels
// Six code values of swing. Small on purpose: the colour difference has to // 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 // 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 // boundary and the whole point of a bilateral is that it refuses to cross
// those. There would be nothing to measure at either resolution. // those. There would be nothing to measure at either resolution.
let source = chroma_pattern(&ctx, SOURCE, HALF_PERIOD, 6); let source = chroma_pattern(&ctx, SOURCE, HALF_PERIOD, 6);
// Sixty percent is an eight-source-pixel radius, which halves to exactly // 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 proxy_size = SOURCE / 2;
let mut proxy_pass = AdjustPass::new(&ctx); 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); let proxy_amp = modulation(&proxy, proxy_size, HALF_PERIOD / 2, chroma_r);
// Both must be doing something: two flat images would agree perfectly and // Both must be doing something: two flat images would agree perfectly and
// prove nothing. About 0.0098 of 0.0188, by the derivation above. // prove nothing. About 0.0098 of 0.0188, by the derivation above.
let untouched = { let untouched = {
let mut pass = AdjustPass::new(&ctx); 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) modulation(&plain, SOURCE, HALF_PERIOD, chroma_r)
}; };
assert!( 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)] { 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); 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!( assert_eq!(
pass.detail_dispatches(), pass.detail_dispatches(),
expected, expected,
@@ -496,7 +526,13 @@ fn each_amount_costs_only_the_dispatches_it_needs() {
} }
let mut neutral = AdjustPass::new(&ctx); 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_dispatches(), 0);
assert_eq!(neutral.detail_allocations(), 0, "nothing was allocated"); 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 // touch the image, and a stage that ran and wrote back the same values
// would still have quantised twice. // would still have quantised twice.
let mut absent = AdjustPass::new(&ctx); 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"); 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 /// The centre pixel's red, green and blue, after `graph` has run over flat
/// mid-grey. /// mid-grey.
fn rendered(ctx: &GpuContext, graph: &EditGraph) -> (u8, u8, u8) { 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"); let source = DemosaicedImage::from_rgba8(ctx, &data, SIZE, SIZE).expect("upload");
// Composed the way the display path composes it. A curve that generates // 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. /// an edit.
fn lifted(channel: Channel) -> EditGraph { fn lifted(channel: Channel) -> EditGraph {
let mut graph = EditGraph::default_chain(); let mut graph = EditGraph::default_chain();
graph.set_param( graph.set_param(curve::ID, curve::coordinate(channel, 2, Axis::Y), 0.75);
curve::ID,
curve::coordinate(channel, 2, Axis::Y),
0.75,
);
graph graph
} }
@@ -62,7 +60,10 @@ fn the_master_curve_lifts_every_component_together() {
let (r0, g0, b0) = rendered(&ctx, &EditGraph::default_chain()); let (r0, g0, b0) = rendered(&ctx, &EditGraph::default_chain());
let (r, g, b) = rendered(&ctx, &lifted(Channel::Master)); 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 // 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 // luminance, so it changes tone and not hue. A tolerance of one code
// value, because the components travel through the ratio separately and // 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() .iter()
.map(|a| { .map(|a| {
let mut c = a.chars(); 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()) format!("Attribute::{head}{}", c.as_str())
}) })
.collect(); .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 // case where a photographer has a kernel switched on at a scale that
// cannot draw it — against the alternative of the preview failing outright // cannot draw it — against the alternative of the preview failing outright
// or `compose_full` growing a resolution argument it has no other use for. // or `compose_full` growing a resolution argument it has no other use for.
if planned.is_empty() if planned.is_empty() && ops.iter().any(|o| o.is_active() && o.detail().is_some()) {
&& ops
.iter()
.any(|o| o.is_active() && o.detail().is_some())
{
return ComposedDetail { return ComposedDetail {
passes: vec![compose_one( passes: vec![compose_one(
RESOLVE_ID, RESOLVE_ID,
+10 -4
View File
@@ -8,8 +8,7 @@
//! so reordering the pipeline needs no code change. //! so reordering the pipeline needs no code change.
use crate::descriptor::{ use crate::descriptor::{
Attribute, Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamId, ParamKind, Presentation,
Facet, LocalizedKey, OpDescriptor, OpId, ParamId, ParamKind, Presentation,
}; };
use crate::framing::{CropRect, Framing}; use crate::framing::{CropRect, Framing};
use crate::mask::MaskStack; use crate::mask::MaskStack;
@@ -378,7 +377,11 @@ impl EditGraph {
/// Only the detail stage needs this. Every point operation is scale-free /// 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 /// — 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. /// 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 (fw, fh) = self.framing.output_size(source.0, source.1);
let view = self.framing.view(); let view = self.framing.view();
// The *viewed* part of the framed image, at source resolution. Zoom // 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 /// Empty for every edit with no active neighbourhood operation, which is
/// almost all of them — and in that case [`Self::compose`] emits the /// almost all of them — and in that case [`Self::compose`] emits the
/// single encoded dispatch it always has. /// 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) self.compose_detail_for(scale, dr_types::ColourSpace::Srgb)
} }
+2 -2
View File
@@ -44,8 +44,8 @@ pub mod preset;
pub mod sidecar; pub mod sidecar;
pub use descriptor::{ pub use descriptor::{
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, ParamKind, Presentation, Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, ParamKind,
Scale, Unit, WidgetDemand, WidgetKind, Presentation, Scale, Unit, WidgetDemand, WidgetKind,
}; };
pub use detail::{ pub use detail::{
compose_detail, ComposedDetail, ComposedDetailPass, DetailPass, DetailStage, RenderScale, 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 { pub fn begin_stroke(&mut self, erase: bool, radius: f32, hardness: f32, flow: f32) -> bool {
let full = self.stroke_points() >= MAX_LAYER_POINTS; let full = self.stroke_points() >= MAX_LAYER_POINTS;
let MaskSource::Brush { strokes } = &mut self.source else { 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; return false;
}; };
if full { if full {
@@ -1121,7 +1125,10 @@ impl MaskStack {
/// Generate a fresh layer id that does not collide with an existing one. /// Generate a fresh layer id that does not collide with an existing one.
pub fn next_id(&self) -> String { 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 // 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 // with `Framing::wgsl_prologue`, and the failure would be a mask that
// is subtly wrong only when straightened. // is subtly wrong only when straightened.
let _ = writeln!( let _ = writeln!(out.body, " var m = sample_mask(uv_src, {slot});");
out.body,
" var m = sample_mask(uv_src, {slot});"
);
let _ = writeln!( let _ = writeln!(
out.body, out.body,
" m = select(m, 1.0 - m, u.{prefix}_invert > 0.5);" " 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!(shader.uniform_fields.contains("mask1_exposure_"));
assert_eq!( assert_eq!(
shader.uniform_values.len(), 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" "one value per emitted field"
); );
} }
@@ -1448,7 +1456,11 @@ mod tests {
// A gradient has no region ids, so nothing can go stale about it. // A gradient has no region ids, so nothing can go stale about it.
let grad = MaskLayer::new( let grad = MaskLayer::new(
"m2", "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)); assert!(!grad.is_stale(999));
} }
@@ -1458,8 +1470,16 @@ mod tests {
let base = segmentation_signature(1600, 1067, 6730, 2); let base = segmentation_signature(1600, 1067, 6730, 2);
assert_eq!(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(1600, 1067, 6730, 5), "tuning");
assert_ne!(base, segmentation_signature(800, 1067, 6730, 2), "proxy size"); assert_ne!(
assert_ne!(base, segmentation_signature(1600, 1067, 42, 2), "region count"); base,
segmentation_signature(800, 1067, 6730, 2),
"proxy size"
);
assert_ne!(
base,
segmentation_signature(1600, 1067, 42, 2),
"region count"
);
} }
#[test] #[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 // 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 // wrong would produce a shader whose storage format does not match the
// texture bound to it. // texture bound to it.
let output_mode = if ops let output_mode = if ops.iter().any(|o| o.is_active() && o.detail().is_some()) {
.iter()
.any(|o| o.is_active() && o.detail().is_some())
{
OutputMode::LinearWorking OutputMode::LinearWorking
} else { } else {
OutputMode::Encoded OutputMode::Encoded
@@ -1389,7 +1386,10 @@ mod tests {
// how a shoulder rolls off. // how a shoulder rolls off.
let mut curve = crate::ops::ToneCurve::new(); let mut curve = crate::ops::ToneCurve::new();
curve.set_param(crate::ops::curve::P2_Y, 0.7); 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; let source = compose(&[Box::new(curve)]).source;
assert_eq!( assert_eq!(
+3 -1
View File
@@ -32,7 +32,9 @@
//! least to perceived sharpness. Scaling all three about a virtual reference //! least to perceived sharpness. Scaling all three about a virtual reference
//! would soften the image even when the correction is right. //! 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::lens::Warp;
use crate::operation::{Helper, Uniform}; use crate::operation::{Helper, Uniform};
+42 -8
View File
@@ -764,9 +764,11 @@ mod tests {
}; };
assert_eq!(gate_of(0.0), 0.0); assert_eq!(gate_of(0.0), 0.0);
assert!((gate_of(1.0) - 0.25).abs() < 1e-6); assert!((gate_of(1.0) - 0.25).abs() < 1e-6);
assert!(chain_at(&sharpening(50.0, 1.0), RenderScale::full((512, 512))).passes[0] assert!(
.source chain_at(&sharpening(50.0, 1.0), RenderScale::full((512, 512))).passes[0]
.contains("gate > 0.0")); .source
.contains("gate > 0.0")
);
} }
#[test] #[test]
@@ -783,7 +785,11 @@ mod tests {
for scale in scales { for scale in scales {
for pass in chain_at(&sharpening(75.0, 1.0), scale).passes { for pass in chain_at(&sharpening(75.0, 1.0), scale).passes {
assert_eq!(pass.uniforms.len() % 4, 0, "{}", pass.label); 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 // 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. // difference nobody discovers until a shader fails to compile.
const RESERVED: &[&str] = &[ const RESERVED: &[&str] = &[
"target", "sample", "filter", "texture", "buffer", "binding", "const", "enum", "mat", "target",
"vec", "ptr", "ref", "shared", "static", "typedef", "union", "unless", "handle", "sample",
"layout", "packed", "premerge", "regardless", "active", "do", "input", "output", "filter",
"private", "resource", "restrict", "self", "std", "where", "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 { 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 //! 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. //! 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::operation::{Helper, Operation, Uniform};
use crate::ops::helpers; 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 /// Master first because it runs first — see the module documentation for
/// why that is the composition order — and because a list showing the /// why that is the composition order — and because a list showing the
/// grade before the tone would be describing a different operation. /// grade before the tone would be describing a different operation.
pub const ALL: [Channel; 4] = [ pub const ALL: [Channel; 4] = [Channel::Master, Channel::Red, Channel::Green, Channel::Blue];
Channel::Master,
Channel::Red,
Channel::Green,
Channel::Blue,
];
/// Position in [`Self::ALL`], and so in every array keyed by channel. /// Position in [`Self::ALL`], and so in every array keyed by channel.
pub const fn index(self) -> usize { 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 // declared from, so a call and its uniform block cannot disagree
// about a name. // about a name.
let args = UNIFORM_NAMES[channel.index()].join(", "); 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 // 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 body = c.wgsl_body();
let master = body.find("apply_tone_gain").expect("the master curve runs"); 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}"); 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. //! profile is a "make the horizon straight" task, which one term does well.
//! The full triple is reachable by loading a profile. //! 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::lens::Warp;
use crate::operation::{Helper, Uniform}; use crate::operation::{Helper, Uniform};
+15 -4
View File
@@ -619,7 +619,10 @@ mod tests {
let scale = RenderScale::full((2000, 1500)); let scale = RenderScale::full((2000, 1500));
let only_clarity = composed(50.0, 0.0, scale); let only_clarity = composed(50.0, 0.0, scale);
assert_eq!(only_clarity.len(), 2, "one operation, two passes"); 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); let both = composed(50.0, 50.0, scale);
assert_eq!(both.len(), 4); assert_eq!(both.len(), 4);
@@ -701,7 +704,10 @@ mod tests {
// acutance family's unit. // acutance family's unit.
let proxy = RenderScale::new((2000, 1500), (6000, 4500)); let proxy = RenderScale::new((2000, 1500), (6000, 4500));
let clarity = Clarity::with_amount(60.0); 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!( assert!(
proxy.source_pixels(96.0) < 40.0, proxy.source_pixels(96.0) < 40.0,
"the same length in the other unit would have collapsed" "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 // slider can produce is `gain * threshold` stops however violent the
// edge — a bound that holds by construction rather than by tuning. // edge — a bound that holds by construction rather than by tuning.
let combine = &Clarity::with_amount(100.0).passes(RenderScale::full((2000, 1500)))[1]; 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 bound = |u: &[Uniform]| {
let get = |n| u.iter().find(|x| x.name == n).unwrap().value; 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 // 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. // 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() assert!(!Texture::new()
.helpers() .helpers()
.iter() .iter()
+7 -1
View File
@@ -820,7 +820,13 @@ mod tests {
// Everything else about the two must match, or one axis is filtered // Everything else about the two must match, or one axis is filtered
// harder than the other and a round blotch comes out oval. // harder than the other and a round blotch comes out oval.
assert_eq!(passes[0].radius, passes[1].radius); 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| { let of = |p: &DetailPass| {
p.uniforms p.uniforms
.iter() .iter()
+6 -5
View File
@@ -67,9 +67,7 @@ use std::fmt;
use std::fmt::Write as _; use std::fmt::Write as _;
use crate::graph::EditGraph; use crate::graph::EditGraph;
use crate::mask::{ use crate::mask::{Falloff, MaskLayer, MaskSource, MaskStack, Morphology, Stroke, DEFAULT_FEATHER};
Falloff, MaskLayer, MaskSource, MaskStack, Morphology, Stroke, DEFAULT_FEATHER,
};
use crate::preset::{resolve, Preset}; use crate::preset::{resolve, Preset};
/// Format version of the document itself. /// Format version of the document itself.
@@ -662,7 +660,6 @@ impl Sidecar {
} }
} }
/// Write one mask layer as its own block. /// Write one mask layer as its own block.
/// ///
/// The version uuid is repeated in the header rather than relying on the /// The version uuid is repeated in the header rather than relying on the
@@ -1292,7 +1289,11 @@ mod tests {
// else. // else.
assert_eq!(g.param(curve::ID, curve::P1_Y), Some(0.15)); assert_eq!(g.param(curve::ID, curve::P1_Y), Some(0.15));
assert_eq!(g.param(curve::ID, curve::P3_Y), Some(0.85)); 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 point in 0..curve::POINTS {
for axis in [curve::Axis::X, curve::Axis::Y] { for axis in [curve::Axis::X, curve::Axis::Y] {
let id = curve::coordinate(channel, point, axis); 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.sort();
present.dedup(); 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::Colour));
assert!( assert!(
present.contains(&Attribute::Geometry), 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.id, "m1");
assert_eq!(layer.name, "Subject"); assert_eq!(layer.name, "Subject");
assert_eq!(layer.source, regions(&[3, 7, 12])); 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] #[test]
@@ -120,7 +127,10 @@ fn flags_and_opacity_survive() {
let layer = &restored.masks().layers()[0]; let layer = &restored.masks().layers()[0];
assert!(layer.invert); assert!(layer.invert);
assert!((layer.opacity - 0.35).abs() < 1e-6); 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 /// 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"; exposure.exposure = 1\n";
let out = Sidecar::parse(text).expect("parse").to_text(); 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]")); assert!(out.contains("[mask default m1]"));
} }
@@ -361,7 +374,13 @@ fn a_brush_layer_writes_one_line_per_stroke() {
.lines() .lines()
.filter(|l| l.starts_with("stroke = ")) .filter(|l| l.starts_with("stroke = "))
.collect(); .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] #[test]
+2 -2
View File
@@ -12,7 +12,8 @@ use dr_pipeline::{EditGraph, Sidecar};
/// A version block carrying `params`, in the on-disk spelling. /// A version block carrying `params`, in the on-disk spelling.
fn sidecar_with(params: &[(&str, f32)]) -> String { 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 { for (key, value) in params {
text.push_str(&format!("{key} = {value}\n")); 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!( println!(
"tiling {}", "tiling {}",
if tiled { "grid, 25% overlap" } else { "whole frame" } if tiled {
"grid, 25% overlap"
} else {
"whole frame"
}
); );
let t0 = std::time::Instant::now(); let t0 = std::time::Instant::now();
@@ -57,10 +61,7 @@ fn main() {
let instances = model let instances = model
.detect(&rgb, width, height, &options) .detect(&rgb, width, height, &options)
.expect("inference"); .expect("inference");
println!( println!("detect {:.0} ms", t1.elapsed().as_secs_f32() * 1000.0);
"detect {:.0} ms",
t1.elapsed().as_secs_f32() * 1000.0
);
println!("found {} instances", instances.len()); println!("found {} instances", instances.len());
for (i, inst) in instances.iter().enumerate() { 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. // where *not* linearising is the correct thing to do.
let rgb = match pixels.len() / (w * h) { let rgb = match pixels.len() / (w * h) {
3 => pixels.iter().map(|&v| v as f32 / 255.0).collect(), 3 => pixels.iter().map(|&v| v as f32 / 255.0).collect(),
1 => pixels 1 => pixels.iter().flat_map(|&v| [v as f32 / 255.0; 3]).collect(),
.iter()
.flat_map(|&v| [v as f32 / 255.0; 3])
.collect(),
n => panic!("unexpected {n} components per pixel"), n => panic!("unexpected {n} components per pixel"),
}; };
+11 -4
View File
@@ -346,8 +346,8 @@ impl Shaped {
radius_px: f32, radius_px: f32,
) -> Self { ) -> Self {
let distance = signed_distance(coverage, width, height, threshold); let distance = signed_distance(coverage, width, height, threshold);
let distance = apply_morphology(&distance, width, height, morphology, radius_px) let distance =
.unwrap_or(distance); apply_morphology(&distance, width, height, morphology, radius_px).unwrap_or(distance);
Self { Self {
distance, distance,
width, width,
@@ -403,7 +403,11 @@ mod tests {
// Distance from the boundary, so the half-pixel comes back off to // Distance from the boundary, so the half-pixel comes back off to
// compare against the centre-to-centre figures. // compare against the centre-to-centre figures.
let at = |x: usize, y: usize| -d[y * 5 + x] + 0.5; 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!((at(0, 0) - (8.0f32).sqrt()).abs() < 1e-4, "{}", at(0, 0));
assert_eq!(at(2, 0), 2.0, "straight up is exactly two"); assert_eq!(at(2, 0), 2.0, "straight up is exactly two");
} }
@@ -489,7 +493,10 @@ mod tests {
m[10 * w + 10] = 0; m[10 * w + 10] = 0;
let before = signed_distance(&m, w, h, 128); 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"); 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"); 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. /// The instance a region belongs to, if any.
pub fn instance_of(&self, region: u32) -> Option<usize> { 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`. /// 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 /// The returned ids are sorted, so the same click always produces the same
/// mask — which is what lets it be a cache key. /// mask — which is what lets it be a cache key.
pub fn regions_for_instance( pub fn regions_for_instance(field: &RegionField, mask: &[f32], options: &PriorOptions) -> Vec<u32> {
field: &RegionField,
mask: &[f32],
options: &PriorOptions,
) -> Vec<u32> {
let mut coverage = vec![0.0f32; field.region_count]; let mut coverage = vec![0.0f32; field.region_count];
let mut sizes = vec![0u32; 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], labels: vec![0, 0, 2, 2, 1, 1, 3, 3],
region_count: 4, region_count: 4,
adjacency: vec![ adjacency: vec![
Edge { a: 0, b: 1, saddle: 1.0 }, Edge {
Edge { a: 0, b: 2, saddle: 1.0 }, a: 0,
Edge { a: 1, b: 3, saddle: 1.0 }, b: 1,
Edge { a: 2, b: 3, saddle: 1.0 }, 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] #[test]
fn zero_strength_is_arm_a_exactly() { fn zero_strength_is_arm_a_exactly() {
let f = field(); 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(); let m = Membership::compute(&f, &[&left_half()], &opts).unwrap();
assert_eq!(apply_semantic_prior(&f, &m, &opts).adjacency, f.adjacency); 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(); let err = Membership::compute(&f, &[&vec![0.0; 3]], &PriorOptions::default()).unwrap_err();
assert_eq!( assert_eq!(
err, 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() { fn different_instances_repel() {
let f = field(); let f = field();
let right_half = vec![0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0]; let right_half = vec![0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0];
let m = Membership::compute( let m = Membership::compute(&f, &[&left_half(), &right_half], &PriorOptions::default())
&f, .unwrap();
&[&left_half(), &right_half],
&PriorOptions::default(),
)
.unwrap();
assert_eq!(m.instance_of(0), Some(0)); assert_eq!(m.instance_of(0), Some(0));
assert_eq!(m.instance_of(2), Some(1)); assert_eq!(m.instance_of(2), Some(1));
+11 -3
View File
@@ -357,7 +357,6 @@ impl SemanticModel {
options, options,
)) ))
} }
} }
/// Turn one forward pass into instances in source-image space. /// 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 (gx1, gy1) = ((gx0 + 1).min(pw - 1), (gy0 + 1).min(ph - 1));
let mut acc = 0.0; 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 { if c == 0.0 {
continue; continue;
} }
@@ -643,7 +646,12 @@ mod tests {
#[test] #[test]
fn letterbox_round_trips_a_landscape_window() { 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); let lb = Letterbox::fit(window.w, window.h);
// A source point maps into the input and back to where it started. // 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 { let Some(undo) = ctl.press_undo.borrow_mut().take() else {
return; return;
}; };
let (previous_anchor, cursor) = let (previous_anchor, cursor) = undo.restore(
undo.restore(&mut ctl.selection.borrow_mut(), &mut ctl.anchor.borrow_mut()); &mut ctl.selection.borrow_mut(),
&mut ctl.anchor.borrow_mut(),
);
ctl.previous_anchor.set(previous_anchor); ctl.previous_anchor.set(previous_anchor);
ctl.set_cursor(cursor); ctl.set_cursor(cursor);
sync_selection(window, ctl, ids); 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. // few places in this codebase where not linearising is correct.
let rgb = rgba let rgb = rgba
.chunks_exact(4) .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(); .collect();
Ok((rgb, pw as usize, ph as usize)) Ok((rgb, pw as usize, ph as usize))
} }
@@ -1458,10 +1464,7 @@ impl DevelopSession {
/// What kind of mask a layer is — "regions", "linear", "radial". /// What kind of mask a layer is — "regions", "linear", "radial".
pub fn mask_kind(&self, id: &str) -> &'static str { pub fn mask_kind(&self, id: &str) -> &'static str {
self.graph self.graph.masks().get(id).map_or("", |l| l.source.kind())
.masks()
.get(id)
.map_or("", |l| l.source.kind())
} }
pub fn mask_inverted(&self, id: &str) -> bool { pub fn mask_inverted(&self, id: &str) -> bool {
@@ -1625,7 +1628,11 @@ impl DevelopSession {
width: 0.3, 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; return None;
} }
self.active_mask = Some(id.clone()); self.active_mask = Some(id.clone());
@@ -1660,7 +1667,8 @@ impl DevelopSession {
pub fn set_mask_feather(&mut self, id: &str, feather: f32) { pub fn set_mask_feather(&mut self, id: &str, feather: f32) {
if let Some(layer) = self.graph.masks_mut().get_mut(id) { if let Some(layer) = self.graph.masks_mut().get_mut(id) {
layer.feather = feather.clamp(0.0, 1.0); 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) { pub fn set_mask_morph_radius(&mut self, id: &str, radius: f32) {
if let Some(layer) = self.graph.masks_mut().get_mut(id) { if let Some(layer) = self.graph.masks_mut().get_mut(id) {
layer.morph_radius = radius.clamp(0.0, 1.0); 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 { pub fn mask_falloff(&self, id: &str) -> usize {
use dr_pipeline::mask::Falloff; use dr_pipeline::mask::Falloff;
self.graph.masks().get(id).map_or(0, |l| { 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 { pub fn mask_is_shapeable(&self, id: &str) -> bool {
use dr_pipeline::mask::MaskSource; use dr_pipeline::mask::MaskSource;
self.graph.masks().get(id).is_some_and(|l| { 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 // `Op` rather than `Discrete`: opacity is dragged, and a drag is
// one decision however many values it passes through. `Discrete` // one decision however many values it passes through. `Discrete`
// would put every intermediate position on the undo stack. // 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() { fn a_job_carries_the_session_it_was_taken_from() {
let Some(ctx) = headless() else { return }; let Some(ctx) = headless() else { return };
let rgba: Vec<u8> = (0..16 * 16).flat_map(|_| [128, 128, 128, 255]).collect(); let rgba: Vec<u8> = (0..16 * 16).flat_map(|_| [128, 128, 128, 255]).collect();
let session = let session = DevelopSession::open_rgb(&ctx, &rgba, 16, 16, dr_types::Orientation::NORMAL)
DevelopSession::open_rgb(&ctx, &rgba, 16, 16, dr_types::Orientation::NORMAL) .expect("session");
.expect("session");
assert_eq!(session.segmentation_job().session(), session.id()); assert_eq!(session.segmentation_job().session(), session.id());
} }
@@ -2523,9 +2538,8 @@ mod tests {
fn an_abandoned_job_does_no_work() { fn an_abandoned_job_does_no_work() {
let Some(ctx) = headless() else { return }; let Some(ctx) = headless() else { return };
let rgba: Vec<u8> = (0..16 * 16).flat_map(|_| [128, 128, 128, 255]).collect(); let rgba: Vec<u8> = (0..16 * 16).flat_map(|_| [128, 128, 128, 255]).collect();
let session = let session = DevelopSession::open_rgb(&ctx, &rgba, 16, 16, dr_types::Orientation::NORMAL)
DevelopSession::open_rgb(&ctx, &rgba, 16, 16, dr_types::Orientation::NORMAL) .expect("session");
.expect("session");
let job = session.segmentation_job(); let job = session.segmentation_job();
job.abandon().now(); job.abandon().now();
@@ -3993,7 +4007,10 @@ mod tests {
let narrowed = session.rows().len(); let narrowed = session.rows().len();
assert!(narrowed > 0, "a tab must show something"); 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 /// The trap: `op_index` counts over *every* capability, so a row that
@@ -4054,7 +4071,6 @@ mod tests {
session.set_active_tab(99); session.set_active_tab(99);
assert_eq!(session.rows().len(), all); assert_eq!(session.rows().len(), all);
} }
} }
/// `dr_pipeline`'s morphology, as `dr_segment` names it. /// `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. /// The ordinary case: the answer comes back to the photograph that asked.
#[test] #[test]
fn a_result_for_the_open_photograph_is_applied() { 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>>>, session: &Rc<RefCell<Option<DevelopSession>>>,
library: &Rc<library_ui::LibraryController>, library: &Rc<library_ui::LibraryController>,
) { ) {
let worth_it = window.get_can_undo() let worth_it =
|| session.borrow().as_ref().is_some_and(|s| !s.is_neutral()); window.get_can_undo() || session.borrow().as_ref().is_some_and(|s| !s.is_neutral());
if !worth_it { if !worth_it {
return; return;
} }