photocraft/docs/tilt-shift-performance.md

19 KiB
Raw Permalink Blame History

Tilt-Shift computation (2026-10-10)

Starting base: a28d786638ffbb84c5fb7d5d30ea1c3d594cd3bc, freshly fetched origin/main. Branch: codex/tilt-shift-performance-20261010. Worktree: C:\Users\Colum\.codex\worktrees\6c32\photocraft. At investigation time, upstream/main was 5963e5b7f0aefa0d94b5cf67ef3c1c9b6d3399dd, two commits ahead: Recent entries and temporary Move. Neither changes filtering. The original checkout, local main and other worktrees were not edited. Cargo uses target/agent-tilt-shift throughout.

Upstream evidence and choice

GitHub open/closed/merged PRs and open/closed issues were searched for blur, Tilt-Shift, Field Blur, sampling, SIMD, portable, std::simd and rustfft. Relevant bodies, discussions, final diffs, merge commits, Gallery history and current source were inspected before the implementation was selected.

Work Status and evidence Consequence for Tilt-Shift
#1524: portable Mosaic reads, merge d651be13 The SIMD-related proposal the human referred to. Its AVX2/pulp portion was experimental and removed before merge: the review found no complete-filter gain to justify dependencies/features/CI. Portable contiguous row reads and exact-output tests landed. Reuse contiguous reads. Main has no general portable-SIMD Gallery backend or nightly std::simd path to reuse.
#1471: adaptive rustfft Motion Blur, merge c7dbb37b Merged. Builds on #902's merged bilinear taps/row convolution. Wide uniform streaks benefit from FFT; short/unsupported cases retain rows. Current code bounds FFT allocations and rejects unsuitable coordinates/samples from that path. Tilt-Shift is spatially varying, not one uniform convolution. A single FFT would change the band; one FFT per existing Gaussian level would preserve spatial blending but replace linear-time box cascades with transforms, padding, complex scratch and numerical differences. No demonstrated crossover warrants that machinery here.
#867: Gaussian running sums and bands, #628: Box Blur, #1602: fixed-channel accumulators carrying closed #1511 Merged, with radius-independent separable passes, traversal/allocation improvements and compiler-friendly accumulators. Gallery already uses separable boxes, with f64 running sums. Replacing them with the f32 Gaussian accumulators would change HDR cancellation/rounding. Avoid a broad kernel/traversal rewrite.
#1499: Radial sampling Merged after review removed a changed-output polar approximation and a separate preview scheduler. Exact transform preparation and interleaved taps remained. Preserve the kernel and reuse current services. Tilt-Shift already prepares its band geometry once per tile and has no radial/disk samples to precompute.
#2578: Iris Blur, merge fa33712d Already merged in the starting base. Prepares Iris geometry and adds exact-output contiguous premultiplication and per-level trailing-halo trimming to the shared helper; only Iris opts in. Opt Tilt-Shift into that established helper. No new algorithm, dependency, cache or scheduler is needed. Field remains unchanged.
#1899: preview worker/cancellation, #1825: keep preview during commit, #2207: preview scale Merged. Latest-request worker; cancel stale work after 150 ms without parameter changes, preserving updates during dragging. Pixel parameters scale with the existing proxy. Leave the UI and worker intact; faster tile computation also shortens the existing worker's filtering work.
Active #2685, #2580, #2739 Respectively Spin/Lens/Extrude/Wind/Trap sampling, general compression/IO/compositing allocation work, and smart-filter stage caching/background jobs. #2685 touches Gallery's Spin path, not Tilt-Shift's Gaussian field. No existing Tilt-Shift fix or overlapping implementation found. Keep this change confined to Tilt-Shift's opt-in and its tests.

Issue #464 records Gaussian preview lag and profiling with copying and waits prominent; #211 tracks broader slow algorithms. They inform the need for complete-filter and preview-workload measurements, rather than claiming a microbenchmark fixes GUI latency.

RustFFT's primary documentation describes O(N log N) transforms and automatic native AVX/SSE/NEON selection, with different wasm SIMD requirements. That is useful existing infrastructure for uniform expensive kernels, but does not remove the spatially varying blend or improve the asymptotic cost of Gallery's existing box passes. No FFT or explicit SIMD candidate is benchmarked here; the comparison above is an algorithmic and maintenance assessment, not a measured claim that they can never be faster.

Numerical and memory contract

