diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index e984bf077d..0be31dff3f 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -42,6 +42,7 @@ pub use images::*; type ClipStack = Vec<(Rect, Option, Matrix)>; +#[cfg_attr(feature = "profile-macros", derive(Debug))] #[repr(u8)] pub enum FrameType { None = 0, @@ -877,6 +878,7 @@ impl RenderState { /// on top of Target, then present. Backbuffer is left clean so it can be reused /// as-is across interactive-transform frames without stale overlay pixels. pub fn present_frame(&mut self, tree: ShapesPoolRef) { + performance::begin_measure!("present_frame"); // Viewer masked passes render a partial scene onto a transparent backbuffer. // SrcOver would keep pass-1 pixels wherever the backbuffer stays transparent. if self.viewer_masked_pass() { @@ -892,7 +894,10 @@ impl RenderState { } ui::render(self, tree); debug::render_wasm_label(self); + performance::begin_measure!("present_frame:flush_submit"); self.surfaces.flush_and_submit(SurfaceId::Target); + performance::end_measure!("present_frame:flush_submit"); + performance::end_measure!("present_frame"); } /// Renders only the canvas background and UI surface (rulers/frame), without @@ -2346,11 +2351,37 @@ impl RenderState { self.render_shape_tree_partial(base_object, tree, timestamp, allow_stop)?; if !self.options.is_interactive_transform() { + performance::begin_measure!("atlas_to_backbuffer"); self.surfaces.draw_tile_atlas_to_backbuffer( &self.viewbox, &self.tile_viewbox, self.background_color, ); + performance::end_measure!("atlas_to_backbuffer"); + } + + #[cfg(feature = "profile-macros")] + { + let usage = get_gpu_state().context.resource_cache_usage(); + let limit_mb = get_gpu_state().context.resource_cache_limit() as f64 / 1_048_576.0; + let mts = get_gpu_state().max_texture_size(); + let atlas_mb = (mts as f64 * mts as f64 * 4.0) / 1_048_576.0; + let cdim = self.surfaces.cache_dimensions(); + let cache_mb = (cdim.width as f64 * cdim.height as f64 * 4.0) / 1_048_576.0; + crate::console_log!( + "[GPU] frame={:?} interactive={} fast={} rcache_mb={:.1} limit_mb={:.1} rcache_n={} cache={}x{} cache_mb={:.1} max_tex={} atlas_mb={:.1}", + frame_type, + self.options.is_interactive_transform(), + self.options.is_fast_mode(), + usage.resource_bytes as f64 / 1_048_576.0, + limit_mb, + usage.resource_count, + cdim.width, + cdim.height, + cache_mb, + mts, + atlas_mb + ); } match frame_type { diff --git a/render-wasm/src/render/gpu_state.rs b/render-wasm/src/render/gpu_state.rs index e934dd4f34..b64dc578da 100644 --- a/render-wasm/src/render/gpu_state.rs +++ b/render-wasm/src/render/gpu_state.rs @@ -6,7 +6,7 @@ use skia_safe::gpu::{ use skia_safe::{self as skia, ISize}; const MIN_MAX_TEXTURE_SIZE: i32 = 512; -const MAX_MAX_TEXTURE_SIZE: i32 = 8192 * 2; +const MAX_MAX_TEXTURE_SIZE: i32 = 4096 * 2; #[derive(Debug, Clone)] pub struct GpuState { diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 3937976cdf..45a567c3b2 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -532,7 +532,16 @@ impl Surfaces { ) { self.tiles.update(viewbox, tile_viewbox); if self.tiles.needs_snapshot() || self.tile_atlas_image.is_none() { - self.tile_atlas_image = Some(self.tile_atlas.image_snapshot()); + // Snapshot only the occupied sub-region (origin stays at 0,0 so the + // `textures` atlas coordinates remain valid) instead of the whole + // max_texture_size² atlas — avoids a full-atlas COW copy each settle. + let snap = match self.tiles.used_bounds() { + Some(bounds) => self.tile_atlas.image_snapshot_with_bounds(bounds), + None => Some(self.tile_atlas.image_snapshot()), + }; + if let Some(image) = snap { + self.tile_atlas_image = Some(image); + } self.tiles.snapshot(); } let Some(atlas_image) = self.tile_atlas_image.as_ref() else { @@ -1579,6 +1588,35 @@ impl TileTextureCache { self.is_updated = false; } + /// Bounding box (from the atlas origin) of the slots actually sampled this + /// frame. Slots are allocated row-major from index 0 and freed slots are + /// reused first, so occupied slots cluster near (0,0): snapshotting only + /// `(0,0)..(max_right,max_bottom)` keeps the `textures`/`transforms` + /// coordinates valid (origin stays 0) while avoiding a full-atlas + /// `image_snapshot` (1GB→content-sized). `None` when nothing is in use. + pub fn used_bounds(&self) -> Option { + let mut max_right = 0.0f32; + let mut max_bottom = 0.0f32; + let mut any = false; + for r in self.textures.iter() { + if r.is_empty() { + continue; + } + any = true; + max_right = max_right.max(r.right); + max_bottom = max_bottom.max(r.bottom); + } + if !any { + return None; + } + Some(skia::IRect::new( + 0, + 0, + max_right.ceil() as i32, + max_bottom.ceil() as i32, + )) + } + fn gc_non_visible(&mut self, tile_viewbox: &TileViewbox) { let marked: Vec<_> = self .grid