Skip to content

Commit d33b062

Browse files
committed
Merge PR hgaiser#114: feat-ish: Grocery bag of improvements
2 parents 95b6562 + 251919e commit d33b062

10 files changed

Lines changed: 279 additions & 158 deletions

File tree

dist/60-moonshine.rules

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess", GROUP="input", MODE="0660"
22
KERNEL=="uhid", TAG+="uaccess", GROUP="input", MODE="0660"
3-
SUBSYSTEM=="hidraw", KERNELS=="uhid", TAG+="uaccess", GROUP="input", MODE="0660"
4-
SUBSYSTEMS=="input", ATTRS{name}=="Moonshine *", TAG+="uaccess", GROUP="input", MODE="0660"
3+
SUBSYSTEM=="hidraw", KERNELS=="uhid", ATTRS{name}=="Moonshine *", ENV{ID_SEAT}="seat9", MODE="0666"
4+
SUBSYSTEMS=="input", ATTRS{name}=="Moonshine *", ENV{ID_SEAT}="seat9", MODE="0666"

moonshine-core/src/crypto.rs

Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,64 @@ use aes::cipher::Array;
22
use aes::cipher::BlockModeEncrypt;
33
use aes::cipher::KeyIvInit;
44
use aes_gcm::{
5-
aead::{Aead, KeyInit},
5+
aead::{generic_array::GenericArray, AeadInPlace, KeyInit},
66
Aes128Gcm, Key, Nonce,
77
};
88
use inout::block_padding::Pkcs7;
99

