Files
fjamespriceandClaude Opus 5.5 6025c504cf perf(camera-raw): develop Dehaze, Clarity and Texture on the GPU in the preview (#6916)
Step 3 of #6916. Dehaze, Clarity and Texture run as fragment passes
(box, minimum and guided filters) around step 2's Shadows and
Highlights, so every setting but Detail develops on the GPU with one
readback.

- algo: camera_raw::spatial resolves Dehaze, Clarity and Texture per
  frame; develop builds them the same way (130 output hashes unchanged,
  same speed). PerPixel gains the three stages, per_pixel() refuses only
  Detail, and Whites/Blacks are measured after any spatial stage.
- gpu: camera_raw_filters.wgsl; the tone frame's pyramid is made on
  demand; plans record four segments around the main shader's passes.
  Dehaze's light uses two exact selections (cutoff value, then cutoff
  index among its ties). Its <=768 px copy comes from the CPU
  (Dehaze::cells, develop's own code), cached per source and white
  balance: on WARP the GPU's copy split CPU ties at the cutoff and moved
  the light by 5.7e-3.
- GPU vs CPU at most 7.6e-5 (Vulkan, DX12, WARP), tolerance 1/4096.
  858x572: Dehaze +40 6.0 ms vs 29 ms, Clarity +50 3.5 vs 16 ms,
  Texture +50 3.4 vs 10 ms, everything 19 ms vs 650-690 ms (Vulkan).

Part of #6916.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 02:42:52 -05:00

132 lines
6.2 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Camera Raw's preview develop on the 858×572 proxy of a 6000×4000 image: the CPU develop
//! against the GPU passes (upload, render, readback), for per-pixel settings and for the spatial
//! stages (Dehaze, Shadows and Highlights, Clarity, Texture) with them. "Source uploaded every
//! time" also makes Dehaze's copy on the CPU every time, as a white balance drag does.
//! `cargo run --release -p photocraft-gpu --example bench_camera_raw [-- --quick]`
use std::time::Instant;
use photocraft_algo::camera_raw::{CameraRaw, Wheel, develop, per_pixel, white_black_points};
use photocraft_gpu::CameraRawDevelop;
fn median(mut v: Vec<f64>) -> f64 {
v.sort_by(f64::total_cmp);
v[v.len() / 2]
}
fn time(runs: usize, mut f: impl FnMut()) -> f64 {
f();
median(
(0..runs)
.map(|_| {
let t = Instant::now();
f();
t.elapsed().as_secs_f64() * 1e3
})
.collect(),
)
}
/// [`time`] for a call that can fail: its first error ends the benchmark instead of being timed.
fn try_time<E>(runs: usize, mut f: impl FnMut() -> Result<(), E>) -> Result<f64, E> {
let mut failed = None;
let ms = time(runs, || {
if failed.is_none() {
failed = f().err();
}
});
failed.map_or(Ok(ms), Err)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let runs = if std::env::args().any(|a| a == "--quick") { 5 } else { 40 };
let (w, h) = (858usize, 572usize);
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
let options = wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, ..Default::default() };
let adapter = pollster::block_on(instance.request_adapter(&options))?;
let (device, queue) = pollster::block_on(adapter.request_device(&Default::default()))?;
let info = adapter.get_info();
println!("adapter: {} ({:?}), proxy {w}x{h}, median of {runs}", info.name, info.backend);
let built = Instant::now();
let mut pass = CameraRawDevelop::new(&device)?;
println!("pipeline build: {:.1} ms", built.elapsed().as_secs_f64() * 1e3);
// A photo-like proxy: gradients, noise and bright patches, so the filter has edges to keep.
let src: Vec<[f32; 4]> = (0..w * h)
.map(|i| {
let (x, y) = ((i % w) as f32 / w as f32, (i / w) as f32 / h as f32);
let n = ((i as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) >> 40) as f32 / (1u64 << 24) as f32;
if (i / w) % 90 < 25 && (i % w) % 130 < 40 {
return [0.95, 0.92, 0.88, 1.0];
}
let c = [0.02 + 0.8 * x * x, 0.05 + 0.6 * y, 0.6 - 0.5 * x * y].map(|v| (v + (n - 0.5) * 0.05).clamp(0.0, 1.0));
[c[0], c[1], c[2], 1.0]
})
.collect();
let scale = w as f32 / 6000.0;
let mut hsl = [0.0; 8];
hsl[1] = 30.0;
hsl[5] = -40.0;
let per_pixel_stages = CameraRaw {
temperature: 20.0,
tint: 5.0,
exposure: 0.4,
contrast: 25.0,
whites: 30.0,
blacks: -20.0,
vibrance: 30.0,
saturation: 10.0,
curve_lights: 20.0,
point_curve: vec![[0.0, 8.0], [128.0, 140.0], [255.0, 250.0]],
hsl_hue: hsl,
hsl_sat: hsl,
hsl_lum: hsl,
grade_shadows: Wheel { hue: 210.0, sat: 30.0, lum: 0.0 },
grade_highlights: Wheel { hue: 40.0, sat: 20.0, lum: 5.0 },
vignette_amount: -30.0,
grain_amount: 25.0,
pixel_scale: scale,
..Default::default()
};
let sets = [
(
"Basic (WB, Exposure, Contrast, Whites, Blacks)",
CameraRaw { temperature: 20.0, exposure: 0.4, contrast: 25.0, whites: 30.0, blacks: -20.0, pixel_scale: scale, ..Default::default() },
),
("every per-pixel stage", per_pixel_stages.clone()),
("Shadows +50", CameraRaw { shadows: 50.0, pixel_scale: scale, ..Default::default() }),
("Highlights -50", CameraRaw { highlights: -50.0, pixel_scale: scale, ..Default::default() }),
("Shadows +50, Highlights -50", CameraRaw { shadows: 50.0, highlights: -50.0, pixel_scale: scale, ..Default::default() }),
("Shadows +50, Highlights -50, every per-pixel stage", CameraRaw { shadows: 50.0, highlights: -50.0, ..per_pixel_stages.clone() }),
("Clarity +50", CameraRaw { clarity: 50.0, pixel_scale: scale, ..Default::default() }),
("Texture +50", CameraRaw { texture: 50.0, pixel_scale: scale, ..Default::default() }),
("Dehaze +40", CameraRaw { dehaze: 40.0, pixel_scale: scale, ..Default::default() }),
("Clarity +50, Texture +50, Dehaze +40, every per-pixel stage", CameraRaw { clarity: 50.0, texture: 50.0, dehaze: 40.0, ..per_pixel_stages.clone() }),
(
"Clarity, Texture, Dehaze, Shadows, Highlights, every per-pixel stage",
CameraRaw { clarity: 50.0, texture: 50.0, dehaze: 40.0, shadows: 50.0, highlights: -50.0, ..per_pixel_stages },
),
];
for (name, p) in &sets {
let toned = p.spatial();
let cpu = time(if toned { runs.min(10) } else { runs }, || {
let mut px = src.clone();
develop(&mut px, w, h, p, false);
});
let points = white_black_points(&src, w, h, p);
let stages = per_pixel(p, w, h, false, points).ok_or("not developed on the GPU")?;
let size = [w as u32, h as u32];
let upload = try_time(runs, || pass.develop(&device, &queue, None, &src, None, size, &stages).map(drop))?;
let cached = try_time(runs, || pass.develop(&device, &queue, None, &src, Some(1), size, &stages).map(drop))?;
println!("{name}:");
println!(" CPU develop {cpu:7.2} ms");
println!(" GPU, source uploaded every time {upload:7.2} ms");
println!(" GPU, source cached (slider drag) {cached:7.2} ms");
if !toned && (p.whites != 0.0 || p.blacks != 0.0) {
let points_ms = time(runs, || {
std::hint::black_box(white_black_points(&src, w, h, p));
});
println!(" Whites/Blacks points on the CPU {points_ms:7.2} ms (cached per WB/Exposure/Contrast in the dialog)");
}
}
Ok(())
}