fix color space for raster editing
This commit is contained in:
parent
885c52c02a
commit
759e41d84a
|
|
@ -94,6 +94,7 @@ impl CanvasPair {
|
||||||
/// in `raw_pixels` / PNG files). The values are decoded to linear premultiplied
|
/// in `raw_pixels` / PNG files). The values are decoded to linear premultiplied
|
||||||
/// before being written to the canvas, which operates entirely in linear space.
|
/// before being written to the canvas, which operates entirely in linear space.
|
||||||
pub fn upload(&self, queue: &wgpu::Queue, pixels: &[u8]) {
|
pub fn upload(&self, queue: &wgpu::Queue, pixels: &[u8]) {
|
||||||
|
eprintln!("[CANVAS] upload: {}x{} pixels={}", self.width, self.height, pixels.len());
|
||||||
// Decode sRGB-premultiplied → linear premultiplied for the GPU canvas.
|
// Decode sRGB-premultiplied → linear premultiplied for the GPU canvas.
|
||||||
let linear: Vec<u8> = pixels.chunks_exact(4).flat_map(|p| {
|
let linear: Vec<u8> = pixels.chunks_exact(4).flat_map(|p| {
|
||||||
let r = (srgb_to_linear(p[0] as f32 / 255.0) * 255.0 + 0.5) as u8;
|
let r = (srgb_to_linear(p[0] as f32 / 255.0) * 255.0 + 0.5) as u8;
|
||||||
|
|
@ -268,147 +269,156 @@ impl GpuBrushEngine {
|
||||||
let needs_new = self.canvases.get(&keyframe_id)
|
let needs_new = self.canvases.get(&keyframe_id)
|
||||||
.map_or(true, |c| c.width != width || c.height != height);
|
.map_or(true, |c| c.width != width || c.height != height);
|
||||||
if needs_new {
|
if needs_new {
|
||||||
|
eprintln!("[CANVAS] ensure_canvas: creating new CanvasPair for kf={:?} {}x{}", keyframe_id, width, height);
|
||||||
self.canvases.insert(keyframe_id, CanvasPair::new(device, width, height));
|
self.canvases.insert(keyframe_id, CanvasPair::new(device, width, height));
|
||||||
|
} else {
|
||||||
|
eprintln!("[CANVAS] ensure_canvas: reusing existing CanvasPair for kf={:?}", keyframe_id);
|
||||||
}
|
}
|
||||||
self.canvases.get_mut(&keyframe_id).unwrap()
|
self.canvases.get_mut(&keyframe_id).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dispatch the brush compute shader for `dabs` onto the canvas of `keyframe_id`.
|
/// Dispatch the brush compute shader for `dabs` onto the canvas of `keyframe_id`.
|
||||||
///
|
///
|
||||||
/// * Pre-fills `dst` from `src` so untouched pixels are preserved.
|
/// Each dab is dispatched as a separate copy+compute+swap so that every dab
|
||||||
/// * Dispatches the compute shader.
|
/// reads the result of the previous one. This is required for the smudge tool:
|
||||||
/// * Swaps src/dst so the just-written texture becomes the new source.
|
/// if all dabs were batched into one dispatch they would all read the pre-batch
|
||||||
|
/// canvas state, breaking the carry-forward that makes smudge drag pixels along.
|
||||||
///
|
///
|
||||||
/// `dab_bbox` is `(x0, y0, x1, y1)` — the union bounding box of all dabs.
|
/// `dab_bbox` is the union bounding box (unused here; kept for API compat).
|
||||||
/// If `dabs` is empty or the bbox is invalid, does nothing.
|
/// If `dabs` is empty, does nothing.
|
||||||
pub fn render_dabs(
|
pub fn render_dabs(
|
||||||
&mut self,
|
&mut self,
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
queue: &wgpu::Queue,
|
queue: &wgpu::Queue,
|
||||||
keyframe_id: Uuid,
|
keyframe_id: Uuid,
|
||||||
dabs: &[GpuDab],
|
dabs: &[GpuDab],
|
||||||
bbox: (i32, i32, i32, i32),
|
_bbox: (i32, i32, i32, i32),
|
||||||
canvas_w: u32,
|
canvas_w: u32,
|
||||||
canvas_h: u32,
|
canvas_h: u32,
|
||||||
) {
|
) {
|
||||||
if dabs.is_empty() || bbox.0 == i32::MAX { return; }
|
if dabs.is_empty() { return; }
|
||||||
|
|
||||||
let canvas = match self.canvases.get_mut(&keyframe_id) {
|
if !self.canvases.contains_key(&keyframe_id) { return; }
|
||||||
Some(c) => c,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clamp bbox to canvas bounds
|
|
||||||
let x0 = bbox.0.max(0) as u32;
|
|
||||||
let y0 = bbox.1.max(0) as u32;
|
|
||||||
let x1 = (bbox.2.min(canvas_w as i32 - 1)).max(0) as u32;
|
|
||||||
let y1 = (bbox.3.min(canvas_h as i32 - 1)).max(0) as u32;
|
|
||||||
if x1 < x0 || y1 < y0 { return; }
|
|
||||||
|
|
||||||
let bbox_w = x1 - x0 + 1;
|
|
||||||
let bbox_h = y1 - y0 + 1;
|
|
||||||
|
|
||||||
// --- Pre-fill dst from src: copy the ENTIRE canvas so every pixel outside
|
|
||||||
// the dab bounding box is preserved across the ping-pong swap.
|
|
||||||
// Copying only the bbox would leave dst with data from two frames ago
|
|
||||||
// in all other regions, causing missing dabs on alternating frames. ---
|
|
||||||
let mut copy_encoder = device.create_command_encoder(
|
|
||||||
&wgpu::CommandEncoderDescriptor { label: Some("canvas_copy_encoder") },
|
|
||||||
);
|
|
||||||
let full_extent = wgpu::Extent3d {
|
let full_extent = wgpu::Extent3d {
|
||||||
width: canvas.width,
|
width: self.canvases[&keyframe_id].width,
|
||||||
height: canvas.height,
|
height: self.canvases[&keyframe_id].height,
|
||||||
depth_or_array_layers: 1,
|
depth_or_array_layers: 1,
|
||||||
};
|
};
|
||||||
copy_encoder.copy_texture_to_texture(
|
|
||||||
wgpu::TexelCopyTextureInfo {
|
|
||||||
texture: canvas.src(),
|
|
||||||
mip_level: 0,
|
|
||||||
origin: wgpu::Origin3d::ZERO,
|
|
||||||
aspect: wgpu::TextureAspect::All,
|
|
||||||
},
|
|
||||||
wgpu::TexelCopyTextureInfo {
|
|
||||||
texture: canvas.dst(),
|
|
||||||
mip_level: 0,
|
|
||||||
origin: wgpu::Origin3d::ZERO,
|
|
||||||
aspect: wgpu::TextureAspect::All,
|
|
||||||
},
|
|
||||||
full_extent,
|
|
||||||
);
|
|
||||||
queue.submit(Some(copy_encoder.finish()));
|
|
||||||
|
|
||||||
// --- Upload dab data and params ---
|
eprintln!("[DAB] render_dabs keyframe={:?} count={}", keyframe_id, dabs.len());
|
||||||
let dab_bytes = bytemuck::cast_slice(dabs);
|
for dab in dabs {
|
||||||
let dab_buf = device.create_buffer(&wgpu::BufferDescriptor {
|
// Per-dab bounding box
|
||||||
label: Some("dab_storage_buf"),
|
let r_fringe = dab.radius + 1.0;
|
||||||
size: dab_bytes.len() as u64,
|
let dx0 = (dab.x - r_fringe).floor() as i32;
|
||||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
let dy0 = (dab.y - r_fringe).floor() as i32;
|
||||||
mapped_at_creation: false,
|
let dx1 = (dab.x + r_fringe).ceil() as i32;
|
||||||
});
|
let dy1 = (dab.y + r_fringe).ceil() as i32;
|
||||||
queue.write_buffer(&dab_buf, 0, dab_bytes);
|
|
||||||
|
|
||||||
let params = DabParams {
|
let x0 = dx0.max(0) as u32;
|
||||||
bbox_x0: x0 as i32,
|
let y0 = dy0.max(0) as u32;
|
||||||
bbox_y0: y0 as i32,
|
let x1 = (dx1.min(canvas_w as i32 - 1)).max(0) as u32;
|
||||||
bbox_w,
|
let y1 = (dy1.min(canvas_h as i32 - 1)).max(0) as u32;
|
||||||
bbox_h,
|
if x1 < x0 || y1 < y0 { continue; }
|
||||||
num_dabs: dabs.len() as u32,
|
|
||||||
canvas_w,
|
|
||||||
canvas_h,
|
|
||||||
_pad: 0,
|
|
||||||
};
|
|
||||||
let params_buf = device.create_buffer(&wgpu::BufferDescriptor {
|
|
||||||
label: Some("dab_params_buf"),
|
|
||||||
size: std::mem::size_of::<DabParams>() as u64,
|
|
||||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
||||||
mapped_at_creation: false,
|
|
||||||
});
|
|
||||||
queue.write_buffer(¶ms_buf, 0, bytemuck::bytes_of(¶ms));
|
|
||||||
|
|
||||||
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
let bbox_w = x1 - x0 + 1;
|
||||||
label: Some("brush_dab_bg"),
|
let bbox_h = y1 - y0 + 1;
|
||||||
layout: &self.compute_bg_layout,
|
|
||||||
entries: &[
|
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 0,
|
|
||||||
resource: dab_buf.as_entire_binding(),
|
|
||||||
},
|
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 1,
|
|
||||||
resource: params_buf.as_entire_binding(),
|
|
||||||
},
|
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 2,
|
|
||||||
resource: wgpu::BindingResource::TextureView(canvas.src_view()),
|
|
||||||
},
|
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 3,
|
|
||||||
resource: wgpu::BindingResource::TextureView(canvas.dst_view()),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Dispatch ---
|
let canvas = self.canvases.get_mut(&keyframe_id).unwrap();
|
||||||
let mut compute_encoder = device.create_command_encoder(
|
|
||||||
&wgpu::CommandEncoderDescriptor { label: Some("brush_dab_encoder") },
|
// Pre-fill dst from src so pixels outside this dab's bbox are preserved.
|
||||||
);
|
let mut copy_enc = device.create_command_encoder(
|
||||||
{
|
&wgpu::CommandEncoderDescriptor { label: Some("canvas_copy_encoder") },
|
||||||
let mut pass = compute_encoder.begin_compute_pass(
|
|
||||||
&wgpu::ComputePassDescriptor {
|
|
||||||
label: Some("brush_dab_pass"),
|
|
||||||
timestamp_writes: None,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
pass.set_pipeline(&self.compute_pipeline);
|
copy_enc.copy_texture_to_texture(
|
||||||
pass.set_bind_group(0, &bg, &[]);
|
wgpu::TexelCopyTextureInfo {
|
||||||
let wg_x = bbox_w.div_ceil(8);
|
texture: canvas.src(),
|
||||||
let wg_y = bbox_h.div_ceil(8);
|
mip_level: 0,
|
||||||
pass.dispatch_workgroups(wg_x, wg_y, 1);
|
origin: wgpu::Origin3d::ZERO,
|
||||||
}
|
aspect: wgpu::TextureAspect::All,
|
||||||
queue.submit(Some(compute_encoder.finish()));
|
},
|
||||||
|
wgpu::TexelCopyTextureInfo {
|
||||||
|
texture: canvas.dst(),
|
||||||
|
mip_level: 0,
|
||||||
|
origin: wgpu::Origin3d::ZERO,
|
||||||
|
aspect: wgpu::TextureAspect::All,
|
||||||
|
},
|
||||||
|
full_extent,
|
||||||
|
);
|
||||||
|
queue.submit(Some(copy_enc.finish()));
|
||||||
|
|
||||||
// Swap: dst is now the authoritative source
|
// Upload single-dab buffer and params
|
||||||
canvas.swap();
|
let dab_bytes = bytemuck::bytes_of(dab);
|
||||||
|
let dab_buf = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("dab_storage_buf"),
|
||||||
|
size: dab_bytes.len() as u64,
|
||||||
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
queue.write_buffer(&dab_buf, 0, dab_bytes);
|
||||||
|
|
||||||
|
let params = DabParams {
|
||||||
|
bbox_x0: x0 as i32,
|
||||||
|
bbox_y0: y0 as i32,
|
||||||
|
bbox_w,
|
||||||
|
bbox_h,
|
||||||
|
num_dabs: 1,
|
||||||
|
canvas_w,
|
||||||
|
canvas_h,
|
||||||
|
_pad: 0,
|
||||||
|
};
|
||||||
|
let params_buf = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("dab_params_buf"),
|
||||||
|
size: std::mem::size_of::<DabParams>() as u64,
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
queue.write_buffer(¶ms_buf, 0, bytemuck::bytes_of(¶ms));
|
||||||
|
|
||||||
|
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("brush_dab_bg"),
|
||||||
|
layout: &self.compute_bg_layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: dab_buf.as_entire_binding(),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: params_buf.as_entire_binding(),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: wgpu::BindingResource::TextureView(canvas.src_view()),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 3,
|
||||||
|
resource: wgpu::BindingResource::TextureView(canvas.dst_view()),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut compute_enc = device.create_command_encoder(
|
||||||
|
&wgpu::CommandEncoderDescriptor { label: Some("brush_dab_encoder") },
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let mut pass = compute_enc.begin_compute_pass(
|
||||||
|
&wgpu::ComputePassDescriptor {
|
||||||
|
label: Some("brush_dab_pass"),
|
||||||
|
timestamp_writes: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pass.set_pipeline(&self.compute_pipeline);
|
||||||
|
pass.set_bind_group(0, &bg, &[]);
|
||||||
|
pass.dispatch_workgroups(bbox_w.div_ceil(8), bbox_h.div_ceil(8), 1);
|
||||||
|
}
|
||||||
|
queue.submit(Some(compute_enc.finish()));
|
||||||
|
|
||||||
|
// Swap: the just-written dst becomes src for the next dab.
|
||||||
|
eprintln!("[DAB] dispatched bbox=({},{},{},{}) current_before_swap={}", x0, y0, x1, y1, canvas.current);
|
||||||
|
canvas.swap();
|
||||||
|
eprintln!("[DAB] after swap: current={}", canvas.current);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the current canvas back to a CPU `Vec<u8>` (raw RGBA, row-major).
|
/// Read the current canvas back to a CPU `Vec<u8>` (raw RGBA, row-major).
|
||||||
|
|
@ -613,7 +623,7 @@ impl CanvasBlitPipeline {
|
||||||
module: &shader,
|
module: &shader,
|
||||||
entry_point: Some("fs_main"),
|
entry_point: Some("fs_main"),
|
||||||
targets: &[Some(wgpu::ColorTargetState {
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
format: wgpu::TextureFormat::Rgba16Float,
|
||||||
blend: None, // canvas already stores premultiplied alpha
|
blend: None, // canvas already stores premultiplied alpha
|
||||||
write_mask: wgpu::ColorWrites::ALL,
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
})],
|
})],
|
||||||
|
|
@ -655,7 +665,7 @@ impl CanvasBlitPipeline {
|
||||||
Self { pipeline, bg_layout, sampler, mask_sampler }
|
Self { pipeline, bg_layout, sampler, mask_sampler }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render the canvas texture into `target_view` (Rgba8Unorm) with the given camera.
|
/// Render the canvas texture into `target_view` (Rgba16Float) with the given camera.
|
||||||
///
|
///
|
||||||
/// `target_view` is cleared to transparent before writing.
|
/// `target_view` is cleared to transparent before writing.
|
||||||
/// `mask_view` is an R8Unorm texture in canvas-pixel space: 255 = keep, 0 = discard.
|
/// `mask_view` is an R8Unorm texture in canvas-pixel space: 255 = keep, 0 = discard.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
// Canvas blit shader.
|
// Canvas blit shader.
|
||||||
//
|
//
|
||||||
// Renders a GPU raster canvas (at document resolution) into the layer's sRGB
|
// Renders a GPU raster canvas (at document resolution) into an Rgba16Float HDR
|
||||||
// render buffer (at viewport resolution), applying the camera transform
|
// buffer (at viewport resolution), applying the camera transform (pan + zoom)
|
||||||
// (pan + zoom) to map document-space pixels to viewport-space pixels.
|
// to map document-space pixels to viewport-space pixels.
|
||||||
|
//
|
||||||
|
// The canvas stores premultiplied linear RGBA. We output it as-is so the HDR
|
||||||
|
// compositor sees the same premultiplied-linear format it always works with,
|
||||||
|
// bypassing the sRGB intermediate used for Vello layers.
|
||||||
//
|
//
|
||||||
// Any viewport pixel whose corresponding document coordinate falls outside
|
// Any viewport pixel whose corresponding document coordinate falls outside
|
||||||
// [0, canvas_w) × [0, canvas_h) outputs transparent black.
|
// [0, canvas_w) × [0, canvas_h) outputs transparent black.
|
||||||
|
|
@ -42,17 +46,6 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linear → sRGB encoding for a single channel.
|
|
||||||
// Applied to premultiplied linear values so the downstream srgb_to_linear
|
|
||||||
// pass round-trips correctly without darkening semi-transparent edges.
|
|
||||||
fn linear_to_srgb(c: f32) -> f32 {
|
|
||||||
return select(
|
|
||||||
1.055 * pow(max(c, 0.0), 1.0 / 2.4) - 0.055,
|
|
||||||
c * 12.92,
|
|
||||||
c <= 0.0031308,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
// Map viewport UV [0,1] → viewport pixel
|
// Map viewport UV [0,1] → viewport pixel
|
||||||
|
|
@ -71,23 +64,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The canvas stores premultiplied linear RGBA.
|
// The canvas stores premultiplied linear RGBA.
|
||||||
// The downstream pipeline (srgb_to_linear → compositor) expects the sRGB
|
// The compositor expects straight-alpha linear (it premultiplies by src_alpha itself),
|
||||||
// buffer to contain straight-alpha sRGB, i.e. the same format Vello outputs:
|
// so unpremultiply here. No sRGB conversion — the HDR buffer is linear throughout.
|
||||||
// sRGB buffer: srgb(r_straight), srgb(g_straight), srgb(b_straight), a
|
|
||||||
// srgb_to_linear: r_straight, g_straight, b_straight, a (linear straight)
|
|
||||||
// compositor: r_straight * a * opacity (premultiplied, correct)
|
|
||||||
//
|
|
||||||
// Without unpremultiplying, the compositor would double-premultiply:
|
|
||||||
// src = (premul_r, premul_g, premul_b, a) → output = premul_r * a = r * a²
|
|
||||||
// which produces a dark halo over transparent regions.
|
|
||||||
let c = textureSample(canvas_tex, canvas_sampler, canvas_uv);
|
let c = textureSample(canvas_tex, canvas_sampler, canvas_uv);
|
||||||
let mask = textureSample(mask_tex, mask_sampler, canvas_uv).r;
|
let mask = textureSample(mask_tex, mask_sampler, canvas_uv).r;
|
||||||
let masked_a = c.a * mask;
|
let masked_a = c.a * mask;
|
||||||
let inv_a = select(0.0, 1.0 / c.a, c.a > 1e-6);
|
let inv_a = select(0.0, 1.0 / c.a, c.a > 1e-6);
|
||||||
return vec4<f32>(
|
return vec4<f32>(c.r * inv_a, c.g * inv_a, c.b * inv_a, masked_a);
|
||||||
linear_to_srgb(c.r * inv_a),
|
|
||||||
linear_to_srgb(c.g * inv_a),
|
|
||||||
linear_to_srgb(c.b * inv_a),
|
|
||||||
masked_a,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -508,6 +508,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
if let Some(ref float_sel) = self.ctx.selection.raster_floating {
|
if let Some(ref float_sel) = self.ctx.selection.raster_floating {
|
||||||
if let Ok(mut gpu_brush) = shared.gpu_brush.lock() {
|
if let Ok(mut gpu_brush) = shared.gpu_brush.lock() {
|
||||||
if !gpu_brush.canvases.contains_key(&float_sel.canvas_id) {
|
if !gpu_brush.canvases.contains_key(&float_sel.canvas_id) {
|
||||||
|
eprintln!("[CANVAS] lazy-init float canvas id={:?}", float_sel.canvas_id);
|
||||||
gpu_brush.ensure_canvas(device, float_sel.canvas_id, float_sel.width, float_sel.height);
|
gpu_brush.ensure_canvas(device, float_sel.canvas_id, float_sel.width, float_sel.height);
|
||||||
if let Some(canvas) = gpu_brush.canvases.get(&float_sel.canvas_id) {
|
if let Some(canvas) = gpu_brush.canvases.get(&float_sel.canvas_id) {
|
||||||
let pixels = if float_sel.pixels.is_empty() {
|
let pixels = if float_sel.pixels.is_empty() {
|
||||||
|
|
@ -536,6 +537,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
);
|
);
|
||||||
// On stroke start, upload the pre-stroke pixel data to both textures
|
// On stroke start, upload the pre-stroke pixel data to both textures
|
||||||
if let Some(ref pixels) = pending.initial_pixels {
|
if let Some(ref pixels) = pending.initial_pixels {
|
||||||
|
eprintln!("[STROKE] uploading initial_pixels for kf={:?} painting_float={}", pending.keyframe_id, self.ctx.painting_float);
|
||||||
if let Some(canvas) = gpu_brush.canvases.get(&pending.keyframe_id) {
|
if let Some(canvas) = gpu_brush.canvases.get(&pending.keyframe_id) {
|
||||||
canvas.upload(queue, pixels);
|
canvas.upload(queue, pixels);
|
||||||
}
|
}
|
||||||
|
|
@ -592,6 +594,14 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
);
|
);
|
||||||
drop(image_cache);
|
drop(image_cache);
|
||||||
|
|
||||||
|
// Debug frame counter (only active during strokes)
|
||||||
|
static FRAME: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||||
|
let dbg_frame = FRAME.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let dbg_stroke = self.ctx.painting_canvas.is_some();
|
||||||
|
if dbg_stroke {
|
||||||
|
eprintln!("[FRAME {}] painting_canvas={:?} painting_float={}", dbg_frame, self.ctx.painting_canvas, self.ctx.painting_float);
|
||||||
|
}
|
||||||
|
|
||||||
// Get buffer pool for layer rendering
|
// Get buffer pool for layer rendering
|
||||||
let mut buffer_pool = shared.buffer_pool.lock().unwrap();
|
let mut buffer_pool = shared.buffer_pool.lock().unwrap();
|
||||||
|
|
||||||
|
|
@ -631,6 +641,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
antialiasing_method: vello::AaConfig::Msaa16,
|
antialiasing_method: vello::AaConfig::Msaa16,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if dbg_stroke { eprintln!("[DRAW] background Vello render"); }
|
||||||
if let Ok(mut renderer) = shared.renderer.lock() {
|
if let Ok(mut renderer) = shared.renderer.lock() {
|
||||||
renderer.render_to_texture(device, queue, &composite_result.background, bg_srgb_view, &bg_render_params).ok();
|
renderer.render_to_texture(device, queue, &composite_result.background, bg_srgb_view, &bg_render_params).ok();
|
||||||
}
|
}
|
||||||
|
|
@ -650,6 +661,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
// Clear to dark gray (stage background outside document bounds)
|
// Clear to dark gray (stage background outside document bounds)
|
||||||
// Note: stage_bg values are already in linear space for HDR compositing
|
// Note: stage_bg values are already in linear space for HDR compositing
|
||||||
let stage_bg = [45.0 / 255.0, 45.0 / 255.0, 48.0 / 255.0, 1.0];
|
let stage_bg = [45.0 / 255.0, 45.0 / 255.0, 48.0 / 255.0, 1.0];
|
||||||
|
if dbg_stroke { eprintln!("[COMPOSITE] background onto HDR"); }
|
||||||
shared.compositor.composite(
|
shared.compositor.composite(
|
||||||
device,
|
device,
|
||||||
queue,
|
queue,
|
||||||
|
|
@ -757,12 +769,14 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
&instance_resources.hdr_texture_view,
|
&instance_resources.hdr_texture_view,
|
||||||
) {
|
) {
|
||||||
// GPU canvas blit path: if a live GPU canvas exists for this
|
// GPU canvas blit path: if a live GPU canvas exists for this
|
||||||
// raster layer, sample it directly instead of rendering the Vello
|
// raster layer, blit it directly into the HDR buffer (premultiplied
|
||||||
// scene (which lags until raw_pixels is updated after readback).
|
// linear → Rgba16Float), bypassing the sRGB intermediate entirely.
|
||||||
|
// Vello path: render to sRGB buffer → srgb_to_linear → HDR buffer.
|
||||||
let used_gpu_canvas = if let Some(kf_id) = gpu_canvas_kf {
|
let used_gpu_canvas = if let Some(kf_id) = gpu_canvas_kf {
|
||||||
let mut used = false;
|
let mut used = false;
|
||||||
if let Ok(gpu_brush) = shared.gpu_brush.lock() {
|
if let Ok(gpu_brush) = shared.gpu_brush.lock() {
|
||||||
if let Some(canvas) = gpu_brush.canvases.get(&kf_id) {
|
if let Some(canvas) = gpu_brush.canvases.get(&kf_id) {
|
||||||
|
if dbg_stroke { eprintln!("[DRAW] GPU canvas blit layer={:?} kf={:?} canvas.current={}", rendered_layer.layer_id, kf_id, canvas.current); }
|
||||||
let camera = crate::gpu_brush::CameraParams {
|
let camera = crate::gpu_brush::CameraParams {
|
||||||
pan_x: self.ctx.pan_offset.x,
|
pan_x: self.ctx.pan_offset.x,
|
||||||
pan_y: self.ctx.pan_offset.y,
|
pan_y: self.ctx.pan_offset.y,
|
||||||
|
|
@ -776,7 +790,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
shared.canvas_blit.blit(
|
shared.canvas_blit.blit(
|
||||||
device, queue,
|
device, queue,
|
||||||
canvas.src_view(),
|
canvas.src_view(),
|
||||||
srgb_view,
|
hdr_layer_view, // blit directly to HDR
|
||||||
&camera,
|
&camera,
|
||||||
None, // no mask on layer canvas blit
|
None, // no mask on layer canvas blit
|
||||||
);
|
);
|
||||||
|
|
@ -789,19 +803,18 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
};
|
};
|
||||||
|
|
||||||
if !used_gpu_canvas {
|
if !used_gpu_canvas {
|
||||||
// Render layer scene to sRGB buffer
|
// Render layer scene to sRGB buffer, then convert to HDR
|
||||||
|
if dbg_stroke { eprintln!("[DRAW] Vello render layer={:?} opacity={}", rendered_layer.layer_id, rendered_layer.opacity); }
|
||||||
if let Ok(mut renderer) = shared.renderer.lock() {
|
if let Ok(mut renderer) = shared.renderer.lock() {
|
||||||
renderer.render_to_texture(device, queue, &rendered_layer.scene, srgb_view, &layer_render_params).ok();
|
renderer.render_to_texture(device, queue, &rendered_layer.scene, srgb_view, &layer_render_params).ok();
|
||||||
}
|
}
|
||||||
|
let mut convert_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("layer_srgb_to_linear_encoder"),
|
||||||
|
});
|
||||||
|
shared.srgb_to_linear.convert(device, &mut convert_encoder, srgb_view, hdr_layer_view);
|
||||||
|
queue.submit(Some(convert_encoder.finish()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert sRGB to linear HDR
|
|
||||||
let mut convert_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
||||||
label: Some("layer_srgb_to_linear_encoder"),
|
|
||||||
});
|
|
||||||
shared.srgb_to_linear.convert(device, &mut convert_encoder, srgb_view, hdr_layer_view);
|
|
||||||
queue.submit(Some(convert_encoder.finish()));
|
|
||||||
|
|
||||||
// Composite this layer onto the HDR accumulator with its opacity
|
// Composite this layer onto the HDR accumulator with its opacity
|
||||||
let compositor_layer = lightningbeam_core::gpu::CompositorLayer::new(
|
let compositor_layer = lightningbeam_core::gpu::CompositorLayer::new(
|
||||||
hdr_layer_handle,
|
hdr_layer_handle,
|
||||||
|
|
@ -809,6 +822,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
rendered_layer.blend_mode,
|
rendered_layer.blend_mode,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if dbg_stroke { eprintln!("[COMPOSITE] layer={:?} opacity={} blend={:?} used_gpu_canvas={}", rendered_layer.layer_id, rendered_layer.opacity, rendered_layer.blend_mode, used_gpu_canvas); }
|
||||||
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
label: Some("layer_composite_encoder"),
|
label: Some("layer_composite_encoder"),
|
||||||
});
|
});
|
||||||
|
|
@ -1003,6 +1017,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
|
|
||||||
// Blit the float GPU canvas on top of all composited layers.
|
// Blit the float GPU canvas on top of all composited layers.
|
||||||
// The float_mask_view clips to the selection shape (None = full float visible).
|
// The float_mask_view clips to the selection shape (None = full float visible).
|
||||||
|
if dbg_stroke { eprintln!("[FRAME {}] float blit section: raster_floating={}", dbg_frame, self.ctx.selection.raster_floating.is_some()); }
|
||||||
if let Some(ref float_sel) = self.ctx.selection.raster_floating {
|
if let Some(ref float_sel) = self.ctx.selection.raster_floating {
|
||||||
let float_canvas_id = float_sel.canvas_id;
|
let float_canvas_id = float_sel.canvas_id;
|
||||||
let float_x = float_sel.x;
|
let float_x = float_sel.x;
|
||||||
|
|
@ -1011,10 +1026,9 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
let float_h = float_sel.height;
|
let float_h = float_sel.height;
|
||||||
if let Ok(gpu_brush) = shared.gpu_brush.lock() {
|
if let Ok(gpu_brush) = shared.gpu_brush.lock() {
|
||||||
if let Some(canvas) = gpu_brush.canvases.get(&float_canvas_id) {
|
if let Some(canvas) = gpu_brush.canvases.get(&float_canvas_id) {
|
||||||
let float_srgb_handle = buffer_pool.acquire(device, layer_spec);
|
if dbg_stroke { eprintln!("[DRAW] float canvas blit canvas_id={:?} canvas.current={}", float_canvas_id, canvas.current); }
|
||||||
let float_hdr_handle = buffer_pool.acquire(device, hdr_spec);
|
let float_hdr_handle = buffer_pool.acquire(device, hdr_spec);
|
||||||
if let (Some(fsrgb_view), Some(fhdr_view), Some(hdr_view)) = (
|
if let (Some(fhdr_view), Some(hdr_view)) = (
|
||||||
buffer_pool.get_view(float_srgb_handle),
|
|
||||||
buffer_pool.get_view(float_hdr_handle),
|
buffer_pool.get_view(float_hdr_handle),
|
||||||
&instance_resources.hdr_texture_view,
|
&instance_resources.hdr_texture_view,
|
||||||
) {
|
) {
|
||||||
|
|
@ -1028,27 +1042,22 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
viewport_h: height as f32,
|
viewport_h: height as f32,
|
||||||
_pad: 0.0,
|
_pad: 0.0,
|
||||||
};
|
};
|
||||||
|
// Blit directly to HDR (straight-alpha linear, no sRGB step)
|
||||||
shared.canvas_blit.blit(
|
shared.canvas_blit.blit(
|
||||||
device, queue,
|
device, queue,
|
||||||
canvas.src_view(),
|
canvas.src_view(),
|
||||||
fsrgb_view,
|
fhdr_view,
|
||||||
&fcamera,
|
&fcamera,
|
||||||
float_mask_view.as_ref(),
|
float_mask_view.as_ref(),
|
||||||
);
|
);
|
||||||
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
||||||
label: Some("float_canvas_srgb_to_linear"),
|
|
||||||
});
|
|
||||||
shared.srgb_to_linear.convert(device, &mut enc, fsrgb_view, fhdr_view);
|
|
||||||
queue.submit(Some(enc.finish()));
|
|
||||||
|
|
||||||
let float_layer = lightningbeam_core::gpu::CompositorLayer::normal(float_hdr_handle, 1.0);
|
let float_layer = lightningbeam_core::gpu::CompositorLayer::normal(float_hdr_handle, 1.0);
|
||||||
|
if dbg_stroke { eprintln!("[COMPOSITE] float canvas onto HDR"); }
|
||||||
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
label: Some("float_canvas_composite"),
|
label: Some("float_canvas_composite"),
|
||||||
});
|
});
|
||||||
shared.compositor.composite(device, queue, &mut enc, &[float_layer], &buffer_pool, hdr_view, None);
|
shared.compositor.composite(device, queue, &mut enc, &[float_layer], &buffer_pool, hdr_view, None);
|
||||||
queue.submit(Some(enc.finish()));
|
queue.submit(Some(enc.finish()));
|
||||||
}
|
}
|
||||||
buffer_pool.release(float_srgb_handle);
|
|
||||||
buffer_pool.release(float_hdr_handle);
|
buffer_pool.release(float_hdr_handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2180,6 +2189,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
antialiasing_method: vello::AaConfig::Msaa16,
|
antialiasing_method: vello::AaConfig::Msaa16,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if self.ctx.painting_canvas.is_some() { eprintln!("[DRAW] overlay Vello render"); }
|
||||||
if let Ok(mut renderer) = shared.renderer.lock() {
|
if let Ok(mut renderer) = shared.renderer.lock() {
|
||||||
renderer.render_to_texture(device, queue, &scene, overlay_srgb_view, &overlay_params).ok();
|
renderer.render_to_texture(device, queue, &scene, overlay_srgb_view, &overlay_params).ok();
|
||||||
}
|
}
|
||||||
|
|
@ -2193,6 +2203,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
|
|
||||||
// Composite overlay onto HDR texture
|
// Composite overlay onto HDR texture
|
||||||
let overlay_layer = lightningbeam_core::gpu::CompositorLayer::normal(overlay_hdr_handle, 1.0);
|
let overlay_layer = lightningbeam_core::gpu::CompositorLayer::normal(overlay_hdr_handle, 1.0);
|
||||||
|
if self.ctx.painting_canvas.is_some() { eprintln!("[COMPOSITE] overlay onto HDR"); }
|
||||||
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
label: Some("overlay_composite_encoder"),
|
label: Some("overlay_composite_encoder"),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue