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.
This commit is contained in:
Wanjohi
2026-10-17 12:10:10 +03:00
parent 07e504f4c3
commit 176dd2dc2b
5 changed files with 72 additions and 5 deletions
+2
View File
@@ -338,6 +338,8 @@ pub fn decode_wire_event(data: &[u8]) -> Option<InputEvent> {
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,
}
}
+5 -1
View File
@@ -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);
}
})
+19 -1
View File
@@ -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<NescopeState>,
@@ -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;
+5 -3
View File
@@ -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<Vec<u8>> {
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 => {
+41
View File
@@ -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<DecodedInput> {
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<u8>, 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<u8>, 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<u16> {
@@ -315,3 +339,20 @@ pub fn keymap_lookup(code: &str) -> Option<u16> {
_ => 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());
}
}