This commit is contained in:
2026-07-13 06:40:14 +03:00
parent a70bedd6bb
commit 356fac571d
18 changed files with 2360 additions and 471 deletions
+14
View File
@@ -1720,6 +1720,16 @@ impl Engine {
layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release);
layer.dirty = true;
self.document.composite_dirty = true;
// Mark the full canvas as dirty so the next render recomposites.
// Without this, dirty_bounds stays None and partial_composite forces
// a full-canvas recomposite anyway — but other property ops set this
// explicitly so we should too for consistency and correctness.
let w = self.document.canvas_width;
let h = self.document.canvas_height;
self.document.dirty_bounds = Some([0, 0, w, h]);
// Invalidate the below-layer composite cache since this layer's
// pixels changed via the effects pipeline.
self.below_cache_dirty = true;
}
}
@@ -1836,6 +1846,10 @@ impl Engine {
}
if did_remove {
self.document.composite_dirty = true;
let w = self.document.canvas_width;
let h = self.document.canvas_height;
self.document.dirty_bounds = Some([0, 0, w, h]);
self.below_cache_dirty = true;
}
if empty_styles {
self.raw_pixel_backup.remove(&id);
+7 -1
View File
@@ -233,6 +233,10 @@ impl Engine {
// Pass 2: Apply layer effects. Restores from backup first so that each
// slider edit always applies on top of the original untouched pixels.
// CRITICAL: Do NOT modify layer.pixels with effects output. layer.pixels
// must always contain the raw drawing data so that new strokes are drawn
// on top of clean pixels (not on effects-applied pixels). The composite
// uses effects_cache for layers with effects, not layer.pixels.
for layer in &mut self.document.layers {
if layer.effects.is_empty() && layer.styles.is_empty() { continue; }
if layer.effects_dirty.load(std::sync::atomic::Ordering::Acquire) {
@@ -258,7 +262,9 @@ impl Engine {
width: layer.width,
height: layer.height,
});
layer.pixels = processed;
// DON'T: layer.pixels = processed;
// layer.pixels stays as raw drawing data. The composite pipeline
// in tiled.rs reads from effects_cache when it exists.
layer.effects_dirty.store(false, std::sync::atomic::Ordering::Release);
layer.dirty = true;
}
+3 -7
View File
@@ -127,9 +127,9 @@ impl Engine {
tip.density,
);
layer.dirty = true;
// Effects are expensive (33MB clone + apply_layer_effects on 4K).
// Only mark the layer as needing an effects pass if it actually
// has any effects or editable styles.
// Mark effects_dirty so the effects pipeline re-runs after the
// stroke ends. This is safe because apply_effects_and_sync_tiles
// no longer modifies layer.pixels — it only updates effects_cache.
if !layer.effects.is_empty() || !layer.styles.is_empty() {
layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release);
}
@@ -166,8 +166,6 @@ impl Engine {
self.stroke_before_buf.as_deref(),
);
layer.dirty = true;
// Avoid triggering the expensive effects pass for layers that
// have no effects/styles.
if !layer.effects.is_empty() || !layer.styles.is_empty() {
layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release);
}
@@ -200,8 +198,6 @@ impl Engine {
let px_color = [color[0], color[1], color[2], (color[3] as f32 * tip.opacity * pressure).round() as u8];
draw_line(layer, x0, y0, x1, y1, px_color, tip.size, mask_ref);
layer.dirty = true;
// Only request an effects re-render if the layer actually has
// effects or editable styles.
if !layer.effects.is_empty() || !layer.styles.is_empty() {
layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release);
}