Save: skip re-encoding unchanged raster keyframes
save_beam re-encoded every resident raster keyframe to PNG (+ proxy) on every save, even untouched frames — the dominant per-save cost for painting/animation projects (the code noted this as deferred "Phase 3"). The infrastructure to do it incrementally already exists: kf.dirty means "current pixels not yet in the container" (set on any edit, cleared on a successful save, per main.rs), and it's preserved in the document clone the save worker receives. Gate the encode on it: a clean keyframe already stored keeps its full + proxy rows untouched; only dirty (or not-yet-stored) frames are re-encoded. Media blobs were already incremental; this closes the raster gap. No new data-loss risk: the mid-save-edit race (edit between the document clone and save completion) is pre-existing and identical to the old full-write path.
This commit is contained in:
parent
a2839f80b1
commit
d6b86a14b1
|
|
@ -399,16 +399,29 @@ pub fn save_beam(
|
|||
}
|
||||
|
||||
// --- raster keyframes -> media rows (PNG), keyed by keyframe id ---
|
||||
// (Phase 0 writes all resident frames each save; a disk-dirty flag to skip
|
||||
// unchanged frames in place is deferred to Phase 3.)
|
||||
// Incremental: only (re)encode a keyframe whose pixels changed since the last save.
|
||||
// `kf.dirty` means "current pixels are not yet in the container" (set on any edit,
|
||||
// cleared on a successful save — see main.rs); a clean frame already stored is kept
|
||||
// in place, skipping the PNG re-encode of every resident frame on every save.
|
||||
// Walk ALL layers (incl. nested in groups/clips) so nested raster keyframes
|
||||
// are persisted too, and so `live_media` covers them — matching the load path,
|
||||
// which arms `needs_fault_in` recursively. Top-level-only projects are unaffected.
|
||||
let mut raster_count = 0usize;
|
||||
let mut raster_skipped = 0usize;
|
||||
for layer in document.all_layers() {
|
||||
if let crate::layer::AnyLayer::Raster(rl) = layer {
|
||||
for kf in &rl.keyframes {
|
||||
if !kf.raw_pixels.is_empty() {
|
||||
// Clean + already stored → keep the existing full + proxy rows untouched.
|
||||
if !kf.dirty && txn.media_exists(kf.id)? {
|
||||
live_media.insert(kf.id);
|
||||
let proxy_id = raster_proxy_media_id(kf.id);
|
||||
if txn.media_exists(proxy_id)? {
|
||||
live_media.insert(proxy_id);
|
||||
}
|
||||
raster_skipped += 1;
|
||||
continue;
|
||||
}
|
||||
let img =
|
||||
crate::brush_engine::image_from_raw(kf.raw_pixels.clone(), kf.width, kf.height);
|
||||
match crate::brush_engine::encode_png(&img) {
|
||||
|
|
@ -608,9 +621,10 @@ pub fn save_beam(
|
|||
}
|
||||
|
||||
eprintln!(
|
||||
"📊 [SAVE_BEAM] ✅ Saved {} audio + {} raster media, {} orphans removed, in {:.2}ms",
|
||||
"📊 [SAVE_BEAM] ✅ Saved {} audio + {} raster media ({} unchanged frames skipped), {} orphans removed, in {:.2}ms",
|
||||
audio_pool_entries.len(),
|
||||
raster_count,
|
||||
raster_skipped,
|
||||
removed,
|
||||
fn_start.elapsed().as_secs_f64() * 1000.0
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue