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,70 @@
# Snapshot Freeze Analysis — Çizim Bitimindeki Takılma
## Problem
Fırça darbesi bittiğinde (pen-up), UI thread kısa süreli donuyor. Bu takılma snapshot (geri alım) mekanizmasının UI thread'de yaptığı büyük bellek tahsislerinden kaynaklanıyor.
## Kök Neden
`end_stroke()` ve `begin_stroke()` fonksiyonları UI thread'de çalışır ve her biri ~33MB'lik `layer.pixels.clone()` çağrısı yapar. 4K canvas'ta (3840×2160×4 = ~33MB) bu kopyalamalar allocate+copy gerektirir.
## Suçlu İşlemler
| # | İşlem | Dosya:Satır | Boyut | Etki |
|---|-------|-------------|-------|------|
| 1 | `layer.pixels.clone()` — before snapshot | `stroke_cache.rs:125` | ~33MB alloc+copy | Yüksek |
| 2 | `layer.pixels.clone()` — after snapshot | `stroke_cache.rs:159` | ~33MB alloc+copy | **En yüksek** |
| 3 | `active_stroke_mask.fill(0)` | `stroke_cache.rs:224` | ~8MB sıfırlama | Orta |
| 4 | `raw_pixel_backup.remove()` — drop | `stroke_cache.rs:219` | ~33MB deallocation | Orta |
| 5 | `apply_effects_and_sync_tiles()` | `partial_composite.rs:206-268` | ~99MB+ (efektli katmanlarda) | Yüksek |
## Akış Diyagramı
```
begin_stroke() end_stroke()
───────────── ─────────────
1. mask.fill(0) [pooled, ~8MB] 1. before = stroke_before.take()
2. before = layer.pixels.clone() [33MB] 2. after = layer.pixels.clone() [33MB] ← DONMA
3. stroke_before = Some(before) 3. thread::spawn(move || { ... })
4. rebuild_below_cache() 4. mask.fill(0) [pooled]
5. raw_pixel_backup.remove()
6. cached_selection_mask = None
```
## Mevcut Havuz Mekanizmaları (İyi)
| Buffer | Boyut | Havuzda mı? | Not |
|--------|-------|-------------|-----|
| `active_stroke_mask` | ~8MB | Evet | `begin_stroke`'da fill(0), stroke arası korunur |
| `composite_scratch` | ~33MB | Evet | Render sırasında kullanılır, serbest bırakılmaz |
| `below_cache` | ~33MB | Evet | Stroke arası korunur, `below_cache_dirty` ile yönetilir |
## Havuzlanmayan Buffer'lar (Kötü)
| Buffer | Boyut | Neden havuzlanmıyor? |
|--------|-------|----------------------|
| `stroke_before` (before snapshot) | ~33MB | Her `begin_stroke`'da `clone()`, her `end_stroke`'da consume edilir |
| `stroke_after` (after snapshot) | ~33MB | Her `end_stroke`'da `clone()`, background thread'e move edilir |
## Kritik Gözlem
`end_stroke()` çağrıldığında fırça darbesi bitmiştir. Bir sonraki `begin_stroke()`'a kadar `layer.pixels`'e hiçbir yazılmaz. Bu, background thread'in raw pointer ile okuma yapabileceği anlamına gelir — unsafe ama güvenli.
## Optimize Edilmiş Akış (Hedef)
```
begin_stroke() end_stroke()
───────────── ─────────────
1. mask.fill(0) [pooled] 1. before = pooled buffer'dan take
2. copy layer.pixels → pooled buf 2. copy layer.pixels → pooled after buf
3. stroke_before = Some(pooled buf) 3. both buflar → background thread
4. rebuild_below_cache() 4. mask.fill(0) [pooled]
5. background thread: sub-rect çıkar, buffer'ları geri gönder
```
## Dosyalar
- `hcie-engine-api/src/stroke_cache.rs` — begin_stroke, end_stroke
- `hcie-engine-api/src/lib.rs` — Engine struct tanımı
- `hcie-document/src/lib.rs` — push_draw_snapshot, push_draw_snapshot_subrect
- `hcie-protocol/src/lib.rs` — Layer struct tanımı