diff options
Diffstat (limited to 'src/server/render_stream.rs')
| -rw-r--r-- | src/server/render_stream.rs | 124 |
1 files changed, 110 insertions, 14 deletions
diff --git a/src/server/render_stream.rs b/src/server/render_stream.rs index 669c0097..b0af26d7 100644 --- a/src/server/render_stream.rs +++ b/src/server/render_stream.rs @@ -18,6 +18,8 @@ pub(crate) enum ClientRenderState { last_surface: Option<Box<PaneSurfaceFrame>>, surface_revision: u64, surface_reuse: bool, + surface_delta: bool, + recompute_pending: bool, }, /// Terminal-ANSI clients keep a terminal diff encoder and sequence number. TerminalAnsi { @@ -34,6 +36,8 @@ impl ClientRenderState { last_surface: None, surface_revision: 0, surface_reuse: false, + surface_delta: false, + recompute_pending: false, }, RenderEncoding::TerminalAnsi => Self::TerminalAnsi { blit_encoder: BlitEncoder::new(), @@ -49,6 +53,35 @@ impl ClientRenderState { } } + pub(crate) fn enable_surface_delta(&mut self, enabled: bool) { + if let Self::Semantic { surface_delta, .. } = self { + *surface_delta = enabled; + } + } + + pub(crate) fn request_recompute(&mut self) { + if let Self::Semantic { + surface_delta: true, + recompute_pending, + .. + } = self + { + *recompute_pending = true; + } else { + self.request_repaint(); + } + } + + pub(crate) fn requires_recompute(&self) -> bool { + matches!( + self, + Self::Semantic { + recompute_pending: true, + .. + } + ) + } + pub(crate) fn reset_baseline(&mut self) { match self { Self::Semantic { last_surface, .. } => *last_surface = None, @@ -127,11 +160,14 @@ impl ClientRenderState { last_surface, surface_revision, surface_reuse, + surface_delta, + recompute_pending, } = self else { return None; }; - if surface.graphics.assets.is_empty() + if !*recompute_pending + && surface.graphics.assets.is_empty() && last_surface.as_deref().is_some_and(|last| { last.projection_revision == surface.projection_revision && last.frame == surface.frame @@ -145,26 +181,41 @@ impl ClientRenderState { return None; } surface.surface_revision = surface_revision.saturating_add(1); - let mut committed_surface = surface.clone(); - committed_surface.graphics.assets.clear(); - let reused = (*surface_reuse) + let assets = std::mem::take(&mut surface.graphics.assets); + let committed_surface = surface.clone(); + surface.graphics.assets = assets; + let mut message = ServerMessage::PaneSurface(surface); + let delta = (*surface_delta) .then_some(last_surface.as_deref()) .flatten() - .filter(|last| { - last.boot_id == surface.boot_id - && last.frame == surface.frame - // Popup cells are not part of the reusable grid; keep their compact codec. - && surface.popup.is_none() - && surface.graphics.assets.is_empty() - }) .and_then(|last| { - crate::protocol::surface_reuse::message(last.surface_revision, &mut surface) - .map_err(|error| tracing::warn!(%error, "failed to encode surface reuse")) + crate::protocol::surface_delta::message(last, &mut message) + .map_err(|error| tracing::warn!(%error, "failed to encode surface delta")) .ok() .flatten() }); + let reused = if let ServerMessage::PaneSurface(surface) = &mut message { + (delta.is_none() && *surface_reuse) + .then_some(last_surface.as_deref()) + .flatten() + .filter(|last| { + last.boot_id == surface.boot_id + && last.frame == surface.frame + // Popup cells are not part of the reusable grid; keep their compact codec. + && surface.popup.is_none() + && surface.graphics.assets.is_empty() + }) + .and_then(|last| { + crate::protocol::surface_reuse::message(last.surface_revision, surface) + .map_err(|error| tracing::warn!(%error, "failed to encode surface reuse")) + .ok() + .flatten() + }) + } else { + None + }; Some(PreparedRender::Semantic { - message: reused.unwrap_or(ServerMessage::PaneSurface(surface)), + message: delta.or(reused).unwrap_or(message), committed_surface: Box::new(committed_surface), }) } @@ -181,6 +232,9 @@ impl ClientRenderState { else { return None; }; + if self.requires_recompute() { + return None; + } let last = last_surface.as_deref()?; if last.boot_id != patch.boot_id || last.projection_revision != patch.projection_revision @@ -201,6 +255,7 @@ impl ClientRenderState { Self::Semantic { last_surface, surface_revision, + recompute_pending, .. }, PreparedRender::Semantic { @@ -209,6 +264,7 @@ impl ClientRenderState { ) => { *surface_revision = committed_surface.surface_revision; *last_surface = Some(committed_surface); + *recompute_pending = false; } ( Self::Semantic { @@ -518,6 +574,46 @@ mod tests { } #[test] + fn surface_delta_recompute_preserves_wire_baseline_but_epoch_reset_drops_it() { + for enabled in [false, true] { + let mut state = ClientRenderState::new(RenderEncoding::SemanticFrame); + state.enable_surface_delta(enabled); + let mut surface = popup_surface("popup"); + surface.popup = None; + surface.frame = FrameData::from_ratatui_buffer( + &ratatui::buffer::Buffer::empty(Rect::new(0, 0, 120, 40)), + None, + ); + let initial = state.prepare_pane_surface(surface.clone()).unwrap(); + state.commit_sent_frame(initial); + state.request_recompute(); + assert_eq!(state.last_pane_surface().is_some(), enabled); + assert_eq!(state.requires_recompute(), enabled); + // A freshness request still emits a new revision when every cell is equal. + let fresh = state.prepare_pane_surface(surface.clone()).unwrap(); + assert_eq!( + matches!(fresh.message(), ServerMessage::EndpointControl { kind, .. } + if kind == crate::protocol::surface_delta::MESSAGE_KIND), + enabled + ); + assert_eq!( + state.requires_recompute(), + enabled, + "prepare must not commit" + ); + state.commit_sent_frame(fresh); + assert!(!state.requires_recompute()); + assert_eq!(state.last_pane_surface().unwrap().surface_revision, 2); + state.request_repaint(); + assert!(state.last_pane_surface().is_none()); + let recovery = state.prepare_pane_surface(surface).unwrap(); + assert!( + matches!(recovery.message(), ServerMessage::PaneSurface(frame) if frame.surface_revision == 3) + ); + } + } + + #[test] fn surface_reuse_preserves_projection_and_patch_baselines_without_resending_cells() { for enabled in [false, true] { let mut state = ClientRenderState::new(RenderEncoding::SemanticFrame); |
