1use blit_alacritty::{SearchResult as AlacrittySearchResult, TerminalDriver as AlacrittyDriver};
2use blit_compositor::{CompositorCommand, CompositorEvent, CompositorHandle};
3use blit_remote::{
4 C2S_ACK, C2S_AUDIO_SUBSCRIBE, C2S_AUDIO_UNSUBSCRIBE, C2S_CLIENT_FEATURES, C2S_CLIENT_METRICS,
5 C2S_CLIPBOARD_GET, C2S_CLIPBOARD_LIST, C2S_CLIPBOARD_SET, C2S_CLOSE, C2S_COPY_RANGE,
6 C2S_CREATE, C2S_CREATE_AT, C2S_CREATE_N, C2S_CREATE2, C2S_DISPLAY_RATE, C2S_FOCUS, C2S_INPUT,
7 C2S_KILL, C2S_MOUSE, C2S_PING, C2S_QUIT, C2S_READ, C2S_RESIZE, C2S_RESTART, C2S_SCROLL,
8 C2S_SEARCH, C2S_SUBSCRIBE, C2S_SURFACE_ACK, C2S_SURFACE_CAPTURE, C2S_SURFACE_CLOSE,
9 C2S_SURFACE_FOCUS, C2S_SURFACE_INPUT, C2S_SURFACE_LIST, C2S_SURFACE_POINTER,
10 C2S_SURFACE_POINTER_AXIS, C2S_SURFACE_RESIZE, C2S_SURFACE_SUBSCRIBE, C2S_SURFACE_TEXT,
11 C2S_SURFACE_UNSUBSCRIBE, C2S_UNSUBSCRIBE, CAPTURE_FORMAT_AVIF, CAPTURE_FORMAT_PNG,
12 CREATE2_HAS_COMMAND, CREATE2_HAS_SRC_PTY, FEATURE_AUDIO, FEATURE_COMPOSITOR,
13 FEATURE_COPY_RANGE, FEATURE_CREATE_NONCE, FEATURE_RESIZE_BATCH, FEATURE_RESTART, FrameState,
14 READ_ANSI, READ_TAIL, S2C_CLOSED, S2C_CREATED, S2C_CREATED_N, S2C_LIST, S2C_PING, S2C_QUIT,
15 S2C_READY, S2C_SEARCH_RESULTS, S2C_SURFACE_CAPTURE, S2C_SURFACE_LIST, S2C_TEXT, S2C_TITLE,
16 SURFACE_FRAME_FLAG_KEYFRAME, build_update_msg, msg_hello, msg_s2c_clipboard_content,
17 msg_s2c_clipboard_list, msg_surface_app_id, msg_surface_created, msg_surface_destroyed,
18 msg_surface_encoder, msg_surface_frame, msg_surface_resized, msg_surface_title,
19};
20use std::collections::{HashMap, HashSet, VecDeque};
21use std::sync::Arc;
22use std::sync::atomic::{AtomicUsize, Ordering};
23use std::time::{Duration, Instant};
24use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
25use tokio::sync::{Mutex, Notify, mpsc};
26
27#[cfg(unix)]
28mod audio;
29mod gpu_libs;
30mod ipc;
31mod nvenc_encode;
32mod pty;
33mod surface_encoder;
34#[cfg(target_os = "linux")]
35mod vaapi_encode;
36
37pub use ipc::{IpcListener, default_ipc_path};
38use pty::{PtyHandle, PtyWriteTarget};
39use surface_encoder::SurfaceEncoder;
40pub use surface_encoder::SurfaceEncoderPreference;
41pub use surface_encoder::SurfaceH264EncoderPreference;
42pub use surface_encoder::SurfaceQuality;
43
44type PtyFds = Arc<std::sync::RwLock<HashMap<u16, PtyWriteTarget>>>;
45pub struct Config {
46 pub shell: String,
47 pub shell_flags: String,
48 pub scrollback: usize,
49 pub ipc_path: String,
50 pub surface_encoders: Vec<SurfaceEncoderPreference>,
51 pub surface_quality: SurfaceQuality,
52 pub vaapi_device: String,
53 #[cfg(unix)]
54 pub fd_channel: Option<std::os::unix::io::RawFd>,
55 pub verbose: bool,
56 pub max_connections: usize,
58 pub max_ptys: usize,
60 pub ping_interval: Duration,
64 pub skip_compositor: bool,
66}
67
68trait PtyDriver: Send {
69 fn size(&self) -> (u16, u16);
70 fn resize(&mut self, rows: u16, cols: u16);
71 fn process(&mut self, data: &[u8]);
72 fn title(&self) -> &str;
73 fn search_result(&self, query: &str) -> Option<PtySearchResult>;
74 fn take_title_dirty(&mut self) -> bool;
75 fn cursor_position(&self) -> (u16, u16);
76 fn synced_output(&self) -> bool;
77 fn snapshot(&mut self, echo: bool, icanon: bool) -> FrameState;
78 fn scrollback_frame(&mut self, offset: usize) -> FrameState;
79 fn reset_modes(&mut self);
80 fn mouse_event(
81 &self,
82 type_: u8,
83 button: u8,
84 col: u16,
85 row: u16,
86 echo: bool,
87 icanon: bool,
88 ) -> Option<Vec<u8>>;
89 fn get_text_range(
90 &self,
91 start_tail: u32,
92 start_col: u16,
93 end_tail: u32,
94 end_col: u16,
95 ) -> String;
96 fn total_lines(&self) -> u32;
97}
98
99struct PtySearchResult {
100 score: u32,
101 primary_source: u8,
102 matched_sources: u8,
103 context: String,
104 scroll_offset: Option<usize>,
105}
106
107impl PtyDriver for AlacrittyDriver {
108 fn size(&self) -> (u16, u16) {
109 AlacrittyDriver::size(self)
110 }
111
112 fn resize(&mut self, rows: u16, cols: u16) {
113 AlacrittyDriver::resize(self, rows, cols);
114 }
115
116 fn process(&mut self, data: &[u8]) {
117 AlacrittyDriver::process(self, data);
118 }
119
120 fn title(&self) -> &str {
121 AlacrittyDriver::title(self)
122 }
123
124 fn search_result(&self, query: &str) -> Option<PtySearchResult> {
125 AlacrittyDriver::search_result(self, query).map(|result: AlacrittySearchResult| {
126 PtySearchResult {
127 score: result.score,
128 primary_source: result.primary_source as u8,
129 matched_sources: result.matched_sources,
130 context: result.context,
131 scroll_offset: result.scroll_offset,
132 }
133 })
134 }
135
136 fn take_title_dirty(&mut self) -> bool {
137 AlacrittyDriver::take_title_dirty(self)
138 }
139
140 fn cursor_position(&self) -> (u16, u16) {
141 AlacrittyDriver::cursor_position(self)
142 }
143
144 fn synced_output(&self) -> bool {
145 AlacrittyDriver::synced_output(self)
146 }
147
148 fn snapshot(&mut self, echo: bool, icanon: bool) -> FrameState {
149 AlacrittyDriver::snapshot(self, echo, icanon)
150 }
151
152 fn scrollback_frame(&mut self, offset: usize) -> FrameState {
153 AlacrittyDriver::scrollback_frame(self, offset)
154 }
155
156 fn reset_modes(&mut self) {
157 AlacrittyDriver::reset_modes(self);
158 }
159
160 fn mouse_event(
161 &self,
162 type_: u8,
163 button: u8,
164 col: u16,
165 row: u16,
166 echo: bool,
167 icanon: bool,
168 ) -> Option<Vec<u8>> {
169 AlacrittyDriver::mouse_event(self, type_, button, col, row, echo, icanon)
170 }
171
172 fn get_text_range(
173 &self,
174 start_tail: u32,
175 start_col: u16,
176 end_tail: u32,
177 end_col: u16,
178 ) -> String {
179 AlacrittyDriver::get_text_range(self, start_tail, start_col, end_tail, end_col)
180 }
181
182 fn total_lines(&self) -> u32 {
183 AlacrittyDriver::total_lines(self)
184 }
185}
186
187const OUTBOX_CAPACITY: usize = 8;
191const OUTBOX_SOFT_QUEUE_LIMIT_FRAMES: usize = 4;
192const OUTBOX_SOFT_QUEUE_LIMIT_BYTES: usize = 128 * 1024;
193const PREVIEW_FRAME_RESERVE: usize = 1;
194const READY_FRAME_QUEUE_CAP: usize = 4;
195const PTY_CHANNEL_CAPACITY: usize = 64;
196const SYNC_OUTPUT_END: &[u8] = b"\x1b[?2026l";
197
198const SURFACE_BURST_FRAMES: u8 = 4;
204
205enum PtyInput {
208 Data(Vec<u8>),
211 SyncBoundary { before: Vec<u8>, after: Vec<u8> },
214 Eof,
216}
217
218const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024;
219
220async fn read_frame(reader: &mut (impl AsyncRead + Unpin)) -> Option<Vec<u8>> {
221 let mut len_buf = [0u8; 4];
222 reader.read_exact(&mut len_buf).await.ok()?;
223 let len = u32::from_le_bytes(len_buf) as usize;
224 if len == 0 {
225 return Some(vec![]);
226 }
227 if len > MAX_FRAME_SIZE {
228 return None;
229 }
230 let mut buf = vec![0u8; len];
231 reader.read_exact(&mut buf).await.ok()?;
232 Some(buf)
233}
234
235async fn write_frame(writer: &mut (impl AsyncWrite + Unpin), payload: &[u8]) -> bool {
236 if payload.len() > u32::MAX as usize {
237 return false;
238 }
239 let len = payload.len() as u32;
240 let mut buf = Vec::with_capacity(4 + payload.len());
241 buf.extend_from_slice(&len.to_le_bytes());
242 buf.extend_from_slice(payload);
243 writer.write_all(&buf).await.is_ok()
244}
245
246async fn write_frame_interleaved(
257 writer: &mut (impl AsyncWrite + Unpin),
258 payload: &[u8],
259 audio_rx: &mut mpsc::Receiver<Vec<u8>>,
260) -> bool {
261 while let Ok(audio_msg) = audio_rx.try_recv() {
265 if !write_frame(writer, &audio_msg).await {
266 return false;
267 }
268 }
269 write_frame(writer, payload).await
270}
271
272struct Pty {
273 handle: PtyHandle,
274 driver: Box<dyn PtyDriver>,
275 tag: String,
277 dirty: bool,
278 ready_frames: VecDeque<FrameState>,
279 byte_rx: mpsc::Receiver<PtyInput>,
281 reader_handle: std::thread::JoinHandle<()>,
282 lflag_cache: (bool, bool),
284 lflag_last: Instant,
285 last_title_send: Instant,
287 title_pending: bool,
289 exited: bool,
291 exit_status: i32,
294 command: Option<String>,
296}
297
298impl Pty {
299 fn mark_dirty(&mut self) {
300 self.dirty = true;
301 }
302
303 fn clear_dirty(&mut self) {
304 self.dirty = false;
305 }
306}
307
308struct CachedSurfaceInfo {
309 surface_id: u16,
310 parent_id: u16,
311 width: u16,
312 height: u16,
313 title: String,
314 app_id: String,
315}
316
317struct LastPixels {
320 width: u32,
321 height: u32,
322 pixels: blit_compositor::PixelData,
323 generation: u64,
326}
327
328struct SharedCompositor {
329 handle: CompositorHandle,
330 surfaces: HashMap<u16, CachedSurfaceInfo>,
331 last_pixels: HashMap<u16, LastPixels>,
332 last_frame_request: HashMap<u16, Instant>,
337 created_at: Instant,
338 pixel_generation: u64,
340 last_blanket_frame_request: Instant,
344 last_configured_size: HashMap<u16, (u16, u16, u16)>,
350 #[cfg(unix)]
353 audio_pipeline: Option<audio::AudioPipeline>,
354 #[cfg(unix)]
357 audio_session_id: u16,
358 #[cfg(unix)]
361 last_audio_restart: Option<Instant>,
362}
363
364fn encode_rgba_to_png(pixels: &[u8], width: u32, height: u32) -> Vec<u8> {
365 let mut buf = Vec::new();
366 {
367 let expected = (width * height * 4) as usize;
368 let actual = pixels.len();
369 if actual != expected {
370 let mut encoder = png::Encoder::new(&mut buf, 1, 1);
372 encoder.set_color(png::ColorType::Rgba);
373 encoder.set_depth(png::BitDepth::Eight);
374 let mut writer = encoder.write_header().unwrap();
375 writer.write_image_data(&[255, 0, 0, 255]).unwrap();
376 eprintln!(
377 "[capture] pixel buffer size mismatch: {width}x{height} expected {expected} got {actual}"
378 );
379 } else {
380 let mut encoder = png::Encoder::new(&mut buf, width, height);
381 encoder.set_color(png::ColorType::Rgba);
382 encoder.set_depth(png::BitDepth::Eight);
383 let mut writer = encoder.write_header().unwrap();
384 writer.write_image_data(pixels).unwrap();
385 }
386 }
387 buf
388}
389
390fn encode_rgba_to_avif(pixels: &[u8], width: u32, height: u32, quality: u8) -> Vec<u8> {
392 let rgba: Vec<rgb::RGBA8> = pixels
393 .chunks_exact(4)
394 .map(|c| rgb::RGBA8::new(c[0], c[1], c[2], c[3]))
395 .collect();
396 let img = ravif::Img::new(&rgba[..], width as usize, height as usize);
397 let q = if quality == 0 { 100.0 } else { quality as f32 };
398 let encoder = ravif::Encoder::new()
399 .with_quality(q)
400 .with_alpha_quality(q)
401 .with_speed(6)
402 .with_alpha_color_mode(ravif::AlphaColorMode::UnassociatedClean)
403 .with_num_threads(None);
404 let result = encoder.encode_rgba(img).expect("AVIF encoding failed");
405 result.avif_file
406}
407
408fn encode_capture(pixels: &[u8], width: u32, height: u32, format: u8, quality: u8) -> Vec<u8> {
410 match format {
411 CAPTURE_FORMAT_AVIF => encode_rgba_to_avif(pixels, width, height, quality),
412 _ => encode_rgba_to_png(pixels, width, height),
413 }
414}
415
416async fn request_surface_capture_with_timeout(
417 command_tx: std::sync::mpsc::Sender<CompositorCommand>,
418 surface_id: u16,
419 scale_120: u16,
420 timeout: Duration,
421) -> Option<(u32, u32, Vec<u8>)> {
422 let (tx, rx) = std::sync::mpsc::sync_channel(1);
423 command_tx
424 .send(CompositorCommand::Capture {
425 surface_id,
426 scale_120,
427 reply: tx,
428 })
429 .ok()?;
430
431 tokio::task::spawn_blocking(move || rx.recv_timeout(timeout))
435 .await
436 .ok()?
437 .ok()
438 .flatten()
439}
440
441const AUDIO_OUTBOX_CAPACITY: usize = 10; struct ClientState {
447 tx: mpsc::Sender<Vec<u8>>,
448 outbox_queued_frames: Arc<AtomicUsize>,
449 outbox_queued_bytes: Arc<AtomicUsize>,
450 audio_tx: mpsc::Sender<Vec<u8>>,
454 lead: Option<u16>,
455 subscriptions: HashSet<u16>,
456 surface_subscriptions: HashSet<u16>,
457 audio_subscribed: bool,
459 #[cfg(unix)]
462 audio_bitrate_kbps: u16,
463 view_sizes: HashMap<u16, (u16, u16)>,
464 scroll_offsets: HashMap<u16, usize>,
465 scroll_caches: HashMap<u16, FrameState>,
466 last_sent: HashMap<u16, FrameState>,
467 preview_next_send_at: HashMap<u16, Instant>,
468 rtt_ms: f32,
470 min_rtt_ms: f32,
472 display_fps: f32,
474 delivery_bps: f32,
476 goodput_bps: f32,
478 goodput_jitter_bps: f32,
480 max_goodput_jitter_bps: f32,
482 last_goodput_sample_bps: f32,
484 avg_frame_bytes: f32,
486 avg_paced_frame_bytes: f32,
488 avg_preview_frame_bytes: f32,
490 avg_surface_frame_bytes: f32,
495 inflight_bytes: usize,
497 inflight_frames: VecDeque<InFlightFrame>,
499 next_send_at: Instant,
501 probe_frames: f32,
504 frames_sent: u32,
506 acks_recv: u32,
507 acked_bytes_since_log: usize,
508 browser_backlog_frames: u16,
509 browser_ack_ahead_frames: u16,
510 browser_apply_ms: f32,
511 last_metrics_update: Instant,
512 last_log: Instant,
513 goodput_window_bytes: usize,
514 goodput_window_start: Instant,
515 surface_next_send_at: Instant,
516 surface_needs_keyframe: bool,
517 surface_burst_remaining: u8,
523 surface_encoders: HashMap<u16, SurfaceEncoder>,
525 surface_inflight_frames: VecDeque<InFlightFrame>,
529 surface_encodes_in_flight: HashSet<u16>,
534 surface_last_encoded_gen: HashMap<u16, u64>,
537 surface_view_sizes: HashMap<u16, (u16, u16, u16)>,
542 surface_codec_support: u8,
545 surface_codec_overrides: HashMap<u16, u8>,
548 surface_quality_overrides: HashMap<u16, SurfaceQuality>,
551 pressed_surface_keys: HashSet<u32>,
555}
556
557struct InFlightFrame {
558 sent_at: Instant,
559 bytes: usize,
560 paced: bool,
561}
562
563fn frame_window(rtt_ms: f32, display_fps: f32) -> usize {
567 let frame_ms = 1_000.0 / display_fps.max(1.0);
568 let base_frames = (rtt_ms / frame_ms).ceil().max(0.0) as usize;
569 let slack_frames = ((base_frames as f32) * 0.125).ceil() as usize + 2;
570 base_frames.saturating_add(slack_frames).max(2)
571}
572
573fn path_rtt_ms(client: &ClientState) -> f32 {
574 if client.min_rtt_ms > 0.0 {
575 client.min_rtt_ms
576 } else {
577 client.rtt_ms
578 }
579}
580
581fn display_need_bps(client: &ClientState) -> f32 {
582 client.avg_paced_frame_bytes.max(256.0) * client.display_fps.max(1.0)
583}
584
585fn effective_rtt_ms(client: &ClientState) -> f32 {
586 let path_rtt = path_rtt_ms(client);
587 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
588 let queue_allowance = frame_ms
589 * if throughput_limited(client) {
590 4.0
591 } else {
592 12.0
593 };
594 client.rtt_ms.clamp(path_rtt, path_rtt + queue_allowance)
595}
596
597fn window_rtt_ms(client: &ClientState) -> f32 {
598 let effective = effective_rtt_ms(client);
599 if !throughput_limited(client) {
600 effective
601 } else {
602 client.rtt_ms.clamp(effective, effective * 2.0)
603 }
604}
605
606fn target_frame_window(client: &ClientState) -> usize {
607 let window_fps = if throughput_limited(client) {
608 pacing_fps(client)
609 } else {
610 browser_pacing_fps(client)
611 };
612 frame_window(window_rtt_ms(client), window_fps)
613 .saturating_add(client.probe_frames.round().max(0.0) as usize)
614}
615
616fn base_queue_ms(client: &ClientState) -> f32 {
617 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
618 frame_ms * if throughput_limited(client) { 2.0 } else { 8.0 }
619}
620
621fn target_queue_ms(client: &ClientState) -> f32 {
622 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
623 let probe_scale = if throughput_limited(client) {
624 0.25
625 } else {
626 1.0
627 };
628 base_queue_ms(client) + client.probe_frames.max(0.0) * frame_ms * probe_scale
629}
630
631fn browser_ready(client: &ClientState) -> bool {
632 client.browser_ack_ahead_frames <= 1
633 && client.browser_apply_ms <= 1.0
634 && !outbox_backpressured(client)
635}
636
637fn bandwidth_floor_bps(client: &ClientState) -> f32 {
638 let browser_ready = browser_ready(client);
639 let backlog_scale = match client.browser_backlog_frames {
640 0..=2 => 0.9,
641 3..=8 => 0.8,
642 _ => 0.65,
643 };
644 let penalty = client
645 .goodput_jitter_bps
646 .max(client.max_goodput_jitter_bps * 0.5)
647 .min(client.goodput_bps * if browser_ready { 0.75 } else { 0.9 });
648 let goodput_floor = (client.goodput_bps - penalty)
649 .max(client.goodput_bps * if browser_ready { 0.35 } else { 0.2 });
650 let delivery_floor = client.delivery_bps * if browser_ready { 1.0 } else { 0.5 };
654 let recent_sample_floor = if browser_ready && client.last_goodput_sample_bps > 0.0 {
655 client.last_goodput_sample_bps * backlog_scale
656 } else {
657 0.0
658 };
659 goodput_floor.max(recent_sample_floor).max(delivery_floor)
660}
661
662fn pacing_fps(client: &ClientState) -> f32 {
663 let frame_bytes = client.avg_paced_frame_bytes.max(256.0);
664 let sustainable = bandwidth_floor_bps(client) / frame_bytes;
665 sustainable.min(browser_pacing_fps(client))
666}
667
668fn throughput_limited(client: &ClientState) -> bool {
669 let floor = bandwidth_floor_bps(client);
670 let lead_bps = client.avg_paced_frame_bytes.max(256.0) * browser_pacing_fps(client);
674 let preview_bps = client.avg_preview_frame_bytes.max(256.0) * client.display_fps.max(1.0);
675 (lead_bps + preview_bps) > floor * 0.9
676}
677
678fn browser_pacing_fps(client: &ClientState) -> f32 {
679 let mut fps = client.display_fps.max(1.0);
680
681 let backlog = client.browser_backlog_frames as f32;
685 if backlog > 4.0 {
686 fps = fps.min(fps * (4.0 / backlog));
687 }
688
689 if client.browser_ack_ahead_frames > 4 {
690 fps = fps.min(client.display_fps.max(1.0) * 0.5);
691 }
692
693 fps.max(1.0)
694}
695
696fn browser_backlog_blocked(client: &ClientState) -> bool {
697 client.browser_backlog_frames > 8
698}
699
700fn byte_budget_for(client: &ClientState, budget_ms: f32) -> usize {
701 let budget_bps = if throughput_limited(client) {
702 bandwidth_floor_bps(client)
703 } else {
704 client.goodput_bps.max(bandwidth_floor_bps(client))
705 };
706 let bytes = budget_bps * budget_ms.max(1.0) / 1_000.0;
707 bytes.ceil().max(client.avg_frame_bytes.max(256.0)) as usize
708}
709
710fn target_byte_window(client: &ClientState) -> usize {
711 let budget = byte_budget_for(client, path_rtt_ms(client) + target_queue_ms(client));
712 let frame_bytes = client.avg_paced_frame_bytes.max(256.0).ceil() as usize;
713 let target_frames = target_frame_window(client);
714 let pipeline_bytes = frame_bytes.saturating_mul(target_frames);
715 const PIPELINE_FLOOR_LIMIT: usize = 32_768; let floor = if pipeline_bytes <= PIPELINE_FLOOR_LIMIT {
722 pipeline_bytes
723 } else {
724 frame_bytes };
726 budget.max(floor)
727}
728
729fn send_interval(client: &ClientState) -> Duration {
730 Duration::from_secs_f64(1.0 / browser_pacing_fps(client).max(1.0) as f64)
731}
732
733fn preview_fps(client: &ClientState) -> f32 {
734 let mut fps = client.display_fps.max(1.0);
735 if client.lead.is_some() && throughput_limited(client) {
736 let avail = bandwidth_floor_bps(client);
741 let lead_bps = client.avg_paced_frame_bytes.max(256.0) * browser_pacing_fps(client);
742 let preview_budget = (avail - lead_bps).max(avail * 0.25).max(0.0);
743 let bw_cap = preview_budget / client.avg_preview_frame_bytes.max(256.0);
744 fps = fps.min(bw_cap.max(1.0));
745 }
746 fps.max(1.0)
747}
748
749fn preview_send_interval(client: &ClientState) -> Duration {
750 Duration::from_secs_f64(1.0 / preview_fps(client) as f64)
751}
752
753fn surface_pacing_fps(client: &ClientState) -> f32 {
764 let frame_bytes = client.avg_surface_frame_bytes.max(256.0);
765 let bw = client.goodput_bps.max(client.delivery_bps);
766 let sustainable = bw / frame_bytes;
767 sustainable.min(client.display_fps).max(1.0)
768}
769
770fn surface_send_interval(client: &ClientState) -> Duration {
771 Duration::from_secs_f64(1.0 / surface_pacing_fps(client).max(1.0) as f64)
772}
773
774fn advance_deadline(deadline: &mut Instant, now: Instant, interval: Duration) {
775 let scheduled = deadline.checked_add(interval).unwrap_or(now + interval);
776 *deadline = if scheduled + interval < now {
777 now + interval
778 } else {
779 scheduled
780 };
781}
782
783fn should_snapshot_pty(dirty: bool, needful: bool, synced_output: bool) -> bool {
784 dirty && needful && !synced_output
785}
786
787fn enqueue_ready_frame(queue: &mut VecDeque<FrameState>, frame: FrameState) -> bool {
788 if queue.len() >= READY_FRAME_QUEUE_CAP {
789 return false;
790 }
791 queue.push_back(frame);
792 true
793}
794
795fn pty_has_visual_update(pty: &Pty) -> bool {
796 pty.dirty || !pty.ready_frames.is_empty() || !pty.byte_rx.is_empty()
797}
798
799fn find_sync_output_end(prefix: &[u8], bytes: &[u8]) -> Option<usize> {
803 if bytes.is_empty() {
804 return None;
805 }
806 let needle = SYNC_OUTPUT_END;
807 let nlen = needle.len();
808
809 if !prefix.is_empty() {
811 let tail = if prefix.len() >= nlen - 1 {
812 &prefix[prefix.len() - (nlen - 1)..]
813 } else {
814 prefix
815 };
816 let combined_len = tail.len() + bytes.len().min(nlen);
817 if combined_len >= nlen {
818 let mut buf = [0u8; 32]; let blen = combined_len.min(buf.len());
821 let tlen = tail.len().min(blen);
822 buf[..tlen].copy_from_slice(&tail[..tlen]);
823 let rest = (blen - tlen).min(bytes.len());
824 buf[tlen..tlen + rest].copy_from_slice(&bytes[..rest]);
825 for i in 0..=(blen.saturating_sub(nlen)) {
826 if &buf[i..i + nlen] == needle {
827 let end_in_bytes = (i + nlen).saturating_sub(tail.len());
828 if end_in_bytes > 0 && end_in_bytes <= bytes.len() {
829 return Some(end_in_bytes);
830 }
831 }
832 }
833 }
834 }
835
836 let mut offset = 0;
838 while let Some(pos) = memchr::memchr(0x1b, &bytes[offset..]) {
839 let abs = offset + pos;
840 if abs + nlen <= bytes.len() && &bytes[abs..abs + nlen] == needle {
841 return Some(abs + nlen);
842 }
843 offset = abs + 1;
844 }
845 None
846}
847
848fn update_sync_scan_tail(tail: &mut Vec<u8>, bytes: &[u8]) {
849 if bytes.is_empty() {
850 return;
851 }
852 tail.extend_from_slice(bytes);
853 let keep = SYNC_OUTPUT_END.len().saturating_sub(1);
854 if tail.len() > keep {
855 let drop = tail.len() - keep;
856 tail.drain(..drop);
857 }
858}
859
860fn preview_deadline(client: &ClientState, pid: u16, now: Instant) -> Instant {
861 client
862 .preview_next_send_at
863 .get(&pid)
864 .copied()
865 .unwrap_or(now)
866}
867
868fn client_has_due_preview(sess: &Session, client: &ClientState, now: Instant) -> bool {
869 if client.lead.is_none() {
870 return false;
871 }
872 client.subscriptions.iter().copied().any(|pid| {
873 Some(pid) != client.lead
874 && preview_deadline(client, pid, now) <= now
875 && sess
876 .ptys
877 .get(&pid)
878 .map(pty_has_visual_update)
879 .unwrap_or(false)
880 })
881}
882
883fn outbox_queued_frames(client: &ClientState) -> usize {
884 client.outbox_queued_frames.load(Ordering::Relaxed)
885}
886
887fn outbox_queued_bytes(client: &ClientState) -> usize {
888 client.outbox_queued_bytes.load(Ordering::Relaxed)
889}
890
891fn outbox_backpressured(client: &ClientState) -> bool {
892 outbox_queued_frames(client) >= OUTBOX_SOFT_QUEUE_LIMIT_FRAMES
893 || outbox_queued_bytes(client) >= OUTBOX_SOFT_QUEUE_LIMIT_BYTES
894}
895
896fn mark_outbox_drained(
897 queued_frames: &Arc<AtomicUsize>,
898 queued_bytes: &Arc<AtomicUsize>,
899 bytes: usize,
900) {
901 let _ = queued_frames.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
902 Some(value.saturating_sub(1))
903 });
904 let _ = queued_bytes.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
905 Some(value.saturating_sub(bytes))
906 });
907}
908
909fn try_send_outbox_tracked(
910 tx: &mpsc::Sender<Vec<u8>>,
911 queued_frames: &Arc<AtomicUsize>,
912 queued_bytes: &Arc<AtomicUsize>,
913 msg: Vec<u8>,
914) -> Result<(), mpsc::error::TrySendError<Vec<u8>>> {
915 let bytes = msg.len();
916 match tx.try_send(msg) {
917 Ok(()) => {
918 queued_frames.fetch_add(1, Ordering::Relaxed);
919 queued_bytes.fetch_add(bytes, Ordering::Relaxed);
920 Ok(())
921 }
922 Err(err) => Err(err),
923 }
924}
925
926fn try_send_outbox(
927 client: &ClientState,
928 msg: Vec<u8>,
929) -> Result<(), mpsc::error::TrySendError<Vec<u8>>> {
930 try_send_outbox_tracked(
931 &client.tx,
932 &client.outbox_queued_frames,
933 &client.outbox_queued_bytes,
934 msg,
935 )
936}
937
938async fn send_outbox_tracked(
939 tx: &mpsc::Sender<Vec<u8>>,
940 queued_frames: &Arc<AtomicUsize>,
941 queued_bytes: &Arc<AtomicUsize>,
942 msg: Vec<u8>,
943) -> Result<(), mpsc::error::SendError<Vec<u8>>> {
944 let bytes = msg.len();
945 tx.send(msg).await?;
946 queued_frames.fetch_add(1, Ordering::Relaxed);
947 queued_bytes.fetch_add(bytes, Ordering::Relaxed);
948 Ok(())
949}
950
951fn can_send_preview(client: &ClientState, pid: u16, now: Instant) -> bool {
952 window_open(client) && now >= preview_deadline(client, pid, now)
953}
954
955fn record_preview_send(client: &mut ClientState, pid: u16, now: Instant) {
956 let mut deadline = client
957 .preview_next_send_at
958 .get(&pid)
959 .copied()
960 .unwrap_or(now);
961 advance_deadline(&mut deadline, now, preview_send_interval(client));
962 client.preview_next_send_at.insert(pid, deadline);
963}
964
965fn window_open(client: &ClientState) -> bool {
966 !browser_backlog_blocked(client)
967 && !outbox_backpressured(client)
968 && client.inflight_frames.len() < target_frame_window(client)
969 && client.inflight_bytes < target_byte_window(client)
970}
971
972fn surface_window_open(client: &ClientState) -> bool {
982 !outbox_backpressured(client)
983}
984
985fn lead_window_open(client: &ClientState, reserve_preview_slot: bool) -> bool {
986 if !reserve_preview_slot || client.lead.is_none() {
987 return window_open(client);
988 }
989 if browser_backlog_blocked(client) || outbox_backpressured(client) {
990 return false;
991 }
992 let target_frames = target_frame_window(client);
993 let reserve_frames = PREVIEW_FRAME_RESERVE.min(target_frames.saturating_sub(1));
994 let frame_limit = target_frames.saturating_sub(reserve_frames).max(1);
995 let reserve_bytes = client.avg_preview_frame_bytes.max(256.0).ceil() as usize;
996 let byte_limit = target_byte_window(client)
997 .saturating_sub(reserve_bytes)
998 .max(client.avg_paced_frame_bytes.max(256.0).ceil() as usize);
999 client.inflight_frames.len() < frame_limit && client.inflight_bytes < byte_limit
1000}
1001
1002fn can_send_frame(client: &ClientState, now: Instant, reserve_preview_slot: bool) -> bool {
1003 lead_window_open(client, reserve_preview_slot) && now >= client.next_send_at
1004}
1005
1006fn record_send(client: &mut ClientState, bytes: usize, now: Instant, paced: bool) {
1007 client.inflight_bytes += bytes;
1008 client.inflight_frames.push_back(InFlightFrame {
1009 sent_at: now,
1010 bytes,
1011 paced,
1012 });
1013 if paced {
1014 let interval = send_interval(client);
1015 advance_deadline(&mut client.next_send_at, now, interval);
1016 }
1017}
1018
1019fn ewma_with_direction(old: f32, sample: f32, rise_alpha: f32, fall_alpha: f32) -> f32 {
1020 let alpha = if sample > old { rise_alpha } else { fall_alpha };
1021 old * (1.0 - alpha) + sample * alpha
1022}
1023
1024fn window_saturated(client: &ClientState, inflight_frames: usize, inflight_bytes: usize) -> bool {
1025 let target_frames = target_frame_window(client);
1026 let target_bytes = target_byte_window(client);
1027 inflight_frames.saturating_mul(10) >= target_frames.saturating_mul(9)
1028 || inflight_bytes.saturating_mul(10) >= target_bytes.saturating_mul(9)
1029}
1030
1031fn record_ack(client: &mut ClientState) {
1032 if let Some(frame) = client.inflight_frames.pop_front() {
1033 let prev_inflight_frames = client.inflight_frames.len() + 1;
1034 let prev_inflight_bytes = client.inflight_bytes;
1035 client.inflight_bytes = client.inflight_bytes.saturating_sub(frame.bytes);
1036 client.acked_bytes_since_log = client.acked_bytes_since_log.saturating_add(frame.bytes);
1037 let sample_ms = frame.sent_at.elapsed().as_secs_f32() * 1_000.0;
1038 client.rtt_ms = ewma_with_direction(client.rtt_ms, sample_ms, 0.125, 0.25);
1039 if client.min_rtt_ms > 0.0 {
1040 client.min_rtt_ms = client.min_rtt_ms.min(sample_ms);
1043 } else {
1044 client.min_rtt_ms = sample_ms;
1045 }
1046 client.min_rtt_ms = client.min_rtt_ms.max(0.5);
1047 let sample_bps = frame.bytes as f32 / sample_ms.max(1.0e-3) * 1_000.0;
1048 client.delivery_bps = ewma_with_direction(client.delivery_bps, sample_bps, 0.5, 0.125);
1049 client.avg_frame_bytes =
1050 ewma_with_direction(client.avg_frame_bytes, frame.bytes as f32, 0.5, 0.125);
1051 if frame.paced {
1052 client.avg_paced_frame_bytes =
1053 ewma_with_direction(client.avg_paced_frame_bytes, frame.bytes as f32, 0.5, 0.125);
1054 } else {
1055 client.avg_preview_frame_bytes = ewma_with_direction(
1056 client.avg_preview_frame_bytes,
1057 frame.bytes as f32,
1058 0.5,
1059 0.125,
1060 );
1061 }
1062 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
1063 let path_rtt = path_rtt_ms(client);
1064 let likely_window_limited =
1065 window_saturated(client, prev_inflight_frames, prev_inflight_bytes);
1066 client.goodput_window_bytes = client.goodput_window_bytes.saturating_add(frame.bytes);
1067 let now = Instant::now();
1068 let goodput_elapsed = now
1069 .duration_since(client.goodput_window_start)
1070 .as_secs_f32();
1071 if goodput_elapsed >= 0.02 {
1072 let sample_goodput = client.goodput_window_bytes as f32 / goodput_elapsed.max(1.0e-3);
1073 if likely_window_limited || client.browser_backlog_frames > 0 {
1074 let prev_goodput_sample = if client.last_goodput_sample_bps > 0.0 {
1075 client.last_goodput_sample_bps
1076 } else {
1077 sample_goodput
1078 };
1079 let jitter_sample = (sample_goodput - prev_goodput_sample).abs();
1080 client.goodput_bps =
1081 ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, 0.125);
1082 let min_reliable = (client.avg_paced_frame_bytes.max(256.0) * 2.0) as usize;
1088 if client.goodput_window_bytes >= min_reliable {
1089 client.goodput_jitter_bps =
1090 ewma_with_direction(client.goodput_jitter_bps, jitter_sample, 0.5, 0.125);
1091 let jitter_decay = if browser_ready(client) && sample_ms < path_rtt * 3.0 {
1092 0.90
1093 } else {
1094 0.98
1095 };
1096 client.max_goodput_jitter_bps =
1097 (client.max_goodput_jitter_bps * jitter_decay).max(jitter_sample);
1098 client.max_goodput_jitter_bps =
1102 client.max_goodput_jitter_bps.min(client.goodput_bps * 0.45);
1103 } else {
1104 client.goodput_jitter_bps *= 0.9;
1106 client.max_goodput_jitter_bps *= 0.95;
1107 }
1108 client.last_goodput_sample_bps =
1112 (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
1113 } else {
1114 let ratio = client.goodput_bps / sample_goodput.max(1.0);
1119 let fall_alpha = if ratio > 10.0 {
1120 0.5
1121 } else if ratio > 3.0 {
1122 0.25
1123 } else {
1124 0.03
1125 };
1126 client.goodput_bps =
1127 ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, fall_alpha);
1128 client.goodput_jitter_bps *= 0.5;
1129 client.max_goodput_jitter_bps *= 0.9;
1130 client.last_goodput_sample_bps =
1131 (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
1132 }
1133 client.goodput_window_bytes = 0;
1134 client.goodput_window_start = now;
1135 }
1136 let queue_baseline_ms = if throughput_limited(client) {
1137 window_rtt_ms(client)
1138 } else {
1139 path_rtt
1140 };
1141 let queue_delay_ms = (sample_ms - queue_baseline_ms).max(0.0);
1142 let max_probe_frames = (browser_pacing_fps(client) * 0.125).max(4.0);
1143 let jitter_ratio = client.max_goodput_jitter_bps / client.goodput_bps.max(1.0);
1144 let low_delay_frames = if throughput_limited(client) { 2.0 } else { 8.0 };
1145 let high_delay_frames = if throughput_limited(client) {
1146 4.0
1147 } else {
1148 12.0
1149 };
1150 if likely_window_limited
1151 && queue_delay_ms <= frame_ms * low_delay_frames
1152 && jitter_ratio < 0.25
1153 {
1154 client.probe_frames = (client.probe_frames + 1.0).min(max_probe_frames);
1155 } else if !likely_window_limited
1156 && browser_ready(client)
1157 && queue_delay_ms <= frame_ms * 2.0
1158 && jitter_ratio < 0.25
1159 {
1160 client.probe_frames = (client.probe_frames + 0.25).min(max_probe_frames * 0.5);
1161 } else if queue_delay_ms > frame_ms * high_delay_frames || jitter_ratio > 0.5 {
1162 client.probe_frames = (client.probe_frames * 0.5).max(1.0);
1163 } else if queue_delay_ms > frame_ms * 2.0 || !browser_ready(client) {
1164 client.probe_frames = (client.probe_frames - 0.5).max(0.0);
1165 }
1166 } else {
1167 client.inflight_bytes = 0;
1168 }
1169}
1170
1171fn record_surface_ack(client: &mut ClientState) {
1178 if let Some(frame) = client.surface_inflight_frames.pop_front() {
1179 client.acked_bytes_since_log = client.acked_bytes_since_log.saturating_add(frame.bytes);
1180
1181 let sample_ms = frame.sent_at.elapsed().as_secs_f32() * 1_000.0;
1182
1183 let sample_bps = frame.bytes as f32 / sample_ms.max(1.0e-3) * 1_000.0;
1185 client.delivery_bps = ewma_with_direction(client.delivery_bps, sample_bps, 0.5, 0.125);
1186
1187 client.goodput_window_bytes = client.goodput_window_bytes.saturating_add(frame.bytes);
1193 let now = Instant::now();
1194 let goodput_elapsed = now
1195 .duration_since(client.goodput_window_start)
1196 .as_secs_f32();
1197 if goodput_elapsed >= 0.02 {
1198 let sample_goodput = client.goodput_window_bytes as f32 / goodput_elapsed.max(1.0e-3);
1199 client.goodput_bps =
1200 ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, 0.125);
1201 client.last_goodput_sample_bps =
1202 (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
1203 client.goodput_window_bytes = 0;
1204 client.goodput_window_start = now;
1205 }
1206 }
1207}
1208
1209fn reset_inflight(client: &mut ClientState) {
1210 client.inflight_bytes = 0;
1211 client.inflight_frames.clear();
1212 client.next_send_at = Instant::now();
1213 client.browser_backlog_frames = 0;
1214 client.browser_ack_ahead_frames = 0;
1215}
1216
1217fn is_unset_view_size(rows: u16, cols: u16) -> bool {
1218 rows == 0 && cols == 0
1219}
1220
1221fn subscribe_client_to(client: &mut ClientState, pty_id: u16) {
1222 if client.subscriptions.insert(pty_id) {
1223 client.last_sent.remove(&pty_id);
1224 client.preview_next_send_at.remove(&pty_id);
1225 }
1226}
1227
1228fn unsubscribe_client_from(client: &mut ClientState, pty_id: u16) -> bool {
1229 let removed_sub = client.subscriptions.remove(&pty_id);
1230 client.last_sent.remove(&pty_id);
1231 client.preview_next_send_at.remove(&pty_id);
1232 client.scroll_offsets.remove(&pty_id);
1233 client.scroll_caches.remove(&pty_id);
1234 let removed_view = client.view_sizes.remove(&pty_id).is_some();
1235 if client.lead == Some(pty_id) {
1236 client.lead = None;
1237 }
1238 removed_sub || removed_view
1239}
1240
1241fn update_client_scroll_state(client: &mut ClientState, pty_id: u16, next_offset: usize) -> bool {
1242 let prev_offset = client.scroll_offsets.get(&pty_id).copied().unwrap_or(0);
1243 if prev_offset == next_offset {
1244 return false;
1245 }
1246
1247 if prev_offset == 0 && next_offset > 0 {
1248 client.scroll_caches.insert(
1249 pty_id,
1250 client.last_sent.get(&pty_id).cloned().unwrap_or_default(),
1251 );
1252 } else if prev_offset > 0
1253 && next_offset == 0
1254 && let Some(cache) = client.scroll_caches.remove(&pty_id)
1255 {
1256 if cache.rows() > 0 && cache.cols() > 0 {
1257 client.last_sent.insert(pty_id, cache);
1258 } else {
1259 client.last_sent.remove(&pty_id);
1260 }
1261 }
1262
1263 if next_offset > 0 {
1264 client.scroll_offsets.insert(pty_id, next_offset);
1265 } else {
1266 client.scroll_offsets.remove(&pty_id);
1267 }
1268 reset_inflight(client);
1269 true
1270}
1271
1272struct Session {
1273 ptys: HashMap<u16, Pty>,
1274 compositor: Option<SharedCompositor>,
1275 next_client_id: u64,
1276 next_compositor_id: u16,
1277 next_pty_id: u16,
1278 tick_fires: u32,
1279 tick_snaps: u32,
1280 surface_commits: u32,
1281 surface_encodes: u32,
1282 surface_encode_bytes: u64,
1283 surface_frames_sent: u32,
1284 last_ping: Instant,
1285 clients: HashMap<u64, ClientState>,
1286}
1287
1288struct SearchResultRow {
1289 pty_id: u16,
1290 score: u32,
1291 primary_source: u8,
1292 matched_sources: u8,
1293 context: String,
1294 scroll_offset: Option<usize>,
1295}
1296
1297struct TickOutcome {
1298 next_deadline: Option<Instant>,
1299}
1300
1301impl Session {
1302 fn new() -> Self {
1303 Self {
1304 ptys: HashMap::new(),
1305 compositor: None,
1306 next_client_id: 1,
1307 next_compositor_id: 1,
1308 next_pty_id: 1,
1309 clients: HashMap::new(),
1310 tick_fires: 0,
1311 tick_snaps: 0,
1312 surface_commits: 0,
1313 surface_encodes: 0,
1314 surface_encode_bytes: 0,
1315 last_ping: Instant::now(),
1316 surface_frames_sent: 0,
1317 }
1318 }
1319
1320 fn ensure_compositor(
1321 &mut self,
1322 verbose: bool,
1323 event_notify: Arc<dyn Fn() + Send + Sync>,
1324 gpu_device: &str,
1325 ) -> &str {
1326 if self.compositor.is_none() {
1327 let session_id = self.next_compositor_id;
1328 self.next_compositor_id = self.next_compositor_id.wrapping_add(1);
1329 let created_at = Instant::now();
1332 let handle = blit_compositor::spawn_compositor(verbose, event_notify, gpu_device);
1333 #[cfg(unix)]
1334 let audio_pipeline = {
1335 let audio_disabled = std::env::var("BLIT_AUDIO")
1336 .map(|v| v == "0")
1337 .unwrap_or(false);
1338 if !audio_disabled && audio::pipewire_available() {
1339 let runtime_dir = std::path::Path::new(&handle.socket_name)
1340 .parent()
1341 .unwrap_or(std::path::Path::new("/tmp"));
1342 let bitrate = std::env::var("BLIT_AUDIO_BITRATE")
1343 .ok()
1344 .and_then(|v| v.parse::<i32>().ok())
1345 .unwrap_or(0);
1346 tokio::task::block_in_place(|| {
1349 match audio::AudioPipeline::spawn(
1350 runtime_dir,
1351 session_id,
1352 bitrate,
1353 verbose,
1354 created_at,
1355 ) {
1356 Ok(pipeline) => {
1357 if verbose {
1358 eprintln!(
1359 "[audio] pipeline started, PULSE_SERVER={}",
1360 pipeline.pulse_server_path(),
1361 );
1362 }
1363 Some(pipeline)
1364 }
1365 Err(e) => {
1366 eprintln!("[audio] failed to start pipeline: {e}");
1367 None
1368 }
1369 }
1370 })
1371 } else {
1372 if verbose && !audio_disabled {
1373 eprintln!("[audio] PipeWire not available, audio disabled");
1374 }
1375 None
1376 }
1377 };
1378
1379 self.compositor = Some(SharedCompositor {
1380 handle,
1381 surfaces: HashMap::new(),
1382 last_pixels: HashMap::new(),
1383 last_frame_request: HashMap::new(),
1384 created_at,
1385 pixel_generation: 0,
1386 last_blanket_frame_request: Instant::now(),
1387 last_configured_size: HashMap::new(),
1388 #[cfg(unix)]
1389 audio_pipeline,
1390 #[cfg(unix)]
1391 audio_session_id: session_id,
1392 #[cfg(unix)]
1393 last_audio_restart: None,
1394 });
1395 }
1396 &self.compositor.as_ref().unwrap().handle.socket_name
1397 }
1398
1399 #[cfg(unix)]
1401 fn pulse_server_path(&self) -> Option<String> {
1402 self.compositor
1403 .as_ref()
1404 .and_then(|cs| cs.audio_pipeline.as_ref())
1405 .map(|ap| ap.pulse_server_path())
1406 }
1407
1408 #[cfg(unix)]
1410 fn pipewire_remote_path(&self) -> Option<String> {
1411 self.compositor
1412 .as_ref()
1413 .and_then(|cs| cs.audio_pipeline.as_ref())
1414 .map(|ap| ap.pipewire_remote_path())
1415 }
1416
1417 fn allocate_pty_id(&mut self, max_ptys: usize) -> Option<u16> {
1418 if max_ptys > 0 && self.ptys.len() >= max_ptys {
1419 return None;
1420 }
1421 let start = self.next_pty_id;
1422 let mut id = start;
1423 loop {
1424 if !self.ptys.contains_key(&id) {
1425 self.next_pty_id = if id == u16::MAX { 1 } else { id + 1 };
1426 return Some(id);
1427 }
1428 id = if id == u16::MAX { 1 } else { id + 1 };
1429 if id == start {
1430 return None;
1431 }
1432 }
1433 }
1434
1435 fn send_to_all(&self, msg: &[u8]) {
1436 for c in self.clients.values() {
1437 let _ = try_send_outbox(c, msg.to_vec());
1438 }
1439 }
1440
1441 fn mediated_size_for_pty(&self, pty_id: u16) -> Option<(u16, u16)> {
1442 let mut min_rows: Option<u16> = None;
1443 let mut min_cols: Option<u16> = None;
1444 for c in self.clients.values() {
1445 if let Some((r, cols)) = c.view_sizes.get(&pty_id).copied() {
1446 min_rows = Some(min_rows.map_or(r, |m: u16| m.min(r)));
1447 min_cols = Some(min_cols.map_or(cols, |m: u16| m.min(cols)));
1448 }
1449 }
1450 match (min_rows, min_cols) {
1451 (Some(r), Some(c)) => Some((r.max(1), c.max(1))),
1452 _ => None,
1453 }
1454 }
1455
1456 fn resize_pty(&mut self, pty_id: u16, rows: u16, cols: u16) -> bool {
1457 let pty = match self.ptys.get_mut(&pty_id) {
1458 Some(p) => p,
1459 None => return false,
1460 };
1461 let (cur_rows, cur_cols) = pty.driver.size();
1462 if cur_rows == rows && cur_cols == cols {
1463 return false;
1464 }
1465 pty.ready_frames.clear();
1466 pty.driver.resize(rows, cols);
1467 pty.mark_dirty();
1468 for c in self.clients.values_mut() {
1469 if c.subscriptions.contains(&pty_id) {
1470 c.last_sent.remove(&pty_id);
1471 }
1472 if c.scroll_caches.remove(&pty_id).is_some() {
1473 reset_inflight(c);
1474 }
1475 }
1476 if !pty.exited {
1477 pty::resize_pty_os(&pty.handle, rows, cols);
1478 }
1479 true
1480 }
1481
1482 fn resize_ptys_to_mediated_sizes<I>(&mut self, pty_ids: I) -> bool
1483 where
1484 I: IntoIterator<Item = u16>,
1485 {
1486 let mut changed = false;
1487 let mut seen = HashSet::new();
1488 for pty_id in pty_ids {
1489 if !seen.insert(pty_id) {
1490 continue;
1491 }
1492 if let Some((rows, cols)) = self.mediated_size_for_pty(pty_id) {
1493 changed |= self.resize_pty(pty_id, rows, cols);
1494 }
1495 }
1496 changed
1497 }
1498
1499 fn mediated_size_for_surface(
1509 &self,
1510 surface_id: u16,
1511 max: Option<(u16, u16)>,
1512 ) -> Option<(u16, u16, u16)> {
1513 let mut min_w: Option<u16> = None;
1514 let mut min_h: Option<u16> = None;
1515 let mut max_scale: u16 = 0;
1516 for c in self.clients.values() {
1517 if let Some(&(w, h, s)) = c.surface_view_sizes.get(&surface_id) {
1518 min_w = Some(min_w.map_or(w, |m: u16| m.min(w)));
1519 min_h = Some(min_h.map_or(h, |m: u16| m.min(h)));
1520 max_scale = max_scale.max(s);
1521 }
1522 }
1523 match (min_w, min_h) {
1524 (Some(w), Some(h)) => {
1525 let (w, h) = (w.max(1), h.max(1));
1526 let (w, h) = if let Some((mw, mh)) = max {
1527 (w.min(mw), h.min(mh))
1528 } else {
1529 (w, h)
1530 };
1531 Some((w, h, max_scale))
1532 }
1533 _ => None,
1534 }
1535 }
1536
1537 fn resize_surface(&mut self, surface_id: u16, width: u16, height: u16, scale_120: u16) -> bool {
1538 let cs = match self.compositor.as_mut() {
1539 Some(cs) => cs,
1540 None => return false,
1541 };
1542 if let Some(&(lw, lh, ls)) = cs.last_configured_size.get(&surface_id)
1550 && lw == width
1551 && lh == height
1552 && ls == scale_120
1553 {
1554 return false;
1555 }
1556 cs.last_configured_size
1557 .insert(surface_id, (width, height, scale_120));
1558 let _ = cs.handle.command_tx.send(CompositorCommand::SurfaceResize {
1559 surface_id,
1560 width,
1561 height,
1562 scale_120,
1563 });
1564 true
1565 }
1566
1567 fn resize_surfaces_to_mediated_sizes<I>(
1568 &mut self,
1569 surface_ids: I,
1570 encoder_preferences: &[SurfaceEncoderPreference],
1571 ) where
1572 I: IntoIterator<Item = u16>,
1573 {
1574 let max = SurfaceEncoderPreference::max_dimensions_for_list(encoder_preferences);
1575 let mut seen = HashSet::new();
1576 for sid in surface_ids {
1577 if !seen.insert(sid) {
1578 continue;
1579 }
1580 if let Some((w, h, scale_120)) = self.mediated_size_for_surface(sid, max) {
1581 self.resize_surface(sid, w, h, scale_120);
1582 }
1583 }
1584 }
1585
1586 fn pty_list_msg(&self) -> Vec<u8> {
1587 let mut msg = vec![S2C_LIST];
1588 let count = self.ptys.len() as u16;
1589 msg.extend_from_slice(&count.to_le_bytes());
1590 let mut ids: Vec<u16> = self.ptys.keys().copied().collect();
1591 ids.sort();
1592 for id in ids {
1593 let pty = &self.ptys[&id];
1594 let tag = pty.tag.as_bytes();
1595 msg.extend_from_slice(&id.to_le_bytes());
1596 msg.extend_from_slice(&(tag.len() as u16).to_le_bytes());
1597 msg.extend_from_slice(tag);
1598 let cmd = pty.command.as_deref().unwrap_or("").as_bytes();
1599 msg.extend_from_slice(&(cmd.len() as u16).to_le_bytes());
1600 msg.extend_from_slice(cmd);
1601 }
1602 msg
1603 }
1604
1605 fn surface_list_msg(&self) -> Vec<u8> {
1606 let cs = match self.compositor.as_ref() {
1607 Some(cs) => cs,
1608 None => {
1609 let mut msg = vec![S2C_SURFACE_LIST];
1610 msg.extend_from_slice(&0u16.to_le_bytes());
1611 return msg;
1612 }
1613 };
1614 let mut msg = vec![S2C_SURFACE_LIST];
1615 let count = cs.surfaces.len() as u16;
1616 msg.extend_from_slice(&count.to_le_bytes());
1617 let mut ids: Vec<u16> = cs.surfaces.keys().copied().collect();
1618 ids.sort();
1619 for id in ids {
1620 let info = &cs.surfaces[&id];
1621 let title = info.title.as_bytes();
1622 let app_id = info.app_id.as_bytes();
1623 msg.extend_from_slice(&info.surface_id.to_le_bytes());
1624 msg.extend_from_slice(&info.parent_id.to_le_bytes());
1625 msg.extend_from_slice(&info.width.to_le_bytes());
1626 msg.extend_from_slice(&info.height.to_le_bytes());
1627 msg.extend_from_slice(&(title.len() as u16).to_le_bytes());
1628 msg.extend_from_slice(title);
1629 msg.extend_from_slice(&(app_id.len() as u16).to_le_bytes());
1630 msg.extend_from_slice(app_id);
1631 }
1632 msg
1633 }
1634}
1635
1636struct AppStateInner {
1637 config: Config,
1638 session: Mutex<Session>,
1639 pty_fds: PtyFds,
1640 delivery_notify: Arc<Notify>,
1641 shutdown_notify: Arc<Notify>,
1643 active_connections: std::sync::atomic::AtomicUsize,
1646}
1647
1648type AppState = Arc<AppStateInner>;
1649
1650fn nudge_delivery(state: &AppState) {
1651 state.delivery_notify.notify_one();
1652}
1653
1654#[cfg(unix)]
1655#[allow(dead_code)]
1656fn spawn_compositor_child(
1657 command: &str,
1658 argv: Option<&[&str]>,
1659 wayland_socket: &str,
1660 dir: Option<&str>,
1661) -> libc::pid_t {
1662 use std::ffi::CString;
1663 let pid = unsafe { libc::fork() };
1664 if pid == 0 {
1665 if let Some(d) = dir {
1666 let c_dir = CString::new(d).unwrap();
1667 unsafe {
1668 libc::chdir(c_dir.as_ptr());
1669 }
1670 }
1671 unsafe {
1672 let wd_path = std::path::Path::new(wayland_socket);
1673 if let Some(dir) = wd_path.parent() {
1674 let xdg = std::env::var_os("XDG_RUNTIME_DIR");
1675 let needs_update = match &xdg {
1676 Some(x) => std::path::Path::new(x) != dir,
1677 None => true,
1678 };
1679 if needs_update {
1680 std::env::set_var("XDG_RUNTIME_DIR", dir);
1681 }
1682 }
1683 std::env::set_var("WAYLAND_DISPLAY", wayland_socket);
1684 std::env::remove_var("DISPLAY");
1685 std::env::remove_var("DBUS_SESSION_BUS_ADDRESS");
1686 std::env::remove_var("DBUS_SYSTEM_BUS_ADDRESS");
1687 }
1688 if let Some(args) = argv {
1689 let prog = CString::new(args[0]).unwrap();
1690 let c_args: Vec<CString> = args.iter().map(|a| CString::new(*a).unwrap()).collect();
1691 let c_ptrs: Vec<*const libc::c_char> = c_args
1692 .iter()
1693 .map(|a| a.as_ptr())
1694 .chain(std::iter::once(std::ptr::null()))
1695 .collect();
1696 unsafe {
1697 libc::execvp(prog.as_ptr(), c_ptrs.as_ptr());
1698 }
1699 } else {
1700 let prog = CString::new(command).unwrap();
1701 let c_ptrs = [prog.as_ptr(), std::ptr::null()];
1702 unsafe {
1703 libc::execvp(prog.as_ptr(), c_ptrs.as_ptr());
1704 libc::_exit(1);
1705 }
1706 }
1707 }
1708 pid
1709}
1710
1711fn xterm256_color(idx: u8) -> (u16, u16, u16) {
1713 const BASE16: [(u8, u8, u8); 16] = [
1715 (0, 0, 0),
1716 (128, 0, 0),
1717 (0, 128, 0),
1718 (128, 128, 0),
1719 (0, 0, 128),
1720 (128, 0, 128),
1721 (0, 128, 128),
1722 (192, 192, 192),
1723 (128, 128, 128),
1724 (255, 0, 0),
1725 (0, 255, 0),
1726 (255, 255, 0),
1727 (0, 0, 255),
1728 (255, 0, 255),
1729 (0, 255, 255),
1730 (255, 255, 255),
1731 ];
1732 let (r8, g8, b8) = if idx < 16 {
1733 BASE16[idx as usize]
1734 } else if idx < 232 {
1735 let n = idx - 16;
1737 let ri = n / 36;
1738 let gi = (n % 36) / 6;
1739 let bi = n % 6;
1740 let to_val = |v: u8| if v == 0 { 0u8 } else { 55 + 40 * v };
1741 (to_val(ri), to_val(gi), to_val(bi))
1742 } else {
1743 let v = 8 + 10 * (idx - 232);
1745 (v, v, v)
1746 };
1747 let scale = |v: u8| (v as u16) << 8 | v as u16;
1749 (scale(r8), scale(g8), scale(b8))
1750}
1751fn parse_terminal_queries(data: &[u8], size: (u16, u16), cursor: (u16, u16)) -> Vec<String> {
1752 const DA1_RESPONSE: &[u8] = b"\x1b[?64;1;2;6;9;15;18;21;22c";
1753
1754 let mut results = Vec::new();
1755 let mut i = 0;
1756 while i < data.len() {
1757 if data[i] != 0x1b || i + 1 >= data.len() {
1758 i += 1;
1759 continue;
1760 }
1761
1762 if data[i + 1] == b']' {
1764 let osc_start = i + 2;
1765 let mut end = osc_start;
1767 while end < data.len() {
1768 if data[end] == 0x07 {
1769 break;
1770 }
1771 if data[end] == 0x1b && end + 1 < data.len() && data[end + 1] == b'\\' {
1772 break;
1773 }
1774 end += 1;
1775 }
1776 if end < data.len() {
1777 let payload = &data[osc_start..end];
1778 if payload == b"11;?" {
1780 results.push("\x1b]11;rgb:0000/0000/0000\x1b\\".into());
1782 }
1783 else if payload == b"10;?" {
1785 results.push("\x1b]10;rgb:ffff/ffff/ffff\x1b\\".into());
1786 }
1787 else if payload.starts_with(b"4;") && payload.ends_with(b";?") {
1789 let idx_bytes = &payload[2..payload.len() - 2];
1790 if let Ok(idx_str) = std::str::from_utf8(idx_bytes)
1791 && let Ok(idx) = idx_str.parse::<u8>()
1792 {
1793 let (r, g, b) = xterm256_color(idx);
1794 results.push(format!("\x1b]4;{idx};rgb:{r:04x}/{g:04x}/{b:04x}\x1b\\"));
1795 }
1796 }
1797 i = end + if data[end] == 0x07 { 1 } else { 2 };
1798 continue;
1799 }
1800 i = end;
1801 continue;
1802 }
1803
1804 if i + 2 >= data.len() || data[i + 1] != b'[' {
1806 i += 1;
1807 continue;
1808 }
1809 i += 2;
1810 let has_q = i < data.len() && data[i] == b'?';
1811 if has_q {
1812 i += 1;
1813 }
1814 let param_start = i;
1815 while i < data.len() && (data[i].is_ascii_digit() || data[i] == b';') {
1816 i += 1;
1817 }
1818 if i >= data.len() {
1819 break;
1820 }
1821 let final_byte = data[i];
1822 let params = &data[param_start..i];
1823 i += 1;
1824 if has_q {
1825 continue;
1826 }
1827 let resp: Option<String> = match final_byte {
1828 b'c' if params.is_empty() || params == b"0" => {
1829 Some(String::from_utf8_lossy(DA1_RESPONSE).into_owned())
1830 }
1831 b'n' if params == b"6" => Some(format!("\x1b[{};{}R", cursor.0 + 1, cursor.1 + 1)),
1832 b'n' if params == b"5" => Some("\x1b[0n".into()),
1833 b't' if params == b"18" => {
1834 let (rows, cols) = size;
1835 Some(format!("\x1b[8;{rows};{cols}t"))
1836 }
1837 b't' if params == b"14" => {
1838 let (rows, cols) = size;
1839 Some(format!("\x1b[4;{};{}t", rows * 16, cols * 8))
1840 }
1841 _ => None,
1842 };
1843 if let Some(r) = resp {
1844 results.push(r);
1845 }
1846 }
1847 results
1848}
1849
1850async fn cleanup_pty_internal(pty_id: u16, state: &AppState) {
1851 state.pty_fds.write().unwrap().remove(&pty_id);
1852 let mut sess = state.session.lock().await;
1853 if let Some(pty) = sess.ptys.get_mut(&pty_id) {
1854 if pty.exited {
1855 return;
1856 }
1857 pty.exited = true;
1858 pty::close_pty(&pty.handle);
1859 pty.exit_status = pty::collect_exit_status(&pty.handle);
1860 pty.mark_dirty();
1861 let msg = blit_remote::msg_exited(pty_id, pty.exit_status);
1862 sess.send_to_all(&msg);
1863 }
1864}
1865
1866fn take_snapshot(pty: &mut Pty) -> FrameState {
1867 if pty.lflag_last.elapsed() >= Duration::from_millis(250) {
1868 pty.lflag_cache = pty::pty_lflag(&pty.handle);
1869 pty.lflag_last = Instant::now();
1870 }
1871 let (echo, icanon) = pty.lflag_cache;
1872 pty.driver.snapshot(echo, icanon)
1873}
1874
1875fn build_scrollback_update(
1876 pty: &mut Pty,
1877 id: u16,
1878 offset: usize,
1879 prev_frame: &FrameState,
1880) -> Option<(Vec<u8>, FrameState)> {
1881 let frame = pty.driver.scrollback_frame(offset);
1882 let msg = build_update_msg(id, &frame, prev_frame);
1883 msg.map(|m| (m, frame))
1884}
1885
1886fn build_search_results_msg(request_id: u16, results: &[SearchResultRow]) -> Vec<u8> {
1887 let count = results.len().min(u16::MAX as usize);
1888 let payload_bytes: usize = results[..count]
1889 .iter()
1890 .map(|result| 14 + result.context.len().min(u16::MAX as usize))
1891 .sum();
1892 let mut msg = Vec::with_capacity(5 + payload_bytes);
1893 msg.push(S2C_SEARCH_RESULTS);
1894 msg.extend_from_slice(&request_id.to_le_bytes());
1895 msg.extend_from_slice(&(count as u16).to_le_bytes());
1896 for result in &results[..count] {
1897 msg.extend_from_slice(&result.pty_id.to_le_bytes());
1898 msg.extend_from_slice(&result.score.to_le_bytes());
1899 msg.push(result.primary_source);
1900 msg.push(result.matched_sources);
1901 let scroll_offset = result
1902 .scroll_offset
1903 .map(|offset| offset.min(u32::MAX as usize - 1) as u32)
1904 .unwrap_or(u32::MAX);
1905 msg.extend_from_slice(&scroll_offset.to_le_bytes());
1906 let context = result.context.as_bytes();
1907 let context_len = context.len().min(u16::MAX as usize);
1908 msg.extend_from_slice(&(context_len as u16).to_le_bytes());
1909 msg.extend_from_slice(&context[..context_len]);
1910 }
1911 msg
1912}
1913
1914enum SendOutcome {
1915 NoChange,
1916 Sent,
1917 Backpressured,
1918}
1919
1920fn try_send_update(
1921 client: &mut ClientState,
1922 pid: u16,
1923 current: FrameState,
1924 msg: Option<Vec<u8>>,
1925 now: Instant,
1926 paced: bool,
1927) -> SendOutcome {
1928 let Some(msg) = msg else {
1929 return SendOutcome::NoChange;
1930 };
1931 let bytes = msg.len();
1932 if try_send_outbox(client, msg).is_ok() {
1933 client.last_sent.insert(pid, current);
1934 record_send(client, bytes, now, paced);
1935 client.frames_sent = client.frames_sent.wrapping_add(1);
1936 SendOutcome::Sent
1937 } else {
1938 client.last_sent.insert(pid, current);
1944 SendOutcome::Backpressured
1945 }
1946}
1947
1948pub async fn run(config: Config) {
1949 let state: AppState = Arc::new(AppStateInner {
1950 config,
1951 session: Mutex::new(Session::new()),
1952 pty_fds: Arc::new(std::sync::RwLock::new(HashMap::new())),
1953 delivery_notify: Arc::new(Notify::new()),
1954 shutdown_notify: Arc::new(Notify::new()),
1955 active_connections: std::sync::atomic::AtomicUsize::new(0),
1956 });
1957
1958 if !state.config.skip_compositor {
1961 let notify = state.delivery_notify.clone();
1962 let event_notify = Arc::new(move || notify.notify_one()) as Arc<dyn Fn() + Send + Sync>;
1963 let mut sess = state.session.lock().await;
1964 sess.ensure_compositor(
1965 state.config.verbose,
1966 event_notify,
1967 &state.config.vaapi_device,
1968 );
1969 }
1970
1971 let delivery_state = state.clone();
1972 tokio::spawn(async move {
1973 let mut next_deadline: Option<Instant> = None;
1974 loop {
1975 if let Some(deadline) = next_deadline {
1976 tokio::select! {
1977 _ = delivery_state.delivery_notify.notified() => {}
1978 _ = tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)) => {}
1979 }
1980 } else {
1981 delivery_state.delivery_notify.notified().await;
1982 }
1983 let outcome = tick(&delivery_state).await;
1984 next_deadline = outcome.next_deadline;
1985 }
1986 });
1987
1988 tokio::spawn(async {
1989 loop {
1990 tokio::time::sleep(Duration::from_secs(5)).await;
1991 pty::reap_zombies();
1992 }
1993 });
1994
1995 #[cfg(unix)]
1996 if let Some(channel_fd) = state.config.fd_channel {
1997 ipc::run_fd_channel(channel_fd, state).await;
1998 return;
1999 }
2000
2001 #[cfg(unix)]
2002 let listener = {
2003 if let Some(l) = IpcListener::from_systemd_fd(state.config.verbose) {
2004 l
2005 } else {
2006 IpcListener::bind(&state.config.ipc_path, state.config.verbose)
2007 }
2008 };
2009 #[cfg(not(unix))]
2010 let mut listener = IpcListener::bind(&state.config.ipc_path, state.config.verbose);
2011
2012 {
2015 let state = state.clone();
2016 tokio::spawn(async move {
2017 #[cfg(unix)]
2018 {
2019 use tokio::signal::unix::{SignalKind, signal};
2020 let mut sigterm = signal(SignalKind::terminate()).expect("signal handler");
2021 let mut sigint = signal(SignalKind::interrupt()).expect("signal handler");
2022 tokio::select! {
2023 _ = sigterm.recv() => {}
2024 _ = sigint.recv() => {}
2025 }
2026 }
2027 #[cfg(not(unix))]
2028 {
2029 let _ = tokio::signal::ctrl_c().await;
2030 }
2031 let sess = state.session.lock().await;
2032 sess.send_to_all(&[S2C_QUIT]);
2033 drop(sess);
2034 state.shutdown_notify.notify_waiters();
2035 });
2036 }
2037
2038 let shutdown = state.shutdown_notify.clone();
2039 loop {
2040 let stream = tokio::select! {
2041 result = listener.accept() => match result {
2042 Ok(s) => s,
2043 Err(e) => {
2044 eprintln!("accept error: {e}");
2045 tokio::time::sleep(Duration::from_millis(100)).await;
2046 continue;
2047 }
2048 },
2049 _ = shutdown.notified() => break,
2050 };
2051 let max = state.config.max_connections;
2052 if max > 0 {
2053 let current = state
2054 .active_connections
2055 .load(std::sync::atomic::Ordering::Relaxed);
2056 if current >= max {
2057 eprintln!("max connections ({max}) reached, rejecting");
2058 drop(stream);
2059 continue;
2060 }
2061 }
2062 state
2063 .active_connections
2064 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2065 let state = state.clone();
2066 tokio::spawn(async move {
2067 handle_client(stream, state.clone()).await;
2068 state
2069 .active_connections
2070 .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
2071 });
2072 }
2073 tokio::time::sleep(Duration::from_millis(100)).await;
2075}
2076
2077const BLANKET_FRAME_INTERVAL: Duration = Duration::from_millis(33);
2082
2083async fn tick(state: &AppState) -> TickOutcome {
2084 let mut sess = state.session.lock().await;
2085 sess.tick_fires += 1;
2086 let mut next_deadline: Option<Instant> = None;
2087 let now = Instant::now();
2088
2089 let ping_interval = state.config.ping_interval;
2091 if !ping_interval.is_zero() && now.duration_since(sess.last_ping) >= ping_interval {
2092 sess.send_to_all(&[S2C_PING]);
2093 sess.last_ping = now;
2094 }
2095 if !ping_interval.is_zero() {
2096 let next_ping = sess.last_ping + ping_interval;
2097 next_deadline = Some(next_deadline.map_or(next_ping, |d: Instant| d.min(next_ping)));
2098 }
2099
2100 let max_fps = sess
2101 .clients
2102 .values()
2103 .map(browser_pacing_fps)
2104 .fold(1.0_f32, f32::max);
2105 let title_interval = Duration::from_secs_f64(1.0 / max_fps as f64);
2106 let ids: Vec<u16> = sess.ptys.keys().copied().collect();
2107 for &id in &ids {
2108 let Some(pty) = sess.ptys.get_mut(&id) else {
2109 continue;
2110 };
2111 if pty.driver.take_title_dirty() {
2112 pty.mark_dirty();
2113 pty.title_pending = true;
2114 }
2115 if pty.title_pending && now.duration_since(pty.last_title_send) >= title_interval {
2116 let msg = {
2117 let title_bytes = pty.driver.title().as_bytes();
2118 let mut msg = Vec::with_capacity(3 + title_bytes.len());
2119 msg.push(S2C_TITLE);
2120 msg.extend_from_slice(&id.to_le_bytes());
2121 msg.extend_from_slice(title_bytes);
2122 msg
2123 };
2124 pty.last_title_send = now;
2125 pty.title_pending = false;
2126 sess.send_to_all(&msg);
2127 }
2128 }
2129
2130 let mut eof_ptys: Vec<u16> = Vec::with_capacity(ids.len());
2133 for &id in &ids {
2134 let Some(pty) = sess.ptys.get_mut(&id) else {
2135 continue;
2136 };
2137 while let Ok(input) = pty.byte_rx.try_recv() {
2138 match input {
2139 PtyInput::Data(data) => {
2140 pty::respond_to_queries(
2141 &pty.handle,
2142 &data,
2143 pty.driver.size(),
2144 pty.driver.cursor_position(),
2145 );
2146 pty.driver.process(&data);
2147 pty.mark_dirty();
2148 }
2149 PtyInput::SyncBoundary { before, after } => {
2150 if !before.is_empty() {
2151 pty::respond_to_queries(
2152 &pty.handle,
2153 &before,
2154 pty.driver.size(),
2155 pty.driver.cursor_position(),
2156 );
2157 pty.driver.process(&before);
2158 pty.mark_dirty();
2159 }
2160 if !pty.driver.synced_output() {
2161 let frame = take_snapshot(pty);
2162 enqueue_ready_frame(&mut pty.ready_frames, frame);
2163 pty.clear_dirty();
2164 }
2165 if !after.is_empty() {
2166 pty::respond_to_queries(
2167 &pty.handle,
2168 &after,
2169 pty.driver.size(),
2170 pty.driver.cursor_position(),
2171 );
2172 pty.driver.process(&after);
2173 pty.mark_dirty();
2174 }
2175 }
2176 PtyInput::Eof => {
2177 eof_ptys.push(id);
2178 }
2179 }
2180 }
2181 }
2182 drop(sess);
2184 for id in eof_ptys {
2185 tokio::time::sleep(Duration::from_millis(50)).await;
2186 cleanup_pty_internal(id, state).await;
2187 }
2188 let mut sess = state.session.lock().await;
2189
2190 let needful_ptys: HashSet<u16> = sess
2194 .clients
2195 .values()
2196 .flat_map(|c| {
2197 let reserve_preview_slot = client_has_due_preview(&sess, c, now);
2198 c.subscriptions.iter().copied().filter(move |pid| {
2199 let scrolled = c.scroll_offsets.get(pid).copied().unwrap_or(0) > 0;
2200 if Some(*pid) == c.lead {
2201 !scrolled && can_send_frame(c, now, reserve_preview_slot)
2202 } else {
2203 !scrolled && can_send_preview(c, *pid, now)
2204 }
2205 })
2206 })
2207 .collect();
2208
2209 let mut snapshots: HashMap<u16, FrameState> = HashMap::new();
2210 for &id in &ids {
2211 let Some(pty) = sess.ptys.get_mut(&id) else {
2212 continue;
2213 };
2214 if needful_ptys.contains(&id)
2215 && let Some(frame) = pty.ready_frames.pop_front()
2216 {
2217 snapshots.insert(id, frame);
2218 sess.tick_snaps += 1;
2219 continue;
2220 }
2221 if !should_snapshot_pty(
2222 pty.dirty,
2223 needful_ptys.contains(&id),
2224 pty.driver.synced_output(),
2225 ) {
2226 continue;
2227 }
2228 snapshots.insert(id, take_snapshot(pty));
2232 pty.clear_dirty();
2233 sess.tick_snaps += 1;
2234 }
2235
2236 let client_ids: Vec<u64> = sess.clients.keys().copied().collect();
2237 for cid in client_ids {
2238 if let Some(c) = sess.clients.get_mut(&cid) {
2244 if c.inflight_bytes == 0 && c.min_rtt_ms > 0.0 && c.rtt_ms > c.min_rtt_ms {
2245 c.rtt_ms = (c.rtt_ms * 0.99 + c.min_rtt_ms * 0.01).max(c.min_rtt_ms);
2246 }
2247 if c.last_metrics_update.elapsed() > Duration::from_secs(1) {
2250 c.browser_backlog_frames = 0;
2251 c.browser_ack_ahead_frames = 0;
2252 }
2253 }
2254 let (
2255 lead,
2256 subscriptions,
2257 scrolled_ptys,
2258 can_send_lead,
2259 lead_has_window,
2260 any_send_window,
2261 lead_deadline,
2262 ) = {
2263 let Some(c) = sess.clients.get(&cid) else {
2264 continue;
2265 };
2266 let reserve_preview_slot = client_has_due_preview(&sess, c, now);
2267 (
2268 c.lead,
2269 c.subscriptions.iter().copied().collect::<Vec<_>>(),
2270 c.scroll_offsets
2271 .iter()
2272 .map(|(&k, &v)| (k, v))
2273 .collect::<Vec<_>>(),
2274 can_send_frame(c, now, reserve_preview_slot),
2275 lead_window_open(c, reserve_preview_slot),
2276 lead_window_open(c, reserve_preview_slot) || window_open(c),
2277 c.next_send_at,
2278 )
2279 };
2280
2281 if subscriptions.is_empty() {
2282 continue;
2283 }
2284
2285 for &(scroll_pid, scroll_offset) in &scrolled_ptys {
2287 if scroll_offset == 0 {
2288 continue;
2289 }
2290 let is_lead = lead == Some(scroll_pid);
2291 let can_send = if is_lead { can_send_lead } else { true };
2292 if can_send {
2293 let prev_frame = {
2294 let Some(c) = sess.clients.get(&cid) else {
2295 continue;
2296 };
2297 c.scroll_caches
2298 .get(&scroll_pid)
2299 .cloned()
2300 .unwrap_or_default()
2301 };
2302 let outcome = if let Some(pty) = sess.ptys.get_mut(&scroll_pid) {
2303 if let Some((msg, new_frame)) =
2304 build_scrollback_update(pty, scroll_pid, scroll_offset, &prev_frame)
2305 {
2306 let Some(c) = sess.clients.get_mut(&cid) else {
2307 break;
2308 };
2309 let bytes = msg.len();
2310 if try_send_outbox(c, msg).is_ok() {
2311 c.scroll_caches.insert(scroll_pid, new_frame);
2312 record_send(c, bytes, now, is_lead);
2313 c.frames_sent += 1;
2314 SendOutcome::Sent
2315 } else {
2316 SendOutcome::Backpressured
2317 }
2318 } else {
2319 SendOutcome::NoChange
2320 }
2321 } else {
2322 SendOutcome::NoChange
2323 };
2324 match outcome {
2325 SendOutcome::Sent => {}
2326 SendOutcome::Backpressured => {
2327 if let Some(pty) = sess.ptys.get_mut(&scroll_pid) {
2328 pty.mark_dirty();
2329 }
2330 }
2331 SendOutcome::NoChange => {}
2332 }
2333 } else if is_lead && lead_has_window {
2334 next_deadline = Some(match next_deadline {
2335 Some(existing) => existing.min(lead_deadline),
2336 None => lead_deadline,
2337 });
2338 }
2339 }
2340
2341 let lead_scroll_offset = lead
2342 .and_then(|pid| {
2343 scrolled_ptys
2344 .iter()
2345 .find(|&&(k, _)| k == pid)
2346 .map(|&(_, v)| v)
2347 })
2348 .unwrap_or(0);
2349
2350 if let Some(pid) = lead {
2351 if lead_scroll_offset == 0 && can_send_lead {
2352 if let Some(cur) = snapshots.get(&pid).cloned() {
2353 let previous = sess
2354 .clients
2355 .get(&cid)
2356 .and_then(|c| c.last_sent.get(&pid).cloned())
2357 .unwrap_or_default();
2358 drop(sess);
2359 let msg = build_update_msg(pid, &cur, &previous);
2360 sess = state.session.lock().await;
2361 let Some(c) = sess.clients.get_mut(&cid) else {
2362 continue;
2363 };
2364 match try_send_update(c, pid, cur, msg, now, true) {
2365 SendOutcome::Sent => {}
2366 SendOutcome::Backpressured => {
2367 if let Some(pty) = sess.ptys.get_mut(&pid) {
2368 pty.mark_dirty();
2369 }
2370 }
2371 SendOutcome::NoChange => {}
2372 }
2373 } else {
2374 let has_pending = sess
2375 .ptys
2376 .get(&pid)
2377 .map(pty_has_visual_update)
2378 .unwrap_or(false);
2379 let _ = has_pending;
2380 }
2381 } else {
2382 let has_pending = sess
2383 .ptys
2384 .get(&pid)
2385 .map(pty_has_visual_update)
2386 .unwrap_or(false);
2387 if has_pending && lead_has_window {
2388 next_deadline = Some(match next_deadline {
2389 Some(existing) => existing.min(lead_deadline),
2390 None => lead_deadline,
2391 });
2392 }
2393 }
2394 }
2395
2396 if !any_send_window {
2397 continue;
2398 }
2399
2400 let mut preview_ids = subscriptions;
2401 preview_ids.retain(|pid| Some(*pid) != lead);
2402 preview_ids.sort_unstable();
2403
2404 for pid in preview_ids {
2405 let (preview_can_send, preview_due_at, preview_has_window) =
2406 match sess.clients.get(&cid) {
2407 Some(c) => (
2408 can_send_preview(c, pid, now),
2409 preview_deadline(c, pid, now),
2410 window_open(c),
2411 ),
2412 None => (false, now, false),
2413 };
2414 if !preview_has_window {
2415 break;
2416 }
2417 if !preview_can_send {
2418 let has_pending = sess
2419 .ptys
2420 .get(&pid)
2421 .map(pty_has_visual_update)
2422 .unwrap_or(false);
2423 if has_pending && preview_due_at > now {
2428 next_deadline = Some(match next_deadline {
2429 Some(existing) => existing.min(preview_due_at),
2430 None => preview_due_at,
2431 });
2432 }
2433 continue;
2434 }
2435 let Some(cur) = snapshots.get(&pid) else {
2436 let has_pending = sess
2437 .ptys
2438 .get(&pid)
2439 .map(pty_has_visual_update)
2440 .unwrap_or(false);
2441 let _ = has_pending;
2442 continue;
2443 };
2444 let cur = cur.clone();
2445 let previous = sess
2446 .clients
2447 .get(&cid)
2448 .and_then(|c| c.last_sent.get(&pid).cloned())
2449 .unwrap_or_default();
2450 drop(sess);
2451 let msg = build_update_msg(pid, &cur, &previous);
2452 sess = state.session.lock().await;
2453 let Some(c) = sess.clients.get_mut(&cid) else {
2454 break;
2455 };
2456 match try_send_update(c, pid, cur, msg, now, false) {
2457 SendOutcome::Sent => {
2458 record_preview_send(c, pid, now);
2459 }
2460 SendOutcome::Backpressured => {
2461 if let Some(pty) = sess.ptys.get_mut(&pid) {
2462 pty.mark_dirty();
2463 }
2464 break;
2465 }
2466 SendOutcome::NoChange => {}
2467 }
2468 }
2469 }
2470
2471 let mut invalidate_client_encoders: Vec<u16> = Vec::new();
2473
2474 let mut surface_commit_count = 0u32;
2475 if let Some(cs) = sess.compositor.as_mut() {
2476 let mut events = Vec::new();
2477 while let Ok(event) = cs.handle.event_rx.try_recv() {
2478 events.push(event);
2479 }
2480 let mut broadcast: Vec<Vec<u8>> = Vec::new();
2481 for event in events {
2482 match event {
2483 CompositorEvent::SurfaceCreated {
2484 surface_id,
2485 title,
2486 app_id,
2487 parent_id,
2488 width,
2489 height,
2490 } => {
2491 broadcast.push(msg_surface_created(
2492 surface_id, parent_id, width, height, &title, &app_id,
2493 ));
2494 cs.surfaces.insert(
2495 surface_id,
2496 CachedSurfaceInfo {
2497 surface_id,
2498 parent_id,
2499 width,
2500 height,
2501 title,
2502 app_id,
2503 },
2504 );
2505 cs.last_pixels.remove(&surface_id);
2506 invalidate_client_encoders.push(surface_id);
2507 }
2508 CompositorEvent::SurfaceDestroyed { surface_id } => {
2509 cs.surfaces.remove(&surface_id);
2510 cs.last_pixels.remove(&surface_id);
2511 cs.last_configured_size.remove(&surface_id);
2512 invalidate_client_encoders.push(surface_id);
2513 broadcast.push(msg_surface_destroyed(surface_id));
2514 }
2515 CompositorEvent::SurfaceCommit {
2516 surface_id,
2517 width,
2518 height,
2519 pixels,
2520 } => {
2521 surface_commit_count += 1;
2522 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
2523 info.width = width as u16;
2524 info.height = height as u16;
2525 }
2526 cs.pixel_generation += 1;
2527 cs.last_pixels.insert(
2528 surface_id,
2529 LastPixels {
2530 width,
2531 height,
2532 pixels,
2533 generation: cs.pixel_generation,
2534 },
2535 );
2536 }
2537 CompositorEvent::SurfaceTitle { surface_id, title } => {
2538 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
2539 info.title = title.clone();
2540 }
2541 broadcast.push(msg_surface_title(surface_id, &title));
2542 }
2543 CompositorEvent::SurfaceAppId { surface_id, app_id } => {
2544 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
2545 info.app_id = app_id.clone();
2546 }
2547 broadcast.push(msg_surface_app_id(surface_id, &app_id));
2548 }
2549 CompositorEvent::SurfaceResized {
2550 surface_id,
2551 width,
2552 height,
2553 } => {
2554 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
2555 info.width = width;
2556 info.height = height;
2557 }
2558 cs.last_pixels.remove(&surface_id);
2559 invalidate_client_encoders.push(surface_id);
2560 broadcast.push(msg_surface_resized(surface_id, width, height));
2561 }
2562 CompositorEvent::ClipboardContent {
2563 mime_type, data, ..
2564 } => {
2565 broadcast.push(msg_s2c_clipboard_content(&mime_type, &data));
2566 }
2567 CompositorEvent::SurfaceCursor { surface_id, cursor } => {
2568 let mut msg = Vec::new();
2573 msg.push(blit_remote::S2C_SURFACE_CURSOR);
2574 msg.extend_from_slice(&surface_id.to_le_bytes());
2575 match &cursor {
2576 blit_compositor::CursorImage::Named(name) => {
2577 msg.push(0); msg.push(name.len() as u8);
2579 msg.extend_from_slice(name.as_bytes());
2580 }
2581 blit_compositor::CursorImage::Hidden => {
2582 msg.push(1); }
2584 blit_compositor::CursorImage::Custom {
2585 hotspot_x,
2586 hotspot_y,
2587 width,
2588 height,
2589 rgba,
2590 } => {
2591 let mut png_buf = Vec::new();
2593 {
2594 let mut encoder =
2595 png::Encoder::new(&mut png_buf, *width as u32, *height as u32);
2596 encoder.set_color(png::ColorType::Rgba);
2597 encoder.set_depth(png::BitDepth::Eight);
2598 if let Ok(mut writer) = encoder.write_header() {
2599 let _ = writer.write_image_data(rgba);
2600 }
2601 }
2602 msg.push(2); msg.extend_from_slice(&hotspot_x.to_le_bytes());
2604 msg.extend_from_slice(&hotspot_y.to_le_bytes());
2605 msg.extend_from_slice(&width.to_le_bytes());
2606 msg.extend_from_slice(&height.to_le_bytes());
2607 msg.extend_from_slice(&png_buf);
2608 }
2609 }
2610 broadcast.push(msg);
2611 }
2612 }
2613 }
2614 for msg in &broadcast {
2615 sess.send_to_all(msg);
2616 }
2617 }
2618 sess.surface_commits += surface_commit_count;
2619
2620 for sid in invalidate_client_encoders {
2623 for c in sess.clients.values_mut() {
2624 c.surface_encoders.remove(&sid);
2625 c.surface_last_encoded_gen.remove(&sid);
2626 }
2627 }
2628
2629 let pixel_snapshot: Vec<(u16, u32, u32, u64)> = sess
2637 .compositor
2638 .as_ref()
2639 .map(|cs| {
2640 cs.last_pixels
2641 .iter()
2642 .map(|(&sid, lp)| (sid, lp.width, lp.height, lp.generation))
2643 .collect()
2644 })
2645 .unwrap_or_default();
2646
2647 struct EncodeJob {
2653 cid: u64,
2654 sid: u16,
2655 px_w: u32,
2656 px_h: u32,
2657 pixels: blit_compositor::PixelData,
2658 needs_keyframe: bool,
2659 encoder: SurfaceEncoder,
2660 generation: u64,
2661 }
2662 struct EncodeResult {
2663 cid: u64,
2664 sid: u16,
2665 px_w: u32,
2666 px_h: u32,
2667 generation: u64,
2668 encoder: SurfaceEncoder,
2669 nal_data: Option<(Vec<u8>, bool)>, codec_flag: u8,
2671 }
2672
2673 let mut encode_jobs: Vec<EncodeJob> = Vec::new();
2674
2675 struct ClientWork {
2678 cid: u64,
2679 subs: HashSet<u16>,
2680 needs_keyframe: bool,
2681 }
2682 let mut client_work: Vec<ClientWork> = Vec::new();
2683
2684 if !pixel_snapshot.is_empty() {
2685 for (&cid, client) in sess.clients.iter_mut() {
2686 if !surface_window_open(client) {
2687 continue;
2688 }
2689 if client.surface_burst_remaining == 0 && client.surface_next_send_at > now {
2695 let deadline = client.surface_next_send_at;
2696 next_deadline = Some(match next_deadline {
2697 Some(existing) => existing.min(deadline),
2698 None => deadline,
2699 });
2700 continue;
2701 }
2702 if client.surface_subscriptions.is_empty() {
2703 continue;
2704 }
2705 client_work.push(ClientWork {
2706 cid,
2707 subs: client.surface_subscriptions.clone(),
2708 needs_keyframe: client.surface_needs_keyframe,
2709 });
2710 }
2715
2716 let mut clients_with_encodes: HashSet<u64> = HashSet::new();
2719 #[cfg(target_os = "linux")]
2720 let mut pending_external_bufs: Option<Vec<blit_compositor::ExternalOutputBuffer>> = None;
2721
2722 for work in &client_work {
2723 for &(sid, px_w, px_h, px_gen) in &pixel_snapshot {
2724 if !work.subs.contains(&sid) {
2725 continue;
2726 }
2727 let client = sess.clients.get_mut(&work.cid).unwrap();
2728
2729 if !work.needs_keyframe
2733 && let Some(&last_gen) = client.surface_last_encoded_gen.get(&sid)
2734 && last_gen == px_gen
2735 {
2736 continue;
2737 }
2738
2739 let pixels = {
2740 let cs = sess.compositor.as_ref().unwrap();
2741 match cs.last_pixels.get(&sid) {
2742 Some(lp) if lp.width == px_w && lp.height == px_h => lp.pixels.clone(),
2743 _ => continue,
2744 }
2745 };
2746 let client = sess.clients.get_mut(&work.cid).unwrap();
2747
2748 if client.surface_encodes_in_flight.contains(&sid) {
2754 continue;
2755 }
2756
2757 let needs_new_encoder = client
2758 .surface_encoders
2759 .get(&sid)
2760 .is_none_or(|e| e.source_dimensions() != (px_w, px_h));
2761 if needs_new_encoder {
2762 client.surface_encoders.remove(&sid);
2763 let codec_support = client
2764 .surface_codec_overrides
2765 .get(&sid)
2766 .copied()
2767 .unwrap_or(client.surface_codec_support);
2768 let quality = client
2769 .surface_quality_overrides
2770 .get(&sid)
2771 .copied()
2772 .unwrap_or(state.config.surface_quality);
2773 match SurfaceEncoder::new(
2774 &state.config.surface_encoders,
2775 px_w,
2776 px_h,
2777 &state.config.vaapi_device,
2778 quality,
2779 state.config.verbose,
2780 codec_support,
2781 ) {
2782 Ok(encoder) => {
2783 #[cfg(target_os = "linux")]
2784 {
2785 let exported = encoder.export_vpp_surfaces();
2786 if !exported.is_empty() {
2787 let va_display = encoder.va_display_usize();
2788 let bufs = exported
2789 .into_iter()
2790 .map(|e| blit_compositor::ExternalOutputBuffer {
2791 fd: std::sync::Arc::new(e.fd),
2792 fourcc: e.fourcc,
2793 modifier: e.modifier,
2794 stride: e.stride,
2795 offset: e.offset,
2796 width: e.width,
2797 height: e.height,
2798 va_surface_id: e.surface_id,
2799 va_display,
2800 })
2801 .collect();
2802 pending_external_bufs = Some(bufs);
2803 }
2804 }
2805 let enc_msg = msg_surface_encoder(sid, encoder.encoder_name());
2806 client.surface_encoders.insert(sid, encoder);
2807 let _ = try_send_outbox(client, enc_msg);
2808 }
2809 Err(err) => {
2810 if state.config.verbose {
2811 eprintln!(
2812 "[surface-encoder] cid={} sid={sid} {px_w}x{px_h}: {err}",
2813 work.cid
2814 );
2815 }
2816 continue;
2817 }
2818 }
2819 }
2820
2821 let encoder = client.surface_encoders.remove(&sid).unwrap();
2822 client.surface_encodes_in_flight.insert(sid);
2823 let needs_kf = work.needs_keyframe || needs_new_encoder;
2826 clients_with_encodes.insert(work.cid);
2827 encode_jobs.push(EncodeJob {
2828 cid: work.cid,
2829 sid,
2830 px_w,
2831 px_h,
2832 pixels,
2833 needs_keyframe: needs_kf,
2834 encoder,
2835 generation: px_gen,
2836 });
2837 }
2838 }
2839
2840 #[cfg(target_os = "linux")]
2842 if let Some(bufs) = pending_external_bufs
2843 && let Some(cs) = sess.compositor.as_ref()
2844 {
2845 let _ = cs.handle.command_tx.send(
2846 blit_compositor::CompositorCommand::SetExternalOutputBuffers { buffers: bufs },
2847 );
2848 cs.handle.wake();
2849 }
2850
2851 for work in &client_work {
2856 if let Some(client) = sess.clients.get_mut(&work.cid)
2857 && clients_with_encodes.contains(&work.cid)
2858 {
2859 let interval = surface_send_interval(client);
2860 advance_deadline(&mut client.surface_next_send_at, now, interval);
2861 }
2862 }
2863 }
2864
2865 if !encode_jobs.is_empty() {
2866 let state2 = state.clone();
2869 tokio::spawn(async move {
2870 let job_ids: Vec<(u64, u16)> = encode_jobs.iter().map(|j| (j.cid, j.sid)).collect();
2873
2874 let handles: Vec<_> = encode_jobs
2875 .into_iter()
2876 .map(|mut job| {
2877 tokio::task::spawn_blocking(move || {
2878 if job.needs_keyframe {
2879 job.encoder.request_keyframe();
2880 }
2881 let nal_data = job.encoder.encode_pixels(&job.pixels);
2882 let codec_flag = job.encoder.codec_flag();
2883 EncodeResult {
2884 cid: job.cid,
2885 sid: job.sid,
2886 px_w: job.px_w,
2887 px_h: job.px_h,
2888 generation: job.generation,
2889 encoder: job.encoder,
2890 nal_data,
2891 codec_flag,
2892 }
2893 })
2894 })
2895 .collect();
2896
2897 const ENCODE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
2900
2901 let mut results = Vec::with_capacity(handles.len());
2902 let mut failed: Vec<(u64, u16)> = Vec::new();
2903 for (i, h) in handles.into_iter().enumerate() {
2904 match tokio::time::timeout(ENCODE_TIMEOUT, h).await {
2905 Ok(Ok(r)) => results.push(r),
2906 Ok(Err(_join_err)) => {
2907 eprintln!(
2909 "[surface-encoder] encode task panicked: cid={} sid={}",
2910 job_ids[i].0, job_ids[i].1
2911 );
2912 failed.push(job_ids[i]);
2913 }
2914 Err(_timeout) => {
2915 eprintln!(
2919 "[surface-encoder] encode timed out ({}s): cid={} sid={}",
2920 ENCODE_TIMEOUT.as_secs(),
2921 job_ids[i].0,
2922 job_ids[i].1
2923 );
2924 failed.push(job_ids[i]);
2925 }
2926 }
2927 }
2928
2929 let mut sess = state2.session.lock().await;
2931 let now = Instant::now();
2932 let mut local_encodes = 0u32;
2933 let mut local_encode_bytes = 0u64;
2934 let mut local_frames_sent = 0u32;
2935
2936 for (cid, sid) in failed {
2940 if let Some(client) = sess.clients.get_mut(&cid) {
2941 client.surface_encodes_in_flight.remove(&sid);
2942 client.surface_needs_keyframe = true;
2948 }
2949 }
2950
2951 for result in results {
2952 let dims_match = sess
2960 .compositor
2961 .as_ref()
2962 .and_then(|cs| cs.last_pixels.get(&result.sid))
2963 .is_some_and(|lp| result.encoder.source_dimensions() == (lp.width, lp.height));
2964 if let Some(client) = sess.clients.get_mut(&result.cid) {
2965 client.surface_encodes_in_flight.remove(&result.sid);
2966 if dims_match {
2967 client.surface_encoders.insert(result.sid, result.encoder);
2968 }
2969 client
2972 .surface_last_encoded_gen
2973 .insert(result.sid, result.generation);
2974 }
2975
2976 let Some((nal_data, is_keyframe)) = result.nal_data else {
2977 continue;
2978 };
2979
2980 local_encodes += 1;
2981 local_encode_bytes += nal_data.len() as u64;
2982
2983 let created_at = sess
2984 .compositor
2985 .as_ref()
2986 .map(|cs| cs.created_at)
2987 .unwrap_or(now);
2988
2989 let flags = result.codec_flag
2990 | if is_keyframe {
2991 SURFACE_FRAME_FLAG_KEYFRAME
2992 } else {
2993 0
2994 };
2995 let timestamp = created_at.elapsed().as_millis() as u32;
2996 let msg = msg_surface_frame(
2997 result.sid,
2998 timestamp,
2999 flags,
3000 result.px_w as u16,
3001 result.px_h as u16,
3002 &nal_data,
3003 );
3004 let bytes = msg.len();
3005
3006 let Some(client) = sess.clients.get_mut(&result.cid) else {
3007 continue;
3008 };
3009
3010 match try_send_outbox(client, msg) {
3017 Err(_e) => {
3018 client.surface_needs_keyframe = true;
3022 }
3023 Ok(()) => {
3024 client.surface_inflight_frames.push_back(InFlightFrame {
3028 sent_at: now,
3029 bytes,
3030 paced: true,
3031 });
3032 if !is_keyframe {
3044 client.avg_surface_frame_bytes = ewma_with_direction(
3045 client.avg_surface_frame_bytes,
3046 bytes as f32,
3047 0.5,
3048 0.125,
3049 );
3050 } else if client.avg_surface_frame_bytes <= 16_384.0 {
3051 client.avg_surface_frame_bytes = (bytes as f32 / 4.0).max(4_096.0);
3060 } else {
3061 client.avg_surface_frame_bytes = ewma_with_direction(
3064 client.avg_surface_frame_bytes,
3065 bytes as f32,
3066 0.05,
3067 0.05,
3068 );
3069 }
3070 client.frames_sent = client.frames_sent.wrapping_add(1);
3071 local_frames_sent += 1;
3072 if client.surface_needs_keyframe && is_keyframe {
3073 client.surface_needs_keyframe = false;
3074 }
3075 client.surface_burst_remaining =
3076 client.surface_burst_remaining.saturating_sub(1);
3077 }
3078 }
3079 }
3080 sess.surface_encodes += local_encodes;
3081 sess.surface_encode_bytes += local_encode_bytes;
3082 sess.surface_frames_sent += local_frames_sent;
3083 drop(sess);
3084 state2.delivery_notify.notify_one();
3086 });
3087 }
3088
3089 {
3100 let mut wanted: HashSet<u16> = HashSet::new();
3106 let mut blanket_requested = false;
3107 if let Some(cs) = sess.compositor.as_ref()
3112 && now.duration_since(cs.last_blanket_frame_request) >= BLANKET_FRAME_INTERVAL
3113 {
3114 for &sid in cs.surfaces.keys() {
3115 wanted.insert(sid);
3116 }
3117 blanket_requested = true;
3118 }
3119 for client in sess.clients.values() {
3120 if !surface_window_open(client) {
3121 continue;
3122 }
3123 let surface_ready = client.surface_next_send_at <= now;
3124 if !surface_ready {
3125 let deadline = client.surface_next_send_at;
3128 next_deadline = Some(match next_deadline {
3129 Some(existing) => existing.min(deadline),
3130 None => deadline,
3131 });
3132 continue;
3133 }
3134 for &sid in &client.surface_subscriptions {
3135 wanted.insert(sid);
3136 }
3137 }
3138
3139 if let Some(cs) = sess.compositor.as_mut() {
3140 if blanket_requested {
3141 cs.last_blanket_frame_request = now;
3142 }
3143
3144 const MIN_REQUEST_INTERVAL: Duration = Duration::from_millis(1);
3151 let mut sent_any = false;
3152 for sid in &wanted {
3153 let dominated = cs
3154 .last_frame_request
3155 .get(sid)
3156 .is_some_and(|&t| now.duration_since(t) < MIN_REQUEST_INTERVAL);
3157 if !dominated {
3158 cs.last_frame_request.insert(*sid, now);
3159 let _ = cs
3160 .handle
3161 .command_tx
3162 .send(CompositorCommand::RequestFrame { surface_id: *sid });
3163 sent_any = true;
3164 }
3165 }
3166 if sent_any {
3167 cs.handle.wake();
3168 }
3169 }
3170 }
3171
3172 #[cfg(unix)]
3174 if let Some(ref mut cs) = sess.compositor
3175 && let Some(ref mut ap) = cs.audio_pipeline
3176 && ap.is_alive()
3177 {
3178 let new_frames = ap.poll_frames();
3179 if !new_frames.is_empty() {
3180 for client in sess.clients.values_mut() {
3181 if client.audio_subscribed {
3182 for frame in &new_frames {
3183 let msg = audio::msg_audio_frame(frame);
3184 let bytes = msg.len();
3185 if client.audio_tx.try_send(msg).is_ok() {
3190 client.goodput_window_bytes += bytes;
3194 }
3195 }
3196 }
3197 }
3198 }
3199 let next_audio = now + Duration::from_millis(20);
3202 next_deadline = Some(next_deadline.map_or(next_audio, |d: Instant| d.min(next_audio)));
3203 }
3204
3205 #[cfg(unix)]
3213 let audio_restart_bitrate: i32 = sess
3214 .clients
3215 .values()
3216 .filter(|c| c.audio_subscribed)
3217 .map(|c| c.audio_bitrate_kbps)
3218 .max()
3219 .map(|kbps| kbps as i32 * 1000)
3220 .unwrap_or(0);
3221 #[cfg(unix)]
3222 if let Some(ref mut cs) = sess.compositor {
3223 let pipeline_dead = cs.audio_pipeline.as_mut().is_some_and(|ap| !ap.is_alive());
3224 if pipeline_dead {
3225 const RESTART_COOLDOWN: Duration = Duration::from_secs(5);
3226 let can_restart = cs
3227 .last_audio_restart
3228 .is_none_or(|t| now.duration_since(t) >= RESTART_COOLDOWN);
3229 if can_restart {
3230 cs.last_audio_restart = Some(now);
3231 cs.audio_pipeline = None;
3234 let runtime_dir = std::path::Path::new(&cs.handle.socket_name)
3235 .parent()
3236 .unwrap_or(std::path::Path::new("/tmp"));
3237 let session_id = cs.audio_session_id;
3238 let epoch = cs.created_at;
3239 let verbose = state.config.verbose;
3240 eprintln!("[audio] pipeline died, restarting...");
3241 let pipeline = tokio::task::block_in_place(|| {
3242 audio::AudioPipeline::spawn(
3243 runtime_dir,
3244 session_id,
3245 audio_restart_bitrate,
3246 verbose,
3247 epoch,
3248 )
3249 });
3250 match pipeline {
3251 Ok(p) => {
3252 eprintln!(
3253 "[audio] pipeline restarted, PULSE_SERVER={}",
3254 p.pulse_server_path(),
3255 );
3256 cs.audio_pipeline = Some(p);
3257 }
3258 Err(e) => {
3259 eprintln!("[audio] failed to restart pipeline: {e}");
3260 }
3261 }
3262 }
3263 }
3264 }
3265
3266 {
3272 let blanket_deadline = now + BLANKET_FRAME_INTERVAL;
3273 next_deadline = Some(next_deadline.map_or(blanket_deadline, |d| d.min(blanket_deadline)));
3274 }
3275
3276 TickOutcome { next_deadline }
3277}
3278
3279async fn handle_client<S: AsyncRead + AsyncWrite + Unpin + Send + 'static>(
3280 stream: S,
3281 state: AppState,
3282) {
3283 let config = &state.config;
3284 let notify_for_compositor = {
3285 let n = state.delivery_notify.clone();
3286 Arc::new(move || n.notify_one()) as Arc<dyn Fn() + Send + Sync>
3287 };
3288 let (mut reader, mut writer) = tokio::io::split(stream);
3289
3290 let (out_tx, mut out_rx) = mpsc::channel::<Vec<u8>>(OUTBOX_CAPACITY);
3291 let (audio_tx, mut audio_rx) = mpsc::channel::<Vec<u8>>(AUDIO_OUTBOX_CAPACITY);
3292 let outbox_frame_counter = Arc::new(AtomicUsize::new(0));
3293 let outbox_byte_counter = Arc::new(AtomicUsize::new(0));
3294 let sender_outbox_queued_frames = outbox_frame_counter.clone();
3295 let sender_outbox_queued_bytes = outbox_byte_counter.clone();
3296 let sender = tokio::spawn(async move {
3297 loop {
3298 while let Ok(audio_msg) = audio_rx.try_recv() {
3301 if !write_frame(&mut writer, &audio_msg).await {
3302 return;
3303 }
3304 }
3305
3306 let msg = tokio::select! {
3310 biased;
3311 msg = audio_rx.recv() => {
3312 match msg {
3314 Some(m) => {
3315 if !write_frame(&mut writer, &m).await {
3316 break;
3317 }
3318 continue;
3319 }
3320 None => break,
3321 }
3322 }
3323 msg = out_rx.recv() => msg,
3324 };
3325
3326 match msg {
3331 Some(m) => {
3332 let bytes = m.len();
3333 let wrote = write_frame_interleaved(&mut writer, &m, &mut audio_rx).await;
3334 mark_outbox_drained(
3335 &sender_outbox_queued_frames,
3336 &sender_outbox_queued_bytes,
3337 bytes,
3338 );
3339 if !wrote {
3340 break;
3341 }
3342 }
3343 None => break,
3344 }
3345 }
3346 });
3347 let client_id;
3348
3349 {
3350 let mut sess = state.session.lock().await;
3351 client_id = sess.next_client_id;
3352 sess.next_client_id += 1;
3353 sess.clients.insert(
3354 client_id,
3355 ClientState {
3356 tx: out_tx,
3357 outbox_queued_frames: outbox_frame_counter,
3358 outbox_queued_bytes: outbox_byte_counter,
3359 audio_tx,
3360 lead: None,
3361 subscriptions: HashSet::new(),
3362 surface_subscriptions: HashSet::new(),
3363 audio_subscribed: false,
3364 #[cfg(unix)]
3365 audio_bitrate_kbps: 0,
3366 view_sizes: HashMap::new(),
3367 scroll_offsets: HashMap::new(),
3368 scroll_caches: HashMap::new(),
3369 last_sent: HashMap::new(),
3370 preview_next_send_at: HashMap::new(),
3371 rtt_ms: 50.0,
3372 min_rtt_ms: 0.0,
3373 display_fps: 60.0,
3374 delivery_bps: 262_144.0,
3379 goodput_bps: 262_144.0,
3380 goodput_jitter_bps: 0.0,
3381 max_goodput_jitter_bps: 0.0,
3382 last_goodput_sample_bps: 0.0,
3383 avg_frame_bytes: 1_024.0,
3384 avg_paced_frame_bytes: 1_024.0,
3385 avg_preview_frame_bytes: 1_024.0,
3386 avg_surface_frame_bytes: 8_192.0,
3387 inflight_bytes: 0,
3388 inflight_frames: VecDeque::new(),
3389 next_send_at: Instant::now(),
3390 probe_frames: 0.0,
3391 frames_sent: 0,
3392 acks_recv: 0,
3393 acked_bytes_since_log: 0,
3394 browser_backlog_frames: 0,
3395 browser_ack_ahead_frames: 0,
3396 browser_apply_ms: 0.0,
3397 last_metrics_update: Instant::now(),
3398 last_log: Instant::now(),
3399 goodput_window_bytes: 0,
3400 goodput_window_start: Instant::now(),
3401 surface_next_send_at: Instant::now(),
3402 surface_needs_keyframe: true,
3403 surface_burst_remaining: SURFACE_BURST_FRAMES,
3404 surface_inflight_frames: VecDeque::new(),
3405 surface_encoders: HashMap::new(),
3406 surface_encodes_in_flight: HashSet::new(),
3407 surface_last_encoded_gen: HashMap::new(),
3408 surface_view_sizes: HashMap::new(),
3409 surface_codec_support: 0,
3410 surface_codec_overrides: HashMap::new(),
3411 surface_quality_overrides: HashMap::new(),
3412 pressed_surface_keys: HashSet::new(),
3413 },
3414 );
3415 state.delivery_notify.notify_one();
3417 if let Some(c) = sess.clients.get(&client_id) {
3418 let mut features = FEATURE_CREATE_NONCE
3419 | FEATURE_RESTART
3420 | FEATURE_RESIZE_BATCH
3421 | FEATURE_COPY_RANGE
3422 | FEATURE_COMPOSITOR;
3423 #[cfg(unix)]
3424 {
3425 let audio_disabled = std::env::var("BLIT_AUDIO")
3426 .map(|v| v == "0")
3427 .unwrap_or(false);
3428 if !audio_disabled && audio::pipewire_available() {
3429 features |= FEATURE_AUDIO;
3430 }
3431 }
3432 let _ = try_send_outbox(c, msg_hello(1, features));
3433 }
3434 let mut initial_msgs = Vec::with_capacity(2 + sess.ptys.len() * 2);
3435 if let Some(cs) = sess.compositor.as_ref() {
3440 for info in cs.surfaces.values() {
3441 let (w, h) = if info.width == 0 && info.height == 0 {
3444 cs.last_pixels
3445 .get(&info.surface_id)
3446 .map(|lp| (lp.width as u16, lp.height as u16))
3447 .unwrap_or((0, 0))
3448 } else {
3449 (info.width, info.height)
3450 };
3451 initial_msgs.push(msg_surface_created(
3452 info.surface_id,
3453 info.parent_id,
3454 w,
3455 h,
3456 &info.title,
3457 &info.app_id,
3458 ));
3459 if w > 0 && h > 0 {
3462 initial_msgs.push(msg_surface_resized(info.surface_id, w, h));
3463 }
3464 }
3465 }
3466 initial_msgs.push(sess.pty_list_msg());
3467 for (&id, pty) in &sess.ptys {
3468 let title = pty.driver.title();
3469 if !title.is_empty() {
3470 let title_bytes = title.as_bytes();
3471 let mut msg = Vec::with_capacity(3 + title_bytes.len());
3472 msg.push(S2C_TITLE);
3473 msg.extend_from_slice(&id.to_le_bytes());
3474 msg.extend_from_slice(title_bytes);
3475 initial_msgs.push(msg);
3476 }
3477 if pty.exited {
3478 initial_msgs.push(blit_remote::msg_exited(id, pty.exit_status));
3479 }
3480 }
3481 initial_msgs.push(vec![S2C_READY]);
3482 let tx = sess.clients.get(&client_id).map(|c| {
3483 (
3484 c.tx.clone(),
3485 c.outbox_queued_frames.clone(),
3486 c.outbox_queued_bytes.clone(),
3487 )
3488 });
3489 drop(sess);
3490 if let Some((tx, queued_frames, queued_bytes)) = tx {
3491 for msg in initial_msgs {
3492 if send_outbox_tracked(&tx, &queued_frames, &queued_bytes, msg)
3493 .await
3494 .is_err()
3495 {
3496 break;
3497 }
3498 }
3499 }
3500 }
3501
3502 if state.config.verbose {
3503 eprintln!("client connected");
3504 }
3505
3506 while let Some(data) = read_frame(&mut reader).await {
3507 if data.is_empty() {
3508 continue;
3509 }
3510
3511 if data[0] == C2S_ACK {
3512 let mut sess = state.session.lock().await;
3513 let (
3514 do_log,
3515 frames_sent,
3516 acks_recv,
3517 rtt_ms,
3518 min_rtt_ms,
3519 eff_rtt_ms,
3520 inflight_bytes,
3521 delivery_bps,
3522 goodput_ewma_bps,
3523 goodput_jitter_bps,
3524 max_goodput_jitter_bps,
3525 avg_frame_bytes,
3526 avg_paced_frame_bytes,
3527 avg_preview_frame_bytes,
3528 display_fps,
3529 paced_fps,
3530 display_need_bps,
3531 probe_frames,
3532 goodput_bps,
3533 window_frames,
3534 window_bytes,
3535 outbox_frames,
3536 browser_backlog_frames,
3537 browser_ack_ahead_frames,
3538 browser_apply_ms,
3539 surface_fps,
3540 avg_surface_frame_bytes,
3541 ) = {
3542 let Some(c) = sess.clients.get_mut(&client_id) else {
3543 continue;
3544 };
3545 c.acks_recv += 1;
3546 record_ack(c);
3547 let do_log = c.last_log.elapsed().as_secs_f32() >= 10.0;
3548 let log_elapsed = c.last_log.elapsed().as_secs_f32().max(1.0e-3);
3549 let paced_fps = pacing_fps(c);
3550 let display_need_bps = display_need_bps(c);
3551 let surface_fps = surface_pacing_fps(c);
3552 let out = (
3553 do_log,
3554 c.frames_sent,
3555 c.acks_recv,
3556 c.rtt_ms,
3557 path_rtt_ms(c),
3558 window_rtt_ms(c),
3559 c.inflight_bytes,
3560 c.delivery_bps,
3561 c.goodput_bps,
3562 c.goodput_jitter_bps,
3563 c.max_goodput_jitter_bps,
3564 c.avg_frame_bytes,
3565 c.avg_paced_frame_bytes,
3566 c.avg_preview_frame_bytes,
3567 c.display_fps,
3568 paced_fps,
3569 display_need_bps,
3570 c.probe_frames,
3571 c.acked_bytes_since_log as f32 / log_elapsed,
3572 target_frame_window(c),
3573 target_byte_window(c),
3574 outbox_queued_frames(c),
3575 c.browser_backlog_frames,
3576 c.browser_ack_ahead_frames,
3577 c.browser_apply_ms,
3578 surface_fps,
3579 c.avg_surface_frame_bytes,
3580 );
3581 if do_log {
3582 c.frames_sent = 0;
3583 c.acks_recv = 0;
3584 c.acked_bytes_since_log = 0;
3585 c.last_log = Instant::now();
3586 }
3587 out
3588 };
3589 if do_log && config.verbose {
3590 let surf_info = sess.compositor.as_ref().map(|cs| {
3591 let surfaces = cs.surfaces.len();
3592 let pending = 0usize;
3593 let subs: usize = sess
3594 .clients
3595 .values()
3596 .map(|c| c.surface_subscriptions.len())
3597 .sum();
3598 (surfaces, pending, subs)
3599 });
3600 let (surf_count, surf_pending, surf_subs) = surf_info.unwrap_or((0, 0, 0));
3601 eprintln!(
3602 "client {client_id}: sent={frames_sent} acks={acks_recv} rtt={rtt_ms:.0}ms min_rtt={min_rtt_ms:.0}ms eff_rtt={eff_rtt_ms:.0}ms window={window_frames}f/{window_bytes}B probe={probe_frames:.0}f inflight={inflight_bytes}B outbox={outbox_frames}f goodput={goodput_bps:.0}B/s goodput_ewma={goodput_ewma_bps:.0}B/s jitter={goodput_jitter_bps:.0}/{max_goodput_jitter_bps:.0}B/s rate={delivery_bps:.0}B/s avg_frame={avg_frame_bytes:.0}B lead_frame={avg_paced_frame_bytes:.0}B preview_frame={avg_preview_frame_bytes:.0}B need={display_need_bps:.0}B/s display_fps={display_fps:.0} paced_fps={paced_fps:.0} surface_fps={surface_fps:.0} surface_frame={avg_surface_frame_bytes:.0}B backlog={browser_backlog_frames} ack_ahead={browser_ack_ahead_frames} apply={browser_apply_ms:.1}ms | tick_fires={} tick_snaps={} | surfaces={surf_count} subs={surf_subs} pending_req={surf_pending} commits={} encodes={} enc_bytes={} surf_sent={}",
3603 sess.tick_fires,
3604 sess.tick_snaps,
3605 sess.surface_commits,
3606 sess.surface_encodes,
3607 sess.surface_encode_bytes,
3608 sess.surface_frames_sent,
3609 );
3610 }
3611 if do_log {
3612 sess.tick_fires = 0;
3613 sess.tick_snaps = 0;
3614 sess.surface_commits = 0;
3615 sess.surface_encodes = 0;
3616 sess.surface_encode_bytes = 0;
3617 sess.surface_frames_sent = 0;
3618 }
3619 nudge_delivery(&state);
3620 continue;
3621 }
3622
3623 if data[0] == C2S_PING {
3624 continue;
3628 }
3629
3630 if data[0] == C2S_DISPLAY_RATE && data.len() >= 3 {
3631 let fps = u16::from_le_bytes([data[1], data[2]]) as f32;
3632 if fps > 0.0 {
3633 let mut sess = state.session.lock().await;
3634 if let Some(c) = sess.clients.get_mut(&client_id) {
3635 c.display_fps = fps;
3636 }
3637 }
3638 nudge_delivery(&state);
3639 continue;
3640 }
3641
3642 if data[0] == C2S_CLIENT_METRICS && data.len() >= 7 {
3643 let backlog_frames = u16::from_le_bytes([data[1], data[2]]);
3644 let ack_ahead_frames = u16::from_le_bytes([data[3], data[4]]);
3645 let apply_ms = u16::from_le_bytes([data[5], data[6]]) as f32 * 0.1;
3646 let mut sess = state.session.lock().await;
3647 if let Some(c) = sess.clients.get_mut(&client_id) {
3648 c.browser_backlog_frames = backlog_frames;
3649 c.browser_ack_ahead_frames = ack_ahead_frames;
3650 c.browser_apply_ms = apply_ms;
3651 c.last_metrics_update = Instant::now();
3652 }
3653 nudge_delivery(&state);
3654 continue;
3655 }
3656
3657 if data[0] == C2S_MOUSE && data.len() >= 9 {
3660 let pid = u16::from_le_bytes([data[1], data[2]]);
3661 let type_ = data[3];
3662 let button = data[4];
3663 let col = u16::from_le_bytes([data[5], data[6]]);
3664 let row = u16::from_le_bytes([data[7], data[8]]);
3665 let sess = state.session.lock().await;
3666 if let Some(pty) = sess.ptys.get(&pid) {
3667 let (echo, icanon) = pty.lflag_cache;
3668 if let Some(seq) = pty
3669 .driver
3670 .mouse_event(type_, button, col, row, echo, icanon)
3671 && let Some(&fd) = state.pty_fds.read().unwrap().get(&pid)
3672 {
3673 pty::pty_write_all(fd, &seq);
3674 }
3675 }
3676 continue;
3677 }
3678
3679 if data[0] == C2S_INPUT && data.len() >= 3 {
3680 let pid = u16::from_le_bytes([data[1], data[2]]);
3681 let mut need_nudge = false;
3682 {
3683 let mut sess = state.session.lock().await;
3684 if let Some(c) = sess.clients.get_mut(&client_id)
3685 && update_client_scroll_state(c, pid, 0)
3686 && let Some(pty) = sess.ptys.get_mut(&pid)
3687 {
3688 pty.mark_dirty();
3689 need_nudge = true;
3690 }
3691 }
3692 if need_nudge {
3693 nudge_delivery(&state);
3694 }
3695 if let Some(&fd) = state.pty_fds.read().unwrap().get(&pid) {
3696 pty::pty_write_all(fd, &data[3..]);
3697 }
3698 continue;
3699 }
3700
3701 if data[0] == C2S_SEARCH && data.len() >= 3 {
3702 let request_id = u16::from_le_bytes([data[1], data[2]]);
3703 let query = std::str::from_utf8(&data[3..]).unwrap_or("").trim();
3704 let mut sess = state.session.lock().await;
3705 let lead = sess.clients.get(&client_id).and_then(|c| c.lead);
3706 let mut ranked: Vec<SearchResultRow> = if query.is_empty() {
3707 Vec::new()
3708 } else {
3709 sess.ptys
3710 .iter()
3711 .filter_map(|(&pty_id, pty)| {
3712 pty.driver
3713 .search_result(query)
3714 .map(|result| SearchResultRow {
3715 pty_id,
3716 score: result.score,
3717 primary_source: result.primary_source,
3718 matched_sources: result.matched_sources,
3719 context: result.context,
3720 scroll_offset: result.scroll_offset,
3721 })
3722 })
3723 .collect()
3724 };
3725 ranked.sort_by(|a, b| {
3726 b.score
3727 .cmp(&a.score)
3728 .then_with(|| (Some(b.pty_id) == lead).cmp(&(Some(a.pty_id) == lead)))
3729 .then_with(|| a.pty_id.cmp(&b.pty_id))
3730 });
3731 if let Some(client) = sess.clients.get_mut(&client_id) {
3732 let _ = try_send_outbox(client, build_search_results_msg(request_id, &ranked));
3733 }
3734 continue;
3735 }
3736
3737 if data[0] == C2S_SURFACE_CAPTURE && data.len() >= 3 {
3738 let surface_id = u16::from_le_bytes([data[1], data[2]]);
3739 let format = data.get(3).copied().unwrap_or(CAPTURE_FORMAT_PNG);
3741 let quality = data.get(4).copied().unwrap_or(0);
3742 let scale_120 = if data.len() >= 7 {
3743 u16::from_le_bytes([data[5], data[6]])
3744 } else {
3745 0
3746 };
3747
3748 let mut reply_msg = vec![S2C_SURFACE_CAPTURE];
3749 reply_msg.extend_from_slice(&surface_id.to_le_bytes());
3750
3751 eprintln!("[capture] acquiring lock for surface {surface_id}");
3752 let (snapshot, command_tx) = {
3753 let sess = state.session.lock().await;
3754 eprintln!("[capture] lock acquired");
3755 let snap = sess
3756 .compositor
3757 .as_ref()
3758 .and_then(|cs| cs.last_pixels.get(&surface_id))
3759 .map(|lp| (lp.width, lp.height, lp.pixels.clone()));
3760 let cmd_tx = sess
3761 .compositor
3762 .as_ref()
3763 .map(|cs| cs.handle.command_tx.clone());
3764 (snap, cmd_tx)
3765 };
3766
3767 let mut captured: Option<(u32, u32, Vec<u8>)> = None;
3772 if let Some(ctx) = command_tx {
3773 captured = request_surface_capture_with_timeout(
3774 ctx,
3775 surface_id,
3776 scale_120,
3777 Duration::from_secs(5),
3778 )
3779 .await;
3780 }
3781
3782 if captured.is_none() {
3785 captured = snapshot.and_then(|(w, h, pixels)| {
3786 if pixels.is_dmabuf() {
3787 return None;
3788 }
3789 let rgba = pixels.to_rgba(w, h);
3790 if rgba.is_empty() {
3791 None
3792 } else {
3793 Some((w, h, rgba))
3794 }
3795 });
3796 }
3797
3798 eprintln!("[capture] acquiring client_tx lock");
3799 let client_tx = {
3800 let sess = state.session.lock().await;
3801 eprintln!("[capture] client_tx lock acquired");
3802 sess.clients.get(&client_id).map(|c| {
3803 (
3804 c.tx.clone(),
3805 c.outbox_queued_frames.clone(),
3806 c.outbox_queued_bytes.clone(),
3807 )
3808 })
3809 };
3810
3811 if let Some((w, h, rgba_pixels)) = captured {
3812 let image_data = encode_capture(&rgba_pixels, w, h, format, quality);
3813 reply_msg.extend_from_slice(&w.to_le_bytes());
3814 reply_msg.extend_from_slice(&h.to_le_bytes());
3815 reply_msg.extend_from_slice(&image_data);
3816 } else {
3817 reply_msg.extend_from_slice(&0u32.to_le_bytes());
3818 reply_msg.extend_from_slice(&0u32.to_le_bytes());
3819 }
3820
3821 if let Some((client_tx, queued_frames, queued_bytes)) = client_tx {
3822 eprintln!("[capture] sending reply: {} bytes", reply_msg.len());
3823 match try_send_outbox_tracked(&client_tx, &queued_frames, &queued_bytes, reply_msg)
3824 {
3825 Ok(()) => eprintln!("[capture] sent OK"),
3826 Err(e) => eprintln!("[capture] try_send failed: {e}"),
3827 }
3828 } else {
3829 eprintln!("[capture] no client_tx");
3830 }
3831 continue;
3832 }
3833
3834 if data[0] == C2S_QUIT {
3835 let sess = state.session.lock().await;
3836 sess.send_to_all(&[S2C_QUIT]);
3837 drop(sess);
3838 state.shutdown_notify.notify_waiters();
3839 break;
3840 }
3841
3842 let mut sess = state.session.lock().await;
3843 let mut need_nudge = false;
3844 match data[0] {
3845 C2S_SCROLL if data.len() >= 7 => {
3846 let pid = u16::from_le_bytes([data[1], data[2]]);
3847 let offset = u32::from_le_bytes([data[3], data[4], data[5], data[6]]) as usize;
3848 if sess.ptys.contains_key(&pid) {
3849 if let Some(c) = sess.clients.get_mut(&client_id) {
3850 update_client_scroll_state(c, pid, offset);
3851 }
3852 if let Some(pty) = sess.ptys.get_mut(&pid) {
3853 pty.mark_dirty();
3854 need_nudge = true;
3855 }
3856 }
3857 }
3858 C2S_RESIZE if data.len() >= 7 => {
3859 let entries = data[1..].chunks_exact(6);
3860 if !entries.remainder().is_empty() {
3861 continue;
3862 }
3863 let mut touched = Vec::with_capacity((data.len() - 1) / 6);
3864 for entry in entries {
3865 let pid = u16::from_le_bytes([entry[0], entry[1]]);
3866 if !sess.ptys.contains_key(&pid) {
3867 continue;
3868 }
3869 let rows = u16::from_le_bytes([entry[2], entry[3]]);
3870 let cols = u16::from_le_bytes([entry[4], entry[5]]);
3871 if let Some(c) = sess.clients.get_mut(&client_id) {
3872 if is_unset_view_size(rows, cols) {
3873 if c.view_sizes.remove(&pid).is_some() {
3874 touched.push(pid);
3875 }
3876 } else if rows == 0 || cols == 0 {
3877 continue;
3878 } else {
3879 c.view_sizes.insert(pid, (rows, cols));
3880 touched.push(pid);
3881 }
3882 }
3883 }
3884 if sess.resize_ptys_to_mediated_sizes(touched) {
3885 need_nudge = true;
3886 }
3887 }
3888 C2S_CREATE => {
3889 let (rows, cols) = if data.len() >= 5 {
3891 (
3892 u16::from_le_bytes([data[1], data[2]]),
3893 u16::from_le_bytes([data[3], data[4]]),
3894 )
3895 } else {
3896 (24, 80)
3897 };
3898 let tag_len = if data.len() >= 7 {
3899 u16::from_le_bytes([data[5], data[6]]) as usize
3900 } else {
3901 0
3902 };
3903 let tag = if data.len() >= 7 + tag_len {
3904 std::str::from_utf8(&data[7..7 + tag_len]).unwrap_or_default()
3905 } else {
3906 ""
3907 };
3908 let cmd_start = 7 + tag_len;
3909 let dir: Option<String> = None;
3910 let create_payload = data
3911 .get(cmd_start..)
3912 .and_then(|bytes| std::str::from_utf8(bytes).ok());
3913 let command = create_payload
3914 .filter(|payload| !payload.contains('\0'))
3915 .map(str::trim)
3916 .filter(|payload| !payload.is_empty());
3917 let argv: Option<Vec<&str>> = create_payload
3918 .filter(|payload| payload.contains('\0'))
3919 .map(|payload| {
3920 payload
3921 .split('\0')
3922 .filter(|arg| !arg.is_empty())
3923 .collect::<Vec<_>>()
3924 })
3925 .filter(|args| !args.is_empty());
3926 let Some(id) = sess.allocate_pty_id(config.max_ptys) else {
3927 continue;
3928 };
3929 let socket_name = sess
3930 .ensure_compositor(
3931 config.verbose,
3932 notify_for_compositor.clone(),
3933 &config.vaapi_device,
3934 )
3935 .to_string();
3936 #[cfg(unix)]
3937 let pulse_server = sess.pulse_server_path();
3938 #[cfg(not(unix))]
3939 let pulse_server: Option<String> = None;
3940 #[cfg(unix)]
3941 let pipewire_remote = sess.pipewire_remote_path();
3942 #[cfg(not(unix))]
3943 let pipewire_remote: Option<String> = None;
3944 if let Some(pty) = pty::spawn_pty(
3945 &config.shell,
3946 &config.shell_flags,
3947 rows,
3948 cols,
3949 id,
3950 tag,
3951 command,
3952 argv.as_deref(),
3953 dir.as_deref(),
3954 config.scrollback,
3955 state.clone(),
3956 Some(&socket_name),
3957 pulse_server.as_deref(),
3958 pipewire_remote.as_deref(),
3959 ) {
3960 let mut msg = Vec::with_capacity(3 + pty.tag.len());
3961 msg.push(S2C_CREATED);
3962 msg.extend_from_slice(&id.to_le_bytes());
3963 msg.extend_from_slice(pty.tag.as_bytes());
3964 sess.ptys.insert(id, pty);
3965 if let Some(c) = sess.clients.get_mut(&client_id) {
3966 c.lead = Some(id);
3967 c.view_sizes.insert(id, (rows, cols));
3968 subscribe_client_to(c, id);
3969 reset_inflight(c);
3970 }
3971 sess.send_to_all(&msg);
3972 need_nudge = true;
3973 }
3974 }
3975 C2S_CREATE_N => {
3976 let nonce = if data.len() >= 3 {
3978 u16::from_le_bytes([data[1], data[2]])
3979 } else {
3980 0
3981 };
3982 let (rows, cols) = if data.len() >= 7 {
3983 (
3984 u16::from_le_bytes([data[3], data[4]]),
3985 u16::from_le_bytes([data[5], data[6]]),
3986 )
3987 } else {
3988 (24, 80)
3989 };
3990 let tag_len = if data.len() >= 9 {
3991 u16::from_le_bytes([data[7], data[8]]) as usize
3992 } else {
3993 0
3994 };
3995 let tag = if data.len() >= 9 + tag_len {
3996 std::str::from_utf8(&data[9..9 + tag_len]).unwrap_or_default()
3997 } else {
3998 ""
3999 };
4000 let cmd_start = 9 + tag_len;
4001 let dir: Option<String> = None;
4002 let create_payload = data
4003 .get(cmd_start..)
4004 .and_then(|bytes| std::str::from_utf8(bytes).ok());
4005 let command = create_payload
4006 .filter(|payload| !payload.contains('\0'))
4007 .map(str::trim)
4008 .filter(|payload| !payload.is_empty());
4009 let argv: Option<Vec<&str>> = create_payload
4010 .filter(|payload| payload.contains('\0'))
4011 .map(|payload| {
4012 payload
4013 .split('\0')
4014 .filter(|arg| !arg.is_empty())
4015 .collect::<Vec<_>>()
4016 })
4017 .filter(|args| !args.is_empty());
4018 let Some(id) = sess.allocate_pty_id(config.max_ptys) else {
4019 continue;
4020 };
4021 let socket_name = sess
4022 .ensure_compositor(
4023 config.verbose,
4024 notify_for_compositor.clone(),
4025 &config.vaapi_device,
4026 )
4027 .to_string();
4028 #[cfg(unix)]
4029 let pulse_server = sess.pulse_server_path();
4030 #[cfg(not(unix))]
4031 let pulse_server: Option<String> = None;
4032 #[cfg(unix)]
4033 let pipewire_remote = sess.pipewire_remote_path();
4034 #[cfg(not(unix))]
4035 let pipewire_remote: Option<String> = None;
4036 if let Some(pty) = pty::spawn_pty(
4037 &config.shell,
4038 &config.shell_flags,
4039 rows,
4040 cols,
4041 id,
4042 tag,
4043 command,
4044 argv.as_deref(),
4045 dir.as_deref(),
4046 config.scrollback,
4047 state.clone(),
4048 Some(&socket_name),
4049 pulse_server.as_deref(),
4050 pipewire_remote.as_deref(),
4051 ) {
4052 let tag_bytes = pty.tag.as_bytes();
4053 let mut nonce_msg = Vec::with_capacity(5 + tag_bytes.len());
4054 nonce_msg.push(S2C_CREATED_N);
4055 nonce_msg.extend_from_slice(&nonce.to_le_bytes());
4056 nonce_msg.extend_from_slice(&id.to_le_bytes());
4057 nonce_msg.extend_from_slice(tag_bytes);
4058 let mut broadcast_msg = Vec::with_capacity(3 + tag_bytes.len());
4059 broadcast_msg.push(S2C_CREATED);
4060 broadcast_msg.extend_from_slice(&id.to_le_bytes());
4061 broadcast_msg.extend_from_slice(tag_bytes);
4062 sess.ptys.insert(id, pty);
4063 if let Some(c) = sess.clients.get_mut(&client_id) {
4064 c.lead = Some(id);
4065 c.view_sizes.insert(id, (rows, cols));
4066 subscribe_client_to(c, id);
4067 reset_inflight(c);
4068 let _ = try_send_outbox(c, nonce_msg);
4069 }
4070 for (&cid, c) in sess.clients.iter() {
4071 if cid != client_id {
4072 let _ = try_send_outbox(c, broadcast_msg.clone());
4073 }
4074 }
4075 need_nudge = true;
4076 }
4077 }
4078 C2S_CREATE_AT => {
4079 let (rows, cols) = if data.len() >= 5 {
4081 (
4082 u16::from_le_bytes([data[1], data[2]]),
4083 u16::from_le_bytes([data[3], data[4]]),
4084 )
4085 } else {
4086 (24, 80)
4087 };
4088 let tag_len = if data.len() >= 7 {
4089 u16::from_le_bytes([data[5], data[6]]) as usize
4090 } else {
4091 0
4092 };
4093 let tag = if data.len() >= 7 + tag_len {
4094 std::str::from_utf8(&data[7..7 + tag_len]).unwrap_or_default()
4095 } else {
4096 ""
4097 };
4098 let src_start = 7 + tag_len;
4099 let dir = if data.len() >= src_start + 2 {
4100 let src_id = u16::from_le_bytes([data[src_start], data[src_start + 1]]);
4101 sess.ptys.get(&src_id).and_then(|p| pty::pty_cwd(&p.handle))
4102 } else {
4103 None
4104 };
4105 let Some(id) = sess.allocate_pty_id(config.max_ptys) else {
4106 continue;
4107 };
4108 let socket_name = sess
4109 .ensure_compositor(
4110 config.verbose,
4111 notify_for_compositor.clone(),
4112 &config.vaapi_device,
4113 )
4114 .to_string();
4115 #[cfg(unix)]
4116 let pulse_server = sess.pulse_server_path();
4117 #[cfg(not(unix))]
4118 let pulse_server: Option<String> = None;
4119 #[cfg(unix)]
4120 let pipewire_remote = sess.pipewire_remote_path();
4121 #[cfg(not(unix))]
4122 let pipewire_remote: Option<String> = None;
4123 if let Some(pty) = pty::spawn_pty(
4124 &config.shell,
4125 &config.shell_flags,
4126 rows,
4127 cols,
4128 id,
4129 tag,
4130 None,
4131 None,
4132 dir.as_deref(),
4133 config.scrollback,
4134 state.clone(),
4135 Some(&socket_name),
4136 pulse_server.as_deref(),
4137 pipewire_remote.as_deref(),
4138 ) {
4139 let mut msg = Vec::with_capacity(3 + pty.tag.len());
4140 msg.push(S2C_CREATED);
4141 msg.extend_from_slice(&id.to_le_bytes());
4142 msg.extend_from_slice(pty.tag.as_bytes());
4143 sess.ptys.insert(id, pty);
4144 if let Some(c) = sess.clients.get_mut(&client_id) {
4145 c.lead = Some(id);
4146 c.view_sizes.insert(id, (rows, cols));
4147 subscribe_client_to(c, id);
4148 reset_inflight(c);
4149 }
4150 sess.send_to_all(&msg);
4151 need_nudge = true;
4152 }
4153 }
4154 C2S_CREATE2 => {
4155 if data.len() < 10 {
4156 continue;
4157 }
4158 let nonce = u16::from_le_bytes([data[1], data[2]]);
4159 let rows = u16::from_le_bytes([data[3], data[4]]);
4160 let cols = u16::from_le_bytes([data[5], data[6]]);
4161 let features = data[7];
4162 let tag_len = u16::from_le_bytes([data[8], data[9]]) as usize;
4163 let tag = if data.len() >= 10 + tag_len {
4164 std::str::from_utf8(&data[10..10 + tag_len]).unwrap_or_default()
4165 } else {
4166 ""
4167 };
4168 let mut cursor = 10 + tag_len;
4169 let dir = if features & CREATE2_HAS_SRC_PTY != 0 && data.len() >= cursor + 2 {
4170 let src_id = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
4171 cursor += 2;
4172 sess.ptys.get(&src_id).and_then(|p| pty::pty_cwd(&p.handle))
4173 } else {
4174 None
4175 };
4176 let create_payload = if features & CREATE2_HAS_COMMAND != 0 {
4177 data.get(cursor..).and_then(|b| std::str::from_utf8(b).ok())
4178 } else {
4179 None
4180 };
4181 let command = create_payload
4182 .filter(|p| !p.contains('\0'))
4183 .map(str::trim)
4184 .filter(|p| !p.is_empty());
4185 let argv: Option<Vec<&str>> = create_payload
4186 .filter(|p| p.contains('\0'))
4187 .map(|p| p.split('\0').filter(|a| !a.is_empty()).collect::<Vec<_>>())
4188 .filter(|a| !a.is_empty());
4189 let Some(id) = sess.allocate_pty_id(config.max_ptys) else {
4190 continue;
4191 };
4192 let socket_name = sess
4193 .ensure_compositor(
4194 config.verbose,
4195 notify_for_compositor.clone(),
4196 &config.vaapi_device,
4197 )
4198 .to_string();
4199 #[cfg(unix)]
4200 let pulse_server = sess.pulse_server_path();
4201 #[cfg(not(unix))]
4202 let pulse_server: Option<String> = None;
4203 #[cfg(unix)]
4204 let pipewire_remote = sess.pipewire_remote_path();
4205 #[cfg(not(unix))]
4206 let pipewire_remote: Option<String> = None;
4207 if let Some(pty) = pty::spawn_pty(
4208 &config.shell,
4209 &config.shell_flags,
4210 rows,
4211 cols,
4212 id,
4213 tag,
4214 command,
4215 argv.as_deref(),
4216 dir.as_deref(),
4217 config.scrollback,
4218 state.clone(),
4219 Some(&socket_name),
4220 pulse_server.as_deref(),
4221 pipewire_remote.as_deref(),
4222 ) {
4223 let tag_bytes = pty.tag.as_bytes();
4224 let mut nonce_msg = Vec::with_capacity(5 + tag_bytes.len());
4225 nonce_msg.push(S2C_CREATED_N);
4226 nonce_msg.extend_from_slice(&nonce.to_le_bytes());
4227 nonce_msg.extend_from_slice(&id.to_le_bytes());
4228 nonce_msg.extend_from_slice(tag_bytes);
4229 let mut broadcast_msg = Vec::with_capacity(3 + tag_bytes.len());
4230 broadcast_msg.push(S2C_CREATED);
4231 broadcast_msg.extend_from_slice(&id.to_le_bytes());
4232 broadcast_msg.extend_from_slice(tag_bytes);
4233 sess.ptys.insert(id, pty);
4234 if let Some(c) = sess.clients.get_mut(&client_id) {
4235 c.lead = Some(id);
4236 c.view_sizes.insert(id, (rows, cols));
4237 subscribe_client_to(c, id);
4238 reset_inflight(c);
4239 let _ = try_send_outbox(c, nonce_msg);
4240 }
4241 for (&cid, c) in sess.clients.iter() {
4242 if cid != client_id {
4243 let _ = try_send_outbox(c, broadcast_msg.clone());
4244 }
4245 }
4246 need_nudge = true;
4247 }
4248 }
4249 C2S_SURFACE_INPUT if data.len() >= 8 => {
4250 let surface_id = u16::from_le_bytes([data[1], data[2]]);
4251 let keycode = u32::from_le_bytes([data[3], data[4], data[5], data[6]]);
4252 let pressed = data[7] != 0;
4253 if let Some(client) = sess.clients.get_mut(&client_id) {
4254 if pressed {
4255 client.pressed_surface_keys.insert(keycode);
4256 } else {
4257 client.pressed_surface_keys.remove(&keycode);
4258 }
4259 }
4260 if let Some(cs) = sess.compositor.as_mut() {
4261 let _ = cs.handle.command_tx.send(CompositorCommand::KeyInput {
4262 surface_id,
4263 keycode,
4264 pressed,
4265 });
4266 cs.handle.wake();
4267 state.delivery_notify.notify_one();
4268 }
4269 }
4270 C2S_SURFACE_TEXT if data.len() >= 3 => {
4271 let _surface_id = u16::from_le_bytes([data[1], data[2]]);
4272 if let Ok(text) = std::str::from_utf8(&data[3..])
4273 && let Some(cs) = sess.compositor.as_mut()
4274 {
4275 let _ = cs.handle.command_tx.send(CompositorCommand::TextInput {
4276 text: text.to_string(),
4277 });
4278 cs.handle.wake();
4279 state.delivery_notify.notify_one();
4280 }
4281 }
4282 C2S_SURFACE_POINTER if data.len() >= 9 => {
4283 let surface_id = u16::from_le_bytes([data[1], data[2]]);
4284 let ptype = data[3];
4285 let button = data[4];
4286 let x = u16::from_le_bytes([data[5], data[6]]) as f64;
4287 let y = u16::from_le_bytes([data[7], data[8]]) as f64;
4288 if let Some(cs) = sess.compositor.as_mut() {
4289 match ptype {
4290 0 | 1 => {
4291 let _ = cs.handle.command_tx.send(CompositorCommand::PointerMotion {
4292 surface_id,
4293 x,
4294 y,
4295 });
4296 let _ = cs.handle.command_tx.send(CompositorCommand::PointerButton {
4297 surface_id,
4298 button: match button {
4299 1 => 0x112,
4300 2 => 0x111,
4301 _ => 0x110,
4302 },
4303 pressed: ptype == 0,
4304 });
4305 }
4306 2 => {
4307 let _ = cs.handle.command_tx.send(CompositorCommand::PointerMotion {
4308 surface_id,
4309 x,
4310 y,
4311 });
4312 }
4313 _ => {}
4314 }
4315 cs.handle.wake();
4316 }
4317 state.delivery_notify.notify_one();
4318 }
4319 C2S_SURFACE_POINTER_AXIS if data.len() >= 8 => {
4320 let surface_id = u16::from_le_bytes([data[1], data[2]]);
4321 let axis = data[3];
4322 let value_x100 = i32::from_le_bytes([data[4], data[5], data[6], data[7]]);
4323 let value = value_x100 as f64 / 100.0;
4324 if let Some(cs) = sess.compositor.as_mut() {
4325 let _ = cs.handle.command_tx.send(CompositorCommand::PointerAxis {
4326 surface_id,
4327 axis,
4328 value,
4329 });
4330 cs.handle.wake();
4331 }
4332 state.delivery_notify.notify_one();
4333 }
4334 C2S_SURFACE_RESIZE if data.len() >= 9 => {
4335 let surface_id = u16::from_le_bytes([data[1], data[2]]);
4336 let width = u16::from_le_bytes([data[3], data[4]]);
4337 let height = u16::from_le_bytes([data[5], data[6]]);
4338 let scale_120 = u16::from_le_bytes([data[7], data[8]]);
4340 if state.config.verbose {
4341 eprintln!(
4342 "C2S_SURFACE_RESIZE: cid={client_id} sid={surface_id} {width}x{height} scale={scale_120}"
4343 );
4344 }
4345 if let Some(c) = sess.clients.get_mut(&client_id) {
4346 if is_unset_view_size(width, height) {
4347 c.surface_view_sizes.remove(&surface_id);
4348 } else if width > 0 && height > 0 {
4349 c.surface_view_sizes
4350 .insert(surface_id, (width, height, scale_120));
4351 }
4352 }
4353 sess.resize_surfaces_to_mediated_sizes(
4354 std::iter::once(surface_id),
4355 &state.config.surface_encoders,
4356 );
4357 }
4358 C2S_SURFACE_FOCUS if data.len() >= 3 => {
4359 let surface_id = u16::from_le_bytes([data[1], data[2]]);
4360 if state.config.verbose {
4361 eprintln!("C2S_SURFACE_FOCUS: cid={client_id} sid={surface_id}");
4362 }
4363 if let Some(cs) = sess.compositor.as_ref() {
4364 let _ = cs
4365 .handle
4366 .command_tx
4367 .send(CompositorCommand::SurfaceFocus { surface_id });
4368 }
4369 }
4370 C2S_SURFACE_CLOSE if data.len() >= 3 => {
4371 let surface_id = u16::from_le_bytes([data[1], data[2]]);
4372 if let Some(cs) = sess.compositor.as_ref() {
4373 let _ = cs
4374 .handle
4375 .command_tx
4376 .send(CompositorCommand::SurfaceClose { surface_id });
4377 cs.handle.wake();
4378 }
4379 }
4380 C2S_SURFACE_SUBSCRIBE if data.len() >= 3 => {
4381 let surface_id = u16::from_le_bytes([data[1], data[2]]);
4382 let codec_support = if data.len() >= 4 { data[3] } else { 0 };
4384 let quality_wire = if data.len() >= 5 { data[4] } else { 0 };
4385 if state.config.verbose {
4386 eprintln!(
4387 "C2S_SURFACE_SUBSCRIBE: cid={client_id} surface={surface_id} codec={codec_support:#04x} quality={quality_wire}"
4388 );
4389 }
4390 if let Some(c) = sess.clients.get_mut(&client_id) {
4391 let was_subscribed = !c.surface_subscriptions.insert(surface_id);
4392 c.surface_needs_keyframe = true;
4393 c.surface_burst_remaining = SURFACE_BURST_FRAMES;
4396
4397 let old_codec = c
4399 .surface_codec_overrides
4400 .get(&surface_id)
4401 .copied()
4402 .unwrap_or(0);
4403 let old_quality = c.surface_quality_overrides.get(&surface_id).copied();
4404 let new_quality = SurfaceQuality::from_wire(quality_wire);
4405
4406 if codec_support != 0 {
4407 c.surface_codec_overrides.insert(surface_id, codec_support);
4408 } else {
4409 c.surface_codec_overrides.remove(&surface_id);
4410 }
4411 if let Some(q) = new_quality {
4412 c.surface_quality_overrides.insert(surface_id, q);
4413 } else {
4414 c.surface_quality_overrides.remove(&surface_id);
4415 }
4416
4417 if was_subscribed && (codec_support != old_codec || new_quality != old_quality)
4419 {
4420 c.surface_encoders.remove(&surface_id);
4421 }
4422 }
4423 state.delivery_notify.notify_one();
4424 }
4425 C2S_SURFACE_UNSUBSCRIBE if data.len() >= 3 => {
4426 let surface_id = u16::from_le_bytes([data[1], data[2]]);
4427 if let Some(c) = sess.clients.get_mut(&client_id) {
4428 c.surface_subscriptions.remove(&surface_id);
4429 c.surface_view_sizes.remove(&surface_id);
4430 c.surface_codec_overrides.remove(&surface_id);
4431 c.surface_quality_overrides.remove(&surface_id);
4432 }
4433 sess.resize_surfaces_to_mediated_sizes(
4434 std::iter::once(surface_id),
4435 &state.config.surface_encoders,
4436 );
4437 }
4438 #[cfg(unix)]
4439 C2S_AUDIO_SUBSCRIBE if data.len() >= 3 => {
4440 let bitrate_kbps = u16::from_le_bytes([data[1], data[2]]);
4441 if let Some(c) = sess.clients.get_mut(&client_id) {
4442 c.audio_subscribed = true;
4443 c.audio_bitrate_kbps = bitrate_kbps;
4444 if state.config.verbose {
4445 eprintln!(
4446 "C2S_AUDIO_SUBSCRIBE: cid={client_id} bitrate_kbps={bitrate_kbps}"
4447 );
4448 }
4449 if let Some(cs) = sess.compositor.as_mut()
4451 && let Some(ref ap) = cs.audio_pipeline
4452 {
4453 let msgs: Vec<_> = ap.ring_frames().map(audio::msg_audio_frame).collect();
4454 if let Some(c) = sess.clients.get(&client_id) {
4455 for msg in msgs {
4456 let _ = c.audio_tx.try_send(msg);
4457 }
4458 }
4459 }
4460 }
4461 if let Some(cs) = sess.compositor.as_ref()
4464 && let Some(ref ap) = cs.audio_pipeline
4465 {
4466 let max_kbps = sess
4467 .clients
4468 .values()
4469 .filter(|c| c.audio_subscribed)
4470 .map(|c| c.audio_bitrate_kbps)
4471 .max()
4472 .unwrap_or(0);
4473 let bitrate = if max_kbps > 0 {
4474 max_kbps as i32 * 1000
4475 } else {
4476 audio::DEFAULT_BITRATE
4477 };
4478 ap.set_bitrate(bitrate);
4479 }
4480 state.delivery_notify.notify_one();
4481 }
4482 #[cfg(unix)]
4483 C2S_AUDIO_UNSUBSCRIBE if !data.is_empty() => {
4484 if let Some(c) = sess.clients.get_mut(&client_id) {
4485 c.audio_subscribed = false;
4486 c.audio_bitrate_kbps = 0;
4487 if state.config.verbose {
4488 eprintln!("C2S_AUDIO_UNSUBSCRIBE: cid={client_id}");
4489 }
4490 }
4491 if let Some(cs) = sess.compositor.as_ref()
4493 && let Some(ref ap) = cs.audio_pipeline
4494 {
4495 let max_kbps = sess
4496 .clients
4497 .values()
4498 .filter(|c| c.audio_subscribed)
4499 .map(|c| c.audio_bitrate_kbps)
4500 .max()
4501 .unwrap_or(0);
4502 let bitrate = if max_kbps > 0 {
4503 max_kbps as i32 * 1000
4504 } else {
4505 audio::DEFAULT_BITRATE
4506 };
4507 ap.set_bitrate(bitrate);
4508 }
4509 }
4510 C2S_SURFACE_ACK if data.len() >= 3 => {
4511 if let Some(c) = sess.clients.get_mut(&client_id) {
4515 c.acks_recv += 1;
4516 record_surface_ack(c);
4517 }
4518 state.delivery_notify.notify_one();
4519 }
4520 C2S_CLIENT_FEATURES if data.len() >= 2 => {
4521 let codec_support = data[1];
4524 if let Some(c) = sess.clients.get_mut(&client_id) {
4525 c.surface_codec_support = codec_support;
4526 }
4527 }
4528 C2S_CLIPBOARD_SET if data.len() >= 5 => {
4529 let mime_len = u16::from_le_bytes([data[1], data[2]]) as usize;
4530 if data.len() >= 3 + mime_len + 4 {
4531 let mime = std::str::from_utf8(&data[3..3 + mime_len])
4532 .unwrap_or("text/plain")
4533 .to_string();
4534 let data_len = u32::from_le_bytes([
4535 data[3 + mime_len],
4536 data[4 + mime_len],
4537 data[5 + mime_len],
4538 data[6 + mime_len],
4539 ]) as usize;
4540 let payload_start = 7 + mime_len;
4541 if data.len() >= payload_start + data_len {
4542 let payload = data[payload_start..payload_start + data_len].to_vec();
4543 if let Some(cs) = sess.compositor.as_ref() {
4544 let _ = cs
4545 .handle
4546 .command_tx
4547 .send(CompositorCommand::ClipboardOffer {
4548 mime_type: mime,
4549 data: payload,
4550 });
4551 }
4552 }
4553 }
4554 }
4555 C2S_CLIPBOARD_LIST if !data.is_empty() => {
4556 if let Some(cs) = sess.compositor.as_ref() {
4557 let command_tx = cs.handle.command_tx.clone();
4558 let client_tx = sess.clients.get(&client_id).map(|c| {
4559 (
4560 c.tx.clone(),
4561 c.outbox_queued_frames.clone(),
4562 c.outbox_queued_bytes.clone(),
4563 )
4564 });
4565 if let Some((client_tx, queued_frames, queued_bytes)) = client_tx {
4566 tokio::task::spawn_blocking(move || {
4567 let (tx, rx) = std::sync::mpsc::sync_channel(1);
4568 if command_tx
4569 .send(CompositorCommand::ClipboardListMimes { reply: tx })
4570 .is_ok()
4571 && let Ok(mimes) = rx.recv_timeout(Duration::from_secs(2))
4572 {
4573 let _ = try_send_outbox_tracked(
4574 &client_tx,
4575 &queued_frames,
4576 &queued_bytes,
4577 msg_s2c_clipboard_list(&mimes),
4578 );
4579 }
4580 });
4581 }
4582 } else {
4583 if let Some(c) = sess.clients.get(&client_id) {
4585 let _ = try_send_outbox(c, msg_s2c_clipboard_list(&[]));
4586 }
4587 }
4588 }
4589 C2S_CLIPBOARD_GET if data.len() >= 3 => {
4590 let mime_len = u16::from_le_bytes([data[1], data[2]]) as usize;
4591 if data.len() >= 3 + mime_len {
4592 let mime = std::str::from_utf8(&data[3..3 + mime_len])
4593 .unwrap_or("text/plain")
4594 .to_string();
4595 if let Some(cs) = sess.compositor.as_ref() {
4596 let command_tx = cs.handle.command_tx.clone();
4597 let client_tx = sess.clients.get(&client_id).map(|c| {
4598 (
4599 c.tx.clone(),
4600 c.outbox_queued_frames.clone(),
4601 c.outbox_queued_bytes.clone(),
4602 )
4603 });
4604 if let Some((client_tx, queued_frames, queued_bytes)) = client_tx {
4605 tokio::task::spawn_blocking(move || {
4606 let (tx, rx) = std::sync::mpsc::sync_channel(1);
4607 if command_tx
4608 .send(CompositorCommand::ClipboardGet {
4609 mime_type: mime.clone(),
4610 reply: tx,
4611 })
4612 .is_ok()
4613 && let Ok(content) = rx.recv_timeout(Duration::from_secs(2))
4614 {
4615 let data = content.unwrap_or_default();
4616 let _ = try_send_outbox_tracked(
4617 &client_tx,
4618 &queued_frames,
4619 &queued_bytes,
4620 msg_s2c_clipboard_content(&mime, &data),
4621 );
4622 }
4623 });
4624 }
4625 } else {
4626 if let Some(c) = sess.clients.get(&client_id) {
4628 let _ = try_send_outbox(c, msg_s2c_clipboard_content(&mime, &[]));
4629 }
4630 }
4631 }
4632 }
4633 C2S_SURFACE_LIST if !data.is_empty() => {
4634 let msg = sess.surface_list_msg();
4635 if let Some(c) = sess.clients.get(&client_id) {
4636 let _ = try_send_outbox(c, msg);
4637 }
4638 }
4639 C2S_FOCUS if data.len() >= 3 => {
4640 let pid = u16::from_le_bytes([data[1], data[2]]);
4641 if sess.ptys.contains_key(&pid) {
4642 let old_pid = sess.clients.get(&client_id).and_then(|c| c.lead);
4643 if let Some(c) = sess.clients.get_mut(&client_id) {
4644 c.lead = Some(pid);
4645 subscribe_client_to(c, pid);
4646 if old_pid == Some(pid) {
4647 update_client_scroll_state(c, pid, 0);
4648 } else {
4649 reset_inflight(c);
4650 }
4651 }
4652 if let Some(pty) = sess.ptys.get_mut(&pid) {
4653 pty.mark_dirty();
4654 need_nudge = true;
4655 }
4656 }
4657 }
4658 C2S_SUBSCRIBE if data.len() >= 3 => {
4659 let pid = u16::from_le_bytes([data[1], data[2]]);
4660 if sess.ptys.contains_key(&pid) {
4661 if let Some(c) = sess.clients.get_mut(&client_id) {
4662 subscribe_client_to(c, pid);
4663 }
4664 if let Some(pty) = sess.ptys.get_mut(&pid) {
4665 pty.mark_dirty();
4666 }
4667 need_nudge = true;
4668 }
4669 }
4670 C2S_UNSUBSCRIBE if data.len() >= 3 => {
4671 let pid = u16::from_le_bytes([data[1], data[2]]);
4672 if sess.ptys.contains_key(&pid) {
4673 let mut touched = Vec::new();
4674 if let Some(c) = sess.clients.get_mut(&client_id) {
4675 if unsubscribe_client_from(c, pid) {
4676 touched.push(pid);
4677 }
4678 reset_inflight(c);
4679 }
4680 if sess.resize_ptys_to_mediated_sizes(touched) {
4681 need_nudge = true;
4682 }
4683 }
4684 }
4685 C2S_RESTART if data.len() >= 3 => {
4686 let pid = u16::from_le_bytes([data[1], data[2]]);
4687 let restart_info = sess
4688 .ptys
4689 .get(&pid)
4690 .filter(|p| p.exited)
4691 .map(|p| (p.driver.size(), p.command.clone(), p.tag.clone()));
4692 if let Some(((rows, cols), command, tag)) = restart_info {
4693 let wayland_display = sess
4694 .compositor
4695 .as_ref()
4696 .map(|cs| cs.handle.socket_name.clone());
4697 #[cfg(unix)]
4698 let pulse_server = sess.pulse_server_path();
4699 #[cfg(not(unix))]
4700 let pulse_server: Option<String> = None;
4701 #[cfg(unix)]
4702 let pipewire_remote = sess.pipewire_remote_path();
4703 #[cfg(not(unix))]
4704 let pipewire_remote: Option<String> = None;
4705 if let Some((new_handle, reader, byte_rx)) = pty::respawn_child(
4706 &state.config.shell,
4707 &state.config.shell_flags,
4708 rows,
4709 cols,
4710 pid,
4711 command.as_deref(),
4712 state.clone(),
4713 wayland_display.as_deref(),
4714 pulse_server.as_deref(),
4715 pipewire_remote.as_deref(),
4716 ) {
4717 let Some(pty) = sess.ptys.get_mut(&pid) else {
4718 break;
4719 };
4720 pty.handle = new_handle;
4721 pty.reader_handle = reader;
4722 pty.byte_rx = byte_rx;
4723 pty.driver.reset_modes();
4724 pty.exited = false;
4725 pty.exit_status = blit_remote::EXIT_STATUS_UNKNOWN;
4726 pty.lflag_cache = pty::pty_lflag(&pty.handle);
4727 pty.lflag_last = Instant::now();
4728 pty.mark_dirty();
4729 if let Some(c) = sess.clients.get_mut(&client_id) {
4730 c.lead = Some(pid);
4731 subscribe_client_to(c, pid);
4732 update_client_scroll_state(c, pid, 0);
4733 reset_inflight(c);
4734 }
4735 let mut msg = Vec::with_capacity(3 + tag.len());
4736 msg.push(S2C_CREATED);
4737 msg.extend_from_slice(&pid.to_le_bytes());
4738 msg.extend_from_slice(tag.as_bytes());
4739 sess.send_to_all(&msg);
4740 need_nudge = true;
4741 }
4742 }
4743 }
4744 C2S_READ if data.len() >= 13 => {
4745 let nonce = u16::from_le_bytes([data[1], data[2]]);
4746 let pid = u16::from_le_bytes([data[3], data[4]]);
4747 let req_offset = u32::from_le_bytes([data[5], data[6], data[7], data[8]]) as usize;
4748 let req_limit =
4749 u32::from_le_bytes([data[9], data[10], data[11], data[12]]) as usize;
4750 let flags = data.get(13).copied().unwrap_or(0);
4751 let ansi = flags & READ_ANSI != 0;
4752 let tail = flags & READ_TAIL != 0;
4753
4754 if let Some(pty) = sess.ptys.get_mut(&pid) {
4755 let (rows, _cols) = pty.driver.size();
4756 let viewport = take_snapshot(pty);
4757 let scrollback_lines = viewport.scrollback_lines() as usize;
4758 let total_lines = scrollback_lines + rows as usize;
4759
4760 let extract = |f: &FrameState| -> String {
4761 if ansi {
4762 f.get_ansi_text()
4763 } else {
4764 f.get_all_text()
4765 }
4766 };
4767
4768 let mut all_lines: Vec<String> =
4769 Vec::with_capacity(scrollback_lines + rows as usize);
4770
4771 let mut scroll_offset = scrollback_lines;
4772 while scroll_offset > 0 {
4773 let frame = pty.driver.scrollback_frame(scroll_offset);
4774 let page = extract(&frame);
4775 let page_lines: Vec<&str> = page.lines().collect();
4776 let take = if scroll_offset < rows as usize {
4777 scroll_offset.min(page_lines.len())
4778 } else {
4779 page_lines.len()
4780 };
4781 for line in &page_lines[..take] {
4782 all_lines.push(line.to_string());
4783 }
4784 if scroll_offset <= rows as usize {
4785 break;
4786 }
4787 scroll_offset = scroll_offset.saturating_sub(rows as usize);
4788 }
4789
4790 for line in extract(&viewport).lines() {
4791 all_lines.push(line.to_string());
4792 }
4793
4794 let (start, end) = if tail {
4795 let end = all_lines.len().saturating_sub(req_offset);
4796 let start = if req_limit == 0 {
4797 0
4798 } else {
4799 end.saturating_sub(req_limit)
4800 };
4801 (start, end)
4802 } else {
4803 let start = req_offset.min(all_lines.len());
4804 let end = if req_limit == 0 {
4805 all_lines.len()
4806 } else {
4807 (start + req_limit).min(all_lines.len())
4808 };
4809 (start, end)
4810 };
4811 let text = all_lines[start..end].join("\n");
4812
4813 let mut msg = Vec::with_capacity(13 + text.len());
4814 msg.push(S2C_TEXT);
4815 msg.extend_from_slice(&nonce.to_le_bytes());
4816 msg.extend_from_slice(&pid.to_le_bytes());
4817 msg.extend_from_slice(&(total_lines as u32).to_le_bytes());
4818 msg.extend_from_slice(&(start as u32).to_le_bytes());
4819 msg.extend_from_slice(text.as_bytes());
4820 if let Some(client) = sess.clients.get(&client_id) {
4821 let _ = try_send_outbox(client, msg);
4822 }
4823 }
4824 }
4825 C2S_COPY_RANGE if data.len() >= 18 => {
4826 let nonce = u16::from_le_bytes([data[1], data[2]]);
4827 let pid = u16::from_le_bytes([data[3], data[4]]);
4828 let start_tail = u32::from_le_bytes([data[5], data[6], data[7], data[8]]);
4829 let start_col = u16::from_le_bytes([data[9], data[10]]);
4830 let end_tail = u32::from_le_bytes([data[11], data[12], data[13], data[14]]);
4831 let end_col = u16::from_le_bytes([data[15], data[16]]);
4832
4833 if let Some(pty) = sess.ptys.get(&pid) {
4834 let text = pty
4835 .driver
4836 .get_text_range(start_tail, start_col, end_tail, end_col);
4837 let total_lines = pty.driver.total_lines();
4838
4839 let mut msg = Vec::with_capacity(13 + text.len());
4840 msg.push(S2C_TEXT);
4841 msg.extend_from_slice(&nonce.to_le_bytes());
4842 msg.extend_from_slice(&pid.to_le_bytes());
4843 msg.extend_from_slice(&total_lines.to_le_bytes());
4844 msg.extend_from_slice(&start_tail.to_le_bytes());
4845 msg.extend_from_slice(text.as_bytes());
4846 if let Some(client) = sess.clients.get(&client_id) {
4847 let _ = try_send_outbox(client, msg);
4848 }
4849 }
4850 }
4851 C2S_KILL if data.len() >= 7 => {
4852 let pid = u16::from_le_bytes([data[1], data[2]]);
4853 let signal = i32::from_le_bytes([data[3], data[4], data[5], data[6]]);
4854 if let Some(pty) = sess.ptys.get(&pid)
4855 && !pty.exited
4856 {
4857 pty::kill_pty(&pty.handle, signal);
4858 }
4859 }
4860 C2S_CLOSE if data.len() >= 3 => {
4861 let pid = u16::from_le_bytes([data[1], data[2]]);
4862 if let Some(pty) = sess.ptys.remove(&pid) {
4863 if !pty.exited {
4864 state.pty_fds.write().unwrap().remove(&pid);
4865 drop(pty.reader_handle);
4866 pty::close_pty(&pty.handle);
4867 }
4868 for client in sess.clients.values_mut() {
4869 unsubscribe_client_from(client, pid);
4870 }
4871 let mut msg = vec![S2C_CLOSED];
4872 msg.extend_from_slice(&pid.to_le_bytes());
4873 sess.send_to_all(&msg);
4874 }
4875 }
4876 _ => {}
4877 }
4878 drop(sess);
4879 if need_nudge {
4880 nudge_delivery(&state);
4881 }
4882 }
4883
4884 {
4885 let mut sess = state.session.lock().await;
4886 let mut need_nudge = false;
4887 let client = sess.clients.remove(&client_id);
4888 let affected_ptys = client
4889 .as_ref()
4890 .map(|c| c.view_sizes.keys().copied().collect::<Vec<_>>())
4891 .unwrap_or_default();
4892 let affected_surfaces = client
4893 .as_ref()
4894 .map(|c| c.surface_view_sizes.keys().copied().collect::<Vec<_>>())
4895 .unwrap_or_default();
4896 if sess.resize_ptys_to_mediated_sizes(affected_ptys) {
4897 need_nudge = true;
4898 }
4899 sess.resize_surfaces_to_mediated_sizes(affected_surfaces, &state.config.surface_encoders);
4900 if let Some(ref client) = client
4904 && !client.pressed_surface_keys.is_empty()
4905 && let Some(cs) = sess.compositor.as_mut()
4906 {
4907 let keycodes: Vec<u32> = client.pressed_surface_keys.iter().copied().collect();
4908 let _ = cs
4909 .handle
4910 .command_tx
4911 .send(CompositorCommand::ReleaseKeys { keycodes });
4912 cs.handle.wake();
4913 }
4914 drop(sess);
4915 if need_nudge {
4916 nudge_delivery(&state);
4917 }
4918 }
4919 sender.abort();
4920 if state.config.verbose {
4921 eprintln!("client disconnected");
4922 }
4923}
4924
4925#[cfg(test)]
4926mod tests {
4927 use super::*;
4928
4929 fn test_client_with_capacity(capacity: usize) -> (ClientState, mpsc::Receiver<Vec<u8>>) {
4930 let (tx, rx) = mpsc::channel(capacity);
4931 let (audio_tx, _audio_rx) = mpsc::channel(AUDIO_OUTBOX_CAPACITY);
4932 let client = ClientState {
4933 tx,
4934 outbox_queued_frames: Arc::new(AtomicUsize::new(0)),
4935 outbox_queued_bytes: Arc::new(AtomicUsize::new(0)),
4936 audio_tx,
4937 lead: None,
4938 subscriptions: HashSet::new(),
4939 surface_subscriptions: HashSet::new(),
4940 audio_subscribed: false,
4941 #[cfg(unix)]
4942 audio_bitrate_kbps: 0,
4943 view_sizes: HashMap::new(),
4944 scroll_offsets: HashMap::new(),
4945 scroll_caches: HashMap::new(),
4946 last_sent: HashMap::new(),
4947 preview_next_send_at: HashMap::new(),
4948 rtt_ms: 50.0,
4949 min_rtt_ms: 50.0,
4950 display_fps: 60.0,
4951 delivery_bps: 262_144.0,
4952 goodput_bps: 262_144.0,
4953 goodput_jitter_bps: 0.0,
4954 max_goodput_jitter_bps: 0.0,
4955 last_goodput_sample_bps: 0.0,
4956 avg_frame_bytes: 1_024.0,
4957 avg_paced_frame_bytes: 1_024.0,
4958 avg_preview_frame_bytes: 1_024.0,
4959 avg_surface_frame_bytes: 8_192.0,
4960 inflight_bytes: 0,
4961 inflight_frames: VecDeque::new(),
4962 next_send_at: Instant::now(),
4963 probe_frames: 0.0,
4964 frames_sent: 0,
4965 acks_recv: 0,
4966 acked_bytes_since_log: 0,
4967 browser_backlog_frames: 0,
4968 browser_ack_ahead_frames: 0,
4969 browser_apply_ms: 0.0,
4970 last_metrics_update: Instant::now(),
4971 last_log: Instant::now(),
4972 goodput_window_bytes: 0,
4973 goodput_window_start: Instant::now(),
4974 surface_next_send_at: Instant::now(),
4975 surface_needs_keyframe: true,
4976 surface_burst_remaining: SURFACE_BURST_FRAMES,
4977 surface_inflight_frames: VecDeque::new(),
4978 surface_encoders: HashMap::new(),
4979 surface_encodes_in_flight: HashSet::new(),
4980 surface_last_encoded_gen: HashMap::new(),
4981 surface_view_sizes: HashMap::new(),
4982 surface_codec_support: 0,
4983 surface_codec_overrides: HashMap::new(),
4984 surface_quality_overrides: HashMap::new(),
4985 pressed_surface_keys: HashSet::new(),
4986 };
4987 (client, rx)
4988 }
4989
4990 fn test_client() -> ClientState {
4991 let (client, _rx) = test_client_with_capacity(OUTBOX_CAPACITY);
4992 client
4993 }
4994
4995 fn fill_inflight(client: &mut ClientState, frames: usize, bytes_per_frame: usize) {
4996 let now = Instant::now();
4997 client.inflight_bytes = frames.saturating_mul(bytes_per_frame);
4998 client.inflight_frames = (0..frames)
4999 .map(|_| InFlightFrame {
5000 sent_at: now,
5001 bytes: bytes_per_frame,
5002 paced: true,
5003 })
5004 .collect();
5005 }
5006
5007 fn sample_frame(text: &str) -> FrameState {
5008 let mut frame = FrameState::new(2, 8);
5009 frame.write_text(0, 0, text, blit_remote::CellStyle::default());
5010 frame
5011 }
5012
5013 #[test]
5014 fn unset_view_size_accepts_zero_pair_only() {
5015 assert!(is_unset_view_size(0, 0));
5016 assert!(!is_unset_view_size(0, 80));
5017 assert!(!is_unset_view_size(u16::MAX, u16::MAX));
5018 }
5019
5020 #[test]
5021 fn unsubscribe_client_from_clears_view_size() {
5022 let mut client = test_client();
5023 client.subscriptions.insert(7);
5024 client.view_sizes.insert(7, (24, 80));
5025 assert!(unsubscribe_client_from(&mut client, 7));
5026 assert!(!client.subscriptions.contains(&7));
5027 assert!(!client.view_sizes.contains_key(&7));
5028 }
5029
5030 #[test]
5031 fn mediated_size_uses_per_pty_view_sizes_without_lead() {
5032 let mut session = Session::new();
5033 let mut c1 = test_client();
5034 let mut c2 = test_client();
5035 c1.view_sizes.insert(7, (30, 120));
5036 c2.view_sizes.insert(7, (24, 100));
5037 session.clients.insert(1, c1);
5038 session.clients.insert(2, c2);
5039 assert_eq!(session.mediated_size_for_pty(7), Some((24, 100)));
5040 }
5041
5042 #[test]
5043 fn mediated_surface_size_picks_min_dimensions_max_scale() {
5044 let mut session = Session::new();
5045 let mut c1 = test_client();
5046 let mut c2 = test_client();
5047 c1.surface_view_sizes.insert(1, (1920, 1080, 240)); c2.surface_view_sizes.insert(1, (1280, 720, 120)); session.clients.insert(1, c1);
5050 session.clients.insert(2, c2);
5051 assert_eq!(
5052 session.mediated_size_for_surface(1, None),
5053 Some((1280, 720, 240))
5054 );
5055 }
5056
5057 #[test]
5058 fn mediated_surface_size_none_when_no_clients() {
5059 let session = Session::new();
5060 assert_eq!(session.mediated_size_for_surface(1, None), None);
5061 }
5062
5063 #[test]
5064 fn mediated_surface_size_single_client() {
5065 let mut session = Session::new();
5066 let mut c1 = test_client();
5067 c1.surface_view_sizes.insert(3, (800, 600, 120));
5068 session.clients.insert(1, c1);
5069 assert_eq!(
5070 session.mediated_size_for_surface(3, None),
5071 Some((800, 600, 120))
5072 );
5073 }
5074
5075 #[test]
5076 fn mediated_surface_size_ignores_other_surfaces() {
5077 let mut session = Session::new();
5078 let mut c1 = test_client();
5079 c1.surface_view_sizes.insert(1, (1920, 1080, 240));
5080 c1.surface_view_sizes.insert(2, (640, 480, 120));
5081 session.clients.insert(1, c1);
5082 assert_eq!(
5083 session.mediated_size_for_surface(1, None),
5084 Some((1920, 1080, 240))
5085 );
5086 assert_eq!(
5087 session.mediated_size_for_surface(2, None),
5088 Some((640, 480, 120))
5089 );
5090 assert_eq!(session.mediated_size_for_surface(3, None), None);
5091 }
5092
5093 #[test]
5094 fn mediated_surface_size_clamped_to_encoder_max() {
5095 let mut session = Session::new();
5096 let mut c1 = test_client();
5097 c1.surface_view_sizes.insert(1, (5000, 3000, 240));
5098 session.clients.insert(1, c1);
5099 assert_eq!(
5100 session.mediated_size_for_surface(1, None),
5101 Some((5000, 3000, 240))
5102 );
5103 assert_eq!(
5104 session.mediated_size_for_surface(1, Some((3840, 2160))),
5105 Some((3840, 2160, 240))
5106 );
5107 }
5108
5109 #[test]
5110 fn due_preview_reserves_the_last_lead_slot() {
5111 let mut client = test_client();
5112 client.lead = Some(1);
5113 client.subscriptions.insert(1);
5114 client.subscriptions.insert(2);
5115
5116 let target_frames = target_frame_window(&client);
5117 let lead_limit = target_frames.saturating_sub(1).max(1);
5118 fill_inflight(&mut client, lead_limit, 512);
5119
5120 assert!(window_open(&client));
5121 assert!(lead_window_open(&client, false));
5122 assert!(!lead_window_open(&client, true));
5123 assert!(can_send_preview(&client, 2, Instant::now()));
5124 }
5125
5126 #[test]
5127 fn entering_scrollback_uses_current_visible_frame_as_baseline() {
5128 let mut client = test_client();
5129 let live = sample_frame("live");
5130 client.lead = Some(7);
5131 client.subscriptions.insert(7);
5132 client.last_sent.insert(7, live.clone());
5133
5134 assert!(update_client_scroll_state(&mut client, 7, 12));
5135 assert_eq!(client.scroll_offsets.get(&7), Some(&12));
5136 assert_eq!(client.scroll_caches.get(&7), Some(&live));
5137 }
5138
5139 #[test]
5140 fn leaving_scrollback_seeds_live_diff_from_scrollback_view() {
5141 let mut client = test_client();
5142 let history = sample_frame("hist");
5143 client.lead = Some(7);
5144 client.subscriptions.insert(7);
5145 client.scroll_offsets.insert(7, 12);
5146 client.scroll_caches.insert(7, history.clone());
5147
5148 assert!(update_client_scroll_state(&mut client, 7, 0));
5149 assert_eq!(client.scroll_offsets.get(&7), None);
5150 assert_eq!(client.last_sent.get(&7), Some(&history));
5151 assert_eq!(client.scroll_caches.get(&7), None);
5152 }
5153
5154 #[tokio::test]
5155 async fn request_surface_capture_returns_pixels_from_compositor() {
5156 let (command_tx, command_rx) = std::sync::mpsc::channel();
5157 std::thread::Builder::new()
5158 .name("test-capture-reply".into())
5159 .spawn(move || {
5160 let CompositorCommand::Capture {
5161 surface_id,
5162 scale_120: _,
5163 reply,
5164 } = command_rx.recv().unwrap()
5165 else {
5166 panic!("expected capture command");
5167 };
5168 assert_eq!(surface_id, 7);
5169 let _ = reply.send(Some((2, 3, vec![1, 2, 3, 4])));
5170 })
5171 .unwrap();
5172
5173 let result =
5174 request_surface_capture_with_timeout(command_tx, 7, 0, Duration::from_millis(50)).await;
5175
5176 assert_eq!(result, Some((2, 3, vec![1, 2, 3, 4])));
5177 }
5178
5179 #[tokio::test]
5180 async fn request_surface_capture_returns_none_when_compositor_disconnects() {
5181 let (command_tx, command_rx) = std::sync::mpsc::channel();
5182 std::thread::Builder::new()
5183 .name("test-capture-drop".into())
5184 .spawn(move || {
5185 let _ = command_rx.recv().unwrap();
5186 })
5187 .unwrap();
5188
5189 let result =
5190 request_surface_capture_with_timeout(command_tx, 7, 0, Duration::from_millis(50)).await;
5191
5192 assert_eq!(result, None);
5193 }
5194
5195 #[test]
5198 fn frame_window_minimum_is_two() {
5199 assert!(frame_window(0.0, 60.0) >= 2);
5200 }
5201
5202 #[test]
5203 fn frame_window_scales_with_rtt() {
5204 let low = frame_window(10.0, 60.0);
5205 let high = frame_window(200.0, 60.0);
5206 assert!(high > low, "higher RTT should need more frames in flight");
5207 }
5208
5209 #[test]
5210 fn frame_window_scales_with_fps() {
5211 let slow = frame_window(100.0, 10.0);
5212 let fast = frame_window(100.0, 120.0);
5213 assert!(fast > slow, "higher fps should need more frames in flight");
5214 }
5215
5216 #[test]
5217 fn frame_window_zero_rtt() {
5218 assert!(frame_window(0.0, 120.0) >= 2);
5219 }
5220
5221 #[test]
5224 fn path_rtt_ms_uses_min_when_positive() {
5225 let mut client = test_client();
5226 client.rtt_ms = 100.0;
5227 client.min_rtt_ms = 30.0;
5228 assert_eq!(path_rtt_ms(&client), 30.0);
5229 }
5230
5231 #[test]
5232 fn path_rtt_ms_falls_back_to_rtt_when_min_zero() {
5233 let mut client = test_client();
5234 client.rtt_ms = 80.0;
5235 client.min_rtt_ms = 0.0;
5236 assert_eq!(path_rtt_ms(&client), 80.0);
5237 }
5238
5239 #[test]
5242 fn ewma_rising_uses_rise_alpha() {
5243 let result = ewma_with_direction(100.0, 200.0, 0.5, 0.1);
5244 assert!((result - 150.0).abs() < 0.01);
5246 }
5247
5248 #[test]
5249 fn ewma_falling_uses_fall_alpha() {
5250 let result = ewma_with_direction(200.0, 100.0, 0.5, 0.1);
5251 assert!((result - 190.0).abs() < 0.01);
5253 }
5254
5255 #[test]
5256 fn ewma_same_value_unchanged() {
5257 let result = ewma_with_direction(50.0, 50.0, 0.5, 0.5);
5258 assert!((result - 50.0).abs() < 0.01);
5259 }
5260
5261 #[test]
5264 fn advance_deadline_steps_forward() {
5265 let now = Instant::now();
5266 let mut deadline = now;
5267 let interval = Duration::from_millis(16);
5268 advance_deadline(&mut deadline, now, interval);
5269 assert!(deadline > now);
5270 assert!(deadline <= now + interval + Duration::from_micros(100));
5271 }
5272
5273 #[test]
5274 fn advance_deadline_resets_when_far_behind() {
5275 let now = Instant::now();
5276 let mut deadline = now - Duration::from_secs(10);
5278 let interval = Duration::from_millis(16);
5279 advance_deadline(&mut deadline, now, interval);
5280 assert!(deadline >= now);
5282 }
5283
5284 #[test]
5285 fn should_snapshot_pty_requires_dirty_and_needful() {
5286 assert!(should_snapshot_pty(true, true, false));
5287 assert!(!should_snapshot_pty(false, true, false));
5288 assert!(!should_snapshot_pty(true, false, false));
5289 }
5290
5291 #[test]
5292 fn should_snapshot_pty_defers_synced_output() {
5293 assert!(!should_snapshot_pty(true, true, true));
5294 assert!(should_snapshot_pty(true, true, false));
5295 }
5296
5297 #[test]
5298 fn enqueue_ready_frame_refuses_new_frames_when_capped() {
5299 let mut queue = VecDeque::new();
5300 for cols in 1..=(READY_FRAME_QUEUE_CAP as u16) {
5301 assert!(enqueue_ready_frame(&mut queue, FrameState::new(1, cols)));
5302 }
5303 assert!(!enqueue_ready_frame(
5304 &mut queue,
5305 FrameState::new(1, READY_FRAME_QUEUE_CAP as u16 + 1),
5306 ));
5307 assert_eq!(queue.len(), READY_FRAME_QUEUE_CAP);
5308 assert_eq!(queue.front().map(FrameState::cols), Some(1));
5309 assert_eq!(
5310 queue.back().map(FrameState::cols),
5311 Some(READY_FRAME_QUEUE_CAP as u16),
5312 );
5313 }
5314
5315 #[test]
5316 fn find_sync_output_end_returns_end_of_first_close_sequence() {
5317 let bytes = b"abc\x1b[?2026lrest\x1b[?2026l";
5318 assert_eq!(find_sync_output_end(&[], bytes), Some(11));
5319 }
5320
5321 #[test]
5322 fn find_sync_output_end_returns_none_without_close_sequence() {
5323 assert_eq!(find_sync_output_end(&[], b"\x1b[?2026hpartial"), None);
5324 }
5325
5326 #[test]
5327 fn find_sync_output_end_detects_boundary_split_across_reads() {
5328 assert_eq!(find_sync_output_end(b"abc\x1b[?20", b"26lrest"), Some(3));
5329 }
5330
5331 #[test]
5332 fn update_sync_scan_tail_keeps_recent_suffix_only() {
5333 let mut tail = Vec::new();
5334 update_sync_scan_tail(&mut tail, b"123456789");
5335 assert_eq!(tail, b"3456789");
5336 }
5337
5338 #[test]
5341 fn window_saturated_at_90_percent_frames() {
5342 let client = test_client();
5343 let target = target_frame_window(&client);
5344 let frames_90 = (target * 9).div_ceil(10); assert!(window_saturated(&client, frames_90, 0));
5346 }
5347
5348 #[test]
5349 fn window_saturated_not_at_low_usage() {
5350 let client = test_client();
5351 assert!(!window_saturated(&client, 1, 0));
5352 }
5353
5354 #[test]
5355 fn window_saturated_at_90_percent_bytes() {
5356 let client = test_client();
5357 let target_bytes = target_byte_window(&client);
5358 let bytes_90 = (target_bytes * 9).div_ceil(10);
5359 assert!(window_saturated(&client, 0, bytes_90));
5360 }
5361
5362 #[test]
5365 fn outbox_queued_frames_zero_when_empty() {
5366 let client = test_client();
5367 assert_eq!(outbox_queued_frames(&client), 0);
5368 }
5369
5370 #[test]
5371 fn outbox_backpressured_when_queue_full() {
5372 let (client, _rx) = test_client_with_capacity(OUTBOX_CAPACITY);
5373 for _ in 0..OUTBOX_SOFT_QUEUE_LIMIT_FRAMES {
5375 let _ = try_send_outbox(&client, vec![0u8]);
5376 }
5377 assert!(outbox_backpressured(&client));
5378 }
5379
5380 #[test]
5381 fn outbox_not_backpressured_by_small_frames_under_byte_budget() {
5382 let (client, _rx) = test_client_with_capacity(OUTBOX_CAPACITY);
5383 for _ in 0..(OUTBOX_SOFT_QUEUE_LIMIT_FRAMES - 1) {
5384 let _ = try_send_outbox(&client, vec![0u8; 512]);
5385 }
5386 assert!(!outbox_backpressured(&client));
5387 }
5388
5389 #[test]
5390 fn outbox_backpressured_by_large_queued_bytes() {
5391 let (client, _rx) = test_client_with_capacity(OUTBOX_CAPACITY);
5392 let _ = try_send_outbox(&client, vec![0u8; OUTBOX_SOFT_QUEUE_LIMIT_BYTES]);
5393 assert!(outbox_backpressured(&client));
5394 }
5395
5396 #[test]
5397 fn outbox_not_backpressured_when_empty() {
5398 let client = test_client();
5399 assert!(!outbox_backpressured(&client));
5400 }
5401
5402 #[test]
5405 fn browser_pacing_fps_matches_display_fps_when_browser_ready() {
5406 let mut client = test_client();
5407 client.rtt_ms = 1.0;
5408 client.min_rtt_ms = 1.0;
5409 client.browser_backlog_frames = 0;
5410 client.browser_ack_ahead_frames = 0;
5411 client.browser_apply_ms = 0.0;
5412 client.goodput_bps = 1_000_000.0;
5413 client.delivery_bps = 1_000_000.0;
5414 client.display_fps = 144.0;
5415 assert!((browser_pacing_fps(&client) - 144.0).abs() < 0.01);
5416 }
5417
5418 #[test]
5419 fn browser_pacing_fps_drops_below_display_fps_when_backlogged() {
5420 let mut client = test_client();
5421 client.browser_backlog_frames = 20;
5422 let fps = browser_pacing_fps(&client);
5423 assert!(fps >= 1.0);
5424 assert!(fps < client.display_fps);
5425 }
5426
5427 #[test]
5430 fn effective_rtt_ms_equals_path_when_queue_is_empty() {
5431 let mut client = test_client();
5432 client.rtt_ms = 1.0;
5433 client.min_rtt_ms = 1.0;
5434 client.browser_backlog_frames = 0;
5435 client.browser_ack_ahead_frames = 0;
5436 client.browser_apply_ms = 0.0;
5437 client.goodput_bps = 1_000_000.0;
5438 client.delivery_bps = 1_000_000.0;
5439 assert!((effective_rtt_ms(&client) - 1.0).abs() < 0.01);
5440 }
5441
5442 #[test]
5443 fn effective_rtt_ms_at_least_path_rtt() {
5444 let client = test_client();
5445 assert!(effective_rtt_ms(&client) >= path_rtt_ms(&client));
5446 }
5447
5448 #[test]
5451 fn target_frame_window_at_least_two() {
5452 let client = test_client();
5453 assert!(target_frame_window(&client) >= 2);
5454 }
5455
5456 #[test]
5457 fn target_frame_window_grows_with_probe() {
5458 let mut client = test_client();
5459 let base = target_frame_window(&client);
5460 client.probe_frames = 10.0;
5461 let probed = target_frame_window(&client);
5462 assert!(probed > base, "probe_frames should grow the window");
5463 }
5464
5465 #[test]
5468 fn bandwidth_floor_bps_at_least_16k() {
5469 let mut client = test_client();
5470 client.goodput_bps = 0.0;
5471 client.delivery_bps = 0.0;
5472 assert_eq!(bandwidth_floor_bps(&client), 0.0);
5473 }
5474
5475 #[test]
5476 fn bandwidth_floor_bps_scales_with_goodput() {
5477 let mut client = test_client();
5478 client.goodput_bps = 1_000_000.0;
5479 client.delivery_bps = 1_000_000.0;
5480 let floor = bandwidth_floor_bps(&client);
5481 assert!(floor > 0.0);
5482 }
5483
5484 #[test]
5485 fn browser_ready_delivery_floor_can_drive_large_frames_to_display_fps() {
5486 let mut client = test_client();
5487 client.display_fps = 60.0;
5488 client.browser_backlog_frames = 0;
5489 client.browser_ack_ahead_frames = 0;
5490 client.browser_apply_ms = 0.2;
5491 client.goodput_bps = 3_000_000.0;
5492 client.delivery_bps = 9_500_000.0;
5493 client.last_goodput_sample_bps = 3_000_000.0;
5494 client.avg_paced_frame_bytes = 150_000.0;
5495 client.avg_preview_frame_bytes = 1_024.0;
5496 client.avg_frame_bytes = 150_000.0;
5497
5498 assert!(
5499 (pacing_fps(&client) - client.display_fps).abs() < 0.01,
5500 "browser-ready delivery floor should let large frames reach display_fps on a fast path",
5501 );
5502 }
5503
5504 #[test]
5507 fn pacing_fps_zero_when_no_bandwidth() {
5508 let mut client = test_client();
5509 client.goodput_bps = 0.0;
5510 client.delivery_bps = 0.0;
5511 client.last_goodput_sample_bps = 0.0;
5512 assert!(
5513 pacing_fps(&client) == 0.0,
5514 "pacing_fps should be 0 with zero bandwidth"
5515 );
5516 }
5517
5518 #[test]
5519 fn pacing_fps_reaches_display_fps_when_not_bandwidth_limited() {
5520 let mut client = test_client();
5521 client.rtt_ms = 1.0;
5522 client.min_rtt_ms = 1.0;
5523 client.browser_backlog_frames = 0;
5524 client.browser_ack_ahead_frames = 0;
5525 client.browser_apply_ms = 0.0;
5526 client.goodput_bps = 1_000_000.0;
5527 client.delivery_bps = 1_000_000.0;
5528 client.display_fps = 60.0;
5529 assert!((pacing_fps(&client) - 60.0).abs() < 0.01);
5530 }
5531
5532 #[test]
5535 fn throughput_limited_when_low_bandwidth() {
5536 let mut client = test_client();
5537 client.goodput_bps = 1_000.0;
5538 client.delivery_bps = 1_000.0;
5539 client.last_goodput_sample_bps = 0.0;
5540 assert!(throughput_limited(&client));
5541 }
5542
5543 #[test]
5544 fn throughput_not_limited_with_high_bandwidth() {
5545 let mut client = test_client();
5546 client.goodput_bps = 100_000_000.0;
5547 client.delivery_bps = 100_000_000.0;
5548 assert!(!throughput_limited(&client));
5549 }
5550
5551 #[test]
5554 fn browser_pacing_fps_at_least_one() {
5555 let client = test_client();
5556 assert!(browser_pacing_fps(&client) >= 1.0);
5557 }
5558
5559 #[test]
5560 fn browser_pacing_fps_reduced_by_high_backlog() {
5561 let mut client = test_client();
5562 let normal = browser_pacing_fps(&client);
5563 client.browser_backlog_frames = 20;
5564 let backlogged = browser_pacing_fps(&client);
5565 assert!(backlogged < normal, "high backlog should reduce pacing fps");
5566 }
5567
5568 #[test]
5569 fn browser_pacing_fps_reduced_by_high_ack_ahead() {
5570 let mut client = test_client();
5571 let normal = browser_pacing_fps(&client);
5572 client.browser_ack_ahead_frames = 10;
5573 let ahead = browser_pacing_fps(&client);
5574 assert!(ahead < normal, "high ack_ahead should reduce pacing fps");
5575 }
5576
5577 #[test]
5580 fn browser_backlog_blocked_over_threshold() {
5581 let mut client = test_client();
5582 client.browser_backlog_frames = 9;
5583 assert!(browser_backlog_blocked(&client));
5584 }
5585
5586 #[test]
5587 fn browser_backlog_not_blocked_under_threshold() {
5588 let mut client = test_client();
5589 client.browser_backlog_frames = 8;
5590 assert!(!browser_backlog_blocked(&client));
5591 }
5592
5593 #[test]
5596 fn byte_budget_for_at_least_one_frame() {
5597 let client = test_client();
5598 let budget = byte_budget_for(&client, 10.0);
5599 assert!(budget >= client.avg_frame_bytes.max(256.0) as usize);
5600 }
5601
5602 #[test]
5603 fn byte_budget_for_grows_with_time() {
5604 let client = test_client();
5605 let short = byte_budget_for(&client, 10.0);
5606 let long = byte_budget_for(&client, 1000.0);
5607 assert!(long >= short);
5608 }
5609
5610 #[test]
5613 fn target_byte_window_positive() {
5614 let client = test_client();
5615 assert!(target_byte_window(&client) > 0);
5616 }
5617
5618 #[test]
5619 fn target_byte_window_covers_frame_window() {
5620 let client = test_client();
5621 let byte_win = target_byte_window(&client);
5622 let frame_win = target_frame_window(&client);
5623 let min_bytes =
5624 (client.avg_paced_frame_bytes.max(256.0) * frame_win.max(2) as f32).ceil() as usize;
5625 assert!(
5626 byte_win >= min_bytes,
5627 "byte window should cover at least frame_window worth of paced frames"
5628 );
5629 }
5630
5631 #[test]
5634 fn send_interval_matches_browser_pacing() {
5635 let client = test_client();
5636 let interval = send_interval(&client);
5637 let expected = Duration::from_secs_f64(1.0 / browser_pacing_fps(&client) as f64);
5638 let diff = interval.abs_diff(expected);
5639 assert!(diff < Duration::from_micros(10));
5640 }
5641
5642 #[test]
5645 fn preview_fps_at_least_one() {
5646 let client = test_client();
5647 assert!(preview_fps(&client) >= 1.0);
5648 }
5649
5650 #[test]
5653 fn window_open_initially() {
5654 let client = test_client();
5655 assert!(window_open(&client));
5656 }
5657
5658 #[test]
5659 fn window_open_false_when_browser_blocked() {
5660 let mut client = test_client();
5661 client.browser_backlog_frames = 20;
5662 assert!(!window_open(&client));
5663 }
5664
5665 #[test]
5666 fn window_open_false_when_inflight_full() {
5667 let mut client = test_client();
5668 let target = target_frame_window(&client);
5669 fill_inflight(&mut client, target + 10, 1024);
5670 assert!(!window_open(&client));
5671 }
5672
5673 #[test]
5676 fn lead_window_open_no_reserve_same_as_window_open() {
5677 let client = test_client();
5678 assert_eq!(lead_window_open(&client, false), window_open(&client));
5679 }
5680
5681 #[test]
5682 fn lead_window_open_reserves_preview_slot() {
5683 let mut client = test_client();
5684 client.lead = Some(1);
5685 client.subscriptions.insert(1);
5686 let target = target_frame_window(&client);
5687 fill_inflight(&mut client, target.saturating_sub(1), 512);
5689 assert!(!lead_window_open(&client, true));
5692 }
5693
5694 #[test]
5697 fn can_send_frame_when_window_open_and_time_due() {
5698 let mut client = test_client();
5699 client.next_send_at = Instant::now() - Duration::from_millis(100);
5700 assert!(can_send_frame(&client, Instant::now(), false));
5701 }
5702
5703 #[test]
5704 fn can_send_frame_false_when_not_due() {
5705 let mut client = test_client();
5706 client.next_send_at = Instant::now() + Duration::from_secs(10);
5707 assert!(!can_send_frame(&client, Instant::now(), false));
5708 }
5709
5710 #[test]
5711 fn can_send_frame_false_when_window_closed() {
5712 let mut client = test_client();
5713 client.browser_backlog_frames = 20; client.next_send_at = Instant::now() - Duration::from_millis(100);
5715 assert!(!can_send_frame(&client, Instant::now(), false));
5716 }
5717
5718 #[test]
5721 fn record_send_increases_inflight() {
5722 let mut client = test_client();
5723 let now = Instant::now();
5724 assert_eq!(client.inflight_bytes, 0);
5725 assert_eq!(client.inflight_frames.len(), 0);
5726
5727 record_send(&mut client, 1000, now, true);
5728 assert_eq!(client.inflight_bytes, 1000);
5729 assert_eq!(client.inflight_frames.len(), 1);
5730
5731 record_send(&mut client, 500, now, false);
5732 assert_eq!(client.inflight_bytes, 1500);
5733 assert_eq!(client.inflight_frames.len(), 2);
5734 }
5735
5736 #[test]
5737 fn record_send_paced_advances_deadline() {
5738 let mut client = test_client();
5739 let now = Instant::now();
5740 client.next_send_at = now;
5741 record_send(&mut client, 1000, now, true);
5742 assert!(client.next_send_at > now);
5743 }
5744
5745 #[test]
5746 fn record_send_unpaced_does_not_advance_deadline() {
5747 let mut client = test_client();
5748 let now = Instant::now();
5749 let before = client.next_send_at;
5750 record_send(&mut client, 1000, now, false);
5751 assert_eq!(client.next_send_at, before);
5752 }
5753
5754 #[test]
5755 fn record_ack_decreases_inflight() {
5756 let mut client = test_client();
5757 let now = Instant::now();
5758 record_send(&mut client, 1000, now, true);
5759 record_send(&mut client, 500, now, true);
5760 assert_eq!(client.inflight_frames.len(), 2);
5761
5762 record_ack(&mut client);
5763 assert_eq!(client.inflight_frames.len(), 1);
5764 assert_eq!(client.inflight_bytes, 500);
5765 }
5766
5767 #[test]
5768 fn record_ack_on_empty_clears_bytes() {
5769 let mut client = test_client();
5770 client.inflight_bytes = 999; record_ack(&mut client);
5772 assert_eq!(client.inflight_bytes, 0);
5773 }
5774
5775 #[test]
5776 fn record_ack_updates_rtt_estimate() {
5777 let mut client = test_client();
5778 let now = Instant::now();
5779 client.inflight_frames.push_back(InFlightFrame {
5780 sent_at: now - Duration::from_millis(20),
5781 bytes: 512,
5782 paced: true,
5783 });
5784 client.inflight_bytes = 512;
5785 let old_rtt = client.rtt_ms;
5786 record_ack(&mut client);
5787 assert!(
5789 (client.rtt_ms - old_rtt).abs() > 0.01,
5790 "rtt_ms should be updated after ack"
5791 );
5792 }
5793
5794 #[test]
5795 fn record_ack_paced_updates_avg_paced_frame_bytes() {
5796 let mut client = test_client();
5797 let now = Instant::now();
5798 client.inflight_frames.push_back(InFlightFrame {
5799 sent_at: now - Duration::from_millis(10),
5800 bytes: 4096,
5801 paced: true,
5802 });
5803 client.inflight_bytes = 4096;
5804 let old_avg = client.avg_paced_frame_bytes;
5805 record_ack(&mut client);
5806 assert!(client.avg_paced_frame_bytes > old_avg);
5808 }
5809
5810 #[test]
5811 fn record_ack_unpaced_updates_avg_preview_frame_bytes() {
5812 let mut client = test_client();
5813 let now = Instant::now();
5814 client.inflight_frames.push_back(InFlightFrame {
5815 sent_at: now - Duration::from_millis(10),
5816 bytes: 8192,
5817 paced: false,
5818 });
5819 client.inflight_bytes = 8192;
5820 let old_avg = client.avg_preview_frame_bytes;
5821 record_ack(&mut client);
5822 assert!(client.avg_preview_frame_bytes > old_avg);
5823 }
5824
5825 #[test]
5828 fn pty_list_msg_empty_session() {
5829 let sess = Session::new();
5830 let msg = sess.pty_list_msg();
5831 assert_eq!(msg[0], S2C_LIST);
5832 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 0);
5833 assert_eq!(msg.len(), 3);
5834 }
5835
5836 #[test]
5837 fn pty_list_msg_includes_tags() {
5838 let _sess = Session::new();
5839 let tag1 = "shell";
5849 let tag2 = "build";
5850
5851 let mut expected = vec![S2C_LIST];
5853 expected.extend_from_slice(&2u16.to_le_bytes());
5854 expected.extend_from_slice(&1u16.to_le_bytes());
5856 expected.extend_from_slice(&(tag1.len() as u16).to_le_bytes());
5857 expected.extend_from_slice(tag1.as_bytes());
5858 expected.extend_from_slice(&3u16.to_le_bytes());
5860 expected.extend_from_slice(&(tag2.len() as u16).to_le_bytes());
5861 expected.extend_from_slice(tag2.as_bytes());
5862
5863 assert_eq!(expected[0], S2C_LIST);
5865 assert_eq!(u16::from_le_bytes([expected[1], expected[2]]), 2);
5866 let msg_str = String::from_utf8_lossy(&expected);
5868 assert!(msg_str.contains("shell"));
5869 assert!(msg_str.contains("build"));
5870 }
5871
5872 #[test]
5875 fn can_send_preview_true_when_due() {
5876 let mut client = test_client();
5877 let now = Instant::now();
5878 client
5879 .preview_next_send_at
5880 .insert(5, now - Duration::from_millis(100));
5881 assert!(can_send_preview(&client, 5, now));
5882 }
5883
5884 #[test]
5885 fn can_send_preview_false_when_not_due() {
5886 let mut client = test_client();
5887 let now = Instant::now();
5888 client
5889 .preview_next_send_at
5890 .insert(5, now + Duration::from_secs(10));
5891 assert!(!can_send_preview(&client, 5, now));
5892 }
5893
5894 #[test]
5895 fn can_send_preview_false_when_window_closed() {
5896 let mut client = test_client();
5897 client.browser_backlog_frames = 20;
5898 let now = Instant::now();
5899 assert!(!can_send_preview(&client, 5, now));
5900 }
5901
5902 #[test]
5903 fn can_send_preview_true_for_unseen_pid() {
5904 let client = test_client();
5905 let now = Instant::now();
5906 assert!(can_send_preview(&client, 99, now));
5908 }
5909
5910 #[test]
5911 fn record_preview_send_sets_future_deadline() {
5912 let mut client = test_client();
5913 let now = Instant::now();
5914 record_preview_send(&mut client, 5, now);
5915 let deadline = client.preview_next_send_at.get(&5).unwrap();
5916 assert!(*deadline > now);
5917 }
5918
5919 #[test]
5920 fn record_preview_send_successive_calls_advance() {
5921 let mut client = test_client();
5922 let now = Instant::now();
5923 record_preview_send(&mut client, 5, now);
5924 let first = *client.preview_next_send_at.get(&5).unwrap();
5925 record_preview_send(&mut client, 5, first);
5926 let second = *client.preview_next_send_at.get(&5).unwrap();
5927 assert!(second > first, "successive sends should advance deadline");
5928 }
5929
5930 fn browser_ready_high_bandwidth_client() -> ClientState {
5944 let mut c = test_client();
5945 c.display_fps = 120.0;
5946 c.rtt_ms = 1.0;
5947 c.min_rtt_ms = 1.0;
5948 c.goodput_bps = 50_000_000.0;
5949 c.delivery_bps = 50_000_000.0;
5950 c.last_goodput_sample_bps = 50_000_000.0;
5951 c.avg_paced_frame_bytes = 30_000.0;
5952 c.avg_preview_frame_bytes = 1_024.0;
5953 c.avg_frame_bytes = 30_000.0;
5954 c.browser_apply_ms = 0.3;
5955 c
5956 }
5957
5958 fn congested_client() -> ClientState {
5961 let mut c = test_client();
5962 c.display_fps = 120.0;
5963 c.rtt_ms = 500.0;
5964 c.min_rtt_ms = 40.0;
5965 c.goodput_bps = 200_000.0;
5966 c.delivery_bps = 150_000.0;
5967 c.last_goodput_sample_bps = 200_000.0;
5968 c.avg_paced_frame_bytes = 50_000.0;
5969 c.avg_preview_frame_bytes = 1_024.0;
5970 c.avg_frame_bytes = 50_000.0;
5971 c.goodput_jitter_bps = 50_000.0;
5972 c.max_goodput_jitter_bps = 200_000.0;
5973 c.browser_apply_ms = 1.0;
5974 c
5975 }
5976
5977 fn sim_ack(client: &mut ClientState, bytes: usize, rtt_ms: f32) {
5981 let sent_at = Instant::now() - Duration::from_millis(rtt_ms as u64);
5982 client.inflight_bytes += bytes;
5983 client.inflight_frames.push_back(InFlightFrame {
5984 sent_at,
5985 bytes,
5986 paced: true,
5987 });
5988 client.goodput_window_start = Instant::now() - Duration::from_millis(25);
5990 record_ack(client);
5991 }
5992
5993 fn sim_acks(client: &mut ClientState, n: usize, bytes: usize, rtt_ms: f32) {
5994 for _ in 0..n {
5995 sim_ack(client, bytes, rtt_ms);
5996 }
5997 }
5998
5999 #[test]
6002 fn browser_ready_high_bandwidth_client_uses_full_display_fps() {
6003 let client = browser_ready_high_bandwidth_client();
6004 assert!(
6005 (pacing_fps(&client) - client.display_fps).abs() < 0.01,
6006 "pacing_fps {} should equal display_fps {} when browser is ready and bandwidth is abundant",
6007 pacing_fps(&client),
6008 client.display_fps,
6009 );
6010 }
6011
6012 #[test]
6013 fn browser_ready_high_bandwidth_client_send_interval_within_one_frame() {
6014 let client = browser_ready_high_bandwidth_client();
6015 let interval_ms = send_interval(&client).as_secs_f32() * 1000.0;
6016 let frame_ms = 1000.0 / client.display_fps;
6017 assert!(
6018 interval_ms <= frame_ms + 0.1,
6019 "send_interval {interval_ms:.2}ms exceeds one frame ({frame_ms:.2}ms) when browser is ready"
6020 );
6021 }
6022
6023 #[test]
6026 fn congested_pipe_reduces_pacing_fps_substantially() {
6027 let client = congested_client();
6028 let fps = pacing_fps(&client);
6029 assert!(
6030 fps < client.display_fps * 0.5,
6031 "pacing_fps {fps:.0} should be well below display_fps {} when congested",
6032 client.display_fps,
6033 );
6034 }
6035
6036 #[test]
6037 fn congested_pipe_is_throughput_limited() {
6038 let client = congested_client();
6039 assert!(
6040 throughput_limited(&client),
6041 "congested client must be recognised as throughput-limited"
6042 );
6043 }
6044
6045 #[test]
6051 fn byte_window_bounded_near_bdp_when_congested() {
6052 let client = congested_client();
6053 let bdp = client.goodput_bps * (path_rtt_ms(&client) / 1_000.0);
6055 let window = target_byte_window(&client);
6056 assert!(
6057 window < bdp as usize * 8,
6058 "byte window {window}B is {:.1}× BDP ({bdp:.0}B); \
6059 expected ≤ 8× — lead_floor may be dominating",
6060 window as f32 / bdp.max(1.0),
6061 );
6062 }
6063
6064 #[test]
6070 fn min_rtt_not_contaminated_by_congested_rtts() {
6071 let mut client = test_client();
6072 client.display_fps = 120.0;
6073 client.rtt_ms = 40.0;
6074 client.min_rtt_ms = 40.0;
6075 client.goodput_bps = 2_000_000.0;
6076 client.delivery_bps = 2_000_000.0;
6077 client.avg_paced_frame_bytes = 30_000.0;
6078 client.avg_preview_frame_bytes = 1_024.0;
6079 let original_min = client.min_rtt_ms;
6080
6081 sim_acks(&mut client, 200, 30_000, 500.0);
6083
6084 assert!(
6085 client.min_rtt_ms < original_min * 2.0,
6086 "min_rtt drifted from {original_min}ms to {:.1}ms after 200 congested ACKs",
6087 client.min_rtt_ms,
6088 );
6089 }
6090
6091 #[test]
6094 fn delivery_bps_rises_quickly_when_congestion_clears() {
6095 let mut client = congested_client();
6096 let before = client.delivery_bps;
6097
6098 sim_acks(&mut client, 10, 30_000, 40.0);
6100
6101 assert!(
6102 client.delivery_bps > before * 2.0,
6103 "delivery_bps {:.0} should more than double from {before:.0} after 10 fast ACKs",
6104 client.delivery_bps,
6105 );
6106 }
6107
6108 #[test]
6109 fn pacing_fps_recovers_after_congestion_clears() {
6110 let mut client = congested_client();
6111
6112 for _ in 0..40 {
6118 let target = target_frame_window(&client).max(2);
6119 for _ in 0..target {
6120 let sent_at = Instant::now() - Duration::from_millis(40);
6121 client.inflight_bytes += 30_000;
6122 client.inflight_frames.push_back(InFlightFrame {
6123 sent_at,
6124 bytes: 30_000,
6125 paced: true,
6126 });
6127 }
6128 client.goodput_window_start = Instant::now() - Duration::from_millis(25);
6129 for _ in 0..target {
6130 record_ack(&mut client);
6131 }
6132 }
6133
6134 let fps = pacing_fps(&client);
6135 assert!(
6136 fps > client.display_fps * 0.7,
6137 "pacing_fps {fps:.0} didn't recover toward display_fps {} \
6138 after window-saturated rounds at low RTT",
6139 client.display_fps,
6140 );
6141 }
6142
6143 #[test]
6144 fn rtt_estimate_drops_quickly_when_congestion_clears() {
6145 let mut client = test_client();
6146 client.rtt_ms = 500.0;
6147 client.min_rtt_ms = 40.0;
6148 client.goodput_bps = 2_000_000.0;
6149 client.avg_paced_frame_bytes = 30_000.0;
6150 client.avg_preview_frame_bytes = 1_024.0;
6151
6152 sim_acks(&mut client, 10, 30_000, 40.0);
6155
6156 assert!(
6157 client.rtt_ms < 300.0,
6158 "rtt_ms {:.0}ms did not fall fast enough after congestion cleared",
6159 client.rtt_ms,
6160 );
6161 }
6162
6163 #[test]
6166 fn probe_collapses_immediately_on_queue_delay() {
6167 let mut client = test_client();
6168 client.display_fps = 120.0;
6169 client.rtt_ms = 40.0;
6170 client.min_rtt_ms = 40.0;
6171 client.goodput_bps = 5_000_000.0;
6172 client.delivery_bps = 5_000_000.0;
6173 client.last_goodput_sample_bps = 5_000_000.0;
6174 client.avg_paced_frame_bytes = 10_000.0;
6175 client.avg_preview_frame_bytes = 1_024.0;
6176 client.probe_frames = 10.0;
6177
6178 sim_acks(&mut client, 5, 10_000, 600.0);
6180
6181 assert!(
6182 client.probe_frames < 5.0,
6183 "probe_frames {:.1} should have collapsed on queue delay signal",
6184 client.probe_frames,
6185 );
6186 }
6187
6188 #[test]
6189 fn probe_grows_when_window_saturated_with_clean_rtt() {
6190 let mut client = test_client();
6191 client.display_fps = 120.0;
6192 client.rtt_ms = 40.0;
6193 client.min_rtt_ms = 40.0;
6194 client.goodput_bps = 5_000_000.0;
6195 client.delivery_bps = 5_000_000.0;
6196 client.last_goodput_sample_bps = 5_000_000.0;
6197 client.avg_paced_frame_bytes = 10_000.0;
6198 client.avg_preview_frame_bytes = 1_024.0;
6199 client.goodput_jitter_bps = 0.0;
6200 client.max_goodput_jitter_bps = 0.0;
6201 client.probe_frames = 0.0;
6202
6203 let target = target_frame_window(&client);
6205 for _ in 0..target {
6206 let sent_at = Instant::now() - Duration::from_millis(40);
6207 client.inflight_bytes += 10_000;
6208 client.inflight_frames.push_back(InFlightFrame {
6209 sent_at,
6210 bytes: 10_000,
6211 paced: true,
6212 });
6213 }
6214
6215 record_ack(&mut client);
6224
6225 assert!(
6226 client.probe_frames > 0.0,
6227 "probe_frames should grow when window-saturated with clean RTT"
6228 );
6229 }
6230
6231 #[test]
6234 fn frame_window_larger_on_high_latency_link() {
6235 let mut lo = test_client();
6236 lo.display_fps = 120.0;
6237 lo.rtt_ms = 10.0;
6238 lo.min_rtt_ms = 10.0;
6239 lo.goodput_bps = 5_000_000.0;
6240 lo.delivery_bps = 5_000_000.0;
6241 lo.avg_paced_frame_bytes = 10_000.0;
6242 lo.avg_preview_frame_bytes = 1_024.0;
6243
6244 let mut hi = test_client();
6245 hi.display_fps = 120.0;
6246 hi.rtt_ms = 200.0;
6247 hi.min_rtt_ms = 200.0;
6248 hi.goodput_bps = 5_000_000.0;
6249 hi.delivery_bps = 5_000_000.0;
6250 hi.avg_paced_frame_bytes = 10_000.0;
6251 hi.avg_preview_frame_bytes = 1_024.0;
6252
6253 let lo_win = target_frame_window(&lo);
6254 let hi_win = target_frame_window(&hi);
6255 assert!(
6256 hi_win > lo_win,
6257 "high-latency link ({hi_win}f) should need more frames in flight \
6258 than low-latency ({lo_win}f)"
6259 );
6260 }
6261
6262 #[test]
6265 fn small_frame_byte_window_enables_pipelining() {
6266 let mut client = test_client();
6271 client.display_fps = 120.0;
6272 client.rtt_ms = 165.0;
6273 client.min_rtt_ms = 8.0;
6274 client.goodput_bps = 11_000.0; client.delivery_bps = 6_800.0;
6276 client.last_goodput_sample_bps = 11_000.0;
6277 client.avg_paced_frame_bytes = 1_120.0;
6278 client.avg_preview_frame_bytes = 1_024.0;
6279 client.goodput_jitter_bps = 4_300.0;
6280 client.max_goodput_jitter_bps = 6_500.0;
6281
6282 let window = target_byte_window(&client);
6283 let frames = target_frame_window(&client);
6284 let pipeline = frames * 1_120;
6285
6286 assert!(
6287 window >= pipeline,
6288 "byte window {window}B should be >= pipeline ({frames}f × 1120B = {pipeline}B) \
6289 so small frames can pipeline across the RTT"
6290 );
6291 }
6292
6293 #[test]
6294 fn large_frame_byte_window_bounded_by_one_frame_floor() {
6295 let mut client = test_client();
6299 client.display_fps = 120.0;
6300 client.rtt_ms = 165.0;
6301 client.min_rtt_ms = 8.0;
6302 client.goodput_bps = 11_000.0;
6303 client.delivery_bps = 6_800.0;
6304 client.last_goodput_sample_bps = 11_000.0;
6305 client.avg_paced_frame_bytes = 50_000.0; client.avg_preview_frame_bytes = 1_024.0;
6307 client.goodput_jitter_bps = 0.0;
6308 client.max_goodput_jitter_bps = 0.0;
6309
6310 let window = target_byte_window(&client);
6311 let frames = target_frame_window(&client);
6312 let pipeline = frames.saturating_mul(50_000);
6313
6314 assert!(
6315 window < pipeline,
6316 "byte window {window}B should be < full pipeline {pipeline}B \
6317 ({frames}f × 50KB) — large frames must use one-frame floor"
6318 );
6319 assert!(
6320 window >= 50_000,
6321 "byte window {window}B must be at least one frame (50KB)"
6322 );
6323 }
6324
6325 #[test]
6328 fn preview_reservation_applies_even_on_low_latency_high_bandwidth_links() {
6329 let mut client = browser_ready_high_bandwidth_client();
6330 client.lead = Some(1);
6331 client.subscriptions.insert(1);
6332 let target = target_frame_window(&client);
6333 fill_inflight(&mut client, target.saturating_sub(1), 512);
6334 assert!(
6335 !lead_window_open(&client, true),
6336 "preview reservation should apply uniformly for lead clients"
6337 );
6338 }
6339
6340 #[test]
6343 fn probe_recovers_on_healthy_path_after_blip() {
6344 let mut client = browser_ready_high_bandwidth_client();
6345 client.probe_frames = 8.0;
6346
6347 sim_acks(&mut client, 3, 30_000, 200.0);
6349 let post_blip = client.probe_frames;
6350 assert!(
6351 post_blip < 4.0,
6352 "probe_frames {post_blip:.1} should have dropped after blip"
6353 );
6354
6355 client.browser_backlog_frames = 0;
6357 client.browser_ack_ahead_frames = 0;
6358 client.browser_apply_ms = 0.3;
6359
6360 sim_acks(&mut client, 20, 30_000, 1.0);
6362
6363 assert!(
6364 client.probe_frames > post_blip,
6365 "probe_frames {:.1} should have recovered from {post_blip:.1} after healthy ACKs",
6366 client.probe_frames,
6367 );
6368 }
6369
6370 #[test]
6371 fn jitter_decays_fast_on_browser_ready_path() {
6372 let mut client = browser_ready_high_bandwidth_client();
6373
6374 client.max_goodput_jitter_bps = client.goodput_bps * 0.4;
6376 client.goodput_jitter_bps = client.goodput_bps * 0.3;
6377 let initial_jitter = client.max_goodput_jitter_bps;
6378
6379 sim_acks(&mut client, 10, 30_000, 1.0);
6381
6382 assert!(
6383 client.max_goodput_jitter_bps < initial_jitter * 0.5,
6384 "max_goodput_jitter_bps {:.0} should have decayed below {:.0} \
6385 (50% of initial {initial_jitter:.0}) after 10 healthy ACKs on a ready path",
6386 client.max_goodput_jitter_bps,
6387 initial_jitter * 0.5,
6388 );
6389 }
6390
6391 #[test]
6392 fn byte_budget_uses_floor_when_goodput_depressed() {
6393 let mut client = browser_ready_high_bandwidth_client();
6394 client.goodput_bps = 100_000.0;
6395
6396 let budget = byte_budget_for(&client, 100.0);
6397 let floor_budget = (bandwidth_floor_bps(&client) * 100.0 / 1_000.0).ceil() as usize;
6398
6399 assert!(
6400 budget >= floor_budget,
6401 "byte_budget {budget} should be at least bandwidth_floor-based {floor_budget} \
6402 when goodput_bps is depressed but delivery_bps is high"
6403 );
6404 }
6405
6406 #[test]
6407 fn probe_floor_maintained_under_congestion_signal() {
6408 let mut client = test_client();
6409 client.display_fps = 120.0;
6410 client.rtt_ms = 40.0;
6411 client.min_rtt_ms = 40.0;
6412 client.goodput_bps = 5_000_000.0;
6413 client.delivery_bps = 5_000_000.0;
6414 client.last_goodput_sample_bps = 5_000_000.0;
6415 client.avg_paced_frame_bytes = 10_000.0;
6416 client.avg_preview_frame_bytes = 1_024.0;
6417 client.probe_frames = 10.0;
6418
6419 sim_acks(&mut client, 20, 10_000, 600.0);
6421
6422 assert!(
6423 client.probe_frames >= 1.0,
6424 "probe_frames {:.1} should not drop below the floor of 1.0",
6425 client.probe_frames,
6426 );
6427 }
6428
6429 #[test]
6432 fn parse_tq_da1_bare() {
6433 let results = parse_terminal_queries(b"\x1b[c", (24, 80), (0, 0));
6434 assert_eq!(results.len(), 1);
6435 assert!(results[0].starts_with("\x1b[?64;"));
6436 }
6437
6438 #[test]
6439 fn parse_tq_da1_with_zero_param() {
6440 let results = parse_terminal_queries(b"\x1b[0c", (24, 80), (0, 0));
6441 assert_eq!(results.len(), 1);
6442 assert!(results[0].starts_with("\x1b[?64;"));
6443 }
6444
6445 #[test]
6446 fn parse_tq_dsr_cursor_position() {
6447 let results = parse_terminal_queries(b"\x1b[6n", (24, 80), (5, 10));
6448 assert_eq!(results.len(), 1);
6449 assert_eq!(results[0], "\x1b[6;11R");
6450 }
6451
6452 #[test]
6453 fn parse_tq_dsr_status() {
6454 let results = parse_terminal_queries(b"\x1b[5n", (24, 80), (0, 0));
6455 assert_eq!(results.len(), 1);
6456 assert_eq!(results[0], "\x1b[0n");
6457 }
6458
6459 #[test]
6460 fn parse_tq_window_size_cells() {
6461 let results = parse_terminal_queries(b"\x1b[18t", (24, 80), (0, 0));
6462 assert_eq!(results.len(), 1);
6463 assert_eq!(results[0], "\x1b[8;24;80t");
6464 }
6465
6466 #[test]
6467 fn parse_tq_window_size_pixels() {
6468 let results = parse_terminal_queries(b"\x1b[14t", (30, 100), (0, 0));
6469 assert_eq!(results.len(), 1);
6470 assert_eq!(results[0], "\x1b[4;480;800t");
6471 }
6472
6473 #[test]
6474 fn parse_tq_multiple_queries() {
6475 let data = b"\x1b[c\x1b[6n\x1b[5n";
6476 let results = parse_terminal_queries(data, (24, 80), (2, 3));
6477 assert_eq!(results.len(), 3);
6478 assert!(results[0].starts_with("\x1b[?64;"));
6479 assert_eq!(results[1], "\x1b[3;4R");
6480 assert_eq!(results[2], "\x1b[0n");
6481 }
6482
6483 #[test]
6484 fn parse_tq_question_mark_sequences_skipped() {
6485 let results = parse_terminal_queries(b"\x1b[?1h", (24, 80), (0, 0));
6486 assert!(results.is_empty());
6487 }
6488
6489 #[test]
6490 fn parse_tq_unknown_final_byte_ignored() {
6491 let results = parse_terminal_queries(b"\x1b[42z", (24, 80), (0, 0));
6492 assert!(results.is_empty());
6493 }
6494
6495 #[test]
6496 fn parse_tq_empty_input() {
6497 let results = parse_terminal_queries(b"", (24, 80), (0, 0));
6498 assert!(results.is_empty());
6499 }
6500
6501 #[test]
6502 fn parse_tq_plain_text_no_csi() {
6503 let results = parse_terminal_queries(b"hello world", (24, 80), (0, 0));
6504 assert!(results.is_empty());
6505 }
6506
6507 #[test]
6508 fn parse_tq_interleaved_with_text() {
6509 let results = parse_terminal_queries(b"abc\x1b[cdef\x1b[6n", (24, 80), (1, 2));
6510 assert_eq!(results.len(), 2);
6511 }
6512
6513 #[test]
6516 fn parse_tq_osc11_background_color_bel() {
6517 let results = parse_terminal_queries(b"\x1b]11;?\x07", (24, 80), (0, 0));
6518 assert_eq!(results.len(), 1);
6519 assert_eq!(results[0], "\x1b]11;rgb:0000/0000/0000\x1b\\");
6520 }
6521
6522 #[test]
6523 fn parse_tq_osc11_background_color_st() {
6524 let results = parse_terminal_queries(b"\x1b]11;?\x1b\\", (24, 80), (0, 0));
6525 assert_eq!(results.len(), 1);
6526 assert_eq!(results[0], "\x1b]11;rgb:0000/0000/0000\x1b\\");
6527 }
6528
6529 #[test]
6530 fn parse_tq_osc10_foreground_color() {
6531 let results = parse_terminal_queries(b"\x1b]10;?\x07", (24, 80), (0, 0));
6532 assert_eq!(results.len(), 1);
6533 assert_eq!(results[0], "\x1b]10;rgb:ffff/ffff/ffff\x1b\\");
6534 }
6535
6536 #[test]
6537 fn parse_tq_osc4_palette_color_0() {
6538 let results = parse_terminal_queries(b"\x1b]4;0;?\x07", (24, 80), (0, 0));
6539 assert_eq!(results.len(), 1);
6540 assert_eq!(results[0], "\x1b]4;0;rgb:0000/0000/0000\x1b\\");
6541 }
6542
6543 #[test]
6544 fn parse_tq_osc4_palette_color_1() {
6545 let results = parse_terminal_queries(b"\x1b]4;1;?\x07", (24, 80), (0, 0));
6546 assert_eq!(results.len(), 1);
6547 assert_eq!(results[0], "\x1b]4;1;rgb:8080/0000/0000\x1b\\");
6548 }
6549
6550 #[test]
6551 fn parse_tq_osc_mixed_with_csi() {
6552 let results =
6553 parse_terminal_queries(b"\x1b]11;?\x07\x1b[c\x1b]4;0;?\x07", (24, 80), (0, 0));
6554 assert_eq!(results.len(), 3);
6555 assert!(results[0].starts_with("\x1b]11;"));
6556 assert!(results[1].starts_with("\x1b[?64;"));
6557 assert!(results[2].starts_with("\x1b]4;0;"));
6558 }
6559
6560 #[test]
6563 fn search_results_empty() {
6564 let msg = build_search_results_msg(42, &[]);
6565 assert_eq!(msg[0], S2C_SEARCH_RESULTS);
6566 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 42);
6567 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 0);
6568 assert_eq!(msg.len(), 5);
6569 }
6570
6571 #[test]
6572 fn search_results_single() {
6573 let results = vec![SearchResultRow {
6574 pty_id: 7,
6575 score: 100,
6576 primary_source: 1,
6577 matched_sources: 3,
6578 context: "hello".into(),
6579 scroll_offset: Some(42),
6580 }];
6581 let msg = build_search_results_msg(1, &results);
6582 assert_eq!(msg[0], S2C_SEARCH_RESULTS);
6583 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 1);
6584 let pty_id = u16::from_le_bytes([msg[5], msg[6]]);
6585 assert_eq!(pty_id, 7);
6586 let score = u32::from_le_bytes([msg[7], msg[8], msg[9], msg[10]]);
6587 assert_eq!(score, 100);
6588 assert_eq!(msg[11], 1);
6589 assert_eq!(msg[12], 3);
6590 let scroll = u32::from_le_bytes([msg[13], msg[14], msg[15], msg[16]]);
6591 assert_eq!(scroll, 42);
6592 let ctx_len = u16::from_le_bytes([msg[17], msg[18]]) as usize;
6593 assert_eq!(ctx_len, 5);
6594 assert_eq!(&msg[19..19 + ctx_len], b"hello");
6595 }
6596
6597 #[test]
6598 fn search_results_none_scroll_offset() {
6599 let results = vec![SearchResultRow {
6600 pty_id: 1,
6601 score: 0,
6602 primary_source: 0,
6603 matched_sources: 0,
6604 context: String::new(),
6605 scroll_offset: None,
6606 }];
6607 let msg = build_search_results_msg(0, &results);
6608 let scroll = u32::from_le_bytes([msg[13], msg[14], msg[15], msg[16]]);
6609 assert_eq!(scroll, u32::MAX);
6610 }
6611
6612 #[test]
6615 fn allocate_pty_id_empty_session() {
6616 let mut sess = Session::new();
6617 assert_eq!(sess.allocate_pty_id(0), Some(1));
6618 }
6619
6620 #[test]
6621 fn allocate_pty_id_rotates() {
6622 let mut sess = Session::new();
6623 assert_eq!(sess.allocate_pty_id(0), Some(1));
6625 assert_eq!(sess.allocate_pty_id(0), Some(2));
6626 assert_eq!(sess.allocate_pty_id(0), Some(3));
6627 }
6628
6629 #[test]
6630 fn allocate_pty_id_wraps_at_max() {
6631 let mut sess = Session::new();
6632 sess.next_pty_id = u16::MAX;
6633 assert_eq!(sess.allocate_pty_id(0), Some(u16::MAX));
6634 assert_eq!(sess.allocate_pty_id(0), Some(1));
6636 }
6637
6638 #[test]
6641 fn try_send_no_change() {
6642 let mut client = test_client();
6643 let frame = sample_frame("x");
6644 let now = Instant::now();
6645 let outcome = try_send_update(&mut client, 1, frame, None, now, false);
6646 assert!(matches!(outcome, SendOutcome::NoChange));
6647 }
6648
6649 #[test]
6650 fn try_send_sent() {
6651 let (mut client, _rx) = test_client_with_capacity(8);
6652 let frame = sample_frame("x");
6653 let now = Instant::now();
6654 let outcome = try_send_update(
6655 &mut client,
6656 1,
6657 frame.clone(),
6658 Some(vec![1, 2, 3]),
6659 now,
6660 true,
6661 );
6662 assert!(matches!(outcome, SendOutcome::Sent));
6663 assert!(client.last_sent.contains_key(&1));
6664 }
6665
6666 #[test]
6667 fn try_send_backpressured() {
6668 let (mut client, _rx) = test_client_with_capacity(1);
6669 let frame = sample_frame("x");
6670 let now = Instant::now();
6671 let _ = try_send_outbox(&client, vec![0]);
6672 let outcome = try_send_update(
6673 &mut client,
6674 1,
6675 frame.clone(),
6676 Some(vec![1, 2, 3]),
6677 now,
6678 true,
6679 );
6680 assert!(matches!(outcome, SendOutcome::Backpressured));
6681 assert!(
6682 client.last_sent.contains_key(&1),
6683 "last_sent should advance even on backpressure"
6684 );
6685 }
6686}