The camera-space tap: OutputMode::CameraLinear, composed with no operations
compose_camera_linear composes the fused pass with an empty operation list, the file's orientation as the baseline, a view rect for the tile, and a store of rgba32float. On the GPU, render_camera_linear is the only entry that accepts it: it fills the profile uniforms neutral — unit white balance, identity matrix, curve off — so what lands in the texture is the sensor's numbers after the lens warp and nothing else (FR-MRG-2). A third bind-group layout carries the format, as the linear one does, and the readback is generalised to any pixel width for the f32 copy. Thirty-two bits because the composite is written back at the sensor's scale: a 14-bit sensor has 16 384 steps to white and f16 keeps 2 048 of them in the top octave.
This commit is contained in:
+233
-7
@@ -101,6 +101,16 @@ pub struct AdjustPass {
|
|||||||
/// switched on does not build a pipeline layout mid-frame.
|
/// switched on does not build a pipeline layout mid-frame.
|
||||||
linear_bind_group_layout: wgpu::BindGroupLayout,
|
linear_bind_group_layout: wgpu::BindGroupLayout,
|
||||||
linear_pipeline_layout: wgpu::PipelineLayout,
|
linear_pipeline_layout: wgpu::PipelineLayout,
|
||||||
|
/// TRACES: FR-MRG-2
|
||||||
|
/// A third layout, writing `rgba32float`, for the camera-space tap a
|
||||||
|
/// merge reads (`OutputMode::CameraLinear`). Same reasoning as the
|
||||||
|
/// linear one: the format is in the layout, so a format is a layout.
|
||||||
|
camera_bind_group_layout: wgpu::BindGroupLayout,
|
||||||
|
camera_pipeline_layout: wgpu::PipelineLayout,
|
||||||
|
/// The camera-space texture the last `render_camera_linear` wrote.
|
||||||
|
/// Separate from `targets`: a different format, and a merge reads it
|
||||||
|
/// back or samples it while the display targets go on being swapped.
|
||||||
|
camera_target: Option<Target>,
|
||||||
/// TRACES: FR-DEV-3d
|
/// TRACES: FR-DEV-3d
|
||||||
/// What the linear intermediate currently holds, and at what size.
|
/// What the linear intermediate currently holds, and at what size.
|
||||||
///
|
///
|
||||||
@@ -303,6 +313,11 @@ impl AdjustPass {
|
|||||||
|
|
||||||
pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||||
|
|
||||||
|
/// TRACES: FR-MRG-2
|
||||||
|
/// The camera-space tap's format: full precision, because what it holds
|
||||||
|
/// is written back as a RAW at the sensor's own scale (FR-MRG-3).
|
||||||
|
pub const CAMERA_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba32Float;
|
||||||
|
|
||||||
pub fn new(ctx: &GpuContext) -> Self {
|
pub fn new(ctx: &GpuContext) -> Self {
|
||||||
let bind_group_layout = Self::layout_writing(ctx, Self::FORMAT, "adjust-bgl");
|
let bind_group_layout = Self::layout_writing(ctx, Self::FORMAT, "adjust-bgl");
|
||||||
|
|
||||||
@@ -330,6 +345,15 @@ impl AdjustPass {
|
|||||||
bind_group_layouts: &[Some(&linear_bind_group_layout)],
|
bind_group_layouts: &[Some(&linear_bind_group_layout)],
|
||||||
immediate_size: 0,
|
immediate_size: 0,
|
||||||
});
|
});
|
||||||
|
let camera_bind_group_layout =
|
||||||
|
Self::layout_writing(ctx, Self::CAMERA_FORMAT, "adjust-camera-bgl");
|
||||||
|
let camera_pipeline_layout =
|
||||||
|
ctx.device
|
||||||
|
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("adjust-camera-layout"),
|
||||||
|
bind_group_layouts: &[Some(&camera_bind_group_layout)],
|
||||||
|
immediate_size: 0,
|
||||||
|
});
|
||||||
|
|
||||||
// A 1x1 single-layer mask, bound when the edit has no local
|
// A 1x1 single-layer mask, bound when the edit has no local
|
||||||
// adjustments. The generated shader never samples it — no layer block
|
// adjustments. The generated shader never samples it — no layer block
|
||||||
@@ -412,6 +436,9 @@ impl AdjustPass {
|
|||||||
detail: DetailRunner::new(ctx),
|
detail: DetailRunner::new(ctx),
|
||||||
linear_bind_group_layout,
|
linear_bind_group_layout,
|
||||||
linear_pipeline_layout,
|
linear_pipeline_layout,
|
||||||
|
camera_bind_group_layout,
|
||||||
|
camera_pipeline_layout,
|
||||||
|
camera_target: None,
|
||||||
colour_key: None,
|
colour_key: None,
|
||||||
colour_dispatches: 0,
|
colour_dispatches: 0,
|
||||||
detail_dispatches: 0,
|
detail_dispatches: 0,
|
||||||
@@ -549,6 +576,7 @@ impl AdjustPass {
|
|||||||
let layout = match shader.output_mode {
|
let layout = match shader.output_mode {
|
||||||
OutputMode::Encoded => &self.pipeline_layout,
|
OutputMode::Encoded => &self.pipeline_layout,
|
||||||
OutputMode::LinearWorking => &self.linear_pipeline_layout,
|
OutputMode::LinearWorking => &self.linear_pipeline_layout,
|
||||||
|
OutputMode::CameraLinear => &self.camera_pipeline_layout,
|
||||||
};
|
};
|
||||||
|
|
||||||
let pipeline =
|
let pipeline =
|
||||||
@@ -1126,28 +1154,206 @@ impl AdjustPass {
|
|||||||
self.copy_output()
|
self.copy_output()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-MRG-2
|
||||||
|
/// Render the camera-space tap: the source after its lens warp and
|
||||||
|
/// nothing else, at full precision.
|
||||||
|
///
|
||||||
|
/// `shader` must come from `EditGraph::compose_camera_linear` — it is
|
||||||
|
/// refused otherwise, for the reason `render_masked` refuses a linear
|
||||||
|
/// one: the storage format is in the layout. The profile uniforms are
|
||||||
|
/// filled neutral here rather than from the source, which is the whole
|
||||||
|
/// point of the mode (`OutputMode::CameraLinear`): unit white balance,
|
||||||
|
/// identity matrix, base curve off. The non-linear flag is kept, so a
|
||||||
|
/// JPEG source is still linearised — camera space for a JPEG is the
|
||||||
|
/// decoded values made linear, which is the best that exists.
|
||||||
|
///
|
||||||
|
/// The texture stays on the device for a merge's warp to sample; see
|
||||||
|
/// [`Self::camera_texture`] and [`Self::read_camera_linear`].
|
||||||
|
pub fn render_camera_linear(
|
||||||
|
&mut self,
|
||||||
|
source: &DemosaicedImage,
|
||||||
|
shader: &ComposedShader,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
) -> Result<&wgpu::Texture, GpuError> {
|
||||||
|
if shader.output_mode != OutputMode::CameraLinear {
|
||||||
|
return Err(GpuError::ShaderCompilation(
|
||||||
|
"render_camera_linear takes the shader from EditGraph::compose_camera_linear \
|
||||||
|
and no other; this one writes a different format"
|
||||||
|
.into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.colour_key = None;
|
||||||
|
let (width, height) = (width.max(1), height.max(1));
|
||||||
|
self.ensure_camera_target(width, height);
|
||||||
|
|
||||||
|
let mut uniforms = Self::fused_uniforms(source, shader);
|
||||||
|
// Neutral profile: the numbers the sensor produced, and only those.
|
||||||
|
let non_linear = uniforms[15];
|
||||||
|
uniforms[0..4].copy_from_slice(&[1.0, 0.0, 0.0, 0.0]);
|
||||||
|
uniforms[4..8].copy_from_slice(&[0.0, 1.0, 0.0, 0.0]);
|
||||||
|
uniforms[8..12].copy_from_slice(&[0.0, 0.0, 1.0, 0.0]);
|
||||||
|
uniforms[12..16].copy_from_slice(&[1.0, 1.0, 1.0, non_linear]);
|
||||||
|
let b = dr_pipeline::BASE_CURVE_UNIFORM_OFFSET;
|
||||||
|
uniforms[b + 10] = 0.0;
|
||||||
|
|
||||||
|
let params_buf = self
|
||||||
|
.ctx
|
||||||
|
.device
|
||||||
|
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some("adjust-camera-params"),
|
||||||
|
contents: bytemuck::cast_slice(&uniforms),
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM,
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = self.pipeline(shader)?;
|
||||||
|
let pipeline = self
|
||||||
|
.cache
|
||||||
|
.get(&shader.structure_hash)
|
||||||
|
.expect("compiled above");
|
||||||
|
let target = self.camera_target.as_ref().expect("ensured above");
|
||||||
|
|
||||||
|
let bind_group = self
|
||||||
|
.ctx
|
||||||
|
.device
|
||||||
|
.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("adjust-camera-bg"),
|
||||||
|
layout: &self.camera_bind_group_layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::TextureView(source.view()),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: params_buf.as_entire_binding(),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: wgpu::BindingResource::TextureView(&target.view),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 3,
|
||||||
|
resource: wgpu::BindingResource::TextureView(&self.empty_masks),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 4,
|
||||||
|
resource: wgpu::BindingResource::TextureView(self.film_curves_view()),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 5,
|
||||||
|
resource: wgpu::BindingResource::TextureView(self.film_lut_view()),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut enc = self
|
||||||
|
.ctx
|
||||||
|
.device
|
||||||
|
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("adjust-camera-encoder"),
|
||||||
|
});
|
||||||
|
{
|
||||||
|
let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
||||||
|
label: Some("adjust-camera-pass"),
|
||||||
|
timestamp_writes: None,
|
||||||
|
});
|
||||||
|
pass.set_pipeline(pipeline);
|
||||||
|
pass.set_bind_group(0, &bind_group, &[]);
|
||||||
|
pass.dispatch_workgroups(width.div_ceil(8), height.div_ceil(8), 1);
|
||||||
|
}
|
||||||
|
self.ctx.queue.submit(Some(enc.finish()));
|
||||||
|
self.colour_dispatches += 1;
|
||||||
|
|
||||||
|
Ok(&self.camera_target.as_ref().expect("ensured above").texture)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The camera-space texture, if one has been rendered.
|
||||||
|
pub fn camera_texture(&self) -> Option<&wgpu::Texture> {
|
||||||
|
self.camera_target.as_ref().map(|t| &t.texture)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-MRG-2
|
||||||
|
/// Read the camera-space tap back: tightly packed RGBA `f32`,
|
||||||
|
/// `width * height * 4` values, alpha 1.0 everywhere.
|
||||||
|
pub fn read_camera_linear(&self) -> Result<(Vec<f32>, u32, u32), GpuError> {
|
||||||
|
let Some(target) = self.camera_target.as_ref() else {
|
||||||
|
return Err(GpuError::Readback("no camera-space render yet".into()));
|
||||||
|
};
|
||||||
|
let (bytes, w, h) = Self::copy_texture(&self.ctx, &target.texture, w_h(target), 16)?;
|
||||||
|
let floats: Vec<f32> = bytes
|
||||||
|
.chunks_exact(4)
|
||||||
|
.map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
|
||||||
|
.collect();
|
||||||
|
Ok((floats, w, h))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_camera_target(&mut self, width: u32, height: u32) {
|
||||||
|
if self
|
||||||
|
.camera_target
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|t| t.width == width && t.height == height)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let texture = self.ctx.device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some("adjust-camera-output"),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: Self::CAMERA_FORMAT,
|
||||||
|
// Written by compute, sampled by a merge's warp, copied out for
|
||||||
|
// the CPU. Never handed to the compositor, so no RENDER_ATTACHMENT.
|
||||||
|
usage: wgpu::TextureUsages::STORAGE_BINDING
|
||||||
|
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||||
|
| wgpu::TextureUsages::COPY_SRC,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
let view = texture.create_view(&Default::default());
|
||||||
|
self.camera_target = Some(Target {
|
||||||
|
texture,
|
||||||
|
view,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// The transfer itself.
|
/// The transfer itself.
|
||||||
fn copy_output(&self) -> Result<(Vec<u8>, u32, u32), GpuError> {
|
fn copy_output(&self) -> Result<(Vec<u8>, u32, u32), GpuError> {
|
||||||
let Some(target) = self.targets[self.current].as_ref() else {
|
let Some(target) = self.targets[self.current].as_ref() else {
|
||||||
return Err(GpuError::Readback("nothing rendered yet".into()));
|
return Err(GpuError::Readback("nothing rendered yet".into()));
|
||||||
};
|
};
|
||||||
let (w, h) = (target.width, target.height);
|
Self::copy_texture(&self.ctx, &target.texture, w_h(target), 4)
|
||||||
|
}
|
||||||
|
|
||||||
let unpadded = w * 4;
|
/// Copy a whole texture to the CPU, `bytes_per_pixel` wide, rows
|
||||||
|
/// unpadded. Shared by the display readback and the camera-space one.
|
||||||
|
fn copy_texture(
|
||||||
|
ctx: &GpuContext,
|
||||||
|
texture: &wgpu::Texture,
|
||||||
|
(w, h): (u32, u32),
|
||||||
|
bytes_per_pixel: u32,
|
||||||
|
) -> Result<(Vec<u8>, u32, u32), GpuError> {
|
||||||
|
let unpadded = w * bytes_per_pixel;
|
||||||
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
|
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
|
||||||
let padded = unpadded.div_ceil(align) * align;
|
let padded = unpadded.div_ceil(align) * align;
|
||||||
|
|
||||||
let buf = self.ctx.device.create_buffer(&wgpu::BufferDescriptor {
|
let buf = ctx.device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("adjust-readback"),
|
label: Some("adjust-readback"),
|
||||||
size: (padded * h) as u64,
|
size: (padded * h) as u64,
|
||||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||||
mapped_at_creation: false,
|
mapped_at_creation: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut enc = self.ctx.device.create_command_encoder(&Default::default());
|
let mut enc = ctx.device.create_command_encoder(&Default::default());
|
||||||
enc.copy_texture_to_buffer(
|
enc.copy_texture_to_buffer(
|
||||||
wgpu::TexelCopyTextureInfo {
|
wgpu::TexelCopyTextureInfo {
|
||||||
texture: &target.texture,
|
texture,
|
||||||
mip_level: 0,
|
mip_level: 0,
|
||||||
origin: wgpu::Origin3d::ZERO,
|
origin: wgpu::Origin3d::ZERO,
|
||||||
aspect: wgpu::TextureAspect::All,
|
aspect: wgpu::TextureAspect::All,
|
||||||
@@ -1166,7 +1372,7 @@ impl AdjustPass {
|
|||||||
depth_or_array_layers: 1,
|
depth_or_array_layers: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.ctx.queue.submit(Some(enc.finish()));
|
ctx.queue.submit(Some(enc.finish()));
|
||||||
|
|
||||||
let slice = buf.slice(..);
|
let slice = buf.slice(..);
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
@@ -1177,7 +1383,7 @@ impl AdjustPass {
|
|||||||
// Polled rather than parked, and bounded rather than spun forever —
|
// Polled rather than parked, and bounded rather than spun forever —
|
||||||
// see `readback::await_mapping`, which the histogram's own transfer
|
// see `readback::await_mapping`, which the histogram's own transfer
|
||||||
// shares for exactly the same reasons.
|
// shares for exactly the same reasons.
|
||||||
await_mapping(&self.ctx, &rx)?;
|
await_mapping(ctx, &rx)?;
|
||||||
|
|
||||||
let data = slice.get_mapped_range();
|
let data = slice.get_mapped_range();
|
||||||
let mut out = Vec::with_capacity((unpadded * h) as usize);
|
let mut out = Vec::with_capacity((unpadded * h) as usize);
|
||||||
@@ -1191,6 +1397,10 @@ impl AdjustPass {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn w_h(t: &Target) -> (u32, u32) {
|
||||||
|
(t.width, t.height)
|
||||||
|
}
|
||||||
|
|
||||||
/// Number the lines of generated source, so a compiler error can be located.
|
/// Number the lines of generated source, so a compiler error can be located.
|
||||||
pub(crate) fn numbered(src: &str) -> String {
|
pub(crate) fn numbered(src: &str) -> String {
|
||||||
src.lines()
|
src.lines()
|
||||||
@@ -1242,6 +1452,10 @@ mod tests {
|
|||||||
// rather than about a camera's colour response.
|
// rather than about a camera's colour response.
|
||||||
color_matrix: Some([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]),
|
color_matrix: Some([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]),
|
||||||
base_curve: BaseCurve::IDENTITY,
|
base_curve: BaseCurve::IDENTITY,
|
||||||
|
samples_per_pixel: 1,
|
||||||
|
profile: None,
|
||||||
|
make: String::new(),
|
||||||
|
model: String::new(),
|
||||||
crop: CropRect {
|
crop: CropRect {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
@@ -1442,6 +1656,10 @@ mod tests {
|
|||||||
wb_coeffs: [1.0, 1.0, 1.0, 1.0],
|
wb_coeffs: [1.0, 1.0, 1.0, 1.0],
|
||||||
color_matrix: Some([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]),
|
color_matrix: Some([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]),
|
||||||
base_curve: BaseCurve::IDENTITY,
|
base_curve: BaseCurve::IDENTITY,
|
||||||
|
samples_per_pixel: 1,
|
||||||
|
profile: None,
|
||||||
|
make: String::new(),
|
||||||
|
model: String::new(),
|
||||||
crop: CropRect {
|
crop: CropRect {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
@@ -1969,6 +2187,10 @@ mod tests {
|
|||||||
wb_coeffs: [1.0, 1.0, 1.0, 1.0],
|
wb_coeffs: [1.0, 1.0, 1.0, 1.0],
|
||||||
color_matrix: Some([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]),
|
color_matrix: Some([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]),
|
||||||
base_curve: BaseCurve::IDENTITY,
|
base_curve: BaseCurve::IDENTITY,
|
||||||
|
samples_per_pixel: 1,
|
||||||
|
profile: None,
|
||||||
|
make: String::new(),
|
||||||
|
model: String::new(),
|
||||||
crop: CropRect {
|
crop: CropRect {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
@@ -2069,6 +2291,10 @@ mod tests {
|
|||||||
wb_coeffs: [1.0, 1.0, 1.0, 1.0],
|
wb_coeffs: [1.0, 1.0, 1.0, 1.0],
|
||||||
color_matrix: Some([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]),
|
color_matrix: Some([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]),
|
||||||
base_curve: BaseCurve::IDENTITY,
|
base_curve: BaseCurve::IDENTITY,
|
||||||
|
samples_per_pixel: 1,
|
||||||
|
profile: None,
|
||||||
|
make: String::new(),
|
||||||
|
model: String::new(),
|
||||||
crop: CropRect {
|
crop: CropRect {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
|||||||
@@ -849,6 +849,14 @@ impl EditGraph {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-MRG-2
|
||||||
|
/// The camera-space tap for a merge: this edit's lens corrections and
|
||||||
|
/// nothing else of it, stored at full precision. See
|
||||||
|
/// [`crate::operation::compose_camera_linear`].
|
||||||
|
pub fn compose_camera_linear(&self, view: crate::framing::CropRect) -> ComposedShader {
|
||||||
|
crate::operation::compose_camera_linear(&self.warps, self.framing.baseline(), view)
|
||||||
|
}
|
||||||
|
|
||||||
/// TRACES: FR-DEV-19c
|
/// TRACES: FR-DEV-19c
|
||||||
/// [`Self::compose_for`], with one layer's mask drawn over the picture.
|
/// [`Self::compose_for`], with one layer's mask drawn over the picture.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -424,6 +424,21 @@ pub enum OutputMode {
|
|||||||
/// clamping before a sharpener sees it would draw a hard edge at precisely
|
/// clamping before a sharpener sees it would draw a hard edge at precisely
|
||||||
/// the luminance a sharpener is most visible at.
|
/// the luminance a sharpener is most visible at.
|
||||||
LinearWorking,
|
LinearWorking,
|
||||||
|
/// TRACES: FR-MRG-2
|
||||||
|
/// `rgba32float`, **camera space**: after the lens warp and nothing else.
|
||||||
|
///
|
||||||
|
/// What a merge stitches (FR-MRG-2). The shader is the linear tail with
|
||||||
|
/// no operations, and the caller fills the reserved uniforms neutral —
|
||||||
|
/// unit white balance, identity matrix, base curve off — so what is
|
||||||
|
/// stored is the sensor's own numbers, demosaiced and undistorted. Only
|
||||||
|
/// [`compose_camera_linear`] produces it, and only
|
||||||
|
/// `AdjustPass::render_camera_linear` accepts it, so the neutral
|
||||||
|
/// uniforms cannot be forgotten by a caller that composed it by mistake.
|
||||||
|
///
|
||||||
|
/// Thirty-two bits rather than sixteen because the composite is written
|
||||||
|
/// back as a RAW at the sensor's scale (FR-MRG-3): a 14-bit sensor has
|
||||||
|
/// 16 384 steps to white and `f16` keeps 2 048 of them in the top octave.
|
||||||
|
CameraLinear,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The result of composing a set of operations into one shader.
|
/// The result of composing a set of operations into one shader.
|
||||||
@@ -573,6 +588,54 @@ pub fn compose_full_revealing(
|
|||||||
spots: &crate::spot::SpotSet,
|
spots: &crate::spot::SpotSet,
|
||||||
warps: &[Box<dyn crate::lens::Warp>],
|
warps: &[Box<dyn crate::lens::Warp>],
|
||||||
reveal: Option<&crate::mask::Reveal>,
|
reveal: Option<&crate::mask::Reveal>,
|
||||||
|
) -> ComposedShader {
|
||||||
|
compose_inner(ops, framing, output, masks, spots, warps, reveal, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-MRG-2
|
||||||
|
/// The camera-space tap: the fused pass with no operations, stopping after
|
||||||
|
/// the lens warp and storing `rgba32float` ([`OutputMode::CameraLinear`]).
|
||||||
|
///
|
||||||
|
/// Takes the warps and a view, because that is all the tap uses of an edit:
|
||||||
|
/// no crop (a merge wants the whole frame, and crops the composite), no
|
||||||
|
/// masks, no spots, no operations. `view` is the tile — the fraction of the
|
||||||
|
/// undistorted frame to render, in `Framing::set_view`'s terms — so that a
|
||||||
|
/// merge pulls source tiles on demand (FR-MRG-11) rather than a frame that
|
||||||
|
/// may not fit. The camera profile's uniforms are still declared — the
|
||||||
|
/// prologue is the same — and the GPU side fills them neutral.
|
||||||
|
pub fn compose_camera_linear(
|
||||||
|
warps: &[Box<dyn crate::lens::Warp>],
|
||||||
|
baseline: dr_types::Orientation,
|
||||||
|
view: crate::framing::CropRect,
|
||||||
|
) -> ComposedShader {
|
||||||
|
let mut framing = Framing::default();
|
||||||
|
// The file's orientation and nothing of the user's: a merge aligned
|
||||||
|
// its frames upright (`dr_pano::Gray::oriented`), so its tiles must be
|
||||||
|
// upright too, and `view` is a fraction of the upright frame.
|
||||||
|
framing.set_baseline(baseline);
|
||||||
|
framing.set_view(view);
|
||||||
|
compose_inner(
|
||||||
|
&[],
|
||||||
|
&framing,
|
||||||
|
ColourSpace::Srgb,
|
||||||
|
&MaskStack::new(),
|
||||||
|
&crate::spot::SpotSet::new(),
|
||||||
|
warps,
|
||||||
|
None,
|
||||||
|
Some(OutputMode::CameraLinear),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn compose_inner(
|
||||||
|
ops: &[Box<dyn Operation>],
|
||||||
|
framing: &Framing,
|
||||||
|
output: ColourSpace,
|
||||||
|
masks: &MaskStack,
|
||||||
|
spots: &crate::spot::SpotSet,
|
||||||
|
warps: &[Box<dyn crate::lens::Warp>],
|
||||||
|
reveal: Option<&crate::mask::Reveal>,
|
||||||
|
forced: Option<OutputMode>,
|
||||||
) -> ComposedShader {
|
) -> ComposedShader {
|
||||||
// The lens corrections, composed into one coordinate transform. Beside
|
// The lens corrections, composed into one coordinate transform. Beside
|
||||||
// `framing` because they are the other half of the same stage: framing
|
// `framing` because they are the other half of the same stage: framing
|
||||||
@@ -603,12 +666,18 @@ pub fn compose_full_revealing(
|
|||||||
// all: a photograph with a spot on it and no sharpening still has a detail
|
// all: a photograph with a spot on it and no sharpening still has a detail
|
||||||
// stage, and a fused pass that encoded its own output there would quantise
|
// stage, and a fused pass that encoded its own output there would quantise
|
||||||
// twice and be bound to a texture of the wrong format.
|
// twice and be bound to a texture of the wrong format.
|
||||||
let output_mode =
|
//
|
||||||
|
// `forced` is the one exception, and it is not a caller flag in the
|
||||||
|
// sense above: `compose_camera_linear` is the only function that passes
|
||||||
|
// it, with an empty operation list, and the mode it forces has its own
|
||||||
|
// storage format and its own render entry on the GPU side.
|
||||||
|
let output_mode = forced.unwrap_or(
|
||||||
if ops.iter().any(|o| o.is_active() && o.detail().is_some()) || !spots.is_neutral() {
|
if ops.iter().any(|o| o.is_active() && o.detail().is_some()) || !spots.is_neutral() {
|
||||||
OutputMode::LinearWorking
|
OutputMode::LinearWorking
|
||||||
} else {
|
} else {
|
||||||
OutputMode::Encoded
|
OutputMode::Encoded
|
||||||
};
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Whether an operation has taken over the rendering. Decided from the
|
// Whether an operation has taken over the rendering. Decided from the
|
||||||
// operations for the same reason `output_mode` is: a caller that got it
|
// operations for the same reason `output_mode` is: a caller that got it
|
||||||
@@ -827,6 +896,16 @@ pub fn compose_full_revealing(
|
|||||||
// would put a hard edge into the very neighbourhood the next pass is
|
// would put a hard edge into the very neighbourhood the next pass is
|
||||||
// about to convolve, which is how sharpeners come to draw dark rings
|
// about to convolve, which is how sharpeners come to draw dark rings
|
||||||
// around specular highlights.
|
// around specular highlights.
|
||||||
|
textureStore(output, vec2<i32>(gid.xy), vec4<f32>(c, 1.0));"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
OutputMode::CameraLinear => (
|
||||||
|
"rgba32float",
|
||||||
|
String::new(),
|
||||||
|
String::new(),
|
||||||
|
" // Camera space, for a merge (FR-MRG-2): the sensor's numbers after the
|
||||||
|
// lens warp, with the profile uniforms filled neutral by the caller so the
|
||||||
|
// prologue above changed nothing. Not clamped, not encoded, full precision.
|
||||||
textureStore(output, vec2<i32>(gid.xy), vec4<f32>(c, 1.0));"
|
textureStore(output, vec2<i32>(gid.xy), vec4<f32>(c, 1.0));"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
),
|
),
|
||||||
|
|||||||
+67
-67
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user