mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-27 19:47:35 -06:00
M5.2d: geometry resample on the GPU
The source is uploaded once per view (kept with the GPU stages) and resampled on the device: orientation as an integer pixel map, the Mitchell area prefilter with the CPU's exact taps, then one bilinear resample through the crop/straighten/flip affine or through the full warp (inverse perspective, manual distortion, embedded DNG WarpRectilinear per plane, lateral CA, manual and embedded vignetting gain, blank outside the source). `Frame::sample_plan` (new) decides prefilter size and mode for both renderers; sources larger than the device's buffer limit are still resampled on the CPU. Equivalence: all 8 orientations with and without crop/straighten/flip, embedded lens + perspective: max 1 LSB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2dfd65b39a
commit
07c3f0261c
+176
-11
@@ -5,7 +5,9 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use lightcraft_develop::DevelopSettings;
|
||||
use lightcraft_geom::Orientation;
|
||||
use lightcraft_pipeline::finish::{FinishParams, MASK_TERMS, mask_terms};
|
||||
use lightcraft_pipeline::geometry::SampleMode;
|
||||
use lightcraft_pipeline::{Plan, RenderRequest, Rendered, SourceInfo, local};
|
||||
use lightcraft_raster::resample::Filter;
|
||||
use lightcraft_raster::{Histogram, Plane, Rgb32f, Rgba8};
|
||||
@@ -18,6 +20,8 @@ use crate::params::{Present, finish_block};
|
||||
#[derive(Default)]
|
||||
pub struct GpuStages {
|
||||
entries: Mutex<Vec<Entry>>,
|
||||
/// The uploaded source (a view renders one photo at several sizes).
|
||||
source: Mutex<Option<(Arc<Rgb32f>, Arc<Buf>)>>,
|
||||
}
|
||||
|
||||
const CAPACITY: usize = 2;
|
||||
@@ -56,6 +60,19 @@ impl GpuStages {
|
||||
}
|
||||
}
|
||||
|
||||
fn source(&self, src: &Arc<Rgb32f>, upload: impl FnOnce() -> Buf) -> Arc<Buf> {
|
||||
let mut g = self.source.lock().unwrap_or_else(|e| e.into_inner());
|
||||
match &*g {
|
||||
Some((s, b)) if Arc::ptr_eq(s, src) => b.clone(),
|
||||
_ => {
|
||||
*g = None; // free the previous photo's buffer first
|
||||
let b = Arc::new(upload());
|
||||
*g = Some((src.clone(), b.clone()));
|
||||
b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lock(&self) -> std::sync::MutexGuard<'_, Vec<Entry>> {
|
||||
self.entries.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
@@ -213,6 +230,127 @@ fn guided_fast(cx: &mut Cx<'_>, p: &Buf, w: usize, h: usize, sigma: f32, eps: f3
|
||||
q
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Geometry (`Frame::sample`)
|
||||
|
||||
/// Integer pixel map `(x, y) → (m0·x + m1·y + m2, m3·x + m4·y + m5)` from an oriented image back
|
||||
/// to the source of `w × h` (`Image::oriented`: optional horizontal flip, then quarter turns).
|
||||
pub(crate) fn orient_map(o: Orientation, w: usize, h: usize) -> [i32; 6] {
|
||||
let (flip, turns) = o.to_parts();
|
||||
let (w, h) = (w as i32, h as i32);
|
||||
let mut ops: Vec<fn(i32, i32) -> [i32; 6]> = Vec::new();
|
||||
if flip {
|
||||
ops.push(|w, _| [-1, 0, w - 1, 0, 1, 0]);
|
||||
}
|
||||
let cw: fn(i32, i32) -> [i32; 6] = |_, h| [0, 1, 0, -1, 0, h - 1];
|
||||
let half: fn(i32, i32) -> [i32; 6] = |w, h| [-1, 0, w - 1, 0, -1, h - 1];
|
||||
match turns {
|
||||
1 => ops.push(cw),
|
||||
2 => ops.push(half),
|
||||
3 => {
|
||||
ops.push(half);
|
||||
ops.push(cw);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// compose: T ← T ∘ op, tracking each op's input size
|
||||
let mut t = [1, 0, 0, 0, 1, 0];
|
||||
let (mut cw_, mut ch_) = (w, h);
|
||||
for op in ops {
|
||||
let o = op(cw_, ch_);
|
||||
t = [
|
||||
t[0] * o[0] + t[1] * o[3],
|
||||
t[0] * o[1] + t[1] * o[4],
|
||||
t[0] * o[2] + t[1] * o[5] + t[2],
|
||||
t[3] * o[0] + t[4] * o[3],
|
||||
t[3] * o[1] + t[4] * o[4],
|
||||
t[3] * o[2] + t[4] * o[5] + t[5],
|
||||
];
|
||||
if o[0] == 0 {
|
||||
(cw_, ch_) = (ch_, cw_);
|
||||
}
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn affine_bits(a: &lightcraft_geom::Affine) -> [u32; 6] {
|
||||
a.0.map(|v| (v as f32).to_bits())
|
||||
}
|
||||
|
||||
/// The `sample_warp` kernel's parameters (layout documented in `geom.wgsl`).
|
||||
fn warp_params(
|
||||
wp: &lightcraft_pipeline::optics::Warp,
|
||||
base: (usize, usize),
|
||||
out: (usize, usize),
|
||||
o2t: &lightcraft_geom::Affine,
|
||||
sx: f64,
|
||||
sy: f64,
|
||||
) -> Vec<u32> {
|
||||
let f = |v: f64| (v as f32).to_bits();
|
||||
let mut p = vec![base.0 as u32, base.1 as u32, out.0 as u32, out.1 as u32];
|
||||
p.extend(affine_bits(o2t));
|
||||
p.extend([f(sx), f(sy), f(wp.w), f(wp.h)]);
|
||||
let persp = wp.persp != lightcraft_geom::Homography::IDENTITY;
|
||||
p.push(persp as u32);
|
||||
p.extend(wp.persp_inv.0.map(f));
|
||||
p.push(f(wp.k1));
|
||||
p.extend(wp.ca.map(f));
|
||||
p.push(f(wp.lens_dist));
|
||||
let warp = wp.lens.and_then(|l| l.warp);
|
||||
p.push(warp.is_some() as u32);
|
||||
let w = warp.unwrap_or_default();
|
||||
p.extend(w.planes.iter().flatten().map(|v| f(*v)));
|
||||
p.extend([f(w.center.x), f(w.center.y), f(w.radius)]);
|
||||
p.extend([f(wp.vig_stops), f(wp.vig_power), f(wp.lens_vig)]);
|
||||
let vig = wp.lens.and_then(|l| l.vignette);
|
||||
p.push(vig.is_some() as u32);
|
||||
let v = vig.unwrap_or_default();
|
||||
p.extend(v.k.map(f));
|
||||
p.extend([f(v.center.x), f(v.center.y), f(v.radius)]);
|
||||
p.push(wp.per_channel() as u32);
|
||||
p.push(wp.has_gain() as u32);
|
||||
debug_assert_eq!(p.len(), 65);
|
||||
p
|
||||
}
|
||||
|
||||
/// Resample the source into the output frame (`Frame::sample`). May return the source buffer
|
||||
/// itself when the frame is the identity.
|
||||
fn sample(cx: &mut Cx<'_>, src: &Arc<Rgb32f>, src_buf: Arc<Buf>, plan: &Plan<'_>) -> Arc<Buf> {
|
||||
let (w, h) = (plan.w, plan.h);
|
||||
let fr = &plan.frame;
|
||||
let sp = fr.sample_plan(src.width, src.height, w, h);
|
||||
let oriented = if fr.orient == Orientation::Normal {
|
||||
src_buf
|
||||
} else {
|
||||
let out = cx.gpu.buffer(sp.ow * sp.oh * 3);
|
||||
let mut p = vec![src.width as u32, src.height as u32, sp.ow as u32, sp.oh as u32];
|
||||
p.extend(orient_map(fr.orient, src.width, src.height).map(|v| v as u32));
|
||||
cx.run("orient", &p, &[Some(&src_buf), Some(&out)], groups2(sp.ow, sp.oh, [16, 16]));
|
||||
Arc::new(out)
|
||||
};
|
||||
let (base, (bw, bh)) = match sp.prefilter {
|
||||
Some(d) => (Arc::new(resize(cx, &oriented, (sp.ow, sp.oh), d, 3, Filter::Mitchell)), d),
|
||||
None => (oriented, (sp.ow, sp.oh)),
|
||||
};
|
||||
match &sp.mode {
|
||||
SampleMode::Copy => base,
|
||||
SampleMode::Affine(xf) => {
|
||||
let out = cx.gpu.buffer(w * h * 3);
|
||||
let mut p = vec![bw as u32, bh as u32, w as u32, h as u32];
|
||||
p.extend(affine_bits(xf));
|
||||
cx.run("sample_affine", &p, &[Some(&base), Some(&out)], groups2(w, h, [16, 16]));
|
||||
Arc::new(out)
|
||||
}
|
||||
SampleMode::Warp(o2t) => {
|
||||
let wp = fr.warp.as_ref().expect("warp mode has a warp");
|
||||
let out = cx.gpu.buffer(w * h * 3);
|
||||
let p = warp_params(wp, (bw, bh), (w, h), o2t, sp.sx, sp.sy);
|
||||
cx.run("sample_warp", &p, &[Some(&base), Some(&out)], groups2(w, h, [16, 16]));
|
||||
Arc::new(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// The render
|
||||
|
||||
@@ -248,7 +386,7 @@ pub fn render(
|
||||
let plan = lightcraft_pipeline::plan(src, info, s, req);
|
||||
let (w, h) = (plan.w, plan.h);
|
||||
let n = w * h;
|
||||
if !gpu.fits(n * 3) || !gpu.fits(src.data.len() * 3) {
|
||||
if !gpu.fits(n * 3) {
|
||||
return None;
|
||||
}
|
||||
let s = &*plan.settings;
|
||||
@@ -256,10 +394,23 @@ pub fn render(
|
||||
let mut host = Host::default();
|
||||
let mut cx = Cx::new(gpu);
|
||||
|
||||
// 1. geometry
|
||||
// 1. geometry (on the CPU when the source exceeds the device's buffer limit)
|
||||
let sampled = match &cached {
|
||||
Some(e) => e.sampled.clone(),
|
||||
None => Arc::new(sample(&mut cx, src, &plan, &mut host)),
|
||||
None if gpu.fits(src.data.len() * 3) => {
|
||||
let upload = || gpu.upload(rgb_words(src));
|
||||
let src_buf = match stages {
|
||||
Some(c) => c.source(src, upload),
|
||||
None => Arc::new(upload()),
|
||||
};
|
||||
sample(&mut cx, src, src_buf, &plan)
|
||||
}
|
||||
None => {
|
||||
let img = plan.frame.sample(src, w, h);
|
||||
let b = Arc::new(gpu.upload(rgb_words(&img)));
|
||||
host.sampled = Some(img);
|
||||
b
|
||||
}
|
||||
};
|
||||
lap("sample", &mut t);
|
||||
|
||||
@@ -314,14 +465,6 @@ pub fn render(
|
||||
Some(Rendered { image, histogram })
|
||||
}
|
||||
|
||||
/// Resample the source into the output frame.
|
||||
fn sample(cx: &mut Cx<'_>, src: &Arc<Rgb32f>, plan: &Plan<'_>, host: &mut Host) -> Buf {
|
||||
let img = plan.frame.sample(src, plan.w, plan.h);
|
||||
let b = cx.gpu.upload(rgb_words(&img));
|
||||
host.sampled = Some(img);
|
||||
b
|
||||
}
|
||||
|
||||
/// The white-balanced, retouched, denoised image.
|
||||
fn linear(cx: &mut Cx<'_>, sampled: &Buf, info: &SourceInfo, plan: &Plan<'_>, host: &mut Host) -> Buf {
|
||||
let (w, h) = (plan.w, plan.h);
|
||||
@@ -553,3 +696,25 @@ fn masks(cx: &mut Cx<'_>, lin: &Buf, prep: &Prep, plan: &Plan<'_>, host: &mut Ho
|
||||
let terms = list.iter().map(|m| mask_terms(&m.adjust)).collect();
|
||||
(Some(alpha), terms)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn orient_map_matches_image_oriented() {
|
||||
use Orientation::*;
|
||||
let img = Rgb32f::from_fn(5, 3, |x, y| [x as f32, y as f32, 0.0]);
|
||||
for o in [Normal, Rotate90, Rotate180, Rotate270, FlipH, Transverse, FlipV, Transpose] {
|
||||
let r = img.oriented(o);
|
||||
let m = orient_map(o, 5, 3);
|
||||
for y in 0..r.height {
|
||||
for x in 0..r.width {
|
||||
let (x, y) = (x as i32, y as i32);
|
||||
let (sx, sy) = (m[0] * x + m[1] * y + m[2], m[3] * x + m[4] * y + m[5]);
|
||||
assert_eq!(r.get(x as usize, y as usize), img.get(sx as usize, sy as usize), "{o:?} at {x},{y}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user