10-
pub(crate) fn encrypt(plaintext: &[u8], key: &[u8], iv: &[u8], tag: &mut [u8]) -> Result<Vec<u8>, aes_gcm::Error> {
11-
let key = Key::<Aes128Gcm>::from_slice(key);
12-
let nonce = Nonce::from_slice(iv);
13-
let cipher = Aes128Gcm::new(key);
14-
15-
// In OpenSSL, encrypting with GCM returns ciphertext usually without tag appended if you use `tag()` to retrieve it separate.
16-
// aes-gcm crate append tag to ciphertext.
17-
let mut ciphertext = cipher.encrypt(nonce, plaintext)?;
10+
/// An AES-128-GCM cipher cached across calls, keyed by the control stream's
11+
/// input key.
12+
///
13+
/// The control stream encrypts feedback and decrypts input using
14+
/// `remote_input_key`, which only changes on key rotation (tracked by
15+
/// `remote_input_key_id`). Rebuilding the cipher — a full AES key schedule plus
16+
/// GHASH table — on every packet showed up on the per-input-event path, so we
17+
/// cache it and only rebuild when the key id changes.
18+
pub(crate) struct GcmCipher {
19+
cipher: Option<Aes128Gcm>,
20+
key_id: i64,
21+
}
1822

19-
// Split tag from ciphertext
20-
let tag_len = 16;
21-
let len = ciphertext.len();
22-
if len < tag_len {
23-
return Err(aes_gcm::Error);
23+
impl GcmCipher {
24+
pub fn new() -> Self {
25+
Self {
26+
cipher: None,
27+
key_id: i64::MIN,
28+
}
2429
}
25-
let actual_ciphertext_len = len - tag_len;
26-
27-
tag.copy_from_slice(&ciphertext[actual_ciphertext_len..]);
28-
ciphertext.truncate(actual_ciphertext_len);
29-
30-
Ok(ciphertext)
31-
}
3230

33-
pub(crate) fn decrypt(ciphertext: &[u8], key: &[u8], iv: &[u8], tag: &[u8]) -> Result<Vec<u8>, aes_gcm::Error> {
34-
let key = Key::<Aes128Gcm>::from_slice(key);
35-
let nonce = Nonce::from_slice(iv);
36-
let cipher = Aes128Gcm::new(key);
31+
/// Return the cached cipher, rebuilding it if the key has rotated.
32+
fn get(&mut self, key: &[u8], key_id: i64) -> Result<&Aes128Gcm, ()> {
33+
if self.cipher.is_none() || self.key_id != key_id {
34+
if key.len() != 16 {
35+
tracing::warn!("Control key must be 16 bytes, got {}.", key.len());
36+
self.cipher = None;
37+
return Err(());
38+
}
39+
self.cipher = Some(Aes128Gcm::new(Key::<Aes128Gcm>::from_slice(key)));
40+
self.key_id = key_id;
41+
}
42+
self.cipher.as_ref().ok_or(())
43+
}
3744

38-
// Append tag to ciphertext for aes-gcm crate
39-
let mut payload = Vec::with_capacity(ciphertext.len() + tag.len());
40-
payload.extend_from_slice(ciphertext);
41-
payload.extend_from_slice(tag);
45+
/// Encrypt `buffer` in place, returning the detached 16-byte tag.
46+
pub fn encrypt(&mut self, key: &[u8], key_id: i64, iv: &[u8], buffer: &mut [u8]) -> Result<[u8; 16], ()> {
47+
let cipher = self.get(key, key_id)?;
48+
let tag = cipher
49+
.encrypt_in_place_detached(Nonce::from_slice(iv), b"", buffer)
50+
.map_err(|e| tracing::warn!("Failed to encrypt control data: {e}"))?;
51+
let mut out = [0u8; 16];
52+
out.copy_from_slice(&tag);
53+
Ok(out)
54+
}
4255

43-
cipher.decrypt(nonce, payload.as_ref())
56+
/// Decrypt `buffer` in place using the detached 16-byte tag.
57+
pub fn decrypt(&mut self, key: &[u8], key_id: i64, iv: &[u8], tag: &[u8; 16], buffer: &mut [u8]) -> Result<(), ()> {
58+
let cipher = self.get(key, key_id)?;
59+
cipher
60+
.decrypt_in_place_detached(Nonce::from_slice(iv), b"", buffer, GenericArray::from_slice(tag))
61+
.map_err(|e| tracing::warn!("Failed to decrypt control message: {e}"))
62+
}
4463
}
4564

4665
pub(crate) fn encrypt_cbc(data: &[u8], key: &[u8], iv: &[u8]) -> Result<Vec<u8>, String> {

moonshine-core/src/session/manager.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -360,14 +360,14 @@ impl SessionManager {
360360
// Resume (reconnect): the streams are already running, so PLAY is a
361361
// no-op — the client picks up the existing streams once it PINGs.
362362
// The reconnecting client is a fresh Moonlight session that expects
363-
// frame numbers to start at 1, so reset the video frame counters and
364-
// force an IDR; otherwise it sees the running counter as a huge frame
365-
// gap and reports a poor connection.
363+
// frame numbers to start at 1, so arm a video stream reset (frame-counter
364+
// reset + forced IDR); otherwise it sees the running counter as a huge
365+
// frame gap and reports a poor connection. The reset fires once the packet
366+
// handler re-learns the client's (usually new) address from its first PING,
367+
// so the forced IDR isn't sent to the stale previous address.
366368
active.reset_video_stream();
367369
guard.session = Some(SessionState::Active(active));
368-
tracing::info!(
369-
"Resuming active session: resetting video frame counter and treating PLAY as no-op."
370-
);
370+
tracing::info!("Resuming active session: arming video stream reset and treating PLAY as no-op.");
371371
return Ok(());
372372
},
373373
None => {

moonshine-core/src/session/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,9 @@ impl ActiveSession {
316316
&self.context
317317
}
318318

319-
/// Reset the video stream's frame counters and force an IDR for a resuming client.
319+
/// Arm a video stream reset (frame-counter reset + forced IDR) for a resuming client.
320+
/// The reset fires once the packet handler re-learns the client's address from its
321+
/// first PING; see [`VideoStreamHandle::request_reset`].
320322
pub(crate) fn reset_video_stream(&self) {
321323
self.video_handle.request_reset();
322324
}

moonshine-core/src/session/stream/control/input/gamepad.rs

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -144,15 +144,22 @@ impl GamepadInfo {
144144
}
145145
}
146146

147+
// Moonlight touch lifecycle event types (LI_TOUCH_EVENT_*).
148+
const TOUCH_EVENT_DOWN: u8 = 0x01;
149+
const TOUCH_EVENT_UP: u8 = 0x02;
150+
const TOUCH_EVENT_MOVE: u8 = 0x03;
151+
const TOUCH_EVENT_CANCEL: u8 = 0x04;
152+
const TOUCH_EVENT_CANCEL_ALL: u8 = 0x07;
153+
147154
#[derive(Debug)]
148155
pub(crate) struct GamepadTouch {
149156
pub index: u8,
150-
_event_type: u8,
157+
event_type: u8,
151158
// zero: [u8; 2], // Alignment/reserved
152159
pointer_id: u32,
153160
pub x: f32,
154161
pub y: f32,
155-
pub pressure: f32,
162+
_pressure: f32,
156163
}
157164

158165
impl GamepadTouch {
@@ -177,12 +184,12 @@ impl GamepadTouch {
177184

178185
Ok(Self {
179186
index: buffer[0],
180-
_event_type: buffer[1],
187+
event_type: buffer[1],
181188
// zero: u16::from_le_bytes(buffer[2..4].try_into().unwrap()),
182189
pointer_id: u32::from_le_bytes(buffer[4..8].try_into().unwrap()),
183190
x: f32::from_le_bytes(buffer[8..12].try_into().unwrap()).clamp(0.0, 1.0),
184191
y: f32::from_le_bytes(buffer[12..16].try_into().unwrap()).clamp(0.0, 1.0),
185-
pressure: f32::from_le_bytes(buffer[16..20].try_into().unwrap()).clamp(0.0, 1.0),
192+
_pressure: f32::from_le_bytes(buffer[16..20].try_into().unwrap()).clamp(0.0, 1.0),
186193
})
187194
}
188195
}
@@ -344,6 +351,10 @@ pub(crate) struct Gamepad {
344351
/// The underlying inputtino joypad, used to inject button presses, stick
345352
/// positions, triggers, touchpad events, and motion data.
346353
gamepad: inputtino::Joypad,
354+
355+
/// Active touchpad pointer ids, tracked so CancelAll can release them.
356+
/// DualSense reports at most two concurrent touch points, so a Vec stays tiny.
357+
touch_points: Vec<u32>,
347358
}
348359

349360
impl Gamepad {
@@ -461,7 +472,10 @@ impl Gamepad {
461472
}
462473
});
463474

464-
Ok(Self { gamepad })
475+
Ok(Self {
476+
gamepad,
477+
touch_points: Vec::new(),
478+
})
465479
}
466480

467481
/// Apply button flags to the gamepad.
@@ -482,14 +496,30 @@ impl Gamepad {
482496

483497
pub fn touch(&mut self, touch: &GamepadTouch) {
484498
if let Joypad::PS5(gamepad) = &self.gamepad {
485-
if touch.pressure > 0.5 {
486-
gamepad.place_finger(
487-
touch.pointer_id,
488-
(touch.x * PS5Joypad::TOUCHPAD_WIDTH as f32) as u16,
489-
(touch.y * PS5Joypad::TOUCHPAD_HEIGHT as f32) as u16,
490-
);
491-
} else {
492-
gamepad.release_finger(touch.pointer_id);
499+
// Drive the touchpad from Moonlight's explicit touch lifecycle event
500+
// rather than inferring up/down from pressure, which clients don't
501+
// reliably populate (the DualSense touchpad has no pressure sensor).
502+
match touch.event_type {
503+
TOUCH_EVENT_DOWN | TOUCH_EVENT_MOVE => {
504+
gamepad.place_finger(
505+
touch.pointer_id,
506+
(touch.x * PS5Joypad::TOUCHPAD_WIDTH as f32) as u16,
507+
(touch.y * PS5Joypad::TOUCHPAD_HEIGHT as f32) as u16,
508+
);
509+
if !self.touch_points.contains(&touch.pointer_id) {
510+
self.touch_points.push(touch.pointer_id);
511+
}
512+
},
513+
TOUCH_EVENT_UP | TOUCH_EVENT_CANCEL => {
514+
gamepad.release_finger(touch.pointer_id);
515+
self.touch_points.retain(|id| *id != touch.pointer_id);
516+
},
517+
TOUCH_EVENT_CANCEL_ALL => {
518+
for pointer_id in self.touch_points.drain(..) {
519+
gamepad.release_finger(pointer_id);
520+
}
521+
},
522+
_ => {},
493523
}
494524
}
495525
}

moonshine-core/src/session/stream/control/input/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ impl InputHandler {
127127
stop_session_manager: ShutdownManager<SessionShutdownReason>,
128128
gamepad_config: GamepadConfig,
129129
) -> Result<Self, ()> {
130-
let (gamepad_tx, gamepad_rx) = mpsc::channel(10);
130+
// Sized generously: the control loop awaits sends into this channel, so a
131+
// full channel stalls all input processing and ENet servicing.
132+
let (gamepad_tx, gamepad_rx) = mpsc::channel(64);
131133

132134
std::thread::spawn(move || {
133135
let rt = tokio::runtime::Builder::new_current_thread()

0 commit comments

Comments
 (0)