Tilt-Shift has one band, described by centerX, centerY, angle, focus, transition and blur; its engine/model has no pins parameter. Six nonzero Gaussian levels plus the zero level retain their existing geometric spacing, three-box radii, smoothstep sigma field and hat weights. Sharp/blurred and transition pixels still blend their original levels. Pixel centres, image coordinates, transparent premultiplication and edge policy are unchanged.

The optimized helper clones the already interleaved source and premultiplies it with the same per-pixel operations. For each Gaussian level it keeps the original top/left, trimming only right/bottom to the output plus that level's summed box radii. Its artificial trailing edge cannot influence output through the three finite-support passes. Keeping the leading edge preserves the running-sum initialization, update order and f64 cancellation, including HDR. No samples or levels are dropped. Cropped-output containment retains the helper's existing fallback to a full window.

The existing source, premultiplied buffer, sigma map, accumulator and reusable level buffer remain. Full-window capacity is still reserved once; populated length and blur scratch shrink. There is no retained cache, feature dispatch, threshold or planning cost. Source halo reads, auto tile sizes, worker count, selection mixing, output writeback, undo/redo and cancellation services remain the same. No change requires a native CPU feature or unsafe code.

One pre-existing crash found by the synthetic tests is fixed: a NaN low-level blur amount previously became f32::clamp's invalid upper bound. It now acts as identity. The regression asserts the frozen starting implementation panics and the corrected kernel returns the source. Valid finite parameters keep exact bits.

tests_tilt_shift_reference.rs freezes the starting scalar Gallery/Tilt-Shift expressions, using the unchanged Gaussian helpers. Differential tests compare every finite f32 bit (including signed zero), with matching NaN classifications: 1–6 channels, RGB/Gray/CMYK/Lab and the expanded Bitmap/Indexed/Duotone/Multichannel storage, U8/U16/F32, alpha/no alpha, narrow and soft band boundaries, translated/rotated bands, tiny/empty/one-row images, blur thresholds, full focus/full blur, extreme and nonfinite parameters, HDR/negative/high-contrast samples, nonfinite pixels, selections, transparent/repeated edges and tile sizes 16/47/128. Cancellation before work and after progress preserves the source. Malformed internal source lengths and zero channels are also checked.

Measurement method

Windows x86_64, Ryzen 7 9700X (8 cores/16 logical), 64 GiB installed RAM; stable Rust 1.99.0 / LLVM 23.1.1. Normal workspace release profile: opt-level 3, thin LTO, one codegen unit. Eight Rayon workers. Builds/tests are stopped during measurements, and process inspection checks for concurrent Cargo/rustc/benchmark work. Ordinary system activity and thermal/allocator variation remain.

bench_tilt_shift constructs deterministic textured RGBA surfaces with varying alpha and HDR F32 values. Two untimed warmups precede each case. Timing covers complete apply_in: source/halo reads, tiled field and level computation, Rayon, writeback and pruning. Fixture construction and output destruction are excluded. It uses the unchanged pipeline, not an isolated hot loop. Baseline executables are built before editing the algorithm and retained in local plan/. The blurred case puts the band outside the image through the low-level Rust API; it is a saturated-field stress case, outside the engine dialog's centre range. All other cases use the supported dialog parameters. No pin list is invented for Tilt-Shift. Model performance is measured for RGB; other models are covered by differential correctness tests.

The preview workload models the existing 24 MP document's 1500×1000 filtered proxy (factor 4), with blur 15/80/300 becoming 3.75/20/75. Position, angle, focus and transition are relative and unchanged. This times filtering a proxy after a parameter update. It excludes proxy construction, Session/history, queue/settle delay, compositing, GPU upload and presentation; it is not actual GUI input/frame latency. The implementation does not alter the preview factor or kernel.

$env:CARGO_TARGET_DIR = 'target/agent-tilt-shift'
$env:RAYON_NUM_THREADS = '8'
cargo run --release -p photocraft-algo --example bench_tilt_shift -- 6000 4000 5 1
cargo run --release -p photocraft-algo --example bench_tilt_shift -- 6000 6000 5 1
cargo run --release -p photocraft-algo --example bench_tilt_shift -- 1500 1000 7 4
cargo run --release -p photocraft-algo --example bench_tilt_shift -- 128 96 7 1

Results

