Files
lightcraft/crates/gpu/src/params.rs
T
Brandon ThomasandClaude Opus 5.5 c7a1e3e2f6 M5.2a: lightcraft-gpu crate (wgpu compute) with the per-pixel stage on the GPU
New L3 crate `lightcraft-gpu`: a wgpu device (Metal/Vulkan/DX12; none on wasm32),
WGSL kernels compiled once, buffer/dispatch/readback helpers, and a renderer that
runs the per-pixel stage (tone map, highlights/shadows, clarity/texture/sharpen,
dehaze, local mask terms, OkLCh colour ops, grading, vignette, gamut map, sRGB
encode, curves, grain) on the device. Earlier stages still run on the CPU and are
uploaded (per-stage hybrid). Stage results are cached per view next to the CPU
StageCache (new `StageCache::extension`).

The CPU pipeline stays the oracle and shares its resolved parameters with the
kernels: `finish::FinishParams` / `mask_terms`, `pipeline::plan` (stage-cache keys),
`local::plane_sigmas` / `nr_params` / `wb_matrix_for`, exact LUTs and OkLab
matrices exported for the shader prelude. Equivalence tests (skip without an
adapter): mean |d| < 0.5 LSB, max <= 3 LSB; measured max 1 LSB on Apple M4 Pro.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:29:54 -04:00

212 lines
5.8 KiB
Rust

