Skip to main content

blit_server/
lib.rs

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