mirror of
https://github.com/penpot/penpot.git
synced 2026-07-24 23:18:06 +00:00
🔧 Fix drag
This commit is contained in:
parent
63ff5c87c2
commit
1141e91b86
@ -395,10 +395,14 @@ pub struct InteractiveDragCrop {
|
||||
/// Viewbox origin (doc-space) at capture time.
|
||||
pub capture_vb_left: f32,
|
||||
pub capture_vb_top: f32,
|
||||
/// Backbuffer pixel origin used for `snapshot_rect` (so we can do 1:1 blits).
|
||||
/// Pixel origin (in target/snapshot space) for 1:1 blits during drag.
|
||||
pub capture_src_left: i32,
|
||||
pub capture_src_top: i32,
|
||||
/// Shared raster image of the full Target taken at settle. All crops point at the same
|
||||
/// image so Skia's texture cache uploads it once per interactive transform.
|
||||
pub image: skia::Image,
|
||||
/// Sub-rect inside `image` belonging to this shape.
|
||||
pub src_irect: skia::IRect,
|
||||
}
|
||||
|
||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32, interest: i32) -> skia::ISize {
|
||||
@ -1691,31 +1695,68 @@ impl RenderState {
|
||||
non_overlapping.push((*id, *bounds, *selrect));
|
||||
}
|
||||
|
||||
// Snapshot from Backbuffer for each accepted shape.
|
||||
// Two-pass build:
|
||||
// 1. Compute each candidate's clamped target-pixel IRect and the union of all of
|
||||
// them.
|
||||
// 2. Carve the union out of `target_snapshot` and pre-warm it as a non-FBO GPU
|
||||
// texture via `take_crop_atlas`. All crops then share that smaller atlas image,
|
||||
// so the first drag frame doesn't pay an upload of the full viewport and per-
|
||||
// shape `draw_image_rect`s sample from a compact, ready-to-go texture.
|
||||
let (snap_w, snap_h) = match self.surfaces.target_snapshot() {
|
||||
Some(img) => (img.width(), img.height()),
|
||||
None => return,
|
||||
};
|
||||
|
||||
let scale = self.get_scale();
|
||||
let vb_left = self.viewbox.area.left;
|
||||
let vb_top = self.viewbox.area.top;
|
||||
|
||||
let mut valid: Vec<(Uuid, skia::IRect, Rect, Rect)> =
|
||||
Vec::with_capacity(non_overlapping.len());
|
||||
let mut union: Option<skia::IRect> = None;
|
||||
for (id, doc_bounds, selrect) in non_overlapping {
|
||||
let left = ((doc_bounds.left - vb_left) * scale).floor() as i32;
|
||||
let top = ((doc_bounds.top - vb_top) * scale).floor() as i32;
|
||||
let right = ((doc_bounds.right - vb_left) * scale).ceil() as i32;
|
||||
let bottom = ((doc_bounds.bottom - vb_top) * scale).ceil() as i32;
|
||||
// Clamp to the snapshot's pixel bounds.
|
||||
let left = left.max(0);
|
||||
let top = top.max(0);
|
||||
let right = right.min(snap_w);
|
||||
let bottom = bottom.min(snap_h);
|
||||
if right <= left || bottom <= top {
|
||||
continue;
|
||||
}
|
||||
let src_irect = skia::IRect::new(left, top, right, bottom);
|
||||
let Some(image) = self
|
||||
.surfaces
|
||||
.snapshot_rect(SurfaceId::Backbuffer, src_irect)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let irect = skia::IRect::new(left, top, right, bottom);
|
||||
union = Some(match union {
|
||||
None => irect,
|
||||
Some(u) => skia::IRect::join(&u, &irect),
|
||||
});
|
||||
valid.push((id, irect, doc_bounds, selrect));
|
||||
}
|
||||
|
||||
let Some(union) = union else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.surfaces.take_crop_atlas(union);
|
||||
let Some(atlas) = self.surfaces.crop_atlas().cloned() else {
|
||||
return;
|
||||
};
|
||||
let atlas_origin = self.surfaces.crop_atlas_origin();
|
||||
|
||||
for (id, target_irect, doc_bounds, selrect) in valid {
|
||||
let local_irect = skia::IRect::new(
|
||||
target_irect.left - atlas_origin.x,
|
||||
target_irect.top - atlas_origin.y,
|
||||
target_irect.right - atlas_origin.x,
|
||||
target_irect.bottom - atlas_origin.y,
|
||||
);
|
||||
let src_doc_bounds = Rect::new(
|
||||
src_irect.left as f32 / scale + vb_left,
|
||||
src_irect.top as f32 / scale + vb_top,
|
||||
src_irect.right as f32 / scale + vb_left,
|
||||
src_irect.bottom as f32 / scale + vb_top,
|
||||
target_irect.left as f32 / scale + vb_left,
|
||||
target_irect.top as f32 / scale + vb_top,
|
||||
target_irect.right as f32 / scale + vb_left,
|
||||
target_irect.bottom as f32 / scale + vb_top,
|
||||
);
|
||||
let fits_viewport_at_capture = doc_bounds.left >= viewport.left
|
||||
&& doc_bounds.top >= viewport.top
|
||||
@ -1729,9 +1770,10 @@ impl RenderState {
|
||||
fits_viewport_at_capture,
|
||||
capture_vb_left: vb_left,
|
||||
capture_vb_top: vb_top,
|
||||
capture_src_left: src_irect.left,
|
||||
capture_src_top: src_irect.top,
|
||||
image,
|
||||
capture_src_left: target_irect.left,
|
||||
capture_src_top: target_irect.top,
|
||||
image: atlas.clone(),
|
||||
src_irect: local_irect,
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -2105,7 +2147,7 @@ impl RenderState {
|
||||
// A full-quality frame is now complete. Refresh Backbuffer and regenerate
|
||||
// the per-shape crop cache so interactive drags can reuse pixels.
|
||||
if !self.options.is_fast_mode() && !self.options.is_interactive_transform() {
|
||||
self.surfaces.copy_target_to_backbuffer();
|
||||
self.surfaces.take_target_snapshot();
|
||||
self.rebuild_backbuffer_crop_cache(tree);
|
||||
}
|
||||
wapi::notify_tiles_render_complete!();
|
||||
@ -3054,7 +3096,22 @@ impl RenderState {
|
||||
|
||||
let x = (doc_left + translation.0) * scale;
|
||||
let y = (doc_top + translation.1) * scale;
|
||||
canvas.draw_image(crop_image, (x, y), Some(&skia::Paint::default()));
|
||||
// The shared raster image covers the whole Target; each crop draws
|
||||
// its own `src_irect` slice. Reusing the image identity across crops
|
||||
// lets Skia upload the bitmap to a GPU texture once per drag.
|
||||
let src_rect = skia::Rect::from_irect(crop.src_irect);
|
||||
let dst_rect = skia::Rect::from_xywh(
|
||||
x,
|
||||
y,
|
||||
crop.src_irect.width() as f32,
|
||||
crop.src_irect.height() as f32,
|
||||
);
|
||||
canvas.draw_image_rect(
|
||||
crop_image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Fast)),
|
||||
dst_rect,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@ -69,7 +69,19 @@ pub struct Surfaces {
|
||||
// for drawing tiles.
|
||||
export: skia::Surface,
|
||||
// Persistent viewport-sized surface used to keep the last presented frame.
|
||||
// Kept for API compatibility but no longer the live cache — see `target_snapshot`.
|
||||
backbuffer: skia::Surface,
|
||||
// CPU-backed raster `Image` populated from `target` at full-render settle via
|
||||
// `read_pixels`. Used to seed interactive frames. Lives in system memory, never in an FBO.
|
||||
target_snapshot: Option<skia::Image>,
|
||||
// Pre-warmed GPU texture image cropped to the union of cacheable shapes' bounds.
|
||||
// Built from `target_snapshot` at settle and used as the shared source for per-shape
|
||||
// drag crops, so the first drag frame doesn't pay a CPU→GPU upload. Created via
|
||||
// `texture_from_image`, which produces a plain (non-FBO) texture.
|
||||
crop_atlas: Option<skia::Image>,
|
||||
// Top-left of `crop_atlas` in target-pixel space. Subtract from a shape's target-space
|
||||
// IRect to get atlas-local coords for `draw_image_rect`.
|
||||
crop_atlas_origin: skia::IPoint,
|
||||
|
||||
tiles: TileTextureCache,
|
||||
// Persistent 1:1 document-space atlas that gets incrementally updated as tiles render.
|
||||
@ -155,6 +167,9 @@ impl Surfaces {
|
||||
debug,
|
||||
export,
|
||||
backbuffer,
|
||||
target_snapshot: None,
|
||||
crop_atlas: None,
|
||||
crop_atlas_origin: skia::IPoint::new(0, 0),
|
||||
tiles,
|
||||
atlas,
|
||||
atlas_origin: skia::Point::new(0.0, 0.0),
|
||||
@ -707,23 +722,96 @@ impl Surfaces {
|
||||
(s.width(), s.height())
|
||||
}
|
||||
|
||||
/// Copy the current `Target` contents into the persistent `Backbuffer`.
|
||||
/// This is a GPU→GPU copy via Skia (no ReadPixels).
|
||||
pub fn copy_target_to_backbuffer(&mut self) {
|
||||
let sampling_options = self.sampling_options;
|
||||
self.target.clone().draw(
|
||||
self.backbuffer.canvas(),
|
||||
(0.0, 0.0),
|
||||
sampling_options,
|
||||
Some(&skia::Paint::default()),
|
||||
);
|
||||
/// Copy `Target` pixels into a CPU `Bitmap` and cache the result as a
|
||||
/// raster `Image`. Replaces the old `Backbuffer` `skia::Surface` (which was
|
||||
/// a GPU framebuffer) with system-memory storage so the cache never holds
|
||||
/// a live FBO — on Firefox + NVIDIA the FBO is suspected of forcing extra
|
||||
/// driver-state serialization between settle and the next interactive
|
||||
/// frame. The trade-off is one GPU→CPU readback at settle; the raster
|
||||
/// image is re-uploaded as a GPU texture on first draw and Skia caches the
|
||||
/// upload for subsequent frames.
|
||||
pub fn take_target_snapshot(&mut self) {
|
||||
let w = self.target.width();
|
||||
let h = self.target.height();
|
||||
if w <= 0 || h <= 0 {
|
||||
self.target_snapshot = None;
|
||||
return;
|
||||
}
|
||||
let mut bitmap = skia::Bitmap::new();
|
||||
bitmap.alloc_n32_pixels((w, h), false);
|
||||
if !self.target.read_pixels_to_bitmap(&bitmap, (0, 0)) {
|
||||
self.target_snapshot = None;
|
||||
return;
|
||||
}
|
||||
bitmap.set_immutable();
|
||||
self.target_snapshot = skia::images::raster_from_bitmap(&bitmap);
|
||||
}
|
||||
|
||||
/// Seed `Target` from `Backbuffer` (last presented frame).
|
||||
/// Returns the cached `Target` snapshot taken at the previous settle, if any.
|
||||
pub fn target_snapshot(&self) -> Option<&skia::Image> {
|
||||
self.target_snapshot.as_ref()
|
||||
}
|
||||
|
||||
/// Carve `src_irect` (target-pixel space) out of `target_snapshot` and upload it as a
|
||||
/// non-FBO GPU texture image. Called at settle once the candidate union is known so the
|
||||
/// first drag frame can blit from a ready GPU texture instead of paying an upload cost.
|
||||
/// Falls back to the raster subset if the GPU upload isn't possible.
|
||||
pub fn take_crop_atlas(&mut self, src_irect: skia::IRect) {
|
||||
self.crop_atlas = None;
|
||||
self.crop_atlas_origin = skia::IPoint::new(0, 0);
|
||||
let Some(raster) = self.target_snapshot.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let bounds = skia::IRect::new(0, 0, raster.width(), raster.height());
|
||||
let Some(clipped) = skia::IRect::intersect(&src_irect, &bounds) else {
|
||||
return;
|
||||
};
|
||||
if clipped.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(subset) = raster.make_subset(
|
||||
None,
|
||||
&clipped,
|
||||
skia::image::RequiredProperties::default(),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
let warmed = self
|
||||
.target
|
||||
.direct_context()
|
||||
.and_then(|mut ctx| {
|
||||
skia::gpu::images::texture_from_image(
|
||||
&mut ctx,
|
||||
&subset,
|
||||
skia::gpu::Mipmapped::No,
|
||||
skia::gpu::Budgeted::Yes,
|
||||
)
|
||||
})
|
||||
.unwrap_or(subset);
|
||||
self.crop_atlas_origin = skia::IPoint::new(clipped.left, clipped.top);
|
||||
self.crop_atlas = Some(warmed);
|
||||
}
|
||||
|
||||
/// Returns the pre-warmed crop atlas image, if any.
|
||||
pub fn crop_atlas(&self) -> Option<&skia::Image> {
|
||||
self.crop_atlas.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the target-pixel origin of `crop_atlas`.
|
||||
pub fn crop_atlas_origin(&self) -> skia::IPoint {
|
||||
self.crop_atlas_origin
|
||||
}
|
||||
|
||||
/// Seed `Target` with the previously-snapshotted frame (the equivalent of
|
||||
/// the old `Backbuffer` blit, but driven by the cached `Image` so we don't
|
||||
/// have to write into `Backbuffer` at settle time).
|
||||
pub fn seed_target_from_backbuffer(&mut self) {
|
||||
let Some(image) = self.target_snapshot.clone() else {
|
||||
return;
|
||||
};
|
||||
let sampling_options = self.sampling_options;
|
||||
self.backbuffer.clone().draw(
|
||||
self.target.canvas(),
|
||||
self.target.canvas().draw_image_with_sampling_options(
|
||||
&image,
|
||||
(0.0, 0.0),
|
||||
sampling_options,
|
||||
Some(&skia::Paint::default()),
|
||||
@ -733,6 +821,9 @@ impl Surfaces {
|
||||
fn reset_from_target(&mut self, target: skia::Surface) -> Result<()> {
|
||||
let dim = (target.width(), target.height());
|
||||
self.target = target;
|
||||
self.target_snapshot = None;
|
||||
self.crop_atlas = None;
|
||||
self.crop_atlas_origin = skia::IPoint::new(0, 0);
|
||||
self.filter = self
|
||||
.target
|
||||
.new_surface_with_dimensions(dim)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user