mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
Merge pull request #9044 from penpot/superalex-atlas-fixes
🐛 Atlas fixes
This commit is contained in:
commit
9ba8d4667c
@ -12,5 +12,13 @@ addToLibrary({
|
||||
} else {
|
||||
return window.cancelAnimationFrame(frameId);
|
||||
}
|
||||
},
|
||||
wapi_notifyTilesRenderComplete: function wapi_notifyTilesRenderComplete() {
|
||||
// The corresponding listener lives on `document` (main thread), so in a
|
||||
// worker context we simply skip the dispatch instead of crashing.
|
||||
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
|
||||
return;
|
||||
}
|
||||
document.dispatchEvent(new CustomEvent('penpot:wasm:tiles-complete'));
|
||||
}
|
||||
});
|
||||
|
||||
@ -866,7 +866,12 @@ pub extern "C" fn set_structure_modifiers() -> Result<()> {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn clean_modifiers() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.shapes.clean_all();
|
||||
let prev_modifier_ids = state.shapes.clean_all();
|
||||
if !prev_modifier_ids.is_empty() {
|
||||
state
|
||||
.render_state
|
||||
.update_tiles_shapes(&prev_modifier_ids, &mut state.shapes)?;
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -44,12 +44,6 @@ const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 3;
|
||||
const MAX_BLOCKING_TIME_MS: i32 = 32;
|
||||
const NODE_BATCH_THRESHOLD: i32 = 3;
|
||||
|
||||
/// Dispatches `penpot:wasm:tiles-complete` on `document` so the UI can react when a full
|
||||
/// tile pass has finished (e.g. remove page-transition blur).
|
||||
fn notify_tiles_render_complete() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
crate::run_script!("document.dispatchEvent(new CustomEvent('penpot:wasm:tiles-complete'))");
|
||||
}
|
||||
const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0;
|
||||
|
||||
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
||||
@ -230,6 +224,7 @@ impl NodeRenderState {
|
||||
/// - `enter(...)` / `exit(...)` should be called when entering and leaving shape
|
||||
/// render contexts.
|
||||
/// - `is_active()` returns whether the current shape is being rendered in focus.
|
||||
#[derive(Clone)]
|
||||
pub struct FocusMode {
|
||||
shapes: Vec<Uuid>,
|
||||
active: bool,
|
||||
@ -1762,7 +1757,7 @@ impl RenderState {
|
||||
self.cancel_animation_frame();
|
||||
self.render_request_id = Some(wapi::request_animation_frame!());
|
||||
} else {
|
||||
notify_tiles_render_complete();
|
||||
wapi::notify_tiles_render_complete!();
|
||||
performance::end_measure!("render");
|
||||
}
|
||||
}
|
||||
@ -1792,6 +1787,26 @@ impl RenderState {
|
||||
) -> Result<(Vec<u8>, i32, i32)> {
|
||||
let target_surface = SurfaceId::Export;
|
||||
|
||||
// `render_shape_pixels` is used by the workspace to render thumbnails using the
|
||||
// same WASM renderer instance. It must not leak any state into the main
|
||||
// viewport renderer (tile cache, atlas, focus mode, render context, etc.).
|
||||
//
|
||||
// In particular, `update_render_context` clears and reconfigures multiple
|
||||
// render surfaces, and `render_area` drives atlas blits. If we don't restore
|
||||
// them, the workspace can temporarily show missing tiles until the next
|
||||
// interaction (e.g. zoom) forces a full context rebuild.
|
||||
let saved_focus_mode = self.focus_mode.clone();
|
||||
let saved_export_context = self.export_context;
|
||||
let saved_render_area = self.render_area;
|
||||
let saved_render_area_with_margins = self.render_area_with_margins;
|
||||
let saved_current_tile = self.current_tile;
|
||||
let saved_pending_nodes = std::mem::take(&mut self.pending_nodes);
|
||||
let saved_nested_fills = std::mem::take(&mut self.nested_fills);
|
||||
let saved_nested_blurs = std::mem::take(&mut self.nested_blurs);
|
||||
let saved_nested_shadows = std::mem::take(&mut self.nested_shadows);
|
||||
let saved_ignore_nested_blurs = self.ignore_nested_blurs;
|
||||
let saved_preview_mode = self.preview_mode;
|
||||
|
||||
// Reset focus mode so all shapes in the export tree are rendered.
|
||||
// Without this, leftover focus_mode state from the workspace could
|
||||
// cause shapes (and their background blur) to be skipped.
|
||||
@ -1843,6 +1858,30 @@ impl RenderState {
|
||||
.expect("PNG encode failed");
|
||||
let skia::ISize { width, height } = image.dimensions();
|
||||
|
||||
// Restore the workspace render state.
|
||||
self.focus_mode = saved_focus_mode;
|
||||
self.export_context = saved_export_context;
|
||||
self.render_area = saved_render_area;
|
||||
self.render_area_with_margins = saved_render_area_with_margins;
|
||||
self.current_tile = saved_current_tile;
|
||||
self.pending_nodes = saved_pending_nodes;
|
||||
self.nested_fills = saved_nested_fills;
|
||||
self.nested_blurs = saved_nested_blurs;
|
||||
self.nested_shadows = saved_nested_shadows;
|
||||
self.ignore_nested_blurs = saved_ignore_nested_blurs;
|
||||
self.preview_mode = saved_preview_mode;
|
||||
|
||||
// Restore render-surface transforms for the workspace context.
|
||||
// If we have a current tile, restore its tile render context; otherwise
|
||||
// fall back to restoring the previous render_area (may be empty).
|
||||
let workspace_scale = self.get_scale();
|
||||
if let Some(tile) = self.current_tile {
|
||||
self.update_render_context(tile);
|
||||
} else if !self.render_area.is_empty() {
|
||||
self.surfaces
|
||||
.update_render_context(self.render_area, workspace_scale);
|
||||
}
|
||||
|
||||
Ok((data.as_bytes().to_vec(), width, height))
|
||||
}
|
||||
|
||||
@ -2817,6 +2856,24 @@ impl RenderState {
|
||||
paint.set_color(self.background_color);
|
||||
s.canvas().draw_rect(tile_rect, &paint);
|
||||
});
|
||||
// Keep Cache surface coherent for render_from_cache.
|
||||
if !self.options.is_fast_mode() {
|
||||
if !self.cache_cleared_this_render {
|
||||
self.surfaces.clear_cache(self.background_color);
|
||||
self.cache_cleared_this_render = true;
|
||||
}
|
||||
let aligned_rect = self.get_aligned_tile_bounds(current_tile);
|
||||
self.surfaces.apply_mut(SurfaceId::Cache as u32, |s| {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(self.background_color);
|
||||
s.canvas().draw_rect(aligned_rect, &paint);
|
||||
});
|
||||
}
|
||||
|
||||
// Clear atlas region to transparent so background shows through.
|
||||
let _ = self
|
||||
.surfaces
|
||||
.clear_doc_rect_in_atlas(&mut self.gpu_state, self.render_area);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,6 +289,33 @@ impl Surfaces {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_doc_rect_in_atlas(
|
||||
&mut self,
|
||||
gpu_state: &mut GpuState,
|
||||
doc_rect: skia::Rect,
|
||||
) -> Result<()> {
|
||||
if doc_rect.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.ensure_atlas_contains(gpu_state, doc_rect)?;
|
||||
|
||||
// Destination is document-space rect mapped into atlas pixel coords.
|
||||
let dst = skia::Rect::from_xywh(
|
||||
(doc_rect.left - self.atlas_origin.x) * self.atlas_scale,
|
||||
(doc_rect.top - self.atlas_origin.y) * self.atlas_scale,
|
||||
doc_rect.width() * self.atlas_scale,
|
||||
doc_rect.height() * self.atlas_scale,
|
||||
);
|
||||
|
||||
let canvas = self.atlas.canvas();
|
||||
canvas.save();
|
||||
canvas.clip_rect(dst, None, true);
|
||||
canvas.clear(skia::Color::TRANSPARENT);
|
||||
canvas.restore();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_tiles(&mut self) {
|
||||
self.tiles.clear();
|
||||
}
|
||||
|
||||
@ -160,14 +160,24 @@ impl State {
|
||||
|
||||
// Only remove the children when is being deleted from the owner
|
||||
if shape.parent_id.is_none() || shape.parent_id == Some(parent_id) {
|
||||
let tiles::TileRect(rsx, rsy, rex, rey) =
|
||||
self.render_state.get_tiles_for_shape(shape, &self.shapes);
|
||||
for x in rsx..=rex {
|
||||
for y in rsy..=rey {
|
||||
let tile = tiles::Tile(x, y);
|
||||
self.render_state.remove_cached_tile(tile);
|
||||
self.render_state.tiles.remove_shape_at(tile, shape.id);
|
||||
}
|
||||
// IMPORTANT:
|
||||
// Do NOT use `get_tiles_for_shape` here. That method intersects the shape
|
||||
// tiles with the current interest area, which means we'd only invalidate
|
||||
// the subset currently near the viewport. When the user later pans/zooms
|
||||
// to reveal previously cached tiles, stale pixels could reappear.
|
||||
//
|
||||
// Instead, remove the shape from *all* tiles where it was indexed, and
|
||||
// drop cached tiles for those entries.
|
||||
let indexed_tiles: Vec<tiles::Tile> = self
|
||||
.render_state
|
||||
.tiles
|
||||
.get_tiles_of(shape.id)
|
||||
.map(|t| t.iter().copied().collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
for tile in indexed_tiles {
|
||||
self.render_state.remove_cached_tile(tile);
|
||||
self.render_state.tiles.remove_shape_at(tile, shape.id);
|
||||
}
|
||||
|
||||
if let Some(shape_to_delete) = self.shapes.get(&id) {
|
||||
|
||||
@ -278,11 +278,35 @@ impl ShapesPoolImpl {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clean_all(&mut self) {
|
||||
/// Clears transient per-frame state (modifiers, structure, scale_content)
|
||||
/// and returns the list of UUIDs that had a `modifier` applied at the
|
||||
/// moment of cleaning. The caller can use that list to re-sync the tile
|
||||
/// index / tile cache for those shapes: after cleaning their modifier is
|
||||
/// gone, but if we don't touch their tiles they keep pointing at the
|
||||
/// previous modified position and the tile texture cache may serve stale
|
||||
/// pixels.
|
||||
pub fn clean_all(&mut self) -> Vec<Uuid> {
|
||||
self.clean_shape_cache();
|
||||
|
||||
let modified_uuids: Vec<Uuid> = if self.modifiers.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let mut idx_to_uuid: HashMap<usize, Uuid> =
|
||||
HashMap::with_capacity(self.uuid_to_idx.len());
|
||||
for (uuid, idx) in self.uuid_to_idx.iter() {
|
||||
idx_to_uuid.insert(*idx, *uuid);
|
||||
}
|
||||
self.modifiers
|
||||
.keys()
|
||||
.filter_map(|idx| idx_to_uuid.get(idx).copied())
|
||||
.collect()
|
||||
};
|
||||
|
||||
self.modifiers = HashMap::default();
|
||||
self.structure = HashMap::default();
|
||||
self.scale_content = HashMap::default();
|
||||
|
||||
modified_uuids
|
||||
}
|
||||
|
||||
pub fn subtree(&self, id: &Uuid) -> ShapesPoolImpl {
|
||||
|
||||
@ -35,5 +35,21 @@ macro_rules! cancel_animation_frame {
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! notify_tiles_render_complete {
|
||||
() => {{
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
unsafe extern "C" {
|
||||
pub fn wapi_notifyTilesRenderComplete();
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
unsafe {
|
||||
wapi_notifyTilesRenderComplete()
|
||||
};
|
||||
}};
|
||||
}
|
||||
|
||||
pub use cancel_animation_frame;
|
||||
pub use notify_tiles_render_complete;
|
||||
pub use request_animation_frame;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user