Brighten her face without touching the sky behind her
A mask layer is an ordinary develop chain plus a rule about where it applies. Nothing in the chain knows it is being masked, so every operation that works globally now works locally and a newly declared op in `ops/` arrives with local support already done. The composer emits each layer after the global chain and before the conversion out of camera space, which is what a photographer means by "and *then* lift the shadows on her face". Op fragments write to a `c` they expect to own, so a layer block shadows it and copies the result back out through a carrier — assigning the outer one from inside is impossible precisely because it is shadowed. The fused dispatch survives: three global adjustments and two masked ones remain one shader, one read, one write. Masks rasterise on the GPU and never exist in CPU memory (ARCH §5.4). That is the whole reason darktable's brush masks lag, and it is architectural rather than tuning, so it is not a thing to inherit and fix later. The rasteriser is a render pass rather than the compute shader it obviously wants to be, and the format is why: R8Unorm is not a core storage format, so a compute path has to widen masks to four bytes per pixel — 768 MB across eight layers of a 24 MP export, against 192 MB at one byte. A colour attachment takes R8Unorm happily. The array slice comes from the attached view, so no slot uniform exists to disagree with where the pass writes. Region masks index a compacted label field rather than the watershed's raw basin roots, because a root is a sparse index into pixel space and indexing a per-region array by one would need a table the size of the image. Changing a selection then costs a few kilobytes, not a re-upload. Stored as region ids, not as pixels: diffable, mergeable per-field under FR-NC-9, and cheap in a sidecar. The ids only mean anything alongside the segmentation that produced them, so each layer carries that signature and is treated as stale rather than applied when it does not match — a confidently wrong mask being much worse than an absent one. Seven device tests render actual frames and read them back. The unit tests either side check halves that would both pass if the two agreed with each other and were both wrong; a mask sampled with x and y swapped satisfies them and fails these.
This commit is contained in:
@@ -26,6 +26,7 @@ use dr_types::{ColourSpace, Transfer};
|
||||
|
||||
use crate::descriptor::{OpDescriptor, ParamId, Presentation};
|
||||
use crate::framing::{Framing, FRAMING_UNIFORM_FIELDS};
|
||||
use crate::mask::MaskStack;
|
||||
|
||||
/// What an operation's parameters affect, for cache invalidation scoping.
|
||||
///
|
||||
@@ -188,6 +189,27 @@ pub fn compose_with_framing(
|
||||
ops: &[Box<dyn Operation>],
|
||||
framing: &Framing,
|
||||
output: ColourSpace,
|
||||
) -> ComposedShader {
|
||||
compose_full(ops, framing, output, &MaskStack::new())
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3
|
||||
/// Compose the global chain, the framing, and the local adjustments.
|
||||
///
|
||||
/// Mask layers are emitted **after** every global operation and before the
|
||||
/// conversion out of camera space, so a local exposure acts on the tones the
|
||||
/// global chain settled on — which is what a photographer means by "and then
|
||||
/// lift the shadows on her face".
|
||||
///
|
||||
/// The fused-dispatch property survives: three global adjustments and two
|
||||
/// masked ones are still one shader, one read and one write. The masks
|
||||
/// themselves arrive as a pre-rasterised texture array (ARCH §5.4), so a
|
||||
/// slider drag over a mask recompiles a shader but re-rasterises nothing.
|
||||
pub fn compose_full(
|
||||
ops: &[Box<dyn Operation>],
|
||||
framing: &Framing,
|
||||
output: ColourSpace,
|
||||
masks: &MaskStack,
|
||||
) -> ComposedShader {
|
||||
let active: Vec<&dyn Operation> = ops
|
||||
.iter()
|
||||
@@ -265,6 +287,21 @@ pub fn compose_with_framing(
|
||||
let _ = writeln!(body, " }}");
|
||||
}
|
||||
|
||||
// The local adjustments, after every global one: a masked exposure should
|
||||
// act on the tones the global chain arrived at, not on the ones it started
|
||||
// from. Their uniforms follow the global ops' in the block for the same
|
||||
// reason those follow framing's — slot order is emission order, and
|
||||
// nothing addresses a slot by number.
|
||||
let layers = crate::mask::compose_layers(masks);
|
||||
uniform_fields.push_str(&layers.uniform_fields);
|
||||
uniform_values.extend_from_slice(&layers.uniform_values);
|
||||
body.push_str(&layers.body);
|
||||
for h in &layers.helpers {
|
||||
if !helpers.iter().any(|existing| existing.name == h.name) {
|
||||
helpers.push(*h);
|
||||
}
|
||||
}
|
||||
|
||||
// Pad the uniform block to a 16-byte boundary. A struct whose size is not
|
||||
// a multiple of 16 is rejected by the WGSL uniform address space rules.
|
||||
let pad = (4 - (uniform_values.len() % 4)) % 4;
|
||||
@@ -308,6 +345,12 @@ struct Params {{
|
||||
@group(0) @binding(0) var source: texture_2d<f32>;
|
||||
@group(0) @binding(1) var<uniform> u: Params;
|
||||
@group(0) @binding(2) var output: texture_storage_2d<rgba8unorm, write>;
|
||||
// The local adjustment masks, one array layer each, rasterised by a separate
|
||||
// pass (ARCH §5.4). Declared unconditionally even when no layer is active, so
|
||||
// that every generated shader shares one bind group layout — a layout that
|
||||
// changed with the edit would mean rebuilding the pipeline layout, and the
|
||||
// cost of the unused declaration is a 1x1 placeholder texture.
|
||||
@group(0) @binding(3) var masks: texture_2d_array<f32>;
|
||||
|
||||
{sampler_helper}{helper_src}{encode_output}
|
||||
// Display-encoded sRGB back to linear, for sources that arrive that way.
|
||||
@@ -662,7 +705,7 @@ fn is_ident_byte(b: u8) -> bool {
|
||||
}
|
||||
|
||||
/// Make an operation id safe to embed in a WGSL identifier.
|
||||
fn sanitise(id: &str) -> String {
|
||||
pub(crate) fn sanitise(id: &str) -> String {
|
||||
id.chars()
|
||||
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
|
||||
.collect()
|
||||
|
||||
Reference in New Issue
Block a user