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