Write out the derivation behind every kernel width these tests assert

A test that cannot be run cannot be checked by running it, and a number
copied out of a test run agrees with whatever the code did on the day.
Each asserted kernel width, tolerance and overshoot bound now carries the
arithmetic that produces it -- the shorter edge, the sigma, the
truncation at two sigmas, and where the rounding falls -- so a reader can
verify the expectation against the recipe without a GPU or a compiler.

Also records the two places where a bound is a bound and not a
measurement: the tolerance in the frame-fraction test is exactly what
rounding a kernel to a whole pixel costs on the smallest frame it uses,
and the halo test's floor and ceiling bracket a peak derived from the
step, the soft limit and the midtone taper rather than from a run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-22 19:18:13 +02:00
co-authored by Claude Opus 5
parent 88ce89428b
commit c9305fd0e6
2 changed files with 66 additions and 1 deletions
+53 -1
View File
@@ -633,7 +633,15 @@ mod tests {
let scale = RenderScale::full((4000, 3000));
let clarity = Clarity::with_amount(100.0).kernel(scale);
let texture = Texture::with_amount(100.0).kernel(scale);
// 1.2% of the *shorter* edge — 3000, not 4000 — and two sigmas wide.
// Both derived rather than observed, because a number copied out of a
// test run agrees with whatever the code did on the day.
//
// `frame_fraction` takes the *shorter* edge: min(4000, 3000) = 3000.
// clarity σ = 0.012 × 3000 = 36.0 → round(36.0 × 2) = 72
// texture σ = 0.0012 × 3000 = 3.6 → round( 3.6 × 2) = 7
//
// (7.2 rounds down, which is why texture is 7 and not 8 — the
// truncation is two sigmas, and two sigmas of 3.6 px is 7.2 px.)
assert_eq!(clarity, 72);
assert_eq!(texture, 7, "a decade finer");
assert!(
@@ -652,6 +660,16 @@ mod tests {
// must cover the same *proportion* of the picture on a proxy as in the
// export, which is what `frame_fraction` gives and what
// `source_pixels` would not.
// The declared proportion is σ × 2 = 0.012 × 2 = 0.024 of the shorter
// edge, and the tolerance is what rounding to a whole pixel costs:
//
// 300 → round(0.024 × 300) = 7 → 7/300 = 0.02333 (−0.00067)
// 1500 → round(0.024 × 1500) = 36 → 36/1500 = 0.02400 ( 0.00000)
// 4500 → round(0.024 × 4500) = 108 → 108/4500 = 0.02400 ( 0.00000)
//
// Half a pixel over the smallest frame here is 0.5/300 = 0.0017, so
// 0.002 is the tolerance a rounded kernel can actually hold — and it
// is a bound, not a fitted number.
let sizes = [(400u32, 300u32), (2000, 1500), (6000, 4500)];
let proportions: Vec<f32> = sizes
.iter()
@@ -670,6 +688,17 @@ mod tests {
// And the contrast with the other unit, stated rather than implied: on
// a one-third proxy a source-pixel radius *shrinks* to a third of the
// proportion it had, which is the bug this choice avoids.
//
// ratio = (2000/6000 + 1500/4500) / 2 = 1/3
// frame_fraction(0.012) → 0.012 × 1500 = 18 px, the same 18 px the
// export of that framing gets, because the
// unit is a proportion of what is rendered;
// source_pixels(96) → 96 × 1/3 = 32 px, a third of the reach
// the same edit had at full size.
//
// 96 is clarity's kernel at 4000 px of shorter edge, so the second line
// is what this control would have done had it been written in the
// 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))));
@@ -685,6 +714,15 @@ mod tests {
// rendering of the frame, so the honest thing is to contribute no
// pass. Unlike the acutance family this is not an approximation being
// hidden: zoom in and the kernel comes back, exactly.
//
// shorter edge = 120
// texture σ = 0.0012 × 120 = 0.144 → round(0.288) = 0 — no pass
// clarity σ = 0.012 × 120 = 1.44 → round(2.88) = 3 — two passes
//
// Texture's kernel crosses back above zero at round(0.0024 × e) ≥ 1,
// i.e. a shorter edge of about 209 px, which is a little larger than a
// contact sheet thumbnail and a great deal smaller than any view a
// photographer judges surface detail in.
let thumbnail = RenderScale::full((160, 120));
assert_eq!(Texture::with_amount(100.0).kernel(thumbnail), 0);
assert!(Texture::with_amount(100.0).passes(thumbnail).is_empty());
@@ -724,6 +762,14 @@ mod tests {
// and nothing can infer that from the WGSL because the offsets come
// from a uniform. An understated radius shows as a seam at every tile
// boundary — an artefact that reads as a driver bug.
//
// shorter edge = 1500
// clarity → round(0.024 × 1500) = 36 px
// texture → round(0.0024 × 1500) = 4 px
//
// so the halo the four passes together need is clarity's 36, taken as
// the maximum rather than the sum: the passes are separate dispatches,
// and a tile is grown for whichever of them reaches furthest.
let scale = RenderScale::full((2000, 1500));
let composed = composed(50.0, 50.0, scale);
let clarity = Clarity::with_amount(50.0).kernel(scale);
@@ -749,6 +795,12 @@ mod tests {
// A third of a stop for clarity, before the midtone taper takes more
// off; a little over a stop for texture, whose overshoot is two pixels
// wide and reads as acutance.
//
// clarity gain = 100/100 × 1.00 = 1.00 ; × 0.35 = 0.35 stops (26%)
// texture gain = 100/100 × 1.25 = 1.25 ; × 1.00 = 1.25 stops
//
// Both at full travel, which is what makes them a bound and not a
// measurement: no picture, and no edge in any picture, can produce more.
assert!((bound(&combine.uniforms) - 0.35).abs() < 1e-6);
let texture = &Texture::with_amount(100.0).passes(RenderScale::full((2000, 1500)))[1];
assert!((bound(&texture.uniforms) - 1.25).abs() < 1e-6);