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