1use blit_alacritty::{SearchResult as AlacrittySearchResult, TerminalDriver as AlacrittyDriver};
2use blit_compositor::{CompositorCommand, CompositorEvent, CompositorHandle};
3use blit_remote::{
4 C2S_ACK, C2S_CLIENT_METRICS, C2S_CLIPBOARD, C2S_CLOSE, C2S_COPY_RANGE, C2S_CREATE,
5 C2S_CREATE_AT, C2S_CREATE_N, C2S_CREATE2, C2S_DISPLAY_RATE, C2S_FOCUS, C2S_INPUT, C2S_KILL,
6 C2S_MOUSE, C2S_READ, C2S_RESIZE, C2S_RESTART, C2S_SCROLL, C2S_SEARCH, C2S_SUBSCRIBE,
7 C2S_SURFACE_ACK, C2S_SURFACE_CAPTURE, C2S_SURFACE_CLOSE, C2S_SURFACE_FOCUS, C2S_SURFACE_INPUT,
8 C2S_SURFACE_LIST, C2S_SURFACE_POINTER, C2S_SURFACE_POINTER_AXIS, C2S_SURFACE_REQUEST_KEYFRAME,
9 C2S_SURFACE_RESIZE, C2S_SURFACE_SUBSCRIBE, C2S_SURFACE_UNSUBSCRIBE, C2S_UNSUBSCRIBE,
10 CAPTURE_FORMAT_AVIF, CAPTURE_FORMAT_PNG, CREATE2_HAS_COMMAND, CREATE2_HAS_SRC_PTY,
11 FEATURE_COMPOSITOR, FEATURE_COPY_RANGE, FEATURE_CREATE_NONCE, FEATURE_RESIZE_BATCH,
12 FEATURE_RESTART, FrameState, READ_ANSI, READ_TAIL, S2C_CLOSED, S2C_CREATED, S2C_CREATED_N,
13 S2C_LIST, S2C_READY, S2C_SEARCH_RESULTS, S2C_SURFACE_CAPTURE, S2C_SURFACE_LIST, S2C_TEXT,
14 S2C_TITLE, SURFACE_FRAME_FLAG_KEYFRAME, build_update_msg, msg_hello, msg_s2c_clipboard,
15 msg_surface_app_id, msg_surface_created, msg_surface_destroyed, msg_surface_frame,
16 msg_surface_resized, msg_surface_title,
17};
18use std::collections::{HashMap, HashSet, VecDeque};
19use std::sync::Arc;
20use std::time::{Duration, Instant};
21use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
22use tokio::sync::{Mutex, Notify, mpsc};
23
24#[cfg(unix)]
25mod dmabuf_zerocopy;
26mod gpu_libs;
27mod ipc;
28mod nvenc_encode;
29mod pty;
30mod surface_encoder;
31#[cfg(unix)]
32mod vaapi_encode;
33
34pub use ipc::{IpcListener, default_ipc_path};
35use pty::{PtyHandle, PtyWriteTarget};
36use surface_encoder::SurfaceEncoder;
37pub use surface_encoder::SurfaceEncoderPreference;
38pub use surface_encoder::SurfaceH264EncoderPreference;
39pub use surface_encoder::SurfaceQuality;
40
41type PtyFds = Arc<std::sync::RwLock<HashMap<u16, PtyWriteTarget>>>;
42pub struct Config {
43 pub shell: String,
44 pub shell_flags: String,
45 pub scrollback: usize,
46 pub ipc_path: String,
47 pub surface_encoders: Vec<SurfaceEncoderPreference>,
48 pub surface_quality: SurfaceQuality,
49 pub vaapi_device: String,
50 #[cfg(unix)]
51 pub fd_channel: Option<std::os::unix::io::RawFd>,
52 pub verbose: bool,
53 pub max_connections: usize,
55 pub max_ptys: usize,
57}
58
59trait PtyDriver: Send {
60 fn size(&self) -> (u16, u16);
61 fn resize(&mut self, rows: u16, cols: u16);
62 fn process(&mut self, data: &[u8]);
63 fn title(&self) -> &str;
64 fn search_result(&self, query: &str) -> Option<PtySearchResult>;
65 fn take_title_dirty(&mut self) -> bool;
66 fn cursor_position(&self) -> (u16, u16);
67 fn synced_output(&self) -> bool;
68 fn snapshot(&mut self, echo: bool, icanon: bool) -> FrameState;
69 fn scrollback_frame(&mut self, offset: usize) -> FrameState;
70 fn reset_modes(&mut self);
71 fn mouse_event(
72 &self,
73 type_: u8,
74 button: u8,
75 col: u16,
76 row: u16,
77 echo: bool,
78 icanon: bool,
79 ) -> Option<Vec<u8>>;
80 fn get_text_range(
81 &self,
82 start_tail: u32,
83 start_col: u16,
84 end_tail: u32,
85 end_col: u16,
86 ) -> String;
87 fn total_lines(&self) -> u32;
88}
89
90struct PtySearchResult {
91 score: u32,
92 primary_source: u8,
93 matched_sources: u8,
94 context: String,
95 scroll_offset: Option<usize>,
96}
97
98impl PtyDriver for AlacrittyDriver {
99 fn size(&self) -> (u16, u16) {
100 AlacrittyDriver::size(self)
101 }
102
103 fn resize(&mut self, rows: u16, cols: u16) {
104 AlacrittyDriver::resize(self, rows, cols);
105 }
106
107 fn process(&mut self, data: &[u8]) {
108 AlacrittyDriver::process(self, data);
109 }
110
111 fn title(&self) -> &str {
112 AlacrittyDriver::title(self)
113 }
114
115 fn search_result(&self, query: &str) -> Option<PtySearchResult> {
116 AlacrittyDriver::search_result(self, query).map(|result: AlacrittySearchResult| {
117 PtySearchResult {
118 score: result.score,
119 primary_source: result.primary_source as u8,
120 matched_sources: result.matched_sources,
121 context: result.context,
122 scroll_offset: result.scroll_offset,
123 }
124 })
125 }
126
127 fn take_title_dirty(&mut self) -> bool {
128 AlacrittyDriver::take_title_dirty(self)
129 }
130
131 fn cursor_position(&self) -> (u16, u16) {
132 AlacrittyDriver::cursor_position(self)
133 }
134
135 fn synced_output(&self) -> bool {
136 AlacrittyDriver::synced_output(self)
137 }
138
139 fn snapshot(&mut self, echo: bool, icanon: bool) -> FrameState {
140 AlacrittyDriver::snapshot(self, echo, icanon)
141 }
142
143 fn scrollback_frame(&mut self, offset: usize) -> FrameState {
144 AlacrittyDriver::scrollback_frame(self, offset)
145 }
146
147 fn reset_modes(&mut self) {
148 AlacrittyDriver::reset_modes(self);
149 }
150
151 fn mouse_event(
152 &self,
153 type_: u8,
154 button: u8,
155 col: u16,
156 row: u16,
157 echo: bool,
158 icanon: bool,
159 ) -> Option<Vec<u8>> {
160 AlacrittyDriver::mouse_event(self, type_, button, col, row, echo, icanon)
161 }
162
163 fn get_text_range(
164 &self,
165 start_tail: u32,
166 start_col: u16,
167 end_tail: u32,
168 end_col: u16,
169 ) -> String {
170 AlacrittyDriver::get_text_range(self, start_tail, start_col, end_tail, end_col)
171 }
172
173 fn total_lines(&self) -> u32 {
174 AlacrittyDriver::total_lines(self)
175 }
176}
177
178const OUTBOX_CAPACITY: usize = 8;
182const OUTBOX_SOFT_QUEUE_LIMIT_FRAMES: usize = 2;
183const PREVIEW_FRAME_RESERVE: usize = 1;
184const READY_FRAME_QUEUE_CAP: usize = 4;
185const PTY_CHANNEL_CAPACITY: usize = 64;
186const SYNC_OUTPUT_END: &[u8] = b"\x1b[?2026l";
187
188enum PtyInput {
191 Data(Vec<u8>),
194 SyncBoundary { before: Vec<u8>, after: Vec<u8> },
197 Eof,
199}
200
201const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024;
202
203async fn read_frame(reader: &mut (impl AsyncRead + Unpin)) -> Option<Vec<u8>> {
204 let mut len_buf = [0u8; 4];
205 reader.read_exact(&mut len_buf).await.ok()?;
206 let len = u32::from_le_bytes(len_buf) as usize;
207 if len == 0 {
208 return Some(vec![]);
209 }
210 if len > MAX_FRAME_SIZE {
211 return None;
212 }
213 let mut buf = vec![0u8; len];
214 reader.read_exact(&mut buf).await.ok()?;
215 Some(buf)
216}
217
218async fn write_frame(writer: &mut (impl AsyncWrite + Unpin), payload: &[u8]) -> bool {
219 if payload.len() > u32::MAX as usize {
220 return false;
221 }
222 let len = payload.len() as u32;
223 let mut buf = Vec::with_capacity(4 + payload.len());
224 buf.extend_from_slice(&len.to_le_bytes());
225 buf.extend_from_slice(payload);
226 writer.write_all(&buf).await.is_ok()
227}
228
229struct Pty {
230 handle: PtyHandle,
231 driver: Box<dyn PtyDriver>,
232 tag: String,
234 dirty: bool,
235 ready_frames: VecDeque<FrameState>,
236 byte_rx: mpsc::Receiver<PtyInput>,
238 reader_handle: std::thread::JoinHandle<()>,
239 lflag_cache: (bool, bool),
241 lflag_last: Instant,
242 last_title_send: Instant,
244 title_pending: bool,
246 exited: bool,
248 exit_status: i32,
251 command: Option<String>,
253}
254
255impl Pty {
256 fn mark_dirty(&mut self) {
257 self.dirty = true;
258 }
259
260 fn clear_dirty(&mut self) {
261 self.dirty = false;
262 }
263}
264
265struct CachedSurfaceInfo {
266 surface_id: u16,
267 parent_id: u16,
268 width: u16,
269 height: u16,
270 scale_120: u16,
274 title: String,
275 app_id: String,
276}
277
278struct BufferedSurfaceFrame {
279 _msg: Vec<u8>,
280 _is_keyframe: bool,
281}
282
283struct LastPixels {
286 width: u32,
287 height: u32,
288 pixels: blit_compositor::PixelData,
289 generation: u64,
292}
293
294struct SharedCompositor {
295 session_id: u16,
296 handle: CompositorHandle,
297 surfaces: HashMap<u16, CachedSurfaceInfo>,
298 last_pixels: HashMap<u16, LastPixels>,
299 pending_frame_requests: HashSet<u16>,
303 created_at: Instant,
304 pixel_generation: u64,
306}
307
308fn encode_rgba_to_png(pixels: &[u8], width: u32, height: u32) -> Vec<u8> {
309 let mut buf = Vec::new();
310 {
311 let expected = (width * height * 4) as usize;
312 let actual = pixels.len();
313 if actual != expected {
314 let mut encoder = png::Encoder::new(&mut buf, 1, 1);
316 encoder.set_color(png::ColorType::Rgba);
317 encoder.set_depth(png::BitDepth::Eight);
318 let mut writer = encoder.write_header().unwrap();
319 writer.write_image_data(&[255, 0, 0, 255]).unwrap();
320 eprintln!(
321 "[capture] pixel buffer size mismatch: {width}x{height} expected {expected} got {actual}"
322 );
323 } else {
324 let mut encoder = png::Encoder::new(&mut buf, width, height);
325 encoder.set_color(png::ColorType::Rgba);
326 encoder.set_depth(png::BitDepth::Eight);
327 let mut writer = encoder.write_header().unwrap();
328 writer.write_image_data(pixels).unwrap();
329 }
330 }
331 buf
332}
333
334fn encode_rgba_to_avif(pixels: &[u8], width: u32, height: u32, quality: u8) -> Vec<u8> {
336 let rgba: Vec<rgb::RGBA8> = pixels
337 .chunks_exact(4)
338 .map(|c| rgb::RGBA8::new(c[0], c[1], c[2], c[3]))
339 .collect();
340 let img = ravif::Img::new(&rgba[..], width as usize, height as usize);
341 let q = if quality == 0 { 100.0 } else { quality as f32 };
342 let encoder = ravif::Encoder::new()
343 .with_quality(q)
344 .with_alpha_quality(q)
345 .with_speed(6)
346 .with_alpha_color_mode(ravif::AlphaColorMode::UnassociatedClean)
347 .with_num_threads(None);
348 let result = encoder.encode_rgba(img).expect("AVIF encoding failed");
349 result.avif_file
350}
351
352fn encode_capture(pixels: &[u8], width: u32, height: u32, format: u8, quality: u8) -> Vec<u8> {
354 match format {
355 CAPTURE_FORMAT_AVIF => encode_rgba_to_avif(pixels, width, height, quality),
356 _ => encode_rgba_to_png(pixels, width, height),
357 }
358}
359
360#[allow(dead_code)] async fn request_surface_capture(
362 command_tx: std::sync::mpsc::Sender<CompositorCommand>,
363 surface_id: u16,
364) -> Option<(u32, u32, Vec<u8>)> {
365 request_surface_capture_with_timeout(command_tx, surface_id, Duration::from_secs(1)).await
366}
367
368#[allow(dead_code)] async fn request_surface_capture_with_timeout(
370 command_tx: std::sync::mpsc::Sender<CompositorCommand>,
371 surface_id: u16,
372 timeout: Duration,
373) -> Option<(u32, u32, Vec<u8>)> {
374 let (tx, rx) = std::sync::mpsc::sync_channel(1);
375 command_tx
376 .send(CompositorCommand::Capture {
377 surface_id,
378 reply: tx,
379 })
380 .ok()?;
381
382 tokio::task::spawn_blocking(move || rx.recv_timeout(timeout))
386 .await
387 .ok()?
388 .ok()
389 .flatten()
390}
391
392struct ClientState {
393 tx: mpsc::Sender<Vec<u8>>,
394 lead: Option<u16>,
395 subscriptions: HashSet<u16>,
396 surface_subscriptions: HashSet<u16>,
397 view_sizes: HashMap<u16, (u16, u16)>,
398 scroll_offsets: HashMap<u16, usize>,
399 scroll_caches: HashMap<u16, FrameState>,
400 last_sent: HashMap<u16, FrameState>,
401 preview_next_send_at: HashMap<u16, Instant>,
402 rtt_ms: f32,
404 min_rtt_ms: f32,
406 display_fps: f32,
408 delivery_bps: f32,
410 goodput_bps: f32,
412 goodput_jitter_bps: f32,
414 max_goodput_jitter_bps: f32,
416 last_goodput_sample_bps: f32,
418 avg_frame_bytes: f32,
420 avg_paced_frame_bytes: f32,
422 avg_preview_frame_bytes: f32,
424 inflight_bytes: usize,
426 inflight_frames: VecDeque<InFlightFrame>,
428 next_send_at: Instant,
430 probe_frames: f32,
433 frames_sent: u32,
435 acks_recv: u32,
436 acked_bytes_since_log: usize,
437 browser_backlog_frames: u16,
438 browser_ack_ahead_frames: u16,
439 browser_apply_ms: f32,
440 last_metrics_update: Instant,
441 last_log: Instant,
442 goodput_window_bytes: usize,
443 goodput_window_start: Instant,
444 surface_next_send_at: Instant,
445 surface_needs_keyframe: bool,
446 surface_encoders: HashMap<u16, SurfaceEncoder>,
448 surface_encodes_in_flight: HashSet<u16>,
453 surface_last_frames: HashMap<u16, BufferedSurfaceFrame>,
455 surface_last_encoded_gen: HashMap<u16, u64>,
458 surface_view_sizes: HashMap<u16, (u16, u16, u16, u8)>,
464 surface_codec_support: u8,
467 pressed_surface_keys: HashSet<u32>,
471}
472
473struct InFlightFrame {
474 sent_at: Instant,
475 bytes: usize,
476 paced: bool,
477}
478
479fn frame_window(rtt_ms: f32, display_fps: f32) -> usize {
483 let frame_ms = 1_000.0 / display_fps.max(1.0);
484 let base_frames = (rtt_ms / frame_ms).ceil().max(0.0) as usize;
485 let slack_frames = ((base_frames as f32) * 0.125).ceil() as usize + 2;
486 base_frames.saturating_add(slack_frames).max(2)
487}
488
489fn path_rtt_ms(client: &ClientState) -> f32 {
490 if client.min_rtt_ms > 0.0 {
491 client.min_rtt_ms
492 } else {
493 client.rtt_ms
494 }
495}
496
497fn display_need_bps(client: &ClientState) -> f32 {
498 client.avg_paced_frame_bytes.max(256.0) * client.display_fps.max(1.0)
499}
500
501fn effective_rtt_ms(client: &ClientState) -> f32 {
502 let path_rtt = path_rtt_ms(client);
503 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
504 let queue_allowance = frame_ms
505 * if throughput_limited(client) {
506 4.0
507 } else {
508 12.0
509 };
510 client.rtt_ms.clamp(path_rtt, path_rtt + queue_allowance)
511}
512
513fn window_rtt_ms(client: &ClientState) -> f32 {
514 let effective = effective_rtt_ms(client);
515 if !throughput_limited(client) {
516 effective
517 } else {
518 client.rtt_ms.clamp(effective, effective * 2.0)
519 }
520}
521
522fn target_frame_window(client: &ClientState) -> usize {
523 let window_fps = if throughput_limited(client) {
524 pacing_fps(client)
525 } else {
526 browser_pacing_fps(client)
527 };
528 frame_window(window_rtt_ms(client), window_fps)
529 .saturating_add(client.probe_frames.round().max(0.0) as usize)
530}
531
532fn base_queue_ms(client: &ClientState) -> f32 {
533 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
534 frame_ms * if throughput_limited(client) { 2.0 } else { 8.0 }
535}
536
537fn target_queue_ms(client: &ClientState) -> f32 {
538 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
539 let probe_scale = if throughput_limited(client) {
540 0.25
541 } else {
542 1.0
543 };
544 base_queue_ms(client) + client.probe_frames.max(0.0) * frame_ms * probe_scale
545}
546
547fn browser_ready(client: &ClientState) -> bool {
548 client.browser_ack_ahead_frames <= 1
549 && client.browser_apply_ms <= 1.0
550 && !outbox_backpressured(client)
551}
552
553fn bandwidth_floor_bps(client: &ClientState) -> f32 {
554 let browser_ready = browser_ready(client);
555 let backlog_scale = match client.browser_backlog_frames {
556 0..=2 => 0.9,
557 3..=8 => 0.8,
558 _ => 0.65,
559 };
560 let penalty = client
561 .goodput_jitter_bps
562 .max(client.max_goodput_jitter_bps * 0.5)
563 .min(client.goodput_bps * if browser_ready { 0.75 } else { 0.9 });
564 let goodput_floor = (client.goodput_bps - penalty)
565 .max(client.goodput_bps * if browser_ready { 0.35 } else { 0.2 });
566 let delivery_floor = client.delivery_bps * if browser_ready { 1.0 } else { 0.5 };
570 let recent_sample_floor = if browser_ready && client.last_goodput_sample_bps > 0.0 {
571 client.last_goodput_sample_bps * backlog_scale
572 } else {
573 0.0
574 };
575 goodput_floor.max(recent_sample_floor).max(delivery_floor)
576}
577
578fn pacing_fps(client: &ClientState) -> f32 {
579 let frame_bytes = client.avg_paced_frame_bytes.max(256.0);
580 let sustainable = bandwidth_floor_bps(client) / frame_bytes;
581 sustainable.min(browser_pacing_fps(client))
582}
583
584fn throughput_limited(client: &ClientState) -> bool {
585 let floor = bandwidth_floor_bps(client);
586 let lead_bps = client.avg_paced_frame_bytes.max(256.0) * browser_pacing_fps(client);
590 let preview_bps = client.avg_preview_frame_bytes.max(256.0) * client.display_fps.max(1.0);
591 (lead_bps + preview_bps) > floor * 0.9
592}
593
594fn browser_pacing_fps(client: &ClientState) -> f32 {
595 let mut fps = client.display_fps.max(1.0);
596
597 let backlog = client.browser_backlog_frames as f32;
601 if backlog > 4.0 {
602 fps = fps.min(fps * (4.0 / backlog));
603 }
604
605 if client.browser_ack_ahead_frames > 4 {
606 fps = fps.min(client.display_fps.max(1.0) * 0.5);
607 }
608
609 fps.max(1.0)
610}
611
612fn browser_backlog_blocked(client: &ClientState) -> bool {
613 client.browser_backlog_frames > 8
614}
615
616fn byte_budget_for(client: &ClientState, budget_ms: f32) -> usize {
617 let budget_bps = if throughput_limited(client) {
618 bandwidth_floor_bps(client)
619 } else {
620 client.goodput_bps.max(bandwidth_floor_bps(client))
621 };
622 let bytes = budget_bps * budget_ms.max(1.0) / 1_000.0;
623 bytes.ceil().max(client.avg_frame_bytes.max(256.0)) as usize
624}
625
626fn target_byte_window(client: &ClientState) -> usize {
627 let budget = byte_budget_for(client, path_rtt_ms(client) + target_queue_ms(client));
628 let frame_bytes = client.avg_paced_frame_bytes.max(256.0).ceil() as usize;
629 let target_frames = target_frame_window(client);
630 let pipeline_bytes = frame_bytes.saturating_mul(target_frames);
631 const PIPELINE_FLOOR_LIMIT: usize = 32_768; let floor = if pipeline_bytes <= PIPELINE_FLOOR_LIMIT {
638 pipeline_bytes
639 } else {
640 frame_bytes };
642 budget.max(floor)
643}
644
645fn send_interval(client: &ClientState) -> Duration {
646 Duration::from_secs_f64(1.0 / browser_pacing_fps(client).max(1.0) as f64)
647}
648
649fn preview_fps(client: &ClientState) -> f32 {
650 let mut fps = client.display_fps.max(1.0);
651 if client.lead.is_some() {
652 let avail = bandwidth_floor_bps(client);
656 let lead_bps = client.avg_paced_frame_bytes.max(256.0) * browser_pacing_fps(client);
657 let preview_budget = (avail - lead_bps).max(avail * 0.25).max(0.0);
658 let bw_cap = preview_budget / client.avg_preview_frame_bytes.max(256.0);
659 fps = fps.min(bw_cap.max(1.0));
660 }
661 fps.max(1.0)
662}
663
664fn preview_send_interval(client: &ClientState) -> Duration {
665 Duration::from_secs_f64(1.0 / preview_fps(client) as f64)
666}
667
668fn advance_deadline(deadline: &mut Instant, now: Instant, interval: Duration) {
669 let scheduled = deadline.checked_add(interval).unwrap_or(now + interval);
670 *deadline = if scheduled + interval < now {
671 now + interval
672 } else {
673 scheduled
674 };
675}
676
677fn should_snapshot_pty(dirty: bool, needful: bool, synced_output: bool) -> bool {
678 dirty && needful && !synced_output
679}
680
681fn enqueue_ready_frame(queue: &mut VecDeque<FrameState>, frame: FrameState) -> bool {
682 if queue.len() >= READY_FRAME_QUEUE_CAP {
683 return false;
684 }
685 queue.push_back(frame);
686 true
687}
688
689fn pty_has_visual_update(pty: &Pty) -> bool {
690 pty.dirty || !pty.ready_frames.is_empty() || !pty.byte_rx.is_empty()
691}
692
693fn find_sync_output_end(prefix: &[u8], bytes: &[u8]) -> Option<usize> {
697 if bytes.is_empty() {
698 return None;
699 }
700 let needle = SYNC_OUTPUT_END;
701 let nlen = needle.len();
702
703 if !prefix.is_empty() {
705 let tail = if prefix.len() >= nlen - 1 {
706 &prefix[prefix.len() - (nlen - 1)..]
707 } else {
708 prefix
709 };
710 let combined_len = tail.len() + bytes.len().min(nlen);
711 if combined_len >= nlen {
712 let mut buf = [0u8; 32]; let blen = combined_len.min(buf.len());
715 let tlen = tail.len().min(blen);
716 buf[..tlen].copy_from_slice(&tail[..tlen]);
717 let rest = (blen - tlen).min(bytes.len());
718 buf[tlen..tlen + rest].copy_from_slice(&bytes[..rest]);
719 for i in 0..=(blen.saturating_sub(nlen)) {
720 if &buf[i..i + nlen] == needle {
721 let end_in_bytes = (i + nlen).saturating_sub(tail.len());
722 if end_in_bytes > 0 && end_in_bytes <= bytes.len() {
723 return Some(end_in_bytes);
724 }
725 }
726 }
727 }
728 }
729
730 let mut offset = 0;
732 while let Some(pos) = memchr::memchr(0x1b, &bytes[offset..]) {
733 let abs = offset + pos;
734 if abs + nlen <= bytes.len() && &bytes[abs..abs + nlen] == needle {
735 return Some(abs + nlen);
736 }
737 offset = abs + 1;
738 }
739 None
740}
741
742fn update_sync_scan_tail(tail: &mut Vec<u8>, bytes: &[u8]) {
743 if bytes.is_empty() {
744 return;
745 }
746 tail.extend_from_slice(bytes);
747 let keep = SYNC_OUTPUT_END.len().saturating_sub(1);
748 if tail.len() > keep {
749 let drop = tail.len() - keep;
750 tail.drain(..drop);
751 }
752}
753
754fn preview_deadline(client: &ClientState, pid: u16, now: Instant) -> Instant {
755 client
756 .preview_next_send_at
757 .get(&pid)
758 .copied()
759 .unwrap_or(now)
760}
761
762fn client_has_due_preview(sess: &Session, client: &ClientState, now: Instant) -> bool {
763 if client.lead.is_none() {
764 return false;
765 }
766 client.subscriptions.iter().copied().any(|pid| {
767 Some(pid) != client.lead
768 && preview_deadline(client, pid, now) <= now
769 && sess
770 .ptys
771 .get(&pid)
772 .map(pty_has_visual_update)
773 .unwrap_or(false)
774 })
775}
776
777fn outbox_queued_frames(client: &ClientState) -> usize {
778 OUTBOX_CAPACITY.saturating_sub(client.tx.capacity())
779}
780
781fn outbox_backpressured(client: &ClientState) -> bool {
782 outbox_queued_frames(client) >= OUTBOX_SOFT_QUEUE_LIMIT_FRAMES
783}
784
785fn can_send_preview(client: &ClientState, pid: u16, now: Instant) -> bool {
786 window_open(client) && now >= preview_deadline(client, pid, now)
787}
788
789fn record_preview_send(client: &mut ClientState, pid: u16, now: Instant) {
790 let mut deadline = client
791 .preview_next_send_at
792 .get(&pid)
793 .copied()
794 .unwrap_or(now);
795 advance_deadline(&mut deadline, now, preview_send_interval(client));
796 client.preview_next_send_at.insert(pid, deadline);
797}
798
799fn window_open(client: &ClientState) -> bool {
800 !browser_backlog_blocked(client)
801 && !outbox_backpressured(client)
802 && client.inflight_frames.len() < target_frame_window(client)
803 && client.inflight_bytes < target_byte_window(client)
804}
805
806fn lead_window_open(client: &ClientState, reserve_preview_slot: bool) -> bool {
807 if !reserve_preview_slot || client.lead.is_none() {
808 return window_open(client);
809 }
810 if browser_backlog_blocked(client) || outbox_backpressured(client) {
811 return false;
812 }
813 let target_frames = target_frame_window(client);
814 let reserve_frames = PREVIEW_FRAME_RESERVE.min(target_frames.saturating_sub(1));
815 let frame_limit = target_frames.saturating_sub(reserve_frames).max(1);
816 let reserve_bytes = client.avg_preview_frame_bytes.max(256.0).ceil() as usize;
817 let byte_limit = target_byte_window(client)
818 .saturating_sub(reserve_bytes)
819 .max(client.avg_paced_frame_bytes.max(256.0).ceil() as usize);
820 client.inflight_frames.len() < frame_limit && client.inflight_bytes < byte_limit
821}
822
823fn can_send_frame(client: &ClientState, now: Instant, reserve_preview_slot: bool) -> bool {
824 lead_window_open(client, reserve_preview_slot) && now >= client.next_send_at
825}
826
827fn record_send(client: &mut ClientState, bytes: usize, now: Instant, paced: bool) {
828 client.inflight_bytes += bytes;
829 client.inflight_frames.push_back(InFlightFrame {
830 sent_at: now,
831 bytes,
832 paced,
833 });
834 if paced {
835 let interval = send_interval(client);
836 advance_deadline(&mut client.next_send_at, now, interval);
837 }
838}
839
840fn ewma_with_direction(old: f32, sample: f32, rise_alpha: f32, fall_alpha: f32) -> f32 {
841 let alpha = if sample > old { rise_alpha } else { fall_alpha };
842 old * (1.0 - alpha) + sample * alpha
843}
844
845fn window_saturated(client: &ClientState, inflight_frames: usize, inflight_bytes: usize) -> bool {
846 let target_frames = target_frame_window(client);
847 let target_bytes = target_byte_window(client);
848 inflight_frames.saturating_mul(10) >= target_frames.saturating_mul(9)
849 || inflight_bytes.saturating_mul(10) >= target_bytes.saturating_mul(9)
850}
851
852fn record_ack(client: &mut ClientState) {
853 if let Some(frame) = client.inflight_frames.pop_front() {
854 let prev_inflight_frames = client.inflight_frames.len() + 1;
855 let prev_inflight_bytes = client.inflight_bytes;
856 client.inflight_bytes = client.inflight_bytes.saturating_sub(frame.bytes);
857 client.acked_bytes_since_log = client.acked_bytes_since_log.saturating_add(frame.bytes);
858 let sample_ms = frame.sent_at.elapsed().as_secs_f32() * 1_000.0;
859 client.rtt_ms = ewma_with_direction(client.rtt_ms, sample_ms, 0.125, 0.25);
860 if client.min_rtt_ms > 0.0 {
861 client.min_rtt_ms = client.min_rtt_ms.min(sample_ms);
864 } else {
865 client.min_rtt_ms = sample_ms;
866 }
867 client.min_rtt_ms = client.min_rtt_ms.max(0.5);
868 let sample_bps = frame.bytes as f32 / sample_ms.max(1.0e-3) * 1_000.0;
869 client.delivery_bps = ewma_with_direction(client.delivery_bps, sample_bps, 0.5, 0.125);
870 client.avg_frame_bytes =
871 ewma_with_direction(client.avg_frame_bytes, frame.bytes as f32, 0.5, 0.125);
872 if frame.paced {
873 client.avg_paced_frame_bytes =
874 ewma_with_direction(client.avg_paced_frame_bytes, frame.bytes as f32, 0.5, 0.125);
875 } else {
876 client.avg_preview_frame_bytes = ewma_with_direction(
877 client.avg_preview_frame_bytes,
878 frame.bytes as f32,
879 0.5,
880 0.125,
881 );
882 }
883 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
884 let path_rtt = path_rtt_ms(client);
885 let likely_window_limited =
886 window_saturated(client, prev_inflight_frames, prev_inflight_bytes);
887 client.goodput_window_bytes = client.goodput_window_bytes.saturating_add(frame.bytes);
888 let now = Instant::now();
889 let goodput_elapsed = now
890 .duration_since(client.goodput_window_start)
891 .as_secs_f32();
892 if goodput_elapsed >= 0.02 {
893 let sample_goodput = client.goodput_window_bytes as f32 / goodput_elapsed.max(1.0e-3);
894 if likely_window_limited || client.browser_backlog_frames > 0 {
895 let prev_goodput_sample = if client.last_goodput_sample_bps > 0.0 {
896 client.last_goodput_sample_bps
897 } else {
898 sample_goodput
899 };
900 let jitter_sample = (sample_goodput - prev_goodput_sample).abs();
901 client.goodput_bps =
902 ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, 0.125);
903 let min_reliable = (client.avg_paced_frame_bytes.max(256.0) * 2.0) as usize;
909 if client.goodput_window_bytes >= min_reliable {
910 client.goodput_jitter_bps =
911 ewma_with_direction(client.goodput_jitter_bps, jitter_sample, 0.5, 0.125);
912 let jitter_decay = if browser_ready(client) && sample_ms < path_rtt * 3.0 {
913 0.90
914 } else {
915 0.98
916 };
917 client.max_goodput_jitter_bps =
918 (client.max_goodput_jitter_bps * jitter_decay).max(jitter_sample);
919 client.max_goodput_jitter_bps =
923 client.max_goodput_jitter_bps.min(client.goodput_bps * 0.45);
924 } else {
925 client.goodput_jitter_bps *= 0.9;
927 client.max_goodput_jitter_bps *= 0.95;
928 }
929 client.last_goodput_sample_bps =
933 (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
934 } else {
935 let ratio = client.goodput_bps / sample_goodput.max(1.0);
940 let fall_alpha = if ratio > 10.0 {
941 0.5
942 } else if ratio > 3.0 {
943 0.25
944 } else {
945 0.03
946 };
947 client.goodput_bps =
948 ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, fall_alpha);
949 client.goodput_jitter_bps *= 0.5;
950 client.max_goodput_jitter_bps *= 0.9;
951 client.last_goodput_sample_bps =
952 (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
953 }
954 client.goodput_window_bytes = 0;
955 client.goodput_window_start = now;
956 }
957 let queue_baseline_ms = if throughput_limited(client) {
958 window_rtt_ms(client)
959 } else {
960 path_rtt
961 };
962 let queue_delay_ms = (sample_ms - queue_baseline_ms).max(0.0);
963 let max_probe_frames = (browser_pacing_fps(client) * 0.125).max(4.0);
964 let jitter_ratio = client.max_goodput_jitter_bps / client.goodput_bps.max(1.0);
965 let low_delay_frames = if throughput_limited(client) { 2.0 } else { 8.0 };
966 let high_delay_frames = if throughput_limited(client) {
967 4.0
968 } else {
969 12.0
970 };
971 if likely_window_limited
972 && queue_delay_ms <= frame_ms * low_delay_frames
973 && jitter_ratio < 0.25
974 {
975 client.probe_frames = (client.probe_frames + 1.0).min(max_probe_frames);
976 } else if !likely_window_limited
977 && browser_ready(client)
978 && queue_delay_ms <= frame_ms * 2.0
979 && jitter_ratio < 0.25
980 {
981 client.probe_frames = (client.probe_frames + 0.25).min(max_probe_frames * 0.5);
982 } else if queue_delay_ms > frame_ms * high_delay_frames || jitter_ratio > 0.5 {
983 client.probe_frames = (client.probe_frames * 0.5).max(1.0);
984 } else if queue_delay_ms > frame_ms * 2.0 || !browser_ready(client) {
985 client.probe_frames = (client.probe_frames - 0.5).max(0.0);
986 }
987 } else {
988 client.inflight_bytes = 0;
989 }
990}
991
992fn reset_inflight(client: &mut ClientState) {
993 client.inflight_bytes = 0;
994 client.inflight_frames.clear();
995 client.next_send_at = Instant::now();
996 client.browser_backlog_frames = 0;
997 client.browser_ack_ahead_frames = 0;
998}
999
1000fn is_unset_view_size(rows: u16, cols: u16) -> bool {
1001 rows == 0 && cols == 0
1002}
1003
1004fn subscribe_client_to(client: &mut ClientState, pty_id: u16) {
1005 if client.subscriptions.insert(pty_id) {
1006 client.last_sent.remove(&pty_id);
1007 client.preview_next_send_at.remove(&pty_id);
1008 }
1009}
1010
1011fn unsubscribe_client_from(client: &mut ClientState, pty_id: u16) -> bool {
1012 let removed_sub = client.subscriptions.remove(&pty_id);
1013 client.last_sent.remove(&pty_id);
1014 client.preview_next_send_at.remove(&pty_id);
1015 client.scroll_offsets.remove(&pty_id);
1016 client.scroll_caches.remove(&pty_id);
1017 let removed_view = client.view_sizes.remove(&pty_id).is_some();
1018 if client.lead == Some(pty_id) {
1019 client.lead = None;
1020 }
1021 removed_sub || removed_view
1022}
1023
1024fn update_client_scroll_state(client: &mut ClientState, pty_id: u16, next_offset: usize) -> bool {
1025 let prev_offset = client.scroll_offsets.get(&pty_id).copied().unwrap_or(0);
1026 if prev_offset == next_offset {
1027 return false;
1028 }
1029
1030 if prev_offset == 0 && next_offset > 0 {
1031 client.scroll_caches.insert(
1032 pty_id,
1033 client.last_sent.get(&pty_id).cloned().unwrap_or_default(),
1034 );
1035 } else if prev_offset > 0
1036 && next_offset == 0
1037 && let Some(cache) = client.scroll_caches.remove(&pty_id)
1038 {
1039 if cache.rows() > 0 && cache.cols() > 0 {
1040 client.last_sent.insert(pty_id, cache);
1041 } else {
1042 client.last_sent.remove(&pty_id);
1043 }
1044 }
1045
1046 if next_offset > 0 {
1047 client.scroll_offsets.insert(pty_id, next_offset);
1048 } else {
1049 client.scroll_offsets.remove(&pty_id);
1050 }
1051 reset_inflight(client);
1052 true
1053}
1054
1055struct Session {
1056 ptys: HashMap<u16, Pty>,
1057 compositor: Option<SharedCompositor>,
1058 next_client_id: u64,
1059 next_compositor_id: u16,
1060 next_pty_id: u16,
1061 tick_fires: u32,
1062 tick_snaps: u32,
1063 surface_commits: u32,
1064 surface_encodes: u32,
1065 surface_encode_bytes: u64,
1066 surface_frames_sent: u32,
1067 clients: HashMap<u64, ClientState>,
1068}
1069
1070struct SearchResultRow {
1071 pty_id: u16,
1072 score: u32,
1073 primary_source: u8,
1074 matched_sources: u8,
1075 context: String,
1076 scroll_offset: Option<usize>,
1077}
1078
1079struct TickOutcome {
1080 did_work: bool,
1081 next_deadline: Option<Instant>,
1082}
1083
1084impl Session {
1085 fn new() -> Self {
1086 Self {
1087 ptys: HashMap::new(),
1088 compositor: None,
1089 next_client_id: 1,
1090 next_compositor_id: 1,
1091 next_pty_id: 1,
1092 clients: HashMap::new(),
1093 tick_fires: 0,
1094 tick_snaps: 0,
1095 surface_commits: 0,
1096 surface_encodes: 0,
1097 surface_encode_bytes: 0,
1098 surface_frames_sent: 0,
1099 }
1100 }
1101
1102 fn ensure_compositor(
1103 &mut self,
1104 verbose: bool,
1105 event_notify: Arc<dyn Fn() + Send + Sync>,
1106 ) -> &str {
1107 if self.compositor.is_none() {
1108 let session_id = self.next_compositor_id;
1109 self.next_compositor_id = self.next_compositor_id.wrapping_add(1);
1110 let handle = blit_compositor::spawn_compositor(verbose, event_notify);
1111 self.compositor = Some(SharedCompositor {
1112 session_id,
1113 handle,
1114 surfaces: HashMap::new(),
1115 last_pixels: HashMap::new(),
1116 pending_frame_requests: HashSet::new(),
1117 created_at: Instant::now(),
1118 pixel_generation: 0,
1119 });
1120 }
1121 &self.compositor.as_ref().unwrap().handle.socket_name
1122 }
1123
1124 fn allocate_pty_id(&mut self, max_ptys: usize) -> Option<u16> {
1125 if max_ptys > 0 && self.ptys.len() >= max_ptys {
1126 return None;
1127 }
1128 let start = self.next_pty_id;
1129 let mut id = start;
1130 loop {
1131 if !self.ptys.contains_key(&id) {
1132 self.next_pty_id = if id == u16::MAX { 1 } else { id + 1 };
1133 return Some(id);
1134 }
1135 id = if id == u16::MAX { 1 } else { id + 1 };
1136 if id == start {
1137 return None;
1138 }
1139 }
1140 }
1141
1142 fn send_to_all(&self, msg: &[u8]) {
1143 for c in self.clients.values() {
1144 let _ = c.tx.try_send(msg.to_vec());
1145 }
1146 }
1147
1148 fn mediated_size_for_pty(&self, pty_id: u16) -> Option<(u16, u16)> {
1149 let mut min_rows: Option<u16> = None;
1150 let mut min_cols: Option<u16> = None;
1151 for c in self.clients.values() {
1152 if let Some((r, cols)) = c.view_sizes.get(&pty_id).copied() {
1153 min_rows = Some(min_rows.map_or(r, |m: u16| m.min(r)));
1154 min_cols = Some(min_cols.map_or(cols, |m: u16| m.min(cols)));
1155 }
1156 }
1157 match (min_rows, min_cols) {
1158 (Some(r), Some(c)) => Some((r.max(1), c.max(1))),
1159 _ => None,
1160 }
1161 }
1162
1163 fn resize_pty(&mut self, pty_id: u16, rows: u16, cols: u16) -> bool {
1164 let pty = match self.ptys.get_mut(&pty_id) {
1165 Some(p) => p,
1166 None => return false,
1167 };
1168 let (cur_rows, cur_cols) = pty.driver.size();
1169 if cur_rows == rows && cur_cols == cols {
1170 return false;
1171 }
1172 pty.ready_frames.clear();
1173 pty.driver.resize(rows, cols);
1174 pty.mark_dirty();
1175 for c in self.clients.values_mut() {
1176 if c.subscriptions.contains(&pty_id) {
1177 c.last_sent.remove(&pty_id);
1178 }
1179 if c.scroll_caches.remove(&pty_id).is_some() {
1180 reset_inflight(c);
1181 }
1182 }
1183 if !pty.exited {
1184 pty::resize_pty_os(&pty.handle, rows, cols);
1185 }
1186 true
1187 }
1188
1189 fn resize_ptys_to_mediated_sizes<I>(&mut self, pty_ids: I) -> bool
1190 where
1191 I: IntoIterator<Item = u16>,
1192 {
1193 let mut changed = false;
1194 let mut seen = HashSet::new();
1195 for pty_id in pty_ids {
1196 if !seen.insert(pty_id) {
1197 continue;
1198 }
1199 if let Some((rows, cols)) = self.mediated_size_for_pty(pty_id) {
1200 changed |= self.resize_pty(pty_id, rows, cols);
1201 }
1202 }
1203 changed
1204 }
1205
1206 fn mediated_size_for_surface(
1216 &self,
1217 surface_id: u16,
1218 max: Option<(u16, u16)>,
1219 ) -> Option<(u16, u16, u16)> {
1220 let mut min_w: Option<u16> = None;
1221 let mut min_h: Option<u16> = None;
1222 let mut max_scale: u16 = 0;
1223 for c in self.clients.values() {
1224 if let Some(&(w, h, s, _codec)) = c.surface_view_sizes.get(&surface_id) {
1225 min_w = Some(min_w.map_or(w, |m: u16| m.min(w)));
1226 min_h = Some(min_h.map_or(h, |m: u16| m.min(h)));
1227 max_scale = max_scale.max(s);
1228 }
1229 }
1230 match (min_w, min_h) {
1231 (Some(w), Some(h)) => {
1232 let (w, h) = (w.max(1), h.max(1));
1233 let (w, h) = if let Some((mw, mh)) = max {
1234 (w.min(mw), h.min(mh))
1235 } else {
1236 (w, h)
1237 };
1238 Some((w, h, max_scale))
1239 }
1240 _ => None,
1241 }
1242 }
1243
1244 fn resize_surface(&mut self, surface_id: u16, width: u16, height: u16, scale_120: u16) -> bool {
1245 let cs = match self.compositor.as_mut() {
1246 Some(cs) => cs,
1247 None => return false,
1248 };
1249 if let Some(info) = cs.surfaces.get(&surface_id)
1250 && info.width == width
1251 && info.height == height
1252 && info.scale_120 == scale_120
1253 {
1254 return false;
1255 }
1256 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
1258 info.scale_120 = scale_120;
1259 }
1260 let _ = cs.handle.command_tx.send(CompositorCommand::SurfaceResize {
1261 surface_id,
1262 width,
1263 height,
1264 scale_120,
1265 });
1266 true
1267 }
1268
1269 fn resize_surfaces_to_mediated_sizes<I>(
1270 &mut self,
1271 surface_ids: I,
1272 encoder_preferences: &[SurfaceEncoderPreference],
1273 ) where
1274 I: IntoIterator<Item = u16>,
1275 {
1276 let max = SurfaceEncoderPreference::max_dimensions_for_list(encoder_preferences);
1277 let mut seen = HashSet::new();
1278 for sid in surface_ids {
1279 if !seen.insert(sid) {
1280 continue;
1281 }
1282 if let Some((w, h, scale_120)) = self.mediated_size_for_surface(sid, max) {
1283 self.resize_surface(sid, w, h, scale_120);
1284 }
1285 }
1286 }
1287
1288 fn pty_list_msg(&self) -> Vec<u8> {
1289 let mut msg = vec![S2C_LIST];
1290 let count = self.ptys.len() as u16;
1291 msg.extend_from_slice(&count.to_le_bytes());
1292 let mut ids: Vec<u16> = self.ptys.keys().copied().collect();
1293 ids.sort();
1294 for id in ids {
1295 let pty = &self.ptys[&id];
1296 let tag = pty.tag.as_bytes();
1297 msg.extend_from_slice(&id.to_le_bytes());
1298 msg.extend_from_slice(&(tag.len() as u16).to_le_bytes());
1299 msg.extend_from_slice(tag);
1300 let cmd = pty.command.as_deref().unwrap_or("").as_bytes();
1301 msg.extend_from_slice(&(cmd.len() as u16).to_le_bytes());
1302 msg.extend_from_slice(cmd);
1303 }
1304 msg
1305 }
1306
1307 fn surface_list_msg(&self) -> Vec<u8> {
1308 let cs = match self.compositor.as_ref() {
1309 Some(cs) => cs,
1310 None => {
1311 let mut msg = vec![S2C_SURFACE_LIST];
1312 msg.extend_from_slice(&0u16.to_le_bytes());
1313 return msg;
1314 }
1315 };
1316 let mut msg = vec![S2C_SURFACE_LIST];
1317 let count = cs.surfaces.len() as u16;
1318 msg.extend_from_slice(&count.to_le_bytes());
1319 let mut ids: Vec<u16> = cs.surfaces.keys().copied().collect();
1320 ids.sort();
1321 for id in ids {
1322 let info = &cs.surfaces[&id];
1323 let title = info.title.as_bytes();
1324 let app_id = info.app_id.as_bytes();
1325 msg.extend_from_slice(&info.surface_id.to_le_bytes());
1326 msg.extend_from_slice(&info.parent_id.to_le_bytes());
1327 msg.extend_from_slice(&info.width.to_le_bytes());
1328 msg.extend_from_slice(&info.height.to_le_bytes());
1329 msg.extend_from_slice(&(title.len() as u16).to_le_bytes());
1330 msg.extend_from_slice(title);
1331 msg.extend_from_slice(&(app_id.len() as u16).to_le_bytes());
1332 msg.extend_from_slice(app_id);
1333 }
1334 msg
1335 }
1336}
1337
1338struct AppStateInner {
1339 config: Config,
1340 session: Mutex<Session>,
1341 pty_fds: PtyFds,
1342 delivery_notify: Arc<Notify>,
1343 active_connections: std::sync::atomic::AtomicUsize,
1346}
1347
1348type AppState = Arc<AppStateInner>;
1349
1350fn nudge_delivery(state: &AppState) {
1351 state.delivery_notify.notify_one();
1352}
1353
1354#[cfg(unix)]
1355#[allow(dead_code)]
1356fn spawn_compositor_child(
1357 command: &str,
1358 argv: Option<&[&str]>,
1359 wayland_socket: &str,
1360 dir: Option<&str>,
1361) -> libc::pid_t {
1362 use std::ffi::CString;
1363 let pid = unsafe { libc::fork() };
1364 if pid == 0 {
1365 if let Some(d) = dir {
1366 let c_dir = CString::new(d).unwrap();
1367 unsafe {
1368 libc::chdir(c_dir.as_ptr());
1369 }
1370 }
1371 unsafe {
1372 let wd_path = std::path::Path::new(wayland_socket);
1373 if let Some(dir) = wd_path.parent() {
1374 let xdg = std::env::var_os("XDG_RUNTIME_DIR");
1375 let needs_update = match &xdg {
1376 Some(x) => std::path::Path::new(x) != dir,
1377 None => true,
1378 };
1379 if needs_update {
1380 std::env::set_var("XDG_RUNTIME_DIR", dir);
1381 }
1382 }
1383 std::env::set_var("WAYLAND_DISPLAY", wayland_socket);
1384 std::env::remove_var("DISPLAY");
1385 }
1386 if let Some(args) = argv {
1387 let prog = CString::new(args[0]).unwrap();
1388 let c_args: Vec<CString> = args.iter().map(|a| CString::new(*a).unwrap()).collect();
1389 let c_ptrs: Vec<*const libc::c_char> = c_args
1390 .iter()
1391 .map(|a| a.as_ptr())
1392 .chain(std::iter::once(std::ptr::null()))
1393 .collect();
1394 unsafe {
1395 libc::execvp(prog.as_ptr(), c_ptrs.as_ptr());
1396 }
1397 } else {
1398 let prog = CString::new(command).unwrap();
1399 let c_ptrs = [prog.as_ptr(), std::ptr::null()];
1400 unsafe {
1401 libc::execvp(prog.as_ptr(), c_ptrs.as_ptr());
1402 libc::_exit(1);
1403 }
1404 }
1405 }
1406 pid
1407}
1408
1409fn parse_terminal_queries(data: &[u8], size: (u16, u16), cursor: (u16, u16)) -> Vec<String> {
1410 const DA1_RESPONSE: &[u8] = b"\x1b[?64;1;2;6;9;15;18;21;22c";
1411
1412 let mut results = Vec::new();
1413 let mut i = 0;
1414 while i < data.len() {
1415 if data[i] != 0x1b || i + 2 >= data.len() || data[i + 1] != b'[' {
1416 i += 1;
1417 continue;
1418 }
1419 i += 2;
1420 let has_q = i < data.len() && data[i] == b'?';
1421 if has_q {
1422 i += 1;
1423 }
1424 let param_start = i;
1425 while i < data.len() && (data[i].is_ascii_digit() || data[i] == b';') {
1426 i += 1;
1427 }
1428 if i >= data.len() {
1429 break;
1430 }
1431 let final_byte = data[i];
1432 let params = &data[param_start..i];
1433 i += 1;
1434 if has_q {
1435 continue;
1436 }
1437 let resp: Option<String> = match final_byte {
1438 b'c' if params.is_empty() || params == b"0" => {
1439 Some(String::from_utf8_lossy(DA1_RESPONSE).into_owned())
1440 }
1441 b'n' if params == b"6" => Some(format!("\x1b[{};{}R", cursor.0 + 1, cursor.1 + 1)),
1442 b'n' if params == b"5" => Some("\x1b[0n".into()),
1443 b't' if params == b"18" => {
1444 let (rows, cols) = size;
1445 Some(format!("\x1b[8;{rows};{cols}t"))
1446 }
1447 b't' if params == b"14" => {
1448 let (rows, cols) = size;
1449 Some(format!("\x1b[4;{};{}t", rows * 16, cols * 8))
1450 }
1451 _ => None,
1452 };
1453 if let Some(r) = resp {
1454 results.push(r);
1455 }
1456 }
1457 results
1458}
1459
1460async fn cleanup_pty_internal(pty_id: u16, state: &AppState) {
1461 state.pty_fds.write().unwrap().remove(&pty_id);
1462 let mut sess = state.session.lock().await;
1463 if let Some(pty) = sess.ptys.get_mut(&pty_id) {
1464 if pty.exited {
1465 return;
1466 }
1467 pty.exited = true;
1468 pty::close_pty(&pty.handle);
1469 pty.exit_status = pty::collect_exit_status(&pty.handle);
1470 pty.mark_dirty();
1471 let msg = blit_remote::msg_exited(pty_id, pty.exit_status);
1472 sess.send_to_all(&msg);
1473 }
1474 let all_exited = sess.ptys.values().all(|p| p.exited);
1475 if all_exited && let Some(cs) = sess.compositor.take() {
1476 cs.handle
1477 .shutdown
1478 .store(true, std::sync::atomic::Ordering::Relaxed);
1479 let _ = cs.handle.command_tx.send(CompositorCommand::Shutdown);
1480 }
1481}
1482
1483fn take_snapshot(pty: &mut Pty) -> FrameState {
1484 if pty.lflag_last.elapsed() >= Duration::from_millis(250) {
1485 pty.lflag_cache = pty::pty_lflag(&pty.handle);
1486 pty.lflag_last = Instant::now();
1487 }
1488 let (echo, icanon) = pty.lflag_cache;
1489 pty.driver.snapshot(echo, icanon)
1490}
1491
1492fn build_scrollback_update(
1493 pty: &mut Pty,
1494 id: u16,
1495 offset: usize,
1496 prev_frame: &FrameState,
1497) -> Option<(Vec<u8>, FrameState)> {
1498 let frame = pty.driver.scrollback_frame(offset);
1499 let msg = build_update_msg(id, &frame, prev_frame);
1500 msg.map(|m| (m, frame))
1501}
1502
1503fn build_search_results_msg(request_id: u16, results: &[SearchResultRow]) -> Vec<u8> {
1504 let count = results.len().min(u16::MAX as usize);
1505 let payload_bytes: usize = results[..count]
1506 .iter()
1507 .map(|result| 14 + result.context.len().min(u16::MAX as usize))
1508 .sum();
1509 let mut msg = Vec::with_capacity(5 + payload_bytes);
1510 msg.push(S2C_SEARCH_RESULTS);
1511 msg.extend_from_slice(&request_id.to_le_bytes());
1512 msg.extend_from_slice(&(count as u16).to_le_bytes());
1513 for result in &results[..count] {
1514 msg.extend_from_slice(&result.pty_id.to_le_bytes());
1515 msg.extend_from_slice(&result.score.to_le_bytes());
1516 msg.push(result.primary_source);
1517 msg.push(result.matched_sources);
1518 let scroll_offset = result
1519 .scroll_offset
1520 .map(|offset| offset.min(u32::MAX as usize - 1) as u32)
1521 .unwrap_or(u32::MAX);
1522 msg.extend_from_slice(&scroll_offset.to_le_bytes());
1523 let context = result.context.as_bytes();
1524 let context_len = context.len().min(u16::MAX as usize);
1525 msg.extend_from_slice(&(context_len as u16).to_le_bytes());
1526 msg.extend_from_slice(&context[..context_len]);
1527 }
1528 msg
1529}
1530
1531enum SendOutcome {
1532 NoChange,
1533 Sent,
1534 Backpressured,
1535}
1536
1537fn try_send_update(
1538 client: &mut ClientState,
1539 pid: u16,
1540 current: FrameState,
1541 msg: Option<Vec<u8>>,
1542 now: Instant,
1543 paced: bool,
1544) -> SendOutcome {
1545 let Some(msg) = msg else {
1546 return SendOutcome::NoChange;
1547 };
1548 let bytes = msg.len();
1549 if client.tx.try_send(msg).is_ok() {
1550 client.last_sent.insert(pid, current);
1551 record_send(client, bytes, now, paced);
1552 client.frames_sent = client.frames_sent.wrapping_add(1);
1553 SendOutcome::Sent
1554 } else {
1555 client.last_sent.insert(pid, current);
1561 SendOutcome::Backpressured
1562 }
1563}
1564
1565pub async fn run(config: Config) {
1566 let state: AppState = Arc::new(AppStateInner {
1567 config,
1568 session: Mutex::new(Session::new()),
1569 pty_fds: Arc::new(std::sync::RwLock::new(HashMap::new())),
1570 delivery_notify: Arc::new(Notify::new()),
1571 active_connections: std::sync::atomic::AtomicUsize::new(0),
1572 });
1573
1574 let delivery_state = state.clone();
1575 tokio::spawn(async move {
1576 let mut next_deadline: Option<Instant> = None;
1577 loop {
1578 if let Some(deadline) = next_deadline {
1579 tokio::select! {
1580 _ = delivery_state.delivery_notify.notified() => {}
1581 _ = tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)) => {}
1582 }
1583 } else {
1584 delivery_state.delivery_notify.notified().await;
1585 }
1586 loop {
1587 let outcome = tick(&delivery_state).await;
1588 next_deadline = outcome.next_deadline;
1589 if !outcome.did_work {
1590 break;
1591 }
1592 tokio::task::yield_now().await;
1593 }
1594 }
1595 });
1596
1597 tokio::spawn(async {
1598 loop {
1599 tokio::time::sleep(Duration::from_secs(5)).await;
1600 pty::reap_zombies();
1601 }
1602 });
1603
1604 #[cfg(unix)]
1605 if let Some(channel_fd) = state.config.fd_channel {
1606 ipc::run_fd_channel(channel_fd, state).await;
1607 return;
1608 }
1609
1610 #[cfg(unix)]
1611 let listener = {
1612 if let Some(l) = IpcListener::from_systemd_fd(state.config.verbose) {
1613 l
1614 } else {
1615 IpcListener::bind(&state.config.ipc_path, state.config.verbose)
1616 }
1617 };
1618 #[cfg(not(unix))]
1619 let mut listener = IpcListener::bind(&state.config.ipc_path, state.config.verbose);
1620
1621 loop {
1622 let stream = match listener.accept().await {
1623 Ok(s) => s,
1624 Err(e) => {
1625 eprintln!("accept error: {e}");
1626 tokio::time::sleep(Duration::from_millis(100)).await;
1627 continue;
1628 }
1629 };
1630 let max = state.config.max_connections;
1631 if max > 0 {
1632 let current = state
1633 .active_connections
1634 .load(std::sync::atomic::Ordering::Relaxed);
1635 if current >= max {
1636 eprintln!("max connections ({max}) reached, rejecting");
1637 drop(stream);
1638 continue;
1639 }
1640 }
1641 state
1642 .active_connections
1643 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1644 let state = state.clone();
1645 tokio::spawn(async move {
1646 handle_client(stream, state.clone()).await;
1647 state
1648 .active_connections
1649 .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
1650 });
1651 }
1652}
1653
1654async fn tick(state: &AppState) -> TickOutcome {
1655 let mut sess = state.session.lock().await;
1656 sess.tick_fires += 1;
1657 let mut did_work = false;
1658 let mut next_deadline: Option<Instant> = None;
1659 let now = Instant::now();
1660
1661 let max_fps = sess
1662 .clients
1663 .values()
1664 .map(browser_pacing_fps)
1665 .fold(1.0_f32, f32::max);
1666 let title_interval = Duration::from_secs_f64(1.0 / max_fps as f64);
1667 let ids: Vec<u16> = sess.ptys.keys().copied().collect();
1668 for &id in &ids {
1669 let Some(pty) = sess.ptys.get_mut(&id) else {
1670 continue;
1671 };
1672 if pty.driver.take_title_dirty() {
1673 pty.mark_dirty();
1674 pty.title_pending = true;
1675 }
1676 if pty.title_pending && now.duration_since(pty.last_title_send) >= title_interval {
1677 let msg = {
1678 let title_bytes = pty.driver.title().as_bytes();
1679 let mut msg = Vec::with_capacity(3 + title_bytes.len());
1680 msg.push(S2C_TITLE);
1681 msg.extend_from_slice(&id.to_le_bytes());
1682 msg.extend_from_slice(title_bytes);
1683 msg
1684 };
1685 pty.last_title_send = now;
1686 pty.title_pending = false;
1687 sess.send_to_all(&msg);
1688 did_work = true;
1689 }
1690 }
1691
1692 let mut eof_ptys: Vec<u16> = Vec::with_capacity(ids.len());
1695 for &id in &ids {
1696 let Some(pty) = sess.ptys.get_mut(&id) else {
1697 continue;
1698 };
1699 while let Ok(input) = pty.byte_rx.try_recv() {
1700 match input {
1701 PtyInput::Data(data) => {
1702 pty::respond_to_queries(
1703 &pty.handle,
1704 &data,
1705 pty.driver.size(),
1706 pty.driver.cursor_position(),
1707 );
1708 pty.driver.process(&data);
1709 pty.mark_dirty();
1710 did_work = true;
1711 }
1712 PtyInput::SyncBoundary { before, after } => {
1713 if !before.is_empty() {
1714 pty::respond_to_queries(
1715 &pty.handle,
1716 &before,
1717 pty.driver.size(),
1718 pty.driver.cursor_position(),
1719 );
1720 pty.driver.process(&before);
1721 pty.mark_dirty();
1722 }
1723 if !pty.driver.synced_output() {
1724 let frame = take_snapshot(pty);
1725 enqueue_ready_frame(&mut pty.ready_frames, frame);
1726 pty.clear_dirty();
1727 }
1728 if !after.is_empty() {
1729 pty::respond_to_queries(
1730 &pty.handle,
1731 &after,
1732 pty.driver.size(),
1733 pty.driver.cursor_position(),
1734 );
1735 pty.driver.process(&after);
1736 pty.mark_dirty();
1737 }
1738 did_work = true;
1739 }
1740 PtyInput::Eof => {
1741 eof_ptys.push(id);
1742 }
1743 }
1744 }
1745 }
1746 drop(sess);
1748 for id in eof_ptys {
1749 tokio::time::sleep(Duration::from_millis(50)).await;
1750 cleanup_pty_internal(id, state).await;
1751 }
1752 let mut sess = state.session.lock().await;
1753
1754 let needful_ptys: HashSet<u16> = sess
1758 .clients
1759 .values()
1760 .flat_map(|c| {
1761 let reserve_preview_slot = client_has_due_preview(&sess, c, now);
1762 c.subscriptions.iter().copied().filter(move |pid| {
1763 let scrolled = c.scroll_offsets.get(pid).copied().unwrap_or(0) > 0;
1764 if Some(*pid) == c.lead {
1765 !scrolled && can_send_frame(c, now, reserve_preview_slot)
1766 } else {
1767 !scrolled && can_send_preview(c, *pid, now)
1768 }
1769 })
1770 })
1771 .collect();
1772
1773 let mut snapshots: HashMap<u16, FrameState> = HashMap::new();
1774 for &id in &ids {
1775 let Some(pty) = sess.ptys.get_mut(&id) else {
1776 continue;
1777 };
1778 if needful_ptys.contains(&id)
1779 && let Some(frame) = pty.ready_frames.pop_front()
1780 {
1781 snapshots.insert(id, frame);
1782 sess.tick_snaps += 1;
1783 did_work = true;
1784 continue;
1785 }
1786 if !should_snapshot_pty(
1787 pty.dirty,
1788 needful_ptys.contains(&id),
1789 pty.driver.synced_output(),
1790 ) {
1791 continue;
1792 }
1793 snapshots.insert(id, take_snapshot(pty));
1797 pty.clear_dirty();
1798 sess.tick_snaps += 1;
1799 did_work = true;
1800 }
1801
1802 let client_ids: Vec<u64> = sess.clients.keys().copied().collect();
1803 for cid in client_ids {
1804 if let Some(c) = sess.clients.get_mut(&cid) {
1810 if c.inflight_bytes == 0 && c.min_rtt_ms > 0.0 && c.rtt_ms > c.min_rtt_ms {
1811 c.rtt_ms = (c.rtt_ms * 0.99 + c.min_rtt_ms * 0.01).max(c.min_rtt_ms);
1812 }
1813 if c.last_metrics_update.elapsed() > Duration::from_secs(1) {
1816 c.browser_backlog_frames = 0;
1817 c.browser_ack_ahead_frames = 0;
1818 }
1819 }
1820 let (
1821 lead,
1822 subscriptions,
1823 scrolled_ptys,
1824 can_send_lead,
1825 lead_has_window,
1826 any_send_window,
1827 lead_deadline,
1828 ) = {
1829 let Some(c) = sess.clients.get(&cid) else {
1830 continue;
1831 };
1832 let reserve_preview_slot = client_has_due_preview(&sess, c, now);
1833 (
1834 c.lead,
1835 c.subscriptions.iter().copied().collect::<Vec<_>>(),
1836 c.scroll_offsets
1837 .iter()
1838 .map(|(&k, &v)| (k, v))
1839 .collect::<Vec<_>>(),
1840 can_send_frame(c, now, reserve_preview_slot),
1841 lead_window_open(c, reserve_preview_slot),
1842 lead_window_open(c, reserve_preview_slot) || window_open(c),
1843 c.next_send_at,
1844 )
1845 };
1846
1847 if subscriptions.is_empty() {
1848 continue;
1849 }
1850
1851 for &(scroll_pid, scroll_offset) in &scrolled_ptys {
1853 if scroll_offset == 0 {
1854 continue;
1855 }
1856 let is_lead = lead == Some(scroll_pid);
1857 let can_send = if is_lead { can_send_lead } else { true };
1858 if can_send {
1859 let prev_frame = {
1860 let Some(c) = sess.clients.get(&cid) else {
1861 continue;
1862 };
1863 c.scroll_caches
1864 .get(&scroll_pid)
1865 .cloned()
1866 .unwrap_or_default()
1867 };
1868 let outcome = if let Some(pty) = sess.ptys.get_mut(&scroll_pid) {
1869 if let Some((msg, new_frame)) =
1870 build_scrollback_update(pty, scroll_pid, scroll_offset, &prev_frame)
1871 {
1872 let Some(c) = sess.clients.get_mut(&cid) else {
1873 break;
1874 };
1875 let bytes = msg.len();
1876 if c.tx.try_send(msg).is_ok() {
1877 c.scroll_caches.insert(scroll_pid, new_frame);
1878 record_send(c, bytes, now, is_lead);
1879 c.frames_sent += 1;
1880 SendOutcome::Sent
1881 } else {
1882 SendOutcome::Backpressured
1883 }
1884 } else {
1885 SendOutcome::NoChange
1886 }
1887 } else {
1888 SendOutcome::NoChange
1889 };
1890 match outcome {
1891 SendOutcome::Sent => did_work = true,
1892 SendOutcome::Backpressured => {
1893 if let Some(pty) = sess.ptys.get_mut(&scroll_pid) {
1894 pty.mark_dirty();
1895 }
1896 }
1897 SendOutcome::NoChange => {}
1898 }
1899 } else if is_lead && lead_has_window {
1900 next_deadline = Some(match next_deadline {
1901 Some(existing) => existing.min(lead_deadline),
1902 None => lead_deadline,
1903 });
1904 }
1905 }
1906
1907 let lead_scroll_offset = lead
1908 .and_then(|pid| {
1909 scrolled_ptys
1910 .iter()
1911 .find(|&&(k, _)| k == pid)
1912 .map(|&(_, v)| v)
1913 })
1914 .unwrap_or(0);
1915
1916 if let Some(pid) = lead {
1917 if lead_scroll_offset == 0 && can_send_lead {
1918 if let Some(cur) = snapshots.get(&pid).cloned() {
1919 let previous = sess
1920 .clients
1921 .get(&cid)
1922 .and_then(|c| c.last_sent.get(&pid).cloned())
1923 .unwrap_or_default();
1924 drop(sess);
1925 let msg = build_update_msg(pid, &cur, &previous);
1926 sess = state.session.lock().await;
1927 let Some(c) = sess.clients.get_mut(&cid) else {
1928 continue;
1929 };
1930 match try_send_update(c, pid, cur, msg, now, true) {
1931 SendOutcome::Sent => did_work = true,
1932 SendOutcome::Backpressured => {
1933 if let Some(pty) = sess.ptys.get_mut(&pid) {
1934 pty.mark_dirty();
1935 }
1936 }
1937 SendOutcome::NoChange => {}
1938 }
1939 } else {
1940 let has_pending = sess
1941 .ptys
1942 .get(&pid)
1943 .map(pty_has_visual_update)
1944 .unwrap_or(false);
1945 let _ = has_pending;
1946 }
1947 } else {
1948 let has_pending = sess
1949 .ptys
1950 .get(&pid)
1951 .map(pty_has_visual_update)
1952 .unwrap_or(false);
1953 if has_pending && lead_has_window {
1954 next_deadline = Some(match next_deadline {
1955 Some(existing) => existing.min(lead_deadline),
1956 None => lead_deadline,
1957 });
1958 }
1959 }
1960 }
1961
1962 if !any_send_window {
1963 continue;
1964 }
1965
1966 let mut preview_ids = subscriptions;
1967 preview_ids.retain(|pid| Some(*pid) != lead);
1968 preview_ids.sort_unstable();
1969
1970 for pid in preview_ids {
1971 let (preview_can_send, preview_due_at, preview_has_window) =
1972 match sess.clients.get(&cid) {
1973 Some(c) => (
1974 can_send_preview(c, pid, now),
1975 preview_deadline(c, pid, now),
1976 window_open(c),
1977 ),
1978 None => (false, now, false),
1979 };
1980 if !preview_has_window {
1981 break;
1982 }
1983 if !preview_can_send {
1984 let has_pending = sess
1985 .ptys
1986 .get(&pid)
1987 .map(pty_has_visual_update)
1988 .unwrap_or(false);
1989 if has_pending && preview_due_at > now {
1994 next_deadline = Some(match next_deadline {
1995 Some(existing) => existing.min(preview_due_at),
1996 None => preview_due_at,
1997 });
1998 }
1999 continue;
2000 }
2001 let Some(cur) = snapshots.get(&pid) else {
2002 let has_pending = sess
2003 .ptys
2004 .get(&pid)
2005 .map(pty_has_visual_update)
2006 .unwrap_or(false);
2007 let _ = has_pending;
2008 continue;
2009 };
2010 let cur = cur.clone();
2011 let previous = sess
2012 .clients
2013 .get(&cid)
2014 .and_then(|c| c.last_sent.get(&pid).cloned())
2015 .unwrap_or_default();
2016 drop(sess);
2017 let msg = build_update_msg(pid, &cur, &previous);
2018 sess = state.session.lock().await;
2019 let Some(c) = sess.clients.get_mut(&cid) else {
2020 break;
2021 };
2022 match try_send_update(c, pid, cur, msg, now, false) {
2023 SendOutcome::Sent => {
2024 record_preview_send(c, pid, now);
2025 did_work = true;
2026 }
2027 SendOutcome::Backpressured => {
2028 if let Some(pty) = sess.ptys.get_mut(&pid) {
2029 pty.mark_dirty();
2030 }
2031 break;
2032 }
2033 SendOutcome::NoChange => {}
2034 }
2035 }
2036 }
2037
2038 let mut invalidate_client_encoders: Vec<u16> = Vec::new();
2040
2041 let mut surface_commit_count = 0u32;
2042 if let Some(cs) = sess.compositor.as_mut() {
2043 let mut events = Vec::new();
2044 while let Ok(event) = cs.handle.event_rx.try_recv() {
2045 events.push(event);
2046 }
2047 let mut broadcast: Vec<Vec<u8>> = Vec::new();
2048 for event in events {
2049 did_work = true;
2050 match event {
2051 CompositorEvent::SurfaceCreated {
2052 surface_id,
2053 title,
2054 app_id,
2055 parent_id,
2056 width,
2057 height,
2058 } => {
2059 broadcast.push(msg_surface_created(
2060 cs.session_id,
2061 surface_id,
2062 parent_id,
2063 width,
2064 height,
2065 &title,
2066 &app_id,
2067 ));
2068 cs.surfaces.insert(
2069 surface_id,
2070 CachedSurfaceInfo {
2071 surface_id,
2072 parent_id,
2073 width,
2074 height,
2075 scale_120: 0,
2076 title,
2077 app_id,
2078 },
2079 );
2080 cs.last_pixels.remove(&surface_id);
2081 invalidate_client_encoders.push(surface_id);
2082 }
2083 CompositorEvent::SurfaceDestroyed { surface_id } => {
2084 cs.surfaces.remove(&surface_id);
2085 cs.last_pixels.remove(&surface_id);
2086 invalidate_client_encoders.push(surface_id);
2087 broadcast.push(msg_surface_destroyed(cs.session_id, surface_id));
2088 }
2089 CompositorEvent::SurfaceCommit {
2090 surface_id,
2091 width,
2092 height,
2093 pixels,
2094 } => {
2095 surface_commit_count += 1;
2096 cs.pending_frame_requests.remove(&surface_id);
2097 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
2098 info.width = width as u16;
2099 info.height = height as u16;
2100 }
2101 cs.pixel_generation += 1;
2104 cs.last_pixels.insert(
2105 surface_id,
2106 LastPixels {
2107 width,
2108 height,
2109 pixels,
2110 generation: cs.pixel_generation,
2111 },
2112 );
2113 }
2114 CompositorEvent::SurfaceTitle { surface_id, title } => {
2115 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
2116 info.title = title.clone();
2117 }
2118 broadcast.push(msg_surface_title(cs.session_id, surface_id, &title));
2119 }
2120 CompositorEvent::SurfaceAppId { surface_id, app_id } => {
2121 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
2122 info.app_id = app_id.clone();
2123 }
2124 broadcast.push(msg_surface_app_id(cs.session_id, surface_id, &app_id));
2125 }
2126 CompositorEvent::SurfaceResized {
2127 surface_id,
2128 width,
2129 height,
2130 } => {
2131 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
2132 info.width = width;
2133 info.height = height;
2134 }
2135 cs.last_pixels.remove(&surface_id);
2136 invalidate_client_encoders.push(surface_id);
2137 broadcast.push(msg_surface_resized(
2138 cs.session_id,
2139 surface_id,
2140 width,
2141 height,
2142 ));
2143 }
2144 CompositorEvent::ClipboardContent {
2145 surface_id,
2146 mime_type,
2147 data,
2148 } => {
2149 broadcast.push(msg_s2c_clipboard(
2150 cs.session_id,
2151 surface_id,
2152 &mime_type,
2153 &data,
2154 ));
2155 }
2156 }
2157 }
2158 for msg in &broadcast {
2159 sess.send_to_all(msg);
2160 }
2161 }
2162 sess.surface_commits += surface_commit_count;
2163
2164 for sid in invalidate_client_encoders {
2167 for c in sess.clients.values_mut() {
2168 c.surface_encoders.remove(&sid);
2169 c.surface_last_frames.remove(&sid);
2170 c.surface_last_encoded_gen.remove(&sid);
2171 }
2172 }
2173
2174 let pixel_snapshot: Vec<(u16, u32, u32, u64)> = sess
2182 .compositor
2183 .as_ref()
2184 .map(|cs| {
2185 cs.last_pixels
2186 .iter()
2187 .map(|(&sid, lp)| (sid, lp.width, lp.height, lp.generation))
2188 .collect()
2189 })
2190 .unwrap_or_default();
2191
2192 struct EncodeJob {
2198 cid: u64,
2199 sid: u16,
2200 px_w: u32,
2201 px_h: u32,
2202 pixels: blit_compositor::PixelData,
2203 needs_keyframe: bool,
2204 encoder: SurfaceEncoder,
2205 generation: u64,
2206 }
2207 struct EncodeResult {
2208 cid: u64,
2209 sid: u16,
2210 px_w: u32,
2211 px_h: u32,
2212 generation: u64,
2213 encoder: SurfaceEncoder,
2214 nal_data: Option<(Vec<u8>, bool)>, codec_flag: u8,
2216 }
2217
2218 let mut encode_jobs: Vec<EncodeJob> = Vec::new();
2219
2220 struct ClientWork {
2223 cid: u64,
2224 subs: HashSet<u16>,
2225 needs_keyframe: bool,
2226 }
2227 let mut client_work: Vec<ClientWork> = Vec::new();
2228
2229 if !pixel_snapshot.is_empty() {
2230 for (&cid, client) in sess.clients.iter_mut() {
2231 if !window_open(client) {
2232 continue;
2233 }
2234 if client.surface_next_send_at > now {
2235 let deadline = client.surface_next_send_at;
2236 next_deadline = Some(match next_deadline {
2237 Some(existing) => existing.min(deadline),
2238 None => deadline,
2239 });
2240 continue;
2241 }
2242 if client.surface_subscriptions.is_empty() {
2243 continue;
2244 }
2245 client_work.push(ClientWork {
2246 cid,
2247 subs: client.surface_subscriptions.clone(),
2248 needs_keyframe: client.surface_needs_keyframe,
2249 });
2250 let interval = send_interval(client);
2251 advance_deadline(&mut client.surface_next_send_at, now, interval);
2252 }
2253
2254 for work in &client_work {
2255 for &(sid, px_w, px_h, px_gen) in &pixel_snapshot {
2256 if !work.subs.contains(&sid) {
2257 continue;
2258 }
2259 let client = sess.clients.get_mut(&work.cid).unwrap();
2260
2261 if !work.needs_keyframe
2265 && let Some(&last_gen) = client.surface_last_encoded_gen.get(&sid)
2266 && last_gen == px_gen
2267 {
2268 continue;
2269 }
2270
2271 let pixels = {
2272 let cs = sess.compositor.as_ref().unwrap();
2273 match cs.last_pixels.get(&sid) {
2274 Some(lp) if lp.width == px_w && lp.height == px_h => lp.pixels.clone(),
2275 _ => continue,
2276 }
2277 };
2278 let client = sess.clients.get_mut(&work.cid).unwrap();
2279
2280 if client.surface_encodes_in_flight.contains(&sid) {
2286 continue;
2287 }
2288
2289 let needs_new_encoder = client
2290 .surface_encoders
2291 .get(&sid)
2292 .is_none_or(|e| e.source_dimensions() != (px_w, px_h));
2293 if needs_new_encoder {
2294 client.surface_encoders.remove(&sid);
2295 client.surface_last_frames.remove(&sid);
2296 match SurfaceEncoder::new(
2297 &state.config.surface_encoders,
2298 px_w,
2299 px_h,
2300 &state.config.vaapi_device,
2301 state.config.surface_quality,
2302 state.config.verbose,
2303 client.surface_codec_support,
2304 ) {
2305 Ok(encoder) => {
2306 client.surface_encoders.insert(sid, encoder);
2307 }
2308 Err(err) => {
2309 if state.config.verbose {
2310 eprintln!(
2311 "[surface-encoder] cid={} sid={sid} {px_w}x{px_h}: {err}",
2312 work.cid
2313 );
2314 }
2315 continue;
2316 }
2317 }
2318 }
2319
2320 let encoder = client.surface_encoders.remove(&sid).unwrap();
2321 client.surface_encodes_in_flight.insert(sid);
2322 let needs_kf = work.needs_keyframe || needs_new_encoder;
2325 encode_jobs.push(EncodeJob {
2326 cid: work.cid,
2327 sid,
2328 px_w,
2329 px_h,
2330 pixels,
2331 needs_keyframe: needs_kf,
2332 encoder,
2333 generation: px_gen,
2334 });
2335 }
2336 }
2337 }
2338
2339 if !encode_jobs.is_empty() {
2340 let state2 = state.clone();
2343 tokio::spawn(async move {
2344 let handles: Vec<_> = encode_jobs
2345 .into_iter()
2346 .map(|mut job| {
2347 tokio::task::spawn_blocking(move || {
2348 let t0 = Instant::now();
2349 if job.needs_keyframe {
2350 job.encoder.request_keyframe();
2351 }
2352 let nal_data = if job.needs_keyframe {
2353 job.encoder.encode_keyframe_pixels(&job.pixels)
2354 } else {
2355 job.encoder.encode_pixels(&job.pixels)
2356 };
2357 let codec_flag = job.encoder.codec_flag();
2358 let encode_ms = t0.elapsed().as_millis();
2359
2360 if encode_ms > 50 {
2361 eprintln!(
2362 "[surface-encoder] cid={} sid={} {}x{} encode took {encode_ms}ms",
2363 job.cid, job.sid, job.px_w, job.px_h
2364 );
2365 }
2366 EncodeResult {
2367 cid: job.cid,
2368 sid: job.sid,
2369 px_w: job.px_w,
2370 px_h: job.px_h,
2371 generation: job.generation,
2372 encoder: job.encoder,
2373 nal_data,
2374 codec_flag,
2375 }
2376 })
2377 })
2378 .collect();
2379
2380 let mut results = Vec::with_capacity(handles.len());
2381 for h in handles {
2382 if let Ok(r) = h.await {
2383 results.push(r);
2384 }
2385 }
2386
2387 let mut sess = state2.session.lock().await;
2389 let now = Instant::now();
2390 let mut local_encodes = 0u32;
2391 let mut local_encode_bytes = 0u64;
2392 let mut local_frames_sent = 0u32;
2393
2394 for result in results {
2395 let dims_match = sess
2403 .compositor
2404 .as_ref()
2405 .and_then(|cs| cs.last_pixels.get(&result.sid))
2406 .is_some_and(|lp| result.encoder.source_dimensions() == (lp.width, lp.height));
2407 if let Some(client) = sess.clients.get_mut(&result.cid) {
2408 client.surface_encodes_in_flight.remove(&result.sid);
2409 if dims_match {
2410 client.surface_encoders.insert(result.sid, result.encoder);
2411 }
2412 client
2415 .surface_last_encoded_gen
2416 .insert(result.sid, result.generation);
2417 }
2418
2419 let Some((nal_data, is_keyframe)) = result.nal_data else {
2420 continue;
2421 };
2422
2423 local_encodes += 1;
2424 local_encode_bytes += nal_data.len() as u64;
2425
2426 let (session_id, created_at) = sess
2427 .compositor
2428 .as_ref()
2429 .map(|cs| (cs.session_id, cs.created_at))
2430 .unwrap_or((0, now));
2431
2432 let flags = result.codec_flag
2433 | if is_keyframe {
2434 SURFACE_FRAME_FLAG_KEYFRAME
2435 } else {
2436 0
2437 };
2438 let timestamp = created_at.elapsed().as_millis() as u32;
2439 let msg = msg_surface_frame(
2440 session_id,
2441 result.sid,
2442 timestamp,
2443 flags,
2444 result.px_w as u16,
2445 result.px_h as u16,
2446 &nal_data,
2447 );
2448 let bytes = msg.len();
2449
2450 let Some(client) = sess.clients.get_mut(&result.cid) else {
2451 continue;
2452 };
2453
2454 client.surface_last_frames.insert(
2455 result.sid,
2456 BufferedSurfaceFrame {
2457 _msg: msg.clone(),
2458 _is_keyframe: is_keyframe,
2459 },
2460 );
2461
2462 match client.tx.try_send(msg) {
2469 Err(_e) => {
2470 client.surface_needs_keyframe = true;
2474 }
2475 Ok(()) => {
2476 record_send(client, bytes, now, true);
2477 client.frames_sent = client.frames_sent.wrapping_add(1);
2478 local_frames_sent += 1;
2479 if client.surface_needs_keyframe && is_keyframe {
2480 client.surface_needs_keyframe = false;
2481 }
2482 }
2483 }
2484 }
2485 sess.surface_encodes += local_encodes;
2486 sess.surface_encode_bytes += local_encode_bytes;
2487 sess.surface_frames_sent += local_frames_sent;
2488 drop(sess);
2489 state2.delivery_notify.notify_one();
2491 });
2492 }
2493
2494 {
2505 let mut wanted: HashSet<u16> = HashSet::new();
2511 for client in sess.clients.values() {
2512 if !window_open(client) {
2513 continue;
2514 }
2515 let surface_ready = client.surface_next_send_at <= now;
2516 if !surface_ready {
2517 let deadline = client.surface_next_send_at;
2520 next_deadline = Some(match next_deadline {
2521 Some(existing) => existing.min(deadline),
2522 None => deadline,
2523 });
2524 continue;
2525 }
2526 for &sid in &client.surface_subscriptions {
2527 wanted.insert(sid);
2528 }
2529 }
2530 if let Some(cs) = sess.compositor.as_mut() {
2534 let mut sent_any = false;
2535 for sid in &wanted {
2536 if !cs.pending_frame_requests.contains(sid) {
2537 cs.pending_frame_requests.insert(*sid);
2538 let _ = cs
2539 .handle
2540 .command_tx
2541 .send(CompositorCommand::RequestFrame { surface_id: *sid });
2542 sent_any = true;
2543 }
2544 }
2545 if sent_any {
2546 cs.handle.wake();
2547 }
2548 }
2549 }
2550
2551 TickOutcome {
2552 did_work,
2553 next_deadline,
2554 }
2555}
2556
2557async fn handle_client<S: AsyncRead + AsyncWrite + Unpin + Send + 'static>(
2558 stream: S,
2559 state: AppState,
2560) {
2561 let config = &state.config;
2562 let notify_for_compositor = {
2563 let n = state.delivery_notify.clone();
2564 Arc::new(move || n.notify_one()) as Arc<dyn Fn() + Send + Sync>
2565 };
2566 let (mut reader, mut writer) = tokio::io::split(stream);
2567
2568 let (out_tx, mut out_rx) = mpsc::channel::<Vec<u8>>(OUTBOX_CAPACITY);
2569 let sender = tokio::spawn(async move {
2570 while let Some(msg) = out_rx.recv().await {
2571 if !write_frame(&mut writer, &msg).await {
2572 break;
2573 }
2574 }
2575 });
2576 let client_id;
2577
2578 {
2579 let mut sess = state.session.lock().await;
2580 client_id = sess.next_client_id;
2581 sess.next_client_id += 1;
2582 sess.clients.insert(
2583 client_id,
2584 ClientState {
2585 tx: out_tx,
2586 lead: None,
2587 subscriptions: HashSet::new(),
2588 surface_subscriptions: HashSet::new(),
2589 view_sizes: HashMap::new(),
2590 scroll_offsets: HashMap::new(),
2591 scroll_caches: HashMap::new(),
2592 last_sent: HashMap::new(),
2593 preview_next_send_at: HashMap::new(),
2594 rtt_ms: 50.0,
2595 min_rtt_ms: 0.0,
2596 display_fps: 60.0,
2597 delivery_bps: 262_144.0,
2602 goodput_bps: 262_144.0,
2603 goodput_jitter_bps: 0.0,
2604 max_goodput_jitter_bps: 0.0,
2605 last_goodput_sample_bps: 0.0,
2606 avg_frame_bytes: 1_024.0,
2607 avg_paced_frame_bytes: 1_024.0,
2608 avg_preview_frame_bytes: 1_024.0,
2609 inflight_bytes: 0,
2610 inflight_frames: VecDeque::new(),
2611 next_send_at: Instant::now(),
2612 probe_frames: 0.0,
2613 frames_sent: 0,
2614 acks_recv: 0,
2615 acked_bytes_since_log: 0,
2616 browser_backlog_frames: 0,
2617 browser_ack_ahead_frames: 0,
2618 browser_apply_ms: 0.0,
2619 last_metrics_update: Instant::now(),
2620 last_log: Instant::now(),
2621 goodput_window_bytes: 0,
2622 goodput_window_start: Instant::now(),
2623 surface_next_send_at: Instant::now(),
2624 surface_needs_keyframe: true,
2625 surface_encoders: HashMap::new(),
2626 surface_encodes_in_flight: HashSet::new(),
2627 surface_last_frames: HashMap::new(),
2628 surface_last_encoded_gen: HashMap::new(),
2629 surface_view_sizes: HashMap::new(),
2630 surface_codec_support: 0,
2631 pressed_surface_keys: HashSet::new(),
2632 },
2633 );
2634 state.delivery_notify.notify_one();
2636 if let Some(c) = sess.clients.get(&client_id) {
2637 let _ = c.tx.try_send(msg_hello(
2638 1,
2639 FEATURE_CREATE_NONCE
2640 | FEATURE_RESTART
2641 | FEATURE_RESIZE_BATCH
2642 | FEATURE_COPY_RANGE
2643 | FEATURE_COMPOSITOR,
2644 ));
2645 }
2646 let mut initial_msgs = Vec::with_capacity(2 + sess.ptys.len() * 2);
2647 initial_msgs.push(sess.pty_list_msg());
2648 for (&id, pty) in &sess.ptys {
2649 let title = pty.driver.title();
2650 if !title.is_empty() {
2651 let title_bytes = title.as_bytes();
2652 let mut msg = Vec::with_capacity(3 + title_bytes.len());
2653 msg.push(S2C_TITLE);
2654 msg.extend_from_slice(&id.to_le_bytes());
2655 msg.extend_from_slice(title_bytes);
2656 initial_msgs.push(msg);
2657 }
2658 if pty.exited {
2659 initial_msgs.push(blit_remote::msg_exited(id, pty.exit_status));
2660 }
2661 }
2662 if let Some(cs) = sess.compositor.as_ref() {
2663 for info in cs.surfaces.values() {
2664 initial_msgs.push(msg_surface_created(
2665 cs.session_id,
2666 info.surface_id,
2667 info.parent_id,
2668 info.width,
2669 info.height,
2670 &info.title,
2671 &info.app_id,
2672 ));
2673 }
2674 }
2675 initial_msgs.push(vec![S2C_READY]);
2676 let tx = sess.clients.get(&client_id).map(|c| c.tx.clone());
2677 drop(sess);
2678 if let Some(tx) = tx {
2679 for msg in initial_msgs {
2680 if tx.send(msg).await.is_err() {
2681 break;
2682 }
2683 }
2684 }
2685 }
2686
2687 if state.config.verbose {
2688 eprintln!("client connected");
2689 }
2690
2691 while let Some(data) = read_frame(&mut reader).await {
2692 if data.is_empty() {
2693 continue;
2694 }
2695
2696 if data[0] == C2S_ACK {
2697 let mut sess = state.session.lock().await;
2698 let (
2699 do_log,
2700 frames_sent,
2701 acks_recv,
2702 rtt_ms,
2703 min_rtt_ms,
2704 eff_rtt_ms,
2705 inflight_bytes,
2706 delivery_bps,
2707 goodput_ewma_bps,
2708 goodput_jitter_bps,
2709 max_goodput_jitter_bps,
2710 avg_frame_bytes,
2711 avg_paced_frame_bytes,
2712 avg_preview_frame_bytes,
2713 display_fps,
2714 paced_fps,
2715 display_need_bps,
2716 probe_frames,
2717 goodput_bps,
2718 window_frames,
2719 window_bytes,
2720 outbox_frames,
2721 browser_backlog_frames,
2722 browser_ack_ahead_frames,
2723 browser_apply_ms,
2724 ) = {
2725 let Some(c) = sess.clients.get_mut(&client_id) else {
2726 continue;
2727 };
2728 c.acks_recv += 1;
2729 record_ack(c);
2730 let do_log = c.last_log.elapsed().as_secs_f32() >= 1.0;
2731 let log_elapsed = c.last_log.elapsed().as_secs_f32().max(1.0e-3);
2732 let paced_fps = pacing_fps(c);
2733 let display_need_bps = display_need_bps(c);
2734 let out = (
2735 do_log,
2736 c.frames_sent,
2737 c.acks_recv,
2738 c.rtt_ms,
2739 path_rtt_ms(c),
2740 window_rtt_ms(c),
2741 c.inflight_bytes,
2742 c.delivery_bps,
2743 c.goodput_bps,
2744 c.goodput_jitter_bps,
2745 c.max_goodput_jitter_bps,
2746 c.avg_frame_bytes,
2747 c.avg_paced_frame_bytes,
2748 c.avg_preview_frame_bytes,
2749 c.display_fps,
2750 paced_fps,
2751 display_need_bps,
2752 c.probe_frames,
2753 c.acked_bytes_since_log as f32 / log_elapsed,
2754 target_frame_window(c),
2755 target_byte_window(c),
2756 outbox_queued_frames(c),
2757 c.browser_backlog_frames,
2758 c.browser_ack_ahead_frames,
2759 c.browser_apply_ms,
2760 );
2761 if do_log {
2762 c.frames_sent = 0;
2763 c.acks_recv = 0;
2764 c.acked_bytes_since_log = 0;
2765 c.last_log = Instant::now();
2766 }
2767 out
2768 };
2769 if do_log && config.verbose {
2770 let surf_info = sess.compositor.as_ref().map(|cs| {
2771 let surfaces = cs.surfaces.len();
2772 let pending = cs.pending_frame_requests.len();
2773 let subs: usize = sess
2774 .clients
2775 .values()
2776 .map(|c| c.surface_subscriptions.len())
2777 .sum();
2778 (surfaces, pending, subs)
2779 });
2780 let (surf_count, surf_pending, surf_subs) = surf_info.unwrap_or((0, 0, 0));
2781 eprintln!(
2782 "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} 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={}",
2783 sess.tick_fires,
2784 sess.tick_snaps,
2785 sess.surface_commits,
2786 sess.surface_encodes,
2787 sess.surface_encode_bytes,
2788 sess.surface_frames_sent,
2789 );
2790 }
2791 if do_log {
2792 sess.tick_fires = 0;
2793 sess.tick_snaps = 0;
2794 sess.surface_commits = 0;
2795 sess.surface_encodes = 0;
2796 sess.surface_encode_bytes = 0;
2797 sess.surface_frames_sent = 0;
2798 }
2799 nudge_delivery(&state);
2800 continue;
2801 }
2802
2803 if data[0] == C2S_DISPLAY_RATE && data.len() >= 3 {
2804 let fps = u16::from_le_bytes([data[1], data[2]]) as f32;
2805 if fps > 0.0 {
2806 let mut sess = state.session.lock().await;
2807 if let Some(c) = sess.clients.get_mut(&client_id) {
2808 c.display_fps = fps;
2809 }
2810 }
2811 nudge_delivery(&state);
2812 continue;
2813 }
2814
2815 if data[0] == C2S_CLIENT_METRICS && data.len() >= 7 {
2816 let backlog_frames = u16::from_le_bytes([data[1], data[2]]);
2817 let ack_ahead_frames = u16::from_le_bytes([data[3], data[4]]);
2818 let apply_ms = u16::from_le_bytes([data[5], data[6]]) as f32 * 0.1;
2819 let mut sess = state.session.lock().await;
2820 if let Some(c) = sess.clients.get_mut(&client_id) {
2821 c.browser_backlog_frames = backlog_frames;
2822 c.browser_ack_ahead_frames = ack_ahead_frames;
2823 c.browser_apply_ms = apply_ms;
2824 c.last_metrics_update = Instant::now();
2825 }
2826 nudge_delivery(&state);
2827 continue;
2828 }
2829
2830 if data[0] == C2S_MOUSE && data.len() >= 9 {
2833 let pid = u16::from_le_bytes([data[1], data[2]]);
2834 let type_ = data[3];
2835 let button = data[4];
2836 let col = u16::from_le_bytes([data[5], data[6]]);
2837 let row = u16::from_le_bytes([data[7], data[8]]);
2838 let sess = state.session.lock().await;
2839 if let Some(pty) = sess.ptys.get(&pid) {
2840 let (echo, icanon) = pty.lflag_cache;
2841 if let Some(seq) = pty
2842 .driver
2843 .mouse_event(type_, button, col, row, echo, icanon)
2844 && let Some(&fd) = state.pty_fds.read().unwrap().get(&pid)
2845 {
2846 pty::pty_write_all(fd, &seq);
2847 }
2848 }
2849 continue;
2850 }
2851
2852 if data[0] == C2S_INPUT && data.len() >= 3 {
2853 let pid = u16::from_le_bytes([data[1], data[2]]);
2854 let mut need_nudge = false;
2855 {
2856 let mut sess = state.session.lock().await;
2857 if let Some(c) = sess.clients.get_mut(&client_id)
2858 && update_client_scroll_state(c, pid, 0)
2859 && let Some(pty) = sess.ptys.get_mut(&pid)
2860 {
2861 pty.mark_dirty();
2862 need_nudge = true;
2863 }
2864 }
2865 if need_nudge {
2866 nudge_delivery(&state);
2867 }
2868 if let Some(&fd) = state.pty_fds.read().unwrap().get(&pid) {
2869 pty::pty_write_all(fd, &data[3..]);
2870 }
2871 continue;
2872 }
2873
2874 if data[0] == C2S_SEARCH && data.len() >= 3 {
2875 let request_id = u16::from_le_bytes([data[1], data[2]]);
2876 let query = std::str::from_utf8(&data[3..]).unwrap_or("").trim();
2877 let mut sess = state.session.lock().await;
2878 let lead = sess.clients.get(&client_id).and_then(|c| c.lead);
2879 let mut ranked: Vec<SearchResultRow> = if query.is_empty() {
2880 Vec::new()
2881 } else {
2882 sess.ptys
2883 .iter()
2884 .filter_map(|(&pty_id, pty)| {
2885 pty.driver
2886 .search_result(query)
2887 .map(|result| SearchResultRow {
2888 pty_id,
2889 score: result.score,
2890 primary_source: result.primary_source,
2891 matched_sources: result.matched_sources,
2892 context: result.context,
2893 scroll_offset: result.scroll_offset,
2894 })
2895 })
2896 .collect()
2897 };
2898 ranked.sort_by(|a, b| {
2899 b.score
2900 .cmp(&a.score)
2901 .then_with(|| (Some(b.pty_id) == lead).cmp(&(Some(a.pty_id) == lead)))
2902 .then_with(|| a.pty_id.cmp(&b.pty_id))
2903 });
2904 if let Some(client) = sess.clients.get_mut(&client_id) {
2905 let _ = client
2906 .tx
2907 .try_send(build_search_results_msg(request_id, &ranked));
2908 }
2909 continue;
2910 }
2911
2912 if data[0] == C2S_SURFACE_CAPTURE && data.len() >= 5 {
2913 let surface_id = u16::from_le_bytes([data[3], data[4]]);
2914 let format = data.get(5).copied().unwrap_or(CAPTURE_FORMAT_PNG);
2916 let quality = data.get(6).copied().unwrap_or(0);
2917
2918 let mut reply_msg = vec![S2C_SURFACE_CAPTURE];
2919 reply_msg.extend_from_slice(&surface_id.to_le_bytes());
2920
2921 let sess = state.session.lock().await;
2922 let captured = sess
2923 .compositor
2924 .as_ref()
2925 .and_then(|cs| cs.last_pixels.get(&surface_id))
2926 .map(|lp| (lp.width, lp.height, lp.pixels.to_rgba(lp.width, lp.height)));
2927 let client_tx = sess.clients.get(&client_id).map(|c| c.tx.clone());
2928 drop(sess);
2929
2930 if let Some((w, h, rgba_pixels)) = captured {
2931 let image_data = encode_capture(&rgba_pixels, w, h, format, quality);
2932 reply_msg.extend_from_slice(&w.to_le_bytes());
2933 reply_msg.extend_from_slice(&h.to_le_bytes());
2934 reply_msg.extend_from_slice(&image_data);
2935 } else {
2936 reply_msg.extend_from_slice(&0u32.to_le_bytes());
2937 reply_msg.extend_from_slice(&0u32.to_le_bytes());
2938 }
2939
2940 if let Some(client_tx) = client_tx {
2941 let _ = client_tx.try_send(reply_msg);
2942 }
2943 continue;
2944 }
2945
2946 let mut sess = state.session.lock().await;
2947 let mut need_nudge = false;
2948 match data[0] {
2949 C2S_SCROLL if data.len() >= 7 => {
2950 let pid = u16::from_le_bytes([data[1], data[2]]);
2951 let offset = u32::from_le_bytes([data[3], data[4], data[5], data[6]]) as usize;
2952 if sess.ptys.contains_key(&pid) {
2953 if let Some(c) = sess.clients.get_mut(&client_id) {
2954 update_client_scroll_state(c, pid, offset);
2955 }
2956 if let Some(pty) = sess.ptys.get_mut(&pid) {
2957 pty.mark_dirty();
2958 need_nudge = true;
2959 }
2960 }
2961 }
2962 C2S_RESIZE if data.len() >= 7 => {
2963 let entries = data[1..].chunks_exact(6);
2964 if !entries.remainder().is_empty() {
2965 continue;
2966 }
2967 let mut touched = Vec::with_capacity((data.len() - 1) / 6);
2968 for entry in entries {
2969 let pid = u16::from_le_bytes([entry[0], entry[1]]);
2970 if !sess.ptys.contains_key(&pid) {
2971 continue;
2972 }
2973 let rows = u16::from_le_bytes([entry[2], entry[3]]);
2974 let cols = u16::from_le_bytes([entry[4], entry[5]]);
2975 if let Some(c) = sess.clients.get_mut(&client_id) {
2976 if is_unset_view_size(rows, cols) {
2977 if c.view_sizes.remove(&pid).is_some() {
2978 touched.push(pid);
2979 }
2980 } else if rows == 0 || cols == 0 {
2981 continue;
2982 } else {
2983 c.view_sizes.insert(pid, (rows, cols));
2984 touched.push(pid);
2985 }
2986 }
2987 }
2988 if sess.resize_ptys_to_mediated_sizes(touched) {
2989 need_nudge = true;
2990 }
2991 }
2992 C2S_CREATE => {
2993 let (rows, cols) = if data.len() >= 5 {
2995 (
2996 u16::from_le_bytes([data[1], data[2]]),
2997 u16::from_le_bytes([data[3], data[4]]),
2998 )
2999 } else {
3000 (24, 80)
3001 };
3002 let tag_len = if data.len() >= 7 {
3003 u16::from_le_bytes([data[5], data[6]]) as usize
3004 } else {
3005 0
3006 };
3007 let tag = if data.len() >= 7 + tag_len {
3008 std::str::from_utf8(&data[7..7 + tag_len]).unwrap_or_default()
3009 } else {
3010 ""
3011 };
3012 let cmd_start = 7 + tag_len;
3013 let dir: Option<String> = None;
3014 let create_payload = data
3015 .get(cmd_start..)
3016 .and_then(|bytes| std::str::from_utf8(bytes).ok());
3017 let command = create_payload
3018 .filter(|payload| !payload.contains('\0'))
3019 .map(str::trim)
3020 .filter(|payload| !payload.is_empty());
3021 let argv: Option<Vec<&str>> = create_payload
3022 .filter(|payload| payload.contains('\0'))
3023 .map(|payload| {
3024 payload
3025 .split('\0')
3026 .filter(|arg| !arg.is_empty())
3027 .collect::<Vec<_>>()
3028 })
3029 .filter(|args| !args.is_empty());
3030 let Some(id) = sess.allocate_pty_id(config.max_ptys) else {
3031 continue;
3032 };
3033 let socket_name = sess
3034 .ensure_compositor(config.verbose, notify_for_compositor.clone())
3035 .to_string();
3036 if let Some(pty) = pty::spawn_pty(
3037 &config.shell,
3038 &config.shell_flags,
3039 rows,
3040 cols,
3041 id,
3042 tag,
3043 command,
3044 argv.as_deref(),
3045 dir.as_deref(),
3046 config.scrollback,
3047 state.clone(),
3048 Some(&socket_name),
3049 ) {
3050 let mut msg = Vec::with_capacity(3 + pty.tag.len());
3051 msg.push(S2C_CREATED);
3052 msg.extend_from_slice(&id.to_le_bytes());
3053 msg.extend_from_slice(pty.tag.as_bytes());
3054 sess.ptys.insert(id, pty);
3055 if let Some(c) = sess.clients.get_mut(&client_id) {
3056 c.lead = Some(id);
3057 c.view_sizes.insert(id, (rows, cols));
3058 subscribe_client_to(c, id);
3059 reset_inflight(c);
3060 }
3061 sess.send_to_all(&msg);
3062 need_nudge = true;
3063 }
3064 }
3065 C2S_CREATE_N => {
3066 let nonce = if data.len() >= 3 {
3068 u16::from_le_bytes([data[1], data[2]])
3069 } else {
3070 0
3071 };
3072 let (rows, cols) = if data.len() >= 7 {
3073 (
3074 u16::from_le_bytes([data[3], data[4]]),
3075 u16::from_le_bytes([data[5], data[6]]),
3076 )
3077 } else {
3078 (24, 80)
3079 };
3080 let tag_len = if data.len() >= 9 {
3081 u16::from_le_bytes([data[7], data[8]]) as usize
3082 } else {
3083 0
3084 };
3085 let tag = if data.len() >= 9 + tag_len {
3086 std::str::from_utf8(&data[9..9 + tag_len]).unwrap_or_default()
3087 } else {
3088 ""
3089 };
3090 let cmd_start = 9 + tag_len;
3091 let dir: Option<String> = None;
3092 let create_payload = data
3093 .get(cmd_start..)
3094 .and_then(|bytes| std::str::from_utf8(bytes).ok());
3095 let command = create_payload
3096 .filter(|payload| !payload.contains('\0'))
3097 .map(str::trim)
3098 .filter(|payload| !payload.is_empty());
3099 let argv: Option<Vec<&str>> = create_payload
3100 .filter(|payload| payload.contains('\0'))
3101 .map(|payload| {
3102 payload
3103 .split('\0')
3104 .filter(|arg| !arg.is_empty())
3105 .collect::<Vec<_>>()
3106 })
3107 .filter(|args| !args.is_empty());
3108 let Some(id) = sess.allocate_pty_id(config.max_ptys) else {
3109 continue;
3110 };
3111 let socket_name = sess
3112 .ensure_compositor(config.verbose, notify_for_compositor.clone())
3113 .to_string();
3114 if let Some(pty) = pty::spawn_pty(
3115 &config.shell,
3116 &config.shell_flags,
3117 rows,
3118 cols,
3119 id,
3120 tag,
3121 command,
3122 argv.as_deref(),
3123 dir.as_deref(),
3124 config.scrollback,
3125 state.clone(),
3126 Some(&socket_name),
3127 ) {
3128 let tag_bytes = pty.tag.as_bytes();
3129 let mut nonce_msg = Vec::with_capacity(5 + tag_bytes.len());
3130 nonce_msg.push(S2C_CREATED_N);
3131 nonce_msg.extend_from_slice(&nonce.to_le_bytes());
3132 nonce_msg.extend_from_slice(&id.to_le_bytes());
3133 nonce_msg.extend_from_slice(tag_bytes);
3134 let mut broadcast_msg = Vec::with_capacity(3 + tag_bytes.len());
3135 broadcast_msg.push(S2C_CREATED);
3136 broadcast_msg.extend_from_slice(&id.to_le_bytes());
3137 broadcast_msg.extend_from_slice(tag_bytes);
3138 sess.ptys.insert(id, pty);
3139 if let Some(c) = sess.clients.get_mut(&client_id) {
3140 c.lead = Some(id);
3141 c.view_sizes.insert(id, (rows, cols));
3142 subscribe_client_to(c, id);
3143 reset_inflight(c);
3144 let _ = c.tx.try_send(nonce_msg);
3145 }
3146 for (&cid, c) in sess.clients.iter() {
3147 if cid != client_id {
3148 let _ = c.tx.try_send(broadcast_msg.clone());
3149 }
3150 }
3151 need_nudge = true;
3152 }
3153 }
3154 C2S_CREATE_AT => {
3155 let (rows, cols) = if data.len() >= 5 {
3157 (
3158 u16::from_le_bytes([data[1], data[2]]),
3159 u16::from_le_bytes([data[3], data[4]]),
3160 )
3161 } else {
3162 (24, 80)
3163 };
3164 let tag_len = if data.len() >= 7 {
3165 u16::from_le_bytes([data[5], data[6]]) as usize
3166 } else {
3167 0
3168 };
3169 let tag = if data.len() >= 7 + tag_len {
3170 std::str::from_utf8(&data[7..7 + tag_len]).unwrap_or_default()
3171 } else {
3172 ""
3173 };
3174 let src_start = 7 + tag_len;
3175 let dir = if data.len() >= src_start + 2 {
3176 let src_id = u16::from_le_bytes([data[src_start], data[src_start + 1]]);
3177 sess.ptys.get(&src_id).and_then(|p| pty::pty_cwd(&p.handle))
3178 } else {
3179 None
3180 };
3181 let Some(id) = sess.allocate_pty_id(config.max_ptys) else {
3182 continue;
3183 };
3184 let socket_name = sess
3185 .ensure_compositor(config.verbose, notify_for_compositor.clone())
3186 .to_string();
3187 if let Some(pty) = pty::spawn_pty(
3188 &config.shell,
3189 &config.shell_flags,
3190 rows,
3191 cols,
3192 id,
3193 tag,
3194 None,
3195 None,
3196 dir.as_deref(),
3197 config.scrollback,
3198 state.clone(),
3199 Some(&socket_name),
3200 ) {
3201 let mut msg = Vec::with_capacity(3 + pty.tag.len());
3202 msg.push(S2C_CREATED);
3203 msg.extend_from_slice(&id.to_le_bytes());
3204 msg.extend_from_slice(pty.tag.as_bytes());
3205 sess.ptys.insert(id, pty);
3206 if let Some(c) = sess.clients.get_mut(&client_id) {
3207 c.lead = Some(id);
3208 c.view_sizes.insert(id, (rows, cols));
3209 subscribe_client_to(c, id);
3210 reset_inflight(c);
3211 }
3212 sess.send_to_all(&msg);
3213 need_nudge = true;
3214 }
3215 }
3216 C2S_CREATE2 => {
3217 if data.len() < 10 {
3218 continue;
3219 }
3220 let nonce = u16::from_le_bytes([data[1], data[2]]);
3221 let rows = u16::from_le_bytes([data[3], data[4]]);
3222 let cols = u16::from_le_bytes([data[5], data[6]]);
3223 let features = data[7];
3224 let tag_len = u16::from_le_bytes([data[8], data[9]]) as usize;
3225 let tag = if data.len() >= 10 + tag_len {
3226 std::str::from_utf8(&data[10..10 + tag_len]).unwrap_or_default()
3227 } else {
3228 ""
3229 };
3230 let mut cursor = 10 + tag_len;
3231 let dir = if features & CREATE2_HAS_SRC_PTY != 0 && data.len() >= cursor + 2 {
3232 let src_id = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
3233 cursor += 2;
3234 sess.ptys.get(&src_id).and_then(|p| pty::pty_cwd(&p.handle))
3235 } else {
3236 None
3237 };
3238 let create_payload = if features & CREATE2_HAS_COMMAND != 0 {
3239 data.get(cursor..).and_then(|b| std::str::from_utf8(b).ok())
3240 } else {
3241 None
3242 };
3243 let command = create_payload
3244 .filter(|p| !p.contains('\0'))
3245 .map(str::trim)
3246 .filter(|p| !p.is_empty());
3247 let argv: Option<Vec<&str>> = create_payload
3248 .filter(|p| p.contains('\0'))
3249 .map(|p| p.split('\0').filter(|a| !a.is_empty()).collect::<Vec<_>>())
3250 .filter(|a| !a.is_empty());
3251 let Some(id) = sess.allocate_pty_id(config.max_ptys) else {
3252 continue;
3253 };
3254 let socket_name = sess
3255 .ensure_compositor(config.verbose, notify_for_compositor.clone())
3256 .to_string();
3257 if let Some(pty) = pty::spawn_pty(
3258 &config.shell,
3259 &config.shell_flags,
3260 rows,
3261 cols,
3262 id,
3263 tag,
3264 command,
3265 argv.as_deref(),
3266 dir.as_deref(),
3267 config.scrollback,
3268 state.clone(),
3269 Some(&socket_name),
3270 ) {
3271 let tag_bytes = pty.tag.as_bytes();
3272 let mut nonce_msg = Vec::with_capacity(5 + tag_bytes.len());
3273 nonce_msg.push(S2C_CREATED_N);
3274 nonce_msg.extend_from_slice(&nonce.to_le_bytes());
3275 nonce_msg.extend_from_slice(&id.to_le_bytes());
3276 nonce_msg.extend_from_slice(tag_bytes);
3277 let mut broadcast_msg = Vec::with_capacity(3 + tag_bytes.len());
3278 broadcast_msg.push(S2C_CREATED);
3279 broadcast_msg.extend_from_slice(&id.to_le_bytes());
3280 broadcast_msg.extend_from_slice(tag_bytes);
3281 sess.ptys.insert(id, pty);
3282 if let Some(c) = sess.clients.get_mut(&client_id) {
3283 c.lead = Some(id);
3284 c.view_sizes.insert(id, (rows, cols));
3285 subscribe_client_to(c, id);
3286 reset_inflight(c);
3287 let _ = c.tx.try_send(nonce_msg);
3288 }
3289 for (&cid, c) in sess.clients.iter() {
3290 if cid != client_id {
3291 let _ = c.tx.try_send(broadcast_msg.clone());
3292 }
3293 }
3294 need_nudge = true;
3295 }
3296 }
3297 C2S_SURFACE_INPUT if data.len() >= 10 => {
3298 let session_id = u16::from_le_bytes([data[1], data[2]]);
3299 let surface_id = u16::from_le_bytes([data[3], data[4]]);
3300 let keycode = u32::from_le_bytes([data[5], data[6], data[7], data[8]]);
3301 let pressed = data[9] != 0;
3302 if let Some(client) = sess.clients.get_mut(&client_id) {
3303 if pressed {
3304 client.pressed_surface_keys.insert(keycode);
3305 } else {
3306 client.pressed_surface_keys.remove(&keycode);
3307 }
3308 }
3309 if let Some(cs) = sess
3310 .compositor
3311 .as_mut()
3312 .filter(|cs| session_id == 0 || cs.session_id == session_id)
3313 {
3314 let _ = cs.handle.command_tx.send(CompositorCommand::KeyInput {
3315 surface_id,
3316 keycode,
3317 pressed,
3318 });
3319 cs.handle.wake();
3320 state.delivery_notify.notify_one();
3321 }
3322 }
3323 C2S_SURFACE_POINTER if data.len() >= 11 => {
3324 let session_id = u16::from_le_bytes([data[1], data[2]]);
3325 let surface_id = u16::from_le_bytes([data[3], data[4]]);
3326 let ptype = data[5];
3327 let button = data[6];
3328 let x = u16::from_le_bytes([data[7], data[8]]) as f64;
3329 let y = u16::from_le_bytes([data[9], data[10]]) as f64;
3330 if let Some(cs) = sess
3331 .compositor
3332 .as_mut()
3333 .filter(|cs| session_id == 0 || cs.session_id == session_id)
3334 {
3335 match ptype {
3336 0 | 1 => {
3337 let _ = cs.handle.command_tx.send(CompositorCommand::PointerMotion {
3338 surface_id,
3339 x,
3340 y,
3341 });
3342 let _ = cs.handle.command_tx.send(CompositorCommand::PointerButton {
3343 surface_id,
3344 button: match button {
3345 1 => 0x112,
3346 2 => 0x111,
3347 _ => 0x110,
3348 },
3349 pressed: ptype == 0,
3350 });
3351 }
3352 2 => {
3353 let _ = cs.handle.command_tx.send(CompositorCommand::PointerMotion {
3354 surface_id,
3355 x,
3356 y,
3357 });
3358 }
3359 _ => {}
3360 }
3361 cs.handle.wake();
3362 }
3363 state.delivery_notify.notify_one();
3364 }
3365 C2S_SURFACE_POINTER_AXIS if data.len() >= 10 => {
3366 let session_id = u16::from_le_bytes([data[1], data[2]]);
3367 let surface_id = u16::from_le_bytes([data[3], data[4]]);
3368 let axis = data[5];
3369 let value_x100 = i32::from_le_bytes([data[6], data[7], data[8], data[9]]);
3370 let value = value_x100 as f64 / 100.0;
3371 if let Some(cs) = sess
3372 .compositor
3373 .as_mut()
3374 .filter(|cs| session_id == 0 || cs.session_id == session_id)
3375 {
3376 let _ = cs.handle.command_tx.send(CompositorCommand::PointerAxis {
3377 surface_id,
3378 axis,
3379 value,
3380 });
3381 cs.handle.wake();
3382 }
3383 state.delivery_notify.notify_one();
3384 }
3385 C2S_SURFACE_RESIZE if data.len() >= 9 => {
3386 let _session_id = u16::from_le_bytes([data[1], data[2]]);
3387 let surface_id = u16::from_le_bytes([data[3], data[4]]);
3388 let width = u16::from_le_bytes([data[5], data[6]]);
3389 let height = u16::from_le_bytes([data[7], data[8]]);
3390 let scale_120 = if data.len() >= 11 {
3392 u16::from_le_bytes([data[9], data[10]])
3393 } else {
3394 0
3395 };
3396 let codec_support = if data.len() >= 12 { data[11] } else { 0 };
3398 if let Some(c) = sess.clients.get_mut(&client_id) {
3399 if is_unset_view_size(width, height) {
3400 c.surface_view_sizes.remove(&surface_id);
3401 } else if width > 0 && height > 0 {
3402 c.surface_view_sizes
3403 .insert(surface_id, (width, height, scale_120, codec_support));
3404 }
3405 c.surface_codec_support = codec_support;
3406 }
3407 sess.resize_surfaces_to_mediated_sizes(
3408 std::iter::once(surface_id),
3409 &state.config.surface_encoders,
3410 );
3411 }
3412 C2S_SURFACE_FOCUS if data.len() >= 5 => {
3413 let session_id = u16::from_le_bytes([data[1], data[2]]);
3414 let surface_id = u16::from_le_bytes([data[3], data[4]]);
3415 if let Some(cs) = sess
3416 .compositor
3417 .as_ref()
3418 .filter(|cs| session_id == 0 || cs.session_id == session_id)
3419 {
3420 let _ = cs
3421 .handle
3422 .command_tx
3423 .send(CompositorCommand::SurfaceFocus { surface_id });
3424 }
3425 }
3426 C2S_SURFACE_CLOSE if data.len() >= 5 => {
3427 let session_id = u16::from_le_bytes([data[1], data[2]]);
3428 let surface_id = u16::from_le_bytes([data[3], data[4]]);
3429 if let Some(cs) = sess
3430 .compositor
3431 .as_ref()
3432 .filter(|cs| session_id == 0 || cs.session_id == session_id)
3433 {
3434 let _ = cs
3435 .handle
3436 .command_tx
3437 .send(CompositorCommand::SurfaceClose { surface_id });
3438 cs.handle.wake();
3439 }
3440 }
3441 C2S_SURFACE_SUBSCRIBE if data.len() >= 5 => {
3442 let session_id = u16::from_le_bytes([data[1], data[2]]);
3443 let surface_id = u16::from_le_bytes([data[3], data[4]]);
3444 if state.config.verbose {
3445 eprintln!(
3446 "C2S_SURFACE_SUBSCRIBE: cid={client_id} session={session_id} surface={surface_id}"
3447 );
3448 }
3449 if let Some(c) = sess.clients.get_mut(&client_id) {
3450 c.surface_subscriptions.insert(surface_id);
3451 }
3452 if let Some(c) = sess.clients.get_mut(&client_id) {
3456 c.surface_needs_keyframe = true;
3457 }
3458 state.delivery_notify.notify_one();
3459 }
3460 C2S_SURFACE_UNSUBSCRIBE if data.len() >= 5 => {
3461 let _session_id = u16::from_le_bytes([data[1], data[2]]);
3462 let surface_id = u16::from_le_bytes([data[3], data[4]]);
3463 if let Some(c) = sess.clients.get_mut(&client_id) {
3464 c.surface_subscriptions.remove(&surface_id);
3465 c.surface_view_sizes.remove(&surface_id);
3466 }
3467 sess.resize_surfaces_to_mediated_sizes(
3468 std::iter::once(surface_id),
3469 &state.config.surface_encoders,
3470 );
3471 }
3472 C2S_SURFACE_REQUEST_KEYFRAME if data.len() >= 3 => {
3473 let _surface_id = u16::from_le_bytes([data[1], data[2]]);
3474 if let Some(c) = sess.clients.get_mut(&client_id) {
3475 c.surface_needs_keyframe = true;
3476 }
3477 state.delivery_notify.notify_one();
3478 }
3479 C2S_SURFACE_ACK if data.len() >= 3 => {
3480 if let Some(c) = sess.clients.get_mut(&client_id) {
3483 c.acks_recv += 1;
3484 record_ack(c);
3485 }
3486 state.delivery_notify.notify_one();
3487 }
3488 C2S_CLIPBOARD if data.len() >= 9 => {
3489 let session_id = u16::from_le_bytes([data[1], data[2]]);
3490 let surface_id = u16::from_le_bytes([data[3], data[4]]);
3491 let mime_len = u16::from_le_bytes([data[5], data[6]]) as usize;
3492 if data.len() >= 7 + mime_len + 4 {
3493 let mime = std::str::from_utf8(&data[7..7 + mime_len])
3494 .unwrap_or("text/plain")
3495 .to_string();
3496 let data_len = u32::from_le_bytes([
3497 data[7 + mime_len],
3498 data[8 + mime_len],
3499 data[9 + mime_len],
3500 data[10 + mime_len],
3501 ]) as usize;
3502 let payload_start = 11 + mime_len;
3503 if data.len() >= payload_start + data_len {
3504 let payload = data[payload_start..payload_start + data_len].to_vec();
3505 if let Some(cs) = sess
3506 .compositor
3507 .as_ref()
3508 .filter(|cs| cs.session_id == session_id)
3509 {
3510 let _ = cs
3511 .handle
3512 .command_tx
3513 .send(CompositorCommand::ClipboardOffer {
3514 surface_id,
3515 mime_type: mime,
3516 data: payload,
3517 });
3518 }
3519 }
3520 }
3521 }
3522 C2S_SURFACE_LIST if data.len() >= 3 => {
3523 let msg = sess.surface_list_msg();
3524 if let Some(c) = sess.clients.get(&client_id) {
3525 let _ = c.tx.try_send(msg);
3526 }
3527 }
3528 C2S_FOCUS if data.len() >= 3 => {
3529 let pid = u16::from_le_bytes([data[1], data[2]]);
3530 if sess.ptys.contains_key(&pid) {
3531 let old_pid = sess.clients.get(&client_id).and_then(|c| c.lead);
3532 if let Some(c) = sess.clients.get_mut(&client_id) {
3533 c.lead = Some(pid);
3534 subscribe_client_to(c, pid);
3535 if old_pid == Some(pid) {
3536 update_client_scroll_state(c, pid, 0);
3537 } else {
3538 reset_inflight(c);
3539 }
3540 }
3541 if let Some(pty) = sess.ptys.get_mut(&pid) {
3542 pty.mark_dirty();
3543 need_nudge = true;
3544 }
3545 }
3546 }
3547 C2S_SUBSCRIBE if data.len() >= 3 => {
3548 let pid = u16::from_le_bytes([data[1], data[2]]);
3549 if sess.ptys.contains_key(&pid) {
3550 if let Some(c) = sess.clients.get_mut(&client_id) {
3551 subscribe_client_to(c, pid);
3552 }
3553 if let Some(pty) = sess.ptys.get_mut(&pid) {
3554 pty.mark_dirty();
3555 }
3556 need_nudge = true;
3557 }
3558 }
3559 C2S_UNSUBSCRIBE if data.len() >= 3 => {
3560 let pid = u16::from_le_bytes([data[1], data[2]]);
3561 if sess.ptys.contains_key(&pid) {
3562 let mut touched = Vec::new();
3563 if let Some(c) = sess.clients.get_mut(&client_id) {
3564 if unsubscribe_client_from(c, pid) {
3565 touched.push(pid);
3566 }
3567 reset_inflight(c);
3568 }
3569 if sess.resize_ptys_to_mediated_sizes(touched) {
3570 need_nudge = true;
3571 }
3572 }
3573 }
3574 C2S_RESTART if data.len() >= 3 => {
3575 let pid = u16::from_le_bytes([data[1], data[2]]);
3576 let restart_info = sess
3577 .ptys
3578 .get(&pid)
3579 .filter(|p| p.exited)
3580 .map(|p| (p.driver.size(), p.command.clone(), p.tag.clone()));
3581 if let Some(((rows, cols), command, tag)) = restart_info {
3582 let wayland_display = sess
3583 .compositor
3584 .as_ref()
3585 .map(|cs| cs.handle.socket_name.clone());
3586 if let Some((new_handle, reader, byte_rx)) = pty::respawn_child(
3587 &state.config.shell,
3588 &state.config.shell_flags,
3589 rows,
3590 cols,
3591 pid,
3592 command.as_deref(),
3593 state.clone(),
3594 wayland_display.as_deref(),
3595 ) {
3596 let Some(pty) = sess.ptys.get_mut(&pid) else {
3597 break;
3598 };
3599 pty.handle = new_handle;
3600 pty.reader_handle = reader;
3601 pty.byte_rx = byte_rx;
3602 pty.driver.reset_modes();
3603 pty.exited = false;
3604 pty.exit_status = blit_remote::EXIT_STATUS_UNKNOWN;
3605 pty.lflag_cache = pty::pty_lflag(&pty.handle);
3606 pty.lflag_last = Instant::now();
3607 pty.mark_dirty();
3608 if let Some(c) = sess.clients.get_mut(&client_id) {
3609 c.lead = Some(pid);
3610 subscribe_client_to(c, pid);
3611 update_client_scroll_state(c, pid, 0);
3612 reset_inflight(c);
3613 }
3614 let mut msg = Vec::with_capacity(3 + tag.len());
3615 msg.push(S2C_CREATED);
3616 msg.extend_from_slice(&pid.to_le_bytes());
3617 msg.extend_from_slice(tag.as_bytes());
3618 sess.send_to_all(&msg);
3619 need_nudge = true;
3620 }
3621 }
3622 }
3623 C2S_READ if data.len() >= 13 => {
3624 let nonce = u16::from_le_bytes([data[1], data[2]]);
3625 let pid = u16::from_le_bytes([data[3], data[4]]);
3626 let req_offset = u32::from_le_bytes([data[5], data[6], data[7], data[8]]) as usize;
3627 let req_limit =
3628 u32::from_le_bytes([data[9], data[10], data[11], data[12]]) as usize;
3629 let flags = data.get(13).copied().unwrap_or(0);
3630 let ansi = flags & READ_ANSI != 0;
3631 let tail = flags & READ_TAIL != 0;
3632
3633 if let Some(pty) = sess.ptys.get_mut(&pid) {
3634 let (rows, _cols) = pty.driver.size();
3635 let viewport = take_snapshot(pty);
3636 let scrollback_lines = viewport.scrollback_lines() as usize;
3637 let total_lines = scrollback_lines + rows as usize;
3638
3639 let extract = |f: &FrameState| -> String {
3640 if ansi {
3641 f.get_ansi_text()
3642 } else {
3643 f.get_all_text()
3644 }
3645 };
3646
3647 let mut all_lines: Vec<String> =
3648 Vec::with_capacity(scrollback_lines + rows as usize);
3649
3650 let mut scroll_offset = scrollback_lines;
3651 while scroll_offset > 0 {
3652 let frame = pty.driver.scrollback_frame(scroll_offset);
3653 let page = extract(&frame);
3654 let page_lines: Vec<&str> = page.lines().collect();
3655 let take = if scroll_offset < rows as usize {
3656 scroll_offset.min(page_lines.len())
3657 } else {
3658 page_lines.len()
3659 };
3660 for line in &page_lines[..take] {
3661 all_lines.push(line.to_string());
3662 }
3663 if scroll_offset <= rows as usize {
3664 break;
3665 }
3666 scroll_offset = scroll_offset.saturating_sub(rows as usize);
3667 }
3668
3669 for line in extract(&viewport).lines() {
3670 all_lines.push(line.to_string());
3671 }
3672
3673 let (start, end) = if tail {
3674 let end = all_lines.len().saturating_sub(req_offset);
3675 let start = if req_limit == 0 {
3676 0
3677 } else {
3678 end.saturating_sub(req_limit)
3679 };
3680 (start, end)
3681 } else {
3682 let start = req_offset.min(all_lines.len());
3683 let end = if req_limit == 0 {
3684 all_lines.len()
3685 } else {
3686 (start + req_limit).min(all_lines.len())
3687 };
3688 (start, end)
3689 };
3690 let text = all_lines[start..end].join("\n");
3691
3692 let mut msg = Vec::with_capacity(13 + text.len());
3693 msg.push(S2C_TEXT);
3694 msg.extend_from_slice(&nonce.to_le_bytes());
3695 msg.extend_from_slice(&pid.to_le_bytes());
3696 msg.extend_from_slice(&(total_lines as u32).to_le_bytes());
3697 msg.extend_from_slice(&(start as u32).to_le_bytes());
3698 msg.extend_from_slice(text.as_bytes());
3699 if let Some(client) = sess.clients.get(&client_id) {
3700 let _ = client.tx.try_send(msg);
3701 }
3702 }
3703 }
3704 C2S_COPY_RANGE if data.len() >= 18 => {
3705 let nonce = u16::from_le_bytes([data[1], data[2]]);
3706 let pid = u16::from_le_bytes([data[3], data[4]]);
3707 let start_tail = u32::from_le_bytes([data[5], data[6], data[7], data[8]]);
3708 let start_col = u16::from_le_bytes([data[9], data[10]]);
3709 let end_tail = u32::from_le_bytes([data[11], data[12], data[13], data[14]]);
3710 let end_col = u16::from_le_bytes([data[15], data[16]]);
3711
3712 if let Some(pty) = sess.ptys.get(&pid) {
3713 let text = pty
3714 .driver
3715 .get_text_range(start_tail, start_col, end_tail, end_col);
3716 let total_lines = pty.driver.total_lines();
3717
3718 let mut msg = Vec::with_capacity(13 + text.len());
3719 msg.push(S2C_TEXT);
3720 msg.extend_from_slice(&nonce.to_le_bytes());
3721 msg.extend_from_slice(&pid.to_le_bytes());
3722 msg.extend_from_slice(&total_lines.to_le_bytes());
3723 msg.extend_from_slice(&start_tail.to_le_bytes());
3724 msg.extend_from_slice(text.as_bytes());
3725 if let Some(client) = sess.clients.get(&client_id) {
3726 let _ = client.tx.try_send(msg);
3727 }
3728 }
3729 }
3730 C2S_KILL if data.len() >= 7 => {
3731 let pid = u16::from_le_bytes([data[1], data[2]]);
3732 let signal = i32::from_le_bytes([data[3], data[4], data[5], data[6]]);
3733 if let Some(pty) = sess.ptys.get(&pid)
3734 && !pty.exited
3735 {
3736 pty::kill_pty(&pty.handle, signal);
3737 }
3738 }
3739 C2S_CLOSE if data.len() >= 3 => {
3740 let pid = u16::from_le_bytes([data[1], data[2]]);
3741 if let Some(pty) = sess.ptys.remove(&pid) {
3742 if !pty.exited {
3743 state.pty_fds.write().unwrap().remove(&pid);
3744 drop(pty.reader_handle);
3745 pty::close_pty(&pty.handle);
3746 }
3747 for client in sess.clients.values_mut() {
3748 unsubscribe_client_from(client, pid);
3749 }
3750 let mut msg = vec![S2C_CLOSED];
3751 msg.extend_from_slice(&pid.to_le_bytes());
3752 sess.send_to_all(&msg);
3753 let all_done = sess.ptys.is_empty() || sess.ptys.values().all(|p| p.exited);
3755 if all_done && let Some(cs) = sess.compositor.take() {
3756 cs.handle
3757 .shutdown
3758 .store(true, std::sync::atomic::Ordering::Relaxed);
3759 let _ = cs.handle.command_tx.send(CompositorCommand::Shutdown);
3760 }
3761 }
3762 }
3763 _ => {}
3764 }
3765 drop(sess);
3766 if need_nudge {
3767 nudge_delivery(&state);
3768 }
3769 }
3770
3771 {
3772 let mut sess = state.session.lock().await;
3773 let mut need_nudge = false;
3774 let client = sess.clients.remove(&client_id);
3775 let affected_ptys = client
3776 .as_ref()
3777 .map(|c| c.view_sizes.keys().copied().collect::<Vec<_>>())
3778 .unwrap_or_default();
3779 let affected_surfaces = client
3780 .as_ref()
3781 .map(|c| c.surface_view_sizes.keys().copied().collect::<Vec<_>>())
3782 .unwrap_or_default();
3783 if sess.resize_ptys_to_mediated_sizes(affected_ptys) {
3784 need_nudge = true;
3785 }
3786 sess.resize_surfaces_to_mediated_sizes(affected_surfaces, &state.config.surface_encoders);
3787 if let Some(ref client) = client
3791 && !client.pressed_surface_keys.is_empty()
3792 && let Some(cs) = sess.compositor.as_mut()
3793 {
3794 let keycodes: Vec<u32> = client.pressed_surface_keys.iter().copied().collect();
3795 let _ = cs
3796 .handle
3797 .command_tx
3798 .send(CompositorCommand::ReleaseKeys { keycodes });
3799 cs.handle.wake();
3800 }
3801 drop(sess);
3802 if need_nudge {
3803 nudge_delivery(&state);
3804 }
3805 }
3806 sender.abort();
3807 if state.config.verbose {
3808 eprintln!("client disconnected");
3809 }
3810}
3811
3812#[cfg(test)]
3813mod tests {
3814 use super::*;
3815
3816 fn test_client_with_capacity(capacity: usize) -> (ClientState, mpsc::Receiver<Vec<u8>>) {
3817 let (tx, rx) = mpsc::channel(capacity);
3818 let client = ClientState {
3819 tx,
3820 lead: None,
3821 subscriptions: HashSet::new(),
3822 surface_subscriptions: HashSet::new(),
3823 view_sizes: HashMap::new(),
3824 scroll_offsets: HashMap::new(),
3825 scroll_caches: HashMap::new(),
3826 last_sent: HashMap::new(),
3827 preview_next_send_at: HashMap::new(),
3828 rtt_ms: 50.0,
3829 min_rtt_ms: 50.0,
3830 display_fps: 60.0,
3831 delivery_bps: 262_144.0,
3832 goodput_bps: 262_144.0,
3833 goodput_jitter_bps: 0.0,
3834 max_goodput_jitter_bps: 0.0,
3835 last_goodput_sample_bps: 0.0,
3836 avg_frame_bytes: 1_024.0,
3837 avg_paced_frame_bytes: 1_024.0,
3838 avg_preview_frame_bytes: 1_024.0,
3839 inflight_bytes: 0,
3840 inflight_frames: VecDeque::new(),
3841 next_send_at: Instant::now(),
3842 probe_frames: 0.0,
3843 frames_sent: 0,
3844 acks_recv: 0,
3845 acked_bytes_since_log: 0,
3846 browser_backlog_frames: 0,
3847 browser_ack_ahead_frames: 0,
3848 browser_apply_ms: 0.0,
3849 last_metrics_update: Instant::now(),
3850 last_log: Instant::now(),
3851 goodput_window_bytes: 0,
3852 goodput_window_start: Instant::now(),
3853 surface_next_send_at: Instant::now(),
3854 surface_needs_keyframe: true,
3855 surface_encoders: HashMap::new(),
3856 surface_encodes_in_flight: HashSet::new(),
3857 surface_last_frames: HashMap::new(),
3858 surface_last_encoded_gen: HashMap::new(),
3859 surface_view_sizes: HashMap::new(),
3860 surface_codec_support: 0,
3861 pressed_surface_keys: HashSet::new(),
3862 };
3863 (client, rx)
3864 }
3865
3866 fn test_client() -> ClientState {
3867 let (client, _rx) = test_client_with_capacity(OUTBOX_CAPACITY);
3868 client
3869 }
3870
3871 fn fill_inflight(client: &mut ClientState, frames: usize, bytes_per_frame: usize) {
3872 let now = Instant::now();
3873 client.inflight_bytes = frames.saturating_mul(bytes_per_frame);
3874 client.inflight_frames = (0..frames)
3875 .map(|_| InFlightFrame {
3876 sent_at: now,
3877 bytes: bytes_per_frame,
3878 paced: true,
3879 })
3880 .collect();
3881 }
3882
3883 fn sample_frame(text: &str) -> FrameState {
3884 let mut frame = FrameState::new(2, 8);
3885 frame.write_text(0, 0, text, blit_remote::CellStyle::default());
3886 frame
3887 }
3888
3889 #[test]
3890 fn unset_view_size_accepts_zero_pair_only() {
3891 assert!(is_unset_view_size(0, 0));
3892 assert!(!is_unset_view_size(0, 80));
3893 assert!(!is_unset_view_size(u16::MAX, u16::MAX));
3894 }
3895
3896 #[test]
3897 fn unsubscribe_client_from_clears_view_size() {
3898 let mut client = test_client();
3899 client.subscriptions.insert(7);
3900 client.view_sizes.insert(7, (24, 80));
3901 assert!(unsubscribe_client_from(&mut client, 7));
3902 assert!(!client.subscriptions.contains(&7));
3903 assert!(!client.view_sizes.contains_key(&7));
3904 }
3905
3906 #[test]
3907 fn mediated_size_uses_per_pty_view_sizes_without_lead() {
3908 let mut session = Session::new();
3909 let mut c1 = test_client();
3910 let mut c2 = test_client();
3911 c1.view_sizes.insert(7, (30, 120));
3912 c2.view_sizes.insert(7, (24, 100));
3913 session.clients.insert(1, c1);
3914 session.clients.insert(2, c2);
3915 assert_eq!(session.mediated_size_for_pty(7), Some((24, 100)));
3916 }
3917
3918 #[test]
3919 fn mediated_surface_size_picks_min_dimensions_max_scale() {
3920 let mut session = Session::new();
3921 let mut c1 = test_client();
3922 let mut c2 = test_client();
3923 c1.surface_view_sizes.insert(1, (1920, 1080, 240, 0)); c2.surface_view_sizes.insert(1, (1280, 720, 120, 0)); session.clients.insert(1, c1);
3926 session.clients.insert(2, c2);
3927 assert_eq!(
3928 session.mediated_size_for_surface(1, None),
3929 Some((1280, 720, 240))
3930 );
3931 }
3932
3933 #[test]
3934 fn mediated_surface_size_none_when_no_clients() {
3935 let session = Session::new();
3936 assert_eq!(session.mediated_size_for_surface(1, None), None);
3937 }
3938
3939 #[test]
3940 fn mediated_surface_size_single_client() {
3941 let mut session = Session::new();
3942 let mut c1 = test_client();
3943 c1.surface_view_sizes.insert(3, (800, 600, 120, 0));
3944 session.clients.insert(1, c1);
3945 assert_eq!(
3946 session.mediated_size_for_surface(3, None),
3947 Some((800, 600, 120))
3948 );
3949 }
3950
3951 #[test]
3952 fn mediated_surface_size_ignores_other_surfaces() {
3953 let mut session = Session::new();
3954 let mut c1 = test_client();
3955 c1.surface_view_sizes.insert(1, (1920, 1080, 240, 0));
3956 c1.surface_view_sizes.insert(2, (640, 480, 120, 0));
3957 session.clients.insert(1, c1);
3958 assert_eq!(
3959 session.mediated_size_for_surface(1, None),
3960 Some((1920, 1080, 240))
3961 );
3962 assert_eq!(
3963 session.mediated_size_for_surface(2, None),
3964 Some((640, 480, 120))
3965 );
3966 assert_eq!(session.mediated_size_for_surface(3, None), None);
3967 }
3968
3969 #[test]
3970 fn mediated_surface_size_clamped_to_encoder_max() {
3971 let mut session = Session::new();
3972 let mut c1 = test_client();
3973 c1.surface_view_sizes.insert(1, (5000, 3000, 240, 0));
3974 session.clients.insert(1, c1);
3975 assert_eq!(
3976 session.mediated_size_for_surface(1, None),
3977 Some((5000, 3000, 240))
3978 );
3979 assert_eq!(
3980 session.mediated_size_for_surface(1, Some((3840, 2160))),
3981 Some((3840, 2160, 240))
3982 );
3983 }
3984
3985 #[test]
3986 fn due_preview_reserves_the_last_lead_slot() {
3987 let mut client = test_client();
3988 client.lead = Some(1);
3989 client.subscriptions.insert(1);
3990 client.subscriptions.insert(2);
3991
3992 let target_frames = target_frame_window(&client);
3993 let lead_limit = target_frames.saturating_sub(1).max(1);
3994 fill_inflight(&mut client, lead_limit, 512);
3995
3996 assert!(window_open(&client));
3997 assert!(lead_window_open(&client, false));
3998 assert!(!lead_window_open(&client, true));
3999 assert!(can_send_preview(&client, 2, Instant::now()));
4000 }
4001
4002 #[test]
4003 fn entering_scrollback_uses_current_visible_frame_as_baseline() {
4004 let mut client = test_client();
4005 let live = sample_frame("live");
4006 client.lead = Some(7);
4007 client.subscriptions.insert(7);
4008 client.last_sent.insert(7, live.clone());
4009
4010 assert!(update_client_scroll_state(&mut client, 7, 12));
4011 assert_eq!(client.scroll_offsets.get(&7), Some(&12));
4012 assert_eq!(client.scroll_caches.get(&7), Some(&live));
4013 }
4014
4015 #[test]
4016 fn leaving_scrollback_seeds_live_diff_from_scrollback_view() {
4017 let mut client = test_client();
4018 let history = sample_frame("hist");
4019 client.lead = Some(7);
4020 client.subscriptions.insert(7);
4021 client.scroll_offsets.insert(7, 12);
4022 client.scroll_caches.insert(7, history.clone());
4023
4024 assert!(update_client_scroll_state(&mut client, 7, 0));
4025 assert_eq!(client.scroll_offsets.get(&7), None);
4026 assert_eq!(client.last_sent.get(&7), Some(&history));
4027 assert_eq!(client.scroll_caches.get(&7), None);
4028 }
4029
4030 #[tokio::test]
4031 async fn request_surface_capture_returns_pixels_from_compositor() {
4032 let (command_tx, command_rx) = std::sync::mpsc::channel();
4033 std::thread::spawn(move || {
4034 let CompositorCommand::Capture { surface_id, reply } = command_rx.recv().unwrap()
4035 else {
4036 panic!("expected capture command");
4037 };
4038 assert_eq!(surface_id, 7);
4039 let _ = reply.send(Some((2, 3, vec![1, 2, 3, 4])));
4040 });
4041
4042 let result =
4043 request_surface_capture_with_timeout(command_tx, 7, Duration::from_millis(50)).await;
4044
4045 assert_eq!(result, Some((2, 3, vec![1, 2, 3, 4])));
4046 }
4047
4048 #[tokio::test]
4049 async fn request_surface_capture_returns_none_when_compositor_disconnects() {
4050 let (command_tx, command_rx) = std::sync::mpsc::channel();
4051 std::thread::spawn(move || {
4052 let _ = command_rx.recv().unwrap();
4053 });
4054
4055 let result =
4056 request_surface_capture_with_timeout(command_tx, 7, Duration::from_millis(50)).await;
4057
4058 assert_eq!(result, None);
4059 }
4060
4061 #[test]
4064 fn frame_window_minimum_is_two() {
4065 assert!(frame_window(0.0, 60.0) >= 2);
4066 }
4067
4068 #[test]
4069 fn frame_window_scales_with_rtt() {
4070 let low = frame_window(10.0, 60.0);
4071 let high = frame_window(200.0, 60.0);
4072 assert!(high > low, "higher RTT should need more frames in flight");
4073 }
4074
4075 #[test]
4076 fn frame_window_scales_with_fps() {
4077 let slow = frame_window(100.0, 10.0);
4078 let fast = frame_window(100.0, 120.0);
4079 assert!(fast > slow, "higher fps should need more frames in flight");
4080 }
4081
4082 #[test]
4083 fn frame_window_zero_rtt() {
4084 assert!(frame_window(0.0, 120.0) >= 2);
4085 }
4086
4087 #[test]
4090 fn path_rtt_ms_uses_min_when_positive() {
4091 let mut client = test_client();
4092 client.rtt_ms = 100.0;
4093 client.min_rtt_ms = 30.0;
4094 assert_eq!(path_rtt_ms(&client), 30.0);
4095 }
4096
4097 #[test]
4098 fn path_rtt_ms_falls_back_to_rtt_when_min_zero() {
4099 let mut client = test_client();
4100 client.rtt_ms = 80.0;
4101 client.min_rtt_ms = 0.0;
4102 assert_eq!(path_rtt_ms(&client), 80.0);
4103 }
4104
4105 #[test]
4108 fn ewma_rising_uses_rise_alpha() {
4109 let result = ewma_with_direction(100.0, 200.0, 0.5, 0.1);
4110 assert!((result - 150.0).abs() < 0.01);
4112 }
4113
4114 #[test]
4115 fn ewma_falling_uses_fall_alpha() {
4116 let result = ewma_with_direction(200.0, 100.0, 0.5, 0.1);
4117 assert!((result - 190.0).abs() < 0.01);
4119 }
4120
4121 #[test]
4122 fn ewma_same_value_unchanged() {
4123 let result = ewma_with_direction(50.0, 50.0, 0.5, 0.5);
4124 assert!((result - 50.0).abs() < 0.01);
4125 }
4126
4127 #[test]
4130 fn advance_deadline_steps_forward() {
4131 let now = Instant::now();
4132 let mut deadline = now;
4133 let interval = Duration::from_millis(16);
4134 advance_deadline(&mut deadline, now, interval);
4135 assert!(deadline > now);
4136 assert!(deadline <= now + interval + Duration::from_micros(100));
4137 }
4138
4139 #[test]
4140 fn advance_deadline_resets_when_far_behind() {
4141 let now = Instant::now();
4142 let mut deadline = now - Duration::from_secs(10);
4144 let interval = Duration::from_millis(16);
4145 advance_deadline(&mut deadline, now, interval);
4146 assert!(deadline >= now);
4148 }
4149
4150 #[test]
4151 fn should_snapshot_pty_requires_dirty_and_needful() {
4152 assert!(should_snapshot_pty(true, true, false));
4153 assert!(!should_snapshot_pty(false, true, false));
4154 assert!(!should_snapshot_pty(true, false, false));
4155 }
4156
4157 #[test]
4158 fn should_snapshot_pty_defers_synced_output() {
4159 assert!(!should_snapshot_pty(true, true, true));
4160 assert!(should_snapshot_pty(true, true, false));
4161 }
4162
4163 #[test]
4164 fn enqueue_ready_frame_refuses_new_frames_when_capped() {
4165 let mut queue = VecDeque::new();
4166 for cols in 1..=(READY_FRAME_QUEUE_CAP as u16) {
4167 assert!(enqueue_ready_frame(&mut queue, FrameState::new(1, cols)));
4168 }
4169 assert!(!enqueue_ready_frame(
4170 &mut queue,
4171 FrameState::new(1, READY_FRAME_QUEUE_CAP as u16 + 1),
4172 ));
4173 assert_eq!(queue.len(), READY_FRAME_QUEUE_CAP);
4174 assert_eq!(queue.front().map(FrameState::cols), Some(1));
4175 assert_eq!(
4176 queue.back().map(FrameState::cols),
4177 Some(READY_FRAME_QUEUE_CAP as u16),
4178 );
4179 }
4180
4181 #[test]
4182 fn find_sync_output_end_returns_end_of_first_close_sequence() {
4183 let bytes = b"abc\x1b[?2026lrest\x1b[?2026l";
4184 assert_eq!(find_sync_output_end(&[], bytes), Some(11));
4185 }
4186
4187 #[test]
4188 fn find_sync_output_end_returns_none_without_close_sequence() {
4189 assert_eq!(find_sync_output_end(&[], b"\x1b[?2026hpartial"), None);
4190 }
4191
4192 #[test]
4193 fn find_sync_output_end_detects_boundary_split_across_reads() {
4194 assert_eq!(find_sync_output_end(b"abc\x1b[?20", b"26lrest"), Some(3));
4195 }
4196
4197 #[test]
4198 fn update_sync_scan_tail_keeps_recent_suffix_only() {
4199 let mut tail = Vec::new();
4200 update_sync_scan_tail(&mut tail, b"123456789");
4201 assert_eq!(tail, b"3456789");
4202 }
4203
4204 #[test]
4207 fn window_saturated_at_90_percent_frames() {
4208 let client = test_client();
4209 let target = target_frame_window(&client);
4210 let frames_90 = (target * 9).div_ceil(10); assert!(window_saturated(&client, frames_90, 0));
4212 }
4213
4214 #[test]
4215 fn window_saturated_not_at_low_usage() {
4216 let client = test_client();
4217 assert!(!window_saturated(&client, 1, 0));
4218 }
4219
4220 #[test]
4221 fn window_saturated_at_90_percent_bytes() {
4222 let client = test_client();
4223 let target_bytes = target_byte_window(&client);
4224 let bytes_90 = (target_bytes * 9).div_ceil(10);
4225 assert!(window_saturated(&client, 0, bytes_90));
4226 }
4227
4228 #[test]
4231 fn outbox_queued_frames_zero_when_empty() {
4232 let client = test_client();
4233 assert_eq!(outbox_queued_frames(&client), 0);
4234 }
4235
4236 #[test]
4237 fn outbox_backpressured_when_queue_full() {
4238 let (client, _rx) = test_client_with_capacity(OUTBOX_CAPACITY);
4239 for _ in 0..OUTBOX_SOFT_QUEUE_LIMIT_FRAMES {
4241 let _ = client.tx.try_send(vec![0u8]);
4242 }
4243 assert!(outbox_backpressured(&client));
4244 }
4245
4246 #[test]
4247 fn outbox_not_backpressured_when_empty() {
4248 let client = test_client();
4249 assert!(!outbox_backpressured(&client));
4250 }
4251
4252 #[test]
4255 fn browser_pacing_fps_matches_display_fps_when_browser_ready() {
4256 let mut client = test_client();
4257 client.rtt_ms = 1.0;
4258 client.min_rtt_ms = 1.0;
4259 client.browser_backlog_frames = 0;
4260 client.browser_ack_ahead_frames = 0;
4261 client.browser_apply_ms = 0.0;
4262 client.goodput_bps = 1_000_000.0;
4263 client.delivery_bps = 1_000_000.0;
4264 client.display_fps = 144.0;
4265 assert!((browser_pacing_fps(&client) - 144.0).abs() < 0.01);
4266 }
4267
4268 #[test]
4269 fn browser_pacing_fps_drops_below_display_fps_when_backlogged() {
4270 let mut client = test_client();
4271 client.browser_backlog_frames = 20;
4272 let fps = browser_pacing_fps(&client);
4273 assert!(fps >= 1.0);
4274 assert!(fps < client.display_fps);
4275 }
4276
4277 #[test]
4280 fn effective_rtt_ms_equals_path_when_queue_is_empty() {
4281 let mut client = test_client();
4282 client.rtt_ms = 1.0;
4283 client.min_rtt_ms = 1.0;
4284 client.browser_backlog_frames = 0;
4285 client.browser_ack_ahead_frames = 0;
4286 client.browser_apply_ms = 0.0;
4287 client.goodput_bps = 1_000_000.0;
4288 client.delivery_bps = 1_000_000.0;
4289 assert!((effective_rtt_ms(&client) - 1.0).abs() < 0.01);
4290 }
4291
4292 #[test]
4293 fn effective_rtt_ms_at_least_path_rtt() {
4294 let client = test_client();
4295 assert!(effective_rtt_ms(&client) >= path_rtt_ms(&client));
4296 }
4297
4298 #[test]
4301 fn target_frame_window_at_least_two() {
4302 let client = test_client();
4303 assert!(target_frame_window(&client) >= 2);
4304 }
4305
4306 #[test]
4307 fn target_frame_window_grows_with_probe() {
4308 let mut client = test_client();
4309 let base = target_frame_window(&client);
4310 client.probe_frames = 10.0;
4311 let probed = target_frame_window(&client);
4312 assert!(probed > base, "probe_frames should grow the window");
4313 }
4314
4315 #[test]
4318 fn bandwidth_floor_bps_at_least_16k() {
4319 let mut client = test_client();
4320 client.goodput_bps = 0.0;
4321 client.delivery_bps = 0.0;
4322 assert_eq!(bandwidth_floor_bps(&client), 0.0);
4323 }
4324
4325 #[test]
4326 fn bandwidth_floor_bps_scales_with_goodput() {
4327 let mut client = test_client();
4328 client.goodput_bps = 1_000_000.0;
4329 client.delivery_bps = 1_000_000.0;
4330 let floor = bandwidth_floor_bps(&client);
4331 assert!(floor > 0.0);
4332 }
4333
4334 #[test]
4335 fn browser_ready_delivery_floor_can_drive_large_frames_to_display_fps() {
4336 let mut client = test_client();
4337 client.display_fps = 60.0;
4338 client.browser_backlog_frames = 0;
4339 client.browser_ack_ahead_frames = 0;
4340 client.browser_apply_ms = 0.2;
4341 client.goodput_bps = 3_000_000.0;
4342 client.delivery_bps = 9_500_000.0;
4343 client.last_goodput_sample_bps = 3_000_000.0;
4344 client.avg_paced_frame_bytes = 150_000.0;
4345 client.avg_preview_frame_bytes = 1_024.0;
4346 client.avg_frame_bytes = 150_000.0;
4347
4348 assert!(
4349 (pacing_fps(&client) - client.display_fps).abs() < 0.01,
4350 "browser-ready delivery floor should let large frames reach display_fps on a fast path",
4351 );
4352 }
4353
4354 #[test]
4357 fn pacing_fps_zero_when_no_bandwidth() {
4358 let mut client = test_client();
4359 client.goodput_bps = 0.0;
4360 client.delivery_bps = 0.0;
4361 client.last_goodput_sample_bps = 0.0;
4362 assert!(
4363 pacing_fps(&client) == 0.0,
4364 "pacing_fps should be 0 with zero bandwidth"
4365 );
4366 }
4367
4368 #[test]
4369 fn pacing_fps_reaches_display_fps_when_not_bandwidth_limited() {
4370 let mut client = test_client();
4371 client.rtt_ms = 1.0;
4372 client.min_rtt_ms = 1.0;
4373 client.browser_backlog_frames = 0;
4374 client.browser_ack_ahead_frames = 0;
4375 client.browser_apply_ms = 0.0;
4376 client.goodput_bps = 1_000_000.0;
4377 client.delivery_bps = 1_000_000.0;
4378 client.display_fps = 60.0;
4379 assert!((pacing_fps(&client) - 60.0).abs() < 0.01);
4380 }
4381
4382 #[test]
4385 fn throughput_limited_when_low_bandwidth() {
4386 let mut client = test_client();
4387 client.goodput_bps = 1_000.0;
4388 client.delivery_bps = 1_000.0;
4389 client.last_goodput_sample_bps = 0.0;
4390 assert!(throughput_limited(&client));
4391 }
4392
4393 #[test]
4394 fn throughput_not_limited_with_high_bandwidth() {
4395 let mut client = test_client();
4396 client.goodput_bps = 100_000_000.0;
4397 client.delivery_bps = 100_000_000.0;
4398 assert!(!throughput_limited(&client));
4399 }
4400
4401 #[test]
4404 fn browser_pacing_fps_at_least_one() {
4405 let client = test_client();
4406 assert!(browser_pacing_fps(&client) >= 1.0);
4407 }
4408
4409 #[test]
4410 fn browser_pacing_fps_reduced_by_high_backlog() {
4411 let mut client = test_client();
4412 let normal = browser_pacing_fps(&client);
4413 client.browser_backlog_frames = 20;
4414 let backlogged = browser_pacing_fps(&client);
4415 assert!(backlogged < normal, "high backlog should reduce pacing fps");
4416 }
4417
4418 #[test]
4419 fn browser_pacing_fps_reduced_by_high_ack_ahead() {
4420 let mut client = test_client();
4421 let normal = browser_pacing_fps(&client);
4422 client.browser_ack_ahead_frames = 10;
4423 let ahead = browser_pacing_fps(&client);
4424 assert!(ahead < normal, "high ack_ahead should reduce pacing fps");
4425 }
4426
4427 #[test]
4430 fn browser_backlog_blocked_over_threshold() {
4431 let mut client = test_client();
4432 client.browser_backlog_frames = 9;
4433 assert!(browser_backlog_blocked(&client));
4434 }
4435
4436 #[test]
4437 fn browser_backlog_not_blocked_under_threshold() {
4438 let mut client = test_client();
4439 client.browser_backlog_frames = 8;
4440 assert!(!browser_backlog_blocked(&client));
4441 }
4442
4443 #[test]
4446 fn byte_budget_for_at_least_one_frame() {
4447 let client = test_client();
4448 let budget = byte_budget_for(&client, 10.0);
4449 assert!(budget >= client.avg_frame_bytes.max(256.0) as usize);
4450 }
4451
4452 #[test]
4453 fn byte_budget_for_grows_with_time() {
4454 let client = test_client();
4455 let short = byte_budget_for(&client, 10.0);
4456 let long = byte_budget_for(&client, 1000.0);
4457 assert!(long >= short);
4458 }
4459
4460 #[test]
4463 fn target_byte_window_positive() {
4464 let client = test_client();
4465 assert!(target_byte_window(&client) > 0);
4466 }
4467
4468 #[test]
4469 fn target_byte_window_covers_frame_window() {
4470 let client = test_client();
4471 let byte_win = target_byte_window(&client);
4472 let frame_win = target_frame_window(&client);
4473 let min_bytes =
4474 (client.avg_paced_frame_bytes.max(256.0) * frame_win.max(2) as f32).ceil() as usize;
4475 assert!(
4476 byte_win >= min_bytes,
4477 "byte window should cover at least frame_window worth of paced frames"
4478 );
4479 }
4480
4481 #[test]
4484 fn send_interval_matches_browser_pacing() {
4485 let client = test_client();
4486 let interval = send_interval(&client);
4487 let expected = Duration::from_secs_f64(1.0 / browser_pacing_fps(&client) as f64);
4488 let diff = interval.abs_diff(expected);
4489 assert!(diff < Duration::from_micros(10));
4490 }
4491
4492 #[test]
4495 fn preview_fps_at_least_one() {
4496 let client = test_client();
4497 assert!(preview_fps(&client) >= 1.0);
4498 }
4499
4500 #[test]
4503 fn window_open_initially() {
4504 let client = test_client();
4505 assert!(window_open(&client));
4506 }
4507
4508 #[test]
4509 fn window_open_false_when_browser_blocked() {
4510 let mut client = test_client();
4511 client.browser_backlog_frames = 20;
4512 assert!(!window_open(&client));
4513 }
4514
4515 #[test]
4516 fn window_open_false_when_inflight_full() {
4517 let mut client = test_client();
4518 let target = target_frame_window(&client);
4519 fill_inflight(&mut client, target + 10, 1024);
4520 assert!(!window_open(&client));
4521 }
4522
4523 #[test]
4526 fn lead_window_open_no_reserve_same_as_window_open() {
4527 let client = test_client();
4528 assert_eq!(lead_window_open(&client, false), window_open(&client));
4529 }
4530
4531 #[test]
4532 fn lead_window_open_reserves_preview_slot() {
4533 let mut client = test_client();
4534 client.lead = Some(1);
4535 client.subscriptions.insert(1);
4536 let target = target_frame_window(&client);
4537 fill_inflight(&mut client, target.saturating_sub(1), 512);
4539 assert!(!lead_window_open(&client, true));
4542 }
4543
4544 #[test]
4547 fn can_send_frame_when_window_open_and_time_due() {
4548 let mut client = test_client();
4549 client.next_send_at = Instant::now() - Duration::from_millis(100);
4550 assert!(can_send_frame(&client, Instant::now(), false));
4551 }
4552
4553 #[test]
4554 fn can_send_frame_false_when_not_due() {
4555 let mut client = test_client();
4556 client.next_send_at = Instant::now() + Duration::from_secs(10);
4557 assert!(!can_send_frame(&client, Instant::now(), false));
4558 }
4559
4560 #[test]
4561 fn can_send_frame_false_when_window_closed() {
4562 let mut client = test_client();
4563 client.browser_backlog_frames = 20; client.next_send_at = Instant::now() - Duration::from_millis(100);
4565 assert!(!can_send_frame(&client, Instant::now(), false));
4566 }
4567
4568 #[test]
4571 fn record_send_increases_inflight() {
4572 let mut client = test_client();
4573 let now = Instant::now();
4574 assert_eq!(client.inflight_bytes, 0);
4575 assert_eq!(client.inflight_frames.len(), 0);
4576
4577 record_send(&mut client, 1000, now, true);
4578 assert_eq!(client.inflight_bytes, 1000);
4579 assert_eq!(client.inflight_frames.len(), 1);
4580
4581 record_send(&mut client, 500, now, false);
4582 assert_eq!(client.inflight_bytes, 1500);
4583 assert_eq!(client.inflight_frames.len(), 2);
4584 }
4585
4586 #[test]
4587 fn record_send_paced_advances_deadline() {
4588 let mut client = test_client();
4589 let now = Instant::now();
4590 client.next_send_at = now;
4591 record_send(&mut client, 1000, now, true);
4592 assert!(client.next_send_at > now);
4593 }
4594
4595 #[test]
4596 fn record_send_unpaced_does_not_advance_deadline() {
4597 let mut client = test_client();
4598 let now = Instant::now();
4599 let before = client.next_send_at;
4600 record_send(&mut client, 1000, now, false);
4601 assert_eq!(client.next_send_at, before);
4602 }
4603
4604 #[test]
4605 fn record_ack_decreases_inflight() {
4606 let mut client = test_client();
4607 let now = Instant::now();
4608 record_send(&mut client, 1000, now, true);
4609 record_send(&mut client, 500, now, true);
4610 assert_eq!(client.inflight_frames.len(), 2);
4611
4612 record_ack(&mut client);
4613 assert_eq!(client.inflight_frames.len(), 1);
4614 assert_eq!(client.inflight_bytes, 500);
4615 }
4616
4617 #[test]
4618 fn record_ack_on_empty_clears_bytes() {
4619 let mut client = test_client();
4620 client.inflight_bytes = 999; record_ack(&mut client);
4622 assert_eq!(client.inflight_bytes, 0);
4623 }
4624
4625 #[test]
4626 fn record_ack_updates_rtt_estimate() {
4627 let mut client = test_client();
4628 let now = Instant::now();
4629 client.inflight_frames.push_back(InFlightFrame {
4630 sent_at: now - Duration::from_millis(20),
4631 bytes: 512,
4632 paced: true,
4633 });
4634 client.inflight_bytes = 512;
4635 let old_rtt = client.rtt_ms;
4636 record_ack(&mut client);
4637 assert!(
4639 (client.rtt_ms - old_rtt).abs() > 0.01,
4640 "rtt_ms should be updated after ack"
4641 );
4642 }
4643
4644 #[test]
4645 fn record_ack_paced_updates_avg_paced_frame_bytes() {
4646 let mut client = test_client();
4647 let now = Instant::now();
4648 client.inflight_frames.push_back(InFlightFrame {
4649 sent_at: now - Duration::from_millis(10),
4650 bytes: 4096,
4651 paced: true,
4652 });
4653 client.inflight_bytes = 4096;
4654 let old_avg = client.avg_paced_frame_bytes;
4655 record_ack(&mut client);
4656 assert!(client.avg_paced_frame_bytes > old_avg);
4658 }
4659
4660 #[test]
4661 fn record_ack_unpaced_updates_avg_preview_frame_bytes() {
4662 let mut client = test_client();
4663 let now = Instant::now();
4664 client.inflight_frames.push_back(InFlightFrame {
4665 sent_at: now - Duration::from_millis(10),
4666 bytes: 8192,
4667 paced: false,
4668 });
4669 client.inflight_bytes = 8192;
4670 let old_avg = client.avg_preview_frame_bytes;
4671 record_ack(&mut client);
4672 assert!(client.avg_preview_frame_bytes > old_avg);
4673 }
4674
4675 #[test]
4678 fn pty_list_msg_empty_session() {
4679 let sess = Session::new();
4680 let msg = sess.pty_list_msg();
4681 assert_eq!(msg[0], S2C_LIST);
4682 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 0);
4683 assert_eq!(msg.len(), 3);
4684 }
4685
4686 #[test]
4687 fn pty_list_msg_includes_tags() {
4688 let _sess = Session::new();
4689 let tag1 = "shell";
4699 let tag2 = "build";
4700
4701 let mut expected = vec![S2C_LIST];
4703 expected.extend_from_slice(&2u16.to_le_bytes());
4704 expected.extend_from_slice(&1u16.to_le_bytes());
4706 expected.extend_from_slice(&(tag1.len() as u16).to_le_bytes());
4707 expected.extend_from_slice(tag1.as_bytes());
4708 expected.extend_from_slice(&3u16.to_le_bytes());
4710 expected.extend_from_slice(&(tag2.len() as u16).to_le_bytes());
4711 expected.extend_from_slice(tag2.as_bytes());
4712
4713 assert_eq!(expected[0], S2C_LIST);
4715 assert_eq!(u16::from_le_bytes([expected[1], expected[2]]), 2);
4716 let msg_str = String::from_utf8_lossy(&expected);
4718 assert!(msg_str.contains("shell"));
4719 assert!(msg_str.contains("build"));
4720 }
4721
4722 #[test]
4725 fn can_send_preview_true_when_due() {
4726 let mut client = test_client();
4727 let now = Instant::now();
4728 client
4729 .preview_next_send_at
4730 .insert(5, now - Duration::from_millis(100));
4731 assert!(can_send_preview(&client, 5, now));
4732 }
4733
4734 #[test]
4735 fn can_send_preview_false_when_not_due() {
4736 let mut client = test_client();
4737 let now = Instant::now();
4738 client
4739 .preview_next_send_at
4740 .insert(5, now + Duration::from_secs(10));
4741 assert!(!can_send_preview(&client, 5, now));
4742 }
4743
4744 #[test]
4745 fn can_send_preview_false_when_window_closed() {
4746 let mut client = test_client();
4747 client.browser_backlog_frames = 20;
4748 let now = Instant::now();
4749 assert!(!can_send_preview(&client, 5, now));
4750 }
4751
4752 #[test]
4753 fn can_send_preview_true_for_unseen_pid() {
4754 let client = test_client();
4755 let now = Instant::now();
4756 assert!(can_send_preview(&client, 99, now));
4758 }
4759
4760 #[test]
4761 fn record_preview_send_sets_future_deadline() {
4762 let mut client = test_client();
4763 let now = Instant::now();
4764 record_preview_send(&mut client, 5, now);
4765 let deadline = client.preview_next_send_at.get(&5).unwrap();
4766 assert!(*deadline > now);
4767 }
4768
4769 #[test]
4770 fn record_preview_send_successive_calls_advance() {
4771 let mut client = test_client();
4772 let now = Instant::now();
4773 record_preview_send(&mut client, 5, now);
4774 let first = *client.preview_next_send_at.get(&5).unwrap();
4775 record_preview_send(&mut client, 5, first);
4776 let second = *client.preview_next_send_at.get(&5).unwrap();
4777 assert!(second > first, "successive sends should advance deadline");
4778 }
4779
4780 fn browser_ready_high_bandwidth_client() -> ClientState {
4794 let mut c = test_client();
4795 c.display_fps = 120.0;
4796 c.rtt_ms = 1.0;
4797 c.min_rtt_ms = 1.0;
4798 c.goodput_bps = 50_000_000.0;
4799 c.delivery_bps = 50_000_000.0;
4800 c.last_goodput_sample_bps = 50_000_000.0;
4801 c.avg_paced_frame_bytes = 30_000.0;
4802 c.avg_preview_frame_bytes = 1_024.0;
4803 c.avg_frame_bytes = 30_000.0;
4804 c.browser_apply_ms = 0.3;
4805 c
4806 }
4807
4808 fn congested_client() -> ClientState {
4811 let mut c = test_client();
4812 c.display_fps = 120.0;
4813 c.rtt_ms = 500.0;
4814 c.min_rtt_ms = 40.0;
4815 c.goodput_bps = 200_000.0;
4816 c.delivery_bps = 150_000.0;
4817 c.last_goodput_sample_bps = 200_000.0;
4818 c.avg_paced_frame_bytes = 50_000.0;
4819 c.avg_preview_frame_bytes = 1_024.0;
4820 c.avg_frame_bytes = 50_000.0;
4821 c.goodput_jitter_bps = 50_000.0;
4822 c.max_goodput_jitter_bps = 200_000.0;
4823 c.browser_apply_ms = 1.0;
4824 c
4825 }
4826
4827 fn sim_ack(client: &mut ClientState, bytes: usize, rtt_ms: f32) {
4831 let sent_at = Instant::now() - Duration::from_millis(rtt_ms as u64);
4832 client.inflight_bytes += bytes;
4833 client.inflight_frames.push_back(InFlightFrame {
4834 sent_at,
4835 bytes,
4836 paced: true,
4837 });
4838 client.goodput_window_start = Instant::now() - Duration::from_millis(25);
4840 record_ack(client);
4841 }
4842
4843 fn sim_acks(client: &mut ClientState, n: usize, bytes: usize, rtt_ms: f32) {
4844 for _ in 0..n {
4845 sim_ack(client, bytes, rtt_ms);
4846 }
4847 }
4848
4849 #[test]
4852 fn browser_ready_high_bandwidth_client_uses_full_display_fps() {
4853 let client = browser_ready_high_bandwidth_client();
4854 assert!(
4855 (pacing_fps(&client) - client.display_fps).abs() < 0.01,
4856 "pacing_fps {} should equal display_fps {} when browser is ready and bandwidth is abundant",
4857 pacing_fps(&client),
4858 client.display_fps,
4859 );
4860 }
4861
4862 #[test]
4863 fn browser_ready_high_bandwidth_client_send_interval_within_one_frame() {
4864 let client = browser_ready_high_bandwidth_client();
4865 let interval_ms = send_interval(&client).as_secs_f32() * 1000.0;
4866 let frame_ms = 1000.0 / client.display_fps;
4867 assert!(
4868 interval_ms <= frame_ms + 0.1,
4869 "send_interval {interval_ms:.2}ms exceeds one frame ({frame_ms:.2}ms) when browser is ready"
4870 );
4871 }
4872
4873 #[test]
4876 fn congested_pipe_reduces_pacing_fps_substantially() {
4877 let client = congested_client();
4878 let fps = pacing_fps(&client);
4879 assert!(
4880 fps < client.display_fps * 0.5,
4881 "pacing_fps {fps:.0} should be well below display_fps {} when congested",
4882 client.display_fps,
4883 );
4884 }
4885
4886 #[test]
4887 fn congested_pipe_is_throughput_limited() {
4888 let client = congested_client();
4889 assert!(
4890 throughput_limited(&client),
4891 "congested client must be recognised as throughput-limited"
4892 );
4893 }
4894
4895 #[test]
4901 fn byte_window_bounded_near_bdp_when_congested() {
4902 let client = congested_client();
4903 let bdp = client.goodput_bps * (path_rtt_ms(&client) / 1_000.0);
4905 let window = target_byte_window(&client);
4906 assert!(
4907 window < bdp as usize * 8,
4908 "byte window {window}B is {:.1}× BDP ({bdp:.0}B); \
4909 expected ≤ 8× — lead_floor may be dominating",
4910 window as f32 / bdp.max(1.0),
4911 );
4912 }
4913
4914 #[test]
4920 fn min_rtt_not_contaminated_by_congested_rtts() {
4921 let mut client = test_client();
4922 client.display_fps = 120.0;
4923 client.rtt_ms = 40.0;
4924 client.min_rtt_ms = 40.0;
4925 client.goodput_bps = 2_000_000.0;
4926 client.delivery_bps = 2_000_000.0;
4927 client.avg_paced_frame_bytes = 30_000.0;
4928 client.avg_preview_frame_bytes = 1_024.0;
4929 let original_min = client.min_rtt_ms;
4930
4931 sim_acks(&mut client, 200, 30_000, 500.0);
4933
4934 assert!(
4935 client.min_rtt_ms < original_min * 2.0,
4936 "min_rtt drifted from {original_min}ms to {:.1}ms after 200 congested ACKs",
4937 client.min_rtt_ms,
4938 );
4939 }
4940
4941 #[test]
4944 fn delivery_bps_rises_quickly_when_congestion_clears() {
4945 let mut client = congested_client();
4946 let before = client.delivery_bps;
4947
4948 sim_acks(&mut client, 10, 30_000, 40.0);
4950
4951 assert!(
4952 client.delivery_bps > before * 2.0,
4953 "delivery_bps {:.0} should more than double from {before:.0} after 10 fast ACKs",
4954 client.delivery_bps,
4955 );
4956 }
4957
4958 #[test]
4959 fn pacing_fps_recovers_after_congestion_clears() {
4960 let mut client = congested_client();
4961
4962 for _ in 0..40 {
4968 let target = target_frame_window(&client).max(2);
4969 for _ in 0..target {
4970 let sent_at = Instant::now() - Duration::from_millis(40);
4971 client.inflight_bytes += 30_000;
4972 client.inflight_frames.push_back(InFlightFrame {
4973 sent_at,
4974 bytes: 30_000,
4975 paced: true,
4976 });
4977 }
4978 client.goodput_window_start = Instant::now() - Duration::from_millis(25);
4979 for _ in 0..target {
4980 record_ack(&mut client);
4981 }
4982 }
4983
4984 let fps = pacing_fps(&client);
4985 assert!(
4986 fps > client.display_fps * 0.7,
4987 "pacing_fps {fps:.0} didn't recover toward display_fps {} \
4988 after window-saturated rounds at low RTT",
4989 client.display_fps,
4990 );
4991 }
4992
4993 #[test]
4994 fn rtt_estimate_drops_quickly_when_congestion_clears() {
4995 let mut client = test_client();
4996 client.rtt_ms = 500.0;
4997 client.min_rtt_ms = 40.0;
4998 client.goodput_bps = 2_000_000.0;
4999 client.avg_paced_frame_bytes = 30_000.0;
5000 client.avg_preview_frame_bytes = 1_024.0;
5001
5002 sim_acks(&mut client, 10, 30_000, 40.0);
5005
5006 assert!(
5007 client.rtt_ms < 300.0,
5008 "rtt_ms {:.0}ms did not fall fast enough after congestion cleared",
5009 client.rtt_ms,
5010 );
5011 }
5012
5013 #[test]
5016 fn probe_collapses_immediately_on_queue_delay() {
5017 let mut client = test_client();
5018 client.display_fps = 120.0;
5019 client.rtt_ms = 40.0;
5020 client.min_rtt_ms = 40.0;
5021 client.goodput_bps = 5_000_000.0;
5022 client.delivery_bps = 5_000_000.0;
5023 client.last_goodput_sample_bps = 5_000_000.0;
5024 client.avg_paced_frame_bytes = 10_000.0;
5025 client.avg_preview_frame_bytes = 1_024.0;
5026 client.probe_frames = 10.0;
5027
5028 sim_acks(&mut client, 5, 10_000, 600.0);
5030
5031 assert!(
5032 client.probe_frames < 5.0,
5033 "probe_frames {:.1} should have collapsed on queue delay signal",
5034 client.probe_frames,
5035 );
5036 }
5037
5038 #[test]
5039 fn probe_grows_when_window_saturated_with_clean_rtt() {
5040 let mut client = test_client();
5041 client.display_fps = 120.0;
5042 client.rtt_ms = 40.0;
5043 client.min_rtt_ms = 40.0;
5044 client.goodput_bps = 5_000_000.0;
5045 client.delivery_bps = 5_000_000.0;
5046 client.last_goodput_sample_bps = 5_000_000.0;
5047 client.avg_paced_frame_bytes = 10_000.0;
5048 client.avg_preview_frame_bytes = 1_024.0;
5049 client.goodput_jitter_bps = 0.0;
5050 client.max_goodput_jitter_bps = 0.0;
5051 client.probe_frames = 0.0;
5052
5053 let target = target_frame_window(&client);
5055 for _ in 0..target {
5056 let sent_at = Instant::now() - Duration::from_millis(40);
5057 client.inflight_bytes += 10_000;
5058 client.inflight_frames.push_back(InFlightFrame {
5059 sent_at,
5060 bytes: 10_000,
5061 paced: true,
5062 });
5063 }
5064
5065 record_ack(&mut client);
5074
5075 assert!(
5076 client.probe_frames > 0.0,
5077 "probe_frames should grow when window-saturated with clean RTT"
5078 );
5079 }
5080
5081 #[test]
5084 fn frame_window_larger_on_high_latency_link() {
5085 let mut lo = test_client();
5086 lo.display_fps = 120.0;
5087 lo.rtt_ms = 10.0;
5088 lo.min_rtt_ms = 10.0;
5089 lo.goodput_bps = 5_000_000.0;
5090 lo.delivery_bps = 5_000_000.0;
5091 lo.avg_paced_frame_bytes = 10_000.0;
5092 lo.avg_preview_frame_bytes = 1_024.0;
5093
5094 let mut hi = test_client();
5095 hi.display_fps = 120.0;
5096 hi.rtt_ms = 200.0;
5097 hi.min_rtt_ms = 200.0;
5098 hi.goodput_bps = 5_000_000.0;
5099 hi.delivery_bps = 5_000_000.0;
5100 hi.avg_paced_frame_bytes = 10_000.0;
5101 hi.avg_preview_frame_bytes = 1_024.0;
5102
5103 let lo_win = target_frame_window(&lo);
5104 let hi_win = target_frame_window(&hi);
5105 assert!(
5106 hi_win > lo_win,
5107 "high-latency link ({hi_win}f) should need more frames in flight \
5108 than low-latency ({lo_win}f)"
5109 );
5110 }
5111
5112 #[test]
5115 fn small_frame_byte_window_enables_pipelining() {
5116 let mut client = test_client();
5121 client.display_fps = 120.0;
5122 client.rtt_ms = 165.0;
5123 client.min_rtt_ms = 8.0;
5124 client.goodput_bps = 11_000.0; client.delivery_bps = 6_800.0;
5126 client.last_goodput_sample_bps = 11_000.0;
5127 client.avg_paced_frame_bytes = 1_120.0;
5128 client.avg_preview_frame_bytes = 1_024.0;
5129 client.goodput_jitter_bps = 4_300.0;
5130 client.max_goodput_jitter_bps = 6_500.0;
5131
5132 let window = target_byte_window(&client);
5133 let frames = target_frame_window(&client);
5134 let pipeline = frames * 1_120;
5135
5136 assert!(
5137 window >= pipeline,
5138 "byte window {window}B should be >= pipeline ({frames}f × 1120B = {pipeline}B) \
5139 so small frames can pipeline across the RTT"
5140 );
5141 }
5142
5143 #[test]
5144 fn large_frame_byte_window_bounded_by_one_frame_floor() {
5145 let mut client = test_client();
5149 client.display_fps = 120.0;
5150 client.rtt_ms = 165.0;
5151 client.min_rtt_ms = 8.0;
5152 client.goodput_bps = 11_000.0;
5153 client.delivery_bps = 6_800.0;
5154 client.last_goodput_sample_bps = 11_000.0;
5155 client.avg_paced_frame_bytes = 50_000.0; client.avg_preview_frame_bytes = 1_024.0;
5157 client.goodput_jitter_bps = 0.0;
5158 client.max_goodput_jitter_bps = 0.0;
5159
5160 let window = target_byte_window(&client);
5161 let frames = target_frame_window(&client);
5162 let pipeline = frames.saturating_mul(50_000);
5163
5164 assert!(
5165 window < pipeline,
5166 "byte window {window}B should be < full pipeline {pipeline}B \
5167 ({frames}f × 50KB) — large frames must use one-frame floor"
5168 );
5169 assert!(
5170 window >= 50_000,
5171 "byte window {window}B must be at least one frame (50KB)"
5172 );
5173 }
5174
5175 #[test]
5178 fn preview_reservation_applies_even_on_low_latency_high_bandwidth_links() {
5179 let mut client = browser_ready_high_bandwidth_client();
5180 client.lead = Some(1);
5181 client.subscriptions.insert(1);
5182 let target = target_frame_window(&client);
5183 fill_inflight(&mut client, target.saturating_sub(1), 512);
5184 assert!(
5185 !lead_window_open(&client, true),
5186 "preview reservation should apply uniformly for lead clients"
5187 );
5188 }
5189
5190 #[test]
5193 fn probe_recovers_on_healthy_path_after_blip() {
5194 let mut client = browser_ready_high_bandwidth_client();
5195 client.probe_frames = 8.0;
5196
5197 sim_acks(&mut client, 3, 30_000, 200.0);
5199 let post_blip = client.probe_frames;
5200 assert!(
5201 post_blip < 4.0,
5202 "probe_frames {post_blip:.1} should have dropped after blip"
5203 );
5204
5205 client.browser_backlog_frames = 0;
5207 client.browser_ack_ahead_frames = 0;
5208 client.browser_apply_ms = 0.3;
5209
5210 sim_acks(&mut client, 20, 30_000, 1.0);
5212
5213 assert!(
5214 client.probe_frames > post_blip,
5215 "probe_frames {:.1} should have recovered from {post_blip:.1} after healthy ACKs",
5216 client.probe_frames,
5217 );
5218 }
5219
5220 #[test]
5221 fn jitter_decays_fast_on_browser_ready_path() {
5222 let mut client = browser_ready_high_bandwidth_client();
5223
5224 client.max_goodput_jitter_bps = client.goodput_bps * 0.4;
5226 client.goodput_jitter_bps = client.goodput_bps * 0.3;
5227 let initial_jitter = client.max_goodput_jitter_bps;
5228
5229 sim_acks(&mut client, 10, 30_000, 1.0);
5231
5232 assert!(
5233 client.max_goodput_jitter_bps < initial_jitter * 0.5,
5234 "max_goodput_jitter_bps {:.0} should have decayed below {:.0} \
5235 (50% of initial {initial_jitter:.0}) after 10 healthy ACKs on a ready path",
5236 client.max_goodput_jitter_bps,
5237 initial_jitter * 0.5,
5238 );
5239 }
5240
5241 #[test]
5242 fn byte_budget_uses_floor_when_goodput_depressed() {
5243 let mut client = browser_ready_high_bandwidth_client();
5244 client.goodput_bps = 100_000.0;
5245
5246 let budget = byte_budget_for(&client, 100.0);
5247 let floor_budget = (bandwidth_floor_bps(&client) * 100.0 / 1_000.0).ceil() as usize;
5248
5249 assert!(
5250 budget >= floor_budget,
5251 "byte_budget {budget} should be at least bandwidth_floor-based {floor_budget} \
5252 when goodput_bps is depressed but delivery_bps is high"
5253 );
5254 }
5255
5256 #[test]
5257 fn probe_floor_maintained_under_congestion_signal() {
5258 let mut client = test_client();
5259 client.display_fps = 120.0;
5260 client.rtt_ms = 40.0;
5261 client.min_rtt_ms = 40.0;
5262 client.goodput_bps = 5_000_000.0;
5263 client.delivery_bps = 5_000_000.0;
5264 client.last_goodput_sample_bps = 5_000_000.0;
5265 client.avg_paced_frame_bytes = 10_000.0;
5266 client.avg_preview_frame_bytes = 1_024.0;
5267 client.probe_frames = 10.0;
5268
5269 sim_acks(&mut client, 20, 10_000, 600.0);
5271
5272 assert!(
5273 client.probe_frames >= 1.0,
5274 "probe_frames {:.1} should not drop below the floor of 1.0",
5275 client.probe_frames,
5276 );
5277 }
5278
5279 #[test]
5282 fn parse_tq_da1_bare() {
5283 let results = parse_terminal_queries(b"\x1b[c", (24, 80), (0, 0));
5284 assert_eq!(results.len(), 1);
5285 assert!(results[0].starts_with("\x1b[?64;"));
5286 }
5287
5288 #[test]
5289 fn parse_tq_da1_with_zero_param() {
5290 let results = parse_terminal_queries(b"\x1b[0c", (24, 80), (0, 0));
5291 assert_eq!(results.len(), 1);
5292 assert!(results[0].starts_with("\x1b[?64;"));
5293 }
5294
5295 #[test]
5296 fn parse_tq_dsr_cursor_position() {
5297 let results = parse_terminal_queries(b"\x1b[6n", (24, 80), (5, 10));
5298 assert_eq!(results.len(), 1);
5299 assert_eq!(results[0], "\x1b[6;11R");
5300 }
5301
5302 #[test]
5303 fn parse_tq_dsr_status() {
5304 let results = parse_terminal_queries(b"\x1b[5n", (24, 80), (0, 0));
5305 assert_eq!(results.len(), 1);
5306 assert_eq!(results[0], "\x1b[0n");
5307 }
5308
5309 #[test]
5310 fn parse_tq_window_size_cells() {
5311 let results = parse_terminal_queries(b"\x1b[18t", (24, 80), (0, 0));
5312 assert_eq!(results.len(), 1);
5313 assert_eq!(results[0], "\x1b[8;24;80t");
5314 }
5315
5316 #[test]
5317 fn parse_tq_window_size_pixels() {
5318 let results = parse_terminal_queries(b"\x1b[14t", (30, 100), (0, 0));
5319 assert_eq!(results.len(), 1);
5320 assert_eq!(results[0], "\x1b[4;480;800t");
5321 }
5322
5323 #[test]
5324 fn parse_tq_multiple_queries() {
5325 let data = b"\x1b[c\x1b[6n\x1b[5n";
5326 let results = parse_terminal_queries(data, (24, 80), (2, 3));
5327 assert_eq!(results.len(), 3);
5328 assert!(results[0].starts_with("\x1b[?64;"));
5329 assert_eq!(results[1], "\x1b[3;4R");
5330 assert_eq!(results[2], "\x1b[0n");
5331 }
5332
5333 #[test]
5334 fn parse_tq_question_mark_sequences_skipped() {
5335 let results = parse_terminal_queries(b"\x1b[?1h", (24, 80), (0, 0));
5336 assert!(results.is_empty());
5337 }
5338
5339 #[test]
5340 fn parse_tq_unknown_final_byte_ignored() {
5341 let results = parse_terminal_queries(b"\x1b[42z", (24, 80), (0, 0));
5342 assert!(results.is_empty());
5343 }
5344
5345 #[test]
5346 fn parse_tq_empty_input() {
5347 let results = parse_terminal_queries(b"", (24, 80), (0, 0));
5348 assert!(results.is_empty());
5349 }
5350
5351 #[test]
5352 fn parse_tq_plain_text_no_csi() {
5353 let results = parse_terminal_queries(b"hello world", (24, 80), (0, 0));
5354 assert!(results.is_empty());
5355 }
5356
5357 #[test]
5358 fn parse_tq_interleaved_with_text() {
5359 let results = parse_terminal_queries(b"abc\x1b[cdef\x1b[6n", (24, 80), (1, 2));
5360 assert_eq!(results.len(), 2);
5361 }
5362
5363 #[test]
5366 fn search_results_empty() {
5367 let msg = build_search_results_msg(42, &[]);
5368 assert_eq!(msg[0], S2C_SEARCH_RESULTS);
5369 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 42);
5370 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 0);
5371 assert_eq!(msg.len(), 5);
5372 }
5373
5374 #[test]
5375 fn search_results_single() {
5376 let results = vec![SearchResultRow {
5377 pty_id: 7,
5378 score: 100,
5379 primary_source: 1,
5380 matched_sources: 3,
5381 context: "hello".into(),
5382 scroll_offset: Some(42),
5383 }];
5384 let msg = build_search_results_msg(1, &results);
5385 assert_eq!(msg[0], S2C_SEARCH_RESULTS);
5386 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 1);
5387 let pty_id = u16::from_le_bytes([msg[5], msg[6]]);
5388 assert_eq!(pty_id, 7);
5389 let score = u32::from_le_bytes([msg[7], msg[8], msg[9], msg[10]]);
5390 assert_eq!(score, 100);
5391 assert_eq!(msg[11], 1);
5392 assert_eq!(msg[12], 3);
5393 let scroll = u32::from_le_bytes([msg[13], msg[14], msg[15], msg[16]]);
5394 assert_eq!(scroll, 42);
5395 let ctx_len = u16::from_le_bytes([msg[17], msg[18]]) as usize;
5396 assert_eq!(ctx_len, 5);
5397 assert_eq!(&msg[19..19 + ctx_len], b"hello");
5398 }
5399
5400 #[test]
5401 fn search_results_none_scroll_offset() {
5402 let results = vec![SearchResultRow {
5403 pty_id: 1,
5404 score: 0,
5405 primary_source: 0,
5406 matched_sources: 0,
5407 context: String::new(),
5408 scroll_offset: None,
5409 }];
5410 let msg = build_search_results_msg(0, &results);
5411 let scroll = u32::from_le_bytes([msg[13], msg[14], msg[15], msg[16]]);
5412 assert_eq!(scroll, u32::MAX);
5413 }
5414
5415 #[test]
5418 fn allocate_pty_id_empty_session() {
5419 let mut sess = Session::new();
5420 assert_eq!(sess.allocate_pty_id(0), Some(1));
5421 }
5422
5423 #[test]
5424 fn allocate_pty_id_rotates() {
5425 let mut sess = Session::new();
5426 assert_eq!(sess.allocate_pty_id(0), Some(1));
5428 assert_eq!(sess.allocate_pty_id(0), Some(2));
5429 assert_eq!(sess.allocate_pty_id(0), Some(3));
5430 }
5431
5432 #[test]
5433 fn allocate_pty_id_wraps_at_max() {
5434 let mut sess = Session::new();
5435 sess.next_pty_id = u16::MAX;
5436 assert_eq!(sess.allocate_pty_id(0), Some(u16::MAX));
5437 assert_eq!(sess.allocate_pty_id(0), Some(1));
5439 }
5440
5441 #[test]
5444 fn try_send_no_change() {
5445 let mut client = test_client();
5446 let frame = sample_frame("x");
5447 let now = Instant::now();
5448 let outcome = try_send_update(&mut client, 1, frame, None, now, false);
5449 assert!(matches!(outcome, SendOutcome::NoChange));
5450 }
5451
5452 #[test]
5453 fn try_send_sent() {
5454 let (mut client, _rx) = test_client_with_capacity(8);
5455 let frame = sample_frame("x");
5456 let now = Instant::now();
5457 let outcome = try_send_update(
5458 &mut client,
5459 1,
5460 frame.clone(),
5461 Some(vec![1, 2, 3]),
5462 now,
5463 true,
5464 );
5465 assert!(matches!(outcome, SendOutcome::Sent));
5466 assert!(client.last_sent.contains_key(&1));
5467 }
5468
5469 #[test]
5470 fn try_send_backpressured() {
5471 let (mut client, _rx) = test_client_with_capacity(1);
5472 let frame = sample_frame("x");
5473 let now = Instant::now();
5474 let _ = client.tx.try_send(vec![0]);
5475 let outcome = try_send_update(
5476 &mut client,
5477 1,
5478 frame.clone(),
5479 Some(vec![1, 2, 3]),
5480 now,
5481 true,
5482 );
5483 assert!(matches!(outcome, SendOutcome::Backpressured));
5484 assert!(
5485 client.last_sent.contains_key(&1),
5486 "last_sent should advance even on backpressure"
5487 );
5488 }
5489}