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;
29#[cfg(target_os = "linux")]
30mod audio_pw;
31mod gpu_libs;
32mod ipc;
33mod nvenc_encode;
34mod pty;
35mod surface_encoder;
36#[cfg(target_os = "linux")]
37mod vaapi_encode;
38
39pub use ipc::{IpcListener, default_ipc_path};
40use pty::{PtyHandle, PtyWriteTarget};
41pub use surface_encoder::ChromaSubsampling;
42use surface_encoder::SurfaceEncoder;
43pub use surface_encoder::SurfaceEncoderPreference;
44pub use surface_encoder::SurfaceH264EncoderPreference;
45pub use surface_encoder::SurfaceQuality;
46
47type PtyFds = Arc<std::sync::RwLock<HashMap<u16, PtyWriteTarget>>>;
48pub struct Config {
49 pub shell: String,
50 pub shell_flags: String,
51 pub scrollback: usize,
52 pub ipc_path: String,
53 pub surface_encoders: Vec<SurfaceEncoderPreference>,
54 pub surface_quality: SurfaceQuality,
55 pub chroma: ChromaSubsampling,
56 pub vaapi_device: String,
57 #[cfg(unix)]
58 pub fd_channel: Option<std::os::unix::io::RawFd>,
59 pub verbose: bool,
60 pub max_connections: usize,
62 pub max_ptys: usize,
64 pub ping_interval: Duration,
68 pub skip_compositor: bool,
70}
71
72trait PtyDriver: Send {
73 fn size(&self) -> (u16, u16);
74 fn resize(&mut self, rows: u16, cols: u16);
75 fn process(&mut self, data: &[u8]);
76 fn title(&self) -> &str;
77 fn search_result(&self, query: &str) -> Option<PtySearchResult>;
78 fn take_title_dirty(&mut self) -> bool;
79 fn cursor_position(&self) -> (u16, u16);
80 fn synced_output(&self) -> bool;
81 fn snapshot(&mut self, echo: bool, icanon: bool) -> FrameState;
82 fn scrollback_frame(&mut self, offset: usize) -> FrameState;
83 fn reset_modes(&mut self);
84 fn mouse_event(
85 &self,
86 type_: u8,
87 button: u8,
88 col: u16,
89 row: u16,
90 echo: bool,
91 icanon: bool,
92 ) -> Option<Vec<u8>>;
93 fn get_text_range(
94 &self,
95 start_tail: u32,
96 start_col: u16,
97 end_tail: u32,
98 end_col: u16,
99 ) -> String;
100 fn total_lines(&self) -> u32;
101}
102
103struct PtySearchResult {
104 score: u32,
105 primary_source: u8,
106 matched_sources: u8,
107 context: String,
108 scroll_offset: Option<usize>,
109}
110
111impl PtyDriver for AlacrittyDriver {
112 fn size(&self) -> (u16, u16) {
113 AlacrittyDriver::size(self)
114 }
115
116 fn resize(&mut self, rows: u16, cols: u16) {
117 AlacrittyDriver::resize(self, rows, cols);
118 }
119
120 fn process(&mut self, data: &[u8]) {
121 AlacrittyDriver::process(self, data);
122 }
123
124 fn title(&self) -> &str {
125 AlacrittyDriver::title(self)
126 }
127
128 fn search_result(&self, query: &str) -> Option<PtySearchResult> {
129 AlacrittyDriver::search_result(self, query).map(|result: AlacrittySearchResult| {
130 PtySearchResult {
131 score: result.score,
132 primary_source: result.primary_source as u8,
133 matched_sources: result.matched_sources,
134 context: result.context,
135 scroll_offset: result.scroll_offset,
136 }
137 })
138 }
139
140 fn take_title_dirty(&mut self) -> bool {
141 AlacrittyDriver::take_title_dirty(self)
142 }
143
144 fn cursor_position(&self) -> (u16, u16) {
145 AlacrittyDriver::cursor_position(self)
146 }
147
148 fn synced_output(&self) -> bool {
149 AlacrittyDriver::synced_output(self)
150 }
151
152 fn snapshot(&mut self, echo: bool, icanon: bool) -> FrameState {
153 AlacrittyDriver::snapshot(self, echo, icanon)
154 }
155
156 fn scrollback_frame(&mut self, offset: usize) -> FrameState {
157 AlacrittyDriver::scrollback_frame(self, offset)
158 }
159
160 fn reset_modes(&mut self) {
161 AlacrittyDriver::reset_modes(self);
162 }
163
164 fn mouse_event(
165 &self,
166 type_: u8,
167 button: u8,
168 col: u16,
169 row: u16,
170 echo: bool,
171 icanon: bool,
172 ) -> Option<Vec<u8>> {
173 AlacrittyDriver::mouse_event(self, type_, button, col, row, echo, icanon)
174 }
175
176 fn get_text_range(
177 &self,
178 start_tail: u32,
179 start_col: u16,
180 end_tail: u32,
181 end_col: u16,
182 ) -> String {
183 AlacrittyDriver::get_text_range(self, start_tail, start_col, end_tail, end_col)
184 }
185
186 fn total_lines(&self) -> u32 {
187 AlacrittyDriver::total_lines(self)
188 }
189}
190
191const OUTBOX_SOFT_QUEUE_LIMIT_FRAMES: usize = 4;
195const OUTBOX_SOFT_QUEUE_LIMIT_BYTES: usize = 1024 * 1024;
200const PREVIEW_FRAME_RESERVE: usize = 1;
201const READY_FRAME_QUEUE_CAP: usize = 4;
202const PTY_CHANNEL_CAPACITY: usize = 64;
203const SYNC_OUTPUT_END: &[u8] = b"\x1b[?2026l";
204
205const SURFACE_BURST_FRAMES: u8 = 4;
211
212enum PtyInput {
215 Data(Vec<u8>),
218 SyncBoundary { before: Vec<u8> },
224 Eof,
226}
227
228const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024;
229
230async fn read_frame(reader: &mut (impl AsyncRead + Unpin)) -> Option<Vec<u8>> {
231 let mut len_buf = [0u8; 4];
232 reader.read_exact(&mut len_buf).await.ok()?;
233 let len = u32::from_le_bytes(len_buf) as usize;
234 if len == 0 {
235 return Some(vec![]);
236 }
237 if len > MAX_FRAME_SIZE {
238 return None;
239 }
240 let mut buf = vec![0u8; len];
241 reader.read_exact(&mut buf).await.ok()?;
242 Some(buf)
243}
244
245async fn write_frame(writer: &mut (impl AsyncWrite + Unpin), payload: &[u8]) -> bool {
246 if payload.len() > u32::MAX as usize {
247 return false;
248 }
249 let len = payload.len() as u32;
250 let mut buf = Vec::with_capacity(4 + payload.len());
251 buf.extend_from_slice(&len.to_le_bytes());
252 buf.extend_from_slice(payload);
253 writer.write_all(&buf).await.is_ok()
254}
255
256const BULK_CHUNK_BYTES: usize = 4 * 1024;
267
268async fn write_frame_interleaved(
278 writer: &mut (impl AsyncWrite + Unpin),
279 payload: &[u8],
280 audio_rx: &mut mpsc::UnboundedReceiver<Vec<u8>>,
281) -> bool {
282 if payload.len() <= BULK_CHUNK_BYTES {
284 while let Ok(audio_msg) = audio_rx.try_recv() {
285 if !write_frame(writer, &audio_msg).await {
286 return false;
287 }
288 }
289 return write_frame(writer, payload).await;
290 }
291
292 let mut offset = 0;
297 while offset < payload.len() {
298 while let Ok(audio_msg) = audio_rx.try_recv() {
299 if !write_frame(writer, &audio_msg).await {
300 return false;
301 }
302 }
303 let end = (offset + BULK_CHUNK_BYTES).min(payload.len());
304 let is_last = end == payload.len();
305 let mut frag = Vec::with_capacity(2 + (end - offset));
306 frag.push(blit_remote::S2C_FRAGMENT);
307 frag.push(if is_last {
308 blit_remote::FRAGMENT_FLAG_LAST
309 } else {
310 0
311 });
312 frag.extend_from_slice(&payload[offset..end]);
313 if !write_frame(writer, &frag).await {
314 return false;
315 }
316 offset = end;
317 }
318 true
319}
320
321struct Pty {
322 handle: PtyHandle,
323 driver: Box<dyn PtyDriver>,
324 tag: String,
326 dirty: bool,
327 ready_frames: VecDeque<FrameState>,
328 byte_rx: mpsc::Receiver<PtyInput>,
330 reader_handle: std::thread::JoinHandle<()>,
331 lflag_cache: (bool, bool),
333 lflag_last: Instant,
334 last_title_send: Instant,
336 title_pending: bool,
338 exited: bool,
340 exit_status: i32,
343 command: Option<String>,
345}
346
347impl Pty {
348 fn mark_dirty(&mut self) {
349 self.dirty = true;
350 }
351
352 fn clear_dirty(&mut self) {
353 self.dirty = false;
354 }
355}
356
357struct CachedSurfaceInfo {
358 surface_id: u16,
359 parent_id: u16,
360 width: u16,
361 height: u16,
362 title: String,
363 app_id: String,
364}
365
366struct LastPixels {
369 width: u32,
370 height: u32,
371 pixels: blit_compositor::PixelData,
372 generation: u64,
375 timestamp_ms: u32,
379}
380
381struct SharedCompositor {
382 handle: CompositorHandle,
383 surfaces: HashMap<u16, CachedSurfaceInfo>,
384 last_pixels: HashMap<u16, LastPixels>,
386 last_frame_request: HashMap<u16, Instant>,
391 created_at: Instant,
392 pixel_generation: u64,
394 last_blanket_frame_request: Instant,
398 last_configured_size: HashMap<u16, (u16, u16, u16)>,
404 #[cfg(target_os = "linux")]
407 audio_pipeline: Option<audio::AudioPipeline>,
408 #[cfg(target_os = "linux")]
414 audio_broadcast: Arc<audio::AudioBroadcast>,
415 #[cfg(target_os = "linux")]
418 audio_session_id: u16,
419 #[cfg(target_os = "linux")]
422 last_audio_restart: Option<Instant>,
423}
424
425fn encode_rgba_to_png(pixels: &[u8], width: u32, height: u32) -> Vec<u8> {
426 let mut buf = Vec::new();
427 {
428 let expected = (width as usize) * (height as usize) * 4;
429 let actual = pixels.len();
430 if actual != expected {
431 let mut encoder = png::Encoder::new(&mut buf, 1, 1);
433 encoder.set_color(png::ColorType::Rgba);
434 encoder.set_depth(png::BitDepth::Eight);
435 let mut writer = encoder.write_header().unwrap();
436 writer.write_image_data(&[255, 0, 0, 255]).unwrap();
437 eprintln!(
438 "[capture] pixel buffer size mismatch: {width}x{height} expected {expected} got {actual}"
439 );
440 } else {
441 let mut encoder = png::Encoder::new(&mut buf, width, height);
442 encoder.set_color(png::ColorType::Rgba);
443 encoder.set_depth(png::BitDepth::Eight);
444 let mut writer = encoder.write_header().unwrap();
445 writer.write_image_data(pixels).unwrap();
446 }
447 }
448 buf
449}
450
451fn encode_rgba_to_avif(pixels: &[u8], width: u32, height: u32, quality: u8) -> Vec<u8> {
453 let rgba: Vec<rgb::RGBA8> = pixels
454 .chunks_exact(4)
455 .map(|c| rgb::RGBA8::new(c[0], c[1], c[2], c[3]))
456 .collect();
457 let img = ravif::Img::new(&rgba[..], width as usize, height as usize);
458 let q = if quality == 0 { 100.0 } else { quality as f32 };
459 let encoder = ravif::Encoder::new()
460 .with_quality(q)
461 .with_alpha_quality(q)
462 .with_speed(6)
463 .with_alpha_color_mode(ravif::AlphaColorMode::UnassociatedClean)
464 .with_num_threads(None);
465 let result = encoder.encode_rgba(img).expect("AVIF encoding failed");
466 result.avif_file
467}
468
469fn encode_capture(pixels: &[u8], width: u32, height: u32, format: u8, quality: u8) -> Vec<u8> {
471 match format {
472 CAPTURE_FORMAT_AVIF => encode_rgba_to_avif(pixels, width, height, quality),
473 _ => encode_rgba_to_png(pixels, width, height),
474 }
475}
476
477async fn request_surface_capture_with_timeout(
478 command_tx: std::sync::mpsc::Sender<CompositorCommand>,
479 surface_id: u16,
480 scale_120: u16,
481 timeout: Duration,
482) -> Option<(u32, u32, Vec<u8>)> {
483 let (tx, rx) = std::sync::mpsc::sync_channel(1);
484 command_tx
485 .send(CompositorCommand::Capture {
486 surface_id,
487 scale_120,
488 reply: tx,
489 })
490 .ok()?;
491
492 tokio::task::spawn_blocking(move || rx.recv_timeout(timeout))
496 .await
497 .ok()?
498 .ok()
499 .flatten()
500}
501
502#[derive(Default)]
506struct SurfaceSubState {
507 encoder: Option<SurfaceEncoder>,
511 next_send_at: Option<Instant>,
513 burst_remaining: u8,
517 creation_in_flight: bool,
522 encode_in_flight: bool,
526 encoder_invalidated: bool,
530 last_encoded_gen: Option<u64>,
533 nal_none_streak: u32,
537 nal_none_latched_at: Option<Instant>,
541 codec_override: u8,
545 quality_override: Option<SurfaceQuality>,
547}
548
549struct ClientState {
550 tx: mpsc::UnboundedSender<Vec<u8>>,
551 outbox_queued_frames: Arc<AtomicUsize>,
552 outbox_queued_bytes: Arc<AtomicUsize>,
553 audio_tx: mpsc::UnboundedSender<Vec<u8>>,
557 lead: Option<u16>,
558 subscriptions: HashSet<u16>,
559 surface_subscriptions: HashSet<u16>,
561 audio_subscribed: bool,
563 #[cfg(target_os = "linux")]
566 audio_bitrate_kbps: u16,
567 view_sizes: HashMap<u16, (u16, u16)>,
568 scroll_offsets: HashMap<u16, usize>,
569 scroll_caches: HashMap<u16, FrameState>,
570 last_sent: HashMap<u16, FrameState>,
571 preview_next_send_at: HashMap<u16, Instant>,
572 rtt_ms: f32,
574 min_rtt_ms: f32,
576 display_fps: f32,
578 delivery_bps: f32,
580 goodput_bps: f32,
582 goodput_jitter_bps: f32,
584 max_goodput_jitter_bps: f32,
586 last_goodput_sample_bps: f32,
588 avg_frame_bytes: f32,
590 avg_paced_frame_bytes: f32,
592 avg_preview_frame_bytes: f32,
594 avg_surface_frame_bytes: f32,
599 inflight_bytes: usize,
601 inflight_frames: VecDeque<InFlightFrame>,
603 next_send_at: Instant,
605 probe_frames: f32,
608 frames_sent: u32,
610 acks_recv: u32,
611 acked_bytes_since_log: usize,
612 browser_backlog_frames: u16,
613 browser_ack_ahead_frames: u16,
614 browser_apply_ms: f32,
615 last_metrics_update: Instant,
616 last_log: Instant,
617 last_window_blocked_log: Instant,
619 last_skip_log: Instant,
621 skip_same_gen_count: u32,
623 skip_in_flight_count: u32,
624 skip_pacing_count: u32,
625 skip_vulkan_await_count: u32,
626 skip_no_subs_count: u32,
628 skip_not_subbed_count: u32,
630 skip_last_pixels_mismatch_count: u32,
632 encode_loop_iters: u32,
634 goodput_window_bytes: usize,
635 goodput_window_start: Instant,
636 surface_subs: HashMap<u16, SurfaceSubState>,
642 surface_needs_keyframe: bool,
643 vulkan_video_surfaces: HashMap<u16, (&'static str, u8)>,
646 surface_inflight_frames: VecDeque<InFlightFrame>,
650 surface_view_sizes: HashMap<u16, (u16, u16, u16)>,
655 surface_codec_support: u8,
658 pressed_surface_keys: HashSet<u32>,
662}
663
664struct InFlightFrame {
665 sent_at: Instant,
666 bytes: usize,
667 paced: bool,
668}
669
670fn frame_window(rtt_ms: f32, display_fps: f32) -> usize {
674 let frame_ms = 1_000.0 / display_fps.max(1.0);
675 let base_frames = (rtt_ms / frame_ms).ceil().max(0.0) as usize;
676 let slack_frames = ((base_frames as f32) * 0.125).ceil() as usize + 2;
677 base_frames.saturating_add(slack_frames).max(2)
678}
679
680fn path_rtt_ms(client: &ClientState) -> f32 {
681 if client.min_rtt_ms > 0.0 {
682 client.min_rtt_ms
683 } else {
684 client.rtt_ms
685 }
686}
687
688fn display_need_bps(client: &ClientState) -> f32 {
689 client.avg_paced_frame_bytes.max(256.0) * client.display_fps.max(1.0)
690}
691
692fn effective_rtt_ms(client: &ClientState) -> f32 {
693 let path_rtt = path_rtt_ms(client);
694 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
695 let queue_allowance = frame_ms
696 * if throughput_limited(client) {
697 4.0
698 } else {
699 12.0
700 };
701 client.rtt_ms.clamp(path_rtt, path_rtt + queue_allowance)
702}
703
704fn window_rtt_ms(client: &ClientState) -> f32 {
705 let effective = effective_rtt_ms(client);
706 if !throughput_limited(client) {
707 effective
708 } else {
709 client.rtt_ms.clamp(effective, effective * 2.0)
710 }
711}
712
713fn target_frame_window(client: &ClientState) -> usize {
714 let window_fps = if throughput_limited(client) {
715 pacing_fps(client)
716 } else {
717 browser_pacing_fps(client)
718 };
719 frame_window(window_rtt_ms(client), window_fps)
720 .saturating_add(client.probe_frames.round().max(0.0) as usize)
721}
722
723fn base_queue_ms(client: &ClientState) -> f32 {
724 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
725 frame_ms * if throughput_limited(client) { 2.0 } else { 8.0 }
726}
727
728fn target_queue_ms(client: &ClientState) -> f32 {
729 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
730 let probe_scale = if throughput_limited(client) {
731 0.25
732 } else {
733 1.0
734 };
735 base_queue_ms(client) + client.probe_frames.max(0.0) * frame_ms * probe_scale
736}
737
738fn browser_ready(client: &ClientState) -> bool {
739 client.browser_ack_ahead_frames <= 1
740 && client.browser_apply_ms <= 1.0
741 && !outbox_backpressured(client)
742}
743
744fn bandwidth_floor_bps(client: &ClientState) -> f32 {
745 let browser_ready = browser_ready(client);
746 let backlog_scale = match client.browser_backlog_frames {
747 0..=2 => 0.9,
748 3..=8 => 0.8,
749 _ => 0.65,
750 };
751 let penalty = client
752 .goodput_jitter_bps
753 .max(client.max_goodput_jitter_bps * 0.5)
754 .min(client.goodput_bps * if browser_ready { 0.75 } else { 0.9 });
755 let goodput_floor = (client.goodput_bps - penalty)
756 .max(client.goodput_bps * if browser_ready { 0.35 } else { 0.2 });
757 let delivery_floor = client.delivery_bps * if browser_ready { 1.0 } else { 0.5 };
761 let recent_sample_floor = if browser_ready && client.last_goodput_sample_bps > 0.0 {
762 client.last_goodput_sample_bps * backlog_scale
763 } else {
764 0.0
765 };
766 goodput_floor.max(recent_sample_floor).max(delivery_floor)
767}
768
769fn pacing_fps(client: &ClientState) -> f32 {
770 let frame_bytes = client.avg_paced_frame_bytes.max(256.0);
771 let sustainable = bandwidth_floor_bps(client) / frame_bytes;
772 sustainable.min(browser_pacing_fps(client))
773}
774
775fn throughput_limited(client: &ClientState) -> bool {
776 let floor = bandwidth_floor_bps(client);
777 let lead_bps = client.avg_paced_frame_bytes.max(256.0) * browser_pacing_fps(client);
781 let preview_bps = client.avg_preview_frame_bytes.max(256.0) * client.display_fps.max(1.0);
782 (lead_bps + preview_bps) > floor * 0.9
783}
784
785fn browser_pacing_fps(client: &ClientState) -> f32 {
786 let mut fps = client.display_fps.max(1.0);
787
788 let backlog = client.browser_backlog_frames as f32;
803 if backlog > 4.0 {
804 fps = fps.min(fps * (2.0 / backlog));
805 }
806
807 if client.browser_ack_ahead_frames > 4 {
808 fps = fps.min(client.display_fps.max(1.0) * 0.5);
809 }
810 if client.browser_ack_ahead_frames > 8 {
811 fps = fps.min(client.display_fps.max(1.0) * 0.25);
812 }
813
814 fps.max(1.0)
815}
816
817fn browser_backlog_blocked(client: &ClientState) -> bool {
818 client.browser_backlog_frames > 8
819}
820
821fn byte_budget_for(client: &ClientState, budget_ms: f32) -> usize {
822 let budget_bps = if throughput_limited(client) {
823 bandwidth_floor_bps(client)
824 } else {
825 client.goodput_bps.max(bandwidth_floor_bps(client))
826 };
827 let bytes = budget_bps * budget_ms.max(1.0) / 1_000.0;
828 bytes.ceil().max(client.avg_frame_bytes.max(256.0)) as usize
829}
830
831fn target_byte_window(client: &ClientState) -> usize {
832 let budget = byte_budget_for(client, path_rtt_ms(client) + target_queue_ms(client));
833 let frame_bytes = client.avg_paced_frame_bytes.max(256.0).ceil() as usize;
834 let target_frames = target_frame_window(client);
835 let pipeline_bytes = frame_bytes.saturating_mul(target_frames);
836 const PIPELINE_FLOOR_LIMIT: usize = 32_768; let floor = if pipeline_bytes <= PIPELINE_FLOOR_LIMIT {
843 pipeline_bytes
844 } else {
845 frame_bytes };
847 budget.max(floor)
848}
849
850fn send_interval(client: &ClientState) -> Duration {
851 Duration::from_secs_f64(1.0 / browser_pacing_fps(client).max(1.0) as f64)
852}
853
854fn preview_fps(client: &ClientState) -> f32 {
855 let mut fps = client.display_fps.max(1.0);
856 if client.lead.is_some() && throughput_limited(client) {
857 let avail = bandwidth_floor_bps(client);
862 let lead_bps = client.avg_paced_frame_bytes.max(256.0) * browser_pacing_fps(client);
863 let preview_budget = (avail - lead_bps).max(avail * 0.25).max(0.0);
864 let bw_cap = preview_budget / client.avg_preview_frame_bytes.max(256.0);
865 fps = fps.min(bw_cap.max(1.0));
866 }
867 fps.max(1.0)
868}
869
870fn preview_send_interval(client: &ClientState) -> Duration {
871 Duration::from_secs_f64(1.0 / preview_fps(client) as f64)
872}
873
874fn surface_pacing_fps(client: &ClientState) -> f32 {
879 browser_pacing_fps(client)
880}
881
882fn surface_send_interval(client: &ClientState) -> Duration {
883 Duration::from_secs_f64(1.0 / surface_pacing_fps(client).max(1.0) as f64)
884}
885
886fn maybe_log_pacing_metrics(sess: &mut Session, client_id: u64, verbose: bool) {
890 let Some(c) = sess.clients.get_mut(&client_id) else {
891 return;
892 };
893 if c.last_log.elapsed().as_secs_f32() < 10.0 {
894 return;
895 }
896 let log_elapsed = c.last_log.elapsed().as_secs_f32().max(1.0e-3);
897 let paced_fps = pacing_fps(c);
898 let display_need_bps_v = display_need_bps(c);
899 let surface_fps = surface_pacing_fps(c);
900 let frames_sent = c.frames_sent;
901 let acks_recv = c.acks_recv;
902 let rtt_ms = c.rtt_ms;
903 let min_rtt_ms = path_rtt_ms(c);
904 let eff_rtt_ms = window_rtt_ms(c);
905 let inflight_bytes = c.inflight_bytes;
906 let delivery_bps = c.delivery_bps;
907 let goodput_ewma_bps = c.goodput_bps;
908 let goodput_jitter_bps = c.goodput_jitter_bps;
909 let max_goodput_jitter_bps = c.max_goodput_jitter_bps;
910 let avg_frame_bytes = c.avg_frame_bytes;
911 let avg_paced_frame_bytes = c.avg_paced_frame_bytes;
912 let avg_preview_frame_bytes = c.avg_preview_frame_bytes;
913 let display_fps = c.display_fps;
914 let probe_frames = c.probe_frames;
915 let goodput_bps = c.acked_bytes_since_log as f32 / log_elapsed;
916 let window_frames = target_frame_window(c);
917 let window_bytes = target_byte_window(c);
918 let outbox_frames = outbox_queued_frames(c);
919 let browser_backlog_frames = c.browser_backlog_frames;
920 let browser_ack_ahead_frames = c.browser_ack_ahead_frames;
921 let browser_apply_ms = c.browser_apply_ms;
922 let avg_surface_frame_bytes = c.avg_surface_frame_bytes;
923 let skip_same_gen = c.skip_same_gen_count;
924 let skip_in_flight = c.skip_in_flight_count;
925 let skip_pacing = c.skip_pacing_count;
926 let skip_vk_await = c.skip_vulkan_await_count;
927 let skip_no_subs = c.skip_no_subs_count;
928 let skip_not_subbed = c.skip_not_subbed_count;
929 let skip_mismatch = c.skip_last_pixels_mismatch_count;
930 let loop_iters = c.encode_loop_iters;
931 let own_subs: usize = c.surface_subscriptions.len();
932 let vk_surfs = c.vulkan_video_surfaces.len();
933 let in_flight_set_len = c
934 .surface_subs
935 .values()
936 .filter(|s| s.encode_in_flight)
937 .count();
938 let surface_burst: u8 = c
939 .surface_subs
940 .values()
941 .map(|s| s.burst_remaining)
942 .max()
943 .unwrap_or(0);
944
945 c.frames_sent = 0;
946 c.acks_recv = 0;
947 c.acked_bytes_since_log = 0;
948 c.skip_same_gen_count = 0;
949 c.skip_in_flight_count = 0;
950 c.skip_pacing_count = 0;
951 c.skip_vulkan_await_count = 0;
952 c.skip_no_subs_count = 0;
953 c.skip_not_subbed_count = 0;
954 c.skip_last_pixels_mismatch_count = 0;
955 c.encode_loop_iters = 0;
956 c.last_log = Instant::now();
957
958 if verbose {
959 let surf_info = sess.compositor.as_ref().map(|cs| {
960 let surfaces = cs.surfaces.len();
961 let pending = 0usize;
962 let subs: usize = sess
963 .clients
964 .values()
965 .map(|c| c.surface_subscriptions.len())
966 .sum();
967 (surfaces, pending, subs)
968 });
969 let (surf_count, surf_pending, surf_subs) = surf_info.unwrap_or((0, 0, 0));
970 eprintln!(
971 "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_v:.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} own_subs={own_subs} pending_req={surf_pending} commits={} encodes={} enc_bytes={} surf_sent={} px_empty_ticks={} px_snap_len={} loop_iters={loop_iters} skip_same_gen={skip_same_gen} skip_in_flight={skip_in_flight} skip_pacing={skip_pacing} skip_vk_await={skip_vk_await} skip_no_subs={skip_no_subs} skip_not_subbed={skip_not_subbed} skip_mismatch={skip_mismatch} vk_surfs={vk_surfs} enc_in_flight_set={in_flight_set_len} burst={surface_burst}",
972 sess.tick_fires,
973 sess.tick_snaps,
974 sess.surface_commits,
975 sess.surface_encodes,
976 sess.surface_encode_bytes,
977 sess.surface_frames_sent,
978 sess.ticks_pixel_snapshot_empty,
979 sess.pixel_snapshot_len,
980 );
981 }
982 sess.tick_fires = 0;
983 sess.tick_snaps = 0;
984 sess.surface_commits = 0;
985 sess.surface_encodes = 0;
986 sess.surface_encode_bytes = 0;
987 sess.surface_frames_sent = 0;
988 sess.ticks_pixel_snapshot_empty = 0;
989}
990
991fn advance_deadline(deadline: &mut Instant, now: Instant, interval: Duration) {
992 let scheduled = deadline.checked_add(interval).unwrap_or(now + interval);
993 *deadline = if scheduled + interval < now {
994 now + interval
995 } else {
996 scheduled
997 };
998}
999
1000fn should_snapshot_pty(dirty: bool, needful: bool, synced_output: bool) -> bool {
1001 dirty && needful && !synced_output
1002}
1003
1004fn enqueue_ready_frame(queue: &mut VecDeque<FrameState>, frame: FrameState) -> bool {
1005 if queue.len() >= READY_FRAME_QUEUE_CAP {
1006 return false;
1007 }
1008 queue.push_back(frame);
1009 true
1010}
1011
1012fn pty_has_visual_update(pty: &Pty) -> bool {
1013 pty.dirty || !pty.ready_frames.is_empty() || !pty.byte_rx.is_empty()
1014}
1015
1016fn find_sync_output_end(prefix: &[u8], bytes: &[u8]) -> Option<usize> {
1020 if bytes.is_empty() {
1021 return None;
1022 }
1023 let needle = SYNC_OUTPUT_END;
1024 let nlen = needle.len();
1025
1026 if !prefix.is_empty() {
1028 let tail = if prefix.len() >= nlen - 1 {
1029 &prefix[prefix.len() - (nlen - 1)..]
1030 } else {
1031 prefix
1032 };
1033 let combined_len = tail.len() + bytes.len().min(nlen);
1034 if combined_len >= nlen {
1035 let mut buf = [0u8; 32]; let blen = combined_len.min(buf.len());
1038 let tlen = tail.len().min(blen);
1039 buf[..tlen].copy_from_slice(&tail[..tlen]);
1040 let rest = (blen - tlen).min(bytes.len());
1041 buf[tlen..tlen + rest].copy_from_slice(&bytes[..rest]);
1042 for i in 0..=(blen.saturating_sub(nlen)) {
1043 if &buf[i..i + nlen] == needle {
1044 let end_in_bytes = (i + nlen).saturating_sub(tail.len());
1045 if end_in_bytes > 0 && end_in_bytes <= bytes.len() {
1046 return Some(end_in_bytes);
1047 }
1048 }
1049 }
1050 }
1051 }
1052
1053 let mut offset = 0;
1055 while let Some(pos) = memchr::memchr(0x1b, &bytes[offset..]) {
1056 let abs = offset + pos;
1057 if abs + nlen <= bytes.len() && &bytes[abs..abs + nlen] == needle {
1058 return Some(abs + nlen);
1059 }
1060 offset = abs + 1;
1061 }
1062 None
1063}
1064
1065fn update_sync_scan_tail(tail: &mut Vec<u8>, bytes: &[u8]) {
1066 if bytes.is_empty() {
1067 return;
1068 }
1069 tail.extend_from_slice(bytes);
1070 let keep = SYNC_OUTPUT_END.len().saturating_sub(1);
1071 if tail.len() > keep {
1072 let drop = tail.len() - keep;
1073 tail.drain(..drop);
1074 }
1075}
1076
1077fn preview_deadline(client: &ClientState, pid: u16, now: Instant) -> Instant {
1078 client
1079 .preview_next_send_at
1080 .get(&pid)
1081 .copied()
1082 .unwrap_or(now)
1083}
1084
1085fn client_has_due_preview(sess: &Session, client: &ClientState, now: Instant) -> bool {
1086 if client.lead.is_none() {
1087 return false;
1088 }
1089 client.subscriptions.iter().copied().any(|pid| {
1090 Some(pid) != client.lead
1091 && preview_deadline(client, pid, now) <= now
1092 && sess
1093 .ptys
1094 .get(&pid)
1095 .map(pty_has_visual_update)
1096 .unwrap_or(false)
1097 })
1098}
1099
1100fn outbox_queued_frames(client: &ClientState) -> usize {
1101 client.outbox_queued_frames.load(Ordering::Relaxed)
1102}
1103
1104fn outbox_queued_bytes(client: &ClientState) -> usize {
1105 client.outbox_queued_bytes.load(Ordering::Relaxed)
1106}
1107
1108fn outbox_backpressured(client: &ClientState) -> bool {
1109 let frames = outbox_queued_frames(client);
1117 if frames >= OUTBOX_SOFT_QUEUE_LIMIT_FRAMES {
1118 return true;
1119 }
1120 frames >= 2 && outbox_queued_bytes(client) >= OUTBOX_SOFT_QUEUE_LIMIT_BYTES
1121}
1122
1123fn mark_outbox_drained(
1124 queued_frames: &Arc<AtomicUsize>,
1125 queued_bytes: &Arc<AtomicUsize>,
1126 bytes: usize,
1127) {
1128 let _ = queued_frames.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
1129 Some(value.saturating_sub(1))
1130 });
1131 let _ = queued_bytes.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
1132 Some(value.saturating_sub(bytes))
1133 });
1134}
1135
1136fn send_outbox_tracked(
1137 tx: &mpsc::UnboundedSender<Vec<u8>>,
1138 queued_frames: &Arc<AtomicUsize>,
1139 queued_bytes: &Arc<AtomicUsize>,
1140 msg: Vec<u8>,
1141) -> Result<(), mpsc::error::SendError<Vec<u8>>> {
1142 let bytes = msg.len();
1143 tx.send(msg)?;
1144 queued_frames.fetch_add(1, Ordering::Relaxed);
1145 queued_bytes.fetch_add(bytes, Ordering::Relaxed);
1146 Ok(())
1147}
1148
1149fn send_outbox(client: &ClientState, msg: Vec<u8>) -> Result<(), mpsc::error::SendError<Vec<u8>>> {
1150 send_outbox_tracked(
1151 &client.tx,
1152 &client.outbox_queued_frames,
1153 &client.outbox_queued_bytes,
1154 msg,
1155 )
1156}
1157
1158fn can_send_preview(client: &ClientState, pid: u16, now: Instant) -> bool {
1159 window_open(client) && now >= preview_deadline(client, pid, now)
1160}
1161
1162fn record_preview_send(client: &mut ClientState, pid: u16, now: Instant) {
1163 let mut deadline = client
1164 .preview_next_send_at
1165 .get(&pid)
1166 .copied()
1167 .unwrap_or(now);
1168 advance_deadline(&mut deadline, now, preview_send_interval(client));
1169 client.preview_next_send_at.insert(pid, deadline);
1170}
1171
1172fn window_open(client: &ClientState) -> bool {
1173 !browser_backlog_blocked(client)
1174 && !outbox_backpressured(client)
1175 && client.inflight_frames.len() < target_frame_window(client)
1176 && client.inflight_bytes < target_byte_window(client)
1177}
1178
1179fn surface_window_open(client: &ClientState) -> bool {
1183 !outbox_backpressured(client)
1184}
1185
1186fn lead_window_open(client: &ClientState, reserve_preview_slot: bool) -> bool {
1187 if !reserve_preview_slot || client.lead.is_none() {
1188 return window_open(client);
1189 }
1190 if browser_backlog_blocked(client) || outbox_backpressured(client) {
1191 return false;
1192 }
1193 let target_frames = target_frame_window(client);
1194 let reserve_frames = PREVIEW_FRAME_RESERVE.min(target_frames.saturating_sub(1));
1195 let frame_limit = target_frames.saturating_sub(reserve_frames).max(1);
1196 let reserve_bytes = client.avg_preview_frame_bytes.max(256.0).ceil() as usize;
1197 let byte_limit = target_byte_window(client)
1198 .saturating_sub(reserve_bytes)
1199 .max(client.avg_paced_frame_bytes.max(256.0).ceil() as usize);
1200 client.inflight_frames.len() < frame_limit && client.inflight_bytes < byte_limit
1201}
1202
1203fn can_send_frame(client: &ClientState, now: Instant, reserve_preview_slot: bool) -> bool {
1204 lead_window_open(client, reserve_preview_slot) && now >= client.next_send_at
1205}
1206
1207fn record_send(client: &mut ClientState, bytes: usize, now: Instant, paced: bool) {
1208 client.inflight_bytes += bytes;
1209 client.inflight_frames.push_back(InFlightFrame {
1210 sent_at: now,
1211 bytes,
1212 paced,
1213 });
1214 if paced {
1215 let interval = send_interval(client);
1216 advance_deadline(&mut client.next_send_at, now, interval);
1217 }
1218}
1219
1220fn ewma_with_direction(old: f32, sample: f32, rise_alpha: f32, fall_alpha: f32) -> f32 {
1221 let alpha = if sample > old { rise_alpha } else { fall_alpha };
1222 old * (1.0 - alpha) + sample * alpha
1223}
1224
1225fn window_saturated(client: &ClientState, inflight_frames: usize, inflight_bytes: usize) -> bool {
1226 let target_frames = target_frame_window(client);
1227 let target_bytes = target_byte_window(client);
1228 inflight_frames.saturating_mul(10) >= target_frames.saturating_mul(9)
1229 || inflight_bytes.saturating_mul(10) >= target_bytes.saturating_mul(9)
1230}
1231
1232fn record_ack(client: &mut ClientState) {
1233 if let Some(frame) = client.inflight_frames.pop_front() {
1234 let prev_inflight_frames = client.inflight_frames.len() + 1;
1235 let prev_inflight_bytes = client.inflight_bytes;
1236 client.inflight_bytes = client.inflight_bytes.saturating_sub(frame.bytes);
1237 client.acked_bytes_since_log = client.acked_bytes_since_log.saturating_add(frame.bytes);
1238 let sample_ms = frame.sent_at.elapsed().as_secs_f32() * 1_000.0;
1239 client.rtt_ms = ewma_with_direction(client.rtt_ms, sample_ms, 0.125, 0.25);
1240 if client.min_rtt_ms > 0.0 {
1241 client.min_rtt_ms = client.min_rtt_ms.min(sample_ms);
1244 } else {
1245 client.min_rtt_ms = sample_ms;
1246 }
1247 client.min_rtt_ms = client.min_rtt_ms.max(0.5);
1248 let sample_bps = frame.bytes as f32 / sample_ms.max(1.0e-3) * 1_000.0;
1249 client.delivery_bps = ewma_with_direction(client.delivery_bps, sample_bps, 0.5, 0.125);
1250 client.avg_frame_bytes =
1251 ewma_with_direction(client.avg_frame_bytes, frame.bytes as f32, 0.5, 0.125);
1252 if frame.paced {
1253 client.avg_paced_frame_bytes =
1254 ewma_with_direction(client.avg_paced_frame_bytes, frame.bytes as f32, 0.5, 0.125);
1255 } else {
1256 client.avg_preview_frame_bytes = ewma_with_direction(
1257 client.avg_preview_frame_bytes,
1258 frame.bytes as f32,
1259 0.5,
1260 0.125,
1261 );
1262 }
1263 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
1264 let path_rtt = path_rtt_ms(client);
1265 let likely_window_limited =
1266 window_saturated(client, prev_inflight_frames, prev_inflight_bytes);
1267 client.goodput_window_bytes = client.goodput_window_bytes.saturating_add(frame.bytes);
1268 let now = Instant::now();
1269 let goodput_elapsed = now
1270 .duration_since(client.goodput_window_start)
1271 .as_secs_f32();
1272 if goodput_elapsed >= 0.02 {
1273 let sample_goodput = client.goodput_window_bytes as f32 / goodput_elapsed.max(1.0e-3);
1274 if likely_window_limited || client.browser_backlog_frames > 0 {
1275 let prev_goodput_sample = if client.last_goodput_sample_bps > 0.0 {
1276 client.last_goodput_sample_bps
1277 } else {
1278 sample_goodput
1279 };
1280 let jitter_sample = (sample_goodput - prev_goodput_sample).abs();
1281 client.goodput_bps =
1282 ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, 0.125);
1283 let min_reliable = (client.avg_paced_frame_bytes.max(256.0) * 2.0) as usize;
1289 if client.goodput_window_bytes >= min_reliable {
1290 client.goodput_jitter_bps =
1291 ewma_with_direction(client.goodput_jitter_bps, jitter_sample, 0.5, 0.125);
1292 let jitter_decay = if browser_ready(client) && sample_ms < path_rtt * 3.0 {
1293 0.90
1294 } else {
1295 0.98
1296 };
1297 client.max_goodput_jitter_bps =
1298 (client.max_goodput_jitter_bps * jitter_decay).max(jitter_sample);
1299 client.max_goodput_jitter_bps =
1303 client.max_goodput_jitter_bps.min(client.goodput_bps * 0.45);
1304 } else {
1305 client.goodput_jitter_bps *= 0.9;
1307 client.max_goodput_jitter_bps *= 0.95;
1308 }
1309 client.last_goodput_sample_bps =
1313 (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
1314 } else {
1315 let ratio = client.goodput_bps / sample_goodput.max(1.0);
1320 let fall_alpha = if ratio > 10.0 {
1321 0.5
1322 } else if ratio > 3.0 {
1323 0.25
1324 } else {
1325 0.03
1326 };
1327 client.goodput_bps =
1328 ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, fall_alpha);
1329 client.goodput_jitter_bps *= 0.5;
1330 client.max_goodput_jitter_bps *= 0.9;
1331 client.last_goodput_sample_bps =
1332 (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
1333 }
1334 client.goodput_window_bytes = 0;
1335 client.goodput_window_start = now;
1336 }
1337 let queue_baseline_ms = if throughput_limited(client) {
1338 window_rtt_ms(client)
1339 } else {
1340 path_rtt
1341 };
1342 let queue_delay_ms = (sample_ms - queue_baseline_ms).max(0.0);
1343 let max_probe_frames = (browser_pacing_fps(client) * 0.125).max(4.0);
1344 let jitter_ratio = client.max_goodput_jitter_bps / client.goodput_bps.max(1.0);
1345 let low_delay_frames = if throughput_limited(client) { 2.0 } else { 8.0 };
1346 let high_delay_frames = if throughput_limited(client) {
1347 4.0
1348 } else {
1349 12.0
1350 };
1351 if likely_window_limited
1352 && queue_delay_ms <= frame_ms * low_delay_frames
1353 && jitter_ratio < 0.25
1354 {
1355 client.probe_frames = (client.probe_frames + 1.0).min(max_probe_frames);
1356 } else if !likely_window_limited
1357 && browser_ready(client)
1358 && queue_delay_ms <= frame_ms * 2.0
1359 && jitter_ratio < 0.25
1360 {
1361 client.probe_frames = (client.probe_frames + 0.25).min(max_probe_frames * 0.5);
1362 } else if queue_delay_ms > frame_ms * high_delay_frames || jitter_ratio > 0.5 {
1363 client.probe_frames = (client.probe_frames * 0.5).max(1.0);
1364 } else if queue_delay_ms > frame_ms * 2.0 || !browser_ready(client) {
1365 client.probe_frames = (client.probe_frames - 0.5).max(0.0);
1366 }
1367 } else {
1368 client.inflight_bytes = 0;
1369 }
1370}
1371
1372fn record_surface_ack(client: &mut ClientState) {
1379 if let Some(frame) = client.surface_inflight_frames.pop_front() {
1380 client.acked_bytes_since_log = client.acked_bytes_since_log.saturating_add(frame.bytes);
1381
1382 let sample_ms = frame.sent_at.elapsed().as_secs_f32() * 1_000.0;
1383
1384 let sample_bps = frame.bytes as f32 / sample_ms.max(1.0e-3) * 1_000.0;
1386 client.delivery_bps = ewma_with_direction(client.delivery_bps, sample_bps, 0.5, 0.125);
1387
1388 client.goodput_window_bytes = client.goodput_window_bytes.saturating_add(frame.bytes);
1394 let now = Instant::now();
1395 let goodput_elapsed = now
1396 .duration_since(client.goodput_window_start)
1397 .as_secs_f32();
1398 if goodput_elapsed >= 0.02 {
1399 let sample_goodput = client.goodput_window_bytes as f32 / goodput_elapsed.max(1.0e-3);
1400 client.goodput_bps =
1401 ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, 0.125);
1402 client.last_goodput_sample_bps =
1403 (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
1404 client.goodput_window_bytes = 0;
1405 client.goodput_window_start = now;
1406 }
1407 }
1408}
1409
1410fn reset_inflight(client: &mut ClientState) {
1411 client.inflight_bytes = 0;
1412 client.inflight_frames.clear();
1413 client.next_send_at = Instant::now();
1414 client.browser_backlog_frames = 0;
1415 client.browser_ack_ahead_frames = 0;
1416}
1417
1418fn is_unset_view_size(rows: u16, cols: u16) -> bool {
1419 rows == 0 && cols == 0
1420}
1421
1422fn subscribe_client_to(client: &mut ClientState, pty_id: u16) {
1423 if client.subscriptions.insert(pty_id) {
1424 client.last_sent.remove(&pty_id);
1425 client.preview_next_send_at.remove(&pty_id);
1426 }
1427}
1428
1429fn unsubscribe_client_from(client: &mut ClientState, pty_id: u16) -> bool {
1430 let removed_sub = client.subscriptions.remove(&pty_id);
1431 client.last_sent.remove(&pty_id);
1432 client.preview_next_send_at.remove(&pty_id);
1433 client.scroll_offsets.remove(&pty_id);
1434 client.scroll_caches.remove(&pty_id);
1435 let removed_view = client.view_sizes.remove(&pty_id).is_some();
1436 if client.lead == Some(pty_id) {
1437 client.lead = None;
1438 }
1439 removed_sub || removed_view
1440}
1441
1442fn update_client_scroll_state(client: &mut ClientState, pty_id: u16, next_offset: usize) -> bool {
1443 let prev_offset = client.scroll_offsets.get(&pty_id).copied().unwrap_or(0);
1444 if prev_offset == next_offset {
1445 return false;
1446 }
1447
1448 if prev_offset == 0 && next_offset > 0 {
1449 client.scroll_caches.insert(
1450 pty_id,
1451 client.last_sent.get(&pty_id).cloned().unwrap_or_default(),
1452 );
1453 } else if prev_offset > 0
1454 && next_offset == 0
1455 && let Some(cache) = client.scroll_caches.remove(&pty_id)
1456 {
1457 if cache.rows() > 0 && cache.cols() > 0 {
1458 client.last_sent.insert(pty_id, cache);
1459 } else {
1460 client.last_sent.remove(&pty_id);
1461 }
1462 }
1463
1464 if next_offset > 0 {
1465 client.scroll_offsets.insert(pty_id, next_offset);
1466 } else {
1467 client.scroll_offsets.remove(&pty_id);
1468 }
1469 reset_inflight(client);
1470 true
1471}
1472
1473struct Session {
1474 ptys: HashMap<u16, Pty>,
1475 compositor: Option<SharedCompositor>,
1476 next_client_id: u64,
1477 next_compositor_id: u16,
1478 next_pty_id: u16,
1479 tick_fires: u32,
1480 tick_snaps: u32,
1481 surface_commits: u32,
1482 surface_encodes: u32,
1483 surface_encode_bytes: u64,
1484 surface_frames_sent: u32,
1485 ticks_pixel_snapshot_empty: u32,
1487 pixel_snapshot_len: usize,
1489 last_ping: Instant,
1490 clients: HashMap<u64, ClientState>,
1491}
1492
1493struct SearchResultRow {
1494 pty_id: u16,
1495 score: u32,
1496 primary_source: u8,
1497 matched_sources: u8,
1498 context: String,
1499 scroll_offset: Option<usize>,
1500}
1501
1502struct TickOutcome {
1503 next_deadline: Option<Instant>,
1504}
1505
1506impl Session {
1507 fn new() -> Self {
1508 Self {
1509 ptys: HashMap::new(),
1510 compositor: None,
1511 next_client_id: 1,
1512 next_compositor_id: 1,
1513 next_pty_id: 1,
1514 clients: HashMap::new(),
1515 tick_fires: 0,
1516 tick_snaps: 0,
1517 surface_commits: 0,
1518 surface_encodes: 0,
1519 surface_encode_bytes: 0,
1520 ticks_pixel_snapshot_empty: 0,
1521 pixel_snapshot_len: 0,
1522 last_ping: Instant::now(),
1523 surface_frames_sent: 0,
1524 }
1525 }
1526
1527 fn ensure_compositor(
1528 &mut self,
1529 verbose: bool,
1530 event_notify: Arc<dyn Fn() + Send + Sync>,
1531 gpu_device: &str,
1532 ) -> &str {
1533 if self.compositor.is_none() {
1534 let session_id = self.next_compositor_id;
1535 self.next_compositor_id = self.next_compositor_id.wrapping_add(1);
1536 let created_at = Instant::now();
1539 let handle = blit_compositor::spawn_compositor(verbose, event_notify, gpu_device);
1540 #[cfg(target_os = "linux")]
1541 let audio_broadcast = audio::AudioBroadcast::new();
1542 #[cfg(target_os = "linux")]
1543 let audio_pipeline = {
1544 let audio_disabled = std::env::var("BLIT_AUDIO")
1545 .map(|v| v == "0")
1546 .unwrap_or(false);
1547 if !audio_disabled && audio::pipewire_available() {
1548 let runtime_dir = std::path::Path::new(&handle.socket_name)
1549 .parent()
1550 .unwrap_or(std::path::Path::new("/tmp"));
1551 let bitrate = std::env::var("BLIT_AUDIO_BITRATE")
1552 .ok()
1553 .and_then(|v| v.parse::<i32>().ok())
1554 .unwrap_or(0);
1555 let broadcast = audio_broadcast.clone();
1558 tokio::task::block_in_place(|| {
1559 match audio::AudioPipeline::spawn(
1560 runtime_dir,
1561 session_id,
1562 bitrate,
1563 verbose,
1564 created_at,
1565 broadcast,
1566 ) {
1567 Ok(pipeline) => {
1568 if verbose {
1569 eprintln!(
1570 "[audio] pipeline started, PULSE_SERVER={}",
1571 pipeline.pulse_server_path(),
1572 );
1573 }
1574 Some(pipeline)
1575 }
1576 Err(e) => {
1577 eprintln!("[audio] failed to start pipeline: {e}");
1578 None
1579 }
1580 }
1581 })
1582 } else {
1583 if verbose && !audio_disabled {
1584 let missing = audio::missing_pipewire_binaries();
1585 let load_err = audio_pw::load_error();
1586 if !missing.is_empty() {
1587 eprintln!(
1588 "[audio] audio disabled: missing binaries on $PATH: {}",
1589 missing.join(", ")
1590 );
1591 }
1592 if !load_err.is_empty() {
1593 eprintln!("[audio] audio disabled: {load_err}");
1594 }
1595 if missing.is_empty() && load_err.is_empty() {
1596 eprintln!(
1597 "[audio] audio disabled (reason not recorded; call pipewire_available() logged above)"
1598 );
1599 }
1600 }
1601 None
1602 }
1603 };
1604
1605 self.compositor = Some(SharedCompositor {
1606 handle,
1607 surfaces: HashMap::new(),
1608 last_pixels: HashMap::new(),
1609 last_frame_request: HashMap::new(),
1610 created_at,
1611 pixel_generation: 0,
1612 last_blanket_frame_request: Instant::now(),
1613 last_configured_size: HashMap::new(),
1614 #[cfg(target_os = "linux")]
1615 audio_pipeline,
1616 #[cfg(target_os = "linux")]
1617 audio_broadcast,
1618 #[cfg(target_os = "linux")]
1619 audio_session_id: session_id,
1620 #[cfg(target_os = "linux")]
1621 last_audio_restart: None,
1622 });
1623 }
1624 &self.compositor.as_ref().unwrap().handle.socket_name
1625 }
1626
1627 #[cfg(target_os = "linux")]
1629 fn pulse_server_path(&self) -> Option<String> {
1630 self.compositor
1631 .as_ref()
1632 .and_then(|cs| cs.audio_pipeline.as_ref())
1633 .map(|ap| ap.pulse_server_path())
1634 }
1635
1636 #[cfg(target_os = "linux")]
1638 fn pipewire_remote_path(&self) -> Option<String> {
1639 self.compositor
1640 .as_ref()
1641 .and_then(|cs| cs.audio_pipeline.as_ref())
1642 .map(|ap| ap.pipewire_remote_path())
1643 }
1644
1645 fn allocate_pty_id(&mut self, max_ptys: usize) -> Option<u16> {
1646 if max_ptys > 0 && self.ptys.len() >= max_ptys {
1647 return None;
1648 }
1649 let start = self.next_pty_id;
1650 let mut id = start;
1651 loop {
1652 if !self.ptys.contains_key(&id) {
1653 self.next_pty_id = if id == u16::MAX { 1 } else { id + 1 };
1654 return Some(id);
1655 }
1656 id = if id == u16::MAX { 1 } else { id + 1 };
1657 if id == start {
1658 return None;
1659 }
1660 }
1661 }
1662
1663 fn send_to_all(&self, msg: &[u8]) {
1664 for c in self.clients.values() {
1665 let _ = send_outbox(c, msg.to_vec());
1666 }
1667 }
1668
1669 fn mediated_size_for_pty(&self, pty_id: u16) -> Option<(u16, u16)> {
1670 let mut min_rows: Option<u16> = None;
1671 let mut min_cols: Option<u16> = None;
1672 for c in self.clients.values() {
1673 if let Some((r, cols)) = c.view_sizes.get(&pty_id).copied() {
1674 min_rows = Some(min_rows.map_or(r, |m: u16| m.min(r)));
1675 min_cols = Some(min_cols.map_or(cols, |m: u16| m.min(cols)));
1676 }
1677 }
1678 match (min_rows, min_cols) {
1679 (Some(r), Some(c)) => Some((r.max(1), c.max(1))),
1680 _ => None,
1681 }
1682 }
1683
1684 fn resize_pty(&mut self, pty_id: u16, rows: u16, cols: u16) -> bool {
1685 let pty = match self.ptys.get_mut(&pty_id) {
1686 Some(p) => p,
1687 None => return false,
1688 };
1689 let (cur_rows, cur_cols) = pty.driver.size();
1690 if cur_rows == rows && cur_cols == cols {
1691 return false;
1692 }
1693 pty.ready_frames.clear();
1694 pty.driver.resize(rows, cols);
1695 pty.mark_dirty();
1696 for c in self.clients.values_mut() {
1697 if c.subscriptions.contains(&pty_id) {
1698 c.last_sent.remove(&pty_id);
1699 }
1700 if c.scroll_caches.remove(&pty_id).is_some() {
1701 reset_inflight(c);
1702 }
1703 }
1704 if !pty.exited {
1705 pty::resize_pty_os(&pty.handle, rows, cols);
1706 }
1707 true
1708 }
1709
1710 fn resize_ptys_to_mediated_sizes<I>(&mut self, pty_ids: I) -> bool
1711 where
1712 I: IntoIterator<Item = u16>,
1713 {
1714 let mut changed = false;
1715 let mut seen = HashSet::new();
1716 for pty_id in pty_ids {
1717 if !seen.insert(pty_id) {
1718 continue;
1719 }
1720 if let Some((rows, cols)) = self.mediated_size_for_pty(pty_id) {
1721 changed |= self.resize_pty(pty_id, rows, cols);
1722 }
1723 }
1724 changed
1725 }
1726
1727 fn mediated_size_for_surface(
1740 &self,
1741 surface_id: u16,
1742 max: Option<(u16, u16)>,
1743 ) -> Option<(u16, u16, u16)> {
1744 let mut min_w: Option<u16> = None;
1745 let mut min_h: Option<u16> = None;
1746 let mut max_scale: u16 = 0;
1747 for c in self.clients.values() {
1748 if let Some(&(w, h, s)) = c.surface_view_sizes.get(&surface_id) {
1749 min_w = Some(min_w.map_or(w, |m: u16| m.min(w)));
1750 min_h = Some(min_h.map_or(h, |m: u16| m.min(h)));
1751 max_scale = max_scale.max(s);
1752 }
1753 }
1754 match (min_w, min_h) {
1755 (Some(w), Some(h)) => {
1756 let (w, h) = (w.max(1), h.max(1));
1757 let (w, h) = if let Some((mw, mh)) = max {
1758 (w.min(mw), h.min(mh))
1759 } else {
1760 (w, h)
1761 };
1762 Some((w, h, max_scale))
1763 }
1764 _ => None,
1765 }
1766 }
1767
1768 fn resize_surface(&mut self, surface_id: u16, width: u16, height: u16, scale_120: u16) -> bool {
1769 let cs = match self.compositor.as_mut() {
1770 Some(cs) => cs,
1771 None => return false,
1772 };
1773 if let Some(&(lw, lh, ls)) = cs.last_configured_size.get(&surface_id)
1781 && lw == width
1782 && lh == height
1783 && ls == scale_120
1784 {
1785 return false;
1786 }
1787 cs.last_configured_size
1788 .insert(surface_id, (width, height, scale_120));
1789 let _ = cs.handle.command_tx.send(CompositorCommand::SurfaceResize {
1790 surface_id,
1791 width,
1792 height,
1793 scale_120,
1794 });
1795 true
1796 }
1797
1798 fn resize_surfaces_to_mediated_sizes<I>(
1799 &mut self,
1800 surface_ids: I,
1801 encoder_preferences: &[SurfaceEncoderPreference],
1802 ) where
1803 I: IntoIterator<Item = u16>,
1804 {
1805 let max = SurfaceEncoderPreference::max_dimensions_for_list(encoder_preferences);
1806 let mut seen = HashSet::new();
1807 for sid in surface_ids {
1808 if !seen.insert(sid) {
1809 continue;
1810 }
1811 if let Some((w, h, scale_120)) = self.mediated_size_for_surface(sid, max) {
1812 self.resize_surface(sid, w, h, scale_120);
1813 }
1814 }
1815 }
1816
1817 fn pty_list_msg(&self) -> Vec<u8> {
1818 let mut msg = vec![S2C_LIST];
1819 let count = self.ptys.len() as u16;
1820 msg.extend_from_slice(&count.to_le_bytes());
1821 let mut ids: Vec<u16> = self.ptys.keys().copied().collect();
1822 ids.sort();
1823 for id in ids {
1824 let pty = &self.ptys[&id];
1825 let tag = pty.tag.as_bytes();
1826 msg.extend_from_slice(&id.to_le_bytes());
1827 msg.extend_from_slice(&(tag.len() as u16).to_le_bytes());
1828 msg.extend_from_slice(tag);
1829 let cmd = pty.command.as_deref().unwrap_or("").as_bytes();
1830 msg.extend_from_slice(&(cmd.len() as u16).to_le_bytes());
1831 msg.extend_from_slice(cmd);
1832 }
1833 msg
1834 }
1835
1836 fn surface_list_msg(&self) -> Vec<u8> {
1837 let cs = match self.compositor.as_ref() {
1838 Some(cs) => cs,
1839 None => {
1840 let mut msg = vec![S2C_SURFACE_LIST];
1841 msg.extend_from_slice(&0u16.to_le_bytes());
1842 return msg;
1843 }
1844 };
1845 let mut msg = vec![S2C_SURFACE_LIST];
1846 let count = cs.surfaces.len() as u16;
1847 msg.extend_from_slice(&count.to_le_bytes());
1848 let mut ids: Vec<u16> = cs.surfaces.keys().copied().collect();
1849 ids.sort();
1850 for id in ids {
1851 let info = &cs.surfaces[&id];
1852 let title = info.title.as_bytes();
1853 let app_id = info.app_id.as_bytes();
1854 msg.extend_from_slice(&info.surface_id.to_le_bytes());
1855 msg.extend_from_slice(&info.parent_id.to_le_bytes());
1856 msg.extend_from_slice(&info.width.to_le_bytes());
1857 msg.extend_from_slice(&info.height.to_le_bytes());
1858 msg.extend_from_slice(&(title.len() as u16).to_le_bytes());
1859 msg.extend_from_slice(title);
1860 msg.extend_from_slice(&(app_id.len() as u16).to_le_bytes());
1861 msg.extend_from_slice(app_id);
1862 }
1863 msg
1864 }
1865}
1866
1867struct AppStateInner {
1868 config: Config,
1869 session: Mutex<Session>,
1870 pty_fds: PtyFds,
1871 delivery_notify: Arc<Notify>,
1872 shutdown_notify: Arc<Notify>,
1874 active_connections: std::sync::atomic::AtomicUsize,
1877}
1878
1879type AppState = Arc<AppStateInner>;
1880
1881fn nudge_delivery(state: &AppState) {
1882 state.delivery_notify.notify_one();
1883}
1884
1885#[cfg(unix)]
1886#[allow(dead_code)]
1887fn spawn_compositor_child(
1888 command: &str,
1889 argv: Option<&[&str]>,
1890 wayland_socket: &str,
1891 dir: Option<&str>,
1892) -> libc::pid_t {
1893 use std::ffi::CString;
1894 let pid = unsafe { libc::fork() };
1895 if pid == 0 {
1896 if let Some(d) = dir {
1897 let c_dir = CString::new(d).unwrap();
1898 unsafe {
1899 libc::chdir(c_dir.as_ptr());
1900 }
1901 }
1902 unsafe {
1903 let wd_path = std::path::Path::new(wayland_socket);
1904 if let Some(dir) = wd_path.parent() {
1905 let xdg = std::env::var_os("XDG_RUNTIME_DIR");
1906 let needs_update = match &xdg {
1907 Some(x) => std::path::Path::new(x) != dir,
1908 None => true,
1909 };
1910 if needs_update {
1911 std::env::set_var("XDG_RUNTIME_DIR", dir);
1912 }
1913 }
1914 std::env::set_var("WAYLAND_DISPLAY", wayland_socket);
1915 std::env::remove_var("DISPLAY");
1916 std::env::remove_var("DBUS_SESSION_BUS_ADDRESS");
1917 std::env::remove_var("DBUS_SYSTEM_BUS_ADDRESS");
1918 }
1919 if let Some(args) = argv {
1920 let prog = CString::new(args[0]).unwrap();
1921 let c_args: Vec<CString> = args.iter().map(|a| CString::new(*a).unwrap()).collect();
1922 let c_ptrs: Vec<*const libc::c_char> = c_args
1923 .iter()
1924 .map(|a| a.as_ptr())
1925 .chain(std::iter::once(std::ptr::null()))
1926 .collect();
1927 unsafe {
1928 libc::execvp(prog.as_ptr(), c_ptrs.as_ptr());
1929 }
1930 } else {
1931 let prog = CString::new(command).unwrap();
1932 let c_ptrs = [prog.as_ptr(), std::ptr::null()];
1933 unsafe {
1934 libc::execvp(prog.as_ptr(), c_ptrs.as_ptr());
1935 libc::_exit(1);
1936 }
1937 }
1938 }
1939 pid
1940}
1941
1942fn xterm256_color(idx: u8) -> (u16, u16, u16) {
1944 const BASE16: [(u8, u8, u8); 16] = [
1946 (0, 0, 0),
1947 (128, 0, 0),
1948 (0, 128, 0),
1949 (128, 128, 0),
1950 (0, 0, 128),
1951 (128, 0, 128),
1952 (0, 128, 128),
1953 (192, 192, 192),
1954 (128, 128, 128),
1955 (255, 0, 0),
1956 (0, 255, 0),
1957 (255, 255, 0),
1958 (0, 0, 255),
1959 (255, 0, 255),
1960 (0, 255, 255),
1961 (255, 255, 255),
1962 ];
1963 let (r8, g8, b8) = if idx < 16 {
1964 BASE16[idx as usize]
1965 } else if idx < 232 {
1966 let n = idx - 16;
1968 let ri = n / 36;
1969 let gi = (n % 36) / 6;
1970 let bi = n % 6;
1971 let to_val = |v: u8| if v == 0 { 0u8 } else { 55 + 40 * v };
1972 (to_val(ri), to_val(gi), to_val(bi))
1973 } else {
1974 let v = 8 + 10 * (idx - 232);
1976 (v, v, v)
1977 };
1978 let scale = |v: u8| (v as u16) << 8 | v as u16;
1980 (scale(r8), scale(g8), scale(b8))
1981}
1982fn parse_terminal_queries(data: &[u8], size: (u16, u16), cursor: (u16, u16)) -> Vec<String> {
1983 const DA1_RESPONSE: &[u8] = b"\x1b[?64;1;2;6;9;15;18;21;22c";
1984
1985 let mut results = Vec::new();
1986 let mut i = 0;
1987 while i < data.len() {
1988 if data[i] != 0x1b || i + 1 >= data.len() {
1989 i += 1;
1990 continue;
1991 }
1992
1993 if data[i + 1] == b']' {
1995 let osc_start = i + 2;
1996 let mut end = osc_start;
1998 while end < data.len() {
1999 if data[end] == 0x07 {
2000 break;
2001 }
2002 if data[end] == 0x1b && end + 1 < data.len() && data[end + 1] == b'\\' {
2003 break;
2004 }
2005 end += 1;
2006 }
2007 if end < data.len() {
2008 let payload = &data[osc_start..end];
2009 if payload == b"11;?" {
2011 results.push("\x1b]11;rgb:0000/0000/0000\x1b\\".into());
2013 }
2014 else if payload == b"10;?" {
2016 results.push("\x1b]10;rgb:ffff/ffff/ffff\x1b\\".into());
2017 }
2018 else if payload.starts_with(b"4;") && payload.ends_with(b";?") {
2020 let idx_bytes = &payload[2..payload.len() - 2];
2021 if let Ok(idx_str) = std::str::from_utf8(idx_bytes)
2022 && let Ok(idx) = idx_str.parse::<u8>()
2023 {
2024 let (r, g, b) = xterm256_color(idx);
2025 results.push(format!("\x1b]4;{idx};rgb:{r:04x}/{g:04x}/{b:04x}\x1b\\"));
2026 }
2027 }
2028 i = end + if data[end] == 0x07 { 1 } else { 2 };
2029 continue;
2030 }
2031 i = end;
2032 continue;
2033 }
2034
2035 if i + 2 >= data.len() || data[i + 1] != b'[' {
2037 i += 1;
2038 continue;
2039 }
2040 i += 2;
2041 let has_q = i < data.len() && data[i] == b'?';
2042 if has_q {
2043 i += 1;
2044 }
2045 let param_start = i;
2046 while i < data.len() && (data[i].is_ascii_digit() || data[i] == b';') {
2047 i += 1;
2048 }
2049 if i >= data.len() {
2050 break;
2051 }
2052 let final_byte = data[i];
2053 let params = &data[param_start..i];
2054 i += 1;
2055 if has_q {
2056 continue;
2057 }
2058 let resp: Option<String> = match final_byte {
2059 b'c' if params.is_empty() || params == b"0" => {
2060 Some(String::from_utf8_lossy(DA1_RESPONSE).into_owned())
2061 }
2062 b'n' if params == b"6" => Some(format!("\x1b[{};{}R", cursor.0 + 1, cursor.1 + 1)),
2063 b'n' if params == b"5" => Some("\x1b[0n".into()),
2064 b't' if params == b"18" => {
2065 let (rows, cols) = size;
2066 Some(format!("\x1b[8;{rows};{cols}t"))
2067 }
2068 b't' if params == b"14" => {
2069 let (rows, cols) = size;
2070 Some(format!("\x1b[4;{};{}t", rows * 16, cols * 8))
2071 }
2072 _ => None,
2073 };
2074 if let Some(r) = resp {
2075 results.push(r);
2076 }
2077 }
2078 results
2079}
2080
2081async fn cleanup_pty_internal(pty_id: u16, state: &AppState) {
2082 state.pty_fds.write().unwrap().remove(&pty_id);
2083 let mut sess = state.session.lock().await;
2084 if let Some(pty) = sess.ptys.get_mut(&pty_id) {
2085 if pty.exited {
2086 return;
2087 }
2088 pty.exited = true;
2089 pty::close_pty(&pty.handle);
2090 pty.exit_status = pty::collect_exit_status(&pty.handle);
2091 pty.mark_dirty();
2092 let msg = blit_remote::msg_exited(pty_id, pty.exit_status);
2093 sess.send_to_all(&msg);
2094 }
2095}
2096
2097fn take_snapshot(pty: &mut Pty) -> FrameState {
2098 if pty.lflag_last.elapsed() >= Duration::from_millis(250) {
2099 pty.lflag_cache = pty::pty_lflag(&pty.handle);
2100 pty.lflag_last = Instant::now();
2101 }
2102 let (echo, icanon) = pty.lflag_cache;
2103 pty.driver.snapshot(echo, icanon)
2104}
2105
2106fn build_scrollback_update(
2107 pty: &mut Pty,
2108 id: u16,
2109 offset: usize,
2110 prev_frame: &FrameState,
2111) -> Option<(Vec<u8>, FrameState)> {
2112 let frame = pty.driver.scrollback_frame(offset);
2113 let msg = build_update_msg(id, &frame, prev_frame);
2114 msg.map(|m| (m, frame))
2115}
2116
2117fn build_search_results_msg(request_id: u16, results: &[SearchResultRow]) -> Vec<u8> {
2118 let count = results.len().min(u16::MAX as usize);
2119 let payload_bytes: usize = results[..count]
2120 .iter()
2121 .map(|result| 14 + result.context.len().min(u16::MAX as usize))
2122 .sum();
2123 let mut msg = Vec::with_capacity(5 + payload_bytes);
2124 msg.push(S2C_SEARCH_RESULTS);
2125 msg.extend_from_slice(&request_id.to_le_bytes());
2126 msg.extend_from_slice(&(count as u16).to_le_bytes());
2127 for result in &results[..count] {
2128 msg.extend_from_slice(&result.pty_id.to_le_bytes());
2129 msg.extend_from_slice(&result.score.to_le_bytes());
2130 msg.push(result.primary_source);
2131 msg.push(result.matched_sources);
2132 let scroll_offset = result
2133 .scroll_offset
2134 .map(|offset| offset.min(u32::MAX as usize - 1) as u32)
2135 .unwrap_or(u32::MAX);
2136 msg.extend_from_slice(&scroll_offset.to_le_bytes());
2137 let context = result.context.as_bytes();
2138 let context_len = context.len().min(u16::MAX as usize);
2139 msg.extend_from_slice(&(context_len as u16).to_le_bytes());
2140 msg.extend_from_slice(&context[..context_len]);
2141 }
2142 msg
2143}
2144
2145enum SendOutcome {
2146 NoChange,
2147 Sent,
2148 Backpressured,
2149}
2150
2151fn try_send_update(
2152 client: &mut ClientState,
2153 pid: u16,
2154 current: FrameState,
2155 msg: Option<Vec<u8>>,
2156 now: Instant,
2157 paced: bool,
2158) -> SendOutcome {
2159 let Some(msg) = msg else {
2160 return SendOutcome::NoChange;
2161 };
2162 let bytes = msg.len();
2163 if send_outbox(client, msg).is_ok() {
2164 client.last_sent.insert(pid, current);
2165 record_send(client, bytes, now, paced);
2166 client.frames_sent = client.frames_sent.wrapping_add(1);
2167 SendOutcome::Sent
2168 } else {
2169 client.last_sent.insert(pid, current);
2173 SendOutcome::Backpressured
2174 }
2175}
2176
2177pub async fn run(config: Config) {
2178 let state: AppState = Arc::new(AppStateInner {
2179 config,
2180 session: Mutex::new(Session::new()),
2181 pty_fds: Arc::new(std::sync::RwLock::new(HashMap::new())),
2182 delivery_notify: Arc::new(Notify::new()),
2183 shutdown_notify: Arc::new(Notify::new()),
2184 active_connections: std::sync::atomic::AtomicUsize::new(0),
2185 });
2186
2187 if !state.config.skip_compositor {
2190 let notify = state.delivery_notify.clone();
2191 let event_notify = Arc::new(move || notify.notify_one()) as Arc<dyn Fn() + Send + Sync>;
2192 let mut sess = state.session.lock().await;
2193 sess.ensure_compositor(
2194 state.config.verbose,
2195 event_notify,
2196 &state.config.vaapi_device,
2197 );
2198 }
2199
2200 let delivery_state = state.clone();
2201 tokio::spawn(async move {
2202 let mut next_deadline: Option<Instant> = None;
2203 loop {
2204 if let Some(deadline) = next_deadline {
2205 tokio::select! {
2206 _ = delivery_state.delivery_notify.notified() => {}
2207 _ = tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)) => {}
2208 }
2209 } else {
2210 delivery_state.delivery_notify.notified().await;
2211 }
2212 let outcome = tick(&delivery_state).await;
2213 next_deadline = outcome.next_deadline;
2214 }
2215 });
2216
2217 tokio::spawn(async {
2218 loop {
2219 tokio::time::sleep(Duration::from_secs(5)).await;
2220 pty::reap_zombies();
2221 }
2222 });
2223
2224 #[cfg(unix)]
2225 if let Some(channel_fd) = state.config.fd_channel {
2226 ipc::run_fd_channel(channel_fd, state).await;
2227 return;
2228 }
2229
2230 #[cfg(unix)]
2231 let listener = {
2232 if let Some(l) = IpcListener::from_systemd_fd(state.config.verbose) {
2233 l
2234 } else {
2235 IpcListener::bind(&state.config.ipc_path, state.config.verbose)
2236 }
2237 };
2238 #[cfg(not(unix))]
2239 let mut listener = IpcListener::bind(&state.config.ipc_path, state.config.verbose);
2240
2241 {
2244 let state = state.clone();
2245 tokio::spawn(async move {
2246 #[cfg(unix)]
2247 {
2248 use tokio::signal::unix::{SignalKind, signal};
2249 let mut sigterm = signal(SignalKind::terminate()).expect("signal handler");
2250 let mut sigint = signal(SignalKind::interrupt()).expect("signal handler");
2251 tokio::select! {
2252 _ = sigterm.recv() => {}
2253 _ = sigint.recv() => {}
2254 }
2255 }
2256 #[cfg(not(unix))]
2257 {
2258 let _ = tokio::signal::ctrl_c().await;
2259 }
2260 let sess = state.session.lock().await;
2261 sess.send_to_all(&[S2C_QUIT]);
2262 drop(sess);
2263 state.shutdown_notify.notify_one();
2264 });
2265 }
2266
2267 let shutdown = state.shutdown_notify.clone();
2268 loop {
2269 let stream = tokio::select! {
2270 result = listener.accept() => match result {
2271 Ok(s) => s,
2272 Err(e) => {
2273 eprintln!("accept error: {e}");
2274 tokio::time::sleep(Duration::from_millis(100)).await;
2275 continue;
2276 }
2277 },
2278 _ = shutdown.notified() => break,
2279 };
2280 let max = state.config.max_connections;
2281 if max > 0 {
2282 let current = state
2283 .active_connections
2284 .load(std::sync::atomic::Ordering::Relaxed);
2285 if current >= max {
2286 eprintln!("max connections ({max}) reached, rejecting");
2287 drop(stream);
2288 continue;
2289 }
2290 }
2291 state
2292 .active_connections
2293 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2294 let state = state.clone();
2295 tokio::spawn(async move {
2296 handle_client(stream, state.clone()).await;
2297 state
2298 .active_connections
2299 .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
2300 });
2301 }
2302 tokio::time::sleep(Duration::from_millis(100)).await;
2304}
2305
2306const BLANKET_FRAME_INTERVAL_IDLE: Duration = Duration::from_millis(250);
2315const BLANKET_FRAME_INTERVAL_SURFACE: Duration = Duration::from_micros(62_500);
2316
2317fn blanket_frame_interval(sess: &Session) -> Option<Duration> {
2323 if sess.clients.is_empty() {
2324 return None;
2325 }
2326 let has_surface_subs = sess
2327 .clients
2328 .values()
2329 .any(|c| !c.surface_subscriptions.is_empty());
2330 if has_surface_subs {
2331 Some(BLANKET_FRAME_INTERVAL_SURFACE)
2332 } else {
2333 Some(BLANKET_FRAME_INTERVAL_IDLE)
2334 }
2335}
2336
2337async fn tick(state: &AppState) -> TickOutcome {
2338 let mut sess = state.session.lock().await;
2339 sess.tick_fires += 1;
2340 let mut next_deadline: Option<Instant> = None;
2341 let now = Instant::now();
2342
2343 let log_client_ids: Vec<u64> = sess.clients.keys().copied().collect();
2347 for cid in log_client_ids {
2348 maybe_log_pacing_metrics(&mut sess, cid, state.config.verbose);
2349 }
2350
2351 let ping_interval = state.config.ping_interval;
2355 if !ping_interval.is_zero() && !sess.clients.is_empty() {
2356 if now.duration_since(sess.last_ping) >= ping_interval {
2357 sess.send_to_all(&[S2C_PING]);
2358 sess.last_ping = now;
2359 }
2360 let next_ping = sess.last_ping + ping_interval;
2361 next_deadline = Some(next_deadline.map_or(next_ping, |d: Instant| d.min(next_ping)));
2362 }
2363
2364 let mut invalidate_client_encoders: Vec<u16> = Vec::new();
2366 let mut resized_surface_ids: Vec<u16> = Vec::new();
2371
2372 let mut surface_commit_count = 0u32;
2373 if let Some(cs) = sess.compositor.as_mut() {
2374 let mut events = Vec::new();
2375 while let Ok(event) = cs.handle.event_rx.try_recv() {
2376 events.push(event);
2377 }
2378 let mut broadcast: Vec<Vec<u8>> = Vec::new();
2379 for event in events {
2380 match event {
2381 CompositorEvent::SurfaceCreated {
2382 surface_id,
2383 title,
2384 app_id,
2385 parent_id,
2386 width,
2387 height,
2388 } => {
2389 broadcast.push(msg_surface_created(
2390 surface_id, parent_id, width, height, &title, &app_id,
2391 ));
2392 cs.surfaces.insert(
2393 surface_id,
2394 CachedSurfaceInfo {
2395 surface_id,
2396 parent_id,
2397 width,
2398 height,
2399 title,
2400 app_id,
2401 },
2402 );
2403 cs.last_pixels.remove(&surface_id);
2404 invalidate_client_encoders.push(surface_id);
2405 }
2406 CompositorEvent::SurfaceDestroyed { surface_id } => {
2407 cs.surfaces.remove(&surface_id);
2408 cs.last_pixels.remove(&surface_id);
2409 cs.last_configured_size.remove(&surface_id);
2410 invalidate_client_encoders.push(surface_id);
2411 broadcast.push(msg_surface_destroyed(surface_id));
2412 }
2413 CompositorEvent::SurfaceCommit {
2414 surface_id,
2415 width,
2416 height,
2417 pixels,
2418 timestamp_ms,
2419 } => {
2420 surface_commit_count += 1;
2421 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
2422 info.width = width as u16;
2423 info.height = height as u16;
2424 }
2425 cs.pixel_generation += 1;
2426 cs.last_pixels.insert(
2427 surface_id,
2428 LastPixels {
2429 width,
2430 height,
2431 pixels,
2432 generation: cs.pixel_generation,
2433 timestamp_ms,
2434 },
2435 );
2436 }
2437 CompositorEvent::SurfaceTitle { surface_id, title } => {
2438 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
2439 info.title = title.clone();
2440 }
2441 broadcast.push(msg_surface_title(surface_id, &title));
2442 }
2443 CompositorEvent::SurfaceAppId { surface_id, app_id } => {
2444 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
2445 info.app_id = app_id.clone();
2446 }
2447 broadcast.push(msg_surface_app_id(surface_id, &app_id));
2448 }
2449 CompositorEvent::SurfaceResized {
2450 surface_id,
2451 width,
2452 height,
2453 } => {
2454 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
2455 info.width = width;
2456 info.height = height;
2457 }
2458 cs.last_pixels.remove(&surface_id);
2459 broadcast.push(msg_surface_resized(surface_id, width, height));
2468 resized_surface_ids.push(surface_id);
2469 }
2470 CompositorEvent::ClipboardContent {
2471 mime_type, data, ..
2472 } => {
2473 broadcast.push(msg_s2c_clipboard_content(&mime_type, &data));
2474 }
2475 CompositorEvent::SurfaceCursor { surface_id, cursor } => {
2476 let mut msg = Vec::new();
2481 msg.push(blit_remote::S2C_SURFACE_CURSOR);
2482 msg.extend_from_slice(&surface_id.to_le_bytes());
2483 match &cursor {
2484 blit_compositor::CursorImage::Named(name) => {
2485 msg.push(0); msg.push(name.len() as u8);
2487 msg.extend_from_slice(name.as_bytes());
2488 }
2489 blit_compositor::CursorImage::Hidden => {
2490 msg.push(1); }
2492 blit_compositor::CursorImage::Custom {
2493 hotspot_x,
2494 hotspot_y,
2495 width,
2496 height,
2497 rgba,
2498 } => {
2499 let mut png_buf = Vec::new();
2501 {
2502 let mut encoder =
2503 png::Encoder::new(&mut png_buf, *width as u32, *height as u32);
2504 encoder.set_color(png::ColorType::Rgba);
2505 encoder.set_depth(png::BitDepth::Eight);
2506 if let Ok(mut writer) = encoder.write_header() {
2507 let _ = writer.write_image_data(rgba);
2508 }
2509 }
2510 msg.push(2); msg.extend_from_slice(&hotspot_x.to_le_bytes());
2512 msg.extend_from_slice(&hotspot_y.to_le_bytes());
2513 msg.extend_from_slice(&width.to_le_bytes());
2514 msg.extend_from_slice(&height.to_le_bytes());
2515 msg.extend_from_slice(&png_buf);
2516 }
2517 }
2518 broadcast.push(msg);
2519 }
2520 }
2521 }
2522 for msg in &broadcast {
2523 sess.send_to_all(msg);
2524 }
2525 }
2526 sess.surface_commits += surface_commit_count;
2527
2528 for sid in invalidate_client_encoders {
2533 for c in sess.clients.values_mut() {
2534 c.surface_subs.remove(&sid);
2535 c.vulkan_video_surfaces.remove(&sid);
2536 }
2537 }
2538
2539 for sid in resized_surface_ids {
2545 for c in sess.clients.values_mut() {
2546 if !c.surface_subscriptions.contains(&sid) {
2547 continue;
2548 }
2549 if let Some(s) = c.surface_subs.get_mut(&sid) {
2550 s.burst_remaining = SURFACE_BURST_FRAMES;
2551 s.next_send_at = None;
2552 s.nal_none_streak = 0;
2553 s.nal_none_latched_at = None;
2554 }
2555 c.surface_needs_keyframe = true;
2556 }
2557 }
2558
2559 let pixel_snapshot: Vec<(u16, u32, u32, u64, u32)> = sess
2570 .compositor
2571 .as_ref()
2572 .map(|cs| {
2573 cs.last_pixels
2574 .iter()
2575 .map(|(&sid, lp)| (sid, lp.width, lp.height, lp.generation, lp.timestamp_ms))
2576 .collect()
2577 })
2578 .unwrap_or_default();
2579 if pixel_snapshot.is_empty() {
2580 sess.ticks_pixel_snapshot_empty = sess.ticks_pixel_snapshot_empty.saturating_add(1);
2581 } else {
2582 sess.pixel_snapshot_len = pixel_snapshot.len();
2583 }
2584
2585 struct EncodeJob {
2591 cid: u64,
2592 sid: u16,
2593 px_w: u32,
2596 px_h: u32,
2597 pixels: blit_compositor::PixelData,
2599 needs_keyframe: bool,
2600 encoder: SurfaceEncoder,
2601 generation: u64,
2602 timestamp_ms: u32,
2604 }
2605 struct EncoderCreateParams {
2606 preferences: Vec<SurfaceEncoderPreference>,
2607 vaapi_device: String,
2608 quality: SurfaceQuality,
2609 verbose: bool,
2610 codec_support: u8,
2611 chroma: ChromaSubsampling,
2612 }
2613 struct CreateJob {
2620 cid: u64,
2621 sid: u16,
2622 px_w: u32,
2623 px_h: u32,
2624 params: EncoderCreateParams,
2625 }
2626 struct CreateResult {
2627 cid: u64,
2628 sid: u16,
2629 encoder: Option<SurfaceEncoder>,
2633 fresh: Option<FreshEncoder>,
2634 }
2635 struct FreshEncoder {
2640 name: &'static str,
2641 codec_string: String,
2642 #[cfg(target_os = "linux")]
2643 external_bufs: Vec<blit_compositor::ExternalOutputBuffer>,
2644 }
2645 struct EncodeResult {
2646 cid: u64,
2647 sid: u16,
2648 px_w: u32,
2650 px_h: u32,
2651 generation: u64,
2652 encoder: SurfaceEncoder,
2653 nal_data: Option<(Vec<u8>, bool)>, codec_flag: u8,
2655 timestamp_ms: u32,
2657 }
2658
2659 let mut encode_jobs: Vec<EncodeJob> = Vec::new();
2660 let mut create_jobs: Vec<CreateJob> = Vec::new();
2661 let mut encode_dispatched_surfaces: HashSet<u16> = HashSet::new();
2665
2666 struct ClientWork {
2670 cid: u64,
2671 subs: HashSet<u16>,
2672 needs_keyframe: bool,
2673 }
2674 let mut client_work: Vec<ClientWork> = Vec::new();
2675
2676 if !pixel_snapshot.is_empty() {
2677 for (&cid, client) in sess.clients.iter_mut() {
2678 if !surface_window_open(client) {
2679 let now_inst = Instant::now();
2681 if now_inst
2682 .duration_since(client.last_window_blocked_log)
2683 .as_secs_f32()
2684 > 5.0
2685 {
2686 client.last_window_blocked_log = now_inst;
2687 let max_burst: u8 = client
2688 .surface_subs
2689 .values()
2690 .map(|s| s.burst_remaining)
2691 .max()
2692 .unwrap_or(0);
2693 eprintln!(
2694 "[surface-gate] cid={cid} surface_window_open=false outbox={}f/{}B (limits {}f/{}B) burst={max_burst}",
2695 outbox_queued_frames(client),
2696 outbox_queued_bytes(client),
2697 OUTBOX_SOFT_QUEUE_LIMIT_FRAMES,
2698 OUTBOX_SOFT_QUEUE_LIMIT_BYTES,
2699 );
2700 }
2701 continue;
2702 }
2703 if client.surface_subscriptions.is_empty() {
2706 client.skip_no_subs_count = client.skip_no_subs_count.saturating_add(1);
2707 continue;
2708 }
2709 let subs: HashSet<u16> = client.surface_subscriptions.iter().copied().collect();
2710 client_work.push(ClientWork {
2711 cid,
2712 subs,
2713 needs_keyframe: client.surface_needs_keyframe,
2714 });
2715 }
2720
2721 let mut encoded_client_surfaces: HashSet<(u64, u16)> = HashSet::new();
2724
2725 let vk_encode_available = sess
2728 .compositor
2729 .as_ref()
2730 .is_some_and(|cs| cs.handle.vulkan_video_encode);
2731 let vk_encode_av1_available = sess
2732 .compositor
2733 .as_ref()
2734 .is_some_and(|cs| cs.handle.vulkan_video_encode_av1);
2735
2736 struct VulkanEncoderSetup {
2738 surface_id: u32,
2739 codec: u8,
2740 qp: u8,
2741 width: u32,
2742 height: u32,
2743 }
2744 let mut pending_vulkan_encoder_setups: Vec<VulkanEncoderSetup> = Vec::new();
2745 let mut pending_vulkan_keyframe_requests: Vec<u32> = Vec::new();
2746
2747 for work in &client_work {
2748 for &sid in &work.subs {
2749 let Some(&(_, px_w, px_h, px_gen, px_timestamp_ms)) =
2750 pixel_snapshot.iter().find(|&&(s, _, _, _, _)| s == sid)
2751 else {
2752 let client = sess.clients.get_mut(&work.cid).unwrap();
2753 client.skip_last_pixels_mismatch_count =
2754 client.skip_last_pixels_mismatch_count.saturating_add(1);
2755 continue;
2756 };
2757 {
2758 let client = sess.clients.get_mut(&work.cid).unwrap();
2759 client.encode_loop_iters = client.encode_loop_iters.saturating_add(1);
2760 }
2761 let client = sess.clients.get_mut(&work.cid).unwrap();
2762
2763 {
2767 let (burst, deadline) = client.surface_subs.get(&sid).map_or((0, now), |s| {
2768 (s.burst_remaining, s.next_send_at.unwrap_or(now))
2769 });
2770 if burst == 0 && deadline > now {
2771 let interval = surface_send_interval(client);
2775 if deadline > now + interval + interval {
2776 client.surface_subs.entry(sid).or_default().next_send_at = Some(now);
2777 } else {
2778 next_deadline = Some(match next_deadline {
2779 Some(existing) => existing.min(deadline),
2780 None => deadline,
2781 });
2782 client.skip_pacing_count = client.skip_pacing_count.saturating_add(1);
2783 continue;
2784 }
2785 }
2786 }
2787
2788 if !work.needs_keyframe
2791 && let Some(last_gen) = client
2792 .surface_subs
2793 .get(&sid)
2794 .and_then(|s| s.last_encoded_gen)
2795 && last_gen == px_gen
2796 {
2797 client.skip_same_gen_count = client.skip_same_gen_count.saturating_add(1);
2798 continue;
2799 }
2800
2801 let pixels: blit_compositor::PixelData = {
2802 let cs = sess.compositor.as_ref().unwrap();
2803 match cs.last_pixels.get(&sid) {
2804 Some(lp) if lp.width == px_w && lp.height == px_h => lp.pixels.clone(),
2805 _ => {
2806 let client = sess.clients.get_mut(&work.cid).unwrap();
2807 client.skip_last_pixels_mismatch_count =
2808 client.skip_last_pixels_mismatch_count.saturating_add(1);
2809 continue;
2810 }
2811 }
2812 };
2813 let client = sess.clients.get_mut(&work.cid).unwrap();
2814
2815 let (enc_w, enc_h) = (px_w, px_h);
2816
2817 if let blit_compositor::PixelData::Encoded {
2821 ref data,
2822 is_keyframe,
2823 codec_flag,
2824 } = pixels
2825 {
2826 let flags = codec_flag
2827 | if is_keyframe {
2828 SURFACE_FRAME_FLAG_KEYFRAME
2829 } else {
2830 0
2831 };
2832 let msg = msg_surface_frame(
2833 sid,
2834 px_timestamp_ms,
2835 flags,
2836 px_w as u16,
2837 px_h as u16,
2838 data,
2839 );
2840 let bytes = msg.len();
2841 match send_outbox(client, msg) {
2842 Err(_e) => {
2843 client.surface_needs_keyframe = true;
2844 }
2845 Ok(()) => {
2846 client.surface_inflight_frames.push_back(InFlightFrame {
2847 sent_at: now,
2848 bytes,
2849 paced: true,
2850 });
2851 if !is_keyframe {
2852 client.avg_surface_frame_bytes = ewma_with_direction(
2853 client.avg_surface_frame_bytes,
2854 bytes as f32,
2855 0.5,
2856 0.125,
2857 );
2858 }
2859 client.frames_sent = client.frames_sent.wrapping_add(1);
2860 if client.surface_needs_keyframe && is_keyframe {
2861 client.surface_needs_keyframe = false;
2862 }
2863 if let Some(s) = client.surface_subs.get_mut(&sid) {
2864 s.burst_remaining = s.burst_remaining.saturating_sub(1);
2865 }
2866 }
2867 }
2868 encoded_client_surfaces.insert((work.cid, sid));
2869 encode_dispatched_surfaces.insert(sid);
2870 client.surface_subs.entry(sid).or_default().last_encoded_gen = Some(px_gen);
2871 continue;
2872 }
2873
2874 if client
2880 .surface_subs
2881 .get(&sid)
2882 .is_some_and(|s| s.encode_in_flight || s.creation_in_flight)
2883 {
2884 client.skip_in_flight_count = client.skip_in_flight_count.saturating_add(1);
2885 let now_inst = Instant::now();
2886 if now_inst.duration_since(client.last_skip_log).as_secs_f32() > 5.0 {
2887 client.last_skip_log = now_inst;
2888 let burst = client
2889 .surface_subs
2890 .get(&sid)
2891 .map_or(0, |s| s.burst_remaining);
2892 eprintln!(
2893 "[encode-skip] cid={} sid={sid} reason=in_flight same_gen={} in_flight={} burst={burst}",
2894 work.cid, client.skip_same_gen_count, client.skip_in_flight_count,
2895 );
2896 }
2897 continue;
2898 }
2899
2900 let has_vulkan_enc = client.vulkan_video_surfaces.contains_key(&sid);
2901 let needs_new_encoder = if has_vulkan_enc {
2902 false
2903 } else {
2904 client
2905 .surface_subs
2906 .get(&sid)
2907 .and_then(|s| s.encoder.as_ref())
2908 .is_none_or(|e| e.source_dimensions() != (enc_w, enc_h))
2909 };
2910
2911 const NAL_NONE_RETRY_BACKOFF: Duration = Duration::from_secs(2);
2920 if needs_new_encoder
2921 && client
2922 .surface_subs
2923 .get(&sid)
2924 .is_some_and(|s| s.nal_none_streak >= 10)
2925 {
2926 let ready_to_retry = client
2927 .surface_subs
2928 .get(&sid)
2929 .and_then(|s| s.nal_none_latched_at)
2930 .is_some_and(|t| now.duration_since(t) >= NAL_NONE_RETRY_BACKOFF);
2931 if ready_to_retry {
2932 if let Some(s) = client.surface_subs.get_mut(&sid) {
2933 s.nal_none_streak = 0;
2934 s.nal_none_latched_at = None;
2935 }
2936 } else {
2937 continue;
2938 }
2939 }
2940
2941 if needs_new_encoder {
2943 let codec_support = client
2944 .surface_subs
2945 .get(&sid)
2946 .map(|s| s.codec_override)
2947 .filter(|&c| c != 0)
2948 .unwrap_or(client.surface_codec_support);
2949 let quality = client
2950 .surface_subs
2951 .get(&sid)
2952 .and_then(|s| s.quality_override)
2953 .unwrap_or(state.config.surface_quality);
2954
2955 for &pref in &state.config.surface_encoders {
2956 if !pref.is_vulkan_video() {
2957 continue;
2958 }
2959 if !pref.supported_by_client(codec_support) {
2960 continue;
2961 }
2962 let available = match pref {
2964 SurfaceEncoderPreference::VulkanVideoH264 => vk_encode_available,
2965 SurfaceEncoderPreference::VulkanVideoAV1 => vk_encode_av1_available,
2966 _ => false,
2967 };
2968 if !available {
2969 continue;
2970 }
2971 if state.config.chroma.is_444() {
2975 continue;
2976 }
2977 let qp = match pref {
2978 SurfaceEncoderPreference::VulkanVideoAV1 => quality.av1_qp_for_vulkan(),
2979 _ => quality.h264_qp(),
2980 };
2981 let enc_name: &'static str = match (pref, state.config.chroma) {
2982 (
2983 SurfaceEncoderPreference::VulkanVideoH264,
2984 ChromaSubsampling::Cs444,
2985 ) => "h264-vulkan 4:4:4",
2986 (SurfaceEncoderPreference::VulkanVideoH264, _) => "h264-vulkan",
2987 (
2988 SurfaceEncoderPreference::VulkanVideoAV1,
2989 ChromaSubsampling::Cs444,
2990 ) => "av1-vulkan 4:4:4",
2991 (SurfaceEncoderPreference::VulkanVideoAV1, _) => "av1-vulkan",
2992 (_, ChromaSubsampling::Cs444) => "vulkan 4:4:4",
2993 _ => "vulkan",
2994 };
2995 pending_vulkan_encoder_setups.push(VulkanEncoderSetup {
2997 surface_id: sid as u32,
2998 codec: pref.vulkan_codec(),
2999 qp,
3000 width: px_w,
3001 height: px_h,
3002 });
3003 pending_vulkan_keyframe_requests.push(sid as u32);
3004 if let Some(s) = client.surface_subs.get_mut(&sid) {
3005 s.encoder = None;
3006 }
3007 client
3008 .vulkan_video_surfaces
3009 .insert(sid, (enc_name, pref.codec_flag()));
3010 let codec_str = match pref {
3011 SurfaceEncoderPreference::VulkanVideoH264 => {
3012 if state.config.chroma.is_444() {
3013 "avc1.F4001f".to_string()
3014 } else {
3015 "avc1.640034".to_string()
3016 }
3017 }
3018 SurfaceEncoderPreference::VulkanVideoAV1 => {
3019 let profile = if state.config.chroma.is_444() { 2 } else { 0 };
3020 let level = surface_encoder::av1_level_for(px_w, px_h);
3021 format!("av01.{profile}.{level}M.08")
3022 }
3023 _ => String::new(),
3024 };
3025 let enc_msg = msg_surface_encoder(sid, enc_name, &codec_str);
3026 let _ = send_outbox(client, enc_msg);
3027 if state.config.verbose {
3028 eprintln!(
3029 "[surface-encoder] cid={} sid={sid} {px_w}x{px_h}: using {enc_name}",
3030 work.cid,
3031 );
3032 }
3033 break;
3034 }
3035
3036 {
3044 let state = client.surface_subs.entry(sid).or_default();
3045 state.encoder = None;
3046 state.creation_in_flight = true;
3047 }
3048 create_jobs.push(CreateJob {
3049 cid: work.cid,
3050 sid,
3051 px_w: enc_w,
3052 px_h: enc_h,
3053 params: EncoderCreateParams {
3054 preferences: state.config.surface_encoders.clone(),
3055 vaapi_device: state.config.vaapi_device.clone(),
3056 quality,
3057 verbose: state.config.verbose,
3058 codec_support,
3059 chroma: state.config.chroma,
3060 },
3061 });
3062 continue;
3063 }
3064
3065 if client.vulkan_video_surfaces.contains_key(&sid) {
3069 if work.needs_keyframe {
3070 pending_vulkan_keyframe_requests.push(sid as u32);
3071 }
3072 client.skip_vulkan_await_count =
3073 client.skip_vulkan_await_count.saturating_add(1);
3074 let now_inst = Instant::now();
3075 if now_inst.duration_since(client.last_skip_log).as_secs_f32() > 5.0 {
3076 client.last_skip_log = now_inst;
3077 eprintln!(
3078 "[encode-skip] cid={} sid={sid} reason=vulkan_await \
3079 (compositor not producing PixelData::Encoded) count={}",
3080 work.cid, client.skip_vulkan_await_count,
3081 );
3082 }
3083 continue;
3084 }
3085
3086 let encoder = client
3087 .surface_subs
3088 .get_mut(&sid)
3089 .and_then(|s| s.encoder.take())
3090 .unwrap();
3091 client.surface_subs.entry(sid).or_default().encode_in_flight = true;
3092 let needs_kf = work.needs_keyframe || needs_new_encoder;
3093 encoded_client_surfaces.insert((work.cid, sid));
3094 encode_dispatched_surfaces.insert(sid);
3095 encode_jobs.push(EncodeJob {
3096 cid: work.cid,
3097 sid,
3098 px_w: enc_w,
3099 px_h: enc_h,
3100 pixels,
3101 needs_keyframe: needs_kf,
3102 encoder,
3103 generation: px_gen,
3104 timestamp_ms: px_timestamp_ms,
3105 });
3106 }
3107 }
3108
3109 if (!pending_vulkan_encoder_setups.is_empty()
3111 || !pending_vulkan_keyframe_requests.is_empty())
3112 && let Some(cs) = sess.compositor.as_ref()
3113 {
3114 for setup in pending_vulkan_encoder_setups {
3115 eprintln!(
3116 "[vulkan-video] sending SetVulkanEncoder sid={} codec={} {}x{} qp={}",
3117 setup.surface_id, setup.codec, setup.width, setup.height, setup.qp,
3118 );
3119 let _ = cs.handle.command_tx.send(
3120 blit_compositor::CompositorCommand::SetVulkanEncoder {
3121 surface_id: setup.surface_id,
3122 codec: setup.codec,
3123 qp: setup.qp,
3124 width: setup.width,
3125 height: setup.height,
3126 },
3127 );
3128 }
3129 for surface_id in pending_vulkan_keyframe_requests {
3130 let _ = cs
3131 .handle
3132 .command_tx
3133 .send(blit_compositor::CompositorCommand::RequestVulkanKeyframe { surface_id });
3134 }
3135 cs.handle.wake();
3136 }
3137
3138 for work in &client_work {
3143 if let Some(client) = sess.clients.get_mut(&work.cid) {
3144 let interval = surface_send_interval(client);
3145 for &sid in &work.subs {
3146 if encoded_client_surfaces.contains(&(work.cid, sid)) {
3147 let deadline = client
3148 .surface_subs
3149 .entry(sid)
3150 .or_default()
3151 .next_send_at
3152 .get_or_insert(now);
3153 advance_deadline(deadline, now, interval);
3154 }
3155 }
3156 }
3157 }
3158 }
3159
3160 if !encode_jobs.is_empty() {
3161 let state2 = state.clone();
3164 tokio::spawn(async move {
3165 let job_ids: Vec<(u64, u16)> = encode_jobs.iter().map(|j| (j.cid, j.sid)).collect();
3169
3170 let handles: Vec<_> = encode_jobs
3171 .into_iter()
3172 .map(|job| {
3173 tokio::task::spawn_blocking(move || {
3174 let mut encoder = job.encoder;
3175 if job.needs_keyframe {
3176 encoder.request_keyframe();
3177 }
3178 let nal_data = encoder.encode_pixels(&job.pixels);
3179 let codec_flag = encoder.codec_flag();
3180 EncodeResult {
3181 cid: job.cid,
3182 sid: job.sid,
3183 px_w: job.px_w,
3184 px_h: job.px_h,
3185 generation: job.generation,
3186 encoder,
3187 nal_data,
3188 codec_flag,
3189 timestamp_ms: job.timestamp_ms,
3190 }
3191 })
3192 })
3193 .collect();
3194
3195 const ENCODE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
3198
3199 let mut results = Vec::with_capacity(handles.len());
3200 let mut failed: Vec<(u64, u16)> = Vec::new();
3201 for (i, h) in handles.into_iter().enumerate() {
3202 let wrapper =
3208 tokio::spawn(async move { tokio::time::timeout(ENCODE_TIMEOUT, h).await });
3209 match wrapper.await {
3210 Ok(Ok(Ok(r))) => results.push(r),
3211 Ok(Ok(Err(_join_err))) => {
3212 let (cid, sid) = job_ids[i];
3214 eprintln!("[surface-encoder] encode task panicked: cid={cid} sid={sid}",);
3215 failed.push(job_ids[i]);
3216 }
3217 Ok(Err(_timeout)) => {
3218 let (cid, sid) = job_ids[i];
3222 eprintln!(
3223 "[surface-encoder] encode timed out ({}s): cid={cid} sid={sid}",
3224 ENCODE_TIMEOUT.as_secs(),
3225 );
3226 failed.push(job_ids[i]);
3227 }
3228 Err(_join_err) => {
3229 eprintln!("[surface-encoder] runtime shutting down, aborting delivery");
3231 return;
3232 }
3233 }
3234 }
3235
3236 let mut sess = state2.session.lock().await;
3238 let now = Instant::now();
3239 let mut local_encodes = 0u32;
3240 let mut local_encode_bytes = 0u64;
3241 let mut local_frames_sent = 0u32;
3242
3243 for (cid, sid) in failed {
3247 if let Some(client) = sess.clients.get_mut(&cid) {
3248 if let Some(s) = client.surface_subs.get_mut(&sid) {
3249 s.encode_in_flight = false;
3250 }
3251 client.surface_needs_keyframe = true;
3257 }
3258 }
3259
3260 for result in results {
3261 let expected_dims: Option<(u32, u32)> = sess
3269 .compositor
3270 .as_ref()
3271 .and_then(|cs| cs.last_pixels.get(&result.sid))
3272 .map(|lp| (lp.width, lp.height));
3273 let dims_match =
3274 expected_dims.is_some_and(|d| result.encoder.source_dimensions() == d);
3275
3276 if let Some(client) = sess.clients.get_mut(&result.cid) {
3277 let state = client.surface_subs.entry(result.sid).or_default();
3278 state.encode_in_flight = false;
3279 let invalidated = std::mem::replace(&mut state.encoder_invalidated, false);
3280 if dims_match && !invalidated {
3281 state.encoder = Some(result.encoder);
3282 }
3283 state.last_encoded_gen = Some(result.generation);
3286 }
3287
3288 let Some((nal_data, is_keyframe)) = result.nal_data else {
3289 if let Some(client) = sess.clients.get_mut(&result.cid) {
3290 let state = client.surface_subs.entry(result.sid).or_default();
3291 state.nal_none_streak += 1;
3292 let streak = state.nal_none_streak;
3293 if streak == 10 {
3294 state.encoder = None;
3295 state.nal_none_latched_at = Some(now);
3296 client.surface_needs_keyframe = true;
3297 eprintln!(
3298 "[encode] nal_data=None x{streak} sid={} cid={} {}x{} — dropping encoder, backing off retry",
3299 result.sid, result.cid, result.px_w, result.px_h,
3300 );
3301 } else if streak < 10 {
3302 eprintln!(
3303 "[encode] nal_data=None sid={} cid={} {}x{}",
3304 result.sid, result.cid, result.px_w, result.px_h,
3305 );
3306 }
3307 }
3309 continue;
3310 };
3311 if let Some(client) = sess.clients.get_mut(&result.cid)
3313 && let Some(s) = client.surface_subs.get_mut(&result.sid)
3314 {
3315 s.nal_none_streak = 0;
3316 }
3317
3318 {
3319 static EC: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
3320 let n = EC.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3321 if n < 5 || n.is_multiple_of(1000) {
3322 eprintln!(
3323 "[encode #{n}] sid={} {}x{} kf={is_keyframe} bytes={}",
3324 result.sid,
3325 result.px_w,
3326 result.px_h,
3327 nal_data.len(),
3328 );
3329 }
3330 }
3331
3332 local_encodes += 1;
3333 local_encode_bytes += nal_data.len() as u64;
3334
3335 let flags = result.codec_flag
3336 | if is_keyframe {
3337 SURFACE_FRAME_FLAG_KEYFRAME
3338 } else {
3339 0
3340 };
3341 let msg = msg_surface_frame(
3342 result.sid,
3343 result.timestamp_ms,
3344 flags,
3345 result.px_w as u16,
3346 result.px_h as u16,
3347 &nal_data,
3348 );
3349 let bytes = msg.len();
3350
3351 let Some(client) = sess.clients.get_mut(&result.cid) else {
3352 continue;
3353 };
3354
3355 match send_outbox(client, msg) {
3362 Err(_e) => {
3363 client.surface_needs_keyframe = true;
3366 }
3367 Ok(()) => {
3368 client.surface_inflight_frames.push_back(InFlightFrame {
3372 sent_at: now,
3373 bytes,
3374 paced: true,
3375 });
3376 if !is_keyframe {
3388 client.avg_surface_frame_bytes = ewma_with_direction(
3389 client.avg_surface_frame_bytes,
3390 bytes as f32,
3391 0.5,
3392 0.125,
3393 );
3394 } else if client.avg_surface_frame_bytes <= 16_384.0 {
3395 client.avg_surface_frame_bytes = (bytes as f32 / 4.0).max(4_096.0);
3404 } else {
3405 client.avg_surface_frame_bytes = ewma_with_direction(
3408 client.avg_surface_frame_bytes,
3409 bytes as f32,
3410 0.05,
3411 0.05,
3412 );
3413 }
3414 client.frames_sent = client.frames_sent.wrapping_add(1);
3415 local_frames_sent += 1;
3416 if client.surface_needs_keyframe && is_keyframe {
3417 client.surface_needs_keyframe = false;
3418 }
3419 if let Some(s) = client.surface_subs.get_mut(&result.sid) {
3420 s.burst_remaining = s.burst_remaining.saturating_sub(1);
3421 }
3422 }
3423 }
3424 }
3425 sess.surface_encodes += local_encodes;
3426 sess.surface_encode_bytes += local_encode_bytes;
3427 sess.surface_frames_sent += local_frames_sent;
3428 drop(sess);
3429 state2.delivery_notify.notify_one();
3431 });
3432 }
3433
3434 if !create_jobs.is_empty() {
3435 let state2 = state.clone();
3443 tokio::spawn(async move {
3444 let job_ids: Vec<(u64, u16)> = create_jobs.iter().map(|j| (j.cid, j.sid)).collect();
3447
3448 let handles: Vec<_> = create_jobs
3449 .into_iter()
3450 .map(|job| {
3451 tokio::task::spawn_blocking(move || {
3452 let params = job.params;
3453 let mut encoder = match SurfaceEncoder::new(
3454 ¶ms.preferences,
3455 job.px_w,
3456 job.px_h,
3457 ¶ms.vaapi_device,
3458 params.quality,
3459 params.verbose,
3460 params.codec_support,
3461 params.chroma,
3462 ) {
3463 Ok(enc) => enc,
3464 Err(err) => {
3465 if params.verbose {
3466 eprintln!(
3467 "[surface-encoder] cid={} sid={} {}x{}: {err}",
3468 job.cid, job.sid, job.px_w, job.px_h,
3469 );
3470 }
3471 return CreateResult {
3472 cid: job.cid,
3473 sid: job.sid,
3474 encoder: None,
3475 fresh: None,
3476 };
3477 }
3478 };
3479
3480 #[cfg(target_os = "linux")]
3481 let external_bufs = {
3482 {
3483 let drm_fd = encoder.drm_fd_raw();
3484 let count = encoder.gbm_buffers().len();
3485 if count > 0 {
3486 encoder.allocate_nv12_buffers(drm_fd, count);
3487 }
3488 }
3489 let gbm_bufs = encoder.gbm_buffers();
3490 if gbm_bufs.is_empty() {
3491 Vec::new()
3492 } else {
3493 let nv12_bufs = encoder.gbm_nv12_buffers();
3494 let (enc_w, enc_h) = encoder.encoder_dimensions();
3495 let bufs: Result<Vec<_>, std::io::Error> = gbm_bufs
3496 .iter()
3497 .enumerate()
3498 .map(|(i, b)| {
3499 let nv12 = nv12_bufs.get(i);
3500 Ok(blit_compositor::ExternalOutputBuffer {
3501 fd: std::sync::Arc::new(b.fd.try_clone()?),
3502 fourcc: 0x34325241,
3503 modifier: 0,
3504 stride: b.stride,
3505 offset: 0,
3506 width: b.width,
3507 height: b.height,
3508 va_surface_id: 0,
3509 va_display: 0,
3510 planes: vec![blit_compositor::ExternalOutputPlane {
3511 offset: 0,
3512 pitch: b.stride,
3513 }],
3514 nv12_fd: nv12.map(|n| n.fd.clone()),
3515 nv12_stride: nv12.map_or(0, |n| n.stride),
3516 nv12_uv_offset: nv12.map_or(0, |n| n.uv_offset),
3517 nv12_modifier: nv12.map_or(0, |n| n.modifier),
3518 nv12_width: enc_w,
3519 nv12_height: enc_h,
3520 })
3521 })
3522 .collect();
3523 match bufs {
3524 Ok(b) => b,
3525 Err(e) => {
3526 eprintln!("[encode] dup gbm fd failed: {e}");
3527 Vec::new()
3528 }
3529 }
3530 }
3531 };
3532 let fresh = FreshEncoder {
3533 name: encoder.encoder_name(),
3534 codec_string: encoder.webcodecs_codec_string(),
3535 #[cfg(target_os = "linux")]
3536 external_bufs,
3537 };
3538 CreateResult {
3539 cid: job.cid,
3540 sid: job.sid,
3541 encoder: Some(encoder),
3542 fresh: Some(fresh),
3543 }
3544 })
3545 })
3546 .collect();
3547
3548 const CREATE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
3549 let mut results: Vec<CreateResult> = Vec::with_capacity(handles.len());
3550 let mut failed: Vec<(u64, u16)> = Vec::new();
3551 for (i, h) in handles.into_iter().enumerate() {
3552 let wrapper =
3553 tokio::spawn(async move { tokio::time::timeout(CREATE_TIMEOUT, h).await });
3554 match wrapper.await {
3555 Ok(Ok(Ok(r))) => results.push(r),
3556 Ok(Ok(Err(_))) | Ok(Err(_)) => {
3557 let (cid, sid) = job_ids[i];
3558 eprintln!("[surface-encoder] create task failed: cid={cid} sid={sid}",);
3559 failed.push(job_ids[i]);
3560 }
3561 Err(_) => return,
3562 }
3563 }
3564
3565 let mut sess = state2.session.lock().await;
3566 let now = Instant::now();
3567
3568 for (cid, sid) in failed {
3571 if let Some(client) = sess.clients.get_mut(&cid)
3572 && let Some(s) = client.surface_subs.get_mut(&sid)
3573 {
3574 s.creation_in_flight = false;
3575 s.nal_none_streak = 10;
3576 s.nal_none_latched_at = Some(now);
3577 }
3578 }
3579
3580 for result in results {
3581 let Some(encoder) = result.encoder else {
3582 if let Some(client) = sess.clients.get_mut(&result.cid)
3583 && let Some(s) = client.surface_subs.get_mut(&result.sid)
3584 {
3585 s.creation_in_flight = false;
3586 s.nal_none_streak = 10;
3587 s.nal_none_latched_at = Some(now);
3588 }
3589 continue;
3590 };
3591
3592 let fresh = result.fresh;
3596 #[cfg(target_os = "linux")]
3597 {
3598 if let Some(f) = &fresh
3599 && !f.external_bufs.is_empty()
3600 && let Some(cs) = sess.compositor.as_mut()
3601 {
3602 cs.last_pixels.remove(&result.sid);
3603 }
3604 }
3605 #[cfg(target_os = "linux")]
3606 let (fresh_meta, external_bufs) = match fresh {
3607 Some(f) => (Some((f.name, f.codec_string)), Some(f.external_bufs)),
3608 None => (None, None),
3609 };
3610 #[cfg(not(target_os = "linux"))]
3611 let fresh_meta = fresh.map(|f| (f.name, f.codec_string));
3612
3613 #[cfg(target_os = "linux")]
3614 if let Some(bufs) = external_bufs
3615 && !bufs.is_empty()
3616 && let Some(cs) = sess.compositor.as_mut()
3617 {
3618 let _ = cs.handle.command_tx.send(
3619 blit_compositor::CompositorCommand::SetExternalOutputBuffers {
3620 surface_id: result.sid as u32,
3621 buffers: bufs,
3622 },
3623 );
3624 cs.handle.wake();
3625 }
3626
3627 if let Some(client) = sess.clients.get_mut(&result.cid) {
3628 let state = client.surface_subs.entry(result.sid).or_default();
3629 state.creation_in_flight = false;
3630 let invalidated = std::mem::replace(&mut state.encoder_invalidated, false);
3631 if invalidated {
3632 continue;
3637 }
3638 state.encoder = Some(encoder);
3639 state.nal_none_streak = 0;
3640 state.nal_none_latched_at = None;
3641 if let Some((name, codec_string)) = fresh_meta {
3642 let enc_msg = msg_surface_encoder(result.sid, name, &codec_string);
3643 let _ = send_outbox(client, enc_msg);
3644 }
3645 }
3646 }
3647 drop(sess);
3648 state2.delivery_notify.notify_one();
3649 });
3650 }
3651
3652 {
3663 let mut wanted: HashSet<u16> = HashSet::new();
3669
3670 for &sid in &encode_dispatched_surfaces {
3674 wanted.insert(sid);
3675 }
3676 let mut blanket_requested = false;
3677 if let Some(cs) = sess.compositor.as_ref()
3682 && let Some(interval) = blanket_frame_interval(&sess)
3683 && now.duration_since(cs.last_blanket_frame_request) >= interval
3684 {
3685 for &sid in cs.surfaces.keys() {
3686 wanted.insert(sid);
3687 }
3688 blanket_requested = true;
3689 }
3690 for client in sess.clients.values() {
3691 if client.surface_subscriptions.is_empty() {
3698 continue;
3699 }
3700 for &sid in &client.surface_subscriptions {
3701 let (burst, deadline) = client.surface_subs.get(&sid).map_or((0, now), |s| {
3702 (s.burst_remaining, s.next_send_at.unwrap_or(now))
3703 });
3704 if deadline <= now || burst > 0 {
3705 wanted.insert(sid);
3706 } else {
3707 next_deadline = Some(match next_deadline {
3708 Some(existing) => existing.min(deadline),
3709 None => deadline,
3710 });
3711 }
3712 }
3713 }
3714
3715 if let Some(cs) = sess.compositor.as_mut() {
3716 if blanket_requested {
3717 cs.last_blanket_frame_request = now;
3718 }
3719
3720 const MIN_REQUEST_INTERVAL: Duration = Duration::from_millis(1);
3727 let mut sent_any = false;
3728 for sid in &wanted {
3729 let dominated = cs
3730 .last_frame_request
3731 .get(sid)
3732 .is_some_and(|&t| now.duration_since(t) < MIN_REQUEST_INTERVAL);
3733 if !dominated {
3734 cs.last_frame_request.insert(*sid, now);
3735 let _ = cs
3736 .handle
3737 .command_tx
3738 .send(CompositorCommand::RequestFrame { surface_id: *sid });
3739 sent_any = true;
3740 }
3741 }
3742 if sent_any {
3743 cs.handle.wake();
3744 }
3745 }
3746 }
3747
3748 drop(sess);
3753 tokio::task::yield_now().await;
3754 sess = state.session.lock().await;
3755
3756 let max_fps = sess
3757 .clients
3758 .values()
3759 .map(browser_pacing_fps)
3760 .fold(1.0_f32, f32::max);
3761 let title_interval = Duration::from_secs_f64(1.0 / max_fps as f64);
3762 let ids: Vec<u16> = sess.ptys.keys().copied().collect();
3763 for &id in &ids {
3764 let Some(pty) = sess.ptys.get_mut(&id) else {
3765 continue;
3766 };
3767 if pty.driver.take_title_dirty() {
3768 pty.mark_dirty();
3769 pty.title_pending = true;
3770 }
3771 if pty.title_pending && now.duration_since(pty.last_title_send) >= title_interval {
3772 let msg = {
3773 let title_bytes = pty.driver.title().as_bytes();
3774 let mut msg = Vec::with_capacity(3 + title_bytes.len());
3775 msg.push(S2C_TITLE);
3776 msg.extend_from_slice(&id.to_le_bytes());
3777 msg.extend_from_slice(title_bytes);
3778 msg
3779 };
3780 pty.last_title_send = now;
3781 pty.title_pending = false;
3782 sess.send_to_all(&msg);
3783 }
3784 }
3785
3786 let ptys_with_subscribers: HashSet<u16> = sess
3799 .clients
3800 .values()
3801 .flat_map(|c| c.subscriptions.iter().copied())
3802 .collect();
3803 let mut eof_ptys: Vec<u16> = Vec::with_capacity(ids.len());
3804 for &id in &ids {
3805 let Some(pty) = sess.ptys.get_mut(&id) else {
3806 continue;
3807 };
3808 let has_subscriber = ptys_with_subscribers.contains(&id);
3809 loop {
3810 if has_subscriber && pty.ready_frames.len() >= READY_FRAME_QUEUE_CAP {
3811 break;
3812 }
3813 let Ok(input) = pty.byte_rx.try_recv() else {
3814 break;
3815 };
3816 match input {
3817 PtyInput::Data(data) => {
3818 pty::respond_to_queries(
3819 &pty.handle,
3820 &data,
3821 pty.driver.size(),
3822 pty.driver.cursor_position(),
3823 );
3824 pty.driver.process(&data);
3825 pty.mark_dirty();
3826 }
3827 PtyInput::SyncBoundary { before } => {
3828 if !before.is_empty() {
3829 pty::respond_to_queries(
3830 &pty.handle,
3831 &before,
3832 pty.driver.size(),
3833 pty.driver.cursor_position(),
3834 );
3835 pty.driver.process(&before);
3836 pty.mark_dirty();
3837 }
3838 if !pty.driver.synced_output() {
3839 let frame = take_snapshot(pty);
3840 enqueue_ready_frame(&mut pty.ready_frames, frame);
3841 pty.clear_dirty();
3842 }
3843 }
3844 PtyInput::Eof => {
3845 eof_ptys.push(id);
3846 }
3847 }
3848 }
3849 }
3850 drop(sess);
3852 for id in eof_ptys {
3853 tokio::time::sleep(Duration::from_millis(50)).await;
3854 cleanup_pty_internal(id, state).await;
3855 }
3856 let mut sess = state.session.lock().await;
3857
3858 let needful_ptys: HashSet<u16> = sess
3862 .clients
3863 .values()
3864 .flat_map(|c| {
3865 let reserve_preview_slot = client_has_due_preview(&sess, c, now);
3866 c.subscriptions.iter().copied().filter(move |pid| {
3867 let scrolled = c.scroll_offsets.get(pid).copied().unwrap_or(0) > 0;
3868 if Some(*pid) == c.lead {
3869 !scrolled && can_send_frame(c, now, reserve_preview_slot)
3870 } else {
3871 !scrolled && can_send_preview(c, *pid, now)
3872 }
3873 })
3874 })
3875 .collect();
3876
3877 let mut snapshots: HashMap<u16, FrameState> = HashMap::new();
3878 for &id in &ids {
3879 let Some(pty) = sess.ptys.get_mut(&id) else {
3880 continue;
3881 };
3882 if needful_ptys.contains(&id)
3883 && let Some(frame) = pty.ready_frames.pop_front()
3884 {
3885 snapshots.insert(id, frame);
3886 sess.tick_snaps += 1;
3887 continue;
3888 }
3889 if !should_snapshot_pty(
3890 pty.dirty,
3891 needful_ptys.contains(&id),
3892 pty.driver.synced_output(),
3893 ) {
3894 continue;
3895 }
3896 snapshots.insert(id, take_snapshot(pty));
3900 pty.clear_dirty();
3901 sess.tick_snaps += 1;
3902 }
3903
3904 let client_ids: Vec<u64> = sess.clients.keys().copied().collect();
3905 for cid in client_ids {
3906 if let Some(c) = sess.clients.get_mut(&cid) {
3912 if c.inflight_bytes == 0 && c.min_rtt_ms > 0.0 && c.rtt_ms > c.min_rtt_ms {
3913 c.rtt_ms = (c.rtt_ms * 0.99 + c.min_rtt_ms * 0.01).max(c.min_rtt_ms);
3914 }
3915 if c.last_metrics_update.elapsed() > Duration::from_secs(1) {
3918 c.browser_backlog_frames = 0;
3919 c.browser_ack_ahead_frames = 0;
3920 }
3921 }
3922 let (
3923 lead,
3924 subscriptions,
3925 scrolled_ptys,
3926 can_send_lead,
3927 lead_has_window,
3928 any_send_window,
3929 lead_deadline,
3930 ) = {
3931 let Some(c) = sess.clients.get(&cid) else {
3932 continue;
3933 };
3934 let reserve_preview_slot = client_has_due_preview(&sess, c, now);
3935 (
3936 c.lead,
3937 c.subscriptions.iter().copied().collect::<Vec<_>>(),
3938 c.scroll_offsets
3939 .iter()
3940 .map(|(&k, &v)| (k, v))
3941 .collect::<Vec<_>>(),
3942 can_send_frame(c, now, reserve_preview_slot),
3943 lead_window_open(c, reserve_preview_slot),
3944 lead_window_open(c, reserve_preview_slot) || window_open(c),
3945 c.next_send_at,
3946 )
3947 };
3948
3949 if subscriptions.is_empty() {
3950 continue;
3951 }
3952
3953 for &(scroll_pid, scroll_offset) in &scrolled_ptys {
3955 if scroll_offset == 0 {
3956 continue;
3957 }
3958 let is_lead = lead == Some(scroll_pid);
3959 let can_send = if is_lead { can_send_lead } else { true };
3960 if can_send {
3961 let prev_frame = {
3962 let Some(c) = sess.clients.get(&cid) else {
3963 continue;
3964 };
3965 c.scroll_caches
3966 .get(&scroll_pid)
3967 .cloned()
3968 .unwrap_or_default()
3969 };
3970 let outcome = if let Some(pty) = sess.ptys.get_mut(&scroll_pid) {
3971 if let Some((msg, new_frame)) =
3972 build_scrollback_update(pty, scroll_pid, scroll_offset, &prev_frame)
3973 {
3974 let Some(c) = sess.clients.get_mut(&cid) else {
3975 break;
3976 };
3977 let bytes = msg.len();
3978 if send_outbox(c, msg).is_ok() {
3979 c.scroll_caches.insert(scroll_pid, new_frame);
3980 record_send(c, bytes, now, is_lead);
3981 c.frames_sent += 1;
3982 SendOutcome::Sent
3983 } else {
3984 SendOutcome::Backpressured
3985 }
3986 } else {
3987 SendOutcome::NoChange
3988 }
3989 } else {
3990 SendOutcome::NoChange
3991 };
3992 match outcome {
3993 SendOutcome::Sent => {}
3994 SendOutcome::Backpressured => {
3995 if let Some(pty) = sess.ptys.get_mut(&scroll_pid) {
3996 pty.mark_dirty();
3997 }
3998 }
3999 SendOutcome::NoChange => {}
4000 }
4001 } else if is_lead && lead_has_window {
4002 next_deadline = Some(match next_deadline {
4003 Some(existing) => existing.min(lead_deadline),
4004 None => lead_deadline,
4005 });
4006 }
4007 }
4008
4009 let lead_scroll_offset = lead
4010 .and_then(|pid| {
4011 scrolled_ptys
4012 .iter()
4013 .find(|&&(k, _)| k == pid)
4014 .map(|&(_, v)| v)
4015 })
4016 .unwrap_or(0);
4017
4018 if let Some(pid) = lead {
4019 if lead_scroll_offset == 0 && can_send_lead {
4020 if let Some(cur) = snapshots.get(&pid).cloned() {
4021 let previous = sess
4022 .clients
4023 .get(&cid)
4024 .and_then(|c| c.last_sent.get(&pid).cloned())
4025 .unwrap_or_default();
4026 drop(sess);
4027 let msg = build_update_msg(pid, &cur, &previous);
4028 sess = state.session.lock().await;
4029 let Some(c) = sess.clients.get_mut(&cid) else {
4030 continue;
4031 };
4032 match try_send_update(c, pid, cur, msg, now, true) {
4033 SendOutcome::Sent => {}
4034 SendOutcome::Backpressured => {
4035 if let Some(pty) = sess.ptys.get_mut(&pid) {
4036 pty.mark_dirty();
4037 }
4038 }
4039 SendOutcome::NoChange => {}
4040 }
4041 } else {
4042 let has_pending = sess
4043 .ptys
4044 .get(&pid)
4045 .map(pty_has_visual_update)
4046 .unwrap_or(false);
4047 let _ = has_pending;
4048 }
4049 } else {
4050 let has_pending = sess
4051 .ptys
4052 .get(&pid)
4053 .map(pty_has_visual_update)
4054 .unwrap_or(false);
4055 if has_pending && lead_has_window {
4056 next_deadline = Some(match next_deadline {
4057 Some(existing) => existing.min(lead_deadline),
4058 None => lead_deadline,
4059 });
4060 }
4061 }
4062 }
4063
4064 if !any_send_window {
4065 continue;
4066 }
4067
4068 let mut preview_ids = subscriptions;
4069 preview_ids.retain(|pid| Some(*pid) != lead);
4070 preview_ids.sort_unstable();
4071
4072 for pid in preview_ids {
4073 let (preview_can_send, preview_due_at, preview_has_window) =
4074 match sess.clients.get(&cid) {
4075 Some(c) => (
4076 can_send_preview(c, pid, now),
4077 preview_deadline(c, pid, now),
4078 window_open(c),
4079 ),
4080 None => (false, now, false),
4081 };
4082 if !preview_has_window {
4083 break;
4084 }
4085 if !preview_can_send {
4086 let has_pending = sess
4087 .ptys
4088 .get(&pid)
4089 .map(pty_has_visual_update)
4090 .unwrap_or(false);
4091 if has_pending && preview_due_at > now {
4096 next_deadline = Some(match next_deadline {
4097 Some(existing) => existing.min(preview_due_at),
4098 None => preview_due_at,
4099 });
4100 }
4101 continue;
4102 }
4103 let Some(cur) = snapshots.get(&pid) else {
4104 let has_pending = sess
4105 .ptys
4106 .get(&pid)
4107 .map(pty_has_visual_update)
4108 .unwrap_or(false);
4109 let _ = has_pending;
4110 continue;
4111 };
4112 let cur = cur.clone();
4113 let previous = sess
4114 .clients
4115 .get(&cid)
4116 .and_then(|c| c.last_sent.get(&pid).cloned())
4117 .unwrap_or_default();
4118 drop(sess);
4119 let msg = build_update_msg(pid, &cur, &previous);
4120 sess = state.session.lock().await;
4121 let Some(c) = sess.clients.get_mut(&cid) else {
4122 break;
4123 };
4124 match try_send_update(c, pid, cur, msg, now, false) {
4125 SendOutcome::Sent => {
4126 record_preview_send(c, pid, now);
4127 }
4128 SendOutcome::Backpressured => {
4129 if let Some(pty) = sess.ptys.get_mut(&pid) {
4130 pty.mark_dirty();
4131 }
4132 break;
4133 }
4134 SendOutcome::NoChange => {}
4135 }
4136 }
4137 }
4138
4139 #[cfg(target_os = "linux")]
4163 let audio_restart_bitrate: i32 = sess
4164 .clients
4165 .values()
4166 .filter(|c| c.audio_subscribed)
4167 .map(|c| c.audio_bitrate_kbps)
4168 .max()
4169 .map(|kbps| kbps as i32 * 1000)
4170 .unwrap_or(0);
4171 #[cfg(target_os = "linux")]
4172 if let Some(ref mut cs) = sess.compositor {
4173 let pipeline_dead = cs.audio_pipeline.as_mut().is_some_and(|ap| !ap.is_alive());
4174 if pipeline_dead {
4175 const RESTART_COOLDOWN: Duration = Duration::from_secs(5);
4176 let can_restart = cs
4177 .last_audio_restart
4178 .is_none_or(|t| now.duration_since(t) >= RESTART_COOLDOWN);
4179 if can_restart {
4180 cs.last_audio_restart = Some(now);
4181 cs.audio_pipeline = None;
4184 let runtime_dir = std::path::Path::new(&cs.handle.socket_name)
4185 .parent()
4186 .unwrap_or(std::path::Path::new("/tmp"));
4187 let session_id = cs.audio_session_id;
4188 let epoch = cs.created_at;
4189 let verbose = state.config.verbose;
4190 let broadcast = cs.audio_broadcast.clone();
4194 eprintln!("[audio] pipeline died, restarting...");
4195 let pipeline = tokio::task::block_in_place(|| {
4196 audio::AudioPipeline::spawn(
4197 runtime_dir,
4198 session_id,
4199 audio_restart_bitrate,
4200 verbose,
4201 epoch,
4202 broadcast,
4203 )
4204 });
4205 match pipeline {
4206 Ok(p) => {
4207 eprintln!(
4208 "[audio] pipeline restarted, PULSE_SERVER={}",
4209 p.pulse_server_path(),
4210 );
4211 cs.audio_pipeline = Some(p);
4212 }
4213 Err(e) => {
4214 eprintln!("[audio] failed to restart pipeline: {e}");
4215 }
4216 }
4217 }
4218 }
4219 }
4220
4221 if let Some(interval) = blanket_frame_interval(&sess) {
4227 let blanket_deadline = now + interval;
4228 next_deadline = Some(next_deadline.map_or(blanket_deadline, |d| d.min(blanket_deadline)));
4229 }
4230
4231 TickOutcome { next_deadline }
4232}
4233
4234async fn handle_client<S: AsyncRead + AsyncWrite + Unpin + Send + 'static>(
4235 stream: S,
4236 state: AppState,
4237) {
4238 let config = &state.config;
4239 let notify_for_compositor = {
4240 let n = state.delivery_notify.clone();
4241 Arc::new(move || n.notify_one()) as Arc<dyn Fn() + Send + Sync>
4242 };
4243 let (mut reader, mut writer) = tokio::io::split(stream);
4244
4245 let (out_tx, mut out_rx) = mpsc::unbounded_channel::<Vec<u8>>();
4246 let (audio_tx, mut audio_rx) = mpsc::unbounded_channel::<Vec<u8>>();
4247 let outbox_frame_counter = Arc::new(AtomicUsize::new(0));
4248 let outbox_byte_counter = Arc::new(AtomicUsize::new(0));
4249 let sender_outbox_queued_frames = outbox_frame_counter.clone();
4250 let sender_outbox_queued_bytes = outbox_byte_counter.clone();
4251 let sender = tokio::spawn(async move {
4252 let audio_debug = std::env::var_os("BLIT_AUDIO_DEBUG").is_some();
4253 let mut audio_window_start = Instant::now();
4254 let mut last_audio_pick_at = Instant::now();
4255 let mut audio_sends_in_window: u32 = 0;
4256 let mut max_audio_pick_gap: u32 = 0;
4257 let mut max_audio_write_ms: u32 = 0;
4258 loop {
4259 while let Ok(audio_msg) = audio_rx.try_recv() {
4262 if !write_frame(&mut writer, &audio_msg).await {
4263 return;
4264 }
4265 if audio_debug {
4266 audio_sends_in_window += 1;
4267 let now = Instant::now();
4268 let pick_gap = now.duration_since(last_audio_pick_at).as_millis() as u32;
4269 last_audio_pick_at = now;
4270 if pick_gap > max_audio_pick_gap {
4271 max_audio_pick_gap = pick_gap;
4272 }
4273 }
4274 }
4275
4276 let msg = tokio::select! {
4280 biased;
4281 msg = audio_rx.recv() => {
4282 match msg {
4284 Some(m) => {
4285 let audio_write_start = Instant::now();
4286 if !write_frame(&mut writer, &m).await {
4287 break;
4288 }
4289 if audio_debug {
4290 let now = Instant::now();
4291 audio_sends_in_window += 1;
4292 let pick_gap = now
4293 .duration_since(last_audio_pick_at)
4294 .as_millis() as u32;
4295 last_audio_pick_at = now;
4296 let write_ms =
4297 now.duration_since(audio_write_start).as_millis() as u32;
4298 if pick_gap > max_audio_pick_gap {
4299 max_audio_pick_gap = pick_gap;
4300 }
4301 if write_ms > max_audio_write_ms {
4302 max_audio_write_ms = write_ms;
4303 }
4304 if now.duration_since(audio_window_start)
4305 >= Duration::from_secs(1)
4306 {
4307 eprintln!(
4308 "[sender audio] writes={} max_pick_gap={}ms max_write={}ms",
4309 audio_sends_in_window,
4310 max_audio_pick_gap,
4311 max_audio_write_ms,
4312 );
4313 audio_sends_in_window = 0;
4314 max_audio_pick_gap = 0;
4315 max_audio_write_ms = 0;
4316 audio_window_start = now;
4317 }
4318 }
4319 continue;
4320 }
4321 None => break,
4322 }
4323 }
4324 msg = out_rx.recv() => msg,
4325 };
4326
4327 match msg {
4332 Some(m) => {
4333 let bytes = m.len();
4334 let write_start = Instant::now();
4335 let wrote = write_frame_interleaved(&mut writer, &m, &mut audio_rx).await;
4336 let write_elapsed = write_start.elapsed();
4337 if write_elapsed.as_millis() > 30 {
4342 eprintln!(
4343 "[sender] slow write: bytes={bytes} elapsed={}ms wrote={wrote}",
4344 write_elapsed.as_millis(),
4345 );
4346 }
4347 mark_outbox_drained(
4348 &sender_outbox_queued_frames,
4349 &sender_outbox_queued_bytes,
4350 bytes,
4351 );
4352 if !wrote {
4353 break;
4354 }
4355 }
4356 None => break,
4357 }
4358 }
4359 });
4360 let client_id;
4361
4362 {
4363 let mut sess = state.session.lock().await;
4364 client_id = sess.next_client_id;
4365 sess.next_client_id += 1;
4366 sess.clients.insert(
4367 client_id,
4368 ClientState {
4369 tx: out_tx,
4370 outbox_queued_frames: outbox_frame_counter,
4371 outbox_queued_bytes: outbox_byte_counter,
4372 audio_tx,
4373 lead: None,
4374 subscriptions: HashSet::new(),
4375 surface_subscriptions: HashSet::new(),
4376 audio_subscribed: false,
4377 #[cfg(target_os = "linux")]
4378 audio_bitrate_kbps: 0,
4379 view_sizes: HashMap::new(),
4380 scroll_offsets: HashMap::new(),
4381 scroll_caches: HashMap::new(),
4382 last_sent: HashMap::new(),
4383 preview_next_send_at: HashMap::new(),
4384 rtt_ms: 50.0,
4385 min_rtt_ms: 0.0,
4386 display_fps: 60.0,
4387 delivery_bps: 262_144.0,
4392 goodput_bps: 262_144.0,
4393 goodput_jitter_bps: 0.0,
4394 max_goodput_jitter_bps: 0.0,
4395 last_goodput_sample_bps: 0.0,
4396 avg_frame_bytes: 1_024.0,
4397 avg_paced_frame_bytes: 1_024.0,
4398 avg_preview_frame_bytes: 1_024.0,
4399 avg_surface_frame_bytes: 8_192.0,
4400 inflight_bytes: 0,
4401 inflight_frames: VecDeque::new(),
4402 next_send_at: Instant::now(),
4403 probe_frames: 0.0,
4404 frames_sent: 0,
4405 acks_recv: 0,
4406 acked_bytes_since_log: 0,
4407 browser_backlog_frames: 0,
4408 browser_ack_ahead_frames: 0,
4409 browser_apply_ms: 0.0,
4410 last_metrics_update: Instant::now(),
4411 last_log: Instant::now(),
4412 last_window_blocked_log: Instant::now(),
4413 last_skip_log: Instant::now(),
4414 skip_same_gen_count: 0,
4415 skip_in_flight_count: 0,
4416 skip_pacing_count: 0,
4417 skip_vulkan_await_count: 0,
4418 skip_no_subs_count: 0,
4419 skip_not_subbed_count: 0,
4420 skip_last_pixels_mismatch_count: 0,
4421 encode_loop_iters: 0,
4422 goodput_window_bytes: 0,
4423 goodput_window_start: Instant::now(),
4424 surface_subs: HashMap::new(),
4425 surface_needs_keyframe: true,
4426 surface_inflight_frames: VecDeque::new(),
4427 vulkan_video_surfaces: HashMap::new(),
4428 surface_view_sizes: HashMap::new(),
4429 surface_codec_support: 0,
4430 pressed_surface_keys: HashSet::new(),
4431 },
4432 );
4433 state.delivery_notify.notify_one();
4435 if let Some(c) = sess.clients.get(&client_id) {
4436 let mut features = FEATURE_CREATE_NONCE
4437 | FEATURE_RESTART
4438 | FEATURE_RESIZE_BATCH
4439 | FEATURE_COPY_RANGE
4440 | FEATURE_COMPOSITOR;
4441 #[cfg(target_os = "linux")]
4442 {
4443 let audio_disabled = std::env::var("BLIT_AUDIO")
4444 .map(|v| v == "0")
4445 .unwrap_or(false);
4446 if !audio_disabled && audio::pipewire_available() {
4447 features |= FEATURE_AUDIO;
4448 }
4449 }
4450 let _ = send_outbox(c, msg_hello(1, features));
4451 }
4452 let mut initial_msgs = Vec::with_capacity(2 + sess.ptys.len() * 2);
4453 if let Some(cs) = sess.compositor.as_ref() {
4458 for info in cs.surfaces.values() {
4459 let (w, h) = if info.width == 0 && info.height == 0 {
4462 cs.last_pixels
4463 .get(&info.surface_id)
4464 .map(|lp| (lp.width as u16, lp.height as u16))
4465 .unwrap_or((0, 0))
4466 } else {
4467 (info.width, info.height)
4468 };
4469 initial_msgs.push(msg_surface_created(
4470 info.surface_id,
4471 info.parent_id,
4472 w,
4473 h,
4474 &info.title,
4475 &info.app_id,
4476 ));
4477 if w > 0 && h > 0 {
4480 initial_msgs.push(msg_surface_resized(info.surface_id, w, h));
4481 }
4482 }
4483 }
4484 initial_msgs.push(sess.pty_list_msg());
4485 for (&id, pty) in &sess.ptys {
4486 let title = pty.driver.title();
4487 if !title.is_empty() {
4488 let title_bytes = title.as_bytes();
4489 let mut msg = Vec::with_capacity(3 + title_bytes.len());
4490 msg.push(S2C_TITLE);
4491 msg.extend_from_slice(&id.to_le_bytes());
4492 msg.extend_from_slice(title_bytes);
4493 initial_msgs.push(msg);
4494 }
4495 if pty.exited {
4496 initial_msgs.push(blit_remote::msg_exited(id, pty.exit_status));
4497 }
4498 }
4499 initial_msgs.push(vec![S2C_READY]);
4500 let tx = sess.clients.get(&client_id).map(|c| {
4501 (
4502 c.tx.clone(),
4503 c.outbox_queued_frames.clone(),
4504 c.outbox_queued_bytes.clone(),
4505 )
4506 });
4507 drop(sess);
4508 if let Some((tx, queued_frames, queued_bytes)) = tx {
4509 for msg in initial_msgs {
4510 if send_outbox_tracked(&tx, &queued_frames, &queued_bytes, msg).is_err() {
4511 break;
4512 }
4513 }
4514 }
4515 }
4516
4517 if state.config.verbose {
4518 eprintln!("client connected");
4519 }
4520
4521 while let Some(data) = read_frame(&mut reader).await {
4522 if data.is_empty() {
4523 continue;
4524 }
4525
4526 if data[0] == C2S_ACK {
4527 let mut sess = state.session.lock().await;
4528 if let Some(c) = sess.clients.get_mut(&client_id) {
4529 c.acks_recv += 1;
4530 record_ack(c);
4531 } else {
4532 continue;
4533 }
4534 maybe_log_pacing_metrics(&mut sess, client_id, config.verbose);
4535 nudge_delivery(&state);
4536 continue;
4537 }
4538
4539 if data[0] == C2S_PING {
4540 continue;
4544 }
4545
4546 if data[0] == C2S_DISPLAY_RATE && data.len() >= 3 {
4547 let fps = u16::from_le_bytes([data[1], data[2]]) as f32;
4548 if fps > 0.0 {
4549 let mut sess = state.session.lock().await;
4550 if let Some(c) = sess.clients.get_mut(&client_id) {
4551 c.display_fps = fps;
4552 }
4553 let max_fps = sess
4556 .clients
4557 .values()
4558 .map(|c| c.display_fps)
4559 .fold(0.0f32, f32::max);
4560 let mhz = (max_fps * 1000.0).round() as u32;
4561 if mhz > 0
4562 && let Some(cs) = &sess.compositor
4563 {
4564 let _ = cs
4565 .handle
4566 .command_tx
4567 .send(blit_compositor::CompositorCommand::SetRefreshRate { mhz });
4568 }
4569 }
4570 nudge_delivery(&state);
4571 continue;
4572 }
4573
4574 if data[0] == C2S_CLIENT_METRICS && data.len() >= 7 {
4575 let backlog_frames = u16::from_le_bytes([data[1], data[2]]);
4576 let ack_ahead_frames = u16::from_le_bytes([data[3], data[4]]);
4577 let apply_ms = u16::from_le_bytes([data[5], data[6]]) as f32 * 0.1;
4578 let mut sess = state.session.lock().await;
4579 if let Some(c) = sess.clients.get_mut(&client_id) {
4580 c.browser_backlog_frames = backlog_frames;
4581 c.browser_ack_ahead_frames = ack_ahead_frames;
4582 c.browser_apply_ms = apply_ms;
4583 c.last_metrics_update = Instant::now();
4584 }
4585 nudge_delivery(&state);
4586 continue;
4587 }
4588
4589 if data[0] == C2S_MOUSE && data.len() >= 9 {
4592 let pid = u16::from_le_bytes([data[1], data[2]]);
4593 let type_ = data[3];
4594 let button = data[4];
4595 let col = u16::from_le_bytes([data[5], data[6]]);
4596 let row = u16::from_le_bytes([data[7], data[8]]);
4597 let sess = state.session.lock().await;
4598 if let Some(pty) = sess.ptys.get(&pid) {
4599 let (echo, icanon) = pty.lflag_cache;
4600 if let Some(seq) = pty
4601 .driver
4602 .mouse_event(type_, button, col, row, echo, icanon)
4603 && let Some(&fd) = state.pty_fds.read().unwrap().get(&pid)
4604 {
4605 pty::pty_write_all(fd, &seq);
4606 }
4607 }
4608 continue;
4609 }
4610
4611 if data[0] == C2S_INPUT && data.len() >= 3 {
4612 let pid = u16::from_le_bytes([data[1], data[2]]);
4613 let mut need_nudge = false;
4614 {
4615 let mut sess = state.session.lock().await;
4616 if let Some(c) = sess.clients.get_mut(&client_id)
4617 && update_client_scroll_state(c, pid, 0)
4618 && let Some(pty) = sess.ptys.get_mut(&pid)
4619 {
4620 pty.mark_dirty();
4621 need_nudge = true;
4622 }
4623 }
4624 if need_nudge {
4625 nudge_delivery(&state);
4626 }
4627 if let Some(&fd) = state.pty_fds.read().unwrap().get(&pid) {
4628 pty::pty_write_all(fd, &data[3..]);
4629 }
4630 continue;
4631 }
4632
4633 if data[0] == C2S_SEARCH && data.len() >= 3 {
4634 let request_id = u16::from_le_bytes([data[1], data[2]]);
4635 let query = std::str::from_utf8(&data[3..]).unwrap_or("").trim();
4636 let mut sess = state.session.lock().await;
4637 let lead = sess.clients.get(&client_id).and_then(|c| c.lead);
4638 let mut ranked: Vec<SearchResultRow> = if query.is_empty() {
4639 Vec::new()
4640 } else {
4641 sess.ptys
4642 .iter()
4643 .filter_map(|(&pty_id, pty)| {
4644 pty.driver
4645 .search_result(query)
4646 .map(|result| SearchResultRow {
4647 pty_id,
4648 score: result.score,
4649 primary_source: result.primary_source,
4650 matched_sources: result.matched_sources,
4651 context: result.context,
4652 scroll_offset: result.scroll_offset,
4653 })
4654 })
4655 .collect()
4656 };
4657 ranked.sort_by(|a, b| {
4658 b.score
4659 .cmp(&a.score)
4660 .then_with(|| (Some(b.pty_id) == lead).cmp(&(Some(a.pty_id) == lead)))
4661 .then_with(|| a.pty_id.cmp(&b.pty_id))
4662 });
4663 if let Some(client) = sess.clients.get_mut(&client_id) {
4664 let _ = send_outbox(client, build_search_results_msg(request_id, &ranked));
4665 }
4666 continue;
4667 }
4668
4669 if data[0] == C2S_SURFACE_CAPTURE && data.len() >= 3 {
4670 let surface_id = u16::from_le_bytes([data[1], data[2]]);
4671 let format = data.get(3).copied().unwrap_or(CAPTURE_FORMAT_PNG);
4673 let quality = data.get(4).copied().unwrap_or(0);
4674 let scale_120 = if data.len() >= 7 {
4675 u16::from_le_bytes([data[5], data[6]])
4676 } else {
4677 0
4678 };
4679
4680 let mut reply_msg = vec![S2C_SURFACE_CAPTURE];
4681 reply_msg.extend_from_slice(&surface_id.to_le_bytes());
4682
4683 eprintln!("[capture] acquiring lock for surface {surface_id}");
4684 let (snapshot, command_tx) = {
4685 let sess = state.session.lock().await;
4686 eprintln!("[capture] lock acquired");
4687 let snap = sess
4688 .compositor
4689 .as_ref()
4690 .and_then(|cs| cs.last_pixels.get(&surface_id))
4691 .map(|lp| (lp.width, lp.height, lp.pixels.clone()));
4692 let cmd_tx = sess
4693 .compositor
4694 .as_ref()
4695 .map(|cs| cs.handle.command_tx.clone());
4696 (snap, cmd_tx)
4697 };
4698
4699 let mut captured: Option<(u32, u32, Vec<u8>)> = None;
4704 if let Some(ctx) = command_tx {
4705 captured = request_surface_capture_with_timeout(
4706 ctx,
4707 surface_id,
4708 scale_120,
4709 Duration::from_secs(5),
4710 )
4711 .await;
4712 }
4713
4714 if captured.is_none() {
4717 captured = snapshot.and_then(|(w, h, pixels)| {
4718 if pixels.is_dmabuf() {
4719 return None;
4720 }
4721 let rgba = pixels.to_rgba(w, h);
4722 if rgba.is_empty() {
4723 None
4724 } else {
4725 Some((w, h, rgba))
4726 }
4727 });
4728 }
4729
4730 eprintln!("[capture] acquiring client_tx lock");
4731 let client_tx = {
4732 let sess = state.session.lock().await;
4733 eprintln!("[capture] client_tx lock acquired");
4734 sess.clients.get(&client_id).map(|c| {
4735 (
4736 c.tx.clone(),
4737 c.outbox_queued_frames.clone(),
4738 c.outbox_queued_bytes.clone(),
4739 )
4740 })
4741 };
4742
4743 if let Some((w, h, rgba_pixels)) = captured {
4744 let image_data = encode_capture(&rgba_pixels, w, h, format, quality);
4745 reply_msg.extend_from_slice(&w.to_le_bytes());
4746 reply_msg.extend_from_slice(&h.to_le_bytes());
4747 reply_msg.extend_from_slice(&image_data);
4748 } else {
4749 reply_msg.extend_from_slice(&0u32.to_le_bytes());
4750 reply_msg.extend_from_slice(&0u32.to_le_bytes());
4751 }
4752
4753 if let Some((client_tx, queued_frames, queued_bytes)) = client_tx {
4754 eprintln!("[capture] sending reply: {} bytes", reply_msg.len());
4755 match send_outbox_tracked(&client_tx, &queued_frames, &queued_bytes, reply_msg) {
4756 Ok(()) => eprintln!("[capture] sent OK"),
4757 Err(e) => eprintln!("[capture] send failed: {e}"),
4758 }
4759 } else {
4760 eprintln!("[capture] no client_tx");
4761 }
4762 continue;
4763 }
4764
4765 if data[0] == C2S_QUIT {
4766 let sess = state.session.lock().await;
4767 sess.send_to_all(&[S2C_QUIT]);
4768 drop(sess);
4769 state.shutdown_notify.notify_one();
4770 break;
4771 }
4772
4773 let mut sess = state.session.lock().await;
4774 let mut need_nudge = false;
4775 match data[0] {
4776 C2S_SCROLL if data.len() >= 7 => {
4777 let pid = u16::from_le_bytes([data[1], data[2]]);
4778 let offset = u32::from_le_bytes([data[3], data[4], data[5], data[6]]) as usize;
4779 if sess.ptys.contains_key(&pid) {
4780 if let Some(c) = sess.clients.get_mut(&client_id) {
4781 update_client_scroll_state(c, pid, offset);
4782 }
4783 if let Some(pty) = sess.ptys.get_mut(&pid) {
4784 pty.mark_dirty();
4785 need_nudge = true;
4786 }
4787 }
4788 }
4789 C2S_RESIZE if data.len() >= 7 => {
4790 let entries = data[1..].chunks_exact(6);
4791 if !entries.remainder().is_empty() {
4792 continue;
4793 }
4794 let mut touched = Vec::with_capacity((data.len() - 1) / 6);
4795 for entry in entries {
4796 let pid = u16::from_le_bytes([entry[0], entry[1]]);
4797 if !sess.ptys.contains_key(&pid) {
4798 continue;
4799 }
4800 let rows = u16::from_le_bytes([entry[2], entry[3]]);
4801 let cols = u16::from_le_bytes([entry[4], entry[5]]);
4802 if let Some(c) = sess.clients.get_mut(&client_id) {
4803 if is_unset_view_size(rows, cols) {
4804 if c.view_sizes.remove(&pid).is_some() {
4805 touched.push(pid);
4806 }
4807 } else if rows == 0 || cols == 0 {
4808 continue;
4809 } else {
4810 c.view_sizes.insert(pid, (rows, cols));
4811 touched.push(pid);
4812 }
4813 }
4814 }
4815 if sess.resize_ptys_to_mediated_sizes(touched) {
4816 need_nudge = true;
4817 }
4818 }
4819 C2S_CREATE => {
4820 let (rows, cols) = if data.len() >= 5 {
4822 (
4823 u16::from_le_bytes([data[1], data[2]]),
4824 u16::from_le_bytes([data[3], data[4]]),
4825 )
4826 } else {
4827 (24, 80)
4828 };
4829 let tag_len = if data.len() >= 7 {
4830 u16::from_le_bytes([data[5], data[6]]) as usize
4831 } else {
4832 0
4833 };
4834 let tag = if data.len() >= 7 + tag_len {
4835 std::str::from_utf8(&data[7..7 + tag_len]).unwrap_or_default()
4836 } else {
4837 ""
4838 };
4839 let cmd_start = 7 + tag_len;
4840 let dir: Option<String> = None;
4841 let create_payload = data
4842 .get(cmd_start..)
4843 .and_then(|bytes| std::str::from_utf8(bytes).ok());
4844 let command = create_payload
4845 .filter(|payload| !payload.contains('\0'))
4846 .map(str::trim)
4847 .filter(|payload| !payload.is_empty());
4848 let argv: Option<Vec<&str>> = create_payload
4849 .filter(|payload| payload.contains('\0'))
4850 .map(|payload| {
4851 payload
4852 .split('\0')
4853 .filter(|arg| !arg.is_empty())
4854 .collect::<Vec<_>>()
4855 })
4856 .filter(|args| !args.is_empty());
4857 let Some(id) = sess.allocate_pty_id(config.max_ptys) else {
4858 continue;
4859 };
4860 let socket_name = sess
4861 .ensure_compositor(
4862 config.verbose,
4863 notify_for_compositor.clone(),
4864 &config.vaapi_device,
4865 )
4866 .to_string();
4867 #[cfg(target_os = "linux")]
4868 let pulse_server = sess.pulse_server_path();
4869 #[cfg(not(target_os = "linux"))]
4870 let pulse_server: Option<String> = None;
4871 #[cfg(target_os = "linux")]
4872 let pipewire_remote = sess.pipewire_remote_path();
4873 #[cfg(not(target_os = "linux"))]
4874 let pipewire_remote: Option<String> = None;
4875 if let Some(pty) = pty::spawn_pty(
4876 &config.shell,
4877 &config.shell_flags,
4878 rows,
4879 cols,
4880 id,
4881 tag,
4882 command,
4883 argv.as_deref(),
4884 dir.as_deref(),
4885 config.scrollback,
4886 state.clone(),
4887 Some(&socket_name),
4888 pulse_server.as_deref(),
4889 pipewire_remote.as_deref(),
4890 ) {
4891 let mut msg = Vec::with_capacity(3 + pty.tag.len());
4892 msg.push(S2C_CREATED);
4893 msg.extend_from_slice(&id.to_le_bytes());
4894 msg.extend_from_slice(pty.tag.as_bytes());
4895 sess.ptys.insert(id, pty);
4896 if let Some(c) = sess.clients.get_mut(&client_id) {
4897 c.lead = Some(id);
4898 c.view_sizes.insert(id, (rows, cols));
4899 subscribe_client_to(c, id);
4900 reset_inflight(c);
4901 }
4902 sess.send_to_all(&msg);
4903 need_nudge = true;
4904 }
4905 }
4906 C2S_CREATE_N => {
4907 let nonce = if data.len() >= 3 {
4909 u16::from_le_bytes([data[1], data[2]])
4910 } else {
4911 0
4912 };
4913 let (rows, cols) = if data.len() >= 7 {
4914 (
4915 u16::from_le_bytes([data[3], data[4]]),
4916 u16::from_le_bytes([data[5], data[6]]),
4917 )
4918 } else {
4919 (24, 80)
4920 };
4921 let tag_len = if data.len() >= 9 {
4922 u16::from_le_bytes([data[7], data[8]]) as usize
4923 } else {
4924 0
4925 };
4926 let tag = if data.len() >= 9 + tag_len {
4927 std::str::from_utf8(&data[9..9 + tag_len]).unwrap_or_default()
4928 } else {
4929 ""
4930 };
4931 let cmd_start = 9 + tag_len;
4932 let dir: Option<String> = None;
4933 let create_payload = data
4934 .get(cmd_start..)
4935 .and_then(|bytes| std::str::from_utf8(bytes).ok());
4936 let command = create_payload
4937 .filter(|payload| !payload.contains('\0'))
4938 .map(str::trim)
4939 .filter(|payload| !payload.is_empty());
4940 let argv: Option<Vec<&str>> = create_payload
4941 .filter(|payload| payload.contains('\0'))
4942 .map(|payload| {
4943 payload
4944 .split('\0')
4945 .filter(|arg| !arg.is_empty())
4946 .collect::<Vec<_>>()
4947 })
4948 .filter(|args| !args.is_empty());
4949 let Some(id) = sess.allocate_pty_id(config.max_ptys) else {
4950 continue;
4951 };
4952 let socket_name = sess
4953 .ensure_compositor(
4954 config.verbose,
4955 notify_for_compositor.clone(),
4956 &config.vaapi_device,
4957 )
4958 .to_string();
4959 #[cfg(target_os = "linux")]
4960 let pulse_server = sess.pulse_server_path();
4961 #[cfg(not(target_os = "linux"))]
4962 let pulse_server: Option<String> = None;
4963 #[cfg(target_os = "linux")]
4964 let pipewire_remote = sess.pipewire_remote_path();
4965 #[cfg(not(target_os = "linux"))]
4966 let pipewire_remote: Option<String> = None;
4967 if let Some(pty) = pty::spawn_pty(
4968 &config.shell,
4969 &config.shell_flags,
4970 rows,
4971 cols,
4972 id,
4973 tag,
4974 command,
4975 argv.as_deref(),
4976 dir.as_deref(),
4977 config.scrollback,
4978 state.clone(),
4979 Some(&socket_name),
4980 pulse_server.as_deref(),
4981 pipewire_remote.as_deref(),
4982 ) {
4983 let tag_bytes = pty.tag.as_bytes();
4984 let mut nonce_msg = Vec::with_capacity(5 + tag_bytes.len());
4985 nonce_msg.push(S2C_CREATED_N);
4986 nonce_msg.extend_from_slice(&nonce.to_le_bytes());
4987 nonce_msg.extend_from_slice(&id.to_le_bytes());
4988 nonce_msg.extend_from_slice(tag_bytes);
4989 let mut broadcast_msg = Vec::with_capacity(3 + tag_bytes.len());
4990 broadcast_msg.push(S2C_CREATED);
4991 broadcast_msg.extend_from_slice(&id.to_le_bytes());
4992 broadcast_msg.extend_from_slice(tag_bytes);
4993 sess.ptys.insert(id, pty);
4994 if let Some(c) = sess.clients.get_mut(&client_id) {
4995 c.lead = Some(id);
4996 c.view_sizes.insert(id, (rows, cols));
4997 subscribe_client_to(c, id);
4998 reset_inflight(c);
4999 let _ = send_outbox(c, nonce_msg);
5000 }
5001 for (&cid, c) in sess.clients.iter() {
5002 if cid != client_id {
5003 let _ = send_outbox(c, broadcast_msg.clone());
5004 }
5005 }
5006 need_nudge = true;
5007 }
5008 }
5009 C2S_CREATE_AT => {
5010 let (rows, cols) = if data.len() >= 5 {
5012 (
5013 u16::from_le_bytes([data[1], data[2]]),
5014 u16::from_le_bytes([data[3], data[4]]),
5015 )
5016 } else {
5017 (24, 80)
5018 };
5019 let tag_len = if data.len() >= 7 {
5020 u16::from_le_bytes([data[5], data[6]]) as usize
5021 } else {
5022 0
5023 };
5024 let tag = if data.len() >= 7 + tag_len {
5025 std::str::from_utf8(&data[7..7 + tag_len]).unwrap_or_default()
5026 } else {
5027 ""
5028 };
5029 let src_start = 7 + tag_len;
5030 let dir = if data.len() >= src_start + 2 {
5031 let src_id = u16::from_le_bytes([data[src_start], data[src_start + 1]]);
5032 sess.ptys.get(&src_id).and_then(|p| pty::pty_cwd(&p.handle))
5033 } else {
5034 None
5035 };
5036 let Some(id) = sess.allocate_pty_id(config.max_ptys) else {
5037 continue;
5038 };
5039 let socket_name = sess
5040 .ensure_compositor(
5041 config.verbose,
5042 notify_for_compositor.clone(),
5043 &config.vaapi_device,
5044 )
5045 .to_string();
5046 #[cfg(target_os = "linux")]
5047 let pulse_server = sess.pulse_server_path();
5048 #[cfg(not(target_os = "linux"))]
5049 let pulse_server: Option<String> = None;
5050 #[cfg(target_os = "linux")]
5051 let pipewire_remote = sess.pipewire_remote_path();
5052 #[cfg(not(target_os = "linux"))]
5053 let pipewire_remote: Option<String> = None;
5054 if let Some(pty) = pty::spawn_pty(
5055 &config.shell,
5056 &config.shell_flags,
5057 rows,
5058 cols,
5059 id,
5060 tag,
5061 None,
5062 None,
5063 dir.as_deref(),
5064 config.scrollback,
5065 state.clone(),
5066 Some(&socket_name),
5067 pulse_server.as_deref(),
5068 pipewire_remote.as_deref(),
5069 ) {
5070 let mut msg = Vec::with_capacity(3 + pty.tag.len());
5071 msg.push(S2C_CREATED);
5072 msg.extend_from_slice(&id.to_le_bytes());
5073 msg.extend_from_slice(pty.tag.as_bytes());
5074 sess.ptys.insert(id, pty);
5075 if let Some(c) = sess.clients.get_mut(&client_id) {
5076 c.lead = Some(id);
5077 c.view_sizes.insert(id, (rows, cols));
5078 subscribe_client_to(c, id);
5079 reset_inflight(c);
5080 }
5081 sess.send_to_all(&msg);
5082 need_nudge = true;
5083 }
5084 }
5085 C2S_CREATE2 => {
5086 if data.len() < 10 {
5087 continue;
5088 }
5089 let nonce = u16::from_le_bytes([data[1], data[2]]);
5090 let rows = u16::from_le_bytes([data[3], data[4]]);
5091 let cols = u16::from_le_bytes([data[5], data[6]]);
5092 let features = data[7];
5093 let tag_len = u16::from_le_bytes([data[8], data[9]]) as usize;
5094 let tag = if data.len() >= 10 + tag_len {
5095 std::str::from_utf8(&data[10..10 + tag_len]).unwrap_or_default()
5096 } else {
5097 ""
5098 };
5099 let mut cursor = 10 + tag_len;
5100 let dir = if features & CREATE2_HAS_SRC_PTY != 0 && data.len() >= cursor + 2 {
5101 let src_id = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
5102 cursor += 2;
5103 sess.ptys.get(&src_id).and_then(|p| pty::pty_cwd(&p.handle))
5104 } else {
5105 None
5106 };
5107 let create_payload = if features & CREATE2_HAS_COMMAND != 0 {
5108 data.get(cursor..).and_then(|b| std::str::from_utf8(b).ok())
5109 } else {
5110 None
5111 };
5112 let command = create_payload
5113 .filter(|p| !p.contains('\0'))
5114 .map(str::trim)
5115 .filter(|p| !p.is_empty());
5116 let argv: Option<Vec<&str>> = create_payload
5117 .filter(|p| p.contains('\0'))
5118 .map(|p| p.split('\0').filter(|a| !a.is_empty()).collect::<Vec<_>>())
5119 .filter(|a| !a.is_empty());
5120 let Some(id) = sess.allocate_pty_id(config.max_ptys) else {
5121 continue;
5122 };
5123 let socket_name = sess
5124 .ensure_compositor(
5125 config.verbose,
5126 notify_for_compositor.clone(),
5127 &config.vaapi_device,
5128 )
5129 .to_string();
5130 #[cfg(target_os = "linux")]
5131 let pulse_server = sess.pulse_server_path();
5132 #[cfg(not(target_os = "linux"))]
5133 let pulse_server: Option<String> = None;
5134 #[cfg(target_os = "linux")]
5135 let pipewire_remote = sess.pipewire_remote_path();
5136 #[cfg(not(target_os = "linux"))]
5137 let pipewire_remote: Option<String> = None;
5138 if let Some(pty) = pty::spawn_pty(
5139 &config.shell,
5140 &config.shell_flags,
5141 rows,
5142 cols,
5143 id,
5144 tag,
5145 command,
5146 argv.as_deref(),
5147 dir.as_deref(),
5148 config.scrollback,
5149 state.clone(),
5150 Some(&socket_name),
5151 pulse_server.as_deref(),
5152 pipewire_remote.as_deref(),
5153 ) {
5154 let tag_bytes = pty.tag.as_bytes();
5155 let mut nonce_msg = Vec::with_capacity(5 + tag_bytes.len());
5156 nonce_msg.push(S2C_CREATED_N);
5157 nonce_msg.extend_from_slice(&nonce.to_le_bytes());
5158 nonce_msg.extend_from_slice(&id.to_le_bytes());
5159 nonce_msg.extend_from_slice(tag_bytes);
5160 let mut broadcast_msg = Vec::with_capacity(3 + tag_bytes.len());
5161 broadcast_msg.push(S2C_CREATED);
5162 broadcast_msg.extend_from_slice(&id.to_le_bytes());
5163 broadcast_msg.extend_from_slice(tag_bytes);
5164 sess.ptys.insert(id, pty);
5165 if let Some(c) = sess.clients.get_mut(&client_id) {
5166 c.lead = Some(id);
5167 c.view_sizes.insert(id, (rows, cols));
5168 subscribe_client_to(c, id);
5169 reset_inflight(c);
5170 let _ = send_outbox(c, nonce_msg);
5171 }
5172 for (&cid, c) in sess.clients.iter() {
5173 if cid != client_id {
5174 let _ = send_outbox(c, broadcast_msg.clone());
5175 }
5176 }
5177 need_nudge = true;
5178 }
5179 }
5180 C2S_SURFACE_INPUT if data.len() >= 8 => {
5181 let surface_id = u16::from_le_bytes([data[1], data[2]]);
5182 let keycode = u32::from_le_bytes([data[3], data[4], data[5], data[6]]);
5183 let pressed = data[7] != 0;
5184 if let Some(client) = sess.clients.get_mut(&client_id) {
5185 if pressed {
5186 client.pressed_surface_keys.insert(keycode);
5187 } else {
5188 client.pressed_surface_keys.remove(&keycode);
5189 }
5190 }
5191 if let Some(cs) = sess.compositor.as_mut() {
5192 let _ = cs.handle.command_tx.send(CompositorCommand::KeyInput {
5193 surface_id,
5194 keycode,
5195 pressed,
5196 });
5197 cs.handle.wake();
5198 state.delivery_notify.notify_one();
5199 }
5200 }
5201 C2S_SURFACE_TEXT if data.len() >= 3 => {
5202 let _surface_id = u16::from_le_bytes([data[1], data[2]]);
5203 if let Ok(text) = std::str::from_utf8(&data[3..])
5204 && let Some(cs) = sess.compositor.as_mut()
5205 {
5206 let _ = cs.handle.command_tx.send(CompositorCommand::TextInput {
5207 text: text.to_string(),
5208 });
5209 cs.handle.wake();
5210 state.delivery_notify.notify_one();
5211 }
5212 }
5213 C2S_SURFACE_POINTER if data.len() >= 9 => {
5214 let surface_id = u16::from_le_bytes([data[1], data[2]]);
5215 let ptype = data[3];
5216 let button = data[4];
5217 let x = u16::from_le_bytes([data[5], data[6]]) as f64;
5218 let y = u16::from_le_bytes([data[7], data[8]]) as f64;
5219 if let Some(cs) = sess.compositor.as_mut() {
5220 match ptype {
5221 0 | 1 => {
5222 let _ = cs.handle.command_tx.send(CompositorCommand::PointerMotion {
5223 surface_id,
5224 x,
5225 y,
5226 });
5227 let _ = cs.handle.command_tx.send(CompositorCommand::PointerButton {
5228 surface_id,
5229 button: match button {
5230 1 => 0x112,
5231 2 => 0x111,
5232 _ => 0x110,
5233 },
5234 pressed: ptype == 0,
5235 });
5236 }
5237 2 => {
5238 let _ = cs.handle.command_tx.send(CompositorCommand::PointerMotion {
5239 surface_id,
5240 x,
5241 y,
5242 });
5243 }
5244 _ => {}
5245 }
5246 cs.handle.wake();
5247 }
5248 state.delivery_notify.notify_one();
5249 }
5250 C2S_SURFACE_POINTER_AXIS if data.len() >= 8 => {
5251 let surface_id = u16::from_le_bytes([data[1], data[2]]);
5252 let axis = data[3];
5253 let value_x100 = i32::from_le_bytes([data[4], data[5], data[6], data[7]]);
5254 let value = value_x100 as f64 / 100.0;
5255 if let Some(cs) = sess.compositor.as_mut() {
5256 let _ = cs.handle.command_tx.send(CompositorCommand::PointerAxis {
5257 surface_id,
5258 axis,
5259 value,
5260 });
5261 cs.handle.wake();
5262 }
5263 state.delivery_notify.notify_one();
5264 }
5265 C2S_SURFACE_RESIZE if data.len() >= 9 => {
5266 let surface_id = u16::from_le_bytes([data[1], data[2]]);
5267 let width = u16::from_le_bytes([data[3], data[4]]);
5268 let height = u16::from_le_bytes([data[5], data[6]]);
5269 let scale_120 = u16::from_le_bytes([data[7], data[8]]);
5271 if state.config.verbose {
5272 eprintln!(
5273 "C2S_SURFACE_RESIZE: cid={client_id} sid={surface_id} {width}x{height} scale={scale_120}"
5274 );
5275 }
5276 if let Some(c) = sess.clients.get_mut(&client_id) {
5277 if is_unset_view_size(width, height) {
5278 c.surface_view_sizes.remove(&surface_id);
5279 } else if width > 0 && height > 0 {
5280 c.surface_view_sizes
5281 .insert(surface_id, (width, height, scale_120));
5282 }
5283 if let Some(s) = c.surface_subs.get_mut(&surface_id) {
5296 s.nal_none_streak = 0;
5297 s.nal_none_latched_at = None;
5298 s.burst_remaining = SURFACE_BURST_FRAMES;
5299 s.next_send_at = None;
5300 }
5301 c.surface_needs_keyframe = true;
5302 }
5303 sess.resize_surfaces_to_mediated_sizes(
5304 std::iter::once(surface_id),
5305 &state.config.surface_encoders,
5306 );
5307 }
5308 C2S_SURFACE_FOCUS if data.len() >= 3 => {
5309 let surface_id = u16::from_le_bytes([data[1], data[2]]);
5310 if state.config.verbose {
5311 eprintln!("C2S_SURFACE_FOCUS: cid={client_id} sid={surface_id}");
5312 }
5313 if let Some(cs) = sess.compositor.as_ref() {
5314 let _ = cs
5315 .handle
5316 .command_tx
5317 .send(CompositorCommand::SurfaceFocus { surface_id });
5318 }
5319 }
5320 C2S_SURFACE_CLOSE if data.len() >= 3 => {
5321 let surface_id = u16::from_le_bytes([data[1], data[2]]);
5322 if let Some(cs) = sess.compositor.as_ref() {
5323 let _ = cs
5324 .handle
5325 .command_tx
5326 .send(CompositorCommand::SurfaceClose { surface_id });
5327 cs.handle.wake();
5328 }
5329 }
5330 C2S_SURFACE_SUBSCRIBE if data.len() >= 3 => {
5331 let surface_id = u16::from_le_bytes([data[1], data[2]]);
5332 let codec_support = if data.len() >= 4 { data[3] } else { 0 };
5334 let quality_wire = if data.len() >= 5 { data[4] } else { 0 };
5335 if state.config.verbose {
5336 eprintln!(
5337 "C2S_SURFACE_SUBSCRIBE: cid={client_id} surface={surface_id} codec={codec_support:#04x} quality={quality_wire}"
5338 );
5339 }
5340 let mut destroy_vulkan_enc_sid = None;
5341 let mut first_subscribe = false;
5342 if let Some(c) = sess.clients.get_mut(&client_id) {
5343 let was_subscribed = !c.surface_subscriptions.insert(surface_id);
5344 let new_quality = SurfaceQuality::from_wire(quality_wire);
5345
5346 let state = c.surface_subs.entry(surface_id).or_default();
5347 let old_codec = state.codec_override;
5348 let old_quality = state.quality_override;
5349
5350 let meaningful_change =
5356 !was_subscribed || codec_support != old_codec || new_quality != old_quality;
5357 state.codec_override = codec_support;
5358 state.quality_override = new_quality;
5359 let task_in_flight = state.encode_in_flight || state.creation_in_flight;
5360 if meaningful_change {
5361 state.burst_remaining = SURFACE_BURST_FRAMES;
5367 state.nal_none_streak = 0;
5368 state.nal_none_latched_at = None;
5369 }
5370 if was_subscribed && (codec_support != old_codec || new_quality != old_quality)
5375 {
5376 state.encoder = None;
5377 if task_in_flight {
5378 state.encoder_invalidated = true;
5379 }
5380 }
5381 if meaningful_change {
5382 c.surface_needs_keyframe = true;
5383 }
5384 first_subscribe = !was_subscribed;
5385 if was_subscribed
5386 && (codec_support != old_codec || new_quality != old_quality)
5387 && c.vulkan_video_surfaces.remove(&surface_id).is_some()
5388 {
5389 destroy_vulkan_enc_sid = Some(surface_id);
5390 }
5391 }
5392 if let Some(sid) = destroy_vulkan_enc_sid
5393 && let Some(cs) = sess.compositor.as_ref()
5394 {
5395 let _ = cs.handle.command_tx.send(
5396 blit_compositor::CompositorCommand::DestroyVulkanEncoder {
5397 surface_id: sid as u32,
5398 },
5399 );
5400 cs.handle.wake();
5401 }
5402 if first_subscribe {
5403 sess.resize_surfaces_to_mediated_sizes(
5404 std::iter::once(surface_id),
5405 &state.config.surface_encoders,
5406 );
5407 }
5408 state.delivery_notify.notify_one();
5409 }
5410 C2S_SURFACE_UNSUBSCRIBE if data.len() >= 3 => {
5411 let surface_id = u16::from_le_bytes([data[1], data[2]]);
5412 let mut removed_vulkan = false;
5413 if let Some(c) = sess.clients.get_mut(&client_id) {
5414 c.surface_subscriptions.remove(&surface_id);
5415 c.surface_subs.remove(&surface_id);
5416 removed_vulkan = c.vulkan_video_surfaces.remove(&surface_id).is_some();
5417 c.surface_view_sizes.remove(&surface_id);
5418 }
5419 if removed_vulkan {
5421 let still_needed = sess
5422 .clients
5423 .values()
5424 .any(|other| other.vulkan_video_surfaces.contains_key(&surface_id));
5425 if !still_needed && let Some(cs) = sess.compositor.as_ref() {
5426 let _ = cs.handle.command_tx.send(
5427 blit_compositor::CompositorCommand::DestroyVulkanEncoder {
5428 surface_id: surface_id as u32,
5429 },
5430 );
5431 cs.handle.wake();
5432 }
5433 }
5434 sess.resize_surfaces_to_mediated_sizes(
5435 std::iter::once(surface_id),
5436 &state.config.surface_encoders,
5437 );
5438 }
5439 #[cfg(target_os = "linux")]
5440 C2S_AUDIO_SUBSCRIBE if data.len() >= 3 => {
5441 let bitrate_kbps = u16::from_le_bytes([data[1], data[2]]);
5442 let audio_tx = sess.clients.get(&client_id).map(|c| c.audio_tx.clone());
5443 if let Some(c) = sess.clients.get_mut(&client_id) {
5444 c.audio_subscribed = true;
5445 c.audio_bitrate_kbps = bitrate_kbps;
5446 if state.config.verbose {
5447 eprintln!(
5448 "C2S_AUDIO_SUBSCRIBE: cid={client_id} bitrate_kbps={bitrate_kbps}"
5449 );
5450 }
5451 }
5452 if let (Some(cs), Some(tx)) = (sess.compositor.as_ref(), audio_tx) {
5457 cs.audio_broadcast.subscribe(client_id, tx);
5458 }
5459 if let Some(cs) = sess.compositor.as_ref()
5462 && let Some(ref ap) = cs.audio_pipeline
5463 {
5464 let max_kbps = sess
5465 .clients
5466 .values()
5467 .filter(|c| c.audio_subscribed)
5468 .map(|c| c.audio_bitrate_kbps)
5469 .max()
5470 .unwrap_or(0);
5471 let bitrate = if max_kbps > 0 {
5472 max_kbps as i32 * 1000
5473 } else {
5474 audio::DEFAULT_BITRATE
5475 };
5476 ap.set_bitrate(bitrate);
5477 }
5478 state.delivery_notify.notify_one();
5479 }
5480 #[cfg(target_os = "linux")]
5481 C2S_AUDIO_UNSUBSCRIBE if !data.is_empty() => {
5482 if let Some(c) = sess.clients.get_mut(&client_id) {
5483 c.audio_subscribed = false;
5484 c.audio_bitrate_kbps = 0;
5485 if state.config.verbose {
5486 eprintln!("C2S_AUDIO_UNSUBSCRIBE: cid={client_id}");
5487 }
5488 }
5489 if let Some(cs) = sess.compositor.as_ref() {
5490 cs.audio_broadcast.unsubscribe(client_id);
5491 }
5492 if let Some(cs) = sess.compositor.as_ref()
5494 && let Some(ref ap) = cs.audio_pipeline
5495 {
5496 let max_kbps = sess
5497 .clients
5498 .values()
5499 .filter(|c| c.audio_subscribed)
5500 .map(|c| c.audio_bitrate_kbps)
5501 .max()
5502 .unwrap_or(0);
5503 let bitrate = if max_kbps > 0 {
5504 max_kbps as i32 * 1000
5505 } else {
5506 audio::DEFAULT_BITRATE
5507 };
5508 ap.set_bitrate(bitrate);
5509 }
5510 }
5511 C2S_SURFACE_ACK if data.len() >= 3 => {
5512 if let Some(c) = sess.clients.get_mut(&client_id) {
5516 c.acks_recv += 1;
5517 record_surface_ack(c);
5518 }
5519 state.delivery_notify.notify_one();
5520 }
5521 C2S_CLIENT_FEATURES if data.len() >= 2 => {
5522 let codec_support = data[1];
5525 if let Some(c) = sess.clients.get_mut(&client_id) {
5526 c.surface_codec_support = codec_support;
5527 }
5528 }
5529 C2S_CLIPBOARD_SET if data.len() >= 5 => {
5530 let mime_len = u16::from_le_bytes([data[1], data[2]]) as usize;
5531 if data.len() >= 3 + mime_len + 4 {
5532 let mime = std::str::from_utf8(&data[3..3 + mime_len])
5533 .unwrap_or("text/plain")
5534 .to_string();
5535 let data_len = u32::from_le_bytes([
5536 data[3 + mime_len],
5537 data[4 + mime_len],
5538 data[5 + mime_len],
5539 data[6 + mime_len],
5540 ]) as usize;
5541 let payload_start = 7 + mime_len;
5542 if data.len() >= payload_start + data_len {
5543 let payload = data[payload_start..payload_start + data_len].to_vec();
5544 if let Some(cs) = sess.compositor.as_ref() {
5545 let _ = cs
5546 .handle
5547 .command_tx
5548 .send(CompositorCommand::ClipboardOffer {
5549 mime_type: mime,
5550 data: payload,
5551 });
5552 }
5553 }
5554 }
5555 }
5556 C2S_CLIPBOARD_LIST if !data.is_empty() => {
5557 if let Some(cs) = sess.compositor.as_ref() {
5558 let command_tx = cs.handle.command_tx.clone();
5559 let client_tx = sess.clients.get(&client_id).map(|c| {
5560 (
5561 c.tx.clone(),
5562 c.outbox_queued_frames.clone(),
5563 c.outbox_queued_bytes.clone(),
5564 )
5565 });
5566 if let Some((client_tx, queued_frames, queued_bytes)) = client_tx {
5567 tokio::task::spawn_blocking(move || {
5568 let (tx, rx) = std::sync::mpsc::sync_channel(1);
5569 if command_tx
5570 .send(CompositorCommand::ClipboardListMimes { reply: tx })
5571 .is_ok()
5572 && let Ok(mimes) = rx.recv_timeout(Duration::from_secs(2))
5573 {
5574 let _ = send_outbox_tracked(
5575 &client_tx,
5576 &queued_frames,
5577 &queued_bytes,
5578 msg_s2c_clipboard_list(&mimes),
5579 );
5580 }
5581 });
5582 }
5583 } else {
5584 if let Some(c) = sess.clients.get(&client_id) {
5586 let _ = send_outbox(c, msg_s2c_clipboard_list(&[]));
5587 }
5588 }
5589 }
5590 C2S_CLIPBOARD_GET if data.len() >= 3 => {
5591 let mime_len = u16::from_le_bytes([data[1], data[2]]) as usize;
5592 if data.len() >= 3 + mime_len {
5593 let mime = std::str::from_utf8(&data[3..3 + mime_len])
5594 .unwrap_or("text/plain")
5595 .to_string();
5596 if let Some(cs) = sess.compositor.as_ref() {
5597 let command_tx = cs.handle.command_tx.clone();
5598 let client_tx = sess.clients.get(&client_id).map(|c| {
5599 (
5600 c.tx.clone(),
5601 c.outbox_queued_frames.clone(),
5602 c.outbox_queued_bytes.clone(),
5603 )
5604 });
5605 if let Some((client_tx, queued_frames, queued_bytes)) = client_tx {
5606 tokio::task::spawn_blocking(move || {
5607 let (tx, rx) = std::sync::mpsc::sync_channel(1);
5608 if command_tx
5609 .send(CompositorCommand::ClipboardGet {
5610 mime_type: mime.clone(),
5611 reply: tx,
5612 })
5613 .is_ok()
5614 && let Ok(content) = rx.recv_timeout(Duration::from_secs(2))
5615 {
5616 let data = content.unwrap_or_default();
5617 let _ = send_outbox_tracked(
5618 &client_tx,
5619 &queued_frames,
5620 &queued_bytes,
5621 msg_s2c_clipboard_content(&mime, &data),
5622 );
5623 }
5624 });
5625 }
5626 } else {
5627 if let Some(c) = sess.clients.get(&client_id) {
5629 let _ = send_outbox(c, msg_s2c_clipboard_content(&mime, &[]));
5630 }
5631 }
5632 }
5633 }
5634 C2S_SURFACE_LIST if !data.is_empty() => {
5635 let msg = sess.surface_list_msg();
5636 if let Some(c) = sess.clients.get(&client_id) {
5637 let _ = send_outbox(c, msg);
5638 }
5639 }
5640 C2S_FOCUS if data.len() >= 3 => {
5641 let pid = u16::from_le_bytes([data[1], data[2]]);
5642 if sess.ptys.contains_key(&pid) {
5643 let old_pid = sess.clients.get(&client_id).and_then(|c| c.lead);
5644 if let Some(c) = sess.clients.get_mut(&client_id) {
5645 c.lead = Some(pid);
5646 subscribe_client_to(c, pid);
5647 if old_pid == Some(pid) {
5648 update_client_scroll_state(c, pid, 0);
5649 } else {
5650 reset_inflight(c);
5651 }
5652 }
5653 if let Some(pty) = sess.ptys.get_mut(&pid) {
5654 pty.mark_dirty();
5655 need_nudge = true;
5656 }
5657 }
5658 }
5659 C2S_SUBSCRIBE if data.len() >= 3 => {
5660 let pid = u16::from_le_bytes([data[1], data[2]]);
5661 if sess.ptys.contains_key(&pid) {
5662 if let Some(c) = sess.clients.get_mut(&client_id) {
5663 subscribe_client_to(c, pid);
5664 }
5665 if let Some(pty) = sess.ptys.get_mut(&pid) {
5666 pty.mark_dirty();
5667 }
5668 need_nudge = true;
5669 }
5670 }
5671 C2S_UNSUBSCRIBE if data.len() >= 3 => {
5672 let pid = u16::from_le_bytes([data[1], data[2]]);
5673 if sess.ptys.contains_key(&pid) {
5674 let mut touched = Vec::new();
5675 if let Some(c) = sess.clients.get_mut(&client_id) {
5676 if unsubscribe_client_from(c, pid) {
5677 touched.push(pid);
5678 }
5679 reset_inflight(c);
5680 }
5681 if sess.resize_ptys_to_mediated_sizes(touched) {
5682 need_nudge = true;
5683 }
5684 }
5685 }
5686 C2S_RESTART if data.len() >= 3 => {
5687 let pid = u16::from_le_bytes([data[1], data[2]]);
5688 let restart_info = sess
5689 .ptys
5690 .get(&pid)
5691 .filter(|p| p.exited)
5692 .map(|p| (p.driver.size(), p.command.clone(), p.tag.clone()));
5693 if let Some(((rows, cols), command, tag)) = restart_info {
5694 let wayland_display = sess
5695 .compositor
5696 .as_ref()
5697 .map(|cs| cs.handle.socket_name.clone());
5698 #[cfg(target_os = "linux")]
5699 let pulse_server = sess.pulse_server_path();
5700 #[cfg(not(target_os = "linux"))]
5701 let pulse_server: Option<String> = None;
5702 #[cfg(target_os = "linux")]
5703 let pipewire_remote = sess.pipewire_remote_path();
5704 #[cfg(not(target_os = "linux"))]
5705 let pipewire_remote: Option<String> = None;
5706 if let Some((new_handle, reader, byte_rx)) = pty::respawn_child(
5707 &state.config.shell,
5708 &state.config.shell_flags,
5709 rows,
5710 cols,
5711 pid,
5712 command.as_deref(),
5713 state.clone(),
5714 wayland_display.as_deref(),
5715 pulse_server.as_deref(),
5716 pipewire_remote.as_deref(),
5717 ) {
5718 let Some(pty) = sess.ptys.get_mut(&pid) else {
5719 break;
5720 };
5721 pty.handle = new_handle;
5722 pty.reader_handle = reader;
5723 pty.byte_rx = byte_rx;
5724 pty.driver.reset_modes();
5725 pty.exited = false;
5726 pty.exit_status = blit_remote::EXIT_STATUS_UNKNOWN;
5727 pty.lflag_cache = pty::pty_lflag(&pty.handle);
5728 pty.lflag_last = Instant::now();
5729 pty.mark_dirty();
5730 if let Some(c) = sess.clients.get_mut(&client_id) {
5731 c.lead = Some(pid);
5732 subscribe_client_to(c, pid);
5733 update_client_scroll_state(c, pid, 0);
5734 reset_inflight(c);
5735 }
5736 let mut msg = Vec::with_capacity(3 + tag.len());
5737 msg.push(S2C_CREATED);
5738 msg.extend_from_slice(&pid.to_le_bytes());
5739 msg.extend_from_slice(tag.as_bytes());
5740 sess.send_to_all(&msg);
5741 need_nudge = true;
5742 }
5743 }
5744 }
5745 C2S_READ if data.len() >= 13 => {
5746 let nonce = u16::from_le_bytes([data[1], data[2]]);
5747 let pid = u16::from_le_bytes([data[3], data[4]]);
5748 let req_offset = u32::from_le_bytes([data[5], data[6], data[7], data[8]]) as usize;
5749 let req_limit =
5750 u32::from_le_bytes([data[9], data[10], data[11], data[12]]) as usize;
5751 let flags = data.get(13).copied().unwrap_or(0);
5752 let ansi = flags & READ_ANSI != 0;
5753 let tail = flags & READ_TAIL != 0;
5754
5755 if let Some(pty) = sess.ptys.get_mut(&pid) {
5756 let (rows, _cols) = pty.driver.size();
5757 let viewport = take_snapshot(pty);
5758 let scrollback_lines = viewport.scrollback_lines() as usize;
5759 let total_lines = scrollback_lines + rows as usize;
5760
5761 let extract = |f: &FrameState| -> String {
5762 if ansi {
5763 f.get_ansi_text()
5764 } else {
5765 f.get_all_text()
5766 }
5767 };
5768
5769 let mut all_lines: Vec<String> =
5770 Vec::with_capacity(scrollback_lines + rows as usize);
5771
5772 let mut scroll_offset = scrollback_lines;
5773 while scroll_offset > 0 {
5774 let frame = pty.driver.scrollback_frame(scroll_offset);
5775 let page = extract(&frame);
5776 let page_lines: Vec<&str> = page.lines().collect();
5777 let take = if scroll_offset < rows as usize {
5778 scroll_offset.min(page_lines.len())
5779 } else {
5780 page_lines.len()
5781 };
5782 for line in &page_lines[..take] {
5783 all_lines.push(line.to_string());
5784 }
5785 if scroll_offset <= rows as usize {
5786 break;
5787 }
5788 scroll_offset = scroll_offset.saturating_sub(rows as usize);
5789 }
5790
5791 for line in extract(&viewport).lines() {
5792 all_lines.push(line.to_string());
5793 }
5794
5795 let (start, end) = if tail {
5796 let end = all_lines.len().saturating_sub(req_offset);
5797 let start = if req_limit == 0 {
5798 0
5799 } else {
5800 end.saturating_sub(req_limit)
5801 };
5802 (start, end)
5803 } else {
5804 let start = req_offset.min(all_lines.len());
5805 let end = if req_limit == 0 {
5806 all_lines.len()
5807 } else {
5808 (start + req_limit).min(all_lines.len())
5809 };
5810 (start, end)
5811 };
5812 let text = all_lines[start..end].join("\n");
5813
5814 let mut msg = Vec::with_capacity(13 + text.len());
5815 msg.push(S2C_TEXT);
5816 msg.extend_from_slice(&nonce.to_le_bytes());
5817 msg.extend_from_slice(&pid.to_le_bytes());
5818 msg.extend_from_slice(&(total_lines as u32).to_le_bytes());
5819 msg.extend_from_slice(&(start as u32).to_le_bytes());
5820 msg.extend_from_slice(text.as_bytes());
5821 if let Some(client) = sess.clients.get(&client_id) {
5822 let _ = send_outbox(client, msg);
5823 }
5824 }
5825 }
5826 C2S_COPY_RANGE if data.len() >= 18 => {
5827 let nonce = u16::from_le_bytes([data[1], data[2]]);
5828 let pid = u16::from_le_bytes([data[3], data[4]]);
5829 let start_tail = u32::from_le_bytes([data[5], data[6], data[7], data[8]]);
5830 let start_col = u16::from_le_bytes([data[9], data[10]]);
5831 let end_tail = u32::from_le_bytes([data[11], data[12], data[13], data[14]]);
5832 let end_col = u16::from_le_bytes([data[15], data[16]]);
5833
5834 if let Some(pty) = sess.ptys.get(&pid) {
5835 let text = pty
5836 .driver
5837 .get_text_range(start_tail, start_col, end_tail, end_col);
5838 let total_lines = pty.driver.total_lines();
5839
5840 let mut msg = Vec::with_capacity(13 + text.len());
5841 msg.push(S2C_TEXT);
5842 msg.extend_from_slice(&nonce.to_le_bytes());
5843 msg.extend_from_slice(&pid.to_le_bytes());
5844 msg.extend_from_slice(&total_lines.to_le_bytes());
5845 msg.extend_from_slice(&start_tail.to_le_bytes());
5846 msg.extend_from_slice(text.as_bytes());
5847 if let Some(client) = sess.clients.get(&client_id) {
5848 let _ = send_outbox(client, msg);
5849 }
5850 }
5851 }
5852 C2S_KILL if data.len() >= 7 => {
5853 let pid = u16::from_le_bytes([data[1], data[2]]);
5854 let signal = i32::from_le_bytes([data[3], data[4], data[5], data[6]]);
5855 if let Some(pty) = sess.ptys.get(&pid)
5856 && !pty.exited
5857 {
5858 pty::kill_pty(&pty.handle, signal);
5859 }
5860 }
5861 C2S_CLOSE if data.len() >= 3 => {
5862 let pid = u16::from_le_bytes([data[1], data[2]]);
5863 if let Some(pty) = sess.ptys.remove(&pid) {
5864 if !pty.exited {
5865 state.pty_fds.write().unwrap().remove(&pid);
5866 drop(pty.reader_handle);
5867 pty::close_pty(&pty.handle);
5868 }
5869 for client in sess.clients.values_mut() {
5870 unsubscribe_client_from(client, pid);
5871 }
5872 let mut msg = vec![S2C_CLOSED];
5873 msg.extend_from_slice(&pid.to_le_bytes());
5874 sess.send_to_all(&msg);
5875 }
5876 }
5877 _ => {}
5878 }
5879 drop(sess);
5880 if need_nudge {
5881 nudge_delivery(&state);
5882 }
5883 }
5884
5885 {
5886 let mut sess = state.session.lock().await;
5887 let mut need_nudge = false;
5888 #[cfg(target_os = "linux")]
5892 if let Some(cs) = sess.compositor.as_ref() {
5893 cs.audio_broadcast.unsubscribe(client_id);
5894 }
5895 let client = sess.clients.remove(&client_id);
5896 let affected_ptys = client
5897 .as_ref()
5898 .map(|c| c.view_sizes.keys().copied().collect::<Vec<_>>())
5899 .unwrap_or_default();
5900 let affected_surfaces = client
5901 .as_ref()
5902 .map(|c| c.surface_view_sizes.keys().copied().collect::<Vec<_>>())
5903 .unwrap_or_default();
5904 if sess.resize_ptys_to_mediated_sizes(affected_ptys) {
5905 need_nudge = true;
5906 }
5907 sess.resize_surfaces_to_mediated_sizes(affected_surfaces, &state.config.surface_encoders);
5908 if let Some(ref client) = client
5912 && !client.pressed_surface_keys.is_empty()
5913 && let Some(cs) = sess.compositor.as_mut()
5914 {
5915 let keycodes: Vec<u32> = client.pressed_surface_keys.iter().copied().collect();
5916 let _ = cs
5917 .handle
5918 .command_tx
5919 .send(CompositorCommand::ReleaseKeys { keycodes });
5920 cs.handle.wake();
5921 }
5922 if let Some(ref client) = client
5925 && !client.vulkan_video_surfaces.is_empty()
5926 && let Some(cs) = sess.compositor.as_ref()
5927 {
5928 for &sid in client.vulkan_video_surfaces.keys() {
5929 let still_needed = sess
5930 .clients
5931 .values()
5932 .any(|c| c.vulkan_video_surfaces.contains_key(&sid));
5933 if !still_needed {
5934 let _ = cs
5935 .handle
5936 .command_tx
5937 .send(CompositorCommand::DestroyVulkanEncoder {
5938 surface_id: sid as u32,
5939 });
5940 }
5941 }
5942 cs.handle.wake();
5943 }
5944 drop(sess);
5945 if need_nudge {
5946 nudge_delivery(&state);
5947 }
5948 }
5949 sender.abort();
5950 if state.config.verbose {
5951 eprintln!("client disconnected");
5952 }
5953}
5954
5955#[cfg(test)]
5956mod tests {
5957 use super::*;
5958
5959 fn test_client_with_capacity(
5960 _capacity: usize,
5961 ) -> (ClientState, mpsc::UnboundedReceiver<Vec<u8>>) {
5962 let (tx, rx) = mpsc::unbounded_channel();
5963 let (audio_tx, _audio_rx) = mpsc::unbounded_channel();
5964 let client = ClientState {
5965 tx,
5966 outbox_queued_frames: Arc::new(AtomicUsize::new(0)),
5967 outbox_queued_bytes: Arc::new(AtomicUsize::new(0)),
5968 audio_tx,
5969 lead: None,
5970 subscriptions: HashSet::new(),
5971 surface_subscriptions: HashSet::new(),
5972 audio_subscribed: false,
5973 #[cfg(target_os = "linux")]
5974 audio_bitrate_kbps: 0,
5975 view_sizes: HashMap::new(),
5976 scroll_offsets: HashMap::new(),
5977 scroll_caches: HashMap::new(),
5978 last_sent: HashMap::new(),
5979 preview_next_send_at: HashMap::new(),
5980 rtt_ms: 50.0,
5981 min_rtt_ms: 50.0,
5982 display_fps: 60.0,
5983 delivery_bps: 262_144.0,
5984 goodput_bps: 262_144.0,
5985 goodput_jitter_bps: 0.0,
5986 max_goodput_jitter_bps: 0.0,
5987 last_goodput_sample_bps: 0.0,
5988 avg_frame_bytes: 1_024.0,
5989 avg_paced_frame_bytes: 1_024.0,
5990 avg_preview_frame_bytes: 1_024.0,
5991 avg_surface_frame_bytes: 8_192.0,
5992 inflight_bytes: 0,
5993 inflight_frames: VecDeque::new(),
5994 next_send_at: Instant::now(),
5995 probe_frames: 0.0,
5996 frames_sent: 0,
5997 acks_recv: 0,
5998 acked_bytes_since_log: 0,
5999 browser_backlog_frames: 0,
6000 browser_ack_ahead_frames: 0,
6001 browser_apply_ms: 0.0,
6002 last_metrics_update: Instant::now(),
6003 last_log: Instant::now(),
6004 last_window_blocked_log: Instant::now(),
6005 last_skip_log: Instant::now(),
6006 skip_same_gen_count: 0,
6007 skip_in_flight_count: 0,
6008 skip_pacing_count: 0,
6009 skip_vulkan_await_count: 0,
6010 skip_no_subs_count: 0,
6011 skip_not_subbed_count: 0,
6012 skip_last_pixels_mismatch_count: 0,
6013 encode_loop_iters: 0,
6014 goodput_window_bytes: 0,
6015 goodput_window_start: Instant::now(),
6016 surface_subs: HashMap::new(),
6017 surface_needs_keyframe: true,
6018 surface_inflight_frames: VecDeque::new(),
6019 vulkan_video_surfaces: HashMap::new(),
6020 surface_view_sizes: HashMap::new(),
6021 surface_codec_support: 0,
6022 pressed_surface_keys: HashSet::new(),
6023 };
6024 (client, rx)
6025 }
6026
6027 fn test_client() -> ClientState {
6028 let (client, _rx) = test_client_with_capacity(0);
6029 client
6030 }
6031
6032 fn fill_inflight(client: &mut ClientState, frames: usize, bytes_per_frame: usize) {
6033 let now = Instant::now();
6034 client.inflight_bytes = frames.saturating_mul(bytes_per_frame);
6035 client.inflight_frames = (0..frames)
6036 .map(|_| InFlightFrame {
6037 sent_at: now,
6038 bytes: bytes_per_frame,
6039 paced: true,
6040 })
6041 .collect();
6042 }
6043
6044 fn sample_frame(text: &str) -> FrameState {
6045 let mut frame = FrameState::new(2, 8);
6046 frame.write_text(0, 0, text, blit_remote::CellStyle::default());
6047 frame
6048 }
6049
6050 #[test]
6051 fn unset_view_size_accepts_zero_pair_only() {
6052 assert!(is_unset_view_size(0, 0));
6053 assert!(!is_unset_view_size(0, 80));
6054 assert!(!is_unset_view_size(u16::MAX, u16::MAX));
6055 }
6056
6057 #[test]
6058 fn unsubscribe_client_from_clears_view_size() {
6059 let mut client = test_client();
6060 client.subscriptions.insert(7);
6061 client.view_sizes.insert(7, (24, 80));
6062 assert!(unsubscribe_client_from(&mut client, 7));
6063 assert!(!client.subscriptions.contains(&7));
6064 assert!(!client.view_sizes.contains_key(&7));
6065 }
6066
6067 #[test]
6068 fn mediated_size_uses_per_pty_view_sizes_without_lead() {
6069 let mut session = Session::new();
6070 let mut c1 = test_client();
6071 let mut c2 = test_client();
6072 c1.view_sizes.insert(7, (30, 120));
6073 c2.view_sizes.insert(7, (24, 100));
6074 session.clients.insert(1, c1);
6075 session.clients.insert(2, c2);
6076 assert_eq!(session.mediated_size_for_pty(7), Some((24, 100)));
6077 }
6078
6079 #[test]
6080 fn mediated_surface_size_picks_min_dimensions_max_scale() {
6081 let mut session = Session::new();
6082 let mut c1 = test_client();
6083 let mut c2 = test_client();
6084 c1.surface_view_sizes.insert(1, (1920, 1080, 240)); c2.surface_view_sizes.insert(1, (1280, 720, 120)); session.clients.insert(1, c1);
6087 session.clients.insert(2, c2);
6088 assert_eq!(
6089 session.mediated_size_for_surface(1, None),
6090 Some((1280, 720, 240))
6091 );
6092 }
6093
6094 #[test]
6095 fn mediated_surface_size_none_when_no_clients() {
6096 let session = Session::new();
6097 assert_eq!(session.mediated_size_for_surface(1, None), None);
6098 }
6099
6100 #[test]
6101 fn mediated_surface_size_single_client() {
6102 let mut session = Session::new();
6103 let mut c1 = test_client();
6104 c1.surface_view_sizes.insert(3, (800, 600, 120));
6105 session.clients.insert(1, c1);
6106 assert_eq!(
6107 session.mediated_size_for_surface(3, None),
6108 Some((800, 600, 120))
6109 );
6110 }
6111
6112 #[test]
6113 fn mediated_surface_size_ignores_other_surfaces() {
6114 let mut session = Session::new();
6115 let mut c1 = test_client();
6116 c1.surface_view_sizes.insert(1, (1920, 1080, 240));
6117 c1.surface_view_sizes.insert(2, (640, 480, 120));
6118 session.clients.insert(1, c1);
6119 assert_eq!(
6120 session.mediated_size_for_surface(1, None),
6121 Some((1920, 1080, 240))
6122 );
6123 assert_eq!(
6124 session.mediated_size_for_surface(2, None),
6125 Some((640, 480, 120))
6126 );
6127 assert_eq!(session.mediated_size_for_surface(3, None), None);
6128 }
6129
6130 #[test]
6131 fn mediated_surface_size_clamped_to_encoder_max() {
6132 let mut session = Session::new();
6133 let mut c1 = test_client();
6134 c1.surface_view_sizes.insert(1, (5000, 3000, 240));
6135 session.clients.insert(1, c1);
6136 assert_eq!(
6137 session.mediated_size_for_surface(1, None),
6138 Some((5000, 3000, 240))
6139 );
6140 assert_eq!(
6141 session.mediated_size_for_surface(1, Some((3840, 2160))),
6142 Some((3840, 2160, 240))
6143 );
6144 }
6145
6146 #[test]
6147 fn mediated_surface_size_picks_min_across_clients() {
6148 let mut session = Session::new();
6149 let mut c1 = test_client();
6150 let mut c2 = test_client();
6151 c1.surface_view_sizes.insert(1, (1920, 1080, 120));
6152 c2.surface_view_sizes.insert(1, (640, 360, 120));
6153 c1.surface_subscriptions.insert(1);
6154 c2.surface_subscriptions.insert(1);
6155 session.clients.insert(1, c1);
6156 session.clients.insert(2, c2);
6157 assert_eq!(
6158 session.mediated_size_for_surface(1, None),
6159 Some((640, 360, 120))
6160 );
6161 }
6162
6163 #[test]
6164 fn due_preview_reserves_the_last_lead_slot() {
6165 let mut client = test_client();
6166 client.lead = Some(1);
6167 client.subscriptions.insert(1);
6168 client.subscriptions.insert(2);
6169
6170 let target_frames = target_frame_window(&client);
6171 let lead_limit = target_frames.saturating_sub(1).max(1);
6172 fill_inflight(&mut client, lead_limit, 512);
6173
6174 assert!(window_open(&client));
6175 assert!(lead_window_open(&client, false));
6176 assert!(!lead_window_open(&client, true));
6177 assert!(can_send_preview(&client, 2, Instant::now()));
6178 }
6179
6180 #[test]
6181 fn entering_scrollback_uses_current_visible_frame_as_baseline() {
6182 let mut client = test_client();
6183 let live = sample_frame("live");
6184 client.lead = Some(7);
6185 client.subscriptions.insert(7);
6186 client.last_sent.insert(7, live.clone());
6187
6188 assert!(update_client_scroll_state(&mut client, 7, 12));
6189 assert_eq!(client.scroll_offsets.get(&7), Some(&12));
6190 assert_eq!(client.scroll_caches.get(&7), Some(&live));
6191 }
6192
6193 #[test]
6194 fn leaving_scrollback_seeds_live_diff_from_scrollback_view() {
6195 let mut client = test_client();
6196 let history = sample_frame("hist");
6197 client.lead = Some(7);
6198 client.subscriptions.insert(7);
6199 client.scroll_offsets.insert(7, 12);
6200 client.scroll_caches.insert(7, history.clone());
6201
6202 assert!(update_client_scroll_state(&mut client, 7, 0));
6203 assert_eq!(client.scroll_offsets.get(&7), None);
6204 assert_eq!(client.last_sent.get(&7), Some(&history));
6205 assert_eq!(client.scroll_caches.get(&7), None);
6206 }
6207
6208 #[tokio::test]
6209 async fn request_surface_capture_returns_pixels_from_compositor() {
6210 let (command_tx, command_rx) = std::sync::mpsc::channel();
6211 std::thread::Builder::new()
6212 .name("test-capture-reply".into())
6213 .spawn(move || {
6214 let CompositorCommand::Capture {
6215 surface_id,
6216 scale_120: _,
6217 reply,
6218 } = command_rx.recv().unwrap()
6219 else {
6220 panic!("expected capture command");
6221 };
6222 assert_eq!(surface_id, 7);
6223 let _ = reply.send(Some((2, 3, vec![1, 2, 3, 4])));
6224 })
6225 .unwrap();
6226
6227 let result =
6228 request_surface_capture_with_timeout(command_tx, 7, 0, Duration::from_millis(50)).await;
6229
6230 assert_eq!(result, Some((2, 3, vec![1, 2, 3, 4])));
6231 }
6232
6233 #[tokio::test]
6234 async fn request_surface_capture_returns_none_when_compositor_disconnects() {
6235 let (command_tx, command_rx) = std::sync::mpsc::channel();
6236 std::thread::Builder::new()
6237 .name("test-capture-drop".into())
6238 .spawn(move || {
6239 let _ = command_rx.recv().unwrap();
6240 })
6241 .unwrap();
6242
6243 let result =
6244 request_surface_capture_with_timeout(command_tx, 7, 0, Duration::from_millis(50)).await;
6245
6246 assert_eq!(result, None);
6247 }
6248
6249 #[test]
6252 fn frame_window_minimum_is_two() {
6253 assert!(frame_window(0.0, 60.0) >= 2);
6254 }
6255
6256 #[test]
6257 fn frame_window_scales_with_rtt() {
6258 let low = frame_window(10.0, 60.0);
6259 let high = frame_window(200.0, 60.0);
6260 assert!(high > low, "higher RTT should need more frames in flight");
6261 }
6262
6263 #[test]
6264 fn frame_window_scales_with_fps() {
6265 let slow = frame_window(100.0, 10.0);
6266 let fast = frame_window(100.0, 120.0);
6267 assert!(fast > slow, "higher fps should need more frames in flight");
6268 }
6269
6270 #[test]
6271 fn frame_window_zero_rtt() {
6272 assert!(frame_window(0.0, 120.0) >= 2);
6273 }
6274
6275 #[test]
6278 fn path_rtt_ms_uses_min_when_positive() {
6279 let mut client = test_client();
6280 client.rtt_ms = 100.0;
6281 client.min_rtt_ms = 30.0;
6282 assert_eq!(path_rtt_ms(&client), 30.0);
6283 }
6284
6285 #[test]
6286 fn path_rtt_ms_falls_back_to_rtt_when_min_zero() {
6287 let mut client = test_client();
6288 client.rtt_ms = 80.0;
6289 client.min_rtt_ms = 0.0;
6290 assert_eq!(path_rtt_ms(&client), 80.0);
6291 }
6292
6293 #[test]
6296 fn ewma_rising_uses_rise_alpha() {
6297 let result = ewma_with_direction(100.0, 200.0, 0.5, 0.1);
6298 assert!((result - 150.0).abs() < 0.01);
6300 }
6301
6302 #[test]
6303 fn ewma_falling_uses_fall_alpha() {
6304 let result = ewma_with_direction(200.0, 100.0, 0.5, 0.1);
6305 assert!((result - 190.0).abs() < 0.01);
6307 }
6308
6309 #[test]
6310 fn ewma_same_value_unchanged() {
6311 let result = ewma_with_direction(50.0, 50.0, 0.5, 0.5);
6312 assert!((result - 50.0).abs() < 0.01);
6313 }
6314
6315 #[test]
6318 fn advance_deadline_steps_forward() {
6319 let now = Instant::now();
6320 let mut deadline = now;
6321 let interval = Duration::from_millis(16);
6322 advance_deadline(&mut deadline, now, interval);
6323 assert!(deadline > now);
6324 assert!(deadline <= now + interval + Duration::from_micros(100));
6325 }
6326
6327 #[test]
6328 fn advance_deadline_resets_when_far_behind() {
6329 let now = Instant::now();
6330 let mut deadline = now - Duration::from_secs(10);
6332 let interval = Duration::from_millis(16);
6333 advance_deadline(&mut deadline, now, interval);
6334 assert!(deadline >= now);
6336 }
6337
6338 #[test]
6339 fn should_snapshot_pty_requires_dirty_and_needful() {
6340 assert!(should_snapshot_pty(true, true, false));
6341 assert!(!should_snapshot_pty(false, true, false));
6342 assert!(!should_snapshot_pty(true, false, false));
6343 }
6344
6345 #[test]
6346 fn should_snapshot_pty_defers_synced_output() {
6347 assert!(!should_snapshot_pty(true, true, true));
6348 assert!(should_snapshot_pty(true, true, false));
6349 }
6350
6351 #[test]
6352 fn enqueue_ready_frame_refuses_new_frames_when_capped() {
6353 let mut queue = VecDeque::new();
6354 for cols in 1..=(READY_FRAME_QUEUE_CAP as u16) {
6355 assert!(enqueue_ready_frame(&mut queue, FrameState::new(1, cols)));
6356 }
6357 assert!(!enqueue_ready_frame(
6358 &mut queue,
6359 FrameState::new(1, READY_FRAME_QUEUE_CAP as u16 + 1),
6360 ));
6361 assert_eq!(queue.len(), READY_FRAME_QUEUE_CAP);
6362 assert_eq!(queue.front().map(FrameState::cols), Some(1));
6363 assert_eq!(
6364 queue.back().map(FrameState::cols),
6365 Some(READY_FRAME_QUEUE_CAP as u16),
6366 );
6367 }
6368
6369 #[test]
6370 fn find_sync_output_end_returns_end_of_first_close_sequence() {
6371 let bytes = b"abc\x1b[?2026lrest\x1b[?2026l";
6372 assert_eq!(find_sync_output_end(&[], bytes), Some(11));
6373 }
6374
6375 #[test]
6376 fn find_sync_output_end_returns_none_without_close_sequence() {
6377 assert_eq!(find_sync_output_end(&[], b"\x1b[?2026hpartial"), None);
6378 }
6379
6380 #[test]
6381 fn find_sync_output_end_detects_boundary_split_across_reads() {
6382 assert_eq!(find_sync_output_end(b"abc\x1b[?20", b"26lrest"), Some(3));
6383 }
6384
6385 #[test]
6386 fn update_sync_scan_tail_keeps_recent_suffix_only() {
6387 let mut tail = Vec::new();
6388 update_sync_scan_tail(&mut tail, b"123456789");
6389 assert_eq!(tail, b"3456789");
6390 }
6391
6392 #[test]
6395 fn window_saturated_at_90_percent_frames() {
6396 let client = test_client();
6397 let target = target_frame_window(&client);
6398 let frames_90 = (target * 9).div_ceil(10); assert!(window_saturated(&client, frames_90, 0));
6400 }
6401
6402 #[test]
6403 fn window_saturated_not_at_low_usage() {
6404 let client = test_client();
6405 assert!(!window_saturated(&client, 1, 0));
6406 }
6407
6408 #[test]
6409 fn window_saturated_at_90_percent_bytes() {
6410 let client = test_client();
6411 let target_bytes = target_byte_window(&client);
6412 let bytes_90 = (target_bytes * 9).div_ceil(10);
6413 assert!(window_saturated(&client, 0, bytes_90));
6414 }
6415
6416 #[test]
6419 fn outbox_queued_frames_zero_when_empty() {
6420 let client = test_client();
6421 assert_eq!(outbox_queued_frames(&client), 0);
6422 }
6423
6424 #[test]
6425 fn outbox_backpressured_when_queue_full() {
6426 let (client, _rx) = test_client_with_capacity(0);
6427 for _ in 0..OUTBOX_SOFT_QUEUE_LIMIT_FRAMES {
6429 let _ = send_outbox(&client, vec![0u8]);
6430 }
6431 assert!(outbox_backpressured(&client));
6432 }
6433
6434 #[test]
6435 fn outbox_not_backpressured_by_small_frames_under_byte_budget() {
6436 let (client, _rx) = test_client_with_capacity(0);
6437 for _ in 0..(OUTBOX_SOFT_QUEUE_LIMIT_FRAMES - 1) {
6438 let _ = send_outbox(&client, vec![0u8; 512]);
6439 }
6440 assert!(!outbox_backpressured(&client));
6441 }
6442
6443 #[test]
6444 fn outbox_backpressured_by_large_queued_bytes() {
6445 let (client, _rx) = test_client_with_capacity(0);
6446 let _ = send_outbox(&client, vec![0u8; OUTBOX_SOFT_QUEUE_LIMIT_BYTES]);
6449 assert!(!outbox_backpressured(&client));
6450 let _ = send_outbox(&client, vec![0u8; 1]);
6452 assert!(outbox_backpressured(&client));
6453 }
6454
6455 #[test]
6456 fn outbox_not_backpressured_when_empty() {
6457 let client = test_client();
6458 assert!(!outbox_backpressured(&client));
6459 }
6460
6461 #[test]
6464 fn browser_pacing_fps_matches_display_fps_when_browser_ready() {
6465 let mut client = test_client();
6466 client.rtt_ms = 1.0;
6467 client.min_rtt_ms = 1.0;
6468 client.browser_backlog_frames = 0;
6469 client.browser_ack_ahead_frames = 0;
6470 client.browser_apply_ms = 0.0;
6471 client.goodput_bps = 1_000_000.0;
6472 client.delivery_bps = 1_000_000.0;
6473 client.display_fps = 144.0;
6474 assert!((browser_pacing_fps(&client) - 144.0).abs() < 0.01);
6475 }
6476
6477 #[test]
6478 fn browser_pacing_fps_drops_below_display_fps_when_backlogged() {
6479 let mut client = test_client();
6480 client.browser_backlog_frames = 20;
6481 let fps = browser_pacing_fps(&client);
6482 assert!(fps >= 1.0);
6483 assert!(fps < client.display_fps);
6484 }
6485
6486 #[test]
6489 fn effective_rtt_ms_equals_path_when_queue_is_empty() {
6490 let mut client = test_client();
6491 client.rtt_ms = 1.0;
6492 client.min_rtt_ms = 1.0;
6493 client.browser_backlog_frames = 0;
6494 client.browser_ack_ahead_frames = 0;
6495 client.browser_apply_ms = 0.0;
6496 client.goodput_bps = 1_000_000.0;
6497 client.delivery_bps = 1_000_000.0;
6498 assert!((effective_rtt_ms(&client) - 1.0).abs() < 0.01);
6499 }
6500
6501 #[test]
6502 fn effective_rtt_ms_at_least_path_rtt() {
6503 let client = test_client();
6504 assert!(effective_rtt_ms(&client) >= path_rtt_ms(&client));
6505 }
6506
6507 #[test]
6510 fn target_frame_window_at_least_two() {
6511 let client = test_client();
6512 assert!(target_frame_window(&client) >= 2);
6513 }
6514
6515 #[test]
6516 fn target_frame_window_grows_with_probe() {
6517 let mut client = test_client();
6518 let base = target_frame_window(&client);
6519 client.probe_frames = 10.0;
6520 let probed = target_frame_window(&client);
6521 assert!(probed > base, "probe_frames should grow the window");
6522 }
6523
6524 #[test]
6527 fn bandwidth_floor_bps_at_least_16k() {
6528 let mut client = test_client();
6529 client.goodput_bps = 0.0;
6530 client.delivery_bps = 0.0;
6531 assert_eq!(bandwidth_floor_bps(&client), 0.0);
6532 }
6533
6534 #[test]
6535 fn bandwidth_floor_bps_scales_with_goodput() {
6536 let mut client = test_client();
6537 client.goodput_bps = 1_000_000.0;
6538 client.delivery_bps = 1_000_000.0;
6539 let floor = bandwidth_floor_bps(&client);
6540 assert!(floor > 0.0);
6541 }
6542
6543 #[test]
6544 fn browser_ready_delivery_floor_can_drive_large_frames_to_display_fps() {
6545 let mut client = test_client();
6546 client.display_fps = 60.0;
6547 client.browser_backlog_frames = 0;
6548 client.browser_ack_ahead_frames = 0;
6549 client.browser_apply_ms = 0.2;
6550 client.goodput_bps = 3_000_000.0;
6551 client.delivery_bps = 9_500_000.0;
6552 client.last_goodput_sample_bps = 3_000_000.0;
6553 client.avg_paced_frame_bytes = 150_000.0;
6554 client.avg_preview_frame_bytes = 1_024.0;
6555 client.avg_frame_bytes = 150_000.0;
6556
6557 assert!(
6558 (pacing_fps(&client) - client.display_fps).abs() < 0.01,
6559 "browser-ready delivery floor should let large frames reach display_fps on a fast path",
6560 );
6561 }
6562
6563 #[test]
6566 fn pacing_fps_zero_when_no_bandwidth() {
6567 let mut client = test_client();
6568 client.goodput_bps = 0.0;
6569 client.delivery_bps = 0.0;
6570 client.last_goodput_sample_bps = 0.0;
6571 assert!(
6572 pacing_fps(&client) == 0.0,
6573 "pacing_fps should be 0 with zero bandwidth"
6574 );
6575 }
6576
6577 #[test]
6578 fn pacing_fps_reaches_display_fps_when_not_bandwidth_limited() {
6579 let mut client = test_client();
6580 client.rtt_ms = 1.0;
6581 client.min_rtt_ms = 1.0;
6582 client.browser_backlog_frames = 0;
6583 client.browser_ack_ahead_frames = 0;
6584 client.browser_apply_ms = 0.0;
6585 client.goodput_bps = 1_000_000.0;
6586 client.delivery_bps = 1_000_000.0;
6587 client.display_fps = 60.0;
6588 assert!((pacing_fps(&client) - 60.0).abs() < 0.01);
6589 }
6590
6591 #[test]
6594 fn throughput_limited_when_low_bandwidth() {
6595 let mut client = test_client();
6596 client.goodput_bps = 1_000.0;
6597 client.delivery_bps = 1_000.0;
6598 client.last_goodput_sample_bps = 0.0;
6599 assert!(throughput_limited(&client));
6600 }
6601
6602 #[test]
6603 fn throughput_not_limited_with_high_bandwidth() {
6604 let mut client = test_client();
6605 client.goodput_bps = 100_000_000.0;
6606 client.delivery_bps = 100_000_000.0;
6607 assert!(!throughput_limited(&client));
6608 }
6609
6610 #[test]
6613 fn browser_pacing_fps_at_least_one() {
6614 let client = test_client();
6615 assert!(browser_pacing_fps(&client) >= 1.0);
6616 }
6617
6618 #[test]
6619 fn browser_pacing_fps_reduced_by_high_backlog() {
6620 let mut client = test_client();
6621 let normal = browser_pacing_fps(&client);
6622 client.browser_backlog_frames = 20;
6623 let backlogged = browser_pacing_fps(&client);
6624 assert!(backlogged < normal, "high backlog should reduce pacing fps");
6625 }
6626
6627 #[test]
6628 fn browser_pacing_fps_reduced_by_high_ack_ahead() {
6629 let mut client = test_client();
6630 let normal = browser_pacing_fps(&client);
6631 client.browser_ack_ahead_frames = 10;
6632 let ahead = browser_pacing_fps(&client);
6633 assert!(ahead < normal, "high ack_ahead should reduce pacing fps");
6634 }
6635
6636 #[test]
6639 fn browser_backlog_blocked_over_threshold() {
6640 let mut client = test_client();
6641 client.browser_backlog_frames = 9;
6642 assert!(browser_backlog_blocked(&client));
6643 }
6644
6645 #[test]
6646 fn browser_backlog_not_blocked_under_threshold() {
6647 let mut client = test_client();
6648 client.browser_backlog_frames = 8;
6649 assert!(!browser_backlog_blocked(&client));
6650 }
6651
6652 #[test]
6655 fn byte_budget_for_at_least_one_frame() {
6656 let client = test_client();
6657 let budget = byte_budget_for(&client, 10.0);
6658 assert!(budget >= client.avg_frame_bytes.max(256.0) as usize);
6659 }
6660
6661 #[test]
6662 fn byte_budget_for_grows_with_time() {
6663 let client = test_client();
6664 let short = byte_budget_for(&client, 10.0);
6665 let long = byte_budget_for(&client, 1000.0);
6666 assert!(long >= short);
6667 }
6668
6669 #[test]
6672 fn target_byte_window_positive() {
6673 let client = test_client();
6674 assert!(target_byte_window(&client) > 0);
6675 }
6676
6677 #[test]
6678 fn target_byte_window_covers_frame_window() {
6679 let client = test_client();
6680 let byte_win = target_byte_window(&client);
6681 let frame_win = target_frame_window(&client);
6682 let min_bytes =
6683 (client.avg_paced_frame_bytes.max(256.0) * frame_win.max(2) as f32).ceil() as usize;
6684 assert!(
6685 byte_win >= min_bytes,
6686 "byte window should cover at least frame_window worth of paced frames"
6687 );
6688 }
6689
6690 #[test]
6693 fn send_interval_matches_browser_pacing() {
6694 let client = test_client();
6695 let interval = send_interval(&client);
6696 let expected = Duration::from_secs_f64(1.0 / browser_pacing_fps(&client) as f64);
6697 let diff = interval.abs_diff(expected);
6698 assert!(diff < Duration::from_micros(10));
6699 }
6700
6701 #[test]
6704 fn preview_fps_at_least_one() {
6705 let client = test_client();
6706 assert!(preview_fps(&client) >= 1.0);
6707 }
6708
6709 #[test]
6712 fn window_open_initially() {
6713 let client = test_client();
6714 assert!(window_open(&client));
6715 }
6716
6717 #[test]
6718 fn window_open_false_when_browser_blocked() {
6719 let mut client = test_client();
6720 client.browser_backlog_frames = 20;
6721 assert!(!window_open(&client));
6722 }
6723
6724 #[test]
6725 fn window_open_false_when_inflight_full() {
6726 let mut client = test_client();
6727 let target = target_frame_window(&client);
6728 fill_inflight(&mut client, target + 10, 1024);
6729 assert!(!window_open(&client));
6730 }
6731
6732 #[test]
6735 fn lead_window_open_no_reserve_same_as_window_open() {
6736 let client = test_client();
6737 assert_eq!(lead_window_open(&client, false), window_open(&client));
6738 }
6739
6740 #[test]
6741 fn lead_window_open_reserves_preview_slot() {
6742 let mut client = test_client();
6743 client.lead = Some(1);
6744 client.subscriptions.insert(1);
6745 let target = target_frame_window(&client);
6746 fill_inflight(&mut client, target.saturating_sub(1), 512);
6748 assert!(!lead_window_open(&client, true));
6751 }
6752
6753 #[test]
6756 fn can_send_frame_when_window_open_and_time_due() {
6757 let mut client = test_client();
6758 client.next_send_at = Instant::now() - Duration::from_millis(100);
6759 assert!(can_send_frame(&client, Instant::now(), false));
6760 }
6761
6762 #[test]
6763 fn can_send_frame_false_when_not_due() {
6764 let mut client = test_client();
6765 client.next_send_at = Instant::now() + Duration::from_secs(10);
6766 assert!(!can_send_frame(&client, Instant::now(), false));
6767 }
6768
6769 #[test]
6770 fn can_send_frame_false_when_window_closed() {
6771 let mut client = test_client();
6772 client.browser_backlog_frames = 20; client.next_send_at = Instant::now() - Duration::from_millis(100);
6774 assert!(!can_send_frame(&client, Instant::now(), false));
6775 }
6776
6777 #[test]
6780 fn record_send_increases_inflight() {
6781 let mut client = test_client();
6782 let now = Instant::now();
6783 assert_eq!(client.inflight_bytes, 0);
6784 assert_eq!(client.inflight_frames.len(), 0);
6785
6786 record_send(&mut client, 1000, now, true);
6787 assert_eq!(client.inflight_bytes, 1000);
6788 assert_eq!(client.inflight_frames.len(), 1);
6789
6790 record_send(&mut client, 500, now, false);
6791 assert_eq!(client.inflight_bytes, 1500);
6792 assert_eq!(client.inflight_frames.len(), 2);
6793 }
6794
6795 #[test]
6796 fn record_send_paced_advances_deadline() {
6797 let mut client = test_client();
6798 let now = Instant::now();
6799 client.next_send_at = now;
6800 record_send(&mut client, 1000, now, true);
6801 assert!(client.next_send_at > now);
6802 }
6803
6804 #[test]
6805 fn record_send_unpaced_does_not_advance_deadline() {
6806 let mut client = test_client();
6807 let now = Instant::now();
6808 let before = client.next_send_at;
6809 record_send(&mut client, 1000, now, false);
6810 assert_eq!(client.next_send_at, before);
6811 }
6812
6813 #[test]
6814 fn record_ack_decreases_inflight() {
6815 let mut client = test_client();
6816 let now = Instant::now();
6817 record_send(&mut client, 1000, now, true);
6818 record_send(&mut client, 500, now, true);
6819 assert_eq!(client.inflight_frames.len(), 2);
6820
6821 record_ack(&mut client);
6822 assert_eq!(client.inflight_frames.len(), 1);
6823 assert_eq!(client.inflight_bytes, 500);
6824 }
6825
6826 #[test]
6827 fn record_ack_on_empty_clears_bytes() {
6828 let mut client = test_client();
6829 client.inflight_bytes = 999; record_ack(&mut client);
6831 assert_eq!(client.inflight_bytes, 0);
6832 }
6833
6834 #[test]
6835 fn record_ack_updates_rtt_estimate() {
6836 let mut client = test_client();
6837 let now = Instant::now();
6838 client.inflight_frames.push_back(InFlightFrame {
6839 sent_at: now - Duration::from_millis(20),
6840 bytes: 512,
6841 paced: true,
6842 });
6843 client.inflight_bytes = 512;
6844 let old_rtt = client.rtt_ms;
6845 record_ack(&mut client);
6846 assert!(
6848 (client.rtt_ms - old_rtt).abs() > 0.01,
6849 "rtt_ms should be updated after ack"
6850 );
6851 }
6852
6853 #[test]
6854 fn record_ack_paced_updates_avg_paced_frame_bytes() {
6855 let mut client = test_client();
6856 let now = Instant::now();
6857 client.inflight_frames.push_back(InFlightFrame {
6858 sent_at: now - Duration::from_millis(10),
6859 bytes: 4096,
6860 paced: true,
6861 });
6862 client.inflight_bytes = 4096;
6863 let old_avg = client.avg_paced_frame_bytes;
6864 record_ack(&mut client);
6865 assert!(client.avg_paced_frame_bytes > old_avg);
6867 }
6868
6869 #[test]
6870 fn record_ack_unpaced_updates_avg_preview_frame_bytes() {
6871 let mut client = test_client();
6872 let now = Instant::now();
6873 client.inflight_frames.push_back(InFlightFrame {
6874 sent_at: now - Duration::from_millis(10),
6875 bytes: 8192,
6876 paced: false,
6877 });
6878 client.inflight_bytes = 8192;
6879 let old_avg = client.avg_preview_frame_bytes;
6880 record_ack(&mut client);
6881 assert!(client.avg_preview_frame_bytes > old_avg);
6882 }
6883
6884 #[test]
6887 fn pty_list_msg_empty_session() {
6888 let sess = Session::new();
6889 let msg = sess.pty_list_msg();
6890 assert_eq!(msg[0], S2C_LIST);
6891 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 0);
6892 assert_eq!(msg.len(), 3);
6893 }
6894
6895 #[test]
6896 fn pty_list_msg_includes_tags() {
6897 let _sess = Session::new();
6898 let tag1 = "shell";
6908 let tag2 = "build";
6909
6910 let mut expected = vec![S2C_LIST];
6912 expected.extend_from_slice(&2u16.to_le_bytes());
6913 expected.extend_from_slice(&1u16.to_le_bytes());
6915 expected.extend_from_slice(&(tag1.len() as u16).to_le_bytes());
6916 expected.extend_from_slice(tag1.as_bytes());
6917 expected.extend_from_slice(&3u16.to_le_bytes());
6919 expected.extend_from_slice(&(tag2.len() as u16).to_le_bytes());
6920 expected.extend_from_slice(tag2.as_bytes());
6921
6922 assert_eq!(expected[0], S2C_LIST);
6924 assert_eq!(u16::from_le_bytes([expected[1], expected[2]]), 2);
6925 let msg_str = String::from_utf8_lossy(&expected);
6927 assert!(msg_str.contains("shell"));
6928 assert!(msg_str.contains("build"));
6929 }
6930
6931 #[test]
6934 fn can_send_preview_true_when_due() {
6935 let mut client = test_client();
6936 let now = Instant::now();
6937 client
6938 .preview_next_send_at
6939 .insert(5, now - Duration::from_millis(100));
6940 assert!(can_send_preview(&client, 5, now));
6941 }
6942
6943 #[test]
6944 fn can_send_preview_false_when_not_due() {
6945 let mut client = test_client();
6946 let now = Instant::now();
6947 client
6948 .preview_next_send_at
6949 .insert(5, now + Duration::from_secs(10));
6950 assert!(!can_send_preview(&client, 5, now));
6951 }
6952
6953 #[test]
6954 fn can_send_preview_false_when_window_closed() {
6955 let mut client = test_client();
6956 client.browser_backlog_frames = 20;
6957 let now = Instant::now();
6958 assert!(!can_send_preview(&client, 5, now));
6959 }
6960
6961 #[test]
6962 fn can_send_preview_true_for_unseen_pid() {
6963 let client = test_client();
6964 let now = Instant::now();
6965 assert!(can_send_preview(&client, 99, now));
6967 }
6968
6969 #[test]
6970 fn record_preview_send_sets_future_deadline() {
6971 let mut client = test_client();
6972 let now = Instant::now();
6973 record_preview_send(&mut client, 5, now);
6974 let deadline = client.preview_next_send_at.get(&5).unwrap();
6975 assert!(*deadline > now);
6976 }
6977
6978 #[test]
6979 fn record_preview_send_successive_calls_advance() {
6980 let mut client = test_client();
6981 let now = Instant::now();
6982 record_preview_send(&mut client, 5, now);
6983 let first = *client.preview_next_send_at.get(&5).unwrap();
6984 record_preview_send(&mut client, 5, first);
6985 let second = *client.preview_next_send_at.get(&5).unwrap();
6986 assert!(second > first, "successive sends should advance deadline");
6987 }
6988
6989 fn browser_ready_high_bandwidth_client() -> ClientState {
7003 let mut c = test_client();
7004 c.display_fps = 120.0;
7005 c.rtt_ms = 1.0;
7006 c.min_rtt_ms = 1.0;
7007 c.goodput_bps = 50_000_000.0;
7008 c.delivery_bps = 50_000_000.0;
7009 c.last_goodput_sample_bps = 50_000_000.0;
7010 c.avg_paced_frame_bytes = 30_000.0;
7011 c.avg_preview_frame_bytes = 1_024.0;
7012 c.avg_frame_bytes = 30_000.0;
7013 c.browser_apply_ms = 0.3;
7014 c
7015 }
7016
7017 fn congested_client() -> ClientState {
7020 let mut c = test_client();
7021 c.display_fps = 120.0;
7022 c.rtt_ms = 500.0;
7023 c.min_rtt_ms = 40.0;
7024 c.goodput_bps = 200_000.0;
7025 c.delivery_bps = 150_000.0;
7026 c.last_goodput_sample_bps = 200_000.0;
7027 c.avg_paced_frame_bytes = 50_000.0;
7028 c.avg_preview_frame_bytes = 1_024.0;
7029 c.avg_frame_bytes = 50_000.0;
7030 c.goodput_jitter_bps = 50_000.0;
7031 c.max_goodput_jitter_bps = 200_000.0;
7032 c.browser_apply_ms = 1.0;
7033 c
7034 }
7035
7036 fn sim_ack(client: &mut ClientState, bytes: usize, rtt_ms: f32) {
7040 let sent_at = Instant::now() - Duration::from_millis(rtt_ms as u64);
7041 client.inflight_bytes += bytes;
7042 client.inflight_frames.push_back(InFlightFrame {
7043 sent_at,
7044 bytes,
7045 paced: true,
7046 });
7047 client.goodput_window_start = Instant::now() - Duration::from_millis(25);
7049 record_ack(client);
7050 }
7051
7052 fn sim_acks(client: &mut ClientState, n: usize, bytes: usize, rtt_ms: f32) {
7053 for _ in 0..n {
7054 sim_ack(client, bytes, rtt_ms);
7055 }
7056 }
7057
7058 #[test]
7061 fn browser_ready_high_bandwidth_client_uses_full_display_fps() {
7062 let client = browser_ready_high_bandwidth_client();
7063 assert!(
7064 (pacing_fps(&client) - client.display_fps).abs() < 0.01,
7065 "pacing_fps {} should equal display_fps {} when browser is ready and bandwidth is abundant",
7066 pacing_fps(&client),
7067 client.display_fps,
7068 );
7069 }
7070
7071 #[test]
7072 fn browser_ready_high_bandwidth_client_send_interval_within_one_frame() {
7073 let client = browser_ready_high_bandwidth_client();
7074 let interval_ms = send_interval(&client).as_secs_f32() * 1000.0;
7075 let frame_ms = 1000.0 / client.display_fps;
7076 assert!(
7077 interval_ms <= frame_ms + 0.1,
7078 "send_interval {interval_ms:.2}ms exceeds one frame ({frame_ms:.2}ms) when browser is ready"
7079 );
7080 }
7081
7082 #[test]
7085 fn congested_pipe_reduces_pacing_fps_substantially() {
7086 let client = congested_client();
7087 let fps = pacing_fps(&client);
7088 assert!(
7089 fps < client.display_fps * 0.5,
7090 "pacing_fps {fps:.0} should be well below display_fps {} when congested",
7091 client.display_fps,
7092 );
7093 }
7094
7095 #[test]
7096 fn congested_pipe_is_throughput_limited() {
7097 let client = congested_client();
7098 assert!(
7099 throughput_limited(&client),
7100 "congested client must be recognised as throughput-limited"
7101 );
7102 }
7103
7104 #[test]
7110 fn byte_window_bounded_near_bdp_when_congested() {
7111 let client = congested_client();
7112 let bdp = client.goodput_bps * (path_rtt_ms(&client) / 1_000.0);
7114 let window = target_byte_window(&client);
7115 assert!(
7116 window < bdp as usize * 8,
7117 "byte window {window}B is {:.1}× BDP ({bdp:.0}B); \
7118 expected ≤ 8× — lead_floor may be dominating",
7119 window as f32 / bdp.max(1.0),
7120 );
7121 }
7122
7123 #[test]
7129 fn min_rtt_not_contaminated_by_congested_rtts() {
7130 let mut client = test_client();
7131 client.display_fps = 120.0;
7132 client.rtt_ms = 40.0;
7133 client.min_rtt_ms = 40.0;
7134 client.goodput_bps = 2_000_000.0;
7135 client.delivery_bps = 2_000_000.0;
7136 client.avg_paced_frame_bytes = 30_000.0;
7137 client.avg_preview_frame_bytes = 1_024.0;
7138 let original_min = client.min_rtt_ms;
7139
7140 sim_acks(&mut client, 200, 30_000, 500.0);
7142
7143 assert!(
7144 client.min_rtt_ms < original_min * 2.0,
7145 "min_rtt drifted from {original_min}ms to {:.1}ms after 200 congested ACKs",
7146 client.min_rtt_ms,
7147 );
7148 }
7149
7150 #[test]
7153 fn delivery_bps_rises_quickly_when_congestion_clears() {
7154 let mut client = congested_client();
7155 let before = client.delivery_bps;
7156
7157 sim_acks(&mut client, 10, 30_000, 40.0);
7159
7160 assert!(
7161 client.delivery_bps > before * 2.0,
7162 "delivery_bps {:.0} should more than double from {before:.0} after 10 fast ACKs",
7163 client.delivery_bps,
7164 );
7165 }
7166
7167 #[test]
7168 fn pacing_fps_recovers_after_congestion_clears() {
7169 let mut client = congested_client();
7170
7171 for _ in 0..40 {
7177 let target = target_frame_window(&client).max(2);
7178 for _ in 0..target {
7179 let sent_at = Instant::now() - Duration::from_millis(40);
7180 client.inflight_bytes += 30_000;
7181 client.inflight_frames.push_back(InFlightFrame {
7182 sent_at,
7183 bytes: 30_000,
7184 paced: true,
7185 });
7186 }
7187 client.goodput_window_start = Instant::now() - Duration::from_millis(25);
7188 for _ in 0..target {
7189 record_ack(&mut client);
7190 }
7191 }
7192
7193 let fps = pacing_fps(&client);
7194 assert!(
7195 fps > client.display_fps * 0.7,
7196 "pacing_fps {fps:.0} didn't recover toward display_fps {} \
7197 after window-saturated rounds at low RTT",
7198 client.display_fps,
7199 );
7200 }
7201
7202 #[test]
7203 fn rtt_estimate_drops_quickly_when_congestion_clears() {
7204 let mut client = test_client();
7205 client.rtt_ms = 500.0;
7206 client.min_rtt_ms = 40.0;
7207 client.goodput_bps = 2_000_000.0;
7208 client.avg_paced_frame_bytes = 30_000.0;
7209 client.avg_preview_frame_bytes = 1_024.0;
7210
7211 sim_acks(&mut client, 10, 30_000, 40.0);
7214
7215 assert!(
7216 client.rtt_ms < 300.0,
7217 "rtt_ms {:.0}ms did not fall fast enough after congestion cleared",
7218 client.rtt_ms,
7219 );
7220 }
7221
7222 #[test]
7225 fn probe_collapses_immediately_on_queue_delay() {
7226 let mut client = test_client();
7227 client.display_fps = 120.0;
7228 client.rtt_ms = 40.0;
7229 client.min_rtt_ms = 40.0;
7230 client.goodput_bps = 5_000_000.0;
7231 client.delivery_bps = 5_000_000.0;
7232 client.last_goodput_sample_bps = 5_000_000.0;
7233 client.avg_paced_frame_bytes = 10_000.0;
7234 client.avg_preview_frame_bytes = 1_024.0;
7235 client.probe_frames = 10.0;
7236
7237 sim_acks(&mut client, 5, 10_000, 600.0);
7239
7240 assert!(
7241 client.probe_frames < 5.0,
7242 "probe_frames {:.1} should have collapsed on queue delay signal",
7243 client.probe_frames,
7244 );
7245 }
7246
7247 #[test]
7248 fn probe_grows_when_window_saturated_with_clean_rtt() {
7249 let mut client = test_client();
7250 client.display_fps = 120.0;
7251 client.rtt_ms = 40.0;
7252 client.min_rtt_ms = 40.0;
7253 client.goodput_bps = 5_000_000.0;
7254 client.delivery_bps = 5_000_000.0;
7255 client.last_goodput_sample_bps = 5_000_000.0;
7256 client.avg_paced_frame_bytes = 10_000.0;
7257 client.avg_preview_frame_bytes = 1_024.0;
7258 client.goodput_jitter_bps = 0.0;
7259 client.max_goodput_jitter_bps = 0.0;
7260 client.probe_frames = 0.0;
7261
7262 let target = target_frame_window(&client);
7264 for _ in 0..target {
7265 let sent_at = Instant::now() - Duration::from_millis(40);
7266 client.inflight_bytes += 10_000;
7267 client.inflight_frames.push_back(InFlightFrame {
7268 sent_at,
7269 bytes: 10_000,
7270 paced: true,
7271 });
7272 }
7273
7274 record_ack(&mut client);
7283
7284 assert!(
7285 client.probe_frames > 0.0,
7286 "probe_frames should grow when window-saturated with clean RTT"
7287 );
7288 }
7289
7290 #[test]
7293 fn frame_window_larger_on_high_latency_link() {
7294 let mut lo = test_client();
7295 lo.display_fps = 120.0;
7296 lo.rtt_ms = 10.0;
7297 lo.min_rtt_ms = 10.0;
7298 lo.goodput_bps = 5_000_000.0;
7299 lo.delivery_bps = 5_000_000.0;
7300 lo.avg_paced_frame_bytes = 10_000.0;
7301 lo.avg_preview_frame_bytes = 1_024.0;
7302
7303 let mut hi = test_client();
7304 hi.display_fps = 120.0;
7305 hi.rtt_ms = 200.0;
7306 hi.min_rtt_ms = 200.0;
7307 hi.goodput_bps = 5_000_000.0;
7308 hi.delivery_bps = 5_000_000.0;
7309 hi.avg_paced_frame_bytes = 10_000.0;
7310 hi.avg_preview_frame_bytes = 1_024.0;
7311
7312 let lo_win = target_frame_window(&lo);
7313 let hi_win = target_frame_window(&hi);
7314 assert!(
7315 hi_win > lo_win,
7316 "high-latency link ({hi_win}f) should need more frames in flight \
7317 than low-latency ({lo_win}f)"
7318 );
7319 }
7320
7321 #[test]
7324 fn small_frame_byte_window_enables_pipelining() {
7325 let mut client = test_client();
7330 client.display_fps = 120.0;
7331 client.rtt_ms = 165.0;
7332 client.min_rtt_ms = 8.0;
7333 client.goodput_bps = 11_000.0; client.delivery_bps = 6_800.0;
7335 client.last_goodput_sample_bps = 11_000.0;
7336 client.avg_paced_frame_bytes = 1_120.0;
7337 client.avg_preview_frame_bytes = 1_024.0;
7338 client.goodput_jitter_bps = 4_300.0;
7339 client.max_goodput_jitter_bps = 6_500.0;
7340
7341 let window = target_byte_window(&client);
7342 let frames = target_frame_window(&client);
7343 let pipeline = frames * 1_120;
7344
7345 assert!(
7346 window >= pipeline,
7347 "byte window {window}B should be >= pipeline ({frames}f × 1120B = {pipeline}B) \
7348 so small frames can pipeline across the RTT"
7349 );
7350 }
7351
7352 #[test]
7353 fn large_frame_byte_window_bounded_by_one_frame_floor() {
7354 let mut client = test_client();
7358 client.display_fps = 120.0;
7359 client.rtt_ms = 165.0;
7360 client.min_rtt_ms = 8.0;
7361 client.goodput_bps = 11_000.0;
7362 client.delivery_bps = 6_800.0;
7363 client.last_goodput_sample_bps = 11_000.0;
7364 client.avg_paced_frame_bytes = 50_000.0; client.avg_preview_frame_bytes = 1_024.0;
7366 client.goodput_jitter_bps = 0.0;
7367 client.max_goodput_jitter_bps = 0.0;
7368
7369 let window = target_byte_window(&client);
7370 let frames = target_frame_window(&client);
7371 let pipeline = frames.saturating_mul(50_000);
7372
7373 assert!(
7374 window < pipeline,
7375 "byte window {window}B should be < full pipeline {pipeline}B \
7376 ({frames}f × 50KB) — large frames must use one-frame floor"
7377 );
7378 assert!(
7379 window >= 50_000,
7380 "byte window {window}B must be at least one frame (50KB)"
7381 );
7382 }
7383
7384 #[test]
7387 fn preview_reservation_applies_even_on_low_latency_high_bandwidth_links() {
7388 let mut client = browser_ready_high_bandwidth_client();
7389 client.lead = Some(1);
7390 client.subscriptions.insert(1);
7391 let target = target_frame_window(&client);
7392 fill_inflight(&mut client, target.saturating_sub(1), 512);
7393 assert!(
7394 !lead_window_open(&client, true),
7395 "preview reservation should apply uniformly for lead clients"
7396 );
7397 }
7398
7399 #[test]
7402 fn probe_recovers_on_healthy_path_after_blip() {
7403 let mut client = browser_ready_high_bandwidth_client();
7404 client.probe_frames = 8.0;
7405
7406 sim_acks(&mut client, 3, 30_000, 200.0);
7408 let post_blip = client.probe_frames;
7409 assert!(
7410 post_blip < 4.0,
7411 "probe_frames {post_blip:.1} should have dropped after blip"
7412 );
7413
7414 client.browser_backlog_frames = 0;
7416 client.browser_ack_ahead_frames = 0;
7417 client.browser_apply_ms = 0.3;
7418
7419 sim_acks(&mut client, 20, 30_000, 1.0);
7421
7422 assert!(
7423 client.probe_frames > post_blip,
7424 "probe_frames {:.1} should have recovered from {post_blip:.1} after healthy ACKs",
7425 client.probe_frames,
7426 );
7427 }
7428
7429 #[test]
7430 fn jitter_decays_fast_on_browser_ready_path() {
7431 let mut client = browser_ready_high_bandwidth_client();
7432
7433 client.max_goodput_jitter_bps = client.goodput_bps * 0.4;
7435 client.goodput_jitter_bps = client.goodput_bps * 0.3;
7436 let initial_jitter = client.max_goodput_jitter_bps;
7437
7438 sim_acks(&mut client, 10, 30_000, 1.0);
7440
7441 assert!(
7442 client.max_goodput_jitter_bps < initial_jitter * 0.5,
7443 "max_goodput_jitter_bps {:.0} should have decayed below {:.0} \
7444 (50% of initial {initial_jitter:.0}) after 10 healthy ACKs on a ready path",
7445 client.max_goodput_jitter_bps,
7446 initial_jitter * 0.5,
7447 );
7448 }
7449
7450 #[test]
7451 fn byte_budget_uses_floor_when_goodput_depressed() {
7452 let mut client = browser_ready_high_bandwidth_client();
7453 client.goodput_bps = 100_000.0;
7454
7455 let budget = byte_budget_for(&client, 100.0);
7456 let floor_budget = (bandwidth_floor_bps(&client) * 100.0 / 1_000.0).ceil() as usize;
7457
7458 assert!(
7459 budget >= floor_budget,
7460 "byte_budget {budget} should be at least bandwidth_floor-based {floor_budget} \
7461 when goodput_bps is depressed but delivery_bps is high"
7462 );
7463 }
7464
7465 #[test]
7466 fn probe_floor_maintained_under_congestion_signal() {
7467 let mut client = test_client();
7468 client.display_fps = 120.0;
7469 client.rtt_ms = 40.0;
7470 client.min_rtt_ms = 40.0;
7471 client.goodput_bps = 5_000_000.0;
7472 client.delivery_bps = 5_000_000.0;
7473 client.last_goodput_sample_bps = 5_000_000.0;
7474 client.avg_paced_frame_bytes = 10_000.0;
7475 client.avg_preview_frame_bytes = 1_024.0;
7476 client.probe_frames = 10.0;
7477
7478 sim_acks(&mut client, 20, 10_000, 600.0);
7480
7481 assert!(
7482 client.probe_frames >= 1.0,
7483 "probe_frames {:.1} should not drop below the floor of 1.0",
7484 client.probe_frames,
7485 );
7486 }
7487
7488 #[test]
7491 fn parse_tq_da1_bare() {
7492 let results = parse_terminal_queries(b"\x1b[c", (24, 80), (0, 0));
7493 assert_eq!(results.len(), 1);
7494 assert!(results[0].starts_with("\x1b[?64;"));
7495 }
7496
7497 #[test]
7498 fn parse_tq_da1_with_zero_param() {
7499 let results = parse_terminal_queries(b"\x1b[0c", (24, 80), (0, 0));
7500 assert_eq!(results.len(), 1);
7501 assert!(results[0].starts_with("\x1b[?64;"));
7502 }
7503
7504 #[test]
7505 fn parse_tq_dsr_cursor_position() {
7506 let results = parse_terminal_queries(b"\x1b[6n", (24, 80), (5, 10));
7507 assert_eq!(results.len(), 1);
7508 assert_eq!(results[0], "\x1b[6;11R");
7509 }
7510
7511 #[test]
7512 fn parse_tq_dsr_status() {
7513 let results = parse_terminal_queries(b"\x1b[5n", (24, 80), (0, 0));
7514 assert_eq!(results.len(), 1);
7515 assert_eq!(results[0], "\x1b[0n");
7516 }
7517
7518 #[test]
7519 fn parse_tq_window_size_cells() {
7520 let results = parse_terminal_queries(b"\x1b[18t", (24, 80), (0, 0));
7521 assert_eq!(results.len(), 1);
7522 assert_eq!(results[0], "\x1b[8;24;80t");
7523 }
7524
7525 #[test]
7526 fn parse_tq_window_size_pixels() {
7527 let results = parse_terminal_queries(b"\x1b[14t", (30, 100), (0, 0));
7528 assert_eq!(results.len(), 1);
7529 assert_eq!(results[0], "\x1b[4;480;800t");
7530 }
7531
7532 #[test]
7533 fn parse_tq_multiple_queries() {
7534 let data = b"\x1b[c\x1b[6n\x1b[5n";
7535 let results = parse_terminal_queries(data, (24, 80), (2, 3));
7536 assert_eq!(results.len(), 3);
7537 assert!(results[0].starts_with("\x1b[?64;"));
7538 assert_eq!(results[1], "\x1b[3;4R");
7539 assert_eq!(results[2], "\x1b[0n");
7540 }
7541
7542 #[test]
7543 fn parse_tq_question_mark_sequences_skipped() {
7544 let results = parse_terminal_queries(b"\x1b[?1h", (24, 80), (0, 0));
7545 assert!(results.is_empty());
7546 }
7547
7548 #[test]
7549 fn parse_tq_unknown_final_byte_ignored() {
7550 let results = parse_terminal_queries(b"\x1b[42z", (24, 80), (0, 0));
7551 assert!(results.is_empty());
7552 }
7553
7554 #[test]
7555 fn parse_tq_empty_input() {
7556 let results = parse_terminal_queries(b"", (24, 80), (0, 0));
7557 assert!(results.is_empty());
7558 }
7559
7560 #[test]
7561 fn parse_tq_plain_text_no_csi() {
7562 let results = parse_terminal_queries(b"hello world", (24, 80), (0, 0));
7563 assert!(results.is_empty());
7564 }
7565
7566 #[test]
7567 fn parse_tq_interleaved_with_text() {
7568 let results = parse_terminal_queries(b"abc\x1b[cdef\x1b[6n", (24, 80), (1, 2));
7569 assert_eq!(results.len(), 2);
7570 }
7571
7572 #[test]
7575 fn parse_tq_osc11_background_color_bel() {
7576 let results = parse_terminal_queries(b"\x1b]11;?\x07", (24, 80), (0, 0));
7577 assert_eq!(results.len(), 1);
7578 assert_eq!(results[0], "\x1b]11;rgb:0000/0000/0000\x1b\\");
7579 }
7580
7581 #[test]
7582 fn parse_tq_osc11_background_color_st() {
7583 let results = parse_terminal_queries(b"\x1b]11;?\x1b\\", (24, 80), (0, 0));
7584 assert_eq!(results.len(), 1);
7585 assert_eq!(results[0], "\x1b]11;rgb:0000/0000/0000\x1b\\");
7586 }
7587
7588 #[test]
7589 fn parse_tq_osc10_foreground_color() {
7590 let results = parse_terminal_queries(b"\x1b]10;?\x07", (24, 80), (0, 0));
7591 assert_eq!(results.len(), 1);
7592 assert_eq!(results[0], "\x1b]10;rgb:ffff/ffff/ffff\x1b\\");
7593 }
7594
7595 #[test]
7596 fn parse_tq_osc4_palette_color_0() {
7597 let results = parse_terminal_queries(b"\x1b]4;0;?\x07", (24, 80), (0, 0));
7598 assert_eq!(results.len(), 1);
7599 assert_eq!(results[0], "\x1b]4;0;rgb:0000/0000/0000\x1b\\");
7600 }
7601
7602 #[test]
7603 fn parse_tq_osc4_palette_color_1() {
7604 let results = parse_terminal_queries(b"\x1b]4;1;?\x07", (24, 80), (0, 0));
7605 assert_eq!(results.len(), 1);
7606 assert_eq!(results[0], "\x1b]4;1;rgb:8080/0000/0000\x1b\\");
7607 }
7608
7609 #[test]
7610 fn parse_tq_osc_mixed_with_csi() {
7611 let results =
7612 parse_terminal_queries(b"\x1b]11;?\x07\x1b[c\x1b]4;0;?\x07", (24, 80), (0, 0));
7613 assert_eq!(results.len(), 3);
7614 assert!(results[0].starts_with("\x1b]11;"));
7615 assert!(results[1].starts_with("\x1b[?64;"));
7616 assert!(results[2].starts_with("\x1b]4;0;"));
7617 }
7618
7619 #[test]
7622 fn search_results_empty() {
7623 let msg = build_search_results_msg(42, &[]);
7624 assert_eq!(msg[0], S2C_SEARCH_RESULTS);
7625 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 42);
7626 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 0);
7627 assert_eq!(msg.len(), 5);
7628 }
7629
7630 #[test]
7631 fn search_results_single() {
7632 let results = vec![SearchResultRow {
7633 pty_id: 7,
7634 score: 100,
7635 primary_source: 1,
7636 matched_sources: 3,
7637 context: "hello".into(),
7638 scroll_offset: Some(42),
7639 }];
7640 let msg = build_search_results_msg(1, &results);
7641 assert_eq!(msg[0], S2C_SEARCH_RESULTS);
7642 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 1);
7643 let pty_id = u16::from_le_bytes([msg[5], msg[6]]);
7644 assert_eq!(pty_id, 7);
7645 let score = u32::from_le_bytes([msg[7], msg[8], msg[9], msg[10]]);
7646 assert_eq!(score, 100);
7647 assert_eq!(msg[11], 1);
7648 assert_eq!(msg[12], 3);
7649 let scroll = u32::from_le_bytes([msg[13], msg[14], msg[15], msg[16]]);
7650 assert_eq!(scroll, 42);
7651 let ctx_len = u16::from_le_bytes([msg[17], msg[18]]) as usize;
7652 assert_eq!(ctx_len, 5);
7653 assert_eq!(&msg[19..19 + ctx_len], b"hello");
7654 }
7655
7656 #[test]
7657 fn search_results_none_scroll_offset() {
7658 let results = vec![SearchResultRow {
7659 pty_id: 1,
7660 score: 0,
7661 primary_source: 0,
7662 matched_sources: 0,
7663 context: String::new(),
7664 scroll_offset: None,
7665 }];
7666 let msg = build_search_results_msg(0, &results);
7667 let scroll = u32::from_le_bytes([msg[13], msg[14], msg[15], msg[16]]);
7668 assert_eq!(scroll, u32::MAX);
7669 }
7670
7671 #[test]
7674 fn allocate_pty_id_empty_session() {
7675 let mut sess = Session::new();
7676 assert_eq!(sess.allocate_pty_id(0), Some(1));
7677 }
7678
7679 #[test]
7680 fn allocate_pty_id_rotates() {
7681 let mut sess = Session::new();
7682 assert_eq!(sess.allocate_pty_id(0), Some(1));
7684 assert_eq!(sess.allocate_pty_id(0), Some(2));
7685 assert_eq!(sess.allocate_pty_id(0), Some(3));
7686 }
7687
7688 #[test]
7689 fn allocate_pty_id_wraps_at_max() {
7690 let mut sess = Session::new();
7691 sess.next_pty_id = u16::MAX;
7692 assert_eq!(sess.allocate_pty_id(0), Some(u16::MAX));
7693 assert_eq!(sess.allocate_pty_id(0), Some(1));
7695 }
7696
7697 #[test]
7700 fn try_send_no_change() {
7701 let mut client = test_client();
7702 let frame = sample_frame("x");
7703 let now = Instant::now();
7704 let outcome = try_send_update(&mut client, 1, frame, None, now, false);
7705 assert!(matches!(outcome, SendOutcome::NoChange));
7706 }
7707
7708 #[test]
7709 fn try_send_sent() {
7710 let (mut client, _rx) = test_client_with_capacity(8);
7711 let frame = sample_frame("x");
7712 let now = Instant::now();
7713 let outcome = try_send_update(
7714 &mut client,
7715 1,
7716 frame.clone(),
7717 Some(vec![1, 2, 3]),
7718 now,
7719 true,
7720 );
7721 assert!(matches!(outcome, SendOutcome::Sent));
7722 assert!(client.last_sent.contains_key(&1));
7723 }
7724
7725 #[test]
7726 fn try_send_backpressured_on_disconnect() {
7727 let (mut client, rx) = test_client_with_capacity(0);
7728 let frame = sample_frame("x");
7729 let now = Instant::now();
7730 drop(rx);
7732 let outcome = try_send_update(
7733 &mut client,
7734 1,
7735 frame.clone(),
7736 Some(vec![1, 2, 3]),
7737 now,
7738 true,
7739 );
7740 assert!(matches!(outcome, SendOutcome::Backpressured));
7741 assert!(
7742 client.last_sent.contains_key(&1),
7743 "last_sent should advance even on disconnect"
7744 );
7745 }
7746}