aboutsummaryrefslogtreecommitdiff
path: root/src/server/render_stream.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/render_stream.rs')
-rw-r--r--src/server/render_stream.rs529
1 files changed, 184 insertions, 345 deletions
diff --git a/src/server/render_stream.rs b/src/server/render_stream.rs
index f14deb5e..3d9e1141 100644
--- a/src/server/render_stream.rs
+++ b/src/server/render_stream.rs
@@ -4,15 +4,20 @@ use ratatui::backend::{Backend, ClearType, TestBackend, WindowSize};
use ratatui::layout::{Position, Rect, Size};
use crate::app::state::AppState;
-use crate::app::Mode;
use crate::protocol::render_ansi::{BlitEncoder, EncodedBlit};
-use crate::protocol::{CursorState, FrameData, RenderEncoding, ServerMessage, TerminalFrame};
+use crate::protocol::{
+ CursorState, FrameData, PaneSurfaceFrame, PaneSurfacePatch, RenderEncoding, ServerMessage,
+ TerminalFrame,
+};
use crate::terminal::TerminalRuntimeRegistry;
/// Per-client render baseline for the negotiated render encoding.
pub(crate) enum ClientRenderState {
/// Semantic clients compare full frame data and skip identical frames.
- Semantic { last_frame: Option<FrameData> },
+ Semantic {
+ last_surface: Option<Box<PaneSurfaceFrame>>,
+ surface_revision: u64,
+ },
/// Terminal-ANSI clients keep a terminal diff encoder and sequence number.
TerminalAnsi {
blit_encoder: BlitEncoder,
@@ -24,7 +29,10 @@ pub(crate) enum ClientRenderState {
impl ClientRenderState {
pub(crate) fn new(render_encoding: RenderEncoding) -> Self {
match render_encoding {
- RenderEncoding::SemanticFrame => Self::Semantic { last_frame: None },
+ RenderEncoding::SemanticFrame => Self::Semantic {
+ last_surface: None,
+ surface_revision: 0,
+ },
RenderEncoding::TerminalAnsi => Self::TerminalAnsi {
blit_encoder: BlitEncoder::new(),
seq: 0,
@@ -35,7 +43,7 @@ impl ClientRenderState {
pub(crate) fn reset_baseline(&mut self) {
match self {
- Self::Semantic { last_frame } => *last_frame = None,
+ Self::Semantic { last_surface, .. } => *last_surface = None,
Self::TerminalAnsi {
blit_encoder,
repaint_pending,
@@ -49,31 +57,16 @@ impl ClientRenderState {
pub(crate) fn request_repaint(&mut self) {
match self {
- Self::Semantic { last_frame } => *last_frame = None,
+ Self::Semantic { last_surface, .. } => *last_surface = None,
Self::TerminalAnsi {
repaint_pending, ..
} => *repaint_pending = true,
}
}
- pub(crate) fn reset_semantic_input_baseline(&mut self) {
- if let Self::Semantic { last_frame } = self {
- *last_frame = None;
- }
- }
-
pub(crate) fn prepare_frame(&mut self, frame: FrameData) -> Option<PreparedRender> {
match self {
- Self::Semantic { last_frame } => {
- if last_frame.as_ref() == Some(&frame) {
- crate::render_prof::event("prepare_frame.semantic.skip_current");
- return None;
- }
- crate::render_prof::event("prepare_frame.semantic.changed");
- Some(PreparedRender::Semantic {
- message: ServerMessage::Frame(frame),
- })
- }
+ Self::Semantic { .. } => None,
Self::TerminalAnsi {
blit_encoder,
seq,
@@ -111,21 +104,89 @@ impl ClientRenderState {
}
}
- pub(crate) fn last_frame(&self) -> Option<&FrameData> {
+ pub(crate) fn last_pane_surface(&self) -> Option<&PaneSurfaceFrame> {
match self {
- Self::Semantic { last_frame } => last_frame.as_ref(),
- Self::TerminalAnsi { blit_encoder, .. } => blit_encoder.last_frame(),
+ Self::Semantic { last_surface, .. } => last_surface.as_deref(),
+ Self::TerminalAnsi { .. } => None,
}
}
+ pub(crate) fn prepare_pane_surface(
+ &mut self,
+ mut surface: PaneSurfaceFrame,
+ ) -> Option<PreparedRender> {
+ let Self::Semantic {
+ last_surface,
+ surface_revision,
+ } = self
+ else {
+ return None;
+ };
+ if surface.graphics.assets.is_empty()
+ && last_surface.as_deref().is_some_and(|last| {
+ last.projection_revision == surface.projection_revision
+ && last.frame == surface.frame
+ && last.panes == surface.panes
+ && last.splits == surface.splits
+ && last.popup == surface.popup
+ && last.graphics.placements == surface.graphics.placements
+ && last.graphics.retained_assets == surface.graphics.retained_assets
+ })
+ {
+ return None;
+ }
+ surface.surface_revision = surface_revision.saturating_add(1);
+ let mut committed_surface = surface.clone();
+ committed_surface.graphics.assets.clear();
+ Some(PreparedRender::Semantic {
+ message: ServerMessage::PaneSurface(surface),
+ committed_surface: Box::new(committed_surface),
+ })
+ }
+
+ pub(crate) fn prepare_pane_surface_patch(
+ &mut self,
+ mut patch: PaneSurfacePatch,
+ mut committed_surface: PaneSurfaceFrame,
+ ) -> Option<PreparedRender> {
+ let Self::Semantic {
+ last_surface,
+ surface_revision,
+ } = self
+ else {
+ return None;
+ };
+ let last = last_surface.as_deref()?;
+ if last.boot_id != patch.boot_id
+ || last.projection_revision != patch.projection_revision
+ || last.surface_revision != patch.base_surface_revision
+ {
+ return None;
+ }
+ let next_revision = surface_revision.saturating_add(1);
+ patch.surface_revision = next_revision;
+ committed_surface.surface_revision = next_revision;
+ committed_surface.graphics.assets.clear();
+ Some(PreparedRender::Semantic {
+ message: ServerMessage::PaneSurfacePatch(patch),
+ committed_surface: Box::new(committed_surface),
+ })
+ }
+
pub(crate) fn commit_sent_frame(&mut self, prepared: PreparedRender) {
match (self, prepared) {
(
- Self::Semantic { last_frame },
+ Self::Semantic {
+ last_surface,
+ surface_revision,
+ },
PreparedRender::Semantic {
- message: ServerMessage::Frame(frame),
+ committed_surface, ..
},
- ) => *last_frame = Some(frame),
+ ) => {
+ *surface_revision = committed_surface.surface_revision;
+ *last_surface = Some(committed_surface);
+ }
(
Self::TerminalAnsi {
blit_encoder,
@@ -145,14 +206,6 @@ impl ClientRenderState {
_ => {}
}
}
-
- #[cfg(test)]
- pub(crate) fn terminal_seq(&self) -> Option<u64> {
- match self {
- Self::Semantic { .. } => None,
- Self::TerminalAnsi { seq, .. } => Some(*seq),
- }
- }
}
fn insert_graphics_before_sync_end(encoded: &mut Vec<u8>, graphics: &[u8]) {
@@ -171,6 +224,7 @@ fn insert_graphics_before_sync_end(encoded: &mut Vec<u8>, graphics: &[u8]) {
pub(crate) enum PreparedRender {
Semantic {
message: ServerMessage,
+ committed_surface: Box<PaneSurfaceFrame>,
},
TerminalAnsi {
message: ServerMessage,
@@ -182,18 +236,23 @@ pub(crate) enum PreparedRender {
impl PreparedRender {
pub(crate) fn message(&self) -> &ServerMessage {
match self {
- Self::Semantic { message } | Self::TerminalAnsi { message, .. } => message,
+ Self::Semantic { message, .. } | Self::TerminalAnsi { message, .. } => message,
}
}
- pub(crate) fn into_frame(self) -> Option<FrameData> {
- match self {
- Self::Semantic {
- message: ServerMessage::Frame(frame),
- } => Some(frame),
- Self::TerminalAnsi { frame, .. } => Some(frame),
- _ => None,
+ pub(crate) fn strip_pane_surface_assets(&mut self) -> bool {
+ let Self::Semantic {
+ message: ServerMessage::PaneSurface(surface),
+ ..
+ } = self
+ else {
+ return false;
+ };
+ if surface.graphics.assets.is_empty() {
+ return false;
}
+ surface.graphics.assets.clear();
+ true
}
}
@@ -280,88 +339,44 @@ impl Backend for CursorTrackingBackend {
}
}
-/// Renders the AppState to an in-memory ratatui Buffer.
-///
-/// This produces the same output as the monolithic binary's terminal draw,
-/// but writes to a `Buffer` instead of stdout. Cursor visibility is captured
-/// from explicit frame cursor intent rather than incidental backend state.
-#[cfg_attr(not(test), allow(dead_code))]
-pub(crate) fn render_virtual(
- app_state: &mut AppState,
- area: Rect,
- resize_panes: bool,
-) -> (ratatui::buffer::Buffer, Option<CursorState>) {
- let terminal_runtimes = TerminalRuntimeRegistry::new();
- render_virtual_with_runtime_registry(
- app_state,
- &terminal_runtimes,
- area,
- resize_panes,
- crate::kitty_graphics::HostCellSize::default(),
- )
-}
+pub(crate) type RenderedTabSurface = (
+ ratatui::buffer::Buffer,
+ Option<CursorState>,
+ Vec<((u16, u16), String, String)>,
+ crate::ui::TabSurfaceLayout,
+);
-pub(crate) fn render_virtual_with_runtime_registry(
- app_state: &mut AppState,
+/// Renders only the active tab's pane surface at an origin-relative client viewport.
+pub(crate) fn render_tab_surface_virtual(
+ app_state: &AppState,
terminal_runtimes: &TerminalRuntimeRegistry,
area: Rect,
resize_panes: bool,
cell_size: crate::kitty_graphics::HostCellSize,
-) -> (ratatui::buffer::Buffer, Option<CursorState>) {
- let popup_visible = app_state.popup_pane.is_some();
- let pre_compute_suppresses_focused_terminal_cursor =
- !popup_visible && focused_terminal_suppresses_host_cursor(app_state, terminal_runtimes);
- if resize_panes {
- crate::ui::compute_view_with_cell_size(app_state, terminal_runtimes, area, cell_size);
- } else {
- crate::ui::compute_view_without_resizing_panes(app_state, terminal_runtimes, area);
- }
- let suppress_focused_terminal_cursor = pre_compute_suppresses_focused_terminal_cursor
- || (!popup_visible
- && focused_terminal_suppresses_host_cursor(app_state, terminal_runtimes));
+) -> RenderedTabSurface {
+ let layout =
+ crate::ui::compute_tab_surface(app_state, terminal_runtimes, area, resize_panes, cell_size);
+ let surface = crate::ui::TabSurfaceView {
+ pane_infos: &layout.pane_infos,
+ split_borders: &layout.split_borders,
+ };
+ let cursor = crate::ui::tab_surface_cursor(app_state, terminal_runtimes, surface);
+ let hyperlinks = crate::ui::tab_surface_hyperlinks(app_state, terminal_runtimes, surface);
let backend = CursorTrackingBackend::new(area.width, area.height);
let mut terminal = ratatui::Terminal::new(backend).expect("TestBackend::new should never fail");
-
terminal
.draw(|frame| {
- crate::ui::render_with_runtime_registry(app_state, terminal_runtimes, frame);
+ crate::ui::render_tab_surface(app_state, terminal_runtimes, surface, frame);
})
.expect("render to TestBackend should never fail");
- let buffer = terminal.backend().buffer().clone();
- let cursor = if popup_visible {
- popup_terminal_cursor(app_state, terminal_runtimes)
- } else if suppress_focused_terminal_cursor {
- None
- } else {
- focused_terminal_cursor(app_state, terminal_runtimes).or_else(|| {
- (!focused_terminal_owns_host_cursor(app_state, terminal_runtimes))
- .then(|| terminal.backend().rendered_cursor())
- .flatten()
- })
- };
-
- (buffer, cursor)
-}
-
-fn popup_terminal_cursor(
- app_state: &AppState,
- terminal_runtimes: &TerminalRuntimeRegistry,
-) -> Option<CursorState> {
- let popup = app_state.popup_pane.as_ref()?;
- let runtime = terminal_runtimes.get(&popup.terminal_id)?;
- if runtime.synchronized_output_active() {
- return None;
- }
- let (_, inner) = crate::ui::popup_pane_rects(app_state, app_state.view.terminal_area)?;
- let cursor = runtime.cursor_state(inner, true)?;
- Some(CursorState {
- x: cursor.x,
- y: cursor.y,
- visible: cursor.visible && !crate::ui::pane_is_scrolled_back(runtime),
- shape: cursor.shape,
- })
+ (
+ terminal.backend().buffer().clone(),
+ cursor,
+ hyperlinks,
+ layout,
+ )
}
/// Renders one server-owned terminal directly for `terminal attach` clients.
@@ -398,242 +413,66 @@ pub(crate) fn render_terminal_virtual(
(buffer, cursor)
}
-pub(crate) fn visible_hyperlinks(
- app_state: &AppState,
- terminal_runtimes: &TerminalRuntimeRegistry,
-) -> Vec<((u16, u16), String, String)> {
- crate::ui::tab_surface_hyperlinks(app_state, terminal_runtimes, app_state.view.tab_surface())
-}
-
-pub(crate) fn focused_terminal_cursor(
- app_state: &AppState,
- terminal_runtimes: &TerminalRuntimeRegistry,
-) -> Option<CursorState> {
- crate::ui::tab_surface_cursor(app_state, terminal_runtimes, app_state.view.tab_surface())
-}
-
-fn focused_terminal_owns_host_cursor(
- app_state: &AppState,
- terminal_runtimes: &TerminalRuntimeRegistry,
-) -> bool {
- if app_state.mode != Mode::Terminal {
- return false;
- }
-
- let Some(ws_idx) = app_state.active else {
- return false;
- };
- let Some(info) = app_state
- .view
- .pane_infos
- .iter()
- .find(|info| info.is_focused)
- else {
- return false;
- };
- if !app_state.pane_exposes_host_cursor(ws_idx, info.id) {
- return false;
- }
-
- app_state
- .runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, info.id)
- .is_some()
-}
-
-fn focused_terminal_suppresses_host_cursor(
- app_state: &AppState,
- terminal_runtimes: &TerminalRuntimeRegistry,
-) -> bool {
- if app_state.mode != Mode::Terminal {
- return false;
- }
-
- let Some(ws_idx) = app_state.active else {
- return false;
- };
- let Some(info) = app_state
- .view
- .pane_infos
- .iter()
- .find(|info| info.is_focused)
- else {
- return false;
- };
- if !app_state.pane_exposes_host_cursor(ws_idx, info.id) {
- return false;
- }
-
- app_state
- .runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, info.id)
- .is_some_and(crate::terminal::TerminalRuntime::synchronized_output_active)
-}
-
#[cfg(test)]
-mod render_scale_benchmark {
- use std::hint::black_box;
- use std::time::Instant;
-
- use ratatui::layout::Direction;
-
+mod tests {
use super::*;
- use crate::app::Mode;
- use crate::terminal::TerminalRuntime;
- use crate::workspace::Workspace;
-
- const AREA: Rect = Rect::new(0, 0, 120, 40);
- const SAMPLE_COUNT: usize = 40;
- const WARMUP_COUNT: usize = 5;
-
- #[derive(Clone, Copy)]
- struct RenderStats {
- median_us: u128,
- p95_us: u128,
- max_us: u128,
- }
-
- fn history() -> String {
- (0..2_000).map(|line| format!("line-{line}\r\n")).collect()
- }
-
- fn runtime(history: &str) -> TerminalRuntime {
- TerminalRuntime::test_with_scrollback_bytes(
- AREA.width,
- AREA.height,
- 1024 * 1024,
- history.as_bytes(),
- )
- }
-
- fn app_with_workspaces(workspace_count: usize) -> AppState {
- let history = history();
- let workspaces = (0..workspace_count)
- .map(|index| {
- let mut workspace = Workspace::test_new(&format!("bench-{}", index + 1));
- let root_pane = workspace.tabs[0].root_pane;
- workspace.tabs[0]
- .runtimes
- .insert(root_pane, runtime(&history));
- workspace
- })
- .collect();
- app_with(workspaces)
- }
-
- fn app_with_active_panes(pane_count: usize) -> AppState {
- let history = history();
- let mut workspace = Workspace::test_new("bench");
- let root_pane = workspace.tabs[0].root_pane;
- workspace.tabs[0]
- .runtimes
- .insert(root_pane, runtime(&history));
- let mut pane_ids = vec![root_pane];
-
- for index in 1..pane_count {
- let target = pane_ids[(index - 1) / 2];
- workspace.tabs[0].layout.focus_pane(target);
- let direction = if index % 2 == 0 {
- Direction::Vertical
- } else {
- Direction::Horizontal
- };
- let pane_id = workspace.test_split(direction);
- workspace.tabs[0]
- .runtimes
- .insert(pane_id, runtime(&history));
- pane_ids.push(pane_id);
- }
-
- app_with(vec![workspace])
- }
-
- fn app_with(workspaces: Vec<Workspace>) -> AppState {
- let mut app = AppState::test_new();
- app.mode = Mode::Terminal;
- app.pane_scrollbars = true;
- app.workspaces = workspaces;
- app.active = Some(0);
- app.selected = 0;
- app
- }
-
- fn profile(mut app: AppState) -> RenderStats {
- for _ in 0..WARMUP_COUNT {
- black_box(render_virtual(&mut app, AREA, true));
- }
-
- let mut samples = Vec::with_capacity(SAMPLE_COUNT);
- for _ in 0..SAMPLE_COUNT {
- let started = Instant::now();
- black_box(render_virtual(&mut app, AREA, true));
- samples.push(started.elapsed().as_micros());
- }
- samples.sort_unstable();
-
- RenderStats {
- median_us: samples[SAMPLE_COUNT / 2],
- p95_us: samples[(SAMPLE_COUNT - 1) * 95 / 100],
- max_us: samples[SAMPLE_COUNT - 1],
- }
- }
-
- fn profile_cardinalities(build: fn(usize) -> AppState) -> [(usize, RenderStats); 3] {
- [1, 15, 50].map(|count| (count, profile(build(count))))
- }
-
- fn print_profiles(label: &str, profiles: [(usize, RenderStats); 3]) {
- let baseline_median_us = profiles[0].1.median_us as f64;
- let baseline_p95_us = profiles[0].1.p95_us as f64;
- println!("{label}");
- println!(" count median_us p95_us max_us median_vs_1x p95_vs_1x");
- for (count, stats) in profiles {
- println!(
- "{count:>10} {:>9} {:>6} {:>6} {:>12.2} {:>9.2}",
- stats.median_us,
- stats.p95_us,
- stats.max_us,
- stats.median_us as f64 / baseline_median_us,
- stats.p95_us as f64 / baseline_p95_us,
- );
+ use crate::protocol::ClientShellPopupSurface;
+
+ fn popup_surface(content: &str) -> PaneSurfaceFrame {
+ let pane = ratatui::buffer::Buffer::with_lines(["pane"]);
+ let popup = ratatui::buffer::Buffer::with_lines([content]);
+ PaneSurfaceFrame {
+ boot_id: "boot-1".into(),
+ projection_revision: 1,
+ surface_revision: 1,
+ frame: FrameData::from_ratatui_buffer_with_hyperlinks(&pane, None, &[]),
+ panes: Vec::new(),
+ splits: Vec::new(),
+ popup: Some(Box::new(ClientShellPopupSurface {
+ terminal_id: "popup-terminal".into(),
+ title: "popup".into(),
+ width: None,
+ height: None,
+ frame: FrameData::from_ratatui_buffer_with_hyperlinks(&popup, None, &[]),
+ mouse_reporting: false,
+ sgr_pixel_mouse: false,
+ pixel_width: 0,
+ pixel_height: 0,
+ })),
+ graphics: crate::protocol::SurfaceGraphicsScene::default(),
}
}
- fn assert_full_render_avoids_aggregate_input_state(mut app: AppState, scenario: &str) {
- crate::pane::reset_aggregate_input_state_reads();
- black_box(render_virtual(&mut app, AREA, true));
- assert_eq!(
- crate::pane::aggregate_input_state_reads(),
- 0,
- "full render collected aggregate input state for {scenario}",
- );
- }
-
- #[tokio::test(flavor = "current_thread")]
- async fn aggregate_input_state_counter_records_reads() {
- let runtime = TerminalRuntime::test_with_screen_bytes(80, 24, b"");
- crate::pane::reset_aggregate_input_state_reads();
- black_box(runtime.input_state());
- assert_eq!(crate::pane::aggregate_input_state_reads(), 1);
- }
-
- #[tokio::test(flavor = "current_thread")]
- async fn full_render_avoids_aggregate_input_state_reads() {
- assert_full_render_avoids_aggregate_input_state(
- app_with_workspaces(15),
- "background workspaces",
- );
- assert_full_render_avoids_aggregate_input_state(app_with_active_panes(15), "active panes");
- }
-
- #[tokio::test(flavor = "current_thread")]
- #[ignore = "manual full-render scaling profile"]
- async fn render_scale_profile() {
- print_profiles(
- "background-workspace resize/layout (one pane each)",
- profile_cardinalities(app_with_workspaces),
- );
- print_profiles(
- "active panes (one workspace)",
- profile_cardinalities(app_with_active_panes),
- );
+ #[test]
+ fn popup_only_surface_changes_are_not_deduplicated() {
+ let mut state = ClientRenderState::new(RenderEncoding::SemanticFrame);
+ let prepared = state
+ .prepare_pane_surface(popup_surface("first"))
+ .expect("initial surface");
+ state.commit_sent_frame(prepared);
+
+ assert!(state
+ .prepare_pane_surface(popup_surface("second"))
+ .is_some());
+ }
+
+ #[test]
+ fn forced_full_surface_keeps_the_connection_revision_monotonic() {
+ let mut state = ClientRenderState::new(RenderEncoding::SemanticFrame);
+ let prepared = state
+ .prepare_pane_surface(popup_surface("first"))
+ .expect("initial surface");
+ state.commit_sent_frame(prepared);
+ state.request_repaint();
+
+ let prepared = state
+ .prepare_pane_surface(popup_surface("replacement"))
+ .expect("forced replacement surface");
+ assert!(matches!(
+ prepared.message(),
+ ServerMessage::PaneSurface(surface) if surface.surface_revision == 2
+ ));
+ state.commit_sent_frame(prepared);
+ assert_eq!(state.last_pane_surface().unwrap().surface_revision, 2);
}
}