//! Parameter blocks: the per-pixel stage's [`FinishParams`] packed into the 32-bit words the
//! `finish` kernel reads (`F_*` constants, generated from [`FIELDS`]), plus its auxiliary tables.
use lightcraft_develop::VignetteStyle;
use lightcraft_pipeline::finish::{FinishParams, MASK_TERMS, srgb_lut};
/// Entries per tone-curve table (`lightcraft_pipeline::finish` builds them at this size).
pub const CURVE_N: u32 = 1024;
/// Field name and number of 32-bit words.
const FIELDS: &[(&str, usize)] = &[
("W", 1),
("H", 1),
("NMASK", 1),
("MASK_OFF", 1),
("SRGB_OFF", 1),
("CURVE_OFF", 1),
("CURVES", 1),
("GAIN", 1),
("EV", 1),
("AIR", 1),
("AIR_PRE", 1),
("HL", 1),
("SH", 1),
("CLAR", 1),
("TEX", 1),
("DEHAZE", 1),
("SHARPEN", 1),
("SHARPEN_MASK", 1),
("HAS_CLAR", 1),
("HAS_TEX", 1),
("HAS_DARK", 1),
("OPS_IDENTITY", 1),
("VIBRANCE", 1),
("SATURATION", 1),
("MIXER", 1),
("MIX_HUE", 8),
("MIX_SAT", 8),
("MIX_LUM", 8),
("BW", 1),
("BW_MIX", 8),
("GRADING", 1),
("WHEELS", 12),
("BLENDING", 1),
("BALANCE", 1),
("SKIN", 1),
("BANDH", 8),
("VIG", 1),
("VIG_AMOUNT", 1),
("VIG_START", 1),
("VIG_WIDTH", 1),
("VIG_ASPECT_MIX", 1),
("VIG_POWER", 1),
("VIG_HL", 1),
("VIG_STYLE", 1),
("GRAIN", 1),
("GRAIN_AMT", 1),
("GRAIN_SC", 1),
("GRAIN_ROUGH", 1),
("GRAIN_SEED", 1),
("GRAIN_AFF", 6),
];
/// `(name, index)` of every field (for the WGSL constants).
pub fn finish_fields() -> Vec<(&'static str, usize)> {
let mut i = 0;
FIELDS
.iter()
.map(|(n, len)| {
let r = (*n, i);
i += len;
r
})
.collect()
}
fn index(name: &str) -> usize {
let mut i = 0;
for (n, len) in FIELDS {
if *n == name {
return i;
}
i += len;
}
panic!("unknown finish field {name}")
}
/// A parameter block being filled by name.
pub struct Block(pub Vec<u32>);
impl Block {
fn new() -> Block {
Block(vec![0; FIELDS.iter().map(|f| f.1).sum()])
}
fn f(&mut self, name: &str, v: f32) {
let i = index(name);
self.0[i] = v.to_bits();
}
fn u(&mut self, name: &str, v: u32) {
let i = index(name);
self.0[i] = v;
}
fn b(&mut self, name: &str, v: bool) {
self.u(name, v as u32);
}
fn fs(&mut self, name: &str, v: &[f32]) {
let i = index(name);
for (k, x) in v.iter().enumerate() {
self.0[i + k] = x.to_bits();
}
}
}
/// Which optional planes are bound.
pub struct Present {
pub clarity: bool,
pub texture: bool,
pub dark: bool,
}
/// The `finish` kernel's parameter block and auxiliary table (tone LUT | sRGB LUT | curve LUTs |
/// mask terms) for `fp` with `masks` (their local adjustments' terms).
pub fn finish_block(fp: &FinishParams, masks: &[[f32; MASK_TERMS]], present: &Present) -> (Vec<u32>, Vec<f32>) {
let mut aux: Vec<f32> = fp.tone.lut().to_vec();
let srgb_off = aux.len();
aux.extend_from_slice(&srgb_lut()[..]);
let curve_off = aux.len();
if let Some(c) = &fp.curves {
for l in c {
assert_eq!(l.v.len(), CURVE_N as usize);
aux.extend_from_slice(&l.v);
}
}
let mask_off = aux.len();
for m in masks {
aux.extend_from_slice(m);
}
let mut p = Block::new();
p.u("W", fp.w as u32);
p.u("H", fp.h as u32);
p.u("NMASK", masks.len() as u32);
p.u("MASK_OFF", mask_off as u32);
p.u("SRGB_OFF", srgb_off as u32);
p.u("CURVE_OFF", curve_off as u32);
p.b("CURVES", fp.curves.is_some());
p.f("GAIN", fp.gain);
p.f("EV", fp.ev);
p.f("AIR", fp.air);
p.f("AIR_PRE", fp.air_pre);
p.f("HL", fp.hl);
p.f("SH", fp.sh);
p.f("CLAR", fp.clar);
p.f("TEX", fp.tex);
p.f("DEHAZE", fp.dehaze);
p.f("SHARPEN", fp.sharpen);
p.f("SHARPEN_MASK", fp.sharpen_mask);
p.b("HAS_CLAR", present.clarity);
p.b("HAS_TEX", present.texture);
p.b("HAS_DARK", present.dark);
let ops = &fp.ops;
p.b("OPS_IDENTITY", ops.is_identity());
p.f("VIBRANCE", ops.vibrance);
p.f("SATURATION", ops.saturation);
p.b("MIXER", ops.mixer);
p.fs("MIX_HUE", &ops.hue);
p.fs("MIX_SAT", &ops.sat);
p.fs("MIX_LUM", &ops.lum);
p.b("BW", ops.bw.is_some());
p.fs("BW_MIX", &ops.bw.unwrap_or([0.0; 8]));
if let Some((wheels, blending, balance)) = &ops.grading {
p.b("GRADING", true);
let w: Vec<f32> = wheels.iter().flat_map(|k| [k.a, k.b, k.lum]).collect();
p.fs("WHEELS", &w);
p.f("BLENDING", *blending);
p.f("BALANCE", *balance);
}
p.f("SKIN", ops.skin);
p.fs("BANDH", lightcraft_pipeline::colorops::band_hues());
if let Some(v) = &fp.vig {
p.b("VIG", true);
p.f("VIG_AMOUNT", v.amount);
p.f("VIG_START", v.start);
p.f("VIG_WIDTH", v.width);
p.f("VIG_ASPECT_MIX", v.aspect_mix);
p.f("VIG_POWER", v.power);
p.f("VIG_HL", v.highlights);
p.u(
"VIG_STYLE",
match v.style {
VignetteStyle::HighlightPriority => 1,
VignetteStyle::PaintOverlay => 2,
VignetteStyle::ColorPriority => 0,
},
);
}
if let Some((amt, cell, rough, seed)) = fp.grain {
p.b("GRAIN", true);
p.f("GRAIN_AMT", amt);
p.f("GRAIN_SC", fp.px_per_long as f32 / cell);
p.f("GRAIN_ROUGH", rough);
p.u("GRAIN_SEED", seed);
let long = fp.ow.max(fp.oh);
let [a, b, c, d, e, f] = fp.out_to_norm.0;
let (kx, ky) = (fp.ow / long, fp.oh / long);
p.fs("GRAIN_AFF", &[(a * kx) as f32, (b * ky) as f32, (c * kx) as f32, (d * ky) as f32, (e * kx) as f32, (f * ky) as f32]);
}
(p.0, aux)
}