gemini feat: optimize drawing performance by caching layer styles during strokes and improve UI/UX for layer styles and menus, improved drop shadow
This commit is contained in:
@@ -190,6 +190,8 @@ pub struct Engine {
|
||||
tile_layers: Vec<Option<TiledLayer>>,
|
||||
svg_sources: std::collections::HashMap<u64, Vec<u8>>,
|
||||
filter_preview_original: Option<Vec<u8>>,
|
||||
stroke_effects_backup: Option<Vec<hcie_protocol::effects::LayerEffect>>,
|
||||
stroke_styles_backup: Option<Vec<hcie_protocol::LayerStyle>>,
|
||||
pub is_eraser: bool,
|
||||
/// Pooled stroke mask buffer. Zeroed on `begin_stroke()` and `end_stroke()`
|
||||
/// but **never dropped** between strokes. Reused if size matches, reallocated
|
||||
@@ -313,6 +315,8 @@ impl Engine {
|
||||
tile_layers: Vec::new(),
|
||||
svg_sources: std::collections::HashMap::new(),
|
||||
filter_preview_original: None,
|
||||
stroke_effects_backup: None,
|
||||
stroke_styles_backup: None,
|
||||
is_eraser: false,
|
||||
active_stroke_mask: None,
|
||||
composite_scratch: None,
|
||||
@@ -1717,6 +1721,47 @@ impl Engine {
|
||||
_ => true,
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure there is at least one effect in layer.effects if we have active styles,
|
||||
// so that hcie-composite's tiled.rs has_effects check evaluates to true.
|
||||
let has_enabled_styles = layer.styles.iter().any(|s| {
|
||||
match s {
|
||||
hcie_protocol::LayerStyle::DropShadow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::InnerShadow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::OuterGlow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::InnerGlow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::BevelEmboss { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::Satin { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::ColorOverlay { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::GradientOverlay { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::PatternOverlay { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::Stroke { enabled, .. } => *enabled,
|
||||
}
|
||||
});
|
||||
|
||||
// Remove any previous dummy effects we added
|
||||
layer.effects.retain(|e| {
|
||||
match e {
|
||||
hcie_protocol::effects::LayerEffect::DropShadow { noise, .. } if *noise == -999.0 => false,
|
||||
_ => true,
|
||||
}
|
||||
});
|
||||
|
||||
if has_enabled_styles && layer.effects.is_empty() {
|
||||
layer.effects.push(hcie_protocol::effects::LayerEffect::DropShadow {
|
||||
enabled: false,
|
||||
blend_mode: "Normal".to_string(),
|
||||
color: [0, 0, 0, 0],
|
||||
opacity: 0.0,
|
||||
angle: 0.0,
|
||||
distance: 0.0,
|
||||
spread: 0.0,
|
||||
size: 0.0,
|
||||
noise: -999.0, // Special sentinel
|
||||
contour: None,
|
||||
});
|
||||
}
|
||||
|
||||
layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release);
|
||||
layer.dirty = true;
|
||||
self.document.composite_dirty = true;
|
||||
@@ -1835,6 +1880,46 @@ impl Engine {
|
||||
_ => true,
|
||||
}
|
||||
});
|
||||
// Ensure there is at least one effect in layer.effects if we have active styles,
|
||||
// so that hcie-composite's tiled.rs has_effects check evaluates to true.
|
||||
let has_enabled_styles = layer.styles.iter().any(|s| {
|
||||
match s {
|
||||
hcie_protocol::LayerStyle::DropShadow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::InnerShadow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::OuterGlow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::InnerGlow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::BevelEmboss { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::Satin { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::ColorOverlay { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::GradientOverlay { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::PatternOverlay { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::Stroke { enabled, .. } => *enabled,
|
||||
}
|
||||
});
|
||||
|
||||
// Remove any previous dummy effects we added
|
||||
layer.effects.retain(|e| {
|
||||
match e {
|
||||
hcie_protocol::effects::LayerEffect::DropShadow { noise, .. } if *noise == -999.0 => false,
|
||||
_ => true,
|
||||
}
|
||||
});
|
||||
|
||||
if has_enabled_styles && layer.effects.is_empty() {
|
||||
layer.effects.push(hcie_protocol::effects::LayerEffect::DropShadow {
|
||||
enabled: false,
|
||||
blend_mode: "Normal".to_string(),
|
||||
color: [0, 0, 0, 0],
|
||||
opacity: 0.0,
|
||||
angle: 0.0,
|
||||
distance: 0.0,
|
||||
spread: 0.0,
|
||||
size: 0.0,
|
||||
noise: -999.0, // Special sentinel
|
||||
contour: None,
|
||||
});
|
||||
}
|
||||
|
||||
layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release);
|
||||
layer.dirty = true;
|
||||
did_remove = true;
|
||||
|
||||
@@ -239,13 +239,52 @@ impl Engine {
|
||||
// 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) {
|
||||
if let Some(raw) = self.raw_pixel_backup.get(&layer.id) {
|
||||
if raw.len() == layer.pixels.len() {
|
||||
layer.pixels.copy_from_slice(raw);
|
||||
}
|
||||
|
||||
// Check if there are any active/enabled effects or styles
|
||||
let has_enabled = layer.effects.iter().any(|e| {
|
||||
match e {
|
||||
hcie_protocol::effects::LayerEffect::DropShadow { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::InnerShadow { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::OuterGlow { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::InnerGlow { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::BevelEmboss { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::Satin { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::ColorOverlay { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::GradientOverlay { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::PatternOverlay { enabled, .. }
|
||||
| hcie_protocol::effects::LayerEffect::Stroke { enabled, .. } => *enabled,
|
||||
}
|
||||
}) || layer.styles.iter().any(|s| {
|
||||
match s {
|
||||
hcie_protocol::LayerStyle::DropShadow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::InnerShadow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::OuterGlow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::InnerGlow { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::BevelEmboss { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::Satin { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::ColorOverlay { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::GradientOverlay { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::PatternOverlay { enabled, .. }
|
||||
| hcie_protocol::LayerStyle::Stroke { enabled, .. } => *enabled,
|
||||
}
|
||||
});
|
||||
|
||||
if !has_enabled {
|
||||
// No active effects: clean up cached rendering and backup to restore raw performance.
|
||||
*layer.effects_cache.lock().unwrap() = None;
|
||||
self.raw_pixel_backup.remove(&layer.id);
|
||||
layer.effects_dirty.store(false, std::sync::atomic::Ordering::Release);
|
||||
layer.dirty = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if layer.effects_dirty.load(std::sync::atomic::Ordering::Acquire) {
|
||||
log::trace!("Applying active effects/styles for layer ID {} because effects are dirty", layer.id);
|
||||
// DON'T restore raw pixels from backup! Restoring raw pixels from backup overwrites
|
||||
// active stroke drawing and wipes out new strokes. Since we never overwrite layer.pixels
|
||||
// with the effects output, layer.pixels already contains the clean, raw pixels.
|
||||
let mut effects: Vec<hcie_fx::LayerEffect> = layer.effects.iter()
|
||||
.filter(|e| !matches!(e, hcie_protocol::effects::LayerEffect::DropShadow { noise, .. } if *noise == -999.0))
|
||||
.map(|e| hcie_fx::protocol_to_hcie_fx_effect(e))
|
||||
.collect();
|
||||
effects.extend(layer.styles.iter().filter_map(|s| hcie_fx::layer_style_to_effect(s)));
|
||||
@@ -262,9 +301,6 @@ impl Engine {
|
||||
width: layer.width,
|
||||
height: layer.height,
|
||||
});
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -127,11 +127,8 @@ impl Engine {
|
||||
tip.density,
|
||||
);
|
||||
layer.dirty = true;
|
||||
// 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);
|
||||
*layer.effects_cache.lock().unwrap() = None;
|
||||
}
|
||||
let dirty_r = match tip.style {
|
||||
// Star/Spray: spread was reduced to 0.65× so 1.8× covers the footprint.
|
||||
|
||||
@@ -126,7 +126,7 @@ impl Engine {
|
||||
pub fn begin_stroke(&mut self, layer_id: u64, x: f32, y: f32) {
|
||||
self.last_stroke_pos = Some((x, y, 1.0));
|
||||
self.sketch_history.clear();
|
||||
if let Some(layer) = self.document.get_layer_by_id(layer_id) {
|
||||
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
|
||||
let layer_pixels = (layer.width * layer.height * 4) as usize; // RGBA byte size
|
||||
let layer_size = (layer.width * layer.height) as usize; // pixel count (for mask)
|
||||
|
||||
@@ -143,6 +143,19 @@ impl Engine {
|
||||
}
|
||||
}
|
||||
|
||||
// Backup and clear layer.effects to disable expensive effect compositing during the stroke
|
||||
if !layer.effects.is_empty() {
|
||||
self.stroke_effects_backup = Some(layer.effects.clone());
|
||||
layer.effects.clear();
|
||||
// Clear the cache so it stops rendering the old effects immediately
|
||||
*layer.effects_cache.lock().unwrap() = None;
|
||||
}
|
||||
if !layer.styles.is_empty() {
|
||||
self.stroke_styles_backup = Some(layer.styles.clone());
|
||||
layer.styles.clear();
|
||||
*layer.effects_cache.lock().unwrap() = None;
|
||||
}
|
||||
|
||||
// Initialize last_stroke_bounds with brush radius around first point
|
||||
// Used for sub-rect snapshot in end_stroke (avoid 33MB clone on 4K)
|
||||
let tip = &self.current_tip;
|
||||
@@ -196,9 +209,20 @@ impl Engine {
|
||||
if let Some((id, before_shapes)) = self.stroke_before.take() {
|
||||
if id == layer_id {
|
||||
if let Some(idx) = self.document.layer_index_by_id(layer_id) {
|
||||
let layer = &self.document.layers[idx];
|
||||
let layer = &mut self.document.layers[idx];
|
||||
let lw = layer.width;
|
||||
let layer_pixels = (layer.width * layer.height * 4) as usize; // RGBA byte size
|
||||
// Restore layer effects from backup so they are re-composited correctly
|
||||
if let Some(backup) = self.stroke_effects_backup.take() {
|
||||
layer.effects = backup;
|
||||
}
|
||||
if let Some(backup) = self.stroke_styles_backup.take() {
|
||||
layer.styles = backup;
|
||||
}
|
||||
|
||||
if !layer.effects.is_empty() || !layer.styles.is_empty() {
|
||||
layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
|
||||
// Take the pooled before buffer (no allocation)
|
||||
log::debug!("[end_stroke] layer_pixels={}, stroke_before_buf={:?}", layer_pixels, self.stroke_before_buf.as_ref().map(|b| b.len()));
|
||||
@@ -286,6 +310,7 @@ impl Engine {
|
||||
self.last_stroke_bounds = None;
|
||||
self.last_stroke_pos = None;
|
||||
self.below_cache_dirty = false;
|
||||
self.document.composite_dirty = true;
|
||||
if let Some(mask) = &mut self.active_stroke_mask {
|
||||
mask.fill(0);
|
||||
}
|
||||
@@ -297,6 +322,7 @@ impl Engine {
|
||||
self.last_stroke_pos = None;
|
||||
self.last_stroke_bounds = None;
|
||||
self.below_cache_dirty = false;
|
||||
self.document.composite_dirty = true;
|
||||
log::trace!("[end_stroke_async] fallback cleanup done, below_cache retained for reuse");
|
||||
if let Some(mask) = &mut self.active_stroke_mask {
|
||||
mask.fill(0);
|
||||
|
||||
Reference in New Issue
Block a user