Phase 4 Tier 1: lazy image-asset bytes paged from the container
Project load no longer eager-reads all image bytes — `ImageAsset.data` stays empty and the renderer's ImageCache pages compressed bytes from the `.beam` on a decode miss (read_packed_media_readonly by asset id), decoding into the byte-bounded Tier-2 cache. Result: instant load, and compressed bytes don't accumulate on the heap. - ImageCache: `container_path` + `resolve_bytes` (asset.data if resident — fresh import or old base64 project — else page from the container); decoders take `&[u8]` and use the decoded dimensions. - Container path threaded App.current_file_path → SharedPaneState → VelloRenderContext, set on the cache each prepare. - load_beam_sqlite drops the 3.5b eager read. (Refinement: a persistent read connection instead of open-per-miss.)
This commit is contained in:
parent
d05cbfcde5
commit
3d0a334014
|
|
@ -651,18 +651,10 @@ fn load_beam_sqlite(path: &Path) -> Result<LoadedProject, String> {
|
||||||
}
|
}
|
||||||
let _ = proxy_load_count;
|
let _ = proxy_load_count;
|
||||||
|
|
||||||
// Eager-read image-asset bytes from the container into `data` (Phase 4 will make
|
// Image-asset bytes are NOT eagerly read (Phase 4 Tier 1 paging): `ImageAsset.data`
|
||||||
// this lazy + LRU). Old projects keep their base64-deserialized `data` and have no
|
// stays empty and the renderer's ImageCache pages bytes from the container on a
|
||||||
// container row — those are skipped (`data` already Some).
|
// decode miss (keyed by asset id). Old base64 projects keep their deserialized
|
||||||
for (id, asset) in document.image_assets.iter_mut() {
|
// `data` (no container row). Loading is instant; only rendered images touch disk.
|
||||||
if asset.data.is_none() {
|
|
||||||
if let Ok(Some(_)) = archive.media_info(*id) {
|
|
||||||
if let Ok(bytes) = archive.read_media_full(*id) {
|
|
||||||
asset.data = Some(bytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Missing external files (referenced entries whose file no longer exists).
|
// Missing external files (referenced entries whose file no longer exists).
|
||||||
let project_dir = path.parent().unwrap_or_else(|| Path::new("."));
|
let project_dir = path.parent().unwrap_or_else(|| Path::new("."));
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ pub struct ImageCache {
|
||||||
/// session uses one path) and the running total.
|
/// session uses one path) and the running total.
|
||||||
sizes: HashMap<Uuid, usize>,
|
sizes: HashMap<Uuid, usize>,
|
||||||
bytes: usize,
|
bytes: usize,
|
||||||
|
/// `.beam` container path for lazily loading compressed `ImageAsset` bytes on a
|
||||||
|
/// decode miss (Tier 1 paging) when `asset.data` isn't resident.
|
||||||
|
container_path: Option<std::path::PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageCache {
|
impl ImageCache {
|
||||||
|
|
@ -50,9 +53,29 @@ impl ImageCache {
|
||||||
lru: Vec::new(),
|
lru: Vec::new(),
|
||||||
sizes: HashMap::new(),
|
sizes: HashMap::new(),
|
||||||
bytes: 0,
|
bytes: 0,
|
||||||
|
container_path: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the `.beam` container path used to lazily load image bytes that aren't
|
||||||
|
/// resident in `asset.data` (Tier 1 paging). Cheap to call each frame.
|
||||||
|
pub fn set_container_path(&mut self, path: Option<std::path::PathBuf>) {
|
||||||
|
self.container_path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve an asset's compressed bytes: prefer the resident `asset.data` (imported
|
||||||
|
/// this session, or an old base64 project), else page from the container.
|
||||||
|
fn resolve_bytes<'a>(&self, asset: &'a ImageAsset) -> Option<std::borrow::Cow<'a, [u8]>> {
|
||||||
|
if let Some(d) = &asset.data {
|
||||||
|
return Some(std::borrow::Cow::Borrowed(d.as_slice()));
|
||||||
|
}
|
||||||
|
let path = self.container_path.as_ref()?;
|
||||||
|
crate::beam_archive::read_packed_media_readonly(path, asset.id)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(std::borrow::Cow::Owned)
|
||||||
|
}
|
||||||
|
|
||||||
/// Mark `id` (size `size` bytes) as most-recently-used; evict LRU entries over budget.
|
/// Mark `id` (size `size` bytes) as most-recently-used; evict LRU entries over budget.
|
||||||
fn touch(&mut self, id: Uuid, size: usize) {
|
fn touch(&mut self, id: Uuid, size: usize) {
|
||||||
if !self.sizes.contains_key(&id) {
|
if !self.sizes.contains_key(&id) {
|
||||||
|
|
@ -82,8 +105,9 @@ impl ImageCache {
|
||||||
return Some(cached);
|
return Some(cached);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode and cache
|
// Decode and cache (bytes from asset.data or paged from the container).
|
||||||
let image = decode_image_asset(asset)?;
|
let bytes = self.resolve_bytes(asset)?;
|
||||||
|
let image = decode_image_brush(&bytes)?;
|
||||||
let arc_image = Arc::new(image);
|
let arc_image = Arc::new(image);
|
||||||
self.cache.insert(asset.id, Arc::clone(&arc_image));
|
self.cache.insert(asset.id, Arc::clone(&arc_image));
|
||||||
self.touch(asset.id, size);
|
self.touch(asset.id, size);
|
||||||
|
|
@ -98,7 +122,8 @@ impl ImageCache {
|
||||||
return Some(cached);
|
return Some(cached);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pixmap = decode_image_to_pixmap(asset)?;
|
let bytes = self.resolve_bytes(asset)?;
|
||||||
|
let pixmap = decode_image_to_pixmap(&bytes)?;
|
||||||
let arc = Arc::new(pixmap);
|
let arc = Arc::new(pixmap);
|
||||||
self.cpu_cache.insert(asset.id, Arc::clone(&arc));
|
self.cpu_cache.insert(asset.id, Arc::clone(&arc));
|
||||||
self.touch(asset.id, size);
|
self.touch(asset.id, size);
|
||||||
|
|
@ -133,12 +158,12 @@ impl Default for ImageCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode an image asset to a premultiplied tiny-skia Pixmap (CPU render path).
|
/// Decode image bytes to a premultiplied tiny-skia Pixmap (CPU render path).
|
||||||
fn decode_image_to_pixmap(asset: &ImageAsset) -> Option<tiny_skia::Pixmap> {
|
fn decode_image_to_pixmap(data: &[u8]) -> Option<tiny_skia::Pixmap> {
|
||||||
let data = asset.data.as_ref()?;
|
|
||||||
let img = image::load_from_memory(data).ok()?;
|
let img = image::load_from_memory(data).ok()?;
|
||||||
let rgba = img.to_rgba8();
|
let rgba = img.to_rgba8();
|
||||||
let mut pixmap = tiny_skia::Pixmap::new(asset.width, asset.height)?;
|
let (iw, ih) = rgba.dimensions();
|
||||||
|
let mut pixmap = tiny_skia::Pixmap::new(iw, ih)?;
|
||||||
for (dst, src) in pixmap.pixels_mut().iter_mut().zip(rgba.pixels()) {
|
for (dst, src) in pixmap.pixels_mut().iter_mut().zip(rgba.pixels()) {
|
||||||
let [r, g, b, a] = src.0;
|
let [r, g, b, a] = src.0;
|
||||||
// Convert straight alpha (image crate output) to premultiplied (tiny-skia internal format)
|
// Convert straight alpha (image crate output) to premultiplied (tiny-skia internal format)
|
||||||
|
|
@ -152,21 +177,17 @@ fn decode_image_to_pixmap(asset: &ImageAsset) -> Option<tiny_skia::Pixmap> {
|
||||||
Some(pixmap)
|
Some(pixmap)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode an image asset to peniko ImageBrush
|
/// Decode image bytes to a peniko ImageBrush (GPU render path).
|
||||||
fn decode_image_asset(asset: &ImageAsset) -> Option<ImageBrush> {
|
fn decode_image_brush(data: &[u8]) -> Option<ImageBrush> {
|
||||||
// Get the raw file data
|
|
||||||
let data = asset.data.as_ref()?;
|
|
||||||
|
|
||||||
// Decode using the image crate
|
|
||||||
let img = image::load_from_memory(data).ok()?;
|
let img = image::load_from_memory(data).ok()?;
|
||||||
let rgba = img.to_rgba8();
|
let rgba = img.to_rgba8();
|
||||||
|
let (iw, ih) = rgba.dimensions();
|
||||||
|
|
||||||
// Create peniko ImageData then ImageBrush
|
|
||||||
let image_data = ImageData {
|
let image_data = ImageData {
|
||||||
data: Blob::from(rgba.into_raw()),
|
data: Blob::from(rgba.into_raw()),
|
||||||
format: ImageFormat::Rgba8,
|
format: ImageFormat::Rgba8,
|
||||||
width: asset.width,
|
width: iw,
|
||||||
height: asset.height,
|
height: ih,
|
||||||
alpha_type: ImageAlphaType::Alpha,
|
alpha_type: ImageAlphaType::Alpha,
|
||||||
};
|
};
|
||||||
Some(ImageBrush::new(image_data))
|
Some(ImageBrush::new(image_data))
|
||||||
|
|
|
||||||
|
|
@ -6472,6 +6472,7 @@ impl eframe::App for EditorApp {
|
||||||
// Create render context
|
// Create render context
|
||||||
let mut ctx = RenderContext {
|
let mut ctx = RenderContext {
|
||||||
shared: panes::SharedPaneState {
|
shared: panes::SharedPaneState {
|
||||||
|
container_path: self.current_file_path.clone(),
|
||||||
onion: {
|
onion: {
|
||||||
// Onion skinning is disabled during playback.
|
// Onion skinning is disabled during playback.
|
||||||
let mut o = self.onion_skin;
|
let mut o = self.onion_skin;
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,9 @@ impl OnionSkinSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SharedPaneState<'a> {
|
pub struct SharedPaneState<'a> {
|
||||||
|
/// Current `.beam` container path (for lazily paging image-asset bytes in the
|
||||||
|
/// renderer's ImageCache). `None` before the project is first saved/loaded.
|
||||||
|
pub container_path: Option<std::path::PathBuf>,
|
||||||
/// Effective onion-skin settings (already gated to off during playback by main.rs).
|
/// Effective onion-skin settings (already gated to off during playback by main.rs).
|
||||||
pub onion: OnionSkinSettings,
|
pub onion: OnionSkinSettings,
|
||||||
/// The raw onion-skin settings, mutable — edited by the Info Panel's controls.
|
/// The raw onion-skin settings, mutable — edited by the Info Panel's controls.
|
||||||
|
|
|
||||||
|
|
@ -437,6 +437,8 @@ struct VelloRenderContext {
|
||||||
tool_state: lightningbeam_core::tool::ToolState,
|
tool_state: lightningbeam_core::tool::ToolState,
|
||||||
/// Onion-skinning settings (already gated off during playback).
|
/// Onion-skinning settings (already gated off during playback).
|
||||||
onion: crate::panes::OnionSkinSettings,
|
onion: crate::panes::OnionSkinSettings,
|
||||||
|
/// `.beam` container path for lazily paging image-asset bytes in the ImageCache.
|
||||||
|
container_path: Option<std::path::PathBuf>,
|
||||||
/// Active layer for tool operations
|
/// Active layer for tool operations
|
||||||
active_layer_id: Option<uuid::Uuid>,
|
active_layer_id: Option<uuid::Uuid>,
|
||||||
/// Delta for drag preview (world space)
|
/// Delta for drag preview (world space)
|
||||||
|
|
@ -975,6 +977,8 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
let _t_after_gpu_dispatches = std::time::Instant::now();
|
let _t_after_gpu_dispatches = std::time::Instant::now();
|
||||||
|
|
||||||
let mut image_cache = shared.image_cache.lock().unwrap();
|
let mut image_cache = shared.image_cache.lock().unwrap();
|
||||||
|
// Let the cache page image bytes from the project container on a decode miss.
|
||||||
|
image_cache.set_container_path(self.ctx.container_path.clone());
|
||||||
|
|
||||||
let composite_result = if shared.is_cpu_renderer {
|
let composite_result = if shared.is_cpu_renderer {
|
||||||
lightningbeam_core::renderer::render_document_for_compositing_cpu(
|
lightningbeam_core::renderer::render_document_for_compositing_cpu(
|
||||||
|
|
@ -12073,6 +12077,7 @@ impl PaneRenderer for StagePane {
|
||||||
document: shared.action_executor.document_arc(),
|
document: shared.action_executor.document_arc(),
|
||||||
tool_state: shared.tool_state.clone(),
|
tool_state: shared.tool_state.clone(),
|
||||||
onion: shared.onion,
|
onion: shared.onion,
|
||||||
|
container_path: shared.container_path.clone(),
|
||||||
active_layer_id: *shared.active_layer_id,
|
active_layer_id: *shared.active_layer_id,
|
||||||
drag_delta,
|
drag_delta,
|
||||||
selection: shared.selection.clone(),
|
selection: shared.selection.clone(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue