diff --git a/crates/canvas-2d/src/context/mod.rs b/crates/canvas-2d/src/context/mod.rs index a4aedb99..de14ef00 100644 --- a/crates/canvas-2d/src/context/mod.rs +++ b/crates/canvas-2d/src/context/mod.rs @@ -246,6 +246,8 @@ pub struct Context { pub(crate) state_stack: Vec, pub(crate) font_color: Color, pub(crate) recording: Option, + /// GPU readback of the surface, with the generation it was read at + pub(crate) snapshot: Option<(u32, Image)>, } impl Drop for Context { @@ -520,6 +522,10 @@ impl Context { } pub fn get_image(&mut self) -> Option { + if let Some(image) = self.cached_snapshot() { + return Some(image); + } + #[cfg(feature = "gl")] { if let Some(ref context) = self.gl_context { @@ -529,16 +535,28 @@ impl Context { self.flush(); - let snapshot = self.surface.image_snapshot(); + if self.surface_data.engine == SurfaceEngine::CPU { + return Some(self.surface.image_snapshot()); + } - // A GPU snapshot is only readable through its context: copy it to the CPU. - let ret = if self.surface_data.engine != SurfaceEngine::CPU { - snapshot.make_raster_image(self.direct_context.as_mut(), CachingHint::Allow) - } else { - Some(snapshot) - }; + // read back once per change + let generation = self.surface.generation_id(); + let snapshot = self.surface.image_snapshot(); + let image = snapshot.make_raster_image(self.direct_context.as_mut(), CachingHint::Allow)?; + self.snapshot = Some((generation, image.clone())); + Some(image) + } - ret + /// The last readback, if nothing was drawn since. + fn cached_snapshot(&mut self) -> Option { + if self.recording.is_some() { + return None; + } + let generation = self.surface.generation_id(); + match &self.snapshot { + Some((cached, image)) if *cached == generation => Some(image.clone()), + _ => None, + } } pub fn draw_pixels(&mut self, image: &Image) { diff --git a/crates/canvas-2d/src/context/recording.rs b/crates/canvas-2d/src/context/recording.rs index 59178c13..431c249b 100644 --- a/crates/canvas-2d/src/context/recording.rs +++ b/crates/canvas-2d/src/context/recording.rs @@ -6,17 +6,24 @@ use skia_safe::{ Rect, M44, }; +use std::sync::atomic::{AtomicU64, Ordering}; + use crate::context::{Context, State}; /// Recordings open with a save of their own: a bottom-level clip can't be undone, and `reset()` must. pub(crate) const RECORDING_BASE: usize = 2; +static REVISIONS: AtomicU64 = AtomicU64::new(1); + pub(crate) struct Recording { recorder: PictureRecorder, bounds: Rect, ops: Vec, /// Began with a full-canvas clear, so no earlier frame shows through. cleared: bool, + /// Changes whenever something may have been drawn + revision: u64, + touched: bool, } impl Recording { @@ -28,17 +35,32 @@ impl Recording { bounds, ops: Vec::new(), cleared: false, + revision: REVISIONS.fetch_add(1, Ordering::Relaxed), + touched: false, } } #[inline] pub(crate) fn canvas(&mut self) -> &Canvas { + self.touched = true; + self.recording_canvas() + } + + #[inline] + fn recording_canvas(&mut self) -> &Canvas { // `restart` begins the next recording before returning. self.recorder .recording_canvas() .expect("recording canvas is always live") } + fn revision(&mut self) -> u64 { + if std::mem::take(&mut self.touched) { + self.revision = REVISIONS.fetch_add(1, Ordering::Relaxed); + } + self.revision + } + fn cut(&mut self, stack: &[State], current: &State) { if let Some(picture) = self.restart(stack, current) { self.ops.push(FrameOp::Picture(picture)); @@ -52,7 +74,7 @@ impl Recording { } fn restart(&mut self, stack: &[State], current: &State) -> Option { - let matrix = self.canvas().local_to_device(); + let matrix = self.recording_canvas().local_to_device(); let picture = self.recorder.finish_recording_as_picture(Some(&self.bounds)); self.recorder.begin_recording(self.bounds, false).save(); let canvas = self.recorder.recording_canvas().expect("just begun"); @@ -210,6 +232,11 @@ impl Context { self.recording = Some(Recording::new(self.surface_data.bounds)); } + /// Unchanged while nothing is drawn. + pub fn revision(&mut self) -> Option { + self.recording.as_mut().map(Recording::revision) + } + /// Frames must be replayed in order. pub fn take_frame(&mut self) -> Frame { let Some(recording) = self.recording.as_mut() else { @@ -249,6 +276,7 @@ impl Context { return false; }; recording.cut(&self.state_stack, &self.state); + recording.touched = true; let len = (row_bytes * info.height().max(0) as usize).min(bytes.len()); recording.ops.push(FrameOp::Pixels(PixelWrite { width: info.width(), @@ -301,6 +329,7 @@ impl Context { }; let matrix = recording.canvas().local_to_device(); recording.cut(&self.state_stack, &self.state); + recording.touched = true; recording.ops.push(FrameOp::External(ExternalDraw { state: StateSnapshot::take(&self.state_stack, &self.state, matrix), src, diff --git a/crates/canvas-2d/src/context/surface.rs b/crates/canvas-2d/src/context/surface.rs index b74aa259..29183e77 100644 --- a/crates/canvas-2d/src/context/surface.rs +++ b/crates/canvas-2d/src/context/surface.rs @@ -75,6 +75,7 @@ impl Context { state_stack: vec![], font_color: Color::new(font_color as u32), recording: None, + snapshot: None, #[cfg(feature = "gl")] window_surface: None, #[cfg(any(feature = "gl", feature = "vulkan", feature = "metal"))] diff --git a/crates/canvas-2d/src/context/surface_d3d.rs b/crates/canvas-2d/src/context/surface_d3d.rs index 015c9454..4415bf9a 100644 --- a/crates/canvas-2d/src/context/surface_d3d.rs +++ b/crates/canvas-2d/src/context/surface_d3d.rs @@ -347,6 +347,7 @@ impl Context { state_stack: vec![], font_color: Color::new(font_color as u32), recording: None, + snapshot: None, #[cfg(feature = "gl")] window_surface: None, #[cfg(any(feature = "gl", feature = "vulkan", feature = "metal"))] diff --git a/crates/canvas-2d/src/context/surface_gl.rs b/crates/canvas-2d/src/context/surface_gl.rs index 4c1b4901..446ba245 100644 --- a/crates/canvas-2d/src/context/surface_gl.rs +++ b/crates/canvas-2d/src/context/surface_gl.rs @@ -232,6 +232,7 @@ impl Context { state_stack: vec![], font_color: Color::new(font_color as u32), recording: None, + snapshot: None, surface_state: SurfaceState::None, }) } diff --git a/crates/canvas-2d/src/context/surface_metal.rs b/crates/canvas-2d/src/context/surface_metal.rs index f4297279..344df498 100644 --- a/crates/canvas-2d/src/context/surface_metal.rs +++ b/crates/canvas-2d/src/context/surface_metal.rs @@ -128,6 +128,7 @@ impl Context { state_stack: vec![], font_color: Color::new(font_color as u32), recording: None, + snapshot: None, #[cfg(feature = "gl")] window_surface: None, #[cfg(any(feature = "gl", feature = "vulkan", feature = "metal"))] @@ -269,6 +270,7 @@ impl Context { state_stack: vec![], font_color: Color::new(font_color as u32), recording: None, + snapshot: None, #[cfg(feature = "gl")] window_surface: None, #[cfg(any(feature = "gl", feature = "vulkan", feature = "metal"))] @@ -353,6 +355,7 @@ impl Context { state_stack: vec![], font_color: Color::new(font_color as u32), recording: None, + snapshot: None, #[cfg(feature = "gl")] window_surface: None, #[cfg(any(feature = "gl", feature = "vulkan", feature = "metal"))] diff --git a/crates/canvas-2d/src/context/surface_vulkan.rs b/crates/canvas-2d/src/context/surface_vulkan.rs index c8b3deed..4be0ba04 100644 --- a/crates/canvas-2d/src/context/surface_vulkan.rs +++ b/crates/canvas-2d/src/context/surface_vulkan.rs @@ -143,6 +143,7 @@ impl Context { state_stack: vec![], font_color: skia_safe::Color::new(font_color as u32), recording: None, + snapshot: None, #[cfg(feature = "gl")] window_surface: None, #[cfg(any(feature = "gl", feature = "vulkan", feature = "metal"))] diff --git a/crates/canvas-c/src/c2d/context.rs b/crates/canvas-c/src/c2d/context.rs index 97a3cc2b..5fe728eb 100644 --- a/crates/canvas-c/src/c2d/context.rs +++ b/crates/canvas-c/src/c2d/context.rs @@ -46,6 +46,8 @@ pub struct CanvasRenderingContext2D { refs: AtomicUsize, /// `context` only records; the real one lives on the render thread. threaded: Option, + /// Threaded: last image and the revision it shows + snapshot: Option<(u64, canvas_2d::context::Image)>, } impl CanvasRenderingContext2D { @@ -245,6 +247,7 @@ impl CanvasRenderingContext2D { engine: Engine::CPU, refs: AtomicUsize::new(1), threaded: None, + snapshot: None, } } @@ -256,6 +259,7 @@ impl CanvasRenderingContext2D { engine: Engine::GL, refs: AtomicUsize::new(1), threaded: None, + snapshot: None, } } @@ -267,6 +271,7 @@ impl CanvasRenderingContext2D { engine: Engine::Vulkan, refs: AtomicUsize::new(1), threaded: None, + snapshot: None, } } @@ -278,6 +283,7 @@ impl CanvasRenderingContext2D { engine: Engine::Metal, refs: AtomicUsize::new(1), threaded: None, + snapshot: None, } } @@ -289,6 +295,7 @@ impl CanvasRenderingContext2D { engine: Engine::D3D, refs: AtomicUsize::new(1), threaded: None, + snapshot: None, } } @@ -323,6 +330,7 @@ impl CanvasRenderingContext2D { engine: Engine::CPU, refs: AtomicUsize::new(1), threaded: Some(target), + snapshot: None, } } @@ -373,7 +381,16 @@ impl CanvasRenderingContext2D { } pub fn image(&mut self) -> Option { - self.with_pixels_source(|context| context.get_image()).flatten() + // skip the render thread if nothing was drawn + let revision = self.context.revision().filter(|_| !self.gpu_lost()); + if let (Some(revision), Some((cached, image))) = (revision, &self.snapshot) { + if *cached == revision { + return Some(image.clone()); + } + } + let image = self.with_pixels_source(|context| context.get_image()).flatten(); + self.snapshot = revision.zip(image.clone()); + image } pub fn image_data( @@ -843,6 +860,7 @@ pub extern "C" fn canvas_native_context_create( engine: Engine::CPU, refs: AtomicUsize::new(1), threaded: None, + snapshot: None, })) } @@ -878,6 +896,7 @@ pub extern "C" fn canvas_native_context_create_gl( engine: Engine::GL, refs: AtomicUsize::new(1), threaded: None, + snapshot: None, })) } @@ -924,6 +943,7 @@ pub extern "C" fn canvas_native_context_create_gl_no_window( engine: Engine::GL, refs: AtomicUsize::new(1), threaded: None, + snapshot: None, })) }