Order: base A, candidate, base B, with all four sizes completed per implementation round. Full-size cases have five timed samples per round; proxy and small cases have seven. Base p50 pools the two unchanged rounds (10/14 samples); candidate p50 uses 5/7 samples. Ranges include every timed sample, not only best runs. Local raw logs are plan/tilt-{baseline,candidate,baseline-repeat}-*.jsonl.

The parameters are fixed by the example: small is blur 15, centred, angle 0, focus 0.1, transition 0.15; medium is blur 80 at angle 27; large is blur 300, angle -71, focus 0.03, transition 0.4. wide_focus is blur 80, angle 0, focus 0.4, transition 0.05; sharp_transition is centred at (0.2, 0.8), blur 80, angle 63, focus 0.05, transition 0.01. focused uses focus 1; blurred uses the low-level outside-image centre (-5, -5). The latter two use blur 80. Proxy blur alone is divided by four. U16/F32 time the rotated medium case.

24 MP (6000×4000)

Depth / case Base ms (range) Candidate ms (range) Speedup
U8 medium 1729.3 (1530.2–1996.6) 1241.4 (1227.0–1256.5) 1.39×
U8 small 609.5 (594.2–616.1) 451.7 (443.1–479.9) 1.35×
U8 large 5340.1 (5042.7–5861.9) 3698.9 (3498.1–3722.3) 1.44×
U8 wide_focus 1204.9 (1177.8–1273.9) 828.7 (792.5–874.5) 1.45×
U8 sharp_transition 1615.6 (1576.2–1636.4) 1148.4 (1135.1–1153.2) 1.41×
U8 focused 433.5 (426.1–438.2) 406.1 (399.6–410.5) 1.07×
U8 blurred 1105.7 (1051.6–1146.0) 963.6 (938.0–1045.8) 1.15×
U16 medium 1935.8 (1877.7–1978.8) 1306.8 (1268.9–1403.0) 1.48×
F32 medium 1961.7 (1858.5–2010.5) 1316.8 (1270.2–1396.5) 1.49×

36 MP (6000×6000)

Depth / case Base ms (range) Candidate ms (range) Speedup
U8 medium 2458.7 (2066.6–2541.9) 1668.0 (1656.9–1682.4) 1.47×
U8 small 806.6 (733.7–864.2) 655.1 (627.0–678.9) 1.23×
U8 large 8856.0 (6766.5–9301.8) 6176.2 (5780.6–6622.0) 1.43×
U8 wide_focus 1612.8 (1562.4–1672.7) 1066.4 (1057.8–1084.3) 1.51×
U8 sharp_transition 2529.7 (2448.6–2590.1) 1714.6 (1705.0–1890.4) 1.48×
U8 focused 646.3 (627.5–680.0) 614.1 (613.8–618.5) 1.05×
U8 blurred 1633.2 (1595.3–1687.9) 1577.6 (1566.4–1623.2) 1.04×
U16 medium 2618.4 (2498.4–2687.4) 2020.7 (1994.9–2027.0) 1.30×
F32 medium 2669.3 (2561.2–2741.6) 2020.5 (1734.9–2173.3) 1.32×

1500×1000 filtered preview proxy, factor 4

Depth / case Base ms (range) Candidate ms (range) Speedup
U8 medium 66.9 (63.1–82.0) 55.9 (54.9–59.0) 1.20×
U8 small 29.4 (27.2–32.9) 28.0 (26.4–29.9) 1.05×
U8 large 179.4 (157.0–205.6) 132.8 (116.7–149.6) 1.35×
U8 wide_focus 52.1 (46.7–72.0) 40.4 (38.9–44.0) 1.29×
U8 sharp_transition 64.3 (54.6–75.4) 44.2 (41.2–45.7) 1.45×
U8 focused 17.9 (16.7–18.7) 15.0 (14.6–15.1) 1.20×
U8 blurred 37.4 (36.1–48.0) 29.5 (28.2–33.0) 1.27×
U16 medium 89.2 (81.3–95.8) 58.1 (56.0–59.0) 1.54×
F32 medium 93.7 (83.1–102.4) 60.3 (57.9–65.8) 1.55×

128×96 small-image overhead

