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