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