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;
750 if backlog > 2.0 {
751 fps = fps.min(fps * (2.0 / backlog));
752 }
753
754 if client.browser_ack_ahead_frames > 4 {
755 fps = fps.min(client.display_fps.max(1.0) * 0.5);
756 }
757 if client.browser_ack_ahead_frames > 8 {
758 fps = fps.min(client.display_fps.max(1.0) * 0.25);
759 }
760
761 fps.max(1.0)
762}
763
764fn browser_backlog_blocked(client: &ClientState) -> bool {
765 client.browser_backlog_frames > 8
766}
767
768fn byte_budget_for(client: &ClientState, budget_ms: f32) -> usize {
769 let budget_bps = if throughput_limited(client) {
770 bandwidth_floor_bps(client)
771 } else {
772 client.goodput_bps.max(bandwidth_floor_bps(client))
773 };
774 let bytes = budget_bps * budget_ms.max(1.0) / 1_000.0;
775 bytes.ceil().max(client.avg_frame_bytes.max(256.0)) as usize
776}
777
778fn target_byte_window(client: &ClientState) -> usize {
779 let budget = byte_budget_for(client, path_rtt_ms(client) + target_queue_ms(client));
780 let frame_bytes = client.avg_paced_frame_bytes.max(256.0).ceil() as usize;
781 let target_frames = target_frame_window(client);
782 let pipeline_bytes = frame_bytes.saturating_mul(target_frames);
783 const PIPELINE_FLOOR_LIMIT: usize = 32_768; let floor = if pipeline_bytes <= PIPELINE_FLOOR_LIMIT {
790 pipeline_bytes
791 } else {
792 frame_bytes };
794 budget.max(floor)
795}
796
797fn send_interval(client: &ClientState) -> Duration {
798 Duration::from_secs_f64(1.0 / browser_pacing_fps(client).max(1.0) as f64)
799}
800
801fn preview_fps(client: &ClientState) -> f32 {
802 let mut fps = client.display_fps.max(1.0);
803 if client.lead.is_some() && throughput_limited(client) {
804 let avail = bandwidth_floor_bps(client);
809 let lead_bps = client.avg_paced_frame_bytes.max(256.0) * browser_pacing_fps(client);
810 let preview_budget = (avail - lead_bps).max(avail * 0.25).max(0.0);
811 let bw_cap = preview_budget / client.avg_preview_frame_bytes.max(256.0);
812 fps = fps.min(bw_cap.max(1.0));
813 }
814 fps.max(1.0)
815}
816
817fn preview_send_interval(client: &ClientState) -> Duration {
818 Duration::from_secs_f64(1.0 / preview_fps(client) as f64)
819}
820
821fn surface_pacing_fps(client: &ClientState) -> f32 {
826 browser_pacing_fps(client)
827}
828
829fn surface_send_interval(client: &ClientState) -> Duration {
830 Duration::from_secs_f64(1.0 / surface_pacing_fps(client).max(1.0) as f64)
831}
832
833fn maybe_log_pacing_metrics(sess: &mut Session, client_id: u64, verbose: bool) {
837 let Some(c) = sess.clients.get_mut(&client_id) else {
838 return;
839 };
840 if c.last_log.elapsed().as_secs_f32() < 10.0 {
841 return;
842 }
843 let log_elapsed = c.last_log.elapsed().as_secs_f32().max(1.0e-3);
844 let paced_fps = pacing_fps(c);
845 let display_need_bps_v = display_need_bps(c);
846 let surface_fps = surface_pacing_fps(c);
847 let frames_sent = c.frames_sent;
848 let acks_recv = c.acks_recv;
849 let rtt_ms = c.rtt_ms;
850 let min_rtt_ms = path_rtt_ms(c);
851 let eff_rtt_ms = window_rtt_ms(c);
852 let inflight_bytes = c.inflight_bytes;
853 let delivery_bps = c.delivery_bps;
854 let goodput_ewma_bps = c.goodput_bps;
855 let goodput_jitter_bps = c.goodput_jitter_bps;
856 let max_goodput_jitter_bps = c.max_goodput_jitter_bps;
857 let avg_frame_bytes = c.avg_frame_bytes;
858 let avg_paced_frame_bytes = c.avg_paced_frame_bytes;
859 let avg_preview_frame_bytes = c.avg_preview_frame_bytes;
860 let display_fps = c.display_fps;
861 let probe_frames = c.probe_frames;
862 let goodput_bps = c.acked_bytes_since_log as f32 / log_elapsed;
863 let window_frames = target_frame_window(c);
864 let window_bytes = target_byte_window(c);
865 let outbox_frames = outbox_queued_frames(c);
866 let browser_backlog_frames = c.browser_backlog_frames;
867 let browser_ack_ahead_frames = c.browser_ack_ahead_frames;
868 let browser_apply_ms = c.browser_apply_ms;
869 let avg_surface_frame_bytes = c.avg_surface_frame_bytes;
870 let skip_same_gen = c.skip_same_gen_count;
871 let skip_in_flight = c.skip_in_flight_count;
872 let skip_pacing = c.skip_pacing_count;
873 let skip_vk_await = c.skip_vulkan_await_count;
874 let skip_no_subs = c.skip_no_subs_count;
875 let skip_not_subbed = c.skip_not_subbed_count;
876 let skip_mismatch = c.skip_last_pixels_mismatch_count;
877 let loop_iters = c.encode_loop_iters;
878 let own_subs: usize = c.surface_subscriptions.len();
879 let vk_surfs = c.vulkan_video_surfaces.len();
880 let in_flight_set_len = c
881 .surface_subs
882 .values()
883 .filter(|s| s.encode_in_flight)
884 .count();
885 let surface_burst: u8 = c
886 .surface_subs
887 .values()
888 .map(|s| s.burst_remaining)
889 .max()
890 .unwrap_or(0);
891
892 c.frames_sent = 0;
893 c.acks_recv = 0;
894 c.acked_bytes_since_log = 0;
895 c.skip_same_gen_count = 0;
896 c.skip_in_flight_count = 0;
897 c.skip_pacing_count = 0;
898 c.skip_vulkan_await_count = 0;
899 c.skip_no_subs_count = 0;
900 c.skip_not_subbed_count = 0;
901 c.skip_last_pixels_mismatch_count = 0;
902 c.encode_loop_iters = 0;
903 c.last_log = Instant::now();
904
905 if verbose {
906 let surf_info = sess.compositor.as_ref().map(|cs| {
907 let surfaces = cs.surfaces.len();
908 let pending = 0usize;
909 let subs: usize = sess
910 .clients
911 .values()
912 .map(|c| c.surface_subscriptions.len())
913 .sum();
914 (surfaces, pending, subs)
915 });
916 let (surf_count, surf_pending, surf_subs) = surf_info.unwrap_or((0, 0, 0));
917 eprintln!(
918 "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}",
919 sess.tick_fires,
920 sess.tick_snaps,
921 sess.surface_commits,
922 sess.surface_encodes,
923 sess.surface_encode_bytes,
924 sess.surface_frames_sent,
925 sess.ticks_pixel_snapshot_empty,
926 sess.pixel_snapshot_len,
927 );
928 }
929 sess.tick_fires = 0;
930 sess.tick_snaps = 0;
931 sess.surface_commits = 0;
932 sess.surface_encodes = 0;
933 sess.surface_encode_bytes = 0;
934 sess.surface_frames_sent = 0;
935 sess.ticks_pixel_snapshot_empty = 0;
936}
937
938fn advance_deadline(deadline: &mut Instant, now: Instant, interval: Duration) {
939 let scheduled = deadline.checked_add(interval).unwrap_or(now + interval);
940 *deadline = if scheduled + interval < now {
941 now + interval
942 } else {
943 scheduled
944 };
945}
946
947fn should_snapshot_pty(dirty: bool, needful: bool, synced_output: bool) -> bool {
948 dirty && needful && !synced_output
949}
950
951fn enqueue_ready_frame(queue: &mut VecDeque<FrameState>, frame: FrameState) -> bool {
952 if queue.len() >= READY_FRAME_QUEUE_CAP {
953 return false;
954 }
955 queue.push_back(frame);
956 true
957}
958
959fn pty_has_visual_update(pty: &Pty) -> bool {
960 pty.dirty || !pty.ready_frames.is_empty() || !pty.byte_rx.is_empty()
961}
962
963fn find_sync_output_end(prefix: &[u8], bytes: &[u8]) -> Option<usize> {
967 if bytes.is_empty() {
968 return None;
969 }
970 let needle = SYNC_OUTPUT_END;
971 let nlen = needle.len();
972
973 if !prefix.is_empty() {
975 let tail = if prefix.len() >= nlen - 1 {
976 &prefix[prefix.len() - (nlen - 1)..]
977 } else {
978 prefix
979 };
980 let combined_len = tail.len() + bytes.len().min(nlen);
981 if combined_len >= nlen {
982 let mut buf = [0u8; 32]; let blen = combined_len.min(buf.len());
985 let tlen = tail.len().min(blen);
986 buf[..tlen].copy_from_slice(&tail[..tlen]);
987 let rest = (blen - tlen).min(bytes.len());
988 buf[tlen..tlen + rest].copy_from_slice(&bytes[..rest]);
989 for i in 0..=(blen.saturating_sub(nlen)) {
990 if &buf[i..i + nlen] == needle {
991 let end_in_bytes = (i + nlen).saturating_sub(tail.len());
992 if end_in_bytes > 0 && end_in_bytes <= bytes.len() {
993 return Some(end_in_bytes);
994 }
995 }
996 }
997 }
998 }
999
1000 let mut offset = 0;
1002 while let Some(pos) = memchr::memchr(0x1b, &bytes[offset..]) {
1003 let abs = offset + pos;
1004 if abs + nlen <= bytes.len() && &bytes[abs..abs + nlen] == needle {
1005 return Some(abs + nlen);
1006 }
1007 offset = abs + 1;
1008 }
1009 None
1010}
1011
1012fn update_sync_scan_tail(tail: &mut Vec<u8>, bytes: &[u8]) {
1013 if bytes.is_empty() {
1014 return;
1015 }
1016 tail.extend_from_slice(bytes);
1017 let keep = SYNC_OUTPUT_END.len().saturating_sub(1);
1018 if tail.len() > keep {
1019 let drop = tail.len() - keep;
1020 tail.drain(..drop);
1021 }
1022}
1023
1024fn preview_deadline(client: &ClientState, pid: u16, now: Instant) -> Instant {
1025 client
1026 .preview_next_send_at
1027 .get(&pid)
1028 .copied()
1029 .unwrap_or(now)
1030}
1031
1032fn client_has_due_preview(sess: &Session, client: &ClientState, now: Instant) -> bool {
1033 if client.lead.is_none() {
1034 return false;
1035 }
1036 client.subscriptions.iter().copied().any(|pid| {
1037 Some(pid) != client.lead
1038 && preview_deadline(client, pid, now) <= now
1039 && sess
1040 .ptys
1041 .get(&pid)
1042 .map(pty_has_visual_update)
1043 .unwrap_or(false)
1044 })
1045}
1046
1047fn outbox_queued_frames(client: &ClientState) -> usize {
1048 client.outbox_queued_frames.load(Ordering::Relaxed)
1049}
1050
1051fn outbox_queued_bytes(client: &ClientState) -> usize {
1052 client.outbox_queued_bytes.load(Ordering::Relaxed)
1053}
1054
1055fn outbox_backpressured(client: &ClientState) -> bool {
1056 let frames = outbox_queued_frames(client);
1064 if frames >= OUTBOX_SOFT_QUEUE_LIMIT_FRAMES {
1065 return true;
1066 }
1067 frames >= 2 && outbox_queued_bytes(client) >= OUTBOX_SOFT_QUEUE_LIMIT_BYTES
1068}
1069
1070fn mark_outbox_drained(
1071 queued_frames: &Arc<AtomicUsize>,
1072 queued_bytes: &Arc<AtomicUsize>,
1073 bytes: usize,
1074) {
1075 let _ = queued_frames.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
1076 Some(value.saturating_sub(1))
1077 });
1078 let _ = queued_bytes.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
1079 Some(value.saturating_sub(bytes))
1080 });
1081}
1082
1083fn send_outbox_tracked(
1084 tx: &mpsc::UnboundedSender<Vec<u8>>,
1085 queued_frames: &Arc<AtomicUsize>,
1086 queued_bytes: &Arc<AtomicUsize>,
1087 msg: Vec<u8>,
1088) -> Result<(), mpsc::error::SendError<Vec<u8>>> {
1089 let bytes = msg.len();
1090 tx.send(msg)?;
1091 queued_frames.fetch_add(1, Ordering::Relaxed);
1092 queued_bytes.fetch_add(bytes, Ordering::Relaxed);
1093 Ok(())
1094}
1095
1096fn send_outbox(client: &ClientState, msg: Vec<u8>) -> Result<(), mpsc::error::SendError<Vec<u8>>> {
1097 send_outbox_tracked(
1098 &client.tx,
1099 &client.outbox_queued_frames,
1100 &client.outbox_queued_bytes,
1101 msg,
1102 )
1103}
1104
1105fn can_send_preview(client: &ClientState, pid: u16, now: Instant) -> bool {
1106 window_open(client) && now >= preview_deadline(client, pid, now)
1107}
1108
1109fn record_preview_send(client: &mut ClientState, pid: u16, now: Instant) {
1110 let mut deadline = client
1111 .preview_next_send_at
1112 .get(&pid)
1113 .copied()
1114 .unwrap_or(now);
1115 advance_deadline(&mut deadline, now, preview_send_interval(client));
1116 client.preview_next_send_at.insert(pid, deadline);
1117}
1118
1119fn window_open(client: &ClientState) -> bool {
1120 !browser_backlog_blocked(client)
1121 && !outbox_backpressured(client)
1122 && client.inflight_frames.len() < target_frame_window(client)
1123 && client.inflight_bytes < target_byte_window(client)
1124}
1125
1126fn surface_window_open(client: &ClientState) -> bool {
1130 !outbox_backpressured(client)
1131}
1132
1133fn lead_window_open(client: &ClientState, reserve_preview_slot: bool) -> bool {
1134 if !reserve_preview_slot || client.lead.is_none() {
1135 return window_open(client);
1136 }
1137 if browser_backlog_blocked(client) || outbox_backpressured(client) {
1138 return false;
1139 }
1140 let target_frames = target_frame_window(client);
1141 let reserve_frames = PREVIEW_FRAME_RESERVE.min(target_frames.saturating_sub(1));
1142 let frame_limit = target_frames.saturating_sub(reserve_frames).max(1);
1143 let reserve_bytes = client.avg_preview_frame_bytes.max(256.0).ceil() as usize;
1144 let byte_limit = target_byte_window(client)
1145 .saturating_sub(reserve_bytes)
1146 .max(client.avg_paced_frame_bytes.max(256.0).ceil() as usize);
1147 client.inflight_frames.len() < frame_limit && client.inflight_bytes < byte_limit
1148}
1149
1150fn can_send_frame(client: &ClientState, now: Instant, reserve_preview_slot: bool) -> bool {
1151 lead_window_open(client, reserve_preview_slot) && now >= client.next_send_at
1152}
1153
1154fn record_send(client: &mut ClientState, bytes: usize, now: Instant, paced: bool) {
1155 client.inflight_bytes += bytes;
1156 client.inflight_frames.push_back(InFlightFrame {
1157 sent_at: now,
1158 bytes,
1159 paced,
1160 });
1161 if paced {
1162 let interval = send_interval(client);
1163 advance_deadline(&mut client.next_send_at, now, interval);
1164 }
1165}
1166
1167fn ewma_with_direction(old: f32, sample: f32, rise_alpha: f32, fall_alpha: f32) -> f32 {
1168 let alpha = if sample > old { rise_alpha } else { fall_alpha };
1169 old * (1.0 - alpha) + sample * alpha
1170}
1171
1172fn window_saturated(client: &ClientState, inflight_frames: usize, inflight_bytes: usize) -> bool {
1173 let target_frames = target_frame_window(client);
1174 let target_bytes = target_byte_window(client);
1175 inflight_frames.saturating_mul(10) >= target_frames.saturating_mul(9)
1176 || inflight_bytes.saturating_mul(10) >= target_bytes.saturating_mul(9)
1177}
1178
1179fn record_ack(client: &mut ClientState) {
1180 if let Some(frame) = client.inflight_frames.pop_front() {
1181 let prev_inflight_frames = client.inflight_frames.len() + 1;
1182 let prev_inflight_bytes = client.inflight_bytes;
1183 client.inflight_bytes = client.inflight_bytes.saturating_sub(frame.bytes);
1184 client.acked_bytes_since_log = client.acked_bytes_since_log.saturating_add(frame.bytes);
1185 let sample_ms = frame.sent_at.elapsed().as_secs_f32() * 1_000.0;
1186 client.rtt_ms = ewma_with_direction(client.rtt_ms, sample_ms, 0.125, 0.25);
1187 if client.min_rtt_ms > 0.0 {
1188 client.min_rtt_ms = client.min_rtt_ms.min(sample_ms);
1191 } else {
1192 client.min_rtt_ms = sample_ms;
1193 }
1194 client.min_rtt_ms = client.min_rtt_ms.max(0.5);
1195 let sample_bps = frame.bytes as f32 / sample_ms.max(1.0e-3) * 1_000.0;
1196 client.delivery_bps = ewma_with_direction(client.delivery_bps, sample_bps, 0.5, 0.125);
1197 client.avg_frame_bytes =
1198 ewma_with_direction(client.avg_frame_bytes, frame.bytes as f32, 0.5, 0.125);
1199 if frame.paced {
1200 client.avg_paced_frame_bytes =
1201 ewma_with_direction(client.avg_paced_frame_bytes, frame.bytes as f32, 0.5, 0.125);
1202 } else {
1203 client.avg_preview_frame_bytes = ewma_with_direction(
1204 client.avg_preview_frame_bytes,
1205 frame.bytes as f32,
1206 0.5,
1207 0.125,
1208 );
1209 }
1210 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
1211 let path_rtt = path_rtt_ms(client);
1212 let likely_window_limited =
1213 window_saturated(client, prev_inflight_frames, prev_inflight_bytes);
1214 client.goodput_window_bytes = client.goodput_window_bytes.saturating_add(frame.bytes);
1215 let now = Instant::now();
1216 let goodput_elapsed = now
1217 .duration_since(client.goodput_window_start)
1218 .as_secs_f32();
1219 if goodput_elapsed >= 0.02 {
1220 let sample_goodput = client.goodput_window_bytes as f32 / goodput_elapsed.max(1.0e-3);
1221 if likely_window_limited || client.browser_backlog_frames > 0 {
1222 let prev_goodput_sample = if client.last_goodput_sample_bps > 0.0 {
1223 client.last_goodput_sample_bps
1224 } else {
1225 sample_goodput
1226 };
1227 let jitter_sample = (sample_goodput - prev_goodput_sample).abs();
1228 client.goodput_bps =
1229 ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, 0.125);
1230 let min_reliable = (client.avg_paced_frame_bytes.max(256.0) * 2.0) as usize;
1236 if client.goodput_window_bytes >= min_reliable {
1237 client.goodput_jitter_bps =
1238 ewma_with_direction(client.goodput_jitter_bps, jitter_sample, 0.5, 0.125);
1239 let jitter_decay = if browser_ready(client) && sample_ms < path_rtt * 3.0 {
1240 0.90
1241 } else {
1242 0.98
1243 };
1244 client.max_goodput_jitter_bps =
1245 (client.max_goodput_jitter_bps * jitter_decay).max(jitter_sample);
1246 client.max_goodput_jitter_bps =
1250 client.max_goodput_jitter_bps.min(client.goodput_bps * 0.45);
1251 } else {
1252 client.goodput_jitter_bps *= 0.9;
1254 client.max_goodput_jitter_bps *= 0.95;
1255 }
1256 client.last_goodput_sample_bps =
1260 (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
1261 } else {
1262 let ratio = client.goodput_bps / sample_goodput.max(1.0);
1267 let fall_alpha = if ratio > 10.0 {
1268 0.5
1269 } else if ratio > 3.0 {
1270 0.25
1271 } else {
1272 0.03
1273 };
1274 client.goodput_bps =
1275 ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, fall_alpha);
1276 client.goodput_jitter_bps *= 0.5;
1277 client.max_goodput_jitter_bps *= 0.9;
1278 client.last_goodput_sample_bps =
1279 (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
1280 }
1281 client.goodput_window_bytes = 0;
1282 client.goodput_window_start = now;
1283 }
1284 let queue_baseline_ms = if throughput_limited(client) {
1285 window_rtt_ms(client)
1286 } else {
1287 path_rtt
1288 };
1289 let queue_delay_ms = (sample_ms - queue_baseline_ms).max(0.0);
1290 let max_probe_frames = (browser_pacing_fps(client) * 0.125).max(4.0);
1291 let jitter_ratio = client.max_goodput_jitter_bps / client.goodput_bps.max(1.0);
1292 let low_delay_frames = if throughput_limited(client) { 2.0 } else { 8.0 };
1293 let high_delay_frames = if throughput_limited(client) {
1294 4.0
1295 } else {
1296 12.0
1297 };
1298 if likely_window_limited
1299 && queue_delay_ms <= frame_ms * low_delay_frames
1300 && jitter_ratio < 0.25
1301 {
1302 client.probe_frames = (client.probe_frames + 1.0).min(max_probe_frames);
1303 } else if !likely_window_limited
1304 && browser_ready(client)
1305 && queue_delay_ms <= frame_ms * 2.0
1306 && jitter_ratio < 0.25
1307 {
1308 client.probe_frames = (client.probe_frames + 0.25).min(max_probe_frames * 0.5);
1309 } else if queue_delay_ms > frame_ms * high_delay_frames || jitter_ratio > 0.5 {
1310 client.probe_frames = (client.probe_frames * 0.5).max(1.0);
1311 } else if queue_delay_ms > frame_ms * 2.0 || !browser_ready(client) {
1312 client.probe_frames = (client.probe_frames - 0.5).max(0.0);
1313 }
1314 } else {
1315 client.inflight_bytes = 0;
1316 }
1317}
1318
1319fn record_surface_ack(client: &mut ClientState) {
1326 if let Some(frame) = client.surface_inflight_frames.pop_front() {
1327 client.acked_bytes_since_log = client.acked_bytes_since_log.saturating_add(frame.bytes);
1328
1329 let sample_ms = frame.sent_at.elapsed().as_secs_f32() * 1_000.0;
1330
1331 let sample_bps = frame.bytes as f32 / sample_ms.max(1.0e-3) * 1_000.0;
1333 client.delivery_bps = ewma_with_direction(client.delivery_bps, sample_bps, 0.5, 0.125);
1334
1335 client.goodput_window_bytes = client.goodput_window_bytes.saturating_add(frame.bytes);
1341 let now = Instant::now();
1342 let goodput_elapsed = now
1343 .duration_since(client.goodput_window_start)
1344 .as_secs_f32();
1345 if goodput_elapsed >= 0.02 {
1346 let sample_goodput = client.goodput_window_bytes as f32 / goodput_elapsed.max(1.0e-3);
1347 client.goodput_bps =
1348 ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, 0.125);
1349 client.last_goodput_sample_bps =
1350 (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
1351 client.goodput_window_bytes = 0;
1352 client.goodput_window_start = now;
1353 }
1354 }
1355}
1356
1357fn reset_inflight(client: &mut ClientState) {
1358 client.inflight_bytes = 0;
1359 client.inflight_frames.clear();
1360 client.next_send_at = Instant::now();
1361 client.browser_backlog_frames = 0;
1362 client.browser_ack_ahead_frames = 0;
1363}
1364
1365fn is_unset_view_size(rows: u16, cols: u16) -> bool {
1366 rows == 0 && cols == 0
1367}
1368
1369fn subscribe_client_to(client: &mut ClientState, pty_id: u16) {
1370 if client.subscriptions.insert(pty_id) {
1371 client.last_sent.remove(&pty_id);
1372 client.preview_next_send_at.remove(&pty_id);
1373 }
1374}
1375
1376fn unsubscribe_client_from(client: &mut ClientState, pty_id: u16) -> bool {
1377 let removed_sub = client.subscriptions.remove(&pty_id);
1378 client.last_sent.remove(&pty_id);
1379 client.preview_next_send_at.remove(&pty_id);
1380 client.scroll_offsets.remove(&pty_id);
1381 client.scroll_caches.remove(&pty_id);
1382 let removed_view = client.view_sizes.remove(&pty_id).is_some();
1383 if client.lead == Some(pty_id) {
1384 client.lead = None;
1385 }
1386 removed_sub || removed_view
1387}
1388
1389fn update_client_scroll_state(client: &mut ClientState, pty_id: u16, next_offset: usize) -> bool {
1390 let prev_offset = client.scroll_offsets.get(&pty_id).copied().unwrap_or(0);
1391 if prev_offset == next_offset {
1392 return false;
1393 }
1394
1395 if prev_offset == 0 && next_offset > 0 {
1396 client.scroll_caches.insert(
1397 pty_id,
1398 client.last_sent.get(&pty_id).cloned().unwrap_or_default(),
1399 );
1400 } else if prev_offset > 0
1401 && next_offset == 0
1402 && let Some(cache) = client.scroll_caches.remove(&pty_id)
1403 {
1404 if cache.rows() > 0 && cache.cols() > 0 {
1405 client.last_sent.insert(pty_id, cache);
1406 } else {
1407 client.last_sent.remove(&pty_id);
1408 }
1409 }
1410
1411 if next_offset > 0 {
1412 client.scroll_offsets.insert(pty_id, next_offset);
1413 } else {
1414 client.scroll_offsets.remove(&pty_id);
1415 }
1416 reset_inflight(client);
1417 true
1418}
1419
1420struct Session {
1421 ptys: HashMap<u16, Pty>,
1422 compositor: Option<SharedCompositor>,
1423 next_client_id: u64,
1424 next_compositor_id: u16,
1425 next_pty_id: u16,
1426 tick_fires: u32,
1427 tick_snaps: u32,
1428 surface_commits: u32,
1429 surface_encodes: u32,
1430 surface_encode_bytes: u64,
1431 surface_frames_sent: u32,
1432 ticks_pixel_snapshot_empty: u32,
1434 pixel_snapshot_len: usize,
1436 last_ping: Instant,
1437 clients: HashMap<u64, ClientState>,
1438}
1439
1440struct SearchResultRow {
1441 pty_id: u16,
1442 score: u32,
1443 primary_source: u8,
1444 matched_sources: u8,
1445 context: String,
1446 scroll_offset: Option<usize>,
1447}
1448
1449struct TickOutcome {
1450 next_deadline: Option<Instant>,
1451}
1452
1453impl Session {
1454 fn new() -> Self {
1455 Self {
1456 ptys: HashMap::new(),
1457 compositor: None,
1458 next_client_id: 1,
1459 next_compositor_id: 1,
1460 next_pty_id: 1,
1461 clients: HashMap::new(),
1462 tick_fires: 0,
1463 tick_snaps: 0,
1464 surface_commits: 0,
1465 surface_encodes: 0,
1466 surface_encode_bytes: 0,
1467 ticks_pixel_snapshot_empty: 0,
1468 pixel_snapshot_len: 0,
1469 last_ping: Instant::now(),
1470 surface_frames_sent: 0,
1471 }
1472 }
1473
1474 fn ensure_compositor(
1475 &mut self,
1476 verbose: bool,
1477 event_notify: Arc<dyn Fn() + Send + Sync>,
1478 gpu_device: &str,
1479 ) -> &str {
1480 if self.compositor.is_none() {
1481 let session_id = self.next_compositor_id;
1482 self.next_compositor_id = self.next_compositor_id.wrapping_add(1);
1483 let created_at = Instant::now();
1486 let handle = blit_compositor::spawn_compositor(verbose, event_notify, gpu_device);
1487 #[cfg(target_os = "linux")]
1488 let audio_pipeline = {
1489 let audio_disabled = std::env::var("BLIT_AUDIO")
1490 .map(|v| v == "0")
1491 .unwrap_or(false);
1492 if !audio_disabled && audio::pipewire_available() {
1493 let runtime_dir = std::path::Path::new(&handle.socket_name)
1494 .parent()
1495 .unwrap_or(std::path::Path::new("/tmp"));
1496 let bitrate = std::env::var("BLIT_AUDIO_BITRATE")
1497 .ok()
1498 .and_then(|v| v.parse::<i32>().ok())
1499 .unwrap_or(0);
1500 tokio::task::block_in_place(|| {
1503 match audio::AudioPipeline::spawn(
1504 runtime_dir,
1505 session_id,
1506 bitrate,
1507 verbose,
1508 created_at,
1509 ) {
1510 Ok(pipeline) => {
1511 if verbose {
1512 eprintln!(
1513 "[audio] pipeline started, PULSE_SERVER={}",
1514 pipeline.pulse_server_path(),
1515 );
1516 }
1517 Some(pipeline)
1518 }
1519 Err(e) => {
1520 eprintln!("[audio] failed to start pipeline: {e}");
1521 None
1522 }
1523 }
1524 })
1525 } else {
1526 if verbose && !audio_disabled {
1527 let missing = audio::missing_pipewire_binaries();
1528 eprintln!(
1529 "[audio] audio disabled: missing binaries on $PATH: {}",
1530 missing.join(", ")
1531 );
1532 }
1533 None
1534 }
1535 };
1536
1537 self.compositor = Some(SharedCompositor {
1538 handle,
1539 surfaces: HashMap::new(),
1540 last_pixels: HashMap::new(),
1541 last_frame_request: HashMap::new(),
1542 created_at,
1543 pixel_generation: 0,
1544 last_blanket_frame_request: Instant::now(),
1545 last_configured_size: HashMap::new(),
1546 #[cfg(target_os = "linux")]
1547 audio_pipeline,
1548 #[cfg(target_os = "linux")]
1549 audio_session_id: session_id,
1550 #[cfg(target_os = "linux")]
1551 last_audio_restart: None,
1552 });
1553 }
1554 &self.compositor.as_ref().unwrap().handle.socket_name
1555 }
1556
1557 #[cfg(target_os = "linux")]
1559 fn pulse_server_path(&self) -> Option<String> {
1560 self.compositor
1561 .as_ref()
1562 .and_then(|cs| cs.audio_pipeline.as_ref())
1563 .map(|ap| ap.pulse_server_path())
1564 }
1565
1566 #[cfg(target_os = "linux")]
1568 fn pipewire_remote_path(&self) -> Option<String> {
1569 self.compositor
1570 .as_ref()
1571 .and_then(|cs| cs.audio_pipeline.as_ref())
1572 .map(|ap| ap.pipewire_remote_path())
1573 }
1574
1575 fn allocate_pty_id(&mut self, max_ptys: usize) -> Option<u16> {
1576 if max_ptys > 0 && self.ptys.len() >= max_ptys {
1577 return None;
1578 }
1579 let start = self.next_pty_id;
1580 let mut id = start;
1581 loop {
1582 if !self.ptys.contains_key(&id) {
1583 self.next_pty_id = if id == u16::MAX { 1 } else { id + 1 };
1584 return Some(id);
1585 }
1586 id = if id == u16::MAX { 1 } else { id + 1 };
1587 if id == start {
1588 return None;
1589 }
1590 }
1591 }
1592
1593 fn send_to_all(&self, msg: &[u8]) {
1594 for c in self.clients.values() {
1595 let _ = send_outbox(c, msg.to_vec());
1596 }
1597 }
1598
1599 fn mediated_size_for_pty(&self, pty_id: u16) -> Option<(u16, u16)> {
1600 let mut min_rows: Option<u16> = None;
1601 let mut min_cols: Option<u16> = None;
1602 for c in self.clients.values() {
1603 if let Some((r, cols)) = c.view_sizes.get(&pty_id).copied() {
1604 min_rows = Some(min_rows.map_or(r, |m: u16| m.min(r)));
1605 min_cols = Some(min_cols.map_or(cols, |m: u16| m.min(cols)));
1606 }
1607 }
1608 match (min_rows, min_cols) {
1609 (Some(r), Some(c)) => Some((r.max(1), c.max(1))),
1610 _ => None,
1611 }
1612 }
1613
1614 fn resize_pty(&mut self, pty_id: u16, rows: u16, cols: u16) -> bool {
1615 let pty = match self.ptys.get_mut(&pty_id) {
1616 Some(p) => p,
1617 None => return false,
1618 };
1619 let (cur_rows, cur_cols) = pty.driver.size();
1620 if cur_rows == rows && cur_cols == cols {
1621 return false;
1622 }
1623 pty.ready_frames.clear();
1624 pty.driver.resize(rows, cols);
1625 pty.mark_dirty();
1626 for c in self.clients.values_mut() {
1627 if c.subscriptions.contains(&pty_id) {
1628 c.last_sent.remove(&pty_id);
1629 }
1630 if c.scroll_caches.remove(&pty_id).is_some() {
1631 reset_inflight(c);
1632 }
1633 }
1634 if !pty.exited {
1635 pty::resize_pty_os(&pty.handle, rows, cols);
1636 }
1637 true
1638 }
1639
1640 fn resize_ptys_to_mediated_sizes<I>(&mut self, pty_ids: I) -> bool
1641 where
1642 I: IntoIterator<Item = u16>,
1643 {
1644 let mut changed = false;
1645 let mut seen = HashSet::new();
1646 for pty_id in pty_ids {
1647 if !seen.insert(pty_id) {
1648 continue;
1649 }
1650 if let Some((rows, cols)) = self.mediated_size_for_pty(pty_id) {
1651 changed |= self.resize_pty(pty_id, rows, cols);
1652 }
1653 }
1654 changed
1655 }
1656
1657 fn mediated_size_for_surface(
1670 &self,
1671 surface_id: u16,
1672 max: Option<(u16, u16)>,
1673 ) -> Option<(u16, u16, u16)> {
1674 let mut min_w: Option<u16> = None;
1675 let mut min_h: Option<u16> = None;
1676 let mut max_scale: u16 = 0;
1677 for c in self.clients.values() {
1678 if let Some(&(w, h, s)) = c.surface_view_sizes.get(&surface_id) {
1679 min_w = Some(min_w.map_or(w, |m: u16| m.min(w)));
1680 min_h = Some(min_h.map_or(h, |m: u16| m.min(h)));
1681 max_scale = max_scale.max(s);
1682 }
1683 }
1684 match (min_w, min_h) {
1685 (Some(w), Some(h)) => {
1686 let (w, h) = (w.max(1), h.max(1));
1687 let (w, h) = if let Some((mw, mh)) = max {
1688 (w.min(mw), h.min(mh))
1689 } else {
1690 (w, h)
1691 };
1692 Some((w, h, max_scale))
1693 }
1694 _ => None,
1695 }
1696 }
1697
1698 fn resize_surface(&mut self, surface_id: u16, width: u16, height: u16, scale_120: u16) -> bool {
1699 let cs = match self.compositor.as_mut() {
1700 Some(cs) => cs,
1701 None => return false,
1702 };
1703 if let Some(&(lw, lh, ls)) = cs.last_configured_size.get(&surface_id)
1711 && lw == width
1712 && lh == height
1713 && ls == scale_120
1714 {
1715 return false;
1716 }
1717 cs.last_configured_size
1718 .insert(surface_id, (width, height, scale_120));
1719 let _ = cs.handle.command_tx.send(CompositorCommand::SurfaceResize {
1720 surface_id,
1721 width,
1722 height,
1723 scale_120,
1724 });
1725 true
1726 }
1727
1728 fn resize_surfaces_to_mediated_sizes<I>(
1729 &mut self,
1730 surface_ids: I,
1731 encoder_preferences: &[SurfaceEncoderPreference],
1732 ) where
1733 I: IntoIterator<Item = u16>,
1734 {
1735 let max = SurfaceEncoderPreference::max_dimensions_for_list(encoder_preferences);
1736 let mut seen = HashSet::new();
1737 for sid in surface_ids {
1738 if !seen.insert(sid) {
1739 continue;
1740 }
1741 if let Some((w, h, scale_120)) = self.mediated_size_for_surface(sid, max) {
1742 self.resize_surface(sid, w, h, scale_120);
1743 }
1744 }
1745 }
1746
1747 fn pty_list_msg(&self) -> Vec<u8> {
1748 let mut msg = vec![S2C_LIST];
1749 let count = self.ptys.len() as u16;
1750 msg.extend_from_slice(&count.to_le_bytes());
1751 let mut ids: Vec<u16> = self.ptys.keys().copied().collect();
1752 ids.sort();
1753 for id in ids {
1754 let pty = &self.ptys[&id];
1755 let tag = pty.tag.as_bytes();
1756 msg.extend_from_slice(&id.to_le_bytes());
1757 msg.extend_from_slice(&(tag.len() as u16).to_le_bytes());
1758 msg.extend_from_slice(tag);
1759 let cmd = pty.command.as_deref().unwrap_or("").as_bytes();
1760 msg.extend_from_slice(&(cmd.len() as u16).to_le_bytes());
1761 msg.extend_from_slice(cmd);
1762 }
1763 msg
1764 }
1765
1766 fn surface_list_msg(&self) -> Vec<u8> {
1767 let cs = match self.compositor.as_ref() {
1768 Some(cs) => cs,
1769 None => {
1770 let mut msg = vec![S2C_SURFACE_LIST];
1771 msg.extend_from_slice(&0u16.to_le_bytes());
1772 return msg;
1773 }
1774 };
1775 let mut msg = vec![S2C_SURFACE_LIST];
1776 let count = cs.surfaces.len() as u16;
1777 msg.extend_from_slice(&count.to_le_bytes());
1778 let mut ids: Vec<u16> = cs.surfaces.keys().copied().collect();
1779 ids.sort();
1780 for id in ids {
1781 let info = &cs.surfaces[&id];
1782 let title = info.title.as_bytes();
1783 let app_id = info.app_id.as_bytes();
1784 msg.extend_from_slice(&info.surface_id.to_le_bytes());
1785 msg.extend_from_slice(&info.parent_id.to_le_bytes());
1786 msg.extend_from_slice(&info.width.to_le_bytes());
1787 msg.extend_from_slice(&info.height.to_le_bytes());
1788 msg.extend_from_slice(&(title.len() as u16).to_le_bytes());
1789 msg.extend_from_slice(title);
1790 msg.extend_from_slice(&(app_id.len() as u16).to_le_bytes());
1791 msg.extend_from_slice(app_id);
1792 }
1793 msg
1794 }
1795}
1796
1797struct AppStateInner {
1798 config: Config,
1799 session: Mutex<Session>,
1800 pty_fds: PtyFds,
1801 delivery_notify: Arc<Notify>,
1802 shutdown_notify: Arc<Notify>,
1804 active_connections: std::sync::atomic::AtomicUsize,
1807}
1808
1809type AppState = Arc<AppStateInner>;
1810
1811fn nudge_delivery(state: &AppState) {
1812 state.delivery_notify.notify_one();
1813}
1814
1815#[cfg(unix)]
1816#[allow(dead_code)]
1817fn spawn_compositor_child(
1818 command: &str,
1819 argv: Option<&[&str]>,
1820 wayland_socket: &str,
1821 dir: Option<&str>,
1822) -> libc::pid_t {
1823 use std::ffi::CString;
1824 let pid = unsafe { libc::fork() };
1825 if pid == 0 {
1826 if let Some(d) = dir {
1827 let c_dir = CString::new(d).unwrap();
1828 unsafe {
1829 libc::chdir(c_dir.as_ptr());
1830 }
1831 }
1832 unsafe {
1833 let wd_path = std::path::Path::new(wayland_socket);
1834 if let Some(dir) = wd_path.parent() {
1835 let xdg = std::env::var_os("XDG_RUNTIME_DIR");
1836 let needs_update = match &xdg {
1837 Some(x) => std::path::Path::new(x) != dir,
1838 None => true,
1839 };
1840 if needs_update {
1841 std::env::set_var("XDG_RUNTIME_DIR", dir);
1842 }
1843 }
1844 std::env::set_var("WAYLAND_DISPLAY", wayland_socket);
1845 std::env::remove_var("DISPLAY");
1846 std::env::remove_var("DBUS_SESSION_BUS_ADDRESS");
1847 std::env::remove_var("DBUS_SYSTEM_BUS_ADDRESS");
1848 }
1849 if let Some(args) = argv {
1850 let prog = CString::new(args[0]).unwrap();
1851 let c_args: Vec<CString> = args.iter().map(|a| CString::new(*a).unwrap()).collect();
1852 let c_ptrs: Vec<*const libc::c_char> = c_args
1853 .iter()
1854 .map(|a| a.as_ptr())
1855 .chain(std::iter::once(std::ptr::null()))
1856 .collect();
1857 unsafe {
1858 libc::execvp(prog.as_ptr(), c_ptrs.as_ptr());
1859 }
1860 } else {
1861 let prog = CString::new(command).unwrap();
1862 let c_ptrs = [prog.as_ptr(), std::ptr::null()];
1863 unsafe {
1864 libc::execvp(prog.as_ptr(), c_ptrs.as_ptr());
1865 libc::_exit(1);
1866 }
1867 }
1868 }
1869 pid
1870}
1871
1872fn xterm256_color(idx: u8) -> (u16, u16, u16) {
1874 const BASE16: [(u8, u8, u8); 16] = [
1876 (0, 0, 0),
1877 (128, 0, 0),
1878 (0, 128, 0),
1879 (128, 128, 0),
1880 (0, 0, 128),
1881 (128, 0, 128),
1882 (0, 128, 128),
1883 (192, 192, 192),
1884 (128, 128, 128),
1885 (255, 0, 0),
1886 (0, 255, 0),
1887 (255, 255, 0),
1888 (0, 0, 255),
1889 (255, 0, 255),
1890 (0, 255, 255),
1891 (255, 255, 255),
1892 ];
1893 let (r8, g8, b8) = if idx < 16 {
1894 BASE16[idx as usize]
1895 } else if idx < 232 {
1896 let n = idx - 16;
1898 let ri = n / 36;
1899 let gi = (n % 36) / 6;
1900 let bi = n % 6;
1901 let to_val = |v: u8| if v == 0 { 0u8 } else { 55 + 40 * v };
1902 (to_val(ri), to_val(gi), to_val(bi))
1903 } else {
1904 let v = 8 + 10 * (idx - 232);
1906 (v, v, v)
1907 };
1908 let scale = |v: u8| (v as u16) << 8 | v as u16;
1910 (scale(r8), scale(g8), scale(b8))
1911}
1912fn parse_terminal_queries(data: &[u8], size: (u16, u16), cursor: (u16, u16)) -> Vec<String> {
1913 const DA1_RESPONSE: &[u8] = b"\x1b[?64;1;2;6;9;15;18;21;22c";
1914
1915 let mut results = Vec::new();
1916 let mut i = 0;
1917 while i < data.len() {
1918 if data[i] != 0x1b || i + 1 >= data.len() {
1919 i += 1;
1920 continue;
1921 }
1922
1923 if data[i + 1] == b']' {
1925 let osc_start = i + 2;
1926 let mut end = osc_start;
1928 while end < data.len() {
1929 if data[end] == 0x07 {
1930 break;
1931 }
1932 if data[end] == 0x1b && end + 1 < data.len() && data[end + 1] == b'\\' {
1933 break;
1934 }
1935 end += 1;
1936 }
1937 if end < data.len() {
1938 let payload = &data[osc_start..end];
1939 if payload == b"11;?" {
1941 results.push("\x1b]11;rgb:0000/0000/0000\x1b\\".into());
1943 }
1944 else if payload == b"10;?" {
1946 results.push("\x1b]10;rgb:ffff/ffff/ffff\x1b\\".into());
1947 }
1948 else if payload.starts_with(b"4;") && payload.ends_with(b";?") {
1950 let idx_bytes = &payload[2..payload.len() - 2];
1951 if let Ok(idx_str) = std::str::from_utf8(idx_bytes)
1952 && let Ok(idx) = idx_str.parse::<u8>()
1953 {
1954 let (r, g, b) = xterm256_color(idx);
1955 results.push(format!("\x1b]4;{idx};rgb:{r:04x}/{g:04x}/{b:04x}\x1b\\"));
1956 }
1957 }
1958 i = end + if data[end] == 0x07 { 1 } else { 2 };
1959 continue;
1960 }
1961 i = end;
1962 continue;
1963 }
1964
1965 if i + 2 >= data.len() || data[i + 1] != b'[' {
1967 i += 1;
1968 continue;
1969 }
1970 i += 2;
1971 let has_q = i < data.len() && data[i] == b'?';
1972 if has_q {
1973 i += 1;
1974 }
1975 let param_start = i;
1976 while i < data.len() && (data[i].is_ascii_digit() || data[i] == b';') {
1977 i += 1;
1978 }
1979 if i >= data.len() {
1980 break;
1981 }
1982 let final_byte = data[i];
1983 let params = &data[param_start..i];
1984 i += 1;
1985 if has_q {
1986 continue;
1987 }
1988 let resp: Option<String> = match final_byte {
1989 b'c' if params.is_empty() || params == b"0" => {
1990 Some(String::from_utf8_lossy(DA1_RESPONSE).into_owned())
1991 }
1992 b'n' if params == b"6" => Some(format!("\x1b[{};{}R", cursor.0 + 1, cursor.1 + 1)),
1993 b'n' if params == b"5" => Some("\x1b[0n".into()),
1994 b't' if params == b"18" => {
1995 let (rows, cols) = size;
1996 Some(format!("\x1b[8;{rows};{cols}t"))
1997 }
1998 b't' if params == b"14" => {
1999 let (rows, cols) = size;
2000 Some(format!("\x1b[4;{};{}t", rows * 16, cols * 8))
2001 }
2002 _ => None,
2003 };
2004 if let Some(r) = resp {
2005 results.push(r);
2006 }
2007 }
2008 results
2009}
2010
2011async fn cleanup_pty_internal(pty_id: u16, state: &AppState) {
2012 state.pty_fds.write().unwrap().remove(&pty_id);
2013 let mut sess = state.session.lock().await;
2014 if let Some(pty) = sess.ptys.get_mut(&pty_id) {
2015 if pty.exited {
2016 return;
2017 }
2018 pty.exited = true;
2019 pty::close_pty(&pty.handle);
2020 pty.exit_status = pty::collect_exit_status(&pty.handle);
2021 pty.mark_dirty();
2022 let msg = blit_remote::msg_exited(pty_id, pty.exit_status);
2023 sess.send_to_all(&msg);
2024 }
2025}
2026
2027fn take_snapshot(pty: &mut Pty) -> FrameState {
2028 if pty.lflag_last.elapsed() >= Duration::from_millis(250) {
2029 pty.lflag_cache = pty::pty_lflag(&pty.handle);
2030 pty.lflag_last = Instant::now();
2031 }
2032 let (echo, icanon) = pty.lflag_cache;
2033 pty.driver.snapshot(echo, icanon)
2034}
2035
2036fn build_scrollback_update(
2037 pty: &mut Pty,
2038 id: u16,
2039 offset: usize,
2040 prev_frame: &FrameState,
2041) -> Option<(Vec<u8>, FrameState)> {
2042 let frame = pty.driver.scrollback_frame(offset);
2043 let msg = build_update_msg(id, &frame, prev_frame);
2044 msg.map(|m| (m, frame))
2045}
2046
2047fn build_search_results_msg(request_id: u16, results: &[SearchResultRow]) -> Vec<u8> {
2048 let count = results.len().min(u16::MAX as usize);
2049 let payload_bytes: usize = results[..count]
2050 .iter()
2051 .map(|result| 14 + result.context.len().min(u16::MAX as usize))
2052 .sum();
2053 let mut msg = Vec::with_capacity(5 + payload_bytes);
2054 msg.push(S2C_SEARCH_RESULTS);
2055 msg.extend_from_slice(&request_id.to_le_bytes());
2056 msg.extend_from_slice(&(count as u16).to_le_bytes());
2057 for result in &results[..count] {
2058 msg.extend_from_slice(&result.pty_id.to_le_bytes());
2059 msg.extend_from_slice(&result.score.to_le_bytes());
2060 msg.push(result.primary_source);
2061 msg.push(result.matched_sources);
2062 let scroll_offset = result
2063 .scroll_offset
2064 .map(|offset| offset.min(u32::MAX as usize - 1) as u32)
2065 .unwrap_or(u32::MAX);
2066 msg.extend_from_slice(&scroll_offset.to_le_bytes());
2067 let context = result.context.as_bytes();
2068 let context_len = context.len().min(u16::MAX as usize);
2069 msg.extend_from_slice(&(context_len as u16).to_le_bytes());
2070 msg.extend_from_slice(&context[..context_len]);
2071 }
2072 msg
2073}
2074
2075enum SendOutcome {
2076 NoChange,
2077 Sent,
2078 Backpressured,
2079}
2080
2081fn try_send_update(
2082 client: &mut ClientState,
2083 pid: u16,
2084 current: FrameState,
2085 msg: Option<Vec<u8>>,
2086 now: Instant,
2087 paced: bool,
2088) -> SendOutcome {
2089 let Some(msg) = msg else {
2090 return SendOutcome::NoChange;
2091 };
2092 let bytes = msg.len();
2093 if send_outbox(client, msg).is_ok() {
2094 client.last_sent.insert(pid, current);
2095 record_send(client, bytes, now, paced);
2096 client.frames_sent = client.frames_sent.wrapping_add(1);
2097 SendOutcome::Sent
2098 } else {
2099 client.last_sent.insert(pid, current);
2103 SendOutcome::Backpressured
2104 }
2105}
2106
2107pub async fn run(config: Config) {
2108 let state: AppState = Arc::new(AppStateInner {
2109 config,
2110 session: Mutex::new(Session::new()),
2111 pty_fds: Arc::new(std::sync::RwLock::new(HashMap::new())),
2112 delivery_notify: Arc::new(Notify::new()),
2113 shutdown_notify: Arc::new(Notify::new()),
2114 active_connections: std::sync::atomic::AtomicUsize::new(0),
2115 });
2116
2117 if !state.config.skip_compositor {
2120 let notify = state.delivery_notify.clone();
2121 let event_notify = Arc::new(move || notify.notify_one()) as Arc<dyn Fn() + Send + Sync>;
2122 let mut sess = state.session.lock().await;
2123 sess.ensure_compositor(
2124 state.config.verbose,
2125 event_notify,
2126 &state.config.vaapi_device,
2127 );
2128 }
2129
2130 let delivery_state = state.clone();
2131 tokio::spawn(async move {
2132 let mut next_deadline: Option<Instant> = None;
2133 loop {
2134 if let Some(deadline) = next_deadline {
2135 tokio::select! {
2136 _ = delivery_state.delivery_notify.notified() => {}
2137 _ = tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)) => {}
2138 }
2139 } else {
2140 delivery_state.delivery_notify.notified().await;
2141 }
2142 let outcome = tick(&delivery_state).await;
2143 next_deadline = outcome.next_deadline;
2144 }
2145 });
2146
2147 tokio::spawn(async {
2148 loop {
2149 tokio::time::sleep(Duration::from_secs(5)).await;
2150 pty::reap_zombies();
2151 }
2152 });
2153
2154 #[cfg(unix)]
2155 if let Some(channel_fd) = state.config.fd_channel {
2156 ipc::run_fd_channel(channel_fd, state).await;
2157 return;
2158 }
2159
2160 #[cfg(unix)]
2161 let listener = {
2162 if let Some(l) = IpcListener::from_systemd_fd(state.config.verbose) {
2163 l
2164 } else {
2165 IpcListener::bind(&state.config.ipc_path, state.config.verbose)
2166 }
2167 };
2168 #[cfg(not(unix))]
2169 let mut listener = IpcListener::bind(&state.config.ipc_path, state.config.verbose);
2170
2171 {
2174 let state = state.clone();
2175 tokio::spawn(async move {
2176 #[cfg(unix)]
2177 {
2178 use tokio::signal::unix::{SignalKind, signal};
2179 let mut sigterm = signal(SignalKind::terminate()).expect("signal handler");
2180 let mut sigint = signal(SignalKind::interrupt()).expect("signal handler");
2181 tokio::select! {
2182 _ = sigterm.recv() => {}
2183 _ = sigint.recv() => {}
2184 }
2185 }
2186 #[cfg(not(unix))]
2187 {
2188 let _ = tokio::signal::ctrl_c().await;
2189 }
2190 let sess = state.session.lock().await;
2191 sess.send_to_all(&[S2C_QUIT]);
2192 drop(sess);
2193 state.shutdown_notify.notify_one();
2194 });
2195 }
2196
2197 let shutdown = state.shutdown_notify.clone();
2198 loop {
2199 let stream = tokio::select! {
2200 result = listener.accept() => match result {
2201 Ok(s) => s,
2202 Err(e) => {
2203 eprintln!("accept error: {e}");
2204 tokio::time::sleep(Duration::from_millis(100)).await;
2205 continue;
2206 }
2207 },
2208 _ = shutdown.notified() => break,
2209 };
2210 let max = state.config.max_connections;
2211 if max > 0 {
2212 let current = state
2213 .active_connections
2214 .load(std::sync::atomic::Ordering::Relaxed);
2215 if current >= max {
2216 eprintln!("max connections ({max}) reached, rejecting");
2217 drop(stream);
2218 continue;
2219 }
2220 }
2221 state
2222 .active_connections
2223 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2224 let state = state.clone();
2225 tokio::spawn(async move {
2226 handle_client(stream, state.clone()).await;
2227 state
2228 .active_connections
2229 .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
2230 });
2231 }
2232 tokio::time::sleep(Duration::from_millis(100)).await;
2234}
2235
2236const BLANKET_FRAME_INTERVAL_IDLE: Duration = Duration::from_millis(33);
2245const BLANKET_FRAME_INTERVAL_SURFACE: Duration = Duration::from_millis(8);
2246
2247fn blanket_frame_interval(sess: &Session) -> Duration {
2248 let has_surface_subs = sess
2249 .clients
2250 .values()
2251 .any(|c| !c.surface_subscriptions.is_empty());
2252 if has_surface_subs {
2253 BLANKET_FRAME_INTERVAL_SURFACE
2254 } else {
2255 BLANKET_FRAME_INTERVAL_IDLE
2256 }
2257}
2258
2259async fn tick(state: &AppState) -> TickOutcome {
2260 let mut sess = state.session.lock().await;
2261 sess.tick_fires += 1;
2262 let mut next_deadline: Option<Instant> = None;
2263 let now = Instant::now();
2264
2265 let log_client_ids: Vec<u64> = sess.clients.keys().copied().collect();
2269 for cid in log_client_ids {
2270 maybe_log_pacing_metrics(&mut sess, cid, state.config.verbose);
2271 }
2272
2273 let ping_interval = state.config.ping_interval;
2275 if !ping_interval.is_zero() && now.duration_since(sess.last_ping) >= ping_interval {
2276 sess.send_to_all(&[S2C_PING]);
2277 sess.last_ping = now;
2278 }
2279 if !ping_interval.is_zero() {
2280 let next_ping = sess.last_ping + ping_interval;
2281 next_deadline = Some(next_deadline.map_or(next_ping, |d: Instant| d.min(next_ping)));
2282 }
2283
2284 let mut invalidate_client_encoders: Vec<u16> = Vec::new();
2286
2287 let mut surface_commit_count = 0u32;
2288 if let Some(cs) = sess.compositor.as_mut() {
2289 let mut events = Vec::new();
2290 while let Ok(event) = cs.handle.event_rx.try_recv() {
2291 events.push(event);
2292 }
2293 let mut broadcast: Vec<Vec<u8>> = Vec::new();
2294 for event in events {
2295 match event {
2296 CompositorEvent::SurfaceCreated {
2297 surface_id,
2298 title,
2299 app_id,
2300 parent_id,
2301 width,
2302 height,
2303 } => {
2304 broadcast.push(msg_surface_created(
2305 surface_id, parent_id, width, height, &title, &app_id,
2306 ));
2307 cs.surfaces.insert(
2308 surface_id,
2309 CachedSurfaceInfo {
2310 surface_id,
2311 parent_id,
2312 width,
2313 height,
2314 title,
2315 app_id,
2316 },
2317 );
2318 cs.last_pixels.remove(&surface_id);
2319 invalidate_client_encoders.push(surface_id);
2320 }
2321 CompositorEvent::SurfaceDestroyed { surface_id } => {
2322 cs.surfaces.remove(&surface_id);
2323 cs.last_pixels.remove(&surface_id);
2324 cs.last_configured_size.remove(&surface_id);
2325 invalidate_client_encoders.push(surface_id);
2326 broadcast.push(msg_surface_destroyed(surface_id));
2327 }
2328 CompositorEvent::SurfaceCommit {
2329 surface_id,
2330 width,
2331 height,
2332 pixels,
2333 timestamp_ms,
2334 } => {
2335 surface_commit_count += 1;
2336 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
2337 info.width = width as u16;
2338 info.height = height as u16;
2339 }
2340 cs.pixel_generation += 1;
2341 cs.last_pixels.insert(
2342 surface_id,
2343 LastPixels {
2344 width,
2345 height,
2346 pixels,
2347 generation: cs.pixel_generation,
2348 timestamp_ms,
2349 },
2350 );
2351 }
2352 CompositorEvent::SurfaceTitle { surface_id, title } => {
2353 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
2354 info.title = title.clone();
2355 }
2356 broadcast.push(msg_surface_title(surface_id, &title));
2357 }
2358 CompositorEvent::SurfaceAppId { surface_id, app_id } => {
2359 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
2360 info.app_id = app_id.clone();
2361 }
2362 broadcast.push(msg_surface_app_id(surface_id, &app_id));
2363 }
2364 CompositorEvent::SurfaceResized {
2365 surface_id,
2366 width,
2367 height,
2368 } => {
2369 if let Some(info) = cs.surfaces.get_mut(&surface_id) {
2370 info.width = width;
2371 info.height = height;
2372 }
2373 cs.last_pixels.remove(&surface_id);
2374 broadcast.push(msg_surface_resized(surface_id, width, height));
2383 }
2384 CompositorEvent::ClipboardContent {
2385 mime_type, data, ..
2386 } => {
2387 broadcast.push(msg_s2c_clipboard_content(&mime_type, &data));
2388 }
2389 CompositorEvent::SurfaceCursor { surface_id, cursor } => {
2390 let mut msg = Vec::new();
2395 msg.push(blit_remote::S2C_SURFACE_CURSOR);
2396 msg.extend_from_slice(&surface_id.to_le_bytes());
2397 match &cursor {
2398 blit_compositor::CursorImage::Named(name) => {
2399 msg.push(0); msg.push(name.len() as u8);
2401 msg.extend_from_slice(name.as_bytes());
2402 }
2403 blit_compositor::CursorImage::Hidden => {
2404 msg.push(1); }
2406 blit_compositor::CursorImage::Custom {
2407 hotspot_x,
2408 hotspot_y,
2409 width,
2410 height,
2411 rgba,
2412 } => {
2413 let mut png_buf = Vec::new();
2415 {
2416 let mut encoder =
2417 png::Encoder::new(&mut png_buf, *width as u32, *height as u32);
2418 encoder.set_color(png::ColorType::Rgba);
2419 encoder.set_depth(png::BitDepth::Eight);
2420 if let Ok(mut writer) = encoder.write_header() {
2421 let _ = writer.write_image_data(rgba);
2422 }
2423 }
2424 msg.push(2); msg.extend_from_slice(&hotspot_x.to_le_bytes());
2426 msg.extend_from_slice(&hotspot_y.to_le_bytes());
2427 msg.extend_from_slice(&width.to_le_bytes());
2428 msg.extend_from_slice(&height.to_le_bytes());
2429 msg.extend_from_slice(&png_buf);
2430 }
2431 }
2432 broadcast.push(msg);
2433 }
2434 }
2435 }
2436 for msg in &broadcast {
2437 sess.send_to_all(msg);
2438 }
2439 }
2440 sess.surface_commits += surface_commit_count;
2441
2442 for sid in invalidate_client_encoders {
2447 for c in sess.clients.values_mut() {
2448 c.surface_subs.remove(&sid);
2449 c.vulkan_video_surfaces.remove(&sid);
2450 }
2451 }
2452
2453 let pixel_snapshot: Vec<(u16, u32, u32, u64, u32)> = sess
2464 .compositor
2465 .as_ref()
2466 .map(|cs| {
2467 cs.last_pixels
2468 .iter()
2469 .map(|(&sid, lp)| (sid, lp.width, lp.height, lp.generation, lp.timestamp_ms))
2470 .collect()
2471 })
2472 .unwrap_or_default();
2473 if pixel_snapshot.is_empty() {
2474 sess.ticks_pixel_snapshot_empty = sess.ticks_pixel_snapshot_empty.saturating_add(1);
2475 } else {
2476 sess.pixel_snapshot_len = pixel_snapshot.len();
2477 }
2478
2479 struct EncodeJob {
2485 cid: u64,
2486 sid: u16,
2487 px_w: u32,
2490 px_h: u32,
2491 pixels: blit_compositor::PixelData,
2493 needs_keyframe: bool,
2494 encoder: SurfaceEncoder,
2495 generation: u64,
2496 timestamp_ms: u32,
2498 }
2499 struct EncoderCreateParams {
2500 preferences: Vec<SurfaceEncoderPreference>,
2501 vaapi_device: String,
2502 quality: SurfaceQuality,
2503 verbose: bool,
2504 codec_support: u8,
2505 chroma: ChromaSubsampling,
2506 }
2507 struct CreateJob {
2514 cid: u64,
2515 sid: u16,
2516 px_w: u32,
2517 px_h: u32,
2518 params: EncoderCreateParams,
2519 }
2520 struct CreateResult {
2521 cid: u64,
2522 sid: u16,
2523 encoder: Option<SurfaceEncoder>,
2527 fresh: Option<FreshEncoder>,
2528 }
2529 struct FreshEncoder {
2534 name: &'static str,
2535 codec_string: String,
2536 #[cfg(target_os = "linux")]
2537 external_bufs: Vec<blit_compositor::ExternalOutputBuffer>,
2538 }
2539 struct EncodeResult {
2540 cid: u64,
2541 sid: u16,
2542 px_w: u32,
2544 px_h: u32,
2545 generation: u64,
2546 encoder: SurfaceEncoder,
2547 nal_data: Option<(Vec<u8>, bool)>, codec_flag: u8,
2549 timestamp_ms: u32,
2551 }
2552
2553 let mut encode_jobs: Vec<EncodeJob> = Vec::new();
2554 let mut create_jobs: Vec<CreateJob> = Vec::new();
2555 let mut encode_dispatched_surfaces: HashSet<u16> = HashSet::new();
2559
2560 struct ClientWork {
2564 cid: u64,
2565 subs: HashSet<u16>,
2566 needs_keyframe: bool,
2567 }
2568 let mut client_work: Vec<ClientWork> = Vec::new();
2569
2570 if !pixel_snapshot.is_empty() {
2571 for (&cid, client) in sess.clients.iter_mut() {
2572 if !surface_window_open(client) {
2573 let now_inst = Instant::now();
2575 if now_inst
2576 .duration_since(client.last_window_blocked_log)
2577 .as_secs_f32()
2578 > 5.0
2579 {
2580 client.last_window_blocked_log = now_inst;
2581 let max_burst: u8 = client
2582 .surface_subs
2583 .values()
2584 .map(|s| s.burst_remaining)
2585 .max()
2586 .unwrap_or(0);
2587 eprintln!(
2588 "[surface-gate] cid={cid} surface_window_open=false outbox={}f/{}B (limits {}f/{}B) burst={max_burst}",
2589 outbox_queued_frames(client),
2590 outbox_queued_bytes(client),
2591 OUTBOX_SOFT_QUEUE_LIMIT_FRAMES,
2592 OUTBOX_SOFT_QUEUE_LIMIT_BYTES,
2593 );
2594 }
2595 continue;
2596 }
2597 if client.surface_subscriptions.is_empty() {
2600 client.skip_no_subs_count = client.skip_no_subs_count.saturating_add(1);
2601 continue;
2602 }
2603 let subs: HashSet<u16> = client.surface_subscriptions.iter().copied().collect();
2604 client_work.push(ClientWork {
2605 cid,
2606 subs,
2607 needs_keyframe: client.surface_needs_keyframe,
2608 });
2609 }
2614
2615 let mut encoded_client_surfaces: HashSet<(u64, u16)> = HashSet::new();
2618
2619 let vk_encode_available = sess
2622 .compositor
2623 .as_ref()
2624 .is_some_and(|cs| cs.handle.vulkan_video_encode);
2625 let vk_encode_av1_available = sess
2626 .compositor
2627 .as_ref()
2628 .is_some_and(|cs| cs.handle.vulkan_video_encode_av1);
2629
2630 struct VulkanEncoderSetup {
2632 surface_id: u32,
2633 codec: u8,
2634 qp: u8,
2635 width: u32,
2636 height: u32,
2637 }
2638 let mut pending_vulkan_encoder_setups: Vec<VulkanEncoderSetup> = Vec::new();
2639 let mut pending_vulkan_keyframe_requests: Vec<u32> = Vec::new();
2640
2641 for work in &client_work {
2642 for &sid in &work.subs {
2643 let Some(&(_, px_w, px_h, px_gen, px_timestamp_ms)) =
2644 pixel_snapshot.iter().find(|&&(s, _, _, _, _)| s == sid)
2645 else {
2646 let client = sess.clients.get_mut(&work.cid).unwrap();
2647 client.skip_last_pixels_mismatch_count =
2648 client.skip_last_pixels_mismatch_count.saturating_add(1);
2649 continue;
2650 };
2651 {
2652 let client = sess.clients.get_mut(&work.cid).unwrap();
2653 client.encode_loop_iters = client.encode_loop_iters.saturating_add(1);
2654 }
2655 let client = sess.clients.get_mut(&work.cid).unwrap();
2656
2657 {
2661 let (burst, deadline) = client.surface_subs.get(&sid).map_or((0, now), |s| {
2662 (s.burst_remaining, s.next_send_at.unwrap_or(now))
2663 });
2664 if burst == 0 && deadline > now {
2665 let interval = surface_send_interval(client);
2669 if deadline > now + interval + interval {
2670 client.surface_subs.entry(sid).or_default().next_send_at = Some(now);
2671 } else {
2672 next_deadline = Some(match next_deadline {
2673 Some(existing) => existing.min(deadline),
2674 None => deadline,
2675 });
2676 client.skip_pacing_count = client.skip_pacing_count.saturating_add(1);
2677 continue;
2678 }
2679 }
2680 }
2681
2682 if !work.needs_keyframe
2685 && let Some(last_gen) = client
2686 .surface_subs
2687 .get(&sid)
2688 .and_then(|s| s.last_encoded_gen)
2689 && last_gen == px_gen
2690 {
2691 client.skip_same_gen_count = client.skip_same_gen_count.saturating_add(1);
2692 continue;
2693 }
2694
2695 let pixels: blit_compositor::PixelData = {
2696 let cs = sess.compositor.as_ref().unwrap();
2697 match cs.last_pixels.get(&sid) {
2698 Some(lp) if lp.width == px_w && lp.height == px_h => lp.pixels.clone(),
2699 _ => {
2700 let client = sess.clients.get_mut(&work.cid).unwrap();
2701 client.skip_last_pixels_mismatch_count =
2702 client.skip_last_pixels_mismatch_count.saturating_add(1);
2703 continue;
2704 }
2705 }
2706 };
2707 let client = sess.clients.get_mut(&work.cid).unwrap();
2708
2709 let (enc_w, enc_h) = (px_w, px_h);
2710
2711 if let blit_compositor::PixelData::Encoded {
2715 ref data,
2716 is_keyframe,
2717 codec_flag,
2718 } = pixels
2719 {
2720 let flags = codec_flag
2721 | if is_keyframe {
2722 SURFACE_FRAME_FLAG_KEYFRAME
2723 } else {
2724 0
2725 };
2726 let msg = msg_surface_frame(
2727 sid,
2728 px_timestamp_ms,
2729 flags,
2730 px_w as u16,
2731 px_h as u16,
2732 data,
2733 );
2734 let bytes = msg.len();
2735 match send_outbox(client, msg) {
2736 Err(_e) => {
2737 client.surface_needs_keyframe = true;
2738 }
2739 Ok(()) => {
2740 client.surface_inflight_frames.push_back(InFlightFrame {
2741 sent_at: now,
2742 bytes,
2743 paced: true,
2744 });
2745 if !is_keyframe {
2746 client.avg_surface_frame_bytes = ewma_with_direction(
2747 client.avg_surface_frame_bytes,
2748 bytes as f32,
2749 0.5,
2750 0.125,
2751 );
2752 }
2753 client.frames_sent = client.frames_sent.wrapping_add(1);
2754 if client.surface_needs_keyframe && is_keyframe {
2755 client.surface_needs_keyframe = false;
2756 }
2757 if let Some(s) = client.surface_subs.get_mut(&sid) {
2758 s.burst_remaining = s.burst_remaining.saturating_sub(1);
2759 }
2760 }
2761 }
2762 encoded_client_surfaces.insert((work.cid, sid));
2763 encode_dispatched_surfaces.insert(sid);
2764 client.surface_subs.entry(sid).or_default().last_encoded_gen = Some(px_gen);
2765 continue;
2766 }
2767
2768 if client
2774 .surface_subs
2775 .get(&sid)
2776 .is_some_and(|s| s.encode_in_flight || s.creation_in_flight)
2777 {
2778 client.skip_in_flight_count = client.skip_in_flight_count.saturating_add(1);
2779 let now_inst = Instant::now();
2780 if now_inst.duration_since(client.last_skip_log).as_secs_f32() > 5.0 {
2781 client.last_skip_log = now_inst;
2782 let burst = client
2783 .surface_subs
2784 .get(&sid)
2785 .map_or(0, |s| s.burst_remaining);
2786 eprintln!(
2787 "[encode-skip] cid={} sid={sid} reason=in_flight same_gen={} in_flight={} burst={burst}",
2788 work.cid, client.skip_same_gen_count, client.skip_in_flight_count,
2789 );
2790 }
2791 continue;
2792 }
2793
2794 let has_vulkan_enc = client.vulkan_video_surfaces.contains_key(&sid);
2795 let needs_new_encoder = if has_vulkan_enc {
2796 false
2797 } else {
2798 client
2799 .surface_subs
2800 .get(&sid)
2801 .and_then(|s| s.encoder.as_ref())
2802 .is_none_or(|e| e.source_dimensions() != (enc_w, enc_h))
2803 };
2804
2805 const NAL_NONE_RETRY_BACKOFF: Duration = Duration::from_secs(2);
2814 if needs_new_encoder
2815 && client
2816 .surface_subs
2817 .get(&sid)
2818 .is_some_and(|s| s.nal_none_streak >= 10)
2819 {
2820 let ready_to_retry = client
2821 .surface_subs
2822 .get(&sid)
2823 .and_then(|s| s.nal_none_latched_at)
2824 .is_some_and(|t| now.duration_since(t) >= NAL_NONE_RETRY_BACKOFF);
2825 if ready_to_retry {
2826 if let Some(s) = client.surface_subs.get_mut(&sid) {
2827 s.nal_none_streak = 0;
2828 s.nal_none_latched_at = None;
2829 }
2830 } else {
2831 continue;
2832 }
2833 }
2834
2835 if needs_new_encoder {
2837 let codec_support = client
2838 .surface_subs
2839 .get(&sid)
2840 .map(|s| s.codec_override)
2841 .filter(|&c| c != 0)
2842 .unwrap_or(client.surface_codec_support);
2843 let quality = client
2844 .surface_subs
2845 .get(&sid)
2846 .and_then(|s| s.quality_override)
2847 .unwrap_or(state.config.surface_quality);
2848
2849 for &pref in &state.config.surface_encoders {
2850 if !pref.is_vulkan_video() {
2851 continue;
2852 }
2853 if !pref.supported_by_client(codec_support) {
2854 continue;
2855 }
2856 let available = match pref {
2858 SurfaceEncoderPreference::VulkanVideoH264 => vk_encode_available,
2859 SurfaceEncoderPreference::VulkanVideoAV1 => vk_encode_av1_available,
2860 _ => false,
2861 };
2862 if !available {
2863 continue;
2864 }
2865 if state.config.chroma.is_444() {
2869 continue;
2870 }
2871 let qp = match pref {
2872 SurfaceEncoderPreference::VulkanVideoAV1 => quality.av1_qp_for_vulkan(),
2873 _ => quality.h264_qp(),
2874 };
2875 let enc_name: &'static str = match (pref, state.config.chroma) {
2876 (
2877 SurfaceEncoderPreference::VulkanVideoH264,
2878 ChromaSubsampling::Cs444,
2879 ) => "h264-vulkan 4:4:4",
2880 (SurfaceEncoderPreference::VulkanVideoH264, _) => "h264-vulkan",
2881 (
2882 SurfaceEncoderPreference::VulkanVideoAV1,
2883 ChromaSubsampling::Cs444,
2884 ) => "av1-vulkan 4:4:4",
2885 (SurfaceEncoderPreference::VulkanVideoAV1, _) => "av1-vulkan",
2886 (_, ChromaSubsampling::Cs444) => "vulkan 4:4:4",
2887 _ => "vulkan",
2888 };
2889 pending_vulkan_encoder_setups.push(VulkanEncoderSetup {
2891 surface_id: sid as u32,
2892 codec: pref.vulkan_codec(),
2893 qp,
2894 width: px_w,
2895 height: px_h,
2896 });
2897 pending_vulkan_keyframe_requests.push(sid as u32);
2898 if let Some(s) = client.surface_subs.get_mut(&sid) {
2899 s.encoder = None;
2900 }
2901 client
2902 .vulkan_video_surfaces
2903 .insert(sid, (enc_name, pref.codec_flag()));
2904 let codec_str = match pref {
2905 SurfaceEncoderPreference::VulkanVideoH264 => {
2906 if state.config.chroma.is_444() {
2907 "avc1.F4001f".to_string()
2908 } else {
2909 "avc1.640034".to_string()
2910 }
2911 }
2912 SurfaceEncoderPreference::VulkanVideoAV1 => {
2913 let profile = if state.config.chroma.is_444() { 2 } else { 0 };
2914 let level = surface_encoder::av1_level_for(px_w, px_h);
2915 format!("av01.{profile}.{level}M.08")
2916 }
2917 _ => String::new(),
2918 };
2919 let enc_msg = msg_surface_encoder(sid, enc_name, &codec_str);
2920 let _ = send_outbox(client, enc_msg);
2921 if state.config.verbose {
2922 eprintln!(
2923 "[surface-encoder] cid={} sid={sid} {px_w}x{px_h}: using {enc_name}",
2924 work.cid,
2925 );
2926 }
2927 break;
2928 }
2929
2930 {
2938 let state = client.surface_subs.entry(sid).or_default();
2939 state.encoder = None;
2940 state.creation_in_flight = true;
2941 }
2942 create_jobs.push(CreateJob {
2943 cid: work.cid,
2944 sid,
2945 px_w: enc_w,
2946 px_h: enc_h,
2947 params: EncoderCreateParams {
2948 preferences: state.config.surface_encoders.clone(),
2949 vaapi_device: state.config.vaapi_device.clone(),
2950 quality,
2951 verbose: state.config.verbose,
2952 codec_support,
2953 chroma: state.config.chroma,
2954 },
2955 });
2956 continue;
2957 }
2958
2959 if client.vulkan_video_surfaces.contains_key(&sid) {
2963 if work.needs_keyframe {
2964 pending_vulkan_keyframe_requests.push(sid as u32);
2965 }
2966 client.skip_vulkan_await_count =
2967 client.skip_vulkan_await_count.saturating_add(1);
2968 let now_inst = Instant::now();
2969 if now_inst.duration_since(client.last_skip_log).as_secs_f32() > 5.0 {
2970 client.last_skip_log = now_inst;
2971 eprintln!(
2972 "[encode-skip] cid={} sid={sid} reason=vulkan_await \
2973 (compositor not producing PixelData::Encoded) count={}",
2974 work.cid, client.skip_vulkan_await_count,
2975 );
2976 }
2977 continue;
2978 }
2979
2980 let encoder = client
2981 .surface_subs
2982 .get_mut(&sid)
2983 .and_then(|s| s.encoder.take())
2984 .unwrap();
2985 client.surface_subs.entry(sid).or_default().encode_in_flight = true;
2986 let needs_kf = work.needs_keyframe || needs_new_encoder;
2987 encoded_client_surfaces.insert((work.cid, sid));
2988 encode_dispatched_surfaces.insert(sid);
2989 encode_jobs.push(EncodeJob {
2990 cid: work.cid,
2991 sid,
2992 px_w: enc_w,
2993 px_h: enc_h,
2994 pixels,
2995 needs_keyframe: needs_kf,
2996 encoder,
2997 generation: px_gen,
2998 timestamp_ms: px_timestamp_ms,
2999 });
3000 }
3001 }
3002
3003 if (!pending_vulkan_encoder_setups.is_empty()
3005 || !pending_vulkan_keyframe_requests.is_empty())
3006 && let Some(cs) = sess.compositor.as_ref()
3007 {
3008 for setup in pending_vulkan_encoder_setups {
3009 eprintln!(
3010 "[vulkan-video] sending SetVulkanEncoder sid={} codec={} {}x{} qp={}",
3011 setup.surface_id, setup.codec, setup.width, setup.height, setup.qp,
3012 );
3013 let _ = cs.handle.command_tx.send(
3014 blit_compositor::CompositorCommand::SetVulkanEncoder {
3015 surface_id: setup.surface_id,
3016 codec: setup.codec,
3017 qp: setup.qp,
3018 width: setup.width,
3019 height: setup.height,
3020 },
3021 );
3022 }
3023 for surface_id in pending_vulkan_keyframe_requests {
3024 let _ = cs
3025 .handle
3026 .command_tx
3027 .send(blit_compositor::CompositorCommand::RequestVulkanKeyframe { surface_id });
3028 }
3029 cs.handle.wake();
3030 }
3031
3032 for work in &client_work {
3037 if let Some(client) = sess.clients.get_mut(&work.cid) {
3038 let interval = surface_send_interval(client);
3039 for &sid in &work.subs {
3040 if encoded_client_surfaces.contains(&(work.cid, sid)) {
3041 let deadline = client
3042 .surface_subs
3043 .entry(sid)
3044 .or_default()
3045 .next_send_at
3046 .get_or_insert(now);
3047 advance_deadline(deadline, now, interval);
3048 }
3049 }
3050 }
3051 }
3052 }
3053
3054 if !encode_jobs.is_empty() {
3055 let state2 = state.clone();
3058 tokio::spawn(async move {
3059 let job_ids: Vec<(u64, u16)> = encode_jobs.iter().map(|j| (j.cid, j.sid)).collect();
3063
3064 let handles: Vec<_> = encode_jobs
3065 .into_iter()
3066 .map(|job| {
3067 tokio::task::spawn_blocking(move || {
3068 let mut encoder = job.encoder;
3069 if job.needs_keyframe {
3070 encoder.request_keyframe();
3071 }
3072 let nal_data = encoder.encode_pixels(&job.pixels);
3073 let codec_flag = encoder.codec_flag();
3074 EncodeResult {
3075 cid: job.cid,
3076 sid: job.sid,
3077 px_w: job.px_w,
3078 px_h: job.px_h,
3079 generation: job.generation,
3080 encoder,
3081 nal_data,
3082 codec_flag,
3083 timestamp_ms: job.timestamp_ms,
3084 }
3085 })
3086 })
3087 .collect();
3088
3089 const ENCODE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
3092
3093 let mut results = Vec::with_capacity(handles.len());
3094 let mut failed: Vec<(u64, u16)> = Vec::new();
3095 for (i, h) in handles.into_iter().enumerate() {
3096 let wrapper =
3102 tokio::spawn(async move { tokio::time::timeout(ENCODE_TIMEOUT, h).await });
3103 match wrapper.await {
3104 Ok(Ok(Ok(r))) => results.push(r),
3105 Ok(Ok(Err(_join_err))) => {
3106 let (cid, sid) = job_ids[i];
3108 eprintln!("[surface-encoder] encode task panicked: cid={cid} sid={sid}",);
3109 failed.push(job_ids[i]);
3110 }
3111 Ok(Err(_timeout)) => {
3112 let (cid, sid) = job_ids[i];
3116 eprintln!(
3117 "[surface-encoder] encode timed out ({}s): cid={cid} sid={sid}",
3118 ENCODE_TIMEOUT.as_secs(),
3119 );
3120 failed.push(job_ids[i]);
3121 }
3122 Err(_join_err) => {
3123 eprintln!("[surface-encoder] runtime shutting down, aborting delivery");
3125 return;
3126 }
3127 }
3128 }
3129
3130 let mut sess = state2.session.lock().await;
3132 let now = Instant::now();
3133 let mut local_encodes = 0u32;
3134 let mut local_encode_bytes = 0u64;
3135 let mut local_frames_sent = 0u32;
3136
3137 for (cid, sid) in failed {
3141 if let Some(client) = sess.clients.get_mut(&cid) {
3142 if let Some(s) = client.surface_subs.get_mut(&sid) {
3143 s.encode_in_flight = false;
3144 }
3145 client.surface_needs_keyframe = true;
3151 }
3152 }
3153
3154 for result in results {
3155 let expected_dims: Option<(u32, u32)> = sess
3163 .compositor
3164 .as_ref()
3165 .and_then(|cs| cs.last_pixels.get(&result.sid))
3166 .map(|lp| (lp.width, lp.height));
3167 let dims_match =
3168 expected_dims.is_some_and(|d| result.encoder.source_dimensions() == d);
3169
3170 if let Some(client) = sess.clients.get_mut(&result.cid) {
3171 let state = client.surface_subs.entry(result.sid).or_default();
3172 state.encode_in_flight = false;
3173 let invalidated = std::mem::replace(&mut state.encoder_invalidated, false);
3174 if dims_match && !invalidated {
3175 state.encoder = Some(result.encoder);
3176 }
3177 state.last_encoded_gen = Some(result.generation);
3180 }
3181
3182 let Some((nal_data, is_keyframe)) = result.nal_data else {
3183 if let Some(client) = sess.clients.get_mut(&result.cid) {
3184 let state = client.surface_subs.entry(result.sid).or_default();
3185 state.nal_none_streak += 1;
3186 let streak = state.nal_none_streak;
3187 if streak == 10 {
3188 state.encoder = None;
3189 state.nal_none_latched_at = Some(now);
3190 client.surface_needs_keyframe = true;
3191 eprintln!(
3192 "[encode] nal_data=None x{streak} sid={} cid={} {}x{} — dropping encoder, backing off retry",
3193 result.sid, result.cid, result.px_w, result.px_h,
3194 );
3195 } else if streak < 10 {
3196 eprintln!(
3197 "[encode] nal_data=None sid={} cid={} {}x{}",
3198 result.sid, result.cid, result.px_w, result.px_h,
3199 );
3200 }
3201 }
3203 continue;
3204 };
3205 if let Some(client) = sess.clients.get_mut(&result.cid)
3207 && let Some(s) = client.surface_subs.get_mut(&result.sid)
3208 {
3209 s.nal_none_streak = 0;
3210 }
3211
3212 {
3213 static EC: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
3214 let n = EC.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3215 if n < 5 || n.is_multiple_of(1000) {
3216 eprintln!(
3217 "[encode #{n}] sid={} {}x{} kf={is_keyframe} bytes={}",
3218 result.sid,
3219 result.px_w,
3220 result.px_h,
3221 nal_data.len(),
3222 );
3223 }
3224 }
3225
3226 local_encodes += 1;
3227 local_encode_bytes += nal_data.len() as u64;
3228
3229 let flags = result.codec_flag
3230 | if is_keyframe {
3231 SURFACE_FRAME_FLAG_KEYFRAME
3232 } else {
3233 0
3234 };
3235 let msg = msg_surface_frame(
3236 result.sid,
3237 result.timestamp_ms,
3238 flags,
3239 result.px_w as u16,
3240 result.px_h as u16,
3241 &nal_data,
3242 );
3243 let bytes = msg.len();
3244
3245 let Some(client) = sess.clients.get_mut(&result.cid) else {
3246 continue;
3247 };
3248
3249 match send_outbox(client, msg) {
3256 Err(_e) => {
3257 client.surface_needs_keyframe = true;
3260 }
3261 Ok(()) => {
3262 client.surface_inflight_frames.push_back(InFlightFrame {
3266 sent_at: now,
3267 bytes,
3268 paced: true,
3269 });
3270 if !is_keyframe {
3282 client.avg_surface_frame_bytes = ewma_with_direction(
3283 client.avg_surface_frame_bytes,
3284 bytes as f32,
3285 0.5,
3286 0.125,
3287 );
3288 } else if client.avg_surface_frame_bytes <= 16_384.0 {
3289 client.avg_surface_frame_bytes = (bytes as f32 / 4.0).max(4_096.0);
3298 } else {
3299 client.avg_surface_frame_bytes = ewma_with_direction(
3302 client.avg_surface_frame_bytes,
3303 bytes as f32,
3304 0.05,
3305 0.05,
3306 );
3307 }
3308 client.frames_sent = client.frames_sent.wrapping_add(1);
3309 local_frames_sent += 1;
3310 if client.surface_needs_keyframe && is_keyframe {
3311 client.surface_needs_keyframe = false;
3312 }
3313 if let Some(s) = client.surface_subs.get_mut(&result.sid) {
3314 s.burst_remaining = s.burst_remaining.saturating_sub(1);
3315 }
3316 }
3317 }
3318 }
3319 sess.surface_encodes += local_encodes;
3320 sess.surface_encode_bytes += local_encode_bytes;
3321 sess.surface_frames_sent += local_frames_sent;
3322 drop(sess);
3323 state2.delivery_notify.notify_one();
3325 });
3326 }
3327
3328 if !create_jobs.is_empty() {
3329 let state2 = state.clone();
3337 tokio::spawn(async move {
3338 let job_ids: Vec<(u64, u16)> = create_jobs.iter().map(|j| (j.cid, j.sid)).collect();
3341
3342 let handles: Vec<_> = create_jobs
3343 .into_iter()
3344 .map(|job| {
3345 tokio::task::spawn_blocking(move || {
3346 let params = job.params;
3347 let mut encoder = match SurfaceEncoder::new(
3348 ¶ms.preferences,
3349 job.px_w,
3350 job.px_h,
3351 ¶ms.vaapi_device,
3352 params.quality,
3353 params.verbose,
3354 params.codec_support,
3355 params.chroma,
3356 ) {
3357 Ok(enc) => enc,
3358 Err(err) => {
3359 if params.verbose {
3360 eprintln!(
3361 "[surface-encoder] cid={} sid={} {}x{}: {err}",
3362 job.cid, job.sid, job.px_w, job.px_h,
3363 );
3364 }
3365 return CreateResult {
3366 cid: job.cid,
3367 sid: job.sid,
3368 encoder: None,
3369 fresh: None,
3370 };
3371 }
3372 };
3373
3374 #[cfg(target_os = "linux")]
3375 let external_bufs = {
3376 {
3377 let drm_fd = encoder.drm_fd_raw();
3378 let count = encoder.gbm_buffers().len();
3379 if count > 0 {
3380 encoder.allocate_nv12_buffers(drm_fd, count);
3381 }
3382 }
3383 let gbm_bufs = encoder.gbm_buffers();
3384 if gbm_bufs.is_empty() {
3385 Vec::new()
3386 } else {
3387 let nv12_bufs = encoder.gbm_nv12_buffers();
3388 let (enc_w, enc_h) = encoder.encoder_dimensions();
3389 let bufs: Result<Vec<_>, std::io::Error> = gbm_bufs
3390 .iter()
3391 .enumerate()
3392 .map(|(i, b)| {
3393 let nv12 = nv12_bufs.get(i);
3394 Ok(blit_compositor::ExternalOutputBuffer {
3395 fd: std::sync::Arc::new(b.fd.try_clone()?),
3396 fourcc: 0x34325241,
3397 modifier: 0,
3398 stride: b.stride,
3399 offset: 0,
3400 width: b.width,
3401 height: b.height,
3402 va_surface_id: 0,
3403 va_display: 0,
3404 planes: vec![blit_compositor::ExternalOutputPlane {
3405 offset: 0,
3406 pitch: b.stride,
3407 }],
3408 nv12_fd: nv12.map(|n| n.fd.clone()),
3409 nv12_stride: nv12.map_or(0, |n| n.stride),
3410 nv12_uv_offset: nv12.map_or(0, |n| n.uv_offset),
3411 nv12_modifier: nv12.map_or(0, |n| n.modifier),
3412 nv12_width: enc_w,
3413 nv12_height: enc_h,
3414 })
3415 })
3416 .collect();
3417 match bufs {
3418 Ok(b) => b,
3419 Err(e) => {
3420 eprintln!("[encode] dup gbm fd failed: {e}");
3421 Vec::new()
3422 }
3423 }
3424 }
3425 };
3426 let fresh = FreshEncoder {
3427 name: encoder.encoder_name(),
3428 codec_string: encoder.webcodecs_codec_string(),
3429 #[cfg(target_os = "linux")]
3430 external_bufs,
3431 };
3432 CreateResult {
3433 cid: job.cid,
3434 sid: job.sid,
3435 encoder: Some(encoder),
3436 fresh: Some(fresh),
3437 }
3438 })
3439 })
3440 .collect();
3441
3442 const CREATE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
3443 let mut results: Vec<CreateResult> = Vec::with_capacity(handles.len());
3444 let mut failed: Vec<(u64, u16)> = Vec::new();
3445 for (i, h) in handles.into_iter().enumerate() {
3446 let wrapper =
3447 tokio::spawn(async move { tokio::time::timeout(CREATE_TIMEOUT, h).await });
3448 match wrapper.await {
3449 Ok(Ok(Ok(r))) => results.push(r),
3450 Ok(Ok(Err(_))) | Ok(Err(_)) => {
3451 let (cid, sid) = job_ids[i];
3452 eprintln!("[surface-encoder] create task failed: cid={cid} sid={sid}",);
3453 failed.push(job_ids[i]);
3454 }
3455 Err(_) => return,
3456 }
3457 }
3458
3459 let mut sess = state2.session.lock().await;
3460 let now = Instant::now();
3461
3462 for (cid, sid) in failed {
3465 if let Some(client) = sess.clients.get_mut(&cid)
3466 && let Some(s) = client.surface_subs.get_mut(&sid)
3467 {
3468 s.creation_in_flight = false;
3469 s.nal_none_streak = 10;
3470 s.nal_none_latched_at = Some(now);
3471 }
3472 }
3473
3474 for result in results {
3475 let Some(encoder) = result.encoder else {
3476 if let Some(client) = sess.clients.get_mut(&result.cid)
3477 && let Some(s) = client.surface_subs.get_mut(&result.sid)
3478 {
3479 s.creation_in_flight = false;
3480 s.nal_none_streak = 10;
3481 s.nal_none_latched_at = Some(now);
3482 }
3483 continue;
3484 };
3485
3486 let fresh = result.fresh;
3490 #[cfg(target_os = "linux")]
3491 {
3492 if let Some(f) = &fresh
3493 && !f.external_bufs.is_empty()
3494 && let Some(cs) = sess.compositor.as_mut()
3495 {
3496 cs.last_pixels.remove(&result.sid);
3497 }
3498 }
3499 #[cfg(target_os = "linux")]
3500 let (fresh_meta, external_bufs) = match fresh {
3501 Some(f) => (Some((f.name, f.codec_string)), Some(f.external_bufs)),
3502 None => (None, None),
3503 };
3504 #[cfg(not(target_os = "linux"))]
3505 let fresh_meta = fresh.map(|f| (f.name, f.codec_string));
3506
3507 #[cfg(target_os = "linux")]
3508 if let Some(bufs) = external_bufs
3509 && !bufs.is_empty()
3510 && let Some(cs) = sess.compositor.as_mut()
3511 {
3512 let _ = cs.handle.command_tx.send(
3513 blit_compositor::CompositorCommand::SetExternalOutputBuffers {
3514 surface_id: result.sid as u32,
3515 buffers: bufs,
3516 },
3517 );
3518 cs.handle.wake();
3519 }
3520
3521 if let Some(client) = sess.clients.get_mut(&result.cid) {
3522 let state = client.surface_subs.entry(result.sid).or_default();
3523 state.creation_in_flight = false;
3524 let invalidated = std::mem::replace(&mut state.encoder_invalidated, false);
3525 if invalidated {
3526 continue;
3531 }
3532 state.encoder = Some(encoder);
3533 state.nal_none_streak = 0;
3534 state.nal_none_latched_at = None;
3535 if let Some((name, codec_string)) = fresh_meta {
3536 let enc_msg = msg_surface_encoder(result.sid, name, &codec_string);
3537 let _ = send_outbox(client, enc_msg);
3538 }
3539 }
3540 }
3541 drop(sess);
3542 state2.delivery_notify.notify_one();
3543 });
3544 }
3545
3546 {
3557 let mut wanted: HashSet<u16> = HashSet::new();
3563
3564 for &sid in &encode_dispatched_surfaces {
3568 wanted.insert(sid);
3569 }
3570 let mut blanket_requested = false;
3571 if let Some(cs) = sess.compositor.as_ref()
3576 && now.duration_since(cs.last_blanket_frame_request) >= blanket_frame_interval(&sess)
3577 {
3578 for &sid in cs.surfaces.keys() {
3579 wanted.insert(sid);
3580 }
3581 blanket_requested = true;
3582 }
3583 for client in sess.clients.values() {
3584 if client.surface_subscriptions.is_empty() {
3591 continue;
3592 }
3593 for &sid in &client.surface_subscriptions {
3594 let (burst, deadline) = client.surface_subs.get(&sid).map_or((0, now), |s| {
3595 (s.burst_remaining, s.next_send_at.unwrap_or(now))
3596 });
3597 if deadline <= now || burst > 0 {
3598 wanted.insert(sid);
3599 } else {
3600 next_deadline = Some(match next_deadline {
3601 Some(existing) => existing.min(deadline),
3602 None => deadline,
3603 });
3604 }
3605 }
3606 }
3607
3608 if let Some(cs) = sess.compositor.as_mut() {
3609 if blanket_requested {
3610 cs.last_blanket_frame_request = now;
3611 }
3612
3613 const MIN_REQUEST_INTERVAL: Duration = Duration::from_millis(1);
3620 let mut sent_any = false;
3621 for sid in &wanted {
3622 let dominated = cs
3623 .last_frame_request
3624 .get(sid)
3625 .is_some_and(|&t| now.duration_since(t) < MIN_REQUEST_INTERVAL);
3626 if !dominated {
3627 cs.last_frame_request.insert(*sid, now);
3628 let _ = cs
3629 .handle
3630 .command_tx
3631 .send(CompositorCommand::RequestFrame { surface_id: *sid });
3632 sent_any = true;
3633 }
3634 }
3635 if sent_any {
3636 cs.handle.wake();
3637 }
3638 }
3639 }
3640
3641 drop(sess);
3646 tokio::task::yield_now().await;
3647 sess = state.session.lock().await;
3648
3649 let max_fps = sess
3650 .clients
3651 .values()
3652 .map(browser_pacing_fps)
3653 .fold(1.0_f32, f32::max);
3654 let title_interval = Duration::from_secs_f64(1.0 / max_fps as f64);
3655 let ids: Vec<u16> = sess.ptys.keys().copied().collect();
3656 for &id in &ids {
3657 let Some(pty) = sess.ptys.get_mut(&id) else {
3658 continue;
3659 };
3660 if pty.driver.take_title_dirty() {
3661 pty.mark_dirty();
3662 pty.title_pending = true;
3663 }
3664 if pty.title_pending && now.duration_since(pty.last_title_send) >= title_interval {
3665 let msg = {
3666 let title_bytes = pty.driver.title().as_bytes();
3667 let mut msg = Vec::with_capacity(3 + title_bytes.len());
3668 msg.push(S2C_TITLE);
3669 msg.extend_from_slice(&id.to_le_bytes());
3670 msg.extend_from_slice(title_bytes);
3671 msg
3672 };
3673 pty.last_title_send = now;
3674 pty.title_pending = false;
3675 sess.send_to_all(&msg);
3676 }
3677 }
3678
3679 let ptys_with_subscribers: HashSet<u16> = sess
3692 .clients
3693 .values()
3694 .flat_map(|c| c.subscriptions.iter().copied())
3695 .collect();
3696 let mut eof_ptys: Vec<u16> = Vec::with_capacity(ids.len());
3697 for &id in &ids {
3698 let Some(pty) = sess.ptys.get_mut(&id) else {
3699 continue;
3700 };
3701 let has_subscriber = ptys_with_subscribers.contains(&id);
3702 loop {
3703 if has_subscriber && pty.ready_frames.len() >= READY_FRAME_QUEUE_CAP {
3704 break;
3705 }
3706 let Ok(input) = pty.byte_rx.try_recv() else {
3707 break;
3708 };
3709 match input {
3710 PtyInput::Data(data) => {
3711 pty::respond_to_queries(
3712 &pty.handle,
3713 &data,
3714 pty.driver.size(),
3715 pty.driver.cursor_position(),
3716 );
3717 pty.driver.process(&data);
3718 pty.mark_dirty();
3719 }
3720 PtyInput::SyncBoundary { before } => {
3721 if !before.is_empty() {
3722 pty::respond_to_queries(
3723 &pty.handle,
3724 &before,
3725 pty.driver.size(),
3726 pty.driver.cursor_position(),
3727 );
3728 pty.driver.process(&before);
3729 pty.mark_dirty();
3730 }
3731 if !pty.driver.synced_output() {
3732 let frame = take_snapshot(pty);
3733 enqueue_ready_frame(&mut pty.ready_frames, frame);
3734 pty.clear_dirty();
3735 }
3736 }
3737 PtyInput::Eof => {
3738 eof_ptys.push(id);
3739 }
3740 }
3741 }
3742 }
3743 drop(sess);
3745 for id in eof_ptys {
3746 tokio::time::sleep(Duration::from_millis(50)).await;
3747 cleanup_pty_internal(id, state).await;
3748 }
3749 let mut sess = state.session.lock().await;
3750
3751 let needful_ptys: HashSet<u16> = sess
3755 .clients
3756 .values()
3757 .flat_map(|c| {
3758 let reserve_preview_slot = client_has_due_preview(&sess, c, now);
3759 c.subscriptions.iter().copied().filter(move |pid| {
3760 let scrolled = c.scroll_offsets.get(pid).copied().unwrap_or(0) > 0;
3761 if Some(*pid) == c.lead {
3762 !scrolled && can_send_frame(c, now, reserve_preview_slot)
3763 } else {
3764 !scrolled && can_send_preview(c, *pid, now)
3765 }
3766 })
3767 })
3768 .collect();
3769
3770 let mut snapshots: HashMap<u16, FrameState> = HashMap::new();
3771 for &id in &ids {
3772 let Some(pty) = sess.ptys.get_mut(&id) else {
3773 continue;
3774 };
3775 if needful_ptys.contains(&id)
3776 && let Some(frame) = pty.ready_frames.pop_front()
3777 {
3778 snapshots.insert(id, frame);
3779 sess.tick_snaps += 1;
3780 continue;
3781 }
3782 if !should_snapshot_pty(
3783 pty.dirty,
3784 needful_ptys.contains(&id),
3785 pty.driver.synced_output(),
3786 ) {
3787 continue;
3788 }
3789 snapshots.insert(id, take_snapshot(pty));
3793 pty.clear_dirty();
3794 sess.tick_snaps += 1;
3795 }
3796
3797 let client_ids: Vec<u64> = sess.clients.keys().copied().collect();
3798 for cid in client_ids {
3799 if let Some(c) = sess.clients.get_mut(&cid) {
3805 if c.inflight_bytes == 0 && c.min_rtt_ms > 0.0 && c.rtt_ms > c.min_rtt_ms {
3806 c.rtt_ms = (c.rtt_ms * 0.99 + c.min_rtt_ms * 0.01).max(c.min_rtt_ms);
3807 }
3808 if c.last_metrics_update.elapsed() > Duration::from_secs(1) {
3811 c.browser_backlog_frames = 0;
3812 c.browser_ack_ahead_frames = 0;
3813 }
3814 }
3815 let (
3816 lead,
3817 subscriptions,
3818 scrolled_ptys,
3819 can_send_lead,
3820 lead_has_window,
3821 any_send_window,
3822 lead_deadline,
3823 ) = {
3824 let Some(c) = sess.clients.get(&cid) else {
3825 continue;
3826 };
3827 let reserve_preview_slot = client_has_due_preview(&sess, c, now);
3828 (
3829 c.lead,
3830 c.subscriptions.iter().copied().collect::<Vec<_>>(),
3831 c.scroll_offsets
3832 .iter()
3833 .map(|(&k, &v)| (k, v))
3834 .collect::<Vec<_>>(),
3835 can_send_frame(c, now, reserve_preview_slot),
3836 lead_window_open(c, reserve_preview_slot),
3837 lead_window_open(c, reserve_preview_slot) || window_open(c),
3838 c.next_send_at,
3839 )
3840 };
3841
3842 if subscriptions.is_empty() {
3843 continue;
3844 }
3845
3846 for &(scroll_pid, scroll_offset) in &scrolled_ptys {
3848 if scroll_offset == 0 {
3849 continue;
3850 }
3851 let is_lead = lead == Some(scroll_pid);
3852 let can_send = if is_lead { can_send_lead } else { true };
3853 if can_send {
3854 let prev_frame = {
3855 let Some(c) = sess.clients.get(&cid) else {
3856 continue;
3857 };
3858 c.scroll_caches
3859 .get(&scroll_pid)
3860 .cloned()
3861 .unwrap_or_default()
3862 };
3863 let outcome = if let Some(pty) = sess.ptys.get_mut(&scroll_pid) {
3864 if let Some((msg, new_frame)) =
3865 build_scrollback_update(pty, scroll_pid, scroll_offset, &prev_frame)
3866 {
3867 let Some(c) = sess.clients.get_mut(&cid) else {
3868 break;
3869 };
3870 let bytes = msg.len();
3871 if send_outbox(c, msg).is_ok() {
3872 c.scroll_caches.insert(scroll_pid, new_frame);
3873 record_send(c, bytes, now, is_lead);
3874 c.frames_sent += 1;
3875 SendOutcome::Sent
3876 } else {
3877 SendOutcome::Backpressured
3878 }
3879 } else {
3880 SendOutcome::NoChange
3881 }
3882 } else {
3883 SendOutcome::NoChange
3884 };
3885 match outcome {
3886 SendOutcome::Sent => {}
3887 SendOutcome::Backpressured => {
3888 if let Some(pty) = sess.ptys.get_mut(&scroll_pid) {
3889 pty.mark_dirty();
3890 }
3891 }
3892 SendOutcome::NoChange => {}
3893 }
3894 } else if is_lead && lead_has_window {
3895 next_deadline = Some(match next_deadline {
3896 Some(existing) => existing.min(lead_deadline),
3897 None => lead_deadline,
3898 });
3899 }
3900 }
3901
3902 let lead_scroll_offset = lead
3903 .and_then(|pid| {
3904 scrolled_ptys
3905 .iter()
3906 .find(|&&(k, _)| k == pid)
3907 .map(|&(_, v)| v)
3908 })
3909 .unwrap_or(0);
3910
3911 if let Some(pid) = lead {
3912 if lead_scroll_offset == 0 && can_send_lead {
3913 if let Some(cur) = snapshots.get(&pid).cloned() {
3914 let previous = sess
3915 .clients
3916 .get(&cid)
3917 .and_then(|c| c.last_sent.get(&pid).cloned())
3918 .unwrap_or_default();
3919 drop(sess);
3920 let msg = build_update_msg(pid, &cur, &previous);
3921 sess = state.session.lock().await;
3922 let Some(c) = sess.clients.get_mut(&cid) else {
3923 continue;
3924 };
3925 match try_send_update(c, pid, cur, msg, now, true) {
3926 SendOutcome::Sent => {}
3927 SendOutcome::Backpressured => {
3928 if let Some(pty) = sess.ptys.get_mut(&pid) {
3929 pty.mark_dirty();
3930 }
3931 }
3932 SendOutcome::NoChange => {}
3933 }
3934 } else {
3935 let has_pending = sess
3936 .ptys
3937 .get(&pid)
3938 .map(pty_has_visual_update)
3939 .unwrap_or(false);
3940 let _ = has_pending;
3941 }
3942 } else {
3943 let has_pending = sess
3944 .ptys
3945 .get(&pid)
3946 .map(pty_has_visual_update)
3947 .unwrap_or(false);
3948 if has_pending && lead_has_window {
3949 next_deadline = Some(match next_deadline {
3950 Some(existing) => existing.min(lead_deadline),
3951 None => lead_deadline,
3952 });
3953 }
3954 }
3955 }
3956
3957 if !any_send_window {
3958 continue;
3959 }
3960
3961 let mut preview_ids = subscriptions;
3962 preview_ids.retain(|pid| Some(*pid) != lead);
3963 preview_ids.sort_unstable();
3964
3965 for pid in preview_ids {
3966 let (preview_can_send, preview_due_at, preview_has_window) =
3967 match sess.clients.get(&cid) {
3968 Some(c) => (
3969 can_send_preview(c, pid, now),
3970 preview_deadline(c, pid, now),
3971 window_open(c),
3972 ),
3973 None => (false, now, false),
3974 };
3975 if !preview_has_window {
3976 break;
3977 }
3978 if !preview_can_send {
3979 let has_pending = sess
3980 .ptys
3981 .get(&pid)
3982 .map(pty_has_visual_update)
3983 .unwrap_or(false);
3984 if has_pending && preview_due_at > now {
3989 next_deadline = Some(match next_deadline {
3990 Some(existing) => existing.min(preview_due_at),
3991 None => preview_due_at,
3992 });
3993 }
3994 continue;
3995 }
3996 let Some(cur) = snapshots.get(&pid) else {
3997 let has_pending = sess
3998 .ptys
3999 .get(&pid)
4000 .map(pty_has_visual_update)
4001 .unwrap_or(false);
4002 let _ = has_pending;
4003 continue;
4004 };
4005 let cur = cur.clone();
4006 let previous = sess
4007 .clients
4008 .get(&cid)
4009 .and_then(|c| c.last_sent.get(&pid).cloned())
4010 .unwrap_or_default();
4011 drop(sess);
4012 let msg = build_update_msg(pid, &cur, &previous);
4013 sess = state.session.lock().await;
4014 let Some(c) = sess.clients.get_mut(&cid) else {
4015 break;
4016 };
4017 match try_send_update(c, pid, cur, msg, now, false) {
4018 SendOutcome::Sent => {
4019 record_preview_send(c, pid, now);
4020 }
4021 SendOutcome::Backpressured => {
4022 if let Some(pty) = sess.ptys.get_mut(&pid) {
4023 pty.mark_dirty();
4024 }
4025 break;
4026 }
4027 SendOutcome::NoChange => {}
4028 }
4029 }
4030 }
4031
4032 #[cfg(target_os = "linux")]
4034 let any_audio_subscribed = sess.clients.values().any(|c| c.audio_subscribed);
4035 #[cfg(target_os = "linux")]
4036 if any_audio_subscribed
4037 && let Some(ref mut cs) = sess.compositor
4038 && let Some(ref mut ap) = cs.audio_pipeline
4039 && ap.is_alive()
4040 {
4041 let new_frames = ap.poll_frames();
4042 if !new_frames.is_empty() {
4043 for client in sess.clients.values_mut() {
4044 if client.audio_subscribed {
4045 for frame in &new_frames {
4046 let msg = audio::msg_audio_frame(frame);
4047 let bytes = msg.len();
4048 if client.audio_tx.send(msg).is_ok() {
4053 client.goodput_window_bytes += bytes;
4057 }
4058 }
4059 }
4060 }
4061 }
4062 let next_audio = now + Duration::from_millis(20);
4065 next_deadline = Some(next_deadline.map_or(next_audio, |d: Instant| d.min(next_audio)));
4066 }
4067
4068 #[cfg(target_os = "linux")]
4076 let audio_restart_bitrate: i32 = sess
4077 .clients
4078 .values()
4079 .filter(|c| c.audio_subscribed)
4080 .map(|c| c.audio_bitrate_kbps)
4081 .max()
4082 .map(|kbps| kbps as i32 * 1000)
4083 .unwrap_or(0);
4084 #[cfg(target_os = "linux")]
4085 if let Some(ref mut cs) = sess.compositor {
4086 let pipeline_dead = cs.audio_pipeline.as_mut().is_some_and(|ap| !ap.is_alive());
4087 if pipeline_dead {
4088 const RESTART_COOLDOWN: Duration = Duration::from_secs(5);
4089 let can_restart = cs
4090 .last_audio_restart
4091 .is_none_or(|t| now.duration_since(t) >= RESTART_COOLDOWN);
4092 if can_restart {
4093 cs.last_audio_restart = Some(now);
4094 cs.audio_pipeline = None;
4097 let runtime_dir = std::path::Path::new(&cs.handle.socket_name)
4098 .parent()
4099 .unwrap_or(std::path::Path::new("/tmp"));
4100 let session_id = cs.audio_session_id;
4101 let epoch = cs.created_at;
4102 let verbose = state.config.verbose;
4103 eprintln!("[audio] pipeline died, restarting...");
4104 let pipeline = tokio::task::block_in_place(|| {
4105 audio::AudioPipeline::spawn(
4106 runtime_dir,
4107 session_id,
4108 audio_restart_bitrate,
4109 verbose,
4110 epoch,
4111 )
4112 });
4113 match pipeline {
4114 Ok(p) => {
4115 eprintln!(
4116 "[audio] pipeline restarted, PULSE_SERVER={}",
4117 p.pulse_server_path(),
4118 );
4119 cs.audio_pipeline = Some(p);
4120 }
4121 Err(e) => {
4122 eprintln!("[audio] failed to restart pipeline: {e}");
4123 }
4124 }
4125 }
4126 }
4127 }
4128
4129 {
4135 let blanket_deadline = now + blanket_frame_interval(&sess);
4136 next_deadline = Some(next_deadline.map_or(blanket_deadline, |d| d.min(blanket_deadline)));
4137 }
4138
4139 TickOutcome { next_deadline }
4140}
4141
4142async fn handle_client<S: AsyncRead + AsyncWrite + Unpin + Send + 'static>(
4143 stream: S,
4144 state: AppState,
4145) {
4146 let config = &state.config;
4147 let notify_for_compositor = {
4148 let n = state.delivery_notify.clone();
4149 Arc::new(move || n.notify_one()) as Arc<dyn Fn() + Send + Sync>
4150 };
4151 let (mut reader, mut writer) = tokio::io::split(stream);
4152
4153 let (out_tx, mut out_rx) = mpsc::unbounded_channel::<Vec<u8>>();
4154 let (audio_tx, mut audio_rx) = mpsc::unbounded_channel::<Vec<u8>>();
4155 let outbox_frame_counter = Arc::new(AtomicUsize::new(0));
4156 let outbox_byte_counter = Arc::new(AtomicUsize::new(0));
4157 let sender_outbox_queued_frames = outbox_frame_counter.clone();
4158 let sender_outbox_queued_bytes = outbox_byte_counter.clone();
4159 let sender = tokio::spawn(async move {
4160 loop {
4161 while let Ok(audio_msg) = audio_rx.try_recv() {
4164 if !write_frame(&mut writer, &audio_msg).await {
4165 return;
4166 }
4167 }
4168
4169 let msg = tokio::select! {
4173 biased;
4174 msg = audio_rx.recv() => {
4175 match msg {
4177 Some(m) => {
4178 if !write_frame(&mut writer, &m).await {
4179 break;
4180 }
4181 continue;
4182 }
4183 None => break,
4184 }
4185 }
4186 msg = out_rx.recv() => msg,
4187 };
4188
4189 match msg {
4194 Some(m) => {
4195 let bytes = m.len();
4196 let write_start = Instant::now();
4197 let wrote = write_frame_interleaved(&mut writer, &m, &mut audio_rx).await;
4198 let write_elapsed = write_start.elapsed();
4199 if write_elapsed.as_millis() > 100 {
4200 eprintln!(
4201 "[sender] slow write: bytes={bytes} elapsed={}ms wrote={wrote}",
4202 write_elapsed.as_millis(),
4203 );
4204 }
4205 mark_outbox_drained(
4206 &sender_outbox_queued_frames,
4207 &sender_outbox_queued_bytes,
4208 bytes,
4209 );
4210 if !wrote {
4211 break;
4212 }
4213 }
4214 None => break,
4215 }
4216 }
4217 });
4218 let client_id;
4219
4220 {
4221 let mut sess = state.session.lock().await;
4222 client_id = sess.next_client_id;
4223 sess.next_client_id += 1;
4224 sess.clients.insert(
4225 client_id,
4226 ClientState {
4227 tx: out_tx,
4228 outbox_queued_frames: outbox_frame_counter,
4229 outbox_queued_bytes: outbox_byte_counter,
4230 audio_tx,
4231 lead: None,
4232 subscriptions: HashSet::new(),
4233 surface_subscriptions: HashSet::new(),
4234 audio_subscribed: false,
4235 #[cfg(target_os = "linux")]
4236 audio_bitrate_kbps: 0,
4237 view_sizes: HashMap::new(),
4238 scroll_offsets: HashMap::new(),
4239 scroll_caches: HashMap::new(),
4240 last_sent: HashMap::new(),
4241 preview_next_send_at: HashMap::new(),
4242 rtt_ms: 50.0,
4243 min_rtt_ms: 0.0,
4244 display_fps: 60.0,
4245 delivery_bps: 262_144.0,
4250 goodput_bps: 262_144.0,
4251 goodput_jitter_bps: 0.0,
4252 max_goodput_jitter_bps: 0.0,
4253 last_goodput_sample_bps: 0.0,
4254 avg_frame_bytes: 1_024.0,
4255 avg_paced_frame_bytes: 1_024.0,
4256 avg_preview_frame_bytes: 1_024.0,
4257 avg_surface_frame_bytes: 8_192.0,
4258 inflight_bytes: 0,
4259 inflight_frames: VecDeque::new(),
4260 next_send_at: Instant::now(),
4261 probe_frames: 0.0,
4262 frames_sent: 0,
4263 acks_recv: 0,
4264 acked_bytes_since_log: 0,
4265 browser_backlog_frames: 0,
4266 browser_ack_ahead_frames: 0,
4267 browser_apply_ms: 0.0,
4268 last_metrics_update: Instant::now(),
4269 last_log: Instant::now(),
4270 last_window_blocked_log: Instant::now(),
4271 last_skip_log: Instant::now(),
4272 skip_same_gen_count: 0,
4273 skip_in_flight_count: 0,
4274 skip_pacing_count: 0,
4275 skip_vulkan_await_count: 0,
4276 skip_no_subs_count: 0,
4277 skip_not_subbed_count: 0,
4278 skip_last_pixels_mismatch_count: 0,
4279 encode_loop_iters: 0,
4280 goodput_window_bytes: 0,
4281 goodput_window_start: Instant::now(),
4282 surface_subs: HashMap::new(),
4283 surface_needs_keyframe: true,
4284 surface_inflight_frames: VecDeque::new(),
4285 vulkan_video_surfaces: HashMap::new(),
4286 surface_view_sizes: HashMap::new(),
4287 surface_codec_support: 0,
4288 pressed_surface_keys: HashSet::new(),
4289 },
4290 );
4291 state.delivery_notify.notify_one();
4293 if let Some(c) = sess.clients.get(&client_id) {
4294 let mut features = FEATURE_CREATE_NONCE
4295 | FEATURE_RESTART
4296 | FEATURE_RESIZE_BATCH
4297 | FEATURE_COPY_RANGE
4298 | FEATURE_COMPOSITOR;
4299 #[cfg(target_os = "linux")]
4300 {
4301 let audio_disabled = std::env::var("BLIT_AUDIO")
4302 .map(|v| v == "0")
4303 .unwrap_or(false);
4304 if !audio_disabled && audio::pipewire_available() {
4305 features |= FEATURE_AUDIO;
4306 }
4307 }
4308 let _ = send_outbox(c, msg_hello(1, features));
4309 }
4310 let mut initial_msgs = Vec::with_capacity(2 + sess.ptys.len() * 2);
4311 if let Some(cs) = sess.compositor.as_ref() {
4316 for info in cs.surfaces.values() {
4317 let (w, h) = if info.width == 0 && info.height == 0 {
4320 cs.last_pixels
4321 .get(&info.surface_id)
4322 .map(|lp| (lp.width as u16, lp.height as u16))
4323 .unwrap_or((0, 0))
4324 } else {
4325 (info.width, info.height)
4326 };
4327 initial_msgs.push(msg_surface_created(
4328 info.surface_id,
4329 info.parent_id,
4330 w,
4331 h,
4332 &info.title,
4333 &info.app_id,
4334 ));
4335 if w > 0 && h > 0 {
4338 initial_msgs.push(msg_surface_resized(info.surface_id, w, h));
4339 }
4340 }
4341 }
4342 initial_msgs.push(sess.pty_list_msg());
4343 for (&id, pty) in &sess.ptys {
4344 let title = pty.driver.title();
4345 if !title.is_empty() {
4346 let title_bytes = title.as_bytes();
4347 let mut msg = Vec::with_capacity(3 + title_bytes.len());
4348 msg.push(S2C_TITLE);
4349 msg.extend_from_slice(&id.to_le_bytes());
4350 msg.extend_from_slice(title_bytes);
4351 initial_msgs.push(msg);
4352 }
4353 if pty.exited {
4354 initial_msgs.push(blit_remote::msg_exited(id, pty.exit_status));
4355 }
4356 }
4357 initial_msgs.push(vec![S2C_READY]);
4358 let tx = sess.clients.get(&client_id).map(|c| {
4359 (
4360 c.tx.clone(),
4361 c.outbox_queued_frames.clone(),
4362 c.outbox_queued_bytes.clone(),
4363 )
4364 });
4365 drop(sess);
4366 if let Some((tx, queued_frames, queued_bytes)) = tx {
4367 for msg in initial_msgs {
4368 if send_outbox_tracked(&tx, &queued_frames, &queued_bytes, msg).is_err() {
4369 break;
4370 }
4371 }
4372 }
4373 }
4374
4375 if state.config.verbose {
4376 eprintln!("client connected");
4377 }
4378
4379 while let Some(data) = read_frame(&mut reader).await {
4380 if data.is_empty() {
4381 continue;
4382 }
4383
4384 if data[0] == C2S_ACK {
4385 let mut sess = state.session.lock().await;
4386 if let Some(c) = sess.clients.get_mut(&client_id) {
4387 c.acks_recv += 1;
4388 record_ack(c);
4389 } else {
4390 continue;
4391 }
4392 maybe_log_pacing_metrics(&mut sess, client_id, config.verbose);
4393 nudge_delivery(&state);
4394 continue;
4395 }
4396
4397 if data[0] == C2S_PING {
4398 continue;
4402 }
4403
4404 if data[0] == C2S_DISPLAY_RATE && data.len() >= 3 {
4405 let fps = u16::from_le_bytes([data[1], data[2]]) as f32;
4406 if fps > 0.0 {
4407 let mut sess = state.session.lock().await;
4408 if let Some(c) = sess.clients.get_mut(&client_id) {
4409 c.display_fps = fps;
4410 }
4411 let max_fps = sess
4414 .clients
4415 .values()
4416 .map(|c| c.display_fps)
4417 .fold(0.0f32, f32::max);
4418 let mhz = (max_fps * 1000.0).round() as u32;
4419 if mhz > 0
4420 && let Some(cs) = &sess.compositor
4421 {
4422 let _ = cs
4423 .handle
4424 .command_tx
4425 .send(blit_compositor::CompositorCommand::SetRefreshRate { mhz });
4426 }
4427 }
4428 nudge_delivery(&state);
4429 continue;
4430 }
4431
4432 if data[0] == C2S_CLIENT_METRICS && data.len() >= 7 {
4433 let backlog_frames = u16::from_le_bytes([data[1], data[2]]);
4434 let ack_ahead_frames = u16::from_le_bytes([data[3], data[4]]);
4435 let apply_ms = u16::from_le_bytes([data[5], data[6]]) as f32 * 0.1;
4436 let mut sess = state.session.lock().await;
4437 if let Some(c) = sess.clients.get_mut(&client_id) {
4438 c.browser_backlog_frames = backlog_frames;
4439 c.browser_ack_ahead_frames = ack_ahead_frames;
4440 c.browser_apply_ms = apply_ms;
4441 c.last_metrics_update = Instant::now();
4442 }
4443 nudge_delivery(&state);
4444 continue;
4445 }
4446
4447 if data[0] == C2S_MOUSE && data.len() >= 9 {
4450 let pid = u16::from_le_bytes([data[1], data[2]]);
4451 let type_ = data[3];
4452 let button = data[4];
4453 let col = u16::from_le_bytes([data[5], data[6]]);
4454 let row = u16::from_le_bytes([data[7], data[8]]);
4455 let sess = state.session.lock().await;
4456 if let Some(pty) = sess.ptys.get(&pid) {
4457 let (echo, icanon) = pty.lflag_cache;
4458 if let Some(seq) = pty
4459 .driver
4460 .mouse_event(type_, button, col, row, echo, icanon)
4461 && let Some(&fd) = state.pty_fds.read().unwrap().get(&pid)
4462 {
4463 pty::pty_write_all(fd, &seq);
4464 }
4465 }
4466 continue;
4467 }
4468
4469 if data[0] == C2S_INPUT && data.len() >= 3 {
4470 let pid = u16::from_le_bytes([data[1], data[2]]);
4471 let mut need_nudge = false;
4472 {
4473 let mut sess = state.session.lock().await;
4474 if let Some(c) = sess.clients.get_mut(&client_id)
4475 && update_client_scroll_state(c, pid, 0)
4476 && let Some(pty) = sess.ptys.get_mut(&pid)
4477 {
4478 pty.mark_dirty();
4479 need_nudge = true;
4480 }
4481 }
4482 if need_nudge {
4483 nudge_delivery(&state);
4484 }
4485 if let Some(&fd) = state.pty_fds.read().unwrap().get(&pid) {
4486 pty::pty_write_all(fd, &data[3..]);
4487 }
4488 continue;
4489 }
4490
4491 if data[0] == C2S_SEARCH && data.len() >= 3 {
4492 let request_id = u16::from_le_bytes([data[1], data[2]]);
4493 let query = std::str::from_utf8(&data[3..]).unwrap_or("").trim();
4494 let mut sess = state.session.lock().await;
4495 let lead = sess.clients.get(&client_id).and_then(|c| c.lead);
4496 let mut ranked: Vec<SearchResultRow> = if query.is_empty() {
4497 Vec::new()
4498 } else {
4499 sess.ptys
4500 .iter()
4501 .filter_map(|(&pty_id, pty)| {
4502 pty.driver
4503 .search_result(query)
4504 .map(|result| SearchResultRow {
4505 pty_id,
4506 score: result.score,
4507 primary_source: result.primary_source,
4508 matched_sources: result.matched_sources,
4509 context: result.context,
4510 scroll_offset: result.scroll_offset,
4511 })
4512 })
4513 .collect()
4514 };
4515 ranked.sort_by(|a, b| {
4516 b.score
4517 .cmp(&a.score)
4518 .then_with(|| (Some(b.pty_id) == lead).cmp(&(Some(a.pty_id) == lead)))
4519 .then_with(|| a.pty_id.cmp(&b.pty_id))
4520 });
4521 if let Some(client) = sess.clients.get_mut(&client_id) {
4522 let _ = send_outbox(client, build_search_results_msg(request_id, &ranked));
4523 }
4524 continue;
4525 }
4526
4527 if data[0] == C2S_SURFACE_CAPTURE && data.len() >= 3 {
4528 let surface_id = u16::from_le_bytes([data[1], data[2]]);
4529 let format = data.get(3).copied().unwrap_or(CAPTURE_FORMAT_PNG);
4531 let quality = data.get(4).copied().unwrap_or(0);
4532 let scale_120 = if data.len() >= 7 {
4533 u16::from_le_bytes([data[5], data[6]])
4534 } else {
4535 0
4536 };
4537
4538 let mut reply_msg = vec![S2C_SURFACE_CAPTURE];
4539 reply_msg.extend_from_slice(&surface_id.to_le_bytes());
4540
4541 eprintln!("[capture] acquiring lock for surface {surface_id}");
4542 let (snapshot, command_tx) = {
4543 let sess = state.session.lock().await;
4544 eprintln!("[capture] lock acquired");
4545 let snap = sess
4546 .compositor
4547 .as_ref()
4548 .and_then(|cs| cs.last_pixels.get(&surface_id))
4549 .map(|lp| (lp.width, lp.height, lp.pixels.clone()));
4550 let cmd_tx = sess
4551 .compositor
4552 .as_ref()
4553 .map(|cs| cs.handle.command_tx.clone());
4554 (snap, cmd_tx)
4555 };
4556
4557 let mut captured: Option<(u32, u32, Vec<u8>)> = None;
4562 if let Some(ctx) = command_tx {
4563 captured = request_surface_capture_with_timeout(
4564 ctx,
4565 surface_id,
4566 scale_120,
4567 Duration::from_secs(5),
4568 )
4569 .await;
4570 }
4571
4572 if captured.is_none() {
4575 captured = snapshot.and_then(|(w, h, pixels)| {
4576 if pixels.is_dmabuf() {
4577 return None;
4578 }
4579 let rgba = pixels.to_rgba(w, h);
4580 if rgba.is_empty() {
4581 None
4582 } else {
4583 Some((w, h, rgba))
4584 }
4585 });
4586 }
4587
4588 eprintln!("[capture] acquiring client_tx lock");
4589 let client_tx = {
4590 let sess = state.session.lock().await;
4591 eprintln!("[capture] client_tx lock acquired");
4592 sess.clients.get(&client_id).map(|c| {
4593 (
4594 c.tx.clone(),
4595 c.outbox_queued_frames.clone(),
4596 c.outbox_queued_bytes.clone(),
4597 )
4598 })
4599 };
4600
4601 if let Some((w, h, rgba_pixels)) = captured {
4602 let image_data = encode_capture(&rgba_pixels, w, h, format, quality);
4603 reply_msg.extend_from_slice(&w.to_le_bytes());
4604 reply_msg.extend_from_slice(&h.to_le_bytes());
4605 reply_msg.extend_from_slice(&image_data);
4606 } else {
4607 reply_msg.extend_from_slice(&0u32.to_le_bytes());
4608 reply_msg.extend_from_slice(&0u32.to_le_bytes());
4609 }
4610
4611 if let Some((client_tx, queued_frames, queued_bytes)) = client_tx {
4612 eprintln!("[capture] sending reply: {} bytes", reply_msg.len());
4613 match send_outbox_tracked(&client_tx, &queued_frames, &queued_bytes, reply_msg) {
4614 Ok(()) => eprintln!("[capture] sent OK"),
4615 Err(e) => eprintln!("[capture] send failed: {e}"),
4616 }
4617 } else {
4618 eprintln!("[capture] no client_tx");
4619 }
4620 continue;
4621 }
4622
4623 if data[0] == C2S_QUIT {
4624 let sess = state.session.lock().await;
4625 sess.send_to_all(&[S2C_QUIT]);
4626 drop(sess);
4627 state.shutdown_notify.notify_one();
4628 break;
4629 }
4630
4631 let mut sess = state.session.lock().await;
4632 let mut need_nudge = false;
4633 match data[0] {
4634 C2S_SCROLL if data.len() >= 7 => {
4635 let pid = u16::from_le_bytes([data[1], data[2]]);
4636 let offset = u32::from_le_bytes([data[3], data[4], data[5], data[6]]) as usize;
4637 if sess.ptys.contains_key(&pid) {
4638 if let Some(c) = sess.clients.get_mut(&client_id) {
4639 update_client_scroll_state(c, pid, offset);
4640 }
4641 if let Some(pty) = sess.ptys.get_mut(&pid) {
4642 pty.mark_dirty();
4643 need_nudge = true;
4644 }
4645 }
4646 }
4647 C2S_RESIZE if data.len() >= 7 => {
4648 let entries = data[1..].chunks_exact(6);
4649 if !entries.remainder().is_empty() {
4650 continue;
4651 }
4652 let mut touched = Vec::with_capacity((data.len() - 1) / 6);
4653 for entry in entries {
4654 let pid = u16::from_le_bytes([entry[0], entry[1]]);
4655 if !sess.ptys.contains_key(&pid) {
4656 continue;
4657 }
4658 let rows = u16::from_le_bytes([entry[2], entry[3]]);
4659 let cols = u16::from_le_bytes([entry[4], entry[5]]);
4660 if let Some(c) = sess.clients.get_mut(&client_id) {
4661 if is_unset_view_size(rows, cols) {
4662 if c.view_sizes.remove(&pid).is_some() {
4663 touched.push(pid);
4664 }
4665 } else if rows == 0 || cols == 0 {
4666 continue;
4667 } else {
4668 c.view_sizes.insert(pid, (rows, cols));
4669 touched.push(pid);
4670 }
4671 }
4672 }
4673 if sess.resize_ptys_to_mediated_sizes(touched) {
4674 need_nudge = true;
4675 }
4676 }
4677 C2S_CREATE => {
4678 let (rows, cols) = if data.len() >= 5 {
4680 (
4681 u16::from_le_bytes([data[1], data[2]]),
4682 u16::from_le_bytes([data[3], data[4]]),
4683 )
4684 } else {
4685 (24, 80)
4686 };
4687 let tag_len = if data.len() >= 7 {
4688 u16::from_le_bytes([data[5], data[6]]) as usize
4689 } else {
4690 0
4691 };
4692 let tag = if data.len() >= 7 + tag_len {
4693 std::str::from_utf8(&data[7..7 + tag_len]).unwrap_or_default()
4694 } else {
4695 ""
4696 };
4697 let cmd_start = 7 + tag_len;
4698 let dir: Option<String> = None;
4699 let create_payload = data
4700 .get(cmd_start..)
4701 .and_then(|bytes| std::str::from_utf8(bytes).ok());
4702 let command = create_payload
4703 .filter(|payload| !payload.contains('\0'))
4704 .map(str::trim)
4705 .filter(|payload| !payload.is_empty());
4706 let argv: Option<Vec<&str>> = create_payload
4707 .filter(|payload| payload.contains('\0'))
4708 .map(|payload| {
4709 payload
4710 .split('\0')
4711 .filter(|arg| !arg.is_empty())
4712 .collect::<Vec<_>>()
4713 })
4714 .filter(|args| !args.is_empty());
4715 let Some(id) = sess.allocate_pty_id(config.max_ptys) else {
4716 continue;
4717 };
4718 let socket_name = sess
4719 .ensure_compositor(
4720 config.verbose,
4721 notify_for_compositor.clone(),
4722 &config.vaapi_device,
4723 )
4724 .to_string();
4725 #[cfg(target_os = "linux")]
4726 let pulse_server = sess.pulse_server_path();
4727 #[cfg(not(target_os = "linux"))]
4728 let pulse_server: Option<String> = None;
4729 #[cfg(target_os = "linux")]
4730 let pipewire_remote = sess.pipewire_remote_path();
4731 #[cfg(not(target_os = "linux"))]
4732 let pipewire_remote: Option<String> = None;
4733 if let Some(pty) = pty::spawn_pty(
4734 &config.shell,
4735 &config.shell_flags,
4736 rows,
4737 cols,
4738 id,
4739 tag,
4740 command,
4741 argv.as_deref(),
4742 dir.as_deref(),
4743 config.scrollback,
4744 state.clone(),
4745 Some(&socket_name),
4746 pulse_server.as_deref(),
4747 pipewire_remote.as_deref(),
4748 ) {
4749 let mut msg = Vec::with_capacity(3 + pty.tag.len());
4750 msg.push(S2C_CREATED);
4751 msg.extend_from_slice(&id.to_le_bytes());
4752 msg.extend_from_slice(pty.tag.as_bytes());
4753 sess.ptys.insert(id, pty);
4754 if let Some(c) = sess.clients.get_mut(&client_id) {
4755 c.lead = Some(id);
4756 c.view_sizes.insert(id, (rows, cols));
4757 subscribe_client_to(c, id);
4758 reset_inflight(c);
4759 }
4760 sess.send_to_all(&msg);
4761 need_nudge = true;
4762 }
4763 }
4764 C2S_CREATE_N => {
4765 let nonce = if data.len() >= 3 {
4767 u16::from_le_bytes([data[1], data[2]])
4768 } else {
4769 0
4770 };
4771 let (rows, cols) = if data.len() >= 7 {
4772 (
4773 u16::from_le_bytes([data[3], data[4]]),
4774 u16::from_le_bytes([data[5], data[6]]),
4775 )
4776 } else {
4777 (24, 80)
4778 };
4779 let tag_len = if data.len() >= 9 {
4780 u16::from_le_bytes([data[7], data[8]]) as usize
4781 } else {
4782 0
4783 };
4784 let tag = if data.len() >= 9 + tag_len {
4785 std::str::from_utf8(&data[9..9 + tag_len]).unwrap_or_default()
4786 } else {
4787 ""
4788 };
4789 let cmd_start = 9 + tag_len;
4790 let dir: Option<String> = None;
4791 let create_payload = data
4792 .get(cmd_start..)
4793 .and_then(|bytes| std::str::from_utf8(bytes).ok());
4794 let command = create_payload
4795 .filter(|payload| !payload.contains('\0'))
4796 .map(str::trim)
4797 .filter(|payload| !payload.is_empty());
4798 let argv: Option<Vec<&str>> = create_payload
4799 .filter(|payload| payload.contains('\0'))
4800 .map(|payload| {
4801 payload
4802 .split('\0')
4803 .filter(|arg| !arg.is_empty())
4804 .collect::<Vec<_>>()
4805 })
4806 .filter(|args| !args.is_empty());
4807 let Some(id) = sess.allocate_pty_id(config.max_ptys) else {
4808 continue;
4809 };
4810 let socket_name = sess
4811 .ensure_compositor(
4812 config.verbose,
4813 notify_for_compositor.clone(),
4814 &config.vaapi_device,
4815 )
4816 .to_string();
4817 #[cfg(target_os = "linux")]
4818 let pulse_server = sess.pulse_server_path();
4819 #[cfg(not(target_os = "linux"))]
4820 let pulse_server: Option<String> = None;
4821 #[cfg(target_os = "linux")]
4822 let pipewire_remote = sess.pipewire_remote_path();
4823 #[cfg(not(target_os = "linux"))]
4824 let pipewire_remote: Option<String> = None;
4825 if let Some(pty) = pty::spawn_pty(
4826 &config.shell,
4827 &config.shell_flags,
4828 rows,
4829 cols,
4830 id,
4831 tag,
4832 command,
4833 argv.as_deref(),
4834 dir.as_deref(),
4835 config.scrollback,
4836 state.clone(),
4837 Some(&socket_name),
4838 pulse_server.as_deref(),
4839 pipewire_remote.as_deref(),
4840 ) {
4841 let tag_bytes = pty.tag.as_bytes();
4842 let mut nonce_msg = Vec::with_capacity(5 + tag_bytes.len());
4843 nonce_msg.push(S2C_CREATED_N);
4844 nonce_msg.extend_from_slice(&nonce.to_le_bytes());
4845 nonce_msg.extend_from_slice(&id.to_le_bytes());
4846 nonce_msg.extend_from_slice(tag_bytes);
4847 let mut broadcast_msg = Vec::with_capacity(3 + tag_bytes.len());
4848 broadcast_msg.push(S2C_CREATED);
4849 broadcast_msg.extend_from_slice(&id.to_le_bytes());
4850 broadcast_msg.extend_from_slice(tag_bytes);
4851 sess.ptys.insert(id, pty);
4852 if let Some(c) = sess.clients.get_mut(&client_id) {
4853 c.lead = Some(id);
4854 c.view_sizes.insert(id, (rows, cols));
4855 subscribe_client_to(c, id);
4856 reset_inflight(c);
4857 let _ = send_outbox(c, nonce_msg);
4858 }
4859 for (&cid, c) in sess.clients.iter() {
4860 if cid != client_id {
4861 let _ = send_outbox(c, broadcast_msg.clone());
4862 }
4863 }
4864 need_nudge = true;
4865 }
4866 }
4867 C2S_CREATE_AT => {
4868 let (rows, cols) = if data.len() >= 5 {
4870 (
4871 u16::from_le_bytes([data[1], data[2]]),
4872 u16::from_le_bytes([data[3], data[4]]),
4873 )
4874 } else {
4875 (24, 80)
4876 };
4877 let tag_len = if data.len() >= 7 {
4878 u16::from_le_bytes([data[5], data[6]]) as usize
4879 } else {
4880 0
4881 };
4882 let tag = if data.len() >= 7 + tag_len {
4883 std::str::from_utf8(&data[7..7 + tag_len]).unwrap_or_default()
4884 } else {
4885 ""
4886 };
4887 let src_start = 7 + tag_len;
4888 let dir = if data.len() >= src_start + 2 {
4889 let src_id = u16::from_le_bytes([data[src_start], data[src_start + 1]]);
4890 sess.ptys.get(&src_id).and_then(|p| pty::pty_cwd(&p.handle))
4891 } else {
4892 None
4893 };
4894 let Some(id) = sess.allocate_pty_id(config.max_ptys) else {
4895 continue;
4896 };
4897 let socket_name = sess
4898 .ensure_compositor(
4899 config.verbose,
4900 notify_for_compositor.clone(),
4901 &config.vaapi_device,
4902 )
4903 .to_string();
4904 #[cfg(target_os = "linux")]
4905 let pulse_server = sess.pulse_server_path();
4906 #[cfg(not(target_os = "linux"))]
4907 let pulse_server: Option<String> = None;
4908 #[cfg(target_os = "linux")]
4909 let pipewire_remote = sess.pipewire_remote_path();
4910 #[cfg(not(target_os = "linux"))]
4911 let pipewire_remote: Option<String> = None;
4912 if let Some(pty) = pty::spawn_pty(
4913 &config.shell,
4914 &config.shell_flags,
4915 rows,
4916 cols,
4917 id,
4918 tag,
4919 None,
4920 None,
4921 dir.as_deref(),
4922 config.scrollback,
4923 state.clone(),
4924 Some(&socket_name),
4925 pulse_server.as_deref(),
4926 pipewire_remote.as_deref(),
4927 ) {
4928 let mut msg = Vec::with_capacity(3 + pty.tag.len());
4929 msg.push(S2C_CREATED);
4930 msg.extend_from_slice(&id.to_le_bytes());
4931 msg.extend_from_slice(pty.tag.as_bytes());
4932 sess.ptys.insert(id, pty);
4933 if let Some(c) = sess.clients.get_mut(&client_id) {
4934 c.lead = Some(id);
4935 c.view_sizes.insert(id, (rows, cols));
4936 subscribe_client_to(c, id);
4937 reset_inflight(c);
4938 }
4939 sess.send_to_all(&msg);
4940 need_nudge = true;
4941 }
4942 }
4943 C2S_CREATE2 => {
4944 if data.len() < 10 {
4945 continue;
4946 }
4947 let nonce = u16::from_le_bytes([data[1], data[2]]);
4948 let rows = u16::from_le_bytes([data[3], data[4]]);
4949 let cols = u16::from_le_bytes([data[5], data[6]]);
4950 let features = data[7];
4951 let tag_len = u16::from_le_bytes([data[8], data[9]]) as usize;
4952 let tag = if data.len() >= 10 + tag_len {
4953 std::str::from_utf8(&data[10..10 + tag_len]).unwrap_or_default()
4954 } else {
4955 ""
4956 };
4957 let mut cursor = 10 + tag_len;
4958 let dir = if features & CREATE2_HAS_SRC_PTY != 0 && data.len() >= cursor + 2 {
4959 let src_id = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
4960 cursor += 2;
4961 sess.ptys.get(&src_id).and_then(|p| pty::pty_cwd(&p.handle))
4962 } else {
4963 None
4964 };
4965 let create_payload = if features & CREATE2_HAS_COMMAND != 0 {
4966 data.get(cursor..).and_then(|b| std::str::from_utf8(b).ok())
4967 } else {
4968 None
4969 };
4970 let command = create_payload
4971 .filter(|p| !p.contains('\0'))
4972 .map(str::trim)
4973 .filter(|p| !p.is_empty());
4974 let argv: Option<Vec<&str>> = create_payload
4975 .filter(|p| p.contains('\0'))
4976 .map(|p| p.split('\0').filter(|a| !a.is_empty()).collect::<Vec<_>>())
4977 .filter(|a| !a.is_empty());
4978 let Some(id) = sess.allocate_pty_id(config.max_ptys) else {
4979 continue;
4980 };
4981 let socket_name = sess
4982 .ensure_compositor(
4983 config.verbose,
4984 notify_for_compositor.clone(),
4985 &config.vaapi_device,
4986 )
4987 .to_string();
4988 #[cfg(target_os = "linux")]
4989 let pulse_server = sess.pulse_server_path();
4990 #[cfg(not(target_os = "linux"))]
4991 let pulse_server: Option<String> = None;
4992 #[cfg(target_os = "linux")]
4993 let pipewire_remote = sess.pipewire_remote_path();
4994 #[cfg(not(target_os = "linux"))]
4995 let pipewire_remote: Option<String> = None;
4996 if let Some(pty) = pty::spawn_pty(
4997 &config.shell,
4998 &config.shell_flags,
4999 rows,
5000 cols,
5001 id,
5002 tag,
5003 command,
5004 argv.as_deref(),
5005 dir.as_deref(),
5006 config.scrollback,
5007 state.clone(),
5008 Some(&socket_name),
5009 pulse_server.as_deref(),
5010 pipewire_remote.as_deref(),
5011 ) {
5012 let tag_bytes = pty.tag.as_bytes();
5013 let mut nonce_msg = Vec::with_capacity(5 + tag_bytes.len());
5014 nonce_msg.push(S2C_CREATED_N);
5015 nonce_msg.extend_from_slice(&nonce.to_le_bytes());
5016 nonce_msg.extend_from_slice(&id.to_le_bytes());
5017 nonce_msg.extend_from_slice(tag_bytes);
5018 let mut broadcast_msg = Vec::with_capacity(3 + tag_bytes.len());
5019 broadcast_msg.push(S2C_CREATED);
5020 broadcast_msg.extend_from_slice(&id.to_le_bytes());
5021 broadcast_msg.extend_from_slice(tag_bytes);
5022 sess.ptys.insert(id, pty);
5023 if let Some(c) = sess.clients.get_mut(&client_id) {
5024 c.lead = Some(id);
5025 c.view_sizes.insert(id, (rows, cols));
5026 subscribe_client_to(c, id);
5027 reset_inflight(c);
5028 let _ = send_outbox(c, nonce_msg);
5029 }
5030 for (&cid, c) in sess.clients.iter() {
5031 if cid != client_id {
5032 let _ = send_outbox(c, broadcast_msg.clone());
5033 }
5034 }
5035 need_nudge = true;
5036 }
5037 }
5038 C2S_SURFACE_INPUT if data.len() >= 8 => {
5039 let surface_id = u16::from_le_bytes([data[1], data[2]]);
5040 let keycode = u32::from_le_bytes([data[3], data[4], data[5], data[6]]);
5041 let pressed = data[7] != 0;
5042 if let Some(client) = sess.clients.get_mut(&client_id) {
5043 if pressed {
5044 client.pressed_surface_keys.insert(keycode);
5045 } else {
5046 client.pressed_surface_keys.remove(&keycode);
5047 }
5048 }
5049 if let Some(cs) = sess.compositor.as_mut() {
5050 let _ = cs.handle.command_tx.send(CompositorCommand::KeyInput {
5051 surface_id,
5052 keycode,
5053 pressed,
5054 });
5055 cs.handle.wake();
5056 state.delivery_notify.notify_one();
5057 }
5058 }
5059 C2S_SURFACE_TEXT if data.len() >= 3 => {
5060 let _surface_id = u16::from_le_bytes([data[1], data[2]]);
5061 if let Ok(text) = std::str::from_utf8(&data[3..])
5062 && let Some(cs) = sess.compositor.as_mut()
5063 {
5064 let _ = cs.handle.command_tx.send(CompositorCommand::TextInput {
5065 text: text.to_string(),
5066 });
5067 cs.handle.wake();
5068 state.delivery_notify.notify_one();
5069 }
5070 }
5071 C2S_SURFACE_POINTER if data.len() >= 9 => {
5072 let surface_id = u16::from_le_bytes([data[1], data[2]]);
5073 let ptype = data[3];
5074 let button = data[4];
5075 let x = u16::from_le_bytes([data[5], data[6]]) as f64;
5076 let y = u16::from_le_bytes([data[7], data[8]]) as f64;
5077 if let Some(cs) = sess.compositor.as_mut() {
5078 match ptype {
5079 0 | 1 => {
5080 let _ = cs.handle.command_tx.send(CompositorCommand::PointerMotion {
5081 surface_id,
5082 x,
5083 y,
5084 });
5085 let _ = cs.handle.command_tx.send(CompositorCommand::PointerButton {
5086 surface_id,
5087 button: match button {
5088 1 => 0x112,
5089 2 => 0x111,
5090 _ => 0x110,
5091 },
5092 pressed: ptype == 0,
5093 });
5094 }
5095 2 => {
5096 let _ = cs.handle.command_tx.send(CompositorCommand::PointerMotion {
5097 surface_id,
5098 x,
5099 y,
5100 });
5101 }
5102 _ => {}
5103 }
5104 cs.handle.wake();
5105 }
5106 state.delivery_notify.notify_one();
5107 }
5108 C2S_SURFACE_POINTER_AXIS if data.len() >= 8 => {
5109 let surface_id = u16::from_le_bytes([data[1], data[2]]);
5110 let axis = data[3];
5111 let value_x100 = i32::from_le_bytes([data[4], data[5], data[6], data[7]]);
5112 let value = value_x100 as f64 / 100.0;
5113 if let Some(cs) = sess.compositor.as_mut() {
5114 let _ = cs.handle.command_tx.send(CompositorCommand::PointerAxis {
5115 surface_id,
5116 axis,
5117 value,
5118 });
5119 cs.handle.wake();
5120 }
5121 state.delivery_notify.notify_one();
5122 }
5123 C2S_SURFACE_RESIZE if data.len() >= 9 => {
5124 let surface_id = u16::from_le_bytes([data[1], data[2]]);
5125 let width = u16::from_le_bytes([data[3], data[4]]);
5126 let height = u16::from_le_bytes([data[5], data[6]]);
5127 let scale_120 = u16::from_le_bytes([data[7], data[8]]);
5129 if state.config.verbose {
5130 eprintln!(
5131 "C2S_SURFACE_RESIZE: cid={client_id} sid={surface_id} {width}x{height} scale={scale_120}"
5132 );
5133 }
5134 if let Some(c) = sess.clients.get_mut(&client_id) {
5135 if is_unset_view_size(width, height) {
5136 c.surface_view_sizes.remove(&surface_id);
5137 } else if width > 0 && height > 0 {
5138 c.surface_view_sizes
5139 .insert(surface_id, (width, height, scale_120));
5140 }
5141 if let Some(s) = c.surface_subs.get_mut(&surface_id) {
5147 s.nal_none_streak = 0;
5148 s.nal_none_latched_at = None;
5149 }
5150 }
5151 sess.resize_surfaces_to_mediated_sizes(
5152 std::iter::once(surface_id),
5153 &state.config.surface_encoders,
5154 );
5155 }
5156 C2S_SURFACE_FOCUS if data.len() >= 3 => {
5157 let surface_id = u16::from_le_bytes([data[1], data[2]]);
5158 if state.config.verbose {
5159 eprintln!("C2S_SURFACE_FOCUS: cid={client_id} sid={surface_id}");
5160 }
5161 if let Some(cs) = sess.compositor.as_ref() {
5162 let _ = cs
5163 .handle
5164 .command_tx
5165 .send(CompositorCommand::SurfaceFocus { surface_id });
5166 }
5167 }
5168 C2S_SURFACE_CLOSE if data.len() >= 3 => {
5169 let surface_id = u16::from_le_bytes([data[1], data[2]]);
5170 if let Some(cs) = sess.compositor.as_ref() {
5171 let _ = cs
5172 .handle
5173 .command_tx
5174 .send(CompositorCommand::SurfaceClose { surface_id });
5175 cs.handle.wake();
5176 }
5177 }
5178 C2S_SURFACE_SUBSCRIBE if data.len() >= 3 => {
5179 let surface_id = u16::from_le_bytes([data[1], data[2]]);
5180 let codec_support = if data.len() >= 4 { data[3] } else { 0 };
5182 let quality_wire = if data.len() >= 5 { data[4] } else { 0 };
5183 if state.config.verbose {
5184 eprintln!(
5185 "C2S_SURFACE_SUBSCRIBE: cid={client_id} surface={surface_id} codec={codec_support:#04x} quality={quality_wire}"
5186 );
5187 }
5188 let mut destroy_vulkan_enc_sid = None;
5189 let mut first_subscribe = false;
5190 if let Some(c) = sess.clients.get_mut(&client_id) {
5191 let was_subscribed = !c.surface_subscriptions.insert(surface_id);
5192 let new_quality = SurfaceQuality::from_wire(quality_wire);
5193
5194 let state = c.surface_subs.entry(surface_id).or_default();
5195 let old_codec = state.codec_override;
5196 let old_quality = state.quality_override;
5197
5198 let meaningful_change =
5204 !was_subscribed || codec_support != old_codec || new_quality != old_quality;
5205 state.codec_override = codec_support;
5206 state.quality_override = new_quality;
5207 let task_in_flight = state.encode_in_flight || state.creation_in_flight;
5208 if meaningful_change {
5209 state.burst_remaining = SURFACE_BURST_FRAMES;
5215 state.nal_none_streak = 0;
5216 state.nal_none_latched_at = None;
5217 }
5218 if was_subscribed && (codec_support != old_codec || new_quality != old_quality)
5223 {
5224 state.encoder = None;
5225 if task_in_flight {
5226 state.encoder_invalidated = true;
5227 }
5228 }
5229 if meaningful_change {
5230 c.surface_needs_keyframe = true;
5231 }
5232 first_subscribe = !was_subscribed;
5233 if was_subscribed
5234 && (codec_support != old_codec || new_quality != old_quality)
5235 && c.vulkan_video_surfaces.remove(&surface_id).is_some()
5236 {
5237 destroy_vulkan_enc_sid = Some(surface_id);
5238 }
5239 }
5240 if let Some(sid) = destroy_vulkan_enc_sid
5241 && let Some(cs) = sess.compositor.as_ref()
5242 {
5243 let _ = cs.handle.command_tx.send(
5244 blit_compositor::CompositorCommand::DestroyVulkanEncoder {
5245 surface_id: sid as u32,
5246 },
5247 );
5248 cs.handle.wake();
5249 }
5250 if first_subscribe {
5251 sess.resize_surfaces_to_mediated_sizes(
5252 std::iter::once(surface_id),
5253 &state.config.surface_encoders,
5254 );
5255 }
5256 state.delivery_notify.notify_one();
5257 }
5258 C2S_SURFACE_UNSUBSCRIBE if data.len() >= 3 => {
5259 let surface_id = u16::from_le_bytes([data[1], data[2]]);
5260 let mut removed_vulkan = false;
5261 if let Some(c) = sess.clients.get_mut(&client_id) {
5262 c.surface_subscriptions.remove(&surface_id);
5263 c.surface_subs.remove(&surface_id);
5264 removed_vulkan = c.vulkan_video_surfaces.remove(&surface_id).is_some();
5265 c.surface_view_sizes.remove(&surface_id);
5266 }
5267 if removed_vulkan {
5269 let still_needed = sess
5270 .clients
5271 .values()
5272 .any(|other| other.vulkan_video_surfaces.contains_key(&surface_id));
5273 if !still_needed && let Some(cs) = sess.compositor.as_ref() {
5274 let _ = cs.handle.command_tx.send(
5275 blit_compositor::CompositorCommand::DestroyVulkanEncoder {
5276 surface_id: surface_id as u32,
5277 },
5278 );
5279 cs.handle.wake();
5280 }
5281 }
5282 sess.resize_surfaces_to_mediated_sizes(
5283 std::iter::once(surface_id),
5284 &state.config.surface_encoders,
5285 );
5286 }
5287 #[cfg(target_os = "linux")]
5288 C2S_AUDIO_SUBSCRIBE if data.len() >= 3 => {
5289 let bitrate_kbps = u16::from_le_bytes([data[1], data[2]]);
5290 if let Some(c) = sess.clients.get_mut(&client_id) {
5291 c.audio_subscribed = true;
5292 c.audio_bitrate_kbps = bitrate_kbps;
5293 if state.config.verbose {
5294 eprintln!(
5295 "C2S_AUDIO_SUBSCRIBE: cid={client_id} bitrate_kbps={bitrate_kbps}"
5296 );
5297 }
5298 if let Some(cs) = sess.compositor.as_mut()
5300 && let Some(ref ap) = cs.audio_pipeline
5301 {
5302 let msgs: Vec<_> = ap.ring_frames().map(audio::msg_audio_frame).collect();
5303 if let Some(c) = sess.clients.get(&client_id) {
5304 for msg in msgs {
5305 let _ = c.audio_tx.send(msg);
5306 }
5307 }
5308 }
5309 }
5310 if let Some(cs) = sess.compositor.as_ref()
5313 && let Some(ref ap) = cs.audio_pipeline
5314 {
5315 let max_kbps = sess
5316 .clients
5317 .values()
5318 .filter(|c| c.audio_subscribed)
5319 .map(|c| c.audio_bitrate_kbps)
5320 .max()
5321 .unwrap_or(0);
5322 let bitrate = if max_kbps > 0 {
5323 max_kbps as i32 * 1000
5324 } else {
5325 audio::DEFAULT_BITRATE
5326 };
5327 ap.set_bitrate(bitrate);
5328 }
5329 state.delivery_notify.notify_one();
5330 }
5331 #[cfg(target_os = "linux")]
5332 C2S_AUDIO_UNSUBSCRIBE if !data.is_empty() => {
5333 if let Some(c) = sess.clients.get_mut(&client_id) {
5334 c.audio_subscribed = false;
5335 c.audio_bitrate_kbps = 0;
5336 if state.config.verbose {
5337 eprintln!("C2S_AUDIO_UNSUBSCRIBE: cid={client_id}");
5338 }
5339 }
5340 if let Some(cs) = sess.compositor.as_ref()
5342 && let Some(ref ap) = cs.audio_pipeline
5343 {
5344 let max_kbps = sess
5345 .clients
5346 .values()
5347 .filter(|c| c.audio_subscribed)
5348 .map(|c| c.audio_bitrate_kbps)
5349 .max()
5350 .unwrap_or(0);
5351 let bitrate = if max_kbps > 0 {
5352 max_kbps as i32 * 1000
5353 } else {
5354 audio::DEFAULT_BITRATE
5355 };
5356 ap.set_bitrate(bitrate);
5357 }
5358 }
5359 C2S_SURFACE_ACK if data.len() >= 3 => {
5360 if let Some(c) = sess.clients.get_mut(&client_id) {
5364 c.acks_recv += 1;
5365 record_surface_ack(c);
5366 }
5367 state.delivery_notify.notify_one();
5368 }
5369 C2S_CLIENT_FEATURES if data.len() >= 2 => {
5370 let codec_support = data[1];
5373 if let Some(c) = sess.clients.get_mut(&client_id) {
5374 c.surface_codec_support = codec_support;
5375 }
5376 }
5377 C2S_CLIPBOARD_SET if data.len() >= 5 => {
5378 let mime_len = u16::from_le_bytes([data[1], data[2]]) as usize;
5379 if data.len() >= 3 + mime_len + 4 {
5380 let mime = std::str::from_utf8(&data[3..3 + mime_len])
5381 .unwrap_or("text/plain")
5382 .to_string();
5383 let data_len = u32::from_le_bytes([
5384 data[3 + mime_len],
5385 data[4 + mime_len],
5386 data[5 + mime_len],
5387 data[6 + mime_len],
5388 ]) as usize;
5389 let payload_start = 7 + mime_len;
5390 if data.len() >= payload_start + data_len {
5391 let payload = data[payload_start..payload_start + data_len].to_vec();
5392 if let Some(cs) = sess.compositor.as_ref() {
5393 let _ = cs
5394 .handle
5395 .command_tx
5396 .send(CompositorCommand::ClipboardOffer {
5397 mime_type: mime,
5398 data: payload,
5399 });
5400 }
5401 }
5402 }
5403 }
5404 C2S_CLIPBOARD_LIST if !data.is_empty() => {
5405 if let Some(cs) = sess.compositor.as_ref() {
5406 let command_tx = cs.handle.command_tx.clone();
5407 let client_tx = sess.clients.get(&client_id).map(|c| {
5408 (
5409 c.tx.clone(),
5410 c.outbox_queued_frames.clone(),
5411 c.outbox_queued_bytes.clone(),
5412 )
5413 });
5414 if let Some((client_tx, queued_frames, queued_bytes)) = client_tx {
5415 tokio::task::spawn_blocking(move || {
5416 let (tx, rx) = std::sync::mpsc::sync_channel(1);
5417 if command_tx
5418 .send(CompositorCommand::ClipboardListMimes { reply: tx })
5419 .is_ok()
5420 && let Ok(mimes) = rx.recv_timeout(Duration::from_secs(2))
5421 {
5422 let _ = send_outbox_tracked(
5423 &client_tx,
5424 &queued_frames,
5425 &queued_bytes,
5426 msg_s2c_clipboard_list(&mimes),
5427 );
5428 }
5429 });
5430 }
5431 } else {
5432 if let Some(c) = sess.clients.get(&client_id) {
5434 let _ = send_outbox(c, msg_s2c_clipboard_list(&[]));
5435 }
5436 }
5437 }
5438 C2S_CLIPBOARD_GET if data.len() >= 3 => {
5439 let mime_len = u16::from_le_bytes([data[1], data[2]]) as usize;
5440 if data.len() >= 3 + mime_len {
5441 let mime = std::str::from_utf8(&data[3..3 + mime_len])
5442 .unwrap_or("text/plain")
5443 .to_string();
5444 if let Some(cs) = sess.compositor.as_ref() {
5445 let command_tx = cs.handle.command_tx.clone();
5446 let client_tx = sess.clients.get(&client_id).map(|c| {
5447 (
5448 c.tx.clone(),
5449 c.outbox_queued_frames.clone(),
5450 c.outbox_queued_bytes.clone(),
5451 )
5452 });
5453 if let Some((client_tx, queued_frames, queued_bytes)) = client_tx {
5454 tokio::task::spawn_blocking(move || {
5455 let (tx, rx) = std::sync::mpsc::sync_channel(1);
5456 if command_tx
5457 .send(CompositorCommand::ClipboardGet {
5458 mime_type: mime.clone(),
5459 reply: tx,
5460 })
5461 .is_ok()
5462 && let Ok(content) = rx.recv_timeout(Duration::from_secs(2))
5463 {
5464 let data = content.unwrap_or_default();
5465 let _ = send_outbox_tracked(
5466 &client_tx,
5467 &queued_frames,
5468 &queued_bytes,
5469 msg_s2c_clipboard_content(&mime, &data),
5470 );
5471 }
5472 });
5473 }
5474 } else {
5475 if let Some(c) = sess.clients.get(&client_id) {
5477 let _ = send_outbox(c, msg_s2c_clipboard_content(&mime, &[]));
5478 }
5479 }
5480 }
5481 }
5482 C2S_SURFACE_LIST if !data.is_empty() => {
5483 let msg = sess.surface_list_msg();
5484 if let Some(c) = sess.clients.get(&client_id) {
5485 let _ = send_outbox(c, msg);
5486 }
5487 }
5488 C2S_FOCUS if data.len() >= 3 => {
5489 let pid = u16::from_le_bytes([data[1], data[2]]);
5490 if sess.ptys.contains_key(&pid) {
5491 let old_pid = sess.clients.get(&client_id).and_then(|c| c.lead);
5492 if let Some(c) = sess.clients.get_mut(&client_id) {
5493 c.lead = Some(pid);
5494 subscribe_client_to(c, pid);
5495 if old_pid == Some(pid) {
5496 update_client_scroll_state(c, pid, 0);
5497 } else {
5498 reset_inflight(c);
5499 }
5500 }
5501 if let Some(pty) = sess.ptys.get_mut(&pid) {
5502 pty.mark_dirty();
5503 need_nudge = true;
5504 }
5505 }
5506 }
5507 C2S_SUBSCRIBE if data.len() >= 3 => {
5508 let pid = u16::from_le_bytes([data[1], data[2]]);
5509 if sess.ptys.contains_key(&pid) {
5510 if let Some(c) = sess.clients.get_mut(&client_id) {
5511 subscribe_client_to(c, pid);
5512 }
5513 if let Some(pty) = sess.ptys.get_mut(&pid) {
5514 pty.mark_dirty();
5515 }
5516 need_nudge = true;
5517 }
5518 }
5519 C2S_UNSUBSCRIBE if data.len() >= 3 => {
5520 let pid = u16::from_le_bytes([data[1], data[2]]);
5521 if sess.ptys.contains_key(&pid) {
5522 let mut touched = Vec::new();
5523 if let Some(c) = sess.clients.get_mut(&client_id) {
5524 if unsubscribe_client_from(c, pid) {
5525 touched.push(pid);
5526 }
5527 reset_inflight(c);
5528 }
5529 if sess.resize_ptys_to_mediated_sizes(touched) {
5530 need_nudge = true;
5531 }
5532 }
5533 }
5534 C2S_RESTART if data.len() >= 3 => {
5535 let pid = u16::from_le_bytes([data[1], data[2]]);
5536 let restart_info = sess
5537 .ptys
5538 .get(&pid)
5539 .filter(|p| p.exited)
5540 .map(|p| (p.driver.size(), p.command.clone(), p.tag.clone()));
5541 if let Some(((rows, cols), command, tag)) = restart_info {
5542 let wayland_display = sess
5543 .compositor
5544 .as_ref()
5545 .map(|cs| cs.handle.socket_name.clone());
5546 #[cfg(target_os = "linux")]
5547 let pulse_server = sess.pulse_server_path();
5548 #[cfg(not(target_os = "linux"))]
5549 let pulse_server: Option<String> = None;
5550 #[cfg(target_os = "linux")]
5551 let pipewire_remote = sess.pipewire_remote_path();
5552 #[cfg(not(target_os = "linux"))]
5553 let pipewire_remote: Option<String> = None;
5554 if let Some((new_handle, reader, byte_rx)) = pty::respawn_child(
5555 &state.config.shell,
5556 &state.config.shell_flags,
5557 rows,
5558 cols,
5559 pid,
5560 command.as_deref(),
5561 state.clone(),
5562 wayland_display.as_deref(),
5563 pulse_server.as_deref(),
5564 pipewire_remote.as_deref(),
5565 ) {
5566 let Some(pty) = sess.ptys.get_mut(&pid) else {
5567 break;
5568 };
5569 pty.handle = new_handle;
5570 pty.reader_handle = reader;
5571 pty.byte_rx = byte_rx;
5572 pty.driver.reset_modes();
5573 pty.exited = false;
5574 pty.exit_status = blit_remote::EXIT_STATUS_UNKNOWN;
5575 pty.lflag_cache = pty::pty_lflag(&pty.handle);
5576 pty.lflag_last = Instant::now();
5577 pty.mark_dirty();
5578 if let Some(c) = sess.clients.get_mut(&client_id) {
5579 c.lead = Some(pid);
5580 subscribe_client_to(c, pid);
5581 update_client_scroll_state(c, pid, 0);
5582 reset_inflight(c);
5583 }
5584 let mut msg = Vec::with_capacity(3 + tag.len());
5585 msg.push(S2C_CREATED);
5586 msg.extend_from_slice(&pid.to_le_bytes());
5587 msg.extend_from_slice(tag.as_bytes());
5588 sess.send_to_all(&msg);
5589 need_nudge = true;
5590 }
5591 }
5592 }
5593 C2S_READ if data.len() >= 13 => {
5594 let nonce = u16::from_le_bytes([data[1], data[2]]);
5595 let pid = u16::from_le_bytes([data[3], data[4]]);
5596 let req_offset = u32::from_le_bytes([data[5], data[6], data[7], data[8]]) as usize;
5597 let req_limit =
5598 u32::from_le_bytes([data[9], data[10], data[11], data[12]]) as usize;
5599 let flags = data.get(13).copied().unwrap_or(0);
5600 let ansi = flags & READ_ANSI != 0;
5601 let tail = flags & READ_TAIL != 0;
5602
5603 if let Some(pty) = sess.ptys.get_mut(&pid) {
5604 let (rows, _cols) = pty.driver.size();
5605 let viewport = take_snapshot(pty);
5606 let scrollback_lines = viewport.scrollback_lines() as usize;
5607 let total_lines = scrollback_lines + rows as usize;
5608
5609 let extract = |f: &FrameState| -> String {
5610 if ansi {
5611 f.get_ansi_text()
5612 } else {
5613 f.get_all_text()
5614 }
5615 };
5616
5617 let mut all_lines: Vec<String> =
5618 Vec::with_capacity(scrollback_lines + rows as usize);
5619
5620 let mut scroll_offset = scrollback_lines;
5621 while scroll_offset > 0 {
5622 let frame = pty.driver.scrollback_frame(scroll_offset);
5623 let page = extract(&frame);
5624 let page_lines: Vec<&str> = page.lines().collect();
5625 let take = if scroll_offset < rows as usize {
5626 scroll_offset.min(page_lines.len())
5627 } else {
5628 page_lines.len()
5629 };
5630 for line in &page_lines[..take] {
5631 all_lines.push(line.to_string());
5632 }
5633 if scroll_offset <= rows as usize {
5634 break;
5635 }
5636 scroll_offset = scroll_offset.saturating_sub(rows as usize);
5637 }
5638
5639 for line in extract(&viewport).lines() {
5640 all_lines.push(line.to_string());
5641 }
5642
5643 let (start, end) = if tail {
5644 let end = all_lines.len().saturating_sub(req_offset);
5645 let start = if req_limit == 0 {
5646 0
5647 } else {
5648 end.saturating_sub(req_limit)
5649 };
5650 (start, end)
5651 } else {
5652 let start = req_offset.min(all_lines.len());
5653 let end = if req_limit == 0 {
5654 all_lines.len()
5655 } else {
5656 (start + req_limit).min(all_lines.len())
5657 };
5658 (start, end)
5659 };
5660 let text = all_lines[start..end].join("\n");
5661
5662 let mut msg = Vec::with_capacity(13 + text.len());
5663 msg.push(S2C_TEXT);
5664 msg.extend_from_slice(&nonce.to_le_bytes());
5665 msg.extend_from_slice(&pid.to_le_bytes());
5666 msg.extend_from_slice(&(total_lines as u32).to_le_bytes());
5667 msg.extend_from_slice(&(start as u32).to_le_bytes());
5668 msg.extend_from_slice(text.as_bytes());
5669 if let Some(client) = sess.clients.get(&client_id) {
5670 let _ = send_outbox(client, msg);
5671 }
5672 }
5673 }
5674 C2S_COPY_RANGE if data.len() >= 18 => {
5675 let nonce = u16::from_le_bytes([data[1], data[2]]);
5676 let pid = u16::from_le_bytes([data[3], data[4]]);
5677 let start_tail = u32::from_le_bytes([data[5], data[6], data[7], data[8]]);
5678 let start_col = u16::from_le_bytes([data[9], data[10]]);
5679 let end_tail = u32::from_le_bytes([data[11], data[12], data[13], data[14]]);
5680 let end_col = u16::from_le_bytes([data[15], data[16]]);
5681
5682 if let Some(pty) = sess.ptys.get(&pid) {
5683 let text = pty
5684 .driver
5685 .get_text_range(start_tail, start_col, end_tail, end_col);
5686 let total_lines = pty.driver.total_lines();
5687
5688 let mut msg = Vec::with_capacity(13 + text.len());
5689 msg.push(S2C_TEXT);
5690 msg.extend_from_slice(&nonce.to_le_bytes());
5691 msg.extend_from_slice(&pid.to_le_bytes());
5692 msg.extend_from_slice(&total_lines.to_le_bytes());
5693 msg.extend_from_slice(&start_tail.to_le_bytes());
5694 msg.extend_from_slice(text.as_bytes());
5695 if let Some(client) = sess.clients.get(&client_id) {
5696 let _ = send_outbox(client, msg);
5697 }
5698 }
5699 }
5700 C2S_KILL if data.len() >= 7 => {
5701 let pid = u16::from_le_bytes([data[1], data[2]]);
5702 let signal = i32::from_le_bytes([data[3], data[4], data[5], data[6]]);
5703 if let Some(pty) = sess.ptys.get(&pid)
5704 && !pty.exited
5705 {
5706 pty::kill_pty(&pty.handle, signal);
5707 }
5708 }
5709 C2S_CLOSE if data.len() >= 3 => {
5710 let pid = u16::from_le_bytes([data[1], data[2]]);
5711 if let Some(pty) = sess.ptys.remove(&pid) {
5712 if !pty.exited {
5713 state.pty_fds.write().unwrap().remove(&pid);
5714 drop(pty.reader_handle);
5715 pty::close_pty(&pty.handle);
5716 }
5717 for client in sess.clients.values_mut() {
5718 unsubscribe_client_from(client, pid);
5719 }
5720 let mut msg = vec![S2C_CLOSED];
5721 msg.extend_from_slice(&pid.to_le_bytes());
5722 sess.send_to_all(&msg);
5723 }
5724 }
5725 _ => {}
5726 }
5727 drop(sess);
5728 if need_nudge {
5729 nudge_delivery(&state);
5730 }
5731 }
5732
5733 {
5734 let mut sess = state.session.lock().await;
5735 let mut need_nudge = false;
5736 let client = sess.clients.remove(&client_id);
5737 let affected_ptys = client
5738 .as_ref()
5739 .map(|c| c.view_sizes.keys().copied().collect::<Vec<_>>())
5740 .unwrap_or_default();
5741 let affected_surfaces = client
5742 .as_ref()
5743 .map(|c| c.surface_view_sizes.keys().copied().collect::<Vec<_>>())
5744 .unwrap_or_default();
5745 if sess.resize_ptys_to_mediated_sizes(affected_ptys) {
5746 need_nudge = true;
5747 }
5748 sess.resize_surfaces_to_mediated_sizes(affected_surfaces, &state.config.surface_encoders);
5749 if let Some(ref client) = client
5753 && !client.pressed_surface_keys.is_empty()
5754 && let Some(cs) = sess.compositor.as_mut()
5755 {
5756 let keycodes: Vec<u32> = client.pressed_surface_keys.iter().copied().collect();
5757 let _ = cs
5758 .handle
5759 .command_tx
5760 .send(CompositorCommand::ReleaseKeys { keycodes });
5761 cs.handle.wake();
5762 }
5763 if let Some(ref client) = client
5766 && !client.vulkan_video_surfaces.is_empty()
5767 && let Some(cs) = sess.compositor.as_ref()
5768 {
5769 for &sid in client.vulkan_video_surfaces.keys() {
5770 let still_needed = sess
5771 .clients
5772 .values()
5773 .any(|c| c.vulkan_video_surfaces.contains_key(&sid));
5774 if !still_needed {
5775 let _ = cs
5776 .handle
5777 .command_tx
5778 .send(CompositorCommand::DestroyVulkanEncoder {
5779 surface_id: sid as u32,
5780 });
5781 }
5782 }
5783 cs.handle.wake();
5784 }
5785 drop(sess);
5786 if need_nudge {
5787 nudge_delivery(&state);
5788 }
5789 }
5790 sender.abort();
5791 if state.config.verbose {
5792 eprintln!("client disconnected");
5793 }
5794}
5795
5796#[cfg(test)]
5797mod tests {
5798 use super::*;
5799
5800 fn test_client_with_capacity(
5801 _capacity: usize,
5802 ) -> (ClientState, mpsc::UnboundedReceiver<Vec<u8>>) {
5803 let (tx, rx) = mpsc::unbounded_channel();
5804 let (audio_tx, _audio_rx) = mpsc::unbounded_channel();
5805 let client = ClientState {
5806 tx,
5807 outbox_queued_frames: Arc::new(AtomicUsize::new(0)),
5808 outbox_queued_bytes: Arc::new(AtomicUsize::new(0)),
5809 audio_tx,
5810 lead: None,
5811 subscriptions: HashSet::new(),
5812 surface_subscriptions: HashSet::new(),
5813 audio_subscribed: false,
5814 #[cfg(target_os = "linux")]
5815 audio_bitrate_kbps: 0,
5816 view_sizes: HashMap::new(),
5817 scroll_offsets: HashMap::new(),
5818 scroll_caches: HashMap::new(),
5819 last_sent: HashMap::new(),
5820 preview_next_send_at: HashMap::new(),
5821 rtt_ms: 50.0,
5822 min_rtt_ms: 50.0,
5823 display_fps: 60.0,
5824 delivery_bps: 262_144.0,
5825 goodput_bps: 262_144.0,
5826 goodput_jitter_bps: 0.0,
5827 max_goodput_jitter_bps: 0.0,
5828 last_goodput_sample_bps: 0.0,
5829 avg_frame_bytes: 1_024.0,
5830 avg_paced_frame_bytes: 1_024.0,
5831 avg_preview_frame_bytes: 1_024.0,
5832 avg_surface_frame_bytes: 8_192.0,
5833 inflight_bytes: 0,
5834 inflight_frames: VecDeque::new(),
5835 next_send_at: Instant::now(),
5836 probe_frames: 0.0,
5837 frames_sent: 0,
5838 acks_recv: 0,
5839 acked_bytes_since_log: 0,
5840 browser_backlog_frames: 0,
5841 browser_ack_ahead_frames: 0,
5842 browser_apply_ms: 0.0,
5843 last_metrics_update: Instant::now(),
5844 last_log: Instant::now(),
5845 last_window_blocked_log: Instant::now(),
5846 last_skip_log: Instant::now(),
5847 skip_same_gen_count: 0,
5848 skip_in_flight_count: 0,
5849 skip_pacing_count: 0,
5850 skip_vulkan_await_count: 0,
5851 skip_no_subs_count: 0,
5852 skip_not_subbed_count: 0,
5853 skip_last_pixels_mismatch_count: 0,
5854 encode_loop_iters: 0,
5855 goodput_window_bytes: 0,
5856 goodput_window_start: Instant::now(),
5857 surface_subs: HashMap::new(),
5858 surface_needs_keyframe: true,
5859 surface_inflight_frames: VecDeque::new(),
5860 vulkan_video_surfaces: HashMap::new(),
5861 surface_view_sizes: HashMap::new(),
5862 surface_codec_support: 0,
5863 pressed_surface_keys: HashSet::new(),
5864 };
5865 (client, rx)
5866 }
5867
5868 fn test_client() -> ClientState {
5869 let (client, _rx) = test_client_with_capacity(0);
5870 client
5871 }
5872
5873 fn fill_inflight(client: &mut ClientState, frames: usize, bytes_per_frame: usize) {
5874 let now = Instant::now();
5875 client.inflight_bytes = frames.saturating_mul(bytes_per_frame);
5876 client.inflight_frames = (0..frames)
5877 .map(|_| InFlightFrame {
5878 sent_at: now,
5879 bytes: bytes_per_frame,
5880 paced: true,
5881 })
5882 .collect();
5883 }
5884
5885 fn sample_frame(text: &str) -> FrameState {
5886 let mut frame = FrameState::new(2, 8);
5887 frame.write_text(0, 0, text, blit_remote::CellStyle::default());
5888 frame
5889 }
5890
5891 #[test]
5892 fn unset_view_size_accepts_zero_pair_only() {
5893 assert!(is_unset_view_size(0, 0));
5894 assert!(!is_unset_view_size(0, 80));
5895 assert!(!is_unset_view_size(u16::MAX, u16::MAX));
5896 }
5897
5898 #[test]
5899 fn unsubscribe_client_from_clears_view_size() {
5900 let mut client = test_client();
5901 client.subscriptions.insert(7);
5902 client.view_sizes.insert(7, (24, 80));
5903 assert!(unsubscribe_client_from(&mut client, 7));
5904 assert!(!client.subscriptions.contains(&7));
5905 assert!(!client.view_sizes.contains_key(&7));
5906 }
5907
5908 #[test]
5909 fn mediated_size_uses_per_pty_view_sizes_without_lead() {
5910 let mut session = Session::new();
5911 let mut c1 = test_client();
5912 let mut c2 = test_client();
5913 c1.view_sizes.insert(7, (30, 120));
5914 c2.view_sizes.insert(7, (24, 100));
5915 session.clients.insert(1, c1);
5916 session.clients.insert(2, c2);
5917 assert_eq!(session.mediated_size_for_pty(7), Some((24, 100)));
5918 }
5919
5920 #[test]
5921 fn mediated_surface_size_picks_min_dimensions_max_scale() {
5922 let mut session = Session::new();
5923 let mut c1 = test_client();
5924 let mut c2 = test_client();
5925 c1.surface_view_sizes.insert(1, (1920, 1080, 240)); c2.surface_view_sizes.insert(1, (1280, 720, 120)); session.clients.insert(1, c1);
5928 session.clients.insert(2, c2);
5929 assert_eq!(
5930 session.mediated_size_for_surface(1, None),
5931 Some((1280, 720, 240))
5932 );
5933 }
5934
5935 #[test]
5936 fn mediated_surface_size_none_when_no_clients() {
5937 let session = Session::new();
5938 assert_eq!(session.mediated_size_for_surface(1, None), None);
5939 }
5940
5941 #[test]
5942 fn mediated_surface_size_single_client() {
5943 let mut session = Session::new();
5944 let mut c1 = test_client();
5945 c1.surface_view_sizes.insert(3, (800, 600, 120));
5946 session.clients.insert(1, c1);
5947 assert_eq!(
5948 session.mediated_size_for_surface(3, None),
5949 Some((800, 600, 120))
5950 );
5951 }
5952
5953 #[test]
5954 fn mediated_surface_size_ignores_other_surfaces() {
5955 let mut session = Session::new();
5956 let mut c1 = test_client();
5957 c1.surface_view_sizes.insert(1, (1920, 1080, 240));
5958 c1.surface_view_sizes.insert(2, (640, 480, 120));
5959 session.clients.insert(1, c1);
5960 assert_eq!(
5961 session.mediated_size_for_surface(1, None),
5962 Some((1920, 1080, 240))
5963 );
5964 assert_eq!(
5965 session.mediated_size_for_surface(2, None),
5966 Some((640, 480, 120))
5967 );
5968 assert_eq!(session.mediated_size_for_surface(3, None), None);
5969 }
5970
5971 #[test]
5972 fn mediated_surface_size_clamped_to_encoder_max() {
5973 let mut session = Session::new();
5974 let mut c1 = test_client();
5975 c1.surface_view_sizes.insert(1, (5000, 3000, 240));
5976 session.clients.insert(1, c1);
5977 assert_eq!(
5978 session.mediated_size_for_surface(1, None),
5979 Some((5000, 3000, 240))
5980 );
5981 assert_eq!(
5982 session.mediated_size_for_surface(1, Some((3840, 2160))),
5983 Some((3840, 2160, 240))
5984 );
5985 }
5986
5987 #[test]
5988 fn mediated_surface_size_picks_min_across_clients() {
5989 let mut session = Session::new();
5990 let mut c1 = test_client();
5991 let mut c2 = test_client();
5992 c1.surface_view_sizes.insert(1, (1920, 1080, 120));
5993 c2.surface_view_sizes.insert(1, (640, 360, 120));
5994 c1.surface_subscriptions.insert(1);
5995 c2.surface_subscriptions.insert(1);
5996 session.clients.insert(1, c1);
5997 session.clients.insert(2, c2);
5998 assert_eq!(
5999 session.mediated_size_for_surface(1, None),
6000 Some((640, 360, 120))
6001 );
6002 }
6003
6004 #[test]
6005 fn due_preview_reserves_the_last_lead_slot() {
6006 let mut client = test_client();
6007 client.lead = Some(1);
6008 client.subscriptions.insert(1);
6009 client.subscriptions.insert(2);
6010
6011 let target_frames = target_frame_window(&client);
6012 let lead_limit = target_frames.saturating_sub(1).max(1);
6013 fill_inflight(&mut client, lead_limit, 512);
6014
6015 assert!(window_open(&client));
6016 assert!(lead_window_open(&client, false));
6017 assert!(!lead_window_open(&client, true));
6018 assert!(can_send_preview(&client, 2, Instant::now()));
6019 }
6020
6021 #[test]
6022 fn entering_scrollback_uses_current_visible_frame_as_baseline() {
6023 let mut client = test_client();
6024 let live = sample_frame("live");
6025 client.lead = Some(7);
6026 client.subscriptions.insert(7);
6027 client.last_sent.insert(7, live.clone());
6028
6029 assert!(update_client_scroll_state(&mut client, 7, 12));
6030 assert_eq!(client.scroll_offsets.get(&7), Some(&12));
6031 assert_eq!(client.scroll_caches.get(&7), Some(&live));
6032 }
6033
6034 #[test]
6035 fn leaving_scrollback_seeds_live_diff_from_scrollback_view() {
6036 let mut client = test_client();
6037 let history = sample_frame("hist");
6038 client.lead = Some(7);
6039 client.subscriptions.insert(7);
6040 client.scroll_offsets.insert(7, 12);
6041 client.scroll_caches.insert(7, history.clone());
6042
6043 assert!(update_client_scroll_state(&mut client, 7, 0));
6044 assert_eq!(client.scroll_offsets.get(&7), None);
6045 assert_eq!(client.last_sent.get(&7), Some(&history));
6046 assert_eq!(client.scroll_caches.get(&7), None);
6047 }
6048
6049 #[tokio::test]
6050 async fn request_surface_capture_returns_pixels_from_compositor() {
6051 let (command_tx, command_rx) = std::sync::mpsc::channel();
6052 std::thread::Builder::new()
6053 .name("test-capture-reply".into())
6054 .spawn(move || {
6055 let CompositorCommand::Capture {
6056 surface_id,
6057 scale_120: _,
6058 reply,
6059 } = command_rx.recv().unwrap()
6060 else {
6061 panic!("expected capture command");
6062 };
6063 assert_eq!(surface_id, 7);
6064 let _ = reply.send(Some((2, 3, vec![1, 2, 3, 4])));
6065 })
6066 .unwrap();
6067
6068 let result =
6069 request_surface_capture_with_timeout(command_tx, 7, 0, Duration::from_millis(50)).await;
6070
6071 assert_eq!(result, Some((2, 3, vec![1, 2, 3, 4])));
6072 }
6073
6074 #[tokio::test]
6075 async fn request_surface_capture_returns_none_when_compositor_disconnects() {
6076 let (command_tx, command_rx) = std::sync::mpsc::channel();
6077 std::thread::Builder::new()
6078 .name("test-capture-drop".into())
6079 .spawn(move || {
6080 let _ = command_rx.recv().unwrap();
6081 })
6082 .unwrap();
6083
6084 let result =
6085 request_surface_capture_with_timeout(command_tx, 7, 0, Duration::from_millis(50)).await;
6086
6087 assert_eq!(result, None);
6088 }
6089
6090 #[test]
6093 fn frame_window_minimum_is_two() {
6094 assert!(frame_window(0.0, 60.0) >= 2);
6095 }
6096
6097 #[test]
6098 fn frame_window_scales_with_rtt() {
6099 let low = frame_window(10.0, 60.0);
6100 let high = frame_window(200.0, 60.0);
6101 assert!(high > low, "higher RTT should need more frames in flight");
6102 }
6103
6104 #[test]
6105 fn frame_window_scales_with_fps() {
6106 let slow = frame_window(100.0, 10.0);
6107 let fast = frame_window(100.0, 120.0);
6108 assert!(fast > slow, "higher fps should need more frames in flight");
6109 }
6110
6111 #[test]
6112 fn frame_window_zero_rtt() {
6113 assert!(frame_window(0.0, 120.0) >= 2);
6114 }
6115
6116 #[test]
6119 fn path_rtt_ms_uses_min_when_positive() {
6120 let mut client = test_client();
6121 client.rtt_ms = 100.0;
6122 client.min_rtt_ms = 30.0;
6123 assert_eq!(path_rtt_ms(&client), 30.0);
6124 }
6125
6126 #[test]
6127 fn path_rtt_ms_falls_back_to_rtt_when_min_zero() {
6128 let mut client = test_client();
6129 client.rtt_ms = 80.0;
6130 client.min_rtt_ms = 0.0;
6131 assert_eq!(path_rtt_ms(&client), 80.0);
6132 }
6133
6134 #[test]
6137 fn ewma_rising_uses_rise_alpha() {
6138 let result = ewma_with_direction(100.0, 200.0, 0.5, 0.1);
6139 assert!((result - 150.0).abs() < 0.01);
6141 }
6142
6143 #[test]
6144 fn ewma_falling_uses_fall_alpha() {
6145 let result = ewma_with_direction(200.0, 100.0, 0.5, 0.1);
6146 assert!((result - 190.0).abs() < 0.01);
6148 }
6149
6150 #[test]
6151 fn ewma_same_value_unchanged() {
6152 let result = ewma_with_direction(50.0, 50.0, 0.5, 0.5);
6153 assert!((result - 50.0).abs() < 0.01);
6154 }
6155
6156 #[test]
6159 fn advance_deadline_steps_forward() {
6160 let now = Instant::now();
6161 let mut deadline = now;
6162 let interval = Duration::from_millis(16);
6163 advance_deadline(&mut deadline, now, interval);
6164 assert!(deadline > now);
6165 assert!(deadline <= now + interval + Duration::from_micros(100));
6166 }
6167
6168 #[test]
6169 fn advance_deadline_resets_when_far_behind() {
6170 let now = Instant::now();
6171 let mut deadline = now - Duration::from_secs(10);
6173 let interval = Duration::from_millis(16);
6174 advance_deadline(&mut deadline, now, interval);
6175 assert!(deadline >= now);
6177 }
6178
6179 #[test]
6180 fn should_snapshot_pty_requires_dirty_and_needful() {
6181 assert!(should_snapshot_pty(true, true, false));
6182 assert!(!should_snapshot_pty(false, true, false));
6183 assert!(!should_snapshot_pty(true, false, false));
6184 }
6185
6186 #[test]
6187 fn should_snapshot_pty_defers_synced_output() {
6188 assert!(!should_snapshot_pty(true, true, true));
6189 assert!(should_snapshot_pty(true, true, false));
6190 }
6191
6192 #[test]
6193 fn enqueue_ready_frame_refuses_new_frames_when_capped() {
6194 let mut queue = VecDeque::new();
6195 for cols in 1..=(READY_FRAME_QUEUE_CAP as u16) {
6196 assert!(enqueue_ready_frame(&mut queue, FrameState::new(1, cols)));
6197 }
6198 assert!(!enqueue_ready_frame(
6199 &mut queue,
6200 FrameState::new(1, READY_FRAME_QUEUE_CAP as u16 + 1),
6201 ));
6202 assert_eq!(queue.len(), READY_FRAME_QUEUE_CAP);
6203 assert_eq!(queue.front().map(FrameState::cols), Some(1));
6204 assert_eq!(
6205 queue.back().map(FrameState::cols),
6206 Some(READY_FRAME_QUEUE_CAP as u16),
6207 );
6208 }
6209
6210 #[test]
6211 fn find_sync_output_end_returns_end_of_first_close_sequence() {
6212 let bytes = b"abc\x1b[?2026lrest\x1b[?2026l";
6213 assert_eq!(find_sync_output_end(&[], bytes), Some(11));
6214 }
6215
6216 #[test]
6217 fn find_sync_output_end_returns_none_without_close_sequence() {
6218 assert_eq!(find_sync_output_end(&[], b"\x1b[?2026hpartial"), None);
6219 }
6220
6221 #[test]
6222 fn find_sync_output_end_detects_boundary_split_across_reads() {
6223 assert_eq!(find_sync_output_end(b"abc\x1b[?20", b"26lrest"), Some(3));
6224 }
6225
6226 #[test]
6227 fn update_sync_scan_tail_keeps_recent_suffix_only() {
6228 let mut tail = Vec::new();
6229 update_sync_scan_tail(&mut tail, b"123456789");
6230 assert_eq!(tail, b"3456789");
6231 }
6232
6233 #[test]
6236 fn window_saturated_at_90_percent_frames() {
6237 let client = test_client();
6238 let target = target_frame_window(&client);
6239 let frames_90 = (target * 9).div_ceil(10); assert!(window_saturated(&client, frames_90, 0));
6241 }
6242
6243 #[test]
6244 fn window_saturated_not_at_low_usage() {
6245 let client = test_client();
6246 assert!(!window_saturated(&client, 1, 0));
6247 }
6248
6249 #[test]
6250 fn window_saturated_at_90_percent_bytes() {
6251 let client = test_client();
6252 let target_bytes = target_byte_window(&client);
6253 let bytes_90 = (target_bytes * 9).div_ceil(10);
6254 assert!(window_saturated(&client, 0, bytes_90));
6255 }
6256
6257 #[test]
6260 fn outbox_queued_frames_zero_when_empty() {
6261 let client = test_client();
6262 assert_eq!(outbox_queued_frames(&client), 0);
6263 }
6264
6265 #[test]
6266 fn outbox_backpressured_when_queue_full() {
6267 let (client, _rx) = test_client_with_capacity(0);
6268 for _ in 0..OUTBOX_SOFT_QUEUE_LIMIT_FRAMES {
6270 let _ = send_outbox(&client, vec![0u8]);
6271 }
6272 assert!(outbox_backpressured(&client));
6273 }
6274
6275 #[test]
6276 fn outbox_not_backpressured_by_small_frames_under_byte_budget() {
6277 let (client, _rx) = test_client_with_capacity(0);
6278 for _ in 0..(OUTBOX_SOFT_QUEUE_LIMIT_FRAMES - 1) {
6279 let _ = send_outbox(&client, vec![0u8; 512]);
6280 }
6281 assert!(!outbox_backpressured(&client));
6282 }
6283
6284 #[test]
6285 fn outbox_backpressured_by_large_queued_bytes() {
6286 let (client, _rx) = test_client_with_capacity(0);
6287 let _ = send_outbox(&client, vec![0u8; OUTBOX_SOFT_QUEUE_LIMIT_BYTES]);
6290 assert!(!outbox_backpressured(&client));
6291 let _ = send_outbox(&client, vec![0u8; 1]);
6293 assert!(outbox_backpressured(&client));
6294 }
6295
6296 #[test]
6297 fn outbox_not_backpressured_when_empty() {
6298 let client = test_client();
6299 assert!(!outbox_backpressured(&client));
6300 }
6301
6302 #[test]
6305 fn browser_pacing_fps_matches_display_fps_when_browser_ready() {
6306 let mut client = test_client();
6307 client.rtt_ms = 1.0;
6308 client.min_rtt_ms = 1.0;
6309 client.browser_backlog_frames = 0;
6310 client.browser_ack_ahead_frames = 0;
6311 client.browser_apply_ms = 0.0;
6312 client.goodput_bps = 1_000_000.0;
6313 client.delivery_bps = 1_000_000.0;
6314 client.display_fps = 144.0;
6315 assert!((browser_pacing_fps(&client) - 144.0).abs() < 0.01);
6316 }
6317
6318 #[test]
6319 fn browser_pacing_fps_drops_below_display_fps_when_backlogged() {
6320 let mut client = test_client();
6321 client.browser_backlog_frames = 20;
6322 let fps = browser_pacing_fps(&client);
6323 assert!(fps >= 1.0);
6324 assert!(fps < client.display_fps);
6325 }
6326
6327 #[test]
6330 fn effective_rtt_ms_equals_path_when_queue_is_empty() {
6331 let mut client = test_client();
6332 client.rtt_ms = 1.0;
6333 client.min_rtt_ms = 1.0;
6334 client.browser_backlog_frames = 0;
6335 client.browser_ack_ahead_frames = 0;
6336 client.browser_apply_ms = 0.0;
6337 client.goodput_bps = 1_000_000.0;
6338 client.delivery_bps = 1_000_000.0;
6339 assert!((effective_rtt_ms(&client) - 1.0).abs() < 0.01);
6340 }
6341
6342 #[test]
6343 fn effective_rtt_ms_at_least_path_rtt() {
6344 let client = test_client();
6345 assert!(effective_rtt_ms(&client) >= path_rtt_ms(&client));
6346 }
6347
6348 #[test]
6351 fn target_frame_window_at_least_two() {
6352 let client = test_client();
6353 assert!(target_frame_window(&client) >= 2);
6354 }
6355
6356 #[test]
6357 fn target_frame_window_grows_with_probe() {
6358 let mut client = test_client();
6359 let base = target_frame_window(&client);
6360 client.probe_frames = 10.0;
6361 let probed = target_frame_window(&client);
6362 assert!(probed > base, "probe_frames should grow the window");
6363 }
6364
6365 #[test]
6368 fn bandwidth_floor_bps_at_least_16k() {
6369 let mut client = test_client();
6370 client.goodput_bps = 0.0;
6371 client.delivery_bps = 0.0;
6372 assert_eq!(bandwidth_floor_bps(&client), 0.0);
6373 }
6374
6375 #[test]
6376 fn bandwidth_floor_bps_scales_with_goodput() {
6377 let mut client = test_client();
6378 client.goodput_bps = 1_000_000.0;
6379 client.delivery_bps = 1_000_000.0;
6380 let floor = bandwidth_floor_bps(&client);
6381 assert!(floor > 0.0);
6382 }
6383
6384 #[test]
6385 fn browser_ready_delivery_floor_can_drive_large_frames_to_display_fps() {
6386 let mut client = test_client();
6387 client.display_fps = 60.0;
6388 client.browser_backlog_frames = 0;
6389 client.browser_ack_ahead_frames = 0;
6390 client.browser_apply_ms = 0.2;
6391 client.goodput_bps = 3_000_000.0;
6392 client.delivery_bps = 9_500_000.0;
6393 client.last_goodput_sample_bps = 3_000_000.0;
6394 client.avg_paced_frame_bytes = 150_000.0;
6395 client.avg_preview_frame_bytes = 1_024.0;
6396 client.avg_frame_bytes = 150_000.0;
6397
6398 assert!(
6399 (pacing_fps(&client) - client.display_fps).abs() < 0.01,
6400 "browser-ready delivery floor should let large frames reach display_fps on a fast path",
6401 );
6402 }
6403
6404 #[test]
6407 fn pacing_fps_zero_when_no_bandwidth() {
6408 let mut client = test_client();
6409 client.goodput_bps = 0.0;
6410 client.delivery_bps = 0.0;
6411 client.last_goodput_sample_bps = 0.0;
6412 assert!(
6413 pacing_fps(&client) == 0.0,
6414 "pacing_fps should be 0 with zero bandwidth"
6415 );
6416 }
6417
6418 #[test]
6419 fn pacing_fps_reaches_display_fps_when_not_bandwidth_limited() {
6420 let mut client = test_client();
6421 client.rtt_ms = 1.0;
6422 client.min_rtt_ms = 1.0;
6423 client.browser_backlog_frames = 0;
6424 client.browser_ack_ahead_frames = 0;
6425 client.browser_apply_ms = 0.0;
6426 client.goodput_bps = 1_000_000.0;
6427 client.delivery_bps = 1_000_000.0;
6428 client.display_fps = 60.0;
6429 assert!((pacing_fps(&client) - 60.0).abs() < 0.01);
6430 }
6431
6432 #[test]
6435 fn throughput_limited_when_low_bandwidth() {
6436 let mut client = test_client();
6437 client.goodput_bps = 1_000.0;
6438 client.delivery_bps = 1_000.0;
6439 client.last_goodput_sample_bps = 0.0;
6440 assert!(throughput_limited(&client));
6441 }
6442
6443 #[test]
6444 fn throughput_not_limited_with_high_bandwidth() {
6445 let mut client = test_client();
6446 client.goodput_bps = 100_000_000.0;
6447 client.delivery_bps = 100_000_000.0;
6448 assert!(!throughput_limited(&client));
6449 }
6450
6451 #[test]
6454 fn browser_pacing_fps_at_least_one() {
6455 let client = test_client();
6456 assert!(browser_pacing_fps(&client) >= 1.0);
6457 }
6458
6459 #[test]
6460 fn browser_pacing_fps_reduced_by_high_backlog() {
6461 let mut client = test_client();
6462 let normal = browser_pacing_fps(&client);
6463 client.browser_backlog_frames = 20;
6464 let backlogged = browser_pacing_fps(&client);
6465 assert!(backlogged < normal, "high backlog should reduce pacing fps");
6466 }
6467
6468 #[test]
6469 fn browser_pacing_fps_reduced_by_high_ack_ahead() {
6470 let mut client = test_client();
6471 let normal = browser_pacing_fps(&client);
6472 client.browser_ack_ahead_frames = 10;
6473 let ahead = browser_pacing_fps(&client);
6474 assert!(ahead < normal, "high ack_ahead should reduce pacing fps");
6475 }
6476
6477 #[test]
6480 fn browser_backlog_blocked_over_threshold() {
6481 let mut client = test_client();
6482 client.browser_backlog_frames = 9;
6483 assert!(browser_backlog_blocked(&client));
6484 }
6485
6486 #[test]
6487 fn browser_backlog_not_blocked_under_threshold() {
6488 let mut client = test_client();
6489 client.browser_backlog_frames = 8;
6490 assert!(!browser_backlog_blocked(&client));
6491 }
6492
6493 #[test]
6496 fn byte_budget_for_at_least_one_frame() {
6497 let client = test_client();
6498 let budget = byte_budget_for(&client, 10.0);
6499 assert!(budget >= client.avg_frame_bytes.max(256.0) as usize);
6500 }
6501
6502 #[test]
6503 fn byte_budget_for_grows_with_time() {
6504 let client = test_client();
6505 let short = byte_budget_for(&client, 10.0);
6506 let long = byte_budget_for(&client, 1000.0);
6507 assert!(long >= short);
6508 }
6509
6510 #[test]
6513 fn target_byte_window_positive() {
6514 let client = test_client();
6515 assert!(target_byte_window(&client) > 0);
6516 }
6517
6518 #[test]
6519 fn target_byte_window_covers_frame_window() {
6520 let client = test_client();
6521 let byte_win = target_byte_window(&client);
6522 let frame_win = target_frame_window(&client);
6523 let min_bytes =
6524 (client.avg_paced_frame_bytes.max(256.0) * frame_win.max(2) as f32).ceil() as usize;
6525 assert!(
6526 byte_win >= min_bytes,
6527 "byte window should cover at least frame_window worth of paced frames"
6528 );
6529 }
6530
6531 #[test]
6534 fn send_interval_matches_browser_pacing() {
6535 let client = test_client();
6536 let interval = send_interval(&client);
6537 let expected = Duration::from_secs_f64(1.0 / browser_pacing_fps(&client) as f64);
6538 let diff = interval.abs_diff(expected);
6539 assert!(diff < Duration::from_micros(10));
6540 }
6541
6542 #[test]
6545 fn preview_fps_at_least_one() {
6546 let client = test_client();
6547 assert!(preview_fps(&client) >= 1.0);
6548 }
6549
6550 #[test]
6553 fn window_open_initially() {
6554 let client = test_client();
6555 assert!(window_open(&client));
6556 }
6557
6558 #[test]
6559 fn window_open_false_when_browser_blocked() {
6560 let mut client = test_client();
6561 client.browser_backlog_frames = 20;
6562 assert!(!window_open(&client));
6563 }
6564
6565 #[test]
6566 fn window_open_false_when_inflight_full() {
6567 let mut client = test_client();
6568 let target = target_frame_window(&client);
6569 fill_inflight(&mut client, target + 10, 1024);
6570 assert!(!window_open(&client));
6571 }
6572
6573 #[test]
6576 fn lead_window_open_no_reserve_same_as_window_open() {
6577 let client = test_client();
6578 assert_eq!(lead_window_open(&client, false), window_open(&client));
6579 }
6580
6581 #[test]
6582 fn lead_window_open_reserves_preview_slot() {
6583 let mut client = test_client();
6584 client.lead = Some(1);
6585 client.subscriptions.insert(1);
6586 let target = target_frame_window(&client);
6587 fill_inflight(&mut client, target.saturating_sub(1), 512);
6589 assert!(!lead_window_open(&client, true));
6592 }
6593
6594 #[test]
6597 fn can_send_frame_when_window_open_and_time_due() {
6598 let mut client = test_client();
6599 client.next_send_at = Instant::now() - Duration::from_millis(100);
6600 assert!(can_send_frame(&client, Instant::now(), false));
6601 }
6602
6603 #[test]
6604 fn can_send_frame_false_when_not_due() {
6605 let mut client = test_client();
6606 client.next_send_at = Instant::now() + Duration::from_secs(10);
6607 assert!(!can_send_frame(&client, Instant::now(), false));
6608 }
6609
6610 #[test]
6611 fn can_send_frame_false_when_window_closed() {
6612 let mut client = test_client();
6613 client.browser_backlog_frames = 20; client.next_send_at = Instant::now() - Duration::from_millis(100);
6615 assert!(!can_send_frame(&client, Instant::now(), false));
6616 }
6617
6618 #[test]
6621 fn record_send_increases_inflight() {
6622 let mut client = test_client();
6623 let now = Instant::now();
6624 assert_eq!(client.inflight_bytes, 0);
6625 assert_eq!(client.inflight_frames.len(), 0);
6626
6627 record_send(&mut client, 1000, now, true);
6628 assert_eq!(client.inflight_bytes, 1000);
6629 assert_eq!(client.inflight_frames.len(), 1);
6630
6631 record_send(&mut client, 500, now, false);
6632 assert_eq!(client.inflight_bytes, 1500);
6633 assert_eq!(client.inflight_frames.len(), 2);
6634 }
6635
6636 #[test]
6637 fn record_send_paced_advances_deadline() {
6638 let mut client = test_client();
6639 let now = Instant::now();
6640 client.next_send_at = now;
6641 record_send(&mut client, 1000, now, true);
6642 assert!(client.next_send_at > now);
6643 }
6644
6645 #[test]
6646 fn record_send_unpaced_does_not_advance_deadline() {
6647 let mut client = test_client();
6648 let now = Instant::now();
6649 let before = client.next_send_at;
6650 record_send(&mut client, 1000, now, false);
6651 assert_eq!(client.next_send_at, before);
6652 }
6653
6654 #[test]
6655 fn record_ack_decreases_inflight() {
6656 let mut client = test_client();
6657 let now = Instant::now();
6658 record_send(&mut client, 1000, now, true);
6659 record_send(&mut client, 500, now, true);
6660 assert_eq!(client.inflight_frames.len(), 2);
6661
6662 record_ack(&mut client);
6663 assert_eq!(client.inflight_frames.len(), 1);
6664 assert_eq!(client.inflight_bytes, 500);
6665 }
6666
6667 #[test]
6668 fn record_ack_on_empty_clears_bytes() {
6669 let mut client = test_client();
6670 client.inflight_bytes = 999; record_ack(&mut client);
6672 assert_eq!(client.inflight_bytes, 0);
6673 }
6674
6675 #[test]
6676 fn record_ack_updates_rtt_estimate() {
6677 let mut client = test_client();
6678 let now = Instant::now();
6679 client.inflight_frames.push_back(InFlightFrame {
6680 sent_at: now - Duration::from_millis(20),
6681 bytes: 512,
6682 paced: true,
6683 });
6684 client.inflight_bytes = 512;
6685 let old_rtt = client.rtt_ms;
6686 record_ack(&mut client);
6687 assert!(
6689 (client.rtt_ms - old_rtt).abs() > 0.01,
6690 "rtt_ms should be updated after ack"
6691 );
6692 }
6693
6694 #[test]
6695 fn record_ack_paced_updates_avg_paced_frame_bytes() {
6696 let mut client = test_client();
6697 let now = Instant::now();
6698 client.inflight_frames.push_back(InFlightFrame {
6699 sent_at: now - Duration::from_millis(10),
6700 bytes: 4096,
6701 paced: true,
6702 });
6703 client.inflight_bytes = 4096;
6704 let old_avg = client.avg_paced_frame_bytes;
6705 record_ack(&mut client);
6706 assert!(client.avg_paced_frame_bytes > old_avg);
6708 }
6709
6710 #[test]
6711 fn record_ack_unpaced_updates_avg_preview_frame_bytes() {
6712 let mut client = test_client();
6713 let now = Instant::now();
6714 client.inflight_frames.push_back(InFlightFrame {
6715 sent_at: now - Duration::from_millis(10),
6716 bytes: 8192,
6717 paced: false,
6718 });
6719 client.inflight_bytes = 8192;
6720 let old_avg = client.avg_preview_frame_bytes;
6721 record_ack(&mut client);
6722 assert!(client.avg_preview_frame_bytes > old_avg);
6723 }
6724
6725 #[test]
6728 fn pty_list_msg_empty_session() {
6729 let sess = Session::new();
6730 let msg = sess.pty_list_msg();
6731 assert_eq!(msg[0], S2C_LIST);
6732 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 0);
6733 assert_eq!(msg.len(), 3);
6734 }
6735
6736 #[test]
6737 fn pty_list_msg_includes_tags() {
6738 let _sess = Session::new();
6739 let tag1 = "shell";
6749 let tag2 = "build";
6750
6751 let mut expected = vec![S2C_LIST];
6753 expected.extend_from_slice(&2u16.to_le_bytes());
6754 expected.extend_from_slice(&1u16.to_le_bytes());
6756 expected.extend_from_slice(&(tag1.len() as u16).to_le_bytes());
6757 expected.extend_from_slice(tag1.as_bytes());
6758 expected.extend_from_slice(&3u16.to_le_bytes());
6760 expected.extend_from_slice(&(tag2.len() as u16).to_le_bytes());
6761 expected.extend_from_slice(tag2.as_bytes());
6762
6763 assert_eq!(expected[0], S2C_LIST);
6765 assert_eq!(u16::from_le_bytes([expected[1], expected[2]]), 2);
6766 let msg_str = String::from_utf8_lossy(&expected);
6768 assert!(msg_str.contains("shell"));
6769 assert!(msg_str.contains("build"));
6770 }
6771
6772 #[test]
6775 fn can_send_preview_true_when_due() {
6776 let mut client = test_client();
6777 let now = Instant::now();
6778 client
6779 .preview_next_send_at
6780 .insert(5, now - Duration::from_millis(100));
6781 assert!(can_send_preview(&client, 5, now));
6782 }
6783
6784 #[test]
6785 fn can_send_preview_false_when_not_due() {
6786 let mut client = test_client();
6787 let now = Instant::now();
6788 client
6789 .preview_next_send_at
6790 .insert(5, now + Duration::from_secs(10));
6791 assert!(!can_send_preview(&client, 5, now));
6792 }
6793
6794 #[test]
6795 fn can_send_preview_false_when_window_closed() {
6796 let mut client = test_client();
6797 client.browser_backlog_frames = 20;
6798 let now = Instant::now();
6799 assert!(!can_send_preview(&client, 5, now));
6800 }
6801
6802 #[test]
6803 fn can_send_preview_true_for_unseen_pid() {
6804 let client = test_client();
6805 let now = Instant::now();
6806 assert!(can_send_preview(&client, 99, now));
6808 }
6809
6810 #[test]
6811 fn record_preview_send_sets_future_deadline() {
6812 let mut client = test_client();
6813 let now = Instant::now();
6814 record_preview_send(&mut client, 5, now);
6815 let deadline = client.preview_next_send_at.get(&5).unwrap();
6816 assert!(*deadline > now);
6817 }
6818
6819 #[test]
6820 fn record_preview_send_successive_calls_advance() {
6821 let mut client = test_client();
6822 let now = Instant::now();
6823 record_preview_send(&mut client, 5, now);
6824 let first = *client.preview_next_send_at.get(&5).unwrap();
6825 record_preview_send(&mut client, 5, first);
6826 let second = *client.preview_next_send_at.get(&5).unwrap();
6827 assert!(second > first, "successive sends should advance deadline");
6828 }
6829
6830 fn browser_ready_high_bandwidth_client() -> ClientState {
6844 let mut c = test_client();
6845 c.display_fps = 120.0;
6846 c.rtt_ms = 1.0;
6847 c.min_rtt_ms = 1.0;
6848 c.goodput_bps = 50_000_000.0;
6849 c.delivery_bps = 50_000_000.0;
6850 c.last_goodput_sample_bps = 50_000_000.0;
6851 c.avg_paced_frame_bytes = 30_000.0;
6852 c.avg_preview_frame_bytes = 1_024.0;
6853 c.avg_frame_bytes = 30_000.0;
6854 c.browser_apply_ms = 0.3;
6855 c
6856 }
6857
6858 fn congested_client() -> ClientState {
6861 let mut c = test_client();
6862 c.display_fps = 120.0;
6863 c.rtt_ms = 500.0;
6864 c.min_rtt_ms = 40.0;
6865 c.goodput_bps = 200_000.0;
6866 c.delivery_bps = 150_000.0;
6867 c.last_goodput_sample_bps = 200_000.0;
6868 c.avg_paced_frame_bytes = 50_000.0;
6869 c.avg_preview_frame_bytes = 1_024.0;
6870 c.avg_frame_bytes = 50_000.0;
6871 c.goodput_jitter_bps = 50_000.0;
6872 c.max_goodput_jitter_bps = 200_000.0;
6873 c.browser_apply_ms = 1.0;
6874 c
6875 }
6876
6877 fn sim_ack(client: &mut ClientState, bytes: usize, rtt_ms: f32) {
6881 let sent_at = Instant::now() - Duration::from_millis(rtt_ms as u64);
6882 client.inflight_bytes += bytes;
6883 client.inflight_frames.push_back(InFlightFrame {
6884 sent_at,
6885 bytes,
6886 paced: true,
6887 });
6888 client.goodput_window_start = Instant::now() - Duration::from_millis(25);
6890 record_ack(client);
6891 }
6892
6893 fn sim_acks(client: &mut ClientState, n: usize, bytes: usize, rtt_ms: f32) {
6894 for _ in 0..n {
6895 sim_ack(client, bytes, rtt_ms);
6896 }
6897 }
6898
6899 #[test]
6902 fn browser_ready_high_bandwidth_client_uses_full_display_fps() {
6903 let client = browser_ready_high_bandwidth_client();
6904 assert!(
6905 (pacing_fps(&client) - client.display_fps).abs() < 0.01,
6906 "pacing_fps {} should equal display_fps {} when browser is ready and bandwidth is abundant",
6907 pacing_fps(&client),
6908 client.display_fps,
6909 );
6910 }
6911
6912 #[test]
6913 fn browser_ready_high_bandwidth_client_send_interval_within_one_frame() {
6914 let client = browser_ready_high_bandwidth_client();
6915 let interval_ms = send_interval(&client).as_secs_f32() * 1000.0;
6916 let frame_ms = 1000.0 / client.display_fps;
6917 assert!(
6918 interval_ms <= frame_ms + 0.1,
6919 "send_interval {interval_ms:.2}ms exceeds one frame ({frame_ms:.2}ms) when browser is ready"
6920 );
6921 }
6922
6923 #[test]
6926 fn congested_pipe_reduces_pacing_fps_substantially() {
6927 let client = congested_client();
6928 let fps = pacing_fps(&client);
6929 assert!(
6930 fps < client.display_fps * 0.5,
6931 "pacing_fps {fps:.0} should be well below display_fps {} when congested",
6932 client.display_fps,
6933 );
6934 }
6935
6936 #[test]
6937 fn congested_pipe_is_throughput_limited() {
6938 let client = congested_client();
6939 assert!(
6940 throughput_limited(&client),
6941 "congested client must be recognised as throughput-limited"
6942 );
6943 }
6944
6945 #[test]
6951 fn byte_window_bounded_near_bdp_when_congested() {
6952 let client = congested_client();
6953 let bdp = client.goodput_bps * (path_rtt_ms(&client) / 1_000.0);
6955 let window = target_byte_window(&client);
6956 assert!(
6957 window < bdp as usize * 8,
6958 "byte window {window}B is {:.1}× BDP ({bdp:.0}B); \
6959 expected ≤ 8× — lead_floor may be dominating",
6960 window as f32 / bdp.max(1.0),
6961 );
6962 }
6963
6964 #[test]
6970 fn min_rtt_not_contaminated_by_congested_rtts() {
6971 let mut client = test_client();
6972 client.display_fps = 120.0;
6973 client.rtt_ms = 40.0;
6974 client.min_rtt_ms = 40.0;
6975 client.goodput_bps = 2_000_000.0;
6976 client.delivery_bps = 2_000_000.0;
6977 client.avg_paced_frame_bytes = 30_000.0;
6978 client.avg_preview_frame_bytes = 1_024.0;
6979 let original_min = client.min_rtt_ms;
6980
6981 sim_acks(&mut client, 200, 30_000, 500.0);
6983
6984 assert!(
6985 client.min_rtt_ms < original_min * 2.0,
6986 "min_rtt drifted from {original_min}ms to {:.1}ms after 200 congested ACKs",
6987 client.min_rtt_ms,
6988 );
6989 }
6990
6991 #[test]
6994 fn delivery_bps_rises_quickly_when_congestion_clears() {
6995 let mut client = congested_client();
6996 let before = client.delivery_bps;
6997
6998 sim_acks(&mut client, 10, 30_000, 40.0);
7000
7001 assert!(
7002 client.delivery_bps > before * 2.0,
7003 "delivery_bps {:.0} should more than double from {before:.0} after 10 fast ACKs",
7004 client.delivery_bps,
7005 );
7006 }
7007
7008 #[test]
7009 fn pacing_fps_recovers_after_congestion_clears() {
7010 let mut client = congested_client();
7011
7012 for _ in 0..40 {
7018 let target = target_frame_window(&client).max(2);
7019 for _ in 0..target {
7020 let sent_at = Instant::now() - Duration::from_millis(40);
7021 client.inflight_bytes += 30_000;
7022 client.inflight_frames.push_back(InFlightFrame {
7023 sent_at,
7024 bytes: 30_000,
7025 paced: true,
7026 });
7027 }
7028 client.goodput_window_start = Instant::now() - Duration::from_millis(25);
7029 for _ in 0..target {
7030 record_ack(&mut client);
7031 }
7032 }
7033
7034 let fps = pacing_fps(&client);
7035 assert!(
7036 fps > client.display_fps * 0.7,
7037 "pacing_fps {fps:.0} didn't recover toward display_fps {} \
7038 after window-saturated rounds at low RTT",
7039 client.display_fps,
7040 );
7041 }
7042
7043 #[test]
7044 fn rtt_estimate_drops_quickly_when_congestion_clears() {
7045 let mut client = test_client();
7046 client.rtt_ms = 500.0;
7047 client.min_rtt_ms = 40.0;
7048 client.goodput_bps = 2_000_000.0;
7049 client.avg_paced_frame_bytes = 30_000.0;
7050 client.avg_preview_frame_bytes = 1_024.0;
7051
7052 sim_acks(&mut client, 10, 30_000, 40.0);
7055
7056 assert!(
7057 client.rtt_ms < 300.0,
7058 "rtt_ms {:.0}ms did not fall fast enough after congestion cleared",
7059 client.rtt_ms,
7060 );
7061 }
7062
7063 #[test]
7066 fn probe_collapses_immediately_on_queue_delay() {
7067 let mut client = test_client();
7068 client.display_fps = 120.0;
7069 client.rtt_ms = 40.0;
7070 client.min_rtt_ms = 40.0;
7071 client.goodput_bps = 5_000_000.0;
7072 client.delivery_bps = 5_000_000.0;
7073 client.last_goodput_sample_bps = 5_000_000.0;
7074 client.avg_paced_frame_bytes = 10_000.0;
7075 client.avg_preview_frame_bytes = 1_024.0;
7076 client.probe_frames = 10.0;
7077
7078 sim_acks(&mut client, 5, 10_000, 600.0);
7080
7081 assert!(
7082 client.probe_frames < 5.0,
7083 "probe_frames {:.1} should have collapsed on queue delay signal",
7084 client.probe_frames,
7085 );
7086 }
7087
7088 #[test]
7089 fn probe_grows_when_window_saturated_with_clean_rtt() {
7090 let mut client = test_client();
7091 client.display_fps = 120.0;
7092 client.rtt_ms = 40.0;
7093 client.min_rtt_ms = 40.0;
7094 client.goodput_bps = 5_000_000.0;
7095 client.delivery_bps = 5_000_000.0;
7096 client.last_goodput_sample_bps = 5_000_000.0;
7097 client.avg_paced_frame_bytes = 10_000.0;
7098 client.avg_preview_frame_bytes = 1_024.0;
7099 client.goodput_jitter_bps = 0.0;
7100 client.max_goodput_jitter_bps = 0.0;
7101 client.probe_frames = 0.0;
7102
7103 let target = target_frame_window(&client);
7105 for _ in 0..target {
7106 let sent_at = Instant::now() - Duration::from_millis(40);
7107 client.inflight_bytes += 10_000;
7108 client.inflight_frames.push_back(InFlightFrame {
7109 sent_at,
7110 bytes: 10_000,
7111 paced: true,
7112 });
7113 }
7114
7115 record_ack(&mut client);
7124
7125 assert!(
7126 client.probe_frames > 0.0,
7127 "probe_frames should grow when window-saturated with clean RTT"
7128 );
7129 }
7130
7131 #[test]
7134 fn frame_window_larger_on_high_latency_link() {
7135 let mut lo = test_client();
7136 lo.display_fps = 120.0;
7137 lo.rtt_ms = 10.0;
7138 lo.min_rtt_ms = 10.0;
7139 lo.goodput_bps = 5_000_000.0;
7140 lo.delivery_bps = 5_000_000.0;
7141 lo.avg_paced_frame_bytes = 10_000.0;
7142 lo.avg_preview_frame_bytes = 1_024.0;
7143
7144 let mut hi = test_client();
7145 hi.display_fps = 120.0;
7146 hi.rtt_ms = 200.0;
7147 hi.min_rtt_ms = 200.0;
7148 hi.goodput_bps = 5_000_000.0;
7149 hi.delivery_bps = 5_000_000.0;
7150 hi.avg_paced_frame_bytes = 10_000.0;
7151 hi.avg_preview_frame_bytes = 1_024.0;
7152
7153 let lo_win = target_frame_window(&lo);
7154 let hi_win = target_frame_window(&hi);
7155 assert!(
7156 hi_win > lo_win,
7157 "high-latency link ({hi_win}f) should need more frames in flight \
7158 than low-latency ({lo_win}f)"
7159 );
7160 }
7161
7162 #[test]
7165 fn small_frame_byte_window_enables_pipelining() {
7166 let mut client = test_client();
7171 client.display_fps = 120.0;
7172 client.rtt_ms = 165.0;
7173 client.min_rtt_ms = 8.0;
7174 client.goodput_bps = 11_000.0; client.delivery_bps = 6_800.0;
7176 client.last_goodput_sample_bps = 11_000.0;
7177 client.avg_paced_frame_bytes = 1_120.0;
7178 client.avg_preview_frame_bytes = 1_024.0;
7179 client.goodput_jitter_bps = 4_300.0;
7180 client.max_goodput_jitter_bps = 6_500.0;
7181
7182 let window = target_byte_window(&client);
7183 let frames = target_frame_window(&client);
7184 let pipeline = frames * 1_120;
7185
7186 assert!(
7187 window >= pipeline,
7188 "byte window {window}B should be >= pipeline ({frames}f × 1120B = {pipeline}B) \
7189 so small frames can pipeline across the RTT"
7190 );
7191 }
7192
7193 #[test]
7194 fn large_frame_byte_window_bounded_by_one_frame_floor() {
7195 let mut client = test_client();
7199 client.display_fps = 120.0;
7200 client.rtt_ms = 165.0;
7201 client.min_rtt_ms = 8.0;
7202 client.goodput_bps = 11_000.0;
7203 client.delivery_bps = 6_800.0;
7204 client.last_goodput_sample_bps = 11_000.0;
7205 client.avg_paced_frame_bytes = 50_000.0; client.avg_preview_frame_bytes = 1_024.0;
7207 client.goodput_jitter_bps = 0.0;
7208 client.max_goodput_jitter_bps = 0.0;
7209
7210 let window = target_byte_window(&client);
7211 let frames = target_frame_window(&client);
7212 let pipeline = frames.saturating_mul(50_000);
7213
7214 assert!(
7215 window < pipeline,
7216 "byte window {window}B should be < full pipeline {pipeline}B \
7217 ({frames}f × 50KB) — large frames must use one-frame floor"
7218 );
7219 assert!(
7220 window >= 50_000,
7221 "byte window {window}B must be at least one frame (50KB)"
7222 );
7223 }
7224
7225 #[test]
7228 fn preview_reservation_applies_even_on_low_latency_high_bandwidth_links() {
7229 let mut client = browser_ready_high_bandwidth_client();
7230 client.lead = Some(1);
7231 client.subscriptions.insert(1);
7232 let target = target_frame_window(&client);
7233 fill_inflight(&mut client, target.saturating_sub(1), 512);
7234 assert!(
7235 !lead_window_open(&client, true),
7236 "preview reservation should apply uniformly for lead clients"
7237 );
7238 }
7239
7240 #[test]
7243 fn probe_recovers_on_healthy_path_after_blip() {
7244 let mut client = browser_ready_high_bandwidth_client();
7245 client.probe_frames = 8.0;
7246
7247 sim_acks(&mut client, 3, 30_000, 200.0);
7249 let post_blip = client.probe_frames;
7250 assert!(
7251 post_blip < 4.0,
7252 "probe_frames {post_blip:.1} should have dropped after blip"
7253 );
7254
7255 client.browser_backlog_frames = 0;
7257 client.browser_ack_ahead_frames = 0;
7258 client.browser_apply_ms = 0.3;
7259
7260 sim_acks(&mut client, 20, 30_000, 1.0);
7262
7263 assert!(
7264 client.probe_frames > post_blip,
7265 "probe_frames {:.1} should have recovered from {post_blip:.1} after healthy ACKs",
7266 client.probe_frames,
7267 );
7268 }
7269
7270 #[test]
7271 fn jitter_decays_fast_on_browser_ready_path() {
7272 let mut client = browser_ready_high_bandwidth_client();
7273
7274 client.max_goodput_jitter_bps = client.goodput_bps * 0.4;
7276 client.goodput_jitter_bps = client.goodput_bps * 0.3;
7277 let initial_jitter = client.max_goodput_jitter_bps;
7278
7279 sim_acks(&mut client, 10, 30_000, 1.0);
7281
7282 assert!(
7283 client.max_goodput_jitter_bps < initial_jitter * 0.5,
7284 "max_goodput_jitter_bps {:.0} should have decayed below {:.0} \
7285 (50% of initial {initial_jitter:.0}) after 10 healthy ACKs on a ready path",
7286 client.max_goodput_jitter_bps,
7287 initial_jitter * 0.5,
7288 );
7289 }
7290
7291 #[test]
7292 fn byte_budget_uses_floor_when_goodput_depressed() {
7293 let mut client = browser_ready_high_bandwidth_client();
7294 client.goodput_bps = 100_000.0;
7295
7296 let budget = byte_budget_for(&client, 100.0);
7297 let floor_budget = (bandwidth_floor_bps(&client) * 100.0 / 1_000.0).ceil() as usize;
7298
7299 assert!(
7300 budget >= floor_budget,
7301 "byte_budget {budget} should be at least bandwidth_floor-based {floor_budget} \
7302 when goodput_bps is depressed but delivery_bps is high"
7303 );
7304 }
7305
7306 #[test]
7307 fn probe_floor_maintained_under_congestion_signal() {
7308 let mut client = test_client();
7309 client.display_fps = 120.0;
7310 client.rtt_ms = 40.0;
7311 client.min_rtt_ms = 40.0;
7312 client.goodput_bps = 5_000_000.0;
7313 client.delivery_bps = 5_000_000.0;
7314 client.last_goodput_sample_bps = 5_000_000.0;
7315 client.avg_paced_frame_bytes = 10_000.0;
7316 client.avg_preview_frame_bytes = 1_024.0;
7317 client.probe_frames = 10.0;
7318
7319 sim_acks(&mut client, 20, 10_000, 600.0);
7321
7322 assert!(
7323 client.probe_frames >= 1.0,
7324 "probe_frames {:.1} should not drop below the floor of 1.0",
7325 client.probe_frames,
7326 );
7327 }
7328
7329 #[test]
7332 fn parse_tq_da1_bare() {
7333 let results = parse_terminal_queries(b"\x1b[c", (24, 80), (0, 0));
7334 assert_eq!(results.len(), 1);
7335 assert!(results[0].starts_with("\x1b[?64;"));
7336 }
7337
7338 #[test]
7339 fn parse_tq_da1_with_zero_param() {
7340 let results = parse_terminal_queries(b"\x1b[0c", (24, 80), (0, 0));
7341 assert_eq!(results.len(), 1);
7342 assert!(results[0].starts_with("\x1b[?64;"));
7343 }
7344
7345 #[test]
7346 fn parse_tq_dsr_cursor_position() {
7347 let results = parse_terminal_queries(b"\x1b[6n", (24, 80), (5, 10));
7348 assert_eq!(results.len(), 1);
7349 assert_eq!(results[0], "\x1b[6;11R");
7350 }
7351
7352 #[test]
7353 fn parse_tq_dsr_status() {
7354 let results = parse_terminal_queries(b"\x1b[5n", (24, 80), (0, 0));
7355 assert_eq!(results.len(), 1);
7356 assert_eq!(results[0], "\x1b[0n");
7357 }
7358
7359 #[test]
7360 fn parse_tq_window_size_cells() {
7361 let results = parse_terminal_queries(b"\x1b[18t", (24, 80), (0, 0));
7362 assert_eq!(results.len(), 1);
7363 assert_eq!(results[0], "\x1b[8;24;80t");
7364 }
7365
7366 #[test]
7367 fn parse_tq_window_size_pixels() {
7368 let results = parse_terminal_queries(b"\x1b[14t", (30, 100), (0, 0));
7369 assert_eq!(results.len(), 1);
7370 assert_eq!(results[0], "\x1b[4;480;800t");
7371 }
7372
7373 #[test]
7374 fn parse_tq_multiple_queries() {
7375 let data = b"\x1b[c\x1b[6n\x1b[5n";
7376 let results = parse_terminal_queries(data, (24, 80), (2, 3));
7377 assert_eq!(results.len(), 3);
7378 assert!(results[0].starts_with("\x1b[?64;"));
7379 assert_eq!(results[1], "\x1b[3;4R");
7380 assert_eq!(results[2], "\x1b[0n");
7381 }
7382
7383 #[test]
7384 fn parse_tq_question_mark_sequences_skipped() {
7385 let results = parse_terminal_queries(b"\x1b[?1h", (24, 80), (0, 0));
7386 assert!(results.is_empty());
7387 }
7388
7389 #[test]
7390 fn parse_tq_unknown_final_byte_ignored() {
7391 let results = parse_terminal_queries(b"\x1b[42z", (24, 80), (0, 0));
7392 assert!(results.is_empty());
7393 }
7394
7395 #[test]
7396 fn parse_tq_empty_input() {
7397 let results = parse_terminal_queries(b"", (24, 80), (0, 0));
7398 assert!(results.is_empty());
7399 }
7400
7401 #[test]
7402 fn parse_tq_plain_text_no_csi() {
7403 let results = parse_terminal_queries(b"hello world", (24, 80), (0, 0));
7404 assert!(results.is_empty());
7405 }
7406
7407 #[test]
7408 fn parse_tq_interleaved_with_text() {
7409 let results = parse_terminal_queries(b"abc\x1b[cdef\x1b[6n", (24, 80), (1, 2));
7410 assert_eq!(results.len(), 2);
7411 }
7412
7413 #[test]
7416 fn parse_tq_osc11_background_color_bel() {
7417 let results = parse_terminal_queries(b"\x1b]11;?\x07", (24, 80), (0, 0));
7418 assert_eq!(results.len(), 1);
7419 assert_eq!(results[0], "\x1b]11;rgb:0000/0000/0000\x1b\\");
7420 }
7421
7422 #[test]
7423 fn parse_tq_osc11_background_color_st() {
7424 let results = parse_terminal_queries(b"\x1b]11;?\x1b\\", (24, 80), (0, 0));
7425 assert_eq!(results.len(), 1);
7426 assert_eq!(results[0], "\x1b]11;rgb:0000/0000/0000\x1b\\");
7427 }
7428
7429 #[test]
7430 fn parse_tq_osc10_foreground_color() {
7431 let results = parse_terminal_queries(b"\x1b]10;?\x07", (24, 80), (0, 0));
7432 assert_eq!(results.len(), 1);
7433 assert_eq!(results[0], "\x1b]10;rgb:ffff/ffff/ffff\x1b\\");
7434 }
7435
7436 #[test]
7437 fn parse_tq_osc4_palette_color_0() {
7438 let results = parse_terminal_queries(b"\x1b]4;0;?\x07", (24, 80), (0, 0));
7439 assert_eq!(results.len(), 1);
7440 assert_eq!(results[0], "\x1b]4;0;rgb:0000/0000/0000\x1b\\");
7441 }
7442
7443 #[test]
7444 fn parse_tq_osc4_palette_color_1() {
7445 let results = parse_terminal_queries(b"\x1b]4;1;?\x07", (24, 80), (0, 0));
7446 assert_eq!(results.len(), 1);
7447 assert_eq!(results[0], "\x1b]4;1;rgb:8080/0000/0000\x1b\\");
7448 }
7449
7450 #[test]
7451 fn parse_tq_osc_mixed_with_csi() {
7452 let results =
7453 parse_terminal_queries(b"\x1b]11;?\x07\x1b[c\x1b]4;0;?\x07", (24, 80), (0, 0));
7454 assert_eq!(results.len(), 3);
7455 assert!(results[0].starts_with("\x1b]11;"));
7456 assert!(results[1].starts_with("\x1b[?64;"));
7457 assert!(results[2].starts_with("\x1b]4;0;"));
7458 }
7459
7460 #[test]
7463 fn search_results_empty() {
7464 let msg = build_search_results_msg(42, &[]);
7465 assert_eq!(msg[0], S2C_SEARCH_RESULTS);
7466 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 42);
7467 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 0);
7468 assert_eq!(msg.len(), 5);
7469 }
7470
7471 #[test]
7472 fn search_results_single() {
7473 let results = vec![SearchResultRow {
7474 pty_id: 7,
7475 score: 100,
7476 primary_source: 1,
7477 matched_sources: 3,
7478 context: "hello".into(),
7479 scroll_offset: Some(42),
7480 }];
7481 let msg = build_search_results_msg(1, &results);
7482 assert_eq!(msg[0], S2C_SEARCH_RESULTS);
7483 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 1);
7484 let pty_id = u16::from_le_bytes([msg[5], msg[6]]);
7485 assert_eq!(pty_id, 7);
7486 let score = u32::from_le_bytes([msg[7], msg[8], msg[9], msg[10]]);
7487 assert_eq!(score, 100);
7488 assert_eq!(msg[11], 1);
7489 assert_eq!(msg[12], 3);
7490 let scroll = u32::from_le_bytes([msg[13], msg[14], msg[15], msg[16]]);
7491 assert_eq!(scroll, 42);
7492 let ctx_len = u16::from_le_bytes([msg[17], msg[18]]) as usize;
7493 assert_eq!(ctx_len, 5);
7494 assert_eq!(&msg[19..19 + ctx_len], b"hello");
7495 }
7496
7497 #[test]
7498 fn search_results_none_scroll_offset() {
7499 let results = vec![SearchResultRow {
7500 pty_id: 1,
7501 score: 0,
7502 primary_source: 0,
7503 matched_sources: 0,
7504 context: String::new(),
7505 scroll_offset: None,
7506 }];
7507 let msg = build_search_results_msg(0, &results);
7508 let scroll = u32::from_le_bytes([msg[13], msg[14], msg[15], msg[16]]);
7509 assert_eq!(scroll, u32::MAX);
7510 }
7511
7512 #[test]
7515 fn allocate_pty_id_empty_session() {
7516 let mut sess = Session::new();
7517 assert_eq!(sess.allocate_pty_id(0), Some(1));
7518 }
7519
7520 #[test]
7521 fn allocate_pty_id_rotates() {
7522 let mut sess = Session::new();
7523 assert_eq!(sess.allocate_pty_id(0), Some(1));
7525 assert_eq!(sess.allocate_pty_id(0), Some(2));
7526 assert_eq!(sess.allocate_pty_id(0), Some(3));
7527 }
7528
7529 #[test]
7530 fn allocate_pty_id_wraps_at_max() {
7531 let mut sess = Session::new();
7532 sess.next_pty_id = u16::MAX;
7533 assert_eq!(sess.allocate_pty_id(0), Some(u16::MAX));
7534 assert_eq!(sess.allocate_pty_id(0), Some(1));
7536 }
7537
7538 #[test]
7541 fn try_send_no_change() {
7542 let mut client = test_client();
7543 let frame = sample_frame("x");
7544 let now = Instant::now();
7545 let outcome = try_send_update(&mut client, 1, frame, None, now, false);
7546 assert!(matches!(outcome, SendOutcome::NoChange));
7547 }
7548
7549 #[test]
7550 fn try_send_sent() {
7551 let (mut client, _rx) = test_client_with_capacity(8);
7552 let frame = sample_frame("x");
7553 let now = Instant::now();
7554 let outcome = try_send_update(
7555 &mut client,
7556 1,
7557 frame.clone(),
7558 Some(vec![1, 2, 3]),
7559 now,
7560 true,
7561 );
7562 assert!(matches!(outcome, SendOutcome::Sent));
7563 assert!(client.last_sent.contains_key(&1));
7564 }
7565
7566 #[test]
7567 fn try_send_backpressured_on_disconnect() {
7568 let (mut client, rx) = test_client_with_capacity(0);
7569 let frame = sample_frame("x");
7570 let now = Instant::now();
7571 drop(rx);
7573 let outcome = try_send_update(
7574 &mut client,
7575 1,
7576 frame.clone(),
7577 Some(vec![1, 2, 3]),
7578 now,
7579 true,
7580 );
7581 assert!(matches!(outcome, SendOutcome::Backpressured));
7582 assert!(
7583 client.last_sent.contains_key(&1),
7584 "last_sent should advance even on disconnect"
7585 );
7586 }
7587}