From 176dd2dc2b9a157d0fdfe71f15f71aff8fe34010 Mon Sep 17 00:00:00 2001 From: Wanjohi Date: Sat, 17 Oct 2026 12:10:10 +0300 Subject: [PATCH] feat(nescope): the box's output follows the client's window size A new input event, INPUT_OUTPUT_SIZE [4][w u16][h u16], rides the input path to nescope, which resizes its output and every window on it. The size is clamped to the launch size, so the box's encode budget holds, and kept even for 4:2:0. apply_resize existed but nothing called it. Old hubs stop a batch at an unknown type, so clients send it alone. --- apps/nescope/src/input.rs | 2 ++ apps/nescope/src/main.rs | 6 ++++- apps/nescope/src/state.rs | 20 +++++++++++++++- apps/neshub/src/session.rs | 8 ++++--- crates/nesprotocol/src/input.rs | 41 +++++++++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 5 deletions(-) diff --git a/apps/nescope/src/input.rs b/apps/nescope/src/input.rs index 825be0b6..ac66d84e 100644 --- a/apps/nescope/src/input.rs +++ b/apps/nescope/src/input.rs @@ -338,6 +338,8 @@ pub fn decode_wire_event(data: &[u8]) -> Option { DecodedInput::MouseWheel { dx: _dx, dy } => { Some(InputEvent::ScrollVertical { amount: dy as f64 }) } + // Not input: the IPC source hands it to the output before this. + DecodedInput::OutputSize { .. } => None, } } diff --git a/apps/nescope/src/main.rs b/apps/nescope/src/main.rs index 6256354e..39717eaa 100644 --- a/apps/nescope/src/main.rs +++ b/apps/nescope/src/main.rs @@ -404,7 +404,11 @@ fn main() { } loop_handle .insert_source(source, move |payload, _, data| { - if let Some(event) = decode_wire_event(&payload) { + if let Some(nesprotocol::input::DecodedInput::OutputSize { width, height }) = + nesprotocol::input::decode_input_event(&payload) + { + data.state.request_output_size(width, height); + } else if let Some(event) = decode_wire_event(&payload) { process_input(event, &mut data.state); } }) diff --git a/apps/nescope/src/state.rs b/apps/nescope/src/state.rs index b682ae66..1f47c083 100644 --- a/apps/nescope/src/state.rs +++ b/apps/nescope/src/state.rs @@ -59,6 +59,10 @@ use crate::input::InputEvent; // CalloopData — threaded through the event loop // --------------------------------------------------------------------------- +/// The smallest side a client's window can make the output. Below it, games +/// lay out menus they cannot fit, and nothing is gained. +const MIN_OUTPUT: u32 = 320; + pub struct CalloopData { pub state: NescopeState, pub display: Display, @@ -195,6 +199,10 @@ pub struct NescopeState { // ── Dimensions + frame rate ─────────────────────────────────────────── pub width: u32, pub height: u32, + /// The size the box was started at, which is what its encode budget was + /// counted for, ref(d-0036): a client's window can shrink the output + /// below it, never grow it past. + pub launch_size: (u32, u32), pub fps: u32, // ── Lifecycle ───────────────────────────────────────────────────────── @@ -328,6 +336,7 @@ impl NescopeState { presentation_seq: 0, width, height, + launch_size: (width, height), fps, loop_handle, game_launched: false, @@ -462,8 +471,17 @@ impl NescopeState { // Resize // ----------------------------------------------------------------------- + /// The client's window is `width`x`height`: follow it, inside the launch + /// size and on even numbers, which every encoder needs for 4:2:0. + pub fn request_output_size(&mut self, width: u16, height: u16) { + let fit = |want: u16, max: u32| (u32::from(want).clamp(MIN_OUTPUT, max) & !1).max(2); + let (w, h) = (fit(width, self.launch_size.0), fit(height, self.launch_size.1)); + if (w, h) != (self.width, self.height) { + self.apply_resize(w, h); + } + } + /// Apply a new output resolution (e.g. from a runtime resize request). - #[allow(unused)] pub fn apply_resize(&mut self, width: u32, height: u32) { tracing::info!("Applying resize: {width}x{height}"); self.width = width; diff --git a/apps/neshub/src/session.rs b/apps/neshub/src/session.rs index 2ae70787..5f7b7838 100644 --- a/apps/neshub/src/session.rs +++ b/apps/neshub/src/session.rs @@ -6,7 +6,9 @@ use tokio::sync::Mutex; use tracing::{debug, info, warn}; use nesprotocol::datagram::{DGRAM_AUDIO, DGRAM_BUFFER_BYTES, DGRAM_VIDEO}; -use nesprotocol::input::{INPUT_KEY, INPUT_MOUSE_BUTTON, INPUT_MOUSE_MOVE, INPUT_MOUSE_WHEEL}; +use nesprotocol::input::{ + INPUT_KEY, INPUT_MOUSE_BUTTON, INPUT_MOUSE_MOVE, INPUT_MOUSE_WHEEL, INPUT_OUTPUT_SIZE, +}; use nesprotocol::{BIDI_CONTROL, BIDI_INPUT, Carrier, STREAM_CURSOR, STREAM_STATS}; use nesprotocol::{ControlMode, ReceiverReport, decode_control_mode, decode_receiver_report}; use nesprotocol::{FRAME_HDR_LEN, STREAM_VERSION, encode_frame}; @@ -521,8 +523,8 @@ fn split_input_events(payload: &[u8]) -> Vec> { let len = match payload[offset] { // [type][up/down][keycode u16 LE] INPUT_KEY => 4, - // [type][dx i16 LE][dy i16 LE] - INPUT_MOUSE_MOVE | INPUT_MOUSE_WHEEL => 5, + // [type][dx i16 LE][dy i16 LE], and [type][w u16 LE][h u16 LE] + INPUT_MOUSE_MOVE | INPUT_MOUSE_WHEEL | INPUT_OUTPUT_SIZE => 5, // [type][button][up/down] INPUT_MOUSE_BUTTON => 3, other => { diff --git a/crates/nesprotocol/src/input.rs b/crates/nesprotocol/src/input.rs index 04c8a119..b66bc4e8 100644 --- a/crates/nesprotocol/src/input.rs +++ b/crates/nesprotocol/src/input.rs @@ -5,6 +5,10 @@ pub const INPUT_KEY: u8 = 0; pub const INPUT_MOUSE_MOVE: u8 = 1; pub const INPUT_MOUSE_BUTTON: u8 = 2; pub const INPUT_MOUSE_WHEEL: u8 = 3; +/// The client's window is now this many pixels: the box's output follows it. +/// Not input, but it rides the input path because that is the one road that +/// already reaches the compositor. +pub const INPUT_OUTPUT_SIZE: u8 = 4; pub const INPUT_KEY_UP: u8 = 0; pub const INPUT_KEY_DOWN: u8 = 1; @@ -110,6 +114,7 @@ pub enum DecodedInput { MouseMove { dx: i16, dy: i16 }, MouseButton { button: u8, down: bool }, MouseWheel { dx: i16, dy: i16 }, + OutputSize { width: u16, height: u16 }, } /// Decode a single input event from raw wire bytes. @@ -119,6 +124,7 @@ pub enum DecodedInput { /// - type=1 INPUT_MOUSE_MOVE: payload=[dx:2B LE][dy:2B LE] /// - type=2 INPUT_MOUSE_BUTTON: payload=[button:1B][down/up:1B] (0=left, 1=middle, 2=right) /// - type=3 INPUT_MOUSE_WHEEL: payload=[dx:2B LE][dy:2B LE] +/// - type=4 INPUT_OUTPUT_SIZE: payload=[width:2B LE][height:2B LE] /// /// Returns `None` if the buffer is too short for the given event type or the /// type byte is unknown. @@ -163,6 +169,14 @@ pub fn decode_input_event(data: &[u8]) -> Option { let dy = i16::from_le_bytes([data[3], data[4]]); Some(DecodedInput::MouseWheel { dx, dy }) } + INPUT_OUTPUT_SIZE => { + if data.len() < 5 { + return None; + } + let width = u16::from_le_bytes([data[1], data[2]]); + let height = u16::from_le_bytes([data[3], data[4]]); + Some(DecodedInput::OutputSize { width, height }) + } _ => None, } } @@ -195,6 +209,16 @@ pub fn encode_mouse_wheel(buf: &mut Vec, dx: i16, dy: i16) { buf.extend_from_slice(&dy.to_le_bytes()); } +/// Encode an output size: [4][width u16 LE][height u16 LE] +/// +/// Send it in a batch of its own: a hub from before it stops reading a batch +/// at a type it does not know, and would drop whatever followed. +pub fn encode_output_size(buf: &mut Vec, width: u16, height: u16) { + buf.push(INPUT_OUTPUT_SIZE); + buf.extend_from_slice(&width.to_le_bytes()); + buf.extend_from_slice(&height.to_le_bytes()); +} + /// Mapping from `KeyboardEvent.code` strings to Linux input keycodes. /// Covers all essential gaming keys. Use `keymap_lookup(code)` to resolve. pub fn keymap_lookup(code: &str) -> Option { @@ -315,3 +339,20 @@ pub fn keymap_lookup(code: &str) -> Option { _ => return None, }) } + +#[cfg(test)] +mod output_size_tests { + use super::*; + + #[test] + fn an_output_size_comes_back_as_sent() { + let mut buf = Vec::new(); + encode_output_size(&mut buf, 2560, 1440); + assert_eq!(buf.len(), 5); + assert!(matches!( + decode_input_event(&buf), + Some(DecodedInput::OutputSize { width: 2560, height: 1440 }) + )); + assert!(decode_input_event(&buf[..4]).is_none()); + } +}