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()); + } +}