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:
2026-07-13 14:37:22 +03:00
parent 356fac571d
commit 2284a7d81f
50 changed files with 777 additions and 239 deletions
+85
View File
@@ -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;
+44 -8
View File
@@ -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;
}
+1 -4
View File
@@ -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.
+28 -2
View File
@@ -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);