Depth / case Base ms (range) Candidate ms (range) Speedup
U8 medium 30.25 (30.09–31.83) 18.14 (17.69–19.34) 1.67×
U8 small 3.93 (3.85–3.99) 3.15 (3.13–3.22) 1.25×
U8 large 278.90 (273.62–313.48) 145.97 (142.65–148.51) 1.91×
U8 wide_focus 26.08 (25.29–27.79) 15.48 (15.38–15.69) 1.69×
U8 sharp_transition 31.12 (30.48–46.89) 17.40 (17.35–17.65) 1.79×
U8 focused 1.56 (1.50–1.64) 1.07 (1.06–1.09) 1.46×
U8 blurred 7.57 (6.82–11.06) 6.19 (6.05–7.02) 1.22×
U16 medium 30.99 (30.58–32.48) 17.94 (17.61–18.07) 1.73×
F32 medium 31.53 (30.28–48.73) 17.90 (17.82–18.74) 1.76×

All nine 24 MP cases have separated observed ranges. Gains are modest for the focused and saturated stress fields. At 36 MP the saturated field's ranges overlap; no convincing gain is established there. Default blur 15 on the preview proxy also has overlapping ranges: its 1.05× pooled median change is inconclusive. Medium/large proxy cases improve 1.20/1.35×, and U16/F32 medium proxy cases improve 1.54/1.55×. The small-image saturated case is also inconclusive; the remaining small-image cases improve 1.25–1.91× with separated ranges.

There is meaningful drift, so the pooled p50 must not imply precise repeatability: 24 MP U8 medium base A/B medians are 1909.1/1564.3 ms (candidate 1241.4), giving 1.54/1.26× against the individual rounds. The 36 MP large case's base A/B medians are 7234.7/9170.0 ms (candidate 6176.2), giving 1.17/1.48×. Process CPU sampling during the repeat found the benchmark using about 7.6 cores and no other heavy CPU consumer. The report retains the full variation rather than attributing it to a proven cause.

For one complete 24 MP suite (fixture construction, warmups and all depths/cases included), sampled Windows PeakWorkingSet64 is 2,162,429,952 bytes for the base and 2,044,227,584 for the candidate, about 5.5% lower (2.01 → 1.90 GiB). This is an allocator-dependent process high-water measurement, not a per-filter live-memory bound. Allocation counts are not instrumented; the source-copy, sigma-map, level-metadata, output and level-buffer allocations remain, and full-window buffer capacity is retained. Smaller populated levels and scratch account for the structural reduction in work. Neither SIMD nor FFT crossover performance is claimed; no adaptive threshold is introduced.

Maximum observed finite pixel error is zero in the differential tests. Every pixel must match, so localized band-edge, seam, opacity or aliasing errors cannot hide in an average-error metric. NaN classifications match. The NaN blur crash regression intentionally differs from the starting kernel by returning identity.

Validation and remaining limits

The full photocraft-algo suite passes: 425 tests, 18 ignored, no failures. All six Tilt-Shift differential/regression tests also pass in release. Strict all-targets Clippy passes for the algorithm crate. The existing engine filter integration group passes all 16 tests, including selection, repeat, undo and 16/32-bit filtering. cargo xtask layers reports 29 crates with no violations; cargo xtask wasm passes the required workspace packages and the HEIF-enabled codec check. Regenerating the scorecard produces identical content.

cargo xtask perf --quick exits successfully; all five configured quick benches (perf_scenarios, type_bench, layout_bench, camera_raw_bench, bevel_bench) complete. Reports are retained locally in target/agent-tilt-shift/perf/{results.json,summary.md}. The regression comparison is skipped because the checked-in baseline is from an Apple M4 Pro/macOS machine class. Quick mode does not enforce full-size budgets. The layout harness still reports unrelated frame overruns; completing this gate does not establish broader responsiveness or budget compliance. Formatting and Git whitespace checks pass.

Algorithm checks also pass for aarch64/x86_64 macOS, aarch64 Windows and aarch64/x86_64 Linux. These are compilation checks from Windows, not executions or performance measurements on those platforms. Native timing evidence is limited to this Windows x86_64 machine. cargo xtask test-corpus --changed correctly skips its suites because no format, IO, compositor, GPU or text crate changed. Command registrations and UI code are unchanged.

The measurements establish equivalence to the starting PhotoCraft kernel, not a new Photoshop oracle or broader parity increase. Cold startup, actual GUI input/frame latency, allocation counts and other platforms' native performance remain unmeasured. No performance budget, scorecard checklist or parity floor is raised from these local results. The shared craftrules checkout was unavailable at the documented paths; the repository's own contributor and validation rules were followed.