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;
|
||||
|
||||
// Eager-read image-asset bytes from the container into `data` (Phase 4 will make
|
||||
// this lazy + LRU). Old projects keep their base64-deserialized `data` and have no
|
||||
// container row — those are skipped (`data` already Some).
|
||||
for (id, asset) in document.image_assets.iter_mut() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Image-asset bytes are NOT eagerly read (Phase 4 Tier 1 paging): `ImageAsset.data`
|
||||
// stays empty and the renderer's ImageCache pages bytes from the container on a
|
||||
// decode miss (keyed by asset id). Old base64 projects keep their deserialized
|
||||
// `data` (no container row). Loading is instant; only rendered images touch disk.
|
||||
|
||||
// Missing external files (referenced entries whose file no longer exists).
|
||||
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.
|
||||
sizes: HashMap<Uuid, 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 {
|
||||
|
|
@ -50,9 +53,29 @@ impl ImageCache {
|
|||
lru: Vec::new(),
|
||||
sizes: HashMap::new(),
|
||||
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.
|
||||
fn touch(&mut self, id: Uuid, size: usize) {
|
||||
if !self.sizes.contains_key(&id) {
|
||||
|
|
@ -82,8 +105,9 @@ impl ImageCache {
|
|||
return Some(cached);
|
||||
}
|
||||
|
||||
// Decode and cache
|
||||
let image = decode_image_asset(asset)?;
|
||||
// Decode and cache (bytes from asset.data or paged from the container).
|
||||
let bytes = self.resolve_bytes(asset)?;
|
||||
let image = decode_image_brush(&bytes)?;
|
||||
let arc_image = Arc::new(image);
|
||||
self.cache.insert(asset.id, Arc::clone(&arc_image));
|
||||
self.touch(asset.id, size);
|
||||
|
|
@ -98,7 +122,8 @@ impl ImageCache {
|
|||
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);
|
||||
self.cpu_cache.insert(asset.id, Arc::clone(&arc));
|
||||
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).
|
||||
fn decode_image_to_pixmap(asset: &ImageAsset) -> Option<tiny_skia::Pixmap> {
|
||||
let data = asset.data.as_ref()?;
|
||||
/// Decode image bytes to a premultiplied tiny-skia Pixmap (CPU render path).
|
||||
fn decode_image_to_pixmap(data: &[u8]) -> Option<tiny_skia::Pixmap> {
|
||||
let img = image::load_from_memory(data).ok()?;
|
||||
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()) {
|
||||
let [r, g, b, a] = src.0;
|
||||
// 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)
|
||||
}
|
||||
|
||||
/// Decode an image asset to peniko ImageBrush
|
||||
fn decode_image_asset(asset: &ImageAsset) -> Option<ImageBrush> {
|
||||
// Get the raw file data
|
||||
let data = asset.data.as_ref()?;
|
||||
|
||||
// Decode using the image crate
|
||||
/// Decode image bytes to a peniko ImageBrush (GPU render path).
|
||||
fn decode_image_brush(data: &[u8]) -> Option<ImageBrush> {
|
||||
let img = image::load_from_memory(data).ok()?;
|
||||
let rgba = img.to_rgba8();
|
||||
let (iw, ih) = rgba.dimensions();
|
||||
|
||||
// Create peniko ImageData then ImageBrush
|
||||
let image_data = ImageData {
|
||||
data: Blob::from(rgba.into_raw()),
|
||||
format: ImageFormat::Rgba8,
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
width: iw,
|
||||
height: ih,
|
||||
alpha_type: ImageAlphaType::Alpha,
|
||||
};
|
||||
Some(ImageBrush::new(image_data))
|
||||
|
|
|
|||
|
|
@ -6472,6 +6472,7 @@ impl eframe::App for EditorApp {
|
|||
// Create render context
|
||||
let mut ctx = RenderContext {
|
||||
shared: panes::SharedPaneState {
|
||||
container_path: self.current_file_path.clone(),
|
||||
onion: {
|
||||
// Onion skinning is disabled during playback.
|
||||
let mut o = self.onion_skin;
|
||||
|
|
|
|||
|
|
@ -175,6 +175,9 @@ impl OnionSkinSettings {
|
|||
}
|
||||
|
||||
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).
|
||||
pub onion: OnionSkinSettings,
|
||||
/// 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,
|
||||
/// Onion-skinning settings (already gated off during playback).
|
||||
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_id: Option<uuid::Uuid>,
|
||||
/// 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 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 {
|
||||
lightningbeam_core::renderer::render_document_for_compositing_cpu(
|
||||
|
|
@ -12073,6 +12077,7 @@ impl PaneRenderer for StagePane {
|
|||
document: shared.action_executor.document_arc(),
|
||||
tool_state: shared.tool_state.clone(),
|
||||
onion: shared.onion,
|
||||
container_path: shared.container_path.clone(),
|
||||
active_layer_id: *shared.active_layer_id,
|
||||
drag_delta,
|
||||
selection: shared.selection.clone(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue