Skip to main content

blit_server/
lib.rs

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