mirror of
https://github.com/penpot/penpot.git
synced 2026-06-17 21:02:05 +00:00
🔧 Use atlas back
This commit is contained in:
parent
c59bd644b0
commit
bbf505ea4f
@ -42,7 +42,12 @@ pub use images::*;
|
||||
|
||||
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
||||
|
||||
const MIN_TILES_PER_FRAME: i32 = 8;
|
||||
/// Per-frame deadline for uncached-tile rendering, measured from the rAF
|
||||
/// timestamp (frame start), so JS work that ran earlier in the tick counts
|
||||
/// against it. The loop yields (Partial frame) once exceeded so heavy
|
||||
/// settles stream at frame rate instead of blocking the main thread.
|
||||
/// ~12ms leaves headroom for compositing + present within a 16.7ms frame.
|
||||
const TILE_FRAME_TIME_BUDGET_MS: i32 = 12;
|
||||
|
||||
#[repr(u8)]
|
||||
pub enum FrameType {
|
||||
@ -399,11 +404,9 @@ pub(crate) struct RenderState {
|
||||
/// (must composite to present the work). Reset when current_tile
|
||||
/// changes.
|
||||
pub current_tile_had_shapes: bool,
|
||||
/// Count of uncached tiles composited in the current rAF. Reset at the top
|
||||
/// of `render_shape_tree_partial`; checked against the adaptive per-frame
|
||||
/// budget (`max(MIN_TILES_PER_FRAME, visible_tile_count)`) to yield (flush +
|
||||
/// return Partial) so a single rAF doesn't hand the GPU one huge command
|
||||
/// buffer. See `MIN_TILES_PER_FRAME`.
|
||||
/// Count of uncached tiles composited in the current rAF. Reset at the
|
||||
/// top of `render_shape_tree_partial`. The yield decision is time-based:
|
||||
/// see `TILE_FRAME_TIME_BUDGET_MS`.
|
||||
pub tiles_on_frame: i32,
|
||||
/// During interactive transforms we keep `Target` between rAFs. Seed the
|
||||
/// interactive backdrop exactly once per gesture (first rAF) so we don't
|
||||
@ -2323,12 +2326,33 @@ impl RenderState {
|
||||
panic!("FrameType::None");
|
||||
}
|
||||
FrameType::Partial => {
|
||||
// Partial frame: flush AND submit so the GPU actually executes this
|
||||
// chunk's draws now
|
||||
self.surfaces.flush_and_submit(SurfaceId::Backbuffer);
|
||||
if !self.options.is_fast_mode()
|
||||
&& !self.options.is_interactive_transform()
|
||||
&& !self.viewer_masked_pass()
|
||||
{
|
||||
// Progressive present: DocAtlas preview as backdrop (covers
|
||||
// not-yet-rendered tiles), finished tiles composited on top.
|
||||
// Cheap since the page atlas made compositing snapshot-free.
|
||||
self.surfaces
|
||||
.draw_atlas_to_backbuffer(self.viewbox, self.background_color);
|
||||
self.surfaces
|
||||
.draw_cached_tiles_over_backbuffer(&self.viewbox, &self.tile_viewbox);
|
||||
self.present_frame(tree);
|
||||
} else {
|
||||
// Gesture previews / masked passes present elsewhere: just
|
||||
// flush so the GPU executes this chunk's draws now.
|
||||
self.surfaces.flush_and_submit(SurfaceId::Backbuffer);
|
||||
}
|
||||
}
|
||||
FrameType::Full => {
|
||||
if !self.options.is_interactive_transform() {
|
||||
// A settle can complete mid-zoom-gesture (fast_mode with the
|
||||
// zoom already diverged from cached_viewbox). The grid then
|
||||
// still holds old-zoom tiles — compositing them at new-zoom
|
||||
// positions flashes wrong-scale tiles with background gaps.
|
||||
// Skip the composite + present and let the preview own the
|
||||
// screen; the post-set_view_end settle presents the real frame.
|
||||
let mid_zoom_gesture = self.options.is_fast_mode() && self.zoom_changed();
|
||||
if !mid_zoom_gesture && !self.options.is_interactive_transform() {
|
||||
self.surfaces.draw_tile_atlas_to_backbuffer(
|
||||
&self.viewbox,
|
||||
&self.tile_viewbox,
|
||||
@ -2344,9 +2368,13 @@ impl RenderState {
|
||||
// builds the cache from it when a drag actually begins.
|
||||
self.backbuffer_is_clean_full_frame =
|
||||
!self.options.is_fast_mode() && !self.options.is_interactive_transform();
|
||||
// present_frame: copy clean Backbuffer → Target, draw UI/debug
|
||||
// overlays on Target only, then flush. Backbuffer stays overlay-free.
|
||||
self.present_frame(tree);
|
||||
if mid_zoom_gesture {
|
||||
self.surfaces.flush_and_submit(SurfaceId::Backbuffer);
|
||||
} else {
|
||||
// present_frame: copy clean Backbuffer → Target, draw UI/debug
|
||||
// overlays on Target only, then flush. Backbuffer stays overlay-free.
|
||||
self.present_frame(tree);
|
||||
}
|
||||
wapi::notify_tiles_render_complete!();
|
||||
performance::end_measure!("render");
|
||||
}
|
||||
@ -3568,9 +3596,17 @@ impl RenderState {
|
||||
let mut should_stop = false;
|
||||
self.viewer_render_root = base_object.copied();
|
||||
self.tiles_on_frame = 0;
|
||||
// Never fewer than the currently-visible tile count, so a
|
||||
// normal viewport composites all its visible tiles in one rAF
|
||||
let tile_budget = MIN_TILES_PER_FRAME.max(self.tile_viewbox.visible_rect.len());
|
||||
// Budget against the rAF frame deadline, not this call: `timestamp` is
|
||||
// the rAF DOMHighResTimeStamp (same epoch as get_time()), so time the
|
||||
// page spent in JS before this tick counts against the budget too.
|
||||
// Fall back to "now" when no usable timestamp was passed (0 on the
|
||||
// first render, sync/test paths, or a stale value).
|
||||
let now = performance::get_time();
|
||||
let frame_start = if timestamp > 0 && now >= timestamp && now - timestamp < 100 {
|
||||
timestamp
|
||||
} else {
|
||||
now
|
||||
};
|
||||
let root_ids = {
|
||||
if let Some(shape_id) = base_object {
|
||||
vec![*shape_id]
|
||||
@ -3632,15 +3668,15 @@ impl RenderState {
|
||||
);
|
||||
}
|
||||
|
||||
// Cap how many such tiles we submit per rAF so the GPU doesn't get one
|
||||
// giant command buffer. Skipped during interactive
|
||||
// transforms
|
||||
// Time-box the work per rAF (at least one tile per
|
||||
// tick guarantees progress). Skipped during interactive
|
||||
// transforms.
|
||||
if allow_stop
|
||||
&& !self.options.is_interactive_transform()
|
||||
&& !self.viewer_masked_pass()
|
||||
{
|
||||
self.tiles_on_frame += 1;
|
||||
if self.tiles_on_frame >= tile_budget {
|
||||
if performance::get_time() - frame_start >= TILE_FRAME_TIME_BUDGET_MS {
|
||||
self.viewer_render_root = None;
|
||||
return Ok(FrameType::Partial);
|
||||
}
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
use crate::error::{Error, Result};
|
||||
use skia_safe::gpu::{
|
||||
self, ganesh::context_options::Enable, gl::FramebufferInfo, gl::TextureInfo, ContextOptions,
|
||||
DirectContext,
|
||||
self, ganesh::context_options::Enable, gl::FramebufferInfo, ContextOptions, DirectContext,
|
||||
};
|
||||
use skia_safe::{self as skia, ISize};
|
||||
|
||||
const MIN_MAX_TEXTURE_SIZE: i32 = 512;
|
||||
const MAX_MAX_TEXTURE_SIZE: i32 = 8192 * 2;
|
||||
|
||||
/// GPU resource-cache budget for Skia-managed (budgeted) render targets.
|
||||
const RESOURCE_CACHE_LIMIT_BYTES: usize = 512 * 1024 * 1024;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GpuState {
|
||||
pub context: DirectContext,
|
||||
@ -28,10 +30,17 @@ impl GpuState {
|
||||
// context_options.allow_multiple_glyph_cache_textures = Enable::Yes;
|
||||
// context_options.allow_path_mask_caching = false;
|
||||
|
||||
let context = gpu::direct_contexts::make_gl(interface, Some(&context_options)).ok_or(
|
||||
let mut context = gpu::direct_contexts::make_gl(interface, Some(&context_options)).ok_or(
|
||||
Error::CriticalError("Failed to create GL context".to_string()),
|
||||
)?;
|
||||
|
||||
// Skia-managed render targets are budgeted against the GPU resource
|
||||
// cache. The default cap is 256 MB (GrResourceCache kDefaultMaxSize),
|
||||
// which a single atlas-sized transient can exhaust on its own. Raise it
|
||||
// so freed atlas/snapshot textures recycle as scratch instead of being
|
||||
// re-allocated from the driver every frame.
|
||||
context.set_resource_cache_limit(RESOURCE_CACHE_LIMIT_BYTES);
|
||||
|
||||
let framebuffer_info = {
|
||||
let mut fboid: gl::types::GLint = 0;
|
||||
unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) };
|
||||
@ -57,54 +66,6 @@ impl GpuState {
|
||||
.clamp(MIN_MAX_TEXTURE_SIZE, MAX_MAX_TEXTURE_SIZE)
|
||||
}
|
||||
|
||||
fn delete_gl_texture(&mut self, texture_id: gl::types::GLuint) -> bool {
|
||||
unsafe {
|
||||
gl::DeleteTextures(1, &texture_id);
|
||||
gl::GetError() == 0
|
||||
}
|
||||
}
|
||||
|
||||
fn create_gl_texture(&mut self, width: i32, height: i32) -> gl::types::GLuint {
|
||||
let mut texture_id: gl::types::GLuint = 0;
|
||||
|
||||
unsafe {
|
||||
gl::GenTextures(1, &mut texture_id);
|
||||
gl::BindTexture(gl::TEXTURE_2D, texture_id);
|
||||
|
||||
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as i32);
|
||||
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as i32);
|
||||
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32);
|
||||
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32);
|
||||
|
||||
gl::TexImage2D(
|
||||
gl::TEXTURE_2D,
|
||||
0,
|
||||
gl::RGBA8 as i32,
|
||||
width,
|
||||
height,
|
||||
0,
|
||||
gl::RGBA,
|
||||
gl::UNSIGNED_BYTE,
|
||||
std::ptr::null(),
|
||||
);
|
||||
}
|
||||
|
||||
texture_id
|
||||
}
|
||||
|
||||
pub fn delete_surface(&mut self, surface: &mut skia::Surface) -> bool {
|
||||
let Some(texture) = skia::gpu::surfaces::get_backend_texture(
|
||||
surface,
|
||||
skia_safe::surface::BackendHandleAccess::FlushRead,
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
let Some(texture_info) = gpu::backend_textures::get_gl_texture_info(&texture) else {
|
||||
return false;
|
||||
};
|
||||
self.delete_gl_texture(texture_info.id)
|
||||
}
|
||||
|
||||
pub fn create_surface_with_isize(
|
||||
&mut self,
|
||||
label: String,
|
||||
@ -115,28 +76,29 @@ impl GpuState {
|
||||
|
||||
pub fn create_surface_with_dimensions(
|
||||
&mut self,
|
||||
label: String,
|
||||
_label: String,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> Result<skia::Surface> {
|
||||
let backend_texture = unsafe {
|
||||
let texture_id = self.create_gl_texture(width, height);
|
||||
let texture_info = TextureInfo {
|
||||
target: gl::TEXTURE_2D,
|
||||
id: texture_id,
|
||||
format: gl::RGBA8,
|
||||
protected: skia::gpu::Protected::No,
|
||||
};
|
||||
gpu::backend_textures::make_gl((width, height), gpu::Mipmapped::No, texture_info, label)
|
||||
};
|
||||
// Skia-managed render target (not a wrapped client texture): snapshots
|
||||
// take the cheap COW-guarded path (`fCachedImage`, dropped for free on
|
||||
// the next write when uniquely held) instead of scheduling an eager
|
||||
// full-texture copy at snapshot time. See draw-atlas-analysis Part III.
|
||||
let image_info = skia::ImageInfo::new(
|
||||
(width, height),
|
||||
skia::ColorType::RGBA8888,
|
||||
skia::AlphaType::Premul,
|
||||
None,
|
||||
);
|
||||
|
||||
let surface = gpu::surfaces::wrap_backend_texture(
|
||||
let surface = gpu::surfaces::render_target(
|
||||
&mut self.context,
|
||||
&backend_texture,
|
||||
gpu::Budgeted::Yes,
|
||||
&image_info,
|
||||
None,
|
||||
gpu::SurfaceOrigin::BottomLeft,
|
||||
None,
|
||||
skia::ColorType::RGBA8888,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.ok_or(Error::CriticalError(
|
||||
@ -165,39 +127,4 @@ impl GpuState {
|
||||
|
||||
Ok(surface)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn create_surface_from_texture(
|
||||
&mut self,
|
||||
width: i32,
|
||||
height: i32,
|
||||
texture_id: u32,
|
||||
) -> skia::Surface {
|
||||
let texture_info = TextureInfo {
|
||||
target: gl::TEXTURE_2D,
|
||||
id: texture_id,
|
||||
format: gl::RGBA8,
|
||||
protected: skia::gpu::Protected::No,
|
||||
};
|
||||
|
||||
let backend_texture = unsafe {
|
||||
gpu::backend_textures::make_gl(
|
||||
(width, height),
|
||||
gpu::Mipmapped::No,
|
||||
texture_info,
|
||||
String::from("export_texture"),
|
||||
)
|
||||
};
|
||||
|
||||
gpu::surfaces::wrap_backend_texture(
|
||||
&mut self.context,
|
||||
&backend_texture,
|
||||
gpu::SurfaceOrigin::BottomLeft,
|
||||
None,
|
||||
skia::ColorType::RGBA8888,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,11 +165,7 @@ impl DocAtlas {
|
||||
new_bottom = new_bottom.max(doc_rect.bottom.ceil());
|
||||
}
|
||||
|
||||
// Pad to reduce realloc frequency. A realloc copies the whole atlas
|
||||
// (up to max_texture_size², hundreds of MB), so sides that actually
|
||||
// grow get a geometric margin — half the current atlas span, at least
|
||||
// a few tiles — making sustained pans cost O(log) reallocs instead of
|
||||
// one per tile.
|
||||
// Pad to reduce realloc frequency
|
||||
let pad = tiles::TILE_SIZE;
|
||||
if needs_init {
|
||||
new_left -= pad;
|
||||
@ -191,17 +187,12 @@ impl DocAtlas {
|
||||
if new_bottom > current_bottom {
|
||||
new_bottom += grow_y;
|
||||
}
|
||||
// Writes are clamped to doc_bounds, so texture past it is wasted;
|
||||
// clip the margin to the document (but never below the rect that
|
||||
// triggered this grow).
|
||||
if let Some(bounds) = self.doc_bounds {
|
||||
new_left = new_left.max((bounds.left - pad).min(doc_rect.left.floor()));
|
||||
new_top = new_top.max((bounds.top - pad).min(doc_rect.top.floor()));
|
||||
new_right = new_right.min((bounds.right + pad).max(doc_rect.right.ceil()));
|
||||
new_bottom = new_bottom.min((bounds.bottom + pad).max(doc_rect.bottom.ceil()));
|
||||
}
|
||||
// Never shrink below current coverage: the old-atlas copy must fit,
|
||||
// and shrinking would invite grow/shrink oscillation.
|
||||
new_left = new_left.min(current_left);
|
||||
new_top = new_top.min(current_top);
|
||||
new_right = new_right.max(current_right);
|
||||
@ -263,7 +254,8 @@ impl DocAtlas {
|
||||
self.origin = skia::Point::new(new_left, new_top);
|
||||
self.size = skia::ISize::new(new_w, new_h);
|
||||
self.scale = new_scale;
|
||||
gpu_state.delete_surface(&mut self.surface);
|
||||
// Old surface is Skia-managed: dropping it on reassignment frees the
|
||||
// texture back to the resource cache.
|
||||
self.surface = new_surface;
|
||||
Ok(())
|
||||
}
|
||||
@ -454,8 +446,6 @@ pub struct Surfaces {
|
||||
// Persistent viewport-sized surface used to keep the last presented frame.
|
||||
backbuffer: skia::Surface,
|
||||
// Atlas used to keep tiles.
|
||||
tile_atlas: skia::Surface,
|
||||
|
||||
tiles: TileTextureCache,
|
||||
pub atlas: DocAtlas,
|
||||
sampling_options: skia::SamplingOptions,
|
||||
@ -467,7 +457,6 @@ pub struct Surfaces {
|
||||
dpr: f32,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Surfaces {
|
||||
pub fn try_new(
|
||||
(width, height): (i32, i32),
|
||||
@ -489,11 +478,6 @@ impl Surfaces {
|
||||
gpu_state.create_surface_with_dimensions("backbuffer".to_string(), width, height)?;
|
||||
|
||||
let max_texture_size = gpu_state.max_texture_size();
|
||||
let tile_atlas = gpu_state.create_surface_with_dimensions(
|
||||
"tile_atlas".to_string(),
|
||||
max_texture_size,
|
||||
max_texture_size,
|
||||
)?;
|
||||
|
||||
let current =
|
||||
gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims)?;
|
||||
@ -513,8 +497,7 @@ impl Surfaces {
|
||||
let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height)?;
|
||||
let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height)?;
|
||||
|
||||
// 512, why not?
|
||||
let tiles = TileTextureCache::new(tile_atlas.width(), 512);
|
||||
let tiles = TileTextureCache::try_new(max_texture_size)?;
|
||||
let atlas = DocAtlas::try_new()?;
|
||||
Ok(Self {
|
||||
target,
|
||||
@ -530,7 +513,6 @@ impl Surfaces {
|
||||
debug,
|
||||
export,
|
||||
backbuffer,
|
||||
tile_atlas,
|
||||
tiles,
|
||||
atlas,
|
||||
sampling_options,
|
||||
@ -549,43 +531,41 @@ impl Surfaces {
|
||||
self.dpr = dpr;
|
||||
}
|
||||
|
||||
pub fn clear_tiles(&mut self) {
|
||||
self.tiles.clear();
|
||||
}
|
||||
|
||||
pub fn draw_tile_atlas_to_backbuffer(
|
||||
&mut self,
|
||||
viewbox: &Viewbox,
|
||||
tile_viewbox: &TileViewbox,
|
||||
background: skia::Color,
|
||||
) {
|
||||
self.tiles.update(viewbox, tile_viewbox);
|
||||
let canvas = self.backbuffer.canvas();
|
||||
canvas.clear(background);
|
||||
// Draw each tile via Surface::draw (clip + offset) instead of
|
||||
// draw_atlas over a full image_snapshot(): the snapshot shares the
|
||||
// atlas texture, so the first tile write after every present forced
|
||||
// a copy-on-write of the whole (max_texture_size²) atlas.
|
||||
for (transform, texture) in self.tiles.transforms.iter().zip(self.tiles.textures.iter()) {
|
||||
if texture.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let dst = skia::Rect::from_xywh(
|
||||
transform.tx,
|
||||
transform.ty,
|
||||
texture.width(),
|
||||
texture.height(),
|
||||
);
|
||||
canvas.save();
|
||||
canvas.clip_rect(dst, None, false);
|
||||
self.tile_atlas.draw(
|
||||
canvas,
|
||||
(transform.tx - texture.left, transform.ty - texture.top),
|
||||
self.atlas_sampling_options,
|
||||
Some(&skia::Paint::default()),
|
||||
);
|
||||
canvas.restore();
|
||||
self.backbuffer.canvas().clear(background);
|
||||
self.draw_cached_tiles_over_backbuffer(viewbox, tile_viewbox);
|
||||
}
|
||||
|
||||
/// Composite the visible cached tiles onto Backbuffer without clearing it
|
||||
/// first. Used by progressive (Partial-frame) presents, where the DocAtlas
|
||||
/// preview is drawn underneath as a backdrop for not-yet-rendered tiles.
|
||||
pub fn draw_cached_tiles_over_backbuffer(
|
||||
&mut self,
|
||||
viewbox: &Viewbox,
|
||||
tile_viewbox: &TileViewbox,
|
||||
) {
|
||||
let (xforms, texs) = self.tiles.visible_batch(viewbox, tile_viewbox);
|
||||
if xforms.is_empty() {
|
||||
return;
|
||||
}
|
||||
let sampling_options = self.atlas_sampling_options;
|
||||
// One snapshot of the managed atlas, one batched draw from it.
|
||||
let image = self.tiles.snapshot();
|
||||
self.backbuffer.canvas().draw_atlas(
|
||||
&image,
|
||||
&xforms,
|
||||
&texs,
|
||||
None,
|
||||
skia::BlendMode::SrcOver,
|
||||
sampling_options,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
/// Draw the persistent atlas onto the backbuffer using the current viewbox transform.
|
||||
@ -864,7 +844,7 @@ impl Surfaces {
|
||||
SurfaceId::UI => &mut self.ui,
|
||||
SurfaceId::Export => &mut self.export,
|
||||
SurfaceId::Atlas => &mut self.atlas.surface,
|
||||
SurfaceId::TileAtlas => &mut self.tile_atlas,
|
||||
SurfaceId::TileAtlas => self.tiles.surface_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -885,7 +865,7 @@ impl Surfaces {
|
||||
SurfaceId::UI => &self.ui,
|
||||
SurfaceId::Export => &self.export,
|
||||
SurfaceId::Atlas => &self.atlas.surface,
|
||||
SurfaceId::TileAtlas => &self.tile_atlas,
|
||||
SurfaceId::TileAtlas => self.tiles.surface(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -927,18 +907,7 @@ impl Surfaces {
|
||||
}
|
||||
|
||||
pub fn clear_tile_atlas(&mut self) {
|
||||
self.tile_atlas.canvas().clear(skia::Color::TRANSPARENT);
|
||||
}
|
||||
|
||||
/// Seed `Backbuffer` from `Target` (last presented frame).
|
||||
pub fn seed_backbuffer_from_target(&mut self) {
|
||||
let sampling_options = self.sampling_options;
|
||||
self.target.draw(
|
||||
self.backbuffer.canvas(),
|
||||
(0.0, 0.0),
|
||||
sampling_options,
|
||||
Some(&skia::Paint::default()),
|
||||
);
|
||||
self.tiles.reset_all();
|
||||
}
|
||||
|
||||
fn reset_from_target(&mut self, target: skia::Surface) -> Result<()> {
|
||||
@ -1097,12 +1066,6 @@ impl Surfaces {
|
||||
self.backbuffer.canvas().clear(color);
|
||||
}
|
||||
|
||||
pub fn clear_backbuffer_rect(&mut self, rect: skia::Rect, color: skia::Color) {
|
||||
let mut paint = Paint::default();
|
||||
paint.set_color(color);
|
||||
self.backbuffer.canvas().draw_rect(rect, &paint);
|
||||
}
|
||||
|
||||
pub fn reset(&mut self, color: skia::Color) {
|
||||
self.canvas(SurfaceId::Fills).restore_to_count(1);
|
||||
self.canvas(SurfaceId::InnerShadows).restore_to_count(1);
|
||||
@ -1213,9 +1176,9 @@ impl Surfaces {
|
||||
.blit_tile_image_into_atlas(gpu_state, &tile_image, tile_doc_rect);
|
||||
self.atlas.tile_doc_rects.insert(*tile, tile_doc_rect);
|
||||
|
||||
// Draws current tile into tile atlas
|
||||
// Write the tile into its allocated slot on the atlas surface.
|
||||
let tile_ref = self.tiles.add(tile_viewbox, tile);
|
||||
self.tile_atlas.canvas().draw_image_rect(
|
||||
self.tiles.surface_mut().canvas().draw_image_rect(
|
||||
&tile_image,
|
||||
None,
|
||||
tile_ref.rect,
|
||||
@ -1261,6 +1224,9 @@ impl Surfaces {
|
||||
return None;
|
||||
}
|
||||
|
||||
// One snapshot of the managed atlas, sampled for every covered tile.
|
||||
let tiles_image = self.tiles.snapshot();
|
||||
|
||||
let canvas = scratch.canvas();
|
||||
canvas.clear(skia::Color::TRANSPARENT);
|
||||
|
||||
@ -1286,31 +1252,21 @@ impl Surfaces {
|
||||
(clip_doc.bottom - vb_top) * scale - iy0,
|
||||
);
|
||||
|
||||
if let Some(tile_ref) = self.tiles.get(tile) {
|
||||
let bounds = skia::IRect::from_ltrb(
|
||||
tile_ref.rect.left as i32,
|
||||
tile_ref.rect.top as i32,
|
||||
tile_ref.rect.right as i32,
|
||||
tile_ref.rect.bottom as i32,
|
||||
);
|
||||
let Some(tile_image) = self.tile_atlas.image_snapshot_with_bounds(bounds)
|
||||
else {
|
||||
panic!("Cannot retrieve tile image");
|
||||
};
|
||||
let iw = tile_image.width() as f32;
|
||||
let ih = tile_image.height() as f32;
|
||||
if let Some(slot) = self.tiles.slot(tile) {
|
||||
let iw = slot.rect.width();
|
||||
let ih = slot.rect.height();
|
||||
let td_w = tile_doc.width().max(1e-6);
|
||||
let td_h = tile_doc.height().max(1e-6);
|
||||
|
||||
// Sub-rect of the tile, in atlas pixels.
|
||||
let src = skia::Rect::from_ltrb(
|
||||
((clip_doc.left - tile_doc.left) / td_w) * iw,
|
||||
((clip_doc.top - tile_doc.top) / td_h) * ih,
|
||||
((clip_doc.right - tile_doc.left) / td_w) * iw,
|
||||
((clip_doc.bottom - tile_doc.top) / td_h) * ih,
|
||||
slot.rect.left + ((clip_doc.left - tile_doc.left) / td_w) * iw,
|
||||
slot.rect.top + ((clip_doc.top - tile_doc.top) / td_h) * ih,
|
||||
slot.rect.left + ((clip_doc.right - tile_doc.left) / td_w) * iw,
|
||||
slot.rect.top + ((clip_doc.bottom - tile_doc.top) / td_h) * ih,
|
||||
);
|
||||
|
||||
canvas.draw_image_rect(
|
||||
tile_image,
|
||||
&tiles_image,
|
||||
Some((&src, skia::canvas::SrcRectConstraint::Fast)),
|
||||
dst,
|
||||
&paint,
|
||||
@ -1351,45 +1307,22 @@ impl Surfaces {
|
||||
self.tiles.mark_stale(tile);
|
||||
}
|
||||
|
||||
pub fn get_tile_image_from_tile_atlas(&mut self, tile: Tile) -> Option<skia::Image> {
|
||||
let Some(tile_ref) = self.tiles.get(tile) else {
|
||||
panic!("Tile not found {}:{}", tile.0, tile.1);
|
||||
};
|
||||
|
||||
let rect = IRect::from_ltrb(
|
||||
tile_ref.rect.left as i32,
|
||||
tile_ref.rect.top as i32,
|
||||
tile_ref.rect.right as i32,
|
||||
tile_ref.rect.bottom as i32,
|
||||
);
|
||||
self.tile_atlas.image_snapshot_with_bounds(rect)
|
||||
}
|
||||
|
||||
pub fn draw_cached_tile_into_backbuffer(&mut self, tile: Tile, rect: &Rect) {
|
||||
// Draw the atlas region via Surface::draw (clip + transform) instead of
|
||||
// image_snapshot_with_bounds + draw_image_rect: a subset snapshot is an
|
||||
// eager GPU copy, paid per tile per frame on pan previews.
|
||||
let Some(tile_ref) = self.tiles.get(tile) else {
|
||||
let Some(slot) = self.tiles.slot(tile) else {
|
||||
panic!("Tile not found {}:{}", tile.0, tile.1);
|
||||
};
|
||||
let src = tile_ref.rect;
|
||||
let src = slot.rect;
|
||||
if src.width() <= 0.0 || src.height() <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let sampling_options = self.atlas_sampling_options;
|
||||
let backbuffer_canvas = self.backbuffer.canvas();
|
||||
backbuffer_canvas.save();
|
||||
backbuffer_canvas.clip_rect(*rect, None, false);
|
||||
backbuffer_canvas.translate((rect.left, rect.top));
|
||||
backbuffer_canvas.scale((rect.width() / src.width(), rect.height() / src.height()));
|
||||
backbuffer_canvas.translate((-src.left, -src.top));
|
||||
self.tile_atlas.draw(
|
||||
backbuffer_canvas,
|
||||
(0.0, 0.0),
|
||||
sampling_options,
|
||||
Some(&skia::Paint::default()),
|
||||
// One snapshot of the managed atlas, plain image draw from it.
|
||||
let image = self.tiles.snapshot();
|
||||
self.backbuffer.canvas().draw_image_rect(
|
||||
&image,
|
||||
Some((&src, skia::canvas::SrcRectConstraint::Fast)),
|
||||
*rect,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
backbuffer_canvas.restore();
|
||||
}
|
||||
|
||||
/// Draws the current tile directly to the backbuffer and cache surfaces without
|
||||
@ -1519,174 +1452,148 @@ impl Surfaces {
|
||||
}
|
||||
}
|
||||
|
||||
/// Side length of the single tile atlas texture (clamped to the GPU cap).
|
||||
const ATLAS_SIZE: i32 = 4096;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TileAtlasTextureRef {
|
||||
pub index: usize,
|
||||
pub rect: skia::Rect,
|
||||
}
|
||||
|
||||
impl TileAtlasTextureRef {
|
||||
pub fn new(index: usize, rect: skia::Rect) -> Self {
|
||||
Self { index, rect }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TileAtlasTextureProvider {
|
||||
pub index: usize,
|
||||
pub length: usize,
|
||||
pub in_use: Vec<bool>,
|
||||
pub rects: Vec<Rect>,
|
||||
}
|
||||
|
||||
impl TileAtlasTextureProvider {
|
||||
pub fn new(texture_size: i32, tile_size: i32) -> Self {
|
||||
let side = texture_size / tile_size;
|
||||
let length = side * side;
|
||||
let mut rects = Vec::with_capacity(length as usize);
|
||||
for i in 0..length {
|
||||
let left = (i % side) as f32 * tile_size as f32;
|
||||
let top = (i / side) as f32 * tile_size as f32;
|
||||
let right = left + tile_size as f32;
|
||||
let bottom = top + tile_size as f32;
|
||||
rects.push(Rect::new(left, top, right, bottom));
|
||||
}
|
||||
Self {
|
||||
index: 0,
|
||||
length: length as usize,
|
||||
in_use: vec![false; length as usize],
|
||||
rects,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allocate(&mut self) -> Option<TileAtlasTextureRef> {
|
||||
let start = self.index;
|
||||
loop {
|
||||
if !self.in_use[self.index] {
|
||||
self.in_use[self.index] = true;
|
||||
return Some(TileAtlasTextureRef::new(self.index, self.rects[self.index]));
|
||||
}
|
||||
|
||||
self.index = (self.index + 1) % self.length;
|
||||
if self.index == start {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deallocate(&mut self, reference: TileAtlasTextureRef) -> bool {
|
||||
// In this case the user of the provider it's trying to release
|
||||
// a reference already freed.
|
||||
if !self.in_use[reference.index] {
|
||||
return false;
|
||||
}
|
||||
self.in_use[reference.index] = false;
|
||||
self.index = reference.index;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// A single Skia-managed atlas texture holding screen-space tiles at the
|
||||
/// current zoom. Because the surface is Skia-owned (budgeted), `image_snapshot`
|
||||
/// takes the cheap COW-guarded path: in the render loop's `write → snapshot →
|
||||
/// draw` order the snapshot is dropped before the next write, so no full-texture
|
||||
/// copy ever executes. That makes the per-page freeze/recycle/evict machinery
|
||||
/// (needed only to keep writes and snapshots off the same wrapped texture)
|
||||
/// unnecessary — one surface plus a slot free list is enough. See
|
||||
/// draw-atlas-analysis Part III.
|
||||
pub struct TileTextureCache {
|
||||
tile_size: f32,
|
||||
provider: TileAtlasTextureProvider,
|
||||
transforms: Vec<skia::RSXform>,
|
||||
textures: Vec<skia::Rect>,
|
||||
side: usize,
|
||||
slots: usize,
|
||||
surface: skia::Surface,
|
||||
grid: HashMap<Tile, TileAtlasTextureRef>,
|
||||
free: Vec<usize>,
|
||||
next: usize,
|
||||
removed: HashSet<Tile>,
|
||||
stale: HashSet<Tile>,
|
||||
}
|
||||
|
||||
impl TileTextureCache {
|
||||
pub fn new(texture_size: i32, capacity: usize) -> Self {
|
||||
Self {
|
||||
pub fn try_new(max_texture_size: i32) -> Result<Self> {
|
||||
let atlas_size = ATLAS_SIZE.min(max_texture_size.max(TILE_SIZE));
|
||||
let side = (atlas_size / TILE_SIZE) as usize;
|
||||
let mut surface = get_gpu_state().create_surface_with_dimensions(
|
||||
"tile_atlas".to_string(),
|
||||
atlas_size,
|
||||
atlas_size,
|
||||
)?;
|
||||
surface.canvas().clear(skia::Color::TRANSPARENT);
|
||||
Ok(Self {
|
||||
tile_size: tiles::TILE_SIZE,
|
||||
provider: TileAtlasTextureProvider::new(texture_size, TILE_SIZE),
|
||||
transforms: Vec::with_capacity(capacity),
|
||||
textures: Vec::with_capacity(capacity),
|
||||
grid: HashMap::with_capacity(capacity),
|
||||
removed: HashSet::with_capacity(capacity),
|
||||
stale: HashSet::with_capacity(capacity),
|
||||
}
|
||||
side,
|
||||
slots: side * side,
|
||||
surface,
|
||||
grid: HashMap::with_capacity(256),
|
||||
free: Vec::new(),
|
||||
next: 0,
|
||||
removed: HashSet::with_capacity(256),
|
||||
stale: HashSet::with_capacity(256),
|
||||
})
|
||||
}
|
||||
|
||||
fn slot_rect(&self, index: usize) -> skia::Rect {
|
||||
let ts = TILE_SIZE as f32;
|
||||
let left = (index % self.side) as f32 * ts;
|
||||
let top = (index / self.side) as f32 * ts;
|
||||
Rect::from_xywh(left, top, ts, ts)
|
||||
}
|
||||
|
||||
fn gc(&mut self) {
|
||||
// Make a real remove
|
||||
for tile in self.removed.iter() {
|
||||
if let Some(tile_ref) = self.grid.remove(tile) {
|
||||
self.provider.deallocate(tile_ref);
|
||||
let removed: Vec<Tile> = self.removed.drain().collect();
|
||||
for tile in removed {
|
||||
if let Some(slot) = self.grid.remove(&tile) {
|
||||
self.free.push(slot.index);
|
||||
}
|
||||
self.stale.remove(tile);
|
||||
self.stale.remove(&tile);
|
||||
}
|
||||
self.removed.clear();
|
||||
}
|
||||
|
||||
fn gc_non_visible(&mut self, tile_viewbox: &TileViewbox) {
|
||||
let marked: Vec<_> = self
|
||||
.grid
|
||||
.iter_mut()
|
||||
.filter_map(|(tile, _)| {
|
||||
if !tile_viewbox.is_in_interest_area(tile) {
|
||||
Some(*tile)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.keys()
|
||||
.filter(|tile| !tile_viewbox.is_in_interest_area(tile))
|
||||
.take(TEXTURES_BATCH_DELETE)
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
for tile in marked.iter() {
|
||||
if let Some(tile_ref) = self.grid.remove(tile) {
|
||||
self.provider.deallocate(tile_ref);
|
||||
if let Some(slot) = self.grid.remove(tile) {
|
||||
self.free.push(slot.index);
|
||||
}
|
||||
self.removed.remove(tile);
|
||||
self.stale.remove(tile);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, viewbox: &Viewbox, tile_viewbox: &TileViewbox) {
|
||||
if self.transforms.len() != tile_viewbox.visible_rect.len() as usize {
|
||||
self.transforms.resize(
|
||||
tile_viewbox.visible_rect.len() as usize,
|
||||
skia::RSXform::new(1.0, 0.0, Point::default()),
|
||||
);
|
||||
}
|
||||
|
||||
if self.textures.len() != tile_viewbox.visible_rect.len() as usize {
|
||||
self.textures.resize(
|
||||
tile_viewbox.visible_rect.len() as usize,
|
||||
skia::Rect::new_empty(),
|
||||
);
|
||||
}
|
||||
|
||||
for texture in self.textures.iter_mut() {
|
||||
texture.set_empty();
|
||||
}
|
||||
|
||||
/// Visible cached tiles as a single (xform, tex-rect) batch, ready for one
|
||||
/// `draw_atlas` call from a single snapshot of the atlas surface.
|
||||
pub fn visible_batch(
|
||||
&self,
|
||||
viewbox: &Viewbox,
|
||||
tile_viewbox: &TileViewbox,
|
||||
) -> (Vec<skia::RSXform>, Vec<skia::Rect>) {
|
||||
let mut xforms: Vec<skia::RSXform> = vec![];
|
||||
let mut texs: Vec<skia::Rect> = vec![];
|
||||
let offset = viewbox.get_offset();
|
||||
let mut index = 0;
|
||||
for y in tile_viewbox.visible_rect.top()..=tile_viewbox.visible_rect.bottom() {
|
||||
for x in tile_viewbox.visible_rect.left()..=tile_viewbox.visible_rect.right() {
|
||||
let tile = Tile(x, y);
|
||||
|
||||
if self.removed.contains(&tile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(tile_ref) = self.grid.get(&tile) else {
|
||||
let Some(slot) = self.grid.get(&tile) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
self.transforms[index].tx = x as f32 * self.tile_size - offset.x;
|
||||
self.transforms[index].ty = y as f32 * self.tile_size - offset.y;
|
||||
|
||||
self.textures[index].set_ltrb(
|
||||
tile_ref.rect.left,
|
||||
tile_ref.rect.top,
|
||||
tile_ref.rect.right,
|
||||
tile_ref.rect.bottom,
|
||||
);
|
||||
|
||||
index += 1;
|
||||
xforms.push(skia::RSXform::new(
|
||||
1.0,
|
||||
0.0,
|
||||
Point::new(
|
||||
x as f32 * self.tile_size - offset.x,
|
||||
y as f32 * self.tile_size - offset.y,
|
||||
),
|
||||
));
|
||||
texs.push(slot.rect);
|
||||
}
|
||||
}
|
||||
(xforms, texs)
|
||||
}
|
||||
|
||||
/// A snapshot of the atlas surface, for sampling cached tiles. Cheap on the
|
||||
/// Skia-managed surface: in `write → snapshot → draw` order the returned
|
||||
/// image is dropped before the next write, so no copy executes.
|
||||
pub fn snapshot(&mut self) -> skia::Image {
|
||||
self.surface.image_snapshot()
|
||||
}
|
||||
|
||||
pub fn surface(&self) -> &skia::Surface {
|
||||
&self.surface
|
||||
}
|
||||
|
||||
pub fn surface_mut(&mut self) -> &mut skia::Surface {
|
||||
&mut self.surface
|
||||
}
|
||||
|
||||
pub fn reset_all(&mut self) {
|
||||
self.grid.clear();
|
||||
self.removed.clear();
|
||||
self.stale.clear();
|
||||
self.free.clear();
|
||||
self.next = 0;
|
||||
self.surface.canvas().clear(skia::Color::TRANSPARENT);
|
||||
}
|
||||
|
||||
pub fn has(&self, tile: Tile) -> bool {
|
||||
@ -1703,39 +1610,67 @@ impl TileTextureCache {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Allocate a writable slot for `tile`.
|
||||
pub fn add(&mut self, tile_viewbox: &TileViewbox, tile: &Tile) -> TileAtlasTextureRef {
|
||||
let capacity = self.provider.length;
|
||||
if self.grid.len() >= capacity {
|
||||
// First we try to remove the obsolete tiles.
|
||||
self.gc();
|
||||
if let Some(old) = self.grid.remove(tile) {
|
||||
self.free.push(old.index);
|
||||
}
|
||||
|
||||
// If we still have a texture capacity problem, then
|
||||
// we try to remove all of those tiles that aren't
|
||||
// in the interest area.
|
||||
if self.grid.len() >= capacity {
|
||||
self.gc_non_visible(tile_viewbox);
|
||||
}
|
||||
|
||||
let Some(tile_ref) = self.provider.allocate() else {
|
||||
panic!("Tile texture allocation failed {}:{}", tile.0, tile.1);
|
||||
let index = self.allocate_index(tile_viewbox);
|
||||
let slot = TileAtlasTextureRef {
|
||||
index,
|
||||
rect: self.slot_rect(index),
|
||||
};
|
||||
|
||||
if let Some(old) = self.grid.insert(*tile, tile_ref.clone()) {
|
||||
self.provider.deallocate(old);
|
||||
}
|
||||
|
||||
self.grid.insert(*tile, slot.clone());
|
||||
self.removed.remove(tile);
|
||||
self.stale.remove(tile);
|
||||
|
||||
tile_ref
|
||||
slot
|
||||
}
|
||||
|
||||
pub fn get(&mut self, tile: Tile) -> Option<&TileAtlasTextureRef> {
|
||||
fn try_take_slot(&mut self) -> Option<usize> {
|
||||
self.free.pop().or_else(|| {
|
||||
(self.next < self.slots).then(|| {
|
||||
self.next += 1;
|
||||
self.next - 1
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Evict one cached tile (prefer one outside the interest area) and reuse
|
||||
/// its slot. The evicted tile simply re-renders on a later frame.
|
||||
fn evict_one(&mut self, tile_viewbox: &TileViewbox) -> Option<usize> {
|
||||
let victim = self
|
||||
.grid
|
||||
.keys()
|
||||
.find(|tile| !tile_viewbox.is_in_interest_area(tile))
|
||||
.or_else(|| self.grid.keys().next())
|
||||
.copied()?;
|
||||
let slot = self.grid.remove(&victim)?;
|
||||
self.removed.remove(&victim);
|
||||
self.stale.remove(&victim);
|
||||
Some(slot.index)
|
||||
}
|
||||
|
||||
fn allocate_index(&mut self, tile_viewbox: &TileViewbox) -> usize {
|
||||
if let Some(index) = self.try_take_slot() {
|
||||
return index;
|
||||
}
|
||||
self.gc();
|
||||
if let Some(index) = self.try_take_slot() {
|
||||
return index;
|
||||
}
|
||||
self.gc_non_visible(tile_viewbox);
|
||||
if let Some(index) = self.try_take_slot() {
|
||||
return index;
|
||||
}
|
||||
// Atlas full: evict a tile and reuse its slot.
|
||||
self.evict_one(tile_viewbox).unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn slot(&self, tile: Tile) -> Option<TileAtlasTextureRef> {
|
||||
if self.removed.contains(&tile) {
|
||||
return None;
|
||||
}
|
||||
self.grid.get(&tile)
|
||||
self.grid.get(&tile).cloned()
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, tile: Tile) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user