feat: implement layer mask editing support with scratch buffer and coordinate normalization
mandatory-regression-gate / deterministic-tests (push) Has been cancelled
mandatory-regression-gate / protected-performance-path (push) Has been cancelled

This commit is contained in:
Your Name
2026-07-24 06:01:30 +03:00
parent 7496f5e208
commit 505562424b
16 changed files with 1224 additions and 155 deletions
+134 -17
View File
@@ -59,6 +59,10 @@ pub struct PendingHistoryItem {
pub return_before: Option<Vec<u8>>,
/// Full-layer after buffer returned from background thread for pool reuse.
pub return_after: Option<Vec<u8>>,
/// Grayscale before mask buffer returned from background thread for pool reuse.
pub return_mask_before: Option<Vec<u8>>,
/// Indicates if this snapshot represents a mask edit rather than a pixel edit.
pub is_mask: bool,
}
impl Engine {
@@ -98,8 +102,33 @@ impl Engine {
self.stroke_before_buf = Some(buf);
}
}
if let Some(buf) = item.return_mask_before {
if self
.mask_before_buf
.as_ref()
.map_or(true, |b| b.len() != buf.len())
{
self.mask_before_buf = Some(buf);
}
}
if let Some(bounds) = item.bounds {
if item.is_mask {
if let Some(bounds) = item.bounds {
self.document.push_mask_draw_snapshot_subrect(
item.layer_idx,
item.before_pixels,
item.after_pixels,
(bounds[0], bounds[1], bounds[2], bounds[3]),
item.description,
);
} else {
self.document.push_mask_draw_snapshot(
item.layer_idx,
item.before_pixels,
item.description,
);
}
} else if let Some(bounds) = item.bounds {
self.document.push_draw_snapshot_subrect(
item.layer_idx,
item.before_pixels,
@@ -191,12 +220,71 @@ impl Engine {
// Pool the "before" pixel buffer: reuse if size matches, otherwise allocate.
// This avoids a ~33MB allocation per stroke on 4K canvases.
match &mut self.stroke_before_buf {
Some(buf) if buf.len() == layer_pixels => {
buf.copy_from_slice(&layer.pixels);
if self.editing_mask {
let mut rgba_buf = match self.stroke_before_buf.take() {
Some(mut b) if b.len() == layer_pixels => {
b.fill(255);
b
}
_ => vec![255u8; layer_pixels],
};
let mut gray_buf = match self.mask_before_buf.take() {
Some(mut b) if b.len() == layer_size => {
b.fill(255);
b
}
_ => vec![255u8; layer_size],
};
let ch = layer.height as usize;
let cw = layer.width as usize;
let full_size = cw * ch;
if let Some(ref mut mask) = layer.mask_pixels {
if mask.len() != full_size {
let mut full_mask = vec![layer.mask_default_color; full_size];
if let Some(mb) = layer.mask_bounds {
let (m_top, m_left, m_bottom, m_right) =
(mb[0].max(0) as u32, mb[1].max(0) as u32, mb[2].max(0) as u32, mb[3].max(0) as u32);
let mw = (m_right - m_left) as usize;
let mh = (m_bottom - m_top) as usize;
if mw > 0 && mh > 0 && mask.len() == mw * mh {
for y in 0..mh {
let gy = m_top as usize + y;
if gy >= ch { break; }
for x in 0..mw {
let gx = m_left as usize + x;
if gx >= cw { break; }
full_mask[gy * cw + gx] = mask[y * mw + x];
}
}
}
}
*mask = full_mask;
layer.mask_bounds = Some([0, 0, ch as i32, cw as i32]);
}
}
_ => {
self.stroke_before_buf = Some(layer.pixels.clone());
if let Some(ref mask) = layer.mask_pixels {
gray_buf.copy_from_slice(mask);
for (i, &v) in mask.iter().enumerate() {
let off = i * 4;
rgba_buf[off] = v;
rgba_buf[off + 1] = v;
rgba_buf[off + 2] = v;
rgba_buf[off + 3] = 255;
}
}
self.stroke_before_buf = Some(rgba_buf);
self.mask_before_buf = Some(gray_buf);
} else {
match &mut self.stroke_before_buf {
Some(buf) if buf.len() == layer_pixels => {
buf.copy_from_slice(&layer.pixels);
}
_ => {
self.stroke_before_buf = Some(layer.pixels.clone());
}
}
}
let before_shapes = if let LayerData::Vector { shapes } = &layer.data {
@@ -254,17 +342,35 @@ impl Engine {
layer_pixels,
self.stroke_before_buf.as_ref().map(|b| b.len())
);
let before = match self.stroke_before_buf.take() {
Some(buf) if buf.len() == layer_pixels => buf,
_ => vec![0u8; layer_pixels],
let mut mask_before = None;
let before = if self.editing_mask {
let gray_len = lw as usize * layer.height as usize;
let mb = match self.mask_before_buf.take() {
Some(buf) if buf.len() == gray_len => buf,
_ => vec![255u8; gray_len],
};
mask_before = Some(mb.clone());
mb
} else {
match self.stroke_before_buf.take() {
Some(buf) if buf.len() == layer_pixels => buf,
_ => vec![0u8; layer_pixels],
}
};
log::debug!("[end_stroke] before.len()={}, lw={}", before.len(), lw);
// Layer 3: Zero-copy after-snapshot via raw pointer.
// SAFETY: After end_stroke(), no mutations happen to layer.pixels
// until the next begin_stroke(). The background thread only reads.
let after_ptr = SendPtr::new(layer.pixels.as_ptr());
let after_len = layer.pixels.len();
let (after_ptr, after_len) = if self.editing_mask {
if layer.mask_pixels.is_none() {
layer.mask_pixels = Some(vec![255; (layer.width * layer.height) as usize]);
}
let mask = layer.mask_pixels.as_ref().unwrap();
(SendPtr::new(mask.as_ptr()), mask.len())
} else {
(SendPtr::new(layer.pixels.as_ptr()), layer.pixels.len())
};
let after_shapes = if let LayerData::Vector { shapes } = &layer.data {
Some(shapes.clone())
@@ -275,6 +381,7 @@ impl Engine {
let style = self.current_tip.style;
let bounds = self.last_stroke_bounds;
let pending_history = self.pending_history.clone();
let is_mask = self.editing_mask;
std::thread::spawn(move || {
let t_start = std::time::Instant::now();
@@ -290,25 +397,33 @@ impl Engine {
before_shapes,
after_shapes,
description: format!(
"Brush Stroke ({})",
"{} ({})",
if is_mask { "Mask Edit" } else { "Brush Stroke" },
crate::stroke_brush::brush_style_label(style)
),
return_before: None,
return_after: None,
return_mask_before: mask_before,
is_mask,
};
if let Some([sx0, sy0, sx1, sy1]) = bounds {
if sx0 < sx1 && sy0 < sy1 {
let rw = sx1 - sx0;
let rh = sy1 - sy0;
let rect_size = (rw * rh * 4) as usize;
let rect_size = if is_mask {
(rw * rh) as usize
} else {
(rw * rh * 4) as usize
};
log::debug!("[end_stroke_bg] bounds=[{},{},{},{}], rw={}, rh={}, rect_size={}, before.len()={}, after.len()={}, lw={}", sx0, sy0, sx1, sy1, rw, rh, rect_size, before.len(), after_slice.len(), lw);
let mut before_rect = vec![0u8; rect_size];
let mut after_rect = vec![0u8; rect_size];
let bpp = if is_mask { 1 } else { 4 };
for row in 0..rh {
let src_start = (((sy0 + row) * lw + sx0) * 4) as usize;
let dst_start = (row * rw * 4) as usize;
let len = (rw * 4) as usize;
let src_start = (((sy0 + row) * lw + sx0) * bpp) as usize;
let dst_start = (row * rw * bpp) as usize;
let len = (rw * bpp) as usize;
before_rect[dst_start..dst_start + len]
.copy_from_slice(&before[src_start..src_start + len]);
after_rect[dst_start..dst_start + len]
@@ -320,7 +435,9 @@ impl Engine {
item.after_pixels = after_rect;
item.bounds = Some([sx0, sy0, sx1, sy1]);
// Return before buffer to pool (after is a borrowed pointer)
item.return_before = Some(before);
if !is_mask {
item.return_before = Some(before);
}
pending_history.lock().unwrap().push(item);
}
log::trace!(