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:
@@ -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) |
|
||||
Reference in New Issue
Block a user