fix: optimize brush stroke handling and improve cursor preview

- Changed default VISION_BACKEND from CPU to GPU for better performance.
- Made `map_brush_style` public to allow external access.
- Updated `Engine` struct to use pooled buffers for stroke snapshots, reducing memory allocations during brush strokes.
- Introduced `CompositeSnapshot` for efficient background compositing without blocking the UI thread.
- Implemented `SendPtr` for safe raw pointer handling across threads.
- Enhanced `commit_pending_history` to recycle buffers and improve performance.
- Fixed cursor preview issues for procedural brushes by removing unnecessary preset synchronization.
- Added tests and documentation for new features and performance improvements.
This commit is contained in:
2026-07-11 08:53:17 +03:00
parent 089ce00b49
commit 0d1b0eae4c
14 changed files with 1021 additions and 305 deletions
@@ -0,0 +1,72 @@
# Brush Stroke End Freeze Fix + Cursor Preview Fix
## Issue 1: Stroke End Freeze (Partially Fixed)
### Current State
- During drawing: canvas updates correctly (synchronous composite)
- After stroke end: still freezes due to `create_composite_snapshot()` being expensive
### Root Cause
`create_composite_snapshot()` in `partial_composite.rs:354` clones ALL layers (33MB per layer on 4K) and ALL tile layers. For a 10-layer 4K document, this is ~330MB of cloning on the UI thread.
### Fix: Skip background composite for brush strokes, use synchronous composite
**File:** `hcie-egui-app/crates/hcie-gui-egui/src/canvas/render.rs`
In `render_composition()`, the after-stroke-end path currently spawns a background thread. Instead:
- Do the synchronous composite directly (like during drawing)
- Only use background thread for very expensive operations (filters, fills)
- This eliminates the freeze for brush strokes
```rust
// After stroke end: do synchronous composite (not background)
// Background thread is too expensive due to create_composite_snapshot()
let (region, buf_ptr, buf_size) = doc.engine.render_composite_region();
// ... upload to texture ...
doc.engine.clear_dirty_flags();
```
## Issue 2: Brush Cursor Preview Shows Wrong Shape
### Current State
- ABR (bitmap) brushes: correct preview
- Procedural brushes (Round, Square, Star, etc.): shows as small circle
- After using ABR brush: preview gets stuck on ABR shape
### Root Cause
The `brushes_panel.rs` changes (from other AI) added `active_brush_preset_id` sync that overrides the brush style. When a procedural style cell is clicked:
1. `state.tool_configs.brush.style` is set correctly
2. But then `active_brush_preset_id` is synced to a preset matching that style
3. If no matching preset exists, or if the preset has a different style, the preview gets confused
### Fix: Remove the `active_brush_preset_id` sync
**File:** `hcie-egui-app/crates/hcie-gui-egui/src/app/tools/brushes_panel.rs`
Remove the 7-line block that was added:
```rust
// Sync active_brush_preset_id to a preset matching this style
// so the cursor preview shows the correct brush tip.
if let Some(matching) = state.brush_presets.iter().find(|p| {
to_tools_style(map_brush_style(p.tip.style)) == style
}) {
state.active_brush_preset_id = matching.id.clone();
}
```
The cursor preview already uses `state.tool_configs.brush.style` directly, not the preset. This sync is unnecessary and causes the preview to get stuck.
## Files to Modify
| File | Change |
|------|--------|
| `hcie-egui-app/crates/hcie-gui-egui/src/canvas/render.rs` | Use synchronous composite after stroke end |
| `hcie-egui-app/crates/hcie-gui-egui/src/app/tools/brushes_panel.rs` | Remove `active_brush_preset_id` sync |
## Verification
1. **Stroke freeze test:** Draw rapid brush strokes — no visible freeze on stroke end
2. **Cursor preview test:** Switch between procedural brushes (Round, Square, Star) — preview should show correct shape
3. **ABR transition test:** Use ABR brush, then switch to procedural — preview should update correctly
4. **Visual regression:** `cargo test -p hcie-engine-api --test visual_regression`
5. **Performance test:** `cargo test -p hcie-engine-api --test performance_stroke_4k -- --nocapture`
@@ -0,0 +1,66 @@
# Double-Buffering Plan: Çizim Bitimindeki Takılmayı Giderme — TAMAMLANDI (Katman 1+2+3)
## Yapılan Değişiklikler
### 1. Engine Struct Güncellendi (`lib.rs`)
- `stroke_before: Option<(u64, Vec<u8>, ...)>``stroke_before: Option<(u64, Option<Vec<VectorShape>>)>`
- Yeni: `stroke_before_buf: Option<Vec<u8>>` — havuzlu before buffer
### 2. SendPtr Wrapper Eklendi (`stroke_cache.rs`)
- `*const u8` yerine `usize` kullanarak `Send` trait'ini çözdük
- Background thread'e raw pointer güvenli bir şekilde gönderiliyor
### 3. PendingHistoryItem Güncellendi (`stroke_cache.rs`)
- `return_before: Option<Vec<u8>>` — background thread'den buffer geri dönüşü
### 4. begin_stroke() Güncellendi (`stroke_cache.rs`)
- `layer.pixels.clone()``copy_from_slice` ile havuzlu buffer'a kopyalama
- İlk stroke'ta ~33MB alloc, sonraki stroke'larda sadece copy
### 5. end_stroke() Güncellendi (`stroke_cache.rs`) — Katman 3
- Before buffer: havuzlu buffer'dan take (sıfır alloc)
- After buffer: **sıfır kopya**`SendPtr` ile raw pointer background thread'e gönderiliyor
- Background thread `unsafe { slice::from_raw_parts(...) }` ile okuyor
### 6. commit_pending_history() Güncellendi (`stroke_cache.rs`)
- Return edilen before buffer'ı havuza geri koyar
### 7. stroke_brush.rs Güncellendi
- `stroke_before.as_ref().map(|(_, px, _)| px.as_slice())``stroke_before_buf.as_deref()`
## Performans Etkisi
| Metrik | Önce | Sonra (Katman 1+2+3) |
|--------|------|-------|
| begin_stroke alloc | ~33MB (her stroke) | İlk stroke'ta ~33MB, sonrasında 0 alloc |
| end_stroke alloc | ~33MB (her stroke) | **Sıfır** (pointer paylaşımı) |
| end_stroke kopya | ~33MB | **Sıfır** (zero-copy after snapshot) |
| Toplam alloc/stroke | ~66MB | **~33MB** (sadece before) |
**Katman 3 Kazanımı:** `end_stroke()`'daki ~33MB alloc+copy tamamen ortadan kalktı.
## Teknik Çözüm: SendPtr
Rust'ta `*const u8` `Send` trait'ini implement etmiyor. Çözüm:
```rust
struct SendPtr(usize); // usize her zaman Send
impl SendPtr {
fn new(p: *const u8) -> Self { Self(p as usize) }
fn as_ptr(&self) -> *const u8 { self.0 as *const u8 }
}
unsafe impl Send for SendPtr {}
```
## Test Sonuçları
-`bitmap_brush_stamp_shape` — başarılı
-`visual_regression` (8 test) — başarılı
-`performance_stroke_4k` — başarılı (1.95s toplam)
- ✅ GUI derlemesi — başarılı
## Dosya Değişiklikleri
| Dosya | Değişiklik |
|-------|------------|
| `hcie-engine-api/src/lib.rs` | Engine struct + init + docstring |
| `hcie-engine-api/src/stroke_cache.rs` | SendPtr + begin_stroke + end_stroke + commit_pending_history |
| `hcie-engine-api/src/stroke_brush.rs` | stroke_before referansı güncellendi |
| `docs/compose/specs/snapshot-freeze-analysis.md` | Bulgular dosyası (yeni) |