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