mirror of
https://github.com/nestriness/nestri.git
synced 2026-11-03 14:12:39 +02:00
release: nescope resize clamp, clean exit, info logging (#442)
dev → prod. - fix(nescope): a window can't ask for more than the output has, and clamped requests are logged (#438) - fix(nescope): a hub that closes the input socket is no longer a panic; nescope ends if the hub stays gone (#440) - fix(nescope, neswire): default log filter is info; dropped audio frames log once per burst (#441)
This commit is contained in:
@@ -537,14 +537,24 @@ impl XwmHandler for CalloopData {
|
||||
h: Option<u32>,
|
||||
_reorder: Option<Reorder>,
|
||||
) {
|
||||
// Honor size requests within reason but always keep position at (0,0).
|
||||
let mut geo = window.geometry();
|
||||
if let Some(w) = w {
|
||||
geo.size.w = w as i32;
|
||||
// Honor size requests, but never past the output: a window larger
|
||||
// than it is cut off, and the pointer cannot reach the cut part. The
|
||||
// output keeps following the client's window instead.
|
||||
if window.is_override_redirect() {
|
||||
return;
|
||||
}
|
||||
if let Some(h) = h {
|
||||
geo.size.h = h as i32;
|
||||
let (req_w, req_h) = (w, h);
|
||||
let cur = window.geometry();
|
||||
let (x, y, w, h) = crate::state::clamp_to_output(
|
||||
(cur.loc.x, cur.loc.y, cur.size.w, cur.size.h),
|
||||
(w, h),
|
||||
(self.state.width as i32, self.state.height as i32),
|
||||
);
|
||||
let asked = (req_w, req_h);
|
||||
if req_w.is_some_and(|v| v as i32 != w) || req_h.is_some_and(|v| v as i32 != h) {
|
||||
self.state.note_clamp(window.window_id(), asked, (w, h));
|
||||
}
|
||||
let geo = Rectangle::new((x, y).into(), (w, h).into());
|
||||
let _ = window.configure(geo);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
use std::io::{self, Read};
|
||||
use std::os::unix::io::{AsFd, BorrowedFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use calloop::{EventSource, Interest, Mode, Poll, PostAction, Readiness, Token, TokenFactory};
|
||||
|
||||
pub struct InputIpcSource {
|
||||
stream: UnixStream,
|
||||
buf: Vec<u8>,
|
||||
/// Set once the hub has closed its end, for the main loop to act on.
|
||||
closed: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl InputIpcSource {
|
||||
@@ -16,14 +20,39 @@ impl InputIpcSource {
|
||||
Ok(Self {
|
||||
stream,
|
||||
buf: Vec::new(),
|
||||
closed: Arc::new(AtomicBool::new(false)),
|
||||
})
|
||||
}
|
||||
|
||||
/// True once the hub has closed the socket. The main loop gives that a
|
||||
/// grace period and then ends nescope: a compositor with no input holds
|
||||
/// the GPU for nothing.
|
||||
pub fn closed_flag(&self) -> Arc<AtomicBool> {
|
||||
self.closed.clone()
|
||||
}
|
||||
|
||||
pub fn try_clone(&self) -> io::Result<UnixStream> {
|
||||
self.stream.try_clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl InputIpcSource {
|
||||
/// Read everything available into `buf`. `true` if the peer has closed.
|
||||
fn fill(&mut self) -> io::Result<bool> {
|
||||
let mut tmp = [0u8; 4096];
|
||||
loop {
|
||||
match self.stream.read(&mut tmp) {
|
||||
Ok(0) => return Ok(true),
|
||||
Ok(n) => self.buf.extend_from_slice(&tmp[..n]),
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => return Ok(false),
|
||||
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
|
||||
Err(e) if e.kind() == io::ErrorKind::ConnectionReset => return Ok(true),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsFd for InputIpcSource {
|
||||
fn as_fd(&self) -> BorrowedFd<'_> {
|
||||
self.stream.as_fd()
|
||||
@@ -45,22 +74,13 @@ impl EventSource for InputIpcSource {
|
||||
where
|
||||
F: FnMut(Self::Event, &mut Self::Metadata),
|
||||
{
|
||||
let mut tmp = [0u8; 4096];
|
||||
loop {
|
||||
match self.stream.read(&mut tmp) {
|
||||
Ok(0) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"IPC socket closed",
|
||||
));
|
||||
}
|
||||
Ok(n) => {
|
||||
self.buf.extend_from_slice(&tmp[..n]);
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
// The hub closing its end is not an error of this source: it is what
|
||||
// the hub does when the box is going down (init signals every process
|
||||
// at once, so the hub can be gone before our own SIGTERM is read), and
|
||||
// an `Err` here is returned out of the event loop and panics `main`,
|
||||
// skipping the cleanup after it. Frames already read are still
|
||||
// delivered, then the source removes itself and the loop carries on.
|
||||
let closed = self.fill()?;
|
||||
|
||||
while self.buf.len() >= 2 {
|
||||
let payload_len = u16::from_le_bytes([self.buf[0], self.buf[1]]) as usize;
|
||||
@@ -73,6 +93,11 @@ impl EventSource for InputIpcSource {
|
||||
callback(payload, &mut ());
|
||||
}
|
||||
|
||||
if closed {
|
||||
self.closed.store(true, Ordering::Relaxed);
|
||||
tracing::warn!("input IPC socket closed by the hub; no more input from it");
|
||||
return Ok(PostAction::Remove);
|
||||
}
|
||||
Ok(PostAction::Continue)
|
||||
}
|
||||
|
||||
@@ -88,3 +113,44 @@ impl EventSource for InputIpcSource {
|
||||
poll.unregister(self.as_fd())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
|
||||
fn pair() -> (InputIpcSource, UnixStream) {
|
||||
let (a, b) = UnixStream::pair().unwrap();
|
||||
a.set_nonblocking(true).unwrap();
|
||||
(
|
||||
InputIpcSource {
|
||||
stream: a,
|
||||
buf: Vec::new(),
|
||||
closed: Arc::new(AtomicBool::new(false)),
|
||||
},
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_open_peer_with_nothing_to_say_is_not_closed() {
|
||||
let (mut source, _peer) = pair();
|
||||
assert!(!source.fill().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closed_peer_is_closed_not_an_error() {
|
||||
let (mut source, peer) = pair();
|
||||
drop(peer);
|
||||
assert!(source.fill().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_the_peer_sent_before_closing_is_kept() {
|
||||
let (mut source, mut peer) = pair();
|
||||
peer.write_all(&[1, 0, 7]).unwrap();
|
||||
drop(peer);
|
||||
assert!(source.fill().unwrap());
|
||||
assert_eq!(source.buf, vec![1, 0, 7]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
//! | `PROTON_ENABLE_WAYLAND` | Set to `1` always, so Proton renders through Wayland |
|
||||
//! | `XCURSOR_THEME` | XCursor theme name for the software cursor |
|
||||
//! | `XCURSOR_SIZE` | XCursor size in pixels |
|
||||
//! | `RUST_LOG` | Tracing filter (e.g. `nescope=debug`) |
|
||||
//! | `RUST_LOG` | Tracing filter, default `info` (e.g. `nescope=debug`) |
|
||||
//!
|
||||
//! # Ctrl+C / shutdown
|
||||
//!
|
||||
@@ -193,9 +193,20 @@ struct Args {
|
||||
// Entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Filter from `RUST_LOG`, or `info` when it is unset or unparsable.
|
||||
///
|
||||
/// `EnvFilter::from_default_env` alone defaults to ERROR, which hid every
|
||||
/// info/warn line from the host journal (nesinit only forwards `RUST_LOG` if
|
||||
/// it has it itself). Same fallback as neshub, nesinit and nesgamepad.
|
||||
fn log_filter(rust_log: Option<&str>) -> tracing_subscriber::EnvFilter {
|
||||
rust_log
|
||||
.and_then(|v| tracing_subscriber::EnvFilter::try_new(v).ok())
|
||||
.unwrap_or_else(|| "info".into())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with_env_filter(log_filter(std::env::var("RUST_LOG").ok().as_deref()))
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
@@ -389,11 +400,15 @@ fn main() {
|
||||
|
||||
// ── Input IPC source ─────────────────────────────────────────────────
|
||||
// Connect to the neshub input socket and feed events into the
|
||||
// compositor seat. Reconnection is handled in the idle callback.
|
||||
// compositor seat. There is no reconnection: when the hub closes the socket
|
||||
// the source removes itself, and the loop below ends nescope after a short
|
||||
// grace (a signal that arrives first ends it sooner).
|
||||
let ipc_path = args.input_ipc.clone();
|
||||
let mut hub_closed_flag: Option<std::sync::Arc<std::sync::atomic::AtomicBool>> = None;
|
||||
match input_ipc::InputIpcSource::connect(&ipc_path) {
|
||||
Ok(source) => {
|
||||
tracing::info!("Connected to input IPC socket: {ipc_path}");
|
||||
hub_closed_flag = Some(source.closed_flag());
|
||||
match source.try_clone() {
|
||||
Ok(write_stream) => {
|
||||
state.ipc_write = Some(write_stream);
|
||||
@@ -476,6 +491,7 @@ fn main() {
|
||||
// never arrives there is nothing to wait for and no game to run.
|
||||
let startup = std::time::Instant::now();
|
||||
let mut xwayland_timed_out = false;
|
||||
let mut hub_closed_at: Option<std::time::Instant> = None;
|
||||
|
||||
event_loop
|
||||
.run(Some(Duration::from_secs(1)), &mut data, move |data| {
|
||||
@@ -578,6 +594,24 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hub gone ──────────────────────────────────────────────
|
||||
// neshub closing the input socket mid-run leaves a compositor
|
||||
// nobody can drive. The grace lets a shutdown that is already
|
||||
// under way (SIGTERM stops the loop itself) finish its own way;
|
||||
// otherwise nescope ends, and nesinit ends the box on seeing it.
|
||||
if hub_closed_flag
|
||||
.as_ref()
|
||||
.is_some_and(|f| f.load(std::sync::atomic::Ordering::Relaxed))
|
||||
{
|
||||
let since = hub_closed_at.get_or_insert_with(std::time::Instant::now);
|
||||
if since.elapsed() > Duration::from_secs(2) {
|
||||
tracing::error!("input hub gone for 2 s with no shutdown — exiting");
|
||||
kill_all_children();
|
||||
data.loop_signal.stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Poll primary process ──────────────────────────────────
|
||||
// The launcher (e.g. Steam's shell wrapper) may exit quickly
|
||||
// while the real game client stays alive as a reparented
|
||||
@@ -834,3 +868,38 @@ fn kill_all_children_sigkill() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod log_filter_tests {
|
||||
use super::log_filter;
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::filter::LevelFilter;
|
||||
|
||||
#[test]
|
||||
fn unset_defaults_to_info_so_warn_is_visible() {
|
||||
let f = log_filter(None);
|
||||
assert_eq!(f.max_level_hint(), Some(LevelFilter::INFO));
|
||||
assert!(Level::WARN <= Level::INFO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_stays_out_of_the_default() {
|
||||
assert_ne!(log_filter(None).max_level_hint(), Some(LevelFilter::DEBUG));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_rust_log_wins() {
|
||||
assert_eq!(
|
||||
log_filter(Some("debug")).max_level_hint(),
|
||||
Some(LevelFilter::DEBUG)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn garbage_falls_back_to_info() {
|
||||
assert_eq!(
|
||||
log_filter(Some("=== not a filter ===")).max_level_hint(),
|
||||
Some(LevelFilter::INFO)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,43 @@ impl WinKey {
|
||||
}
|
||||
}
|
||||
|
||||
/// A window's geometry after it asked for `want` (either side may be absent),
|
||||
/// held inside an `output` of its own size: no larger, and moved back in if
|
||||
/// the request would put part of it outside. Pure so it can be tested.
|
||||
pub fn clamp_to_output(
|
||||
cur: (i32, i32, i32, i32),
|
||||
want: (Option<u32>, Option<u32>),
|
||||
output: (i32, i32),
|
||||
) -> (i32, i32, i32, i32) {
|
||||
let side = |req: Option<u32>, cur: i32, max: i32| {
|
||||
let v = req.map_or(cur, |v| v.min(i32::MAX as u32) as i32);
|
||||
v.clamp(1, max.max(1))
|
||||
};
|
||||
let w = side(want.0, cur.2, output.0);
|
||||
let h = side(want.1, cur.3, output.1);
|
||||
let x = cur.0.clamp(0, (output.0 - w).max(0));
|
||||
let y = cur.1.clamp(0, (output.1 - h).max(0));
|
||||
(x, y, w, h)
|
||||
}
|
||||
|
||||
impl NescopeState {
|
||||
/// Say a request was clamped. A window that keeps asking is fighting the
|
||||
/// configure: warn once when the run passes 50 inside 5 seconds.
|
||||
pub fn note_clamp(&mut self, id: u32, asked: (Option<u32>, Option<u32>), got: (i32, i32)) {
|
||||
let now = std::time::Instant::now();
|
||||
let run = self.clamp_runs.entry(id).or_insert((now, 0));
|
||||
if now.duration_since(run.0) > std::time::Duration::from_secs(5) {
|
||||
*run = (now, 0);
|
||||
}
|
||||
run.1 += 1;
|
||||
let n = run.1;
|
||||
tracing::debug!(window = id, ?asked, clamped = ?got, repeats = n, "configure_request clamped");
|
||||
if n == 50 {
|
||||
tracing::warn!(window = id, ?asked, clamped = ?got, "window keeps asking past the output: configure ping-pong?");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CalloopData {
|
||||
pub state: NescopeState,
|
||||
pub display: Display<NescopeState>,
|
||||
@@ -239,6 +276,8 @@ pub struct NescopeState {
|
||||
/// counted for, ref(d-0036): a client's window can shrink the output
|
||||
/// below it, never grow it past.
|
||||
pub launch_size: (u32, u32),
|
||||
/// Per X11 window: when its run of clamped requests began and how many.
|
||||
pub clamp_runs: std::collections::HashMap<u32, (std::time::Instant, u32)>,
|
||||
pub fps: u32,
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────
|
||||
@@ -376,6 +415,7 @@ impl NescopeState {
|
||||
width,
|
||||
height,
|
||||
launch_size: (width, height),
|
||||
clamp_runs: std::collections::HashMap::new(),
|
||||
fps,
|
||||
loop_handle,
|
||||
game_launched: false,
|
||||
@@ -1213,3 +1253,43 @@ mod fit_output_tests {
|
||||
assert_eq!(fit_output((10, 10), (1920, 1080)), (320, 320));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod clamp_to_output_tests {
|
||||
use super::clamp_to_output;
|
||||
|
||||
const OUT: (i32, i32) = (1280, 720);
|
||||
|
||||
#[test]
|
||||
fn a_window_that_fits_is_left_alone() {
|
||||
assert_eq!(clamp_to_output((0, 0, 800, 600), (Some(1000), Some(700)), OUT), (0, 0, 1000, 700));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_launch_size_game_is_held_to_the_shrunk_output() {
|
||||
assert_eq!(clamp_to_output((0, 0, 1280, 720), (Some(1920), Some(1080)), OUT), (0, 0, 1280, 720));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_width_only_request_leaves_the_height() {
|
||||
assert_eq!(clamp_to_output((0, 0, 1280, 500), (Some(1920), None), OUT), (0, 0, 1280, 500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_height_only_request_leaves_the_width() {
|
||||
assert_eq!(clamp_to_output((0, 0, 640, 720), (None, Some(2000)), OUT), (0, 0, 640, 720));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_pushed_past_the_edge_is_moved_back() {
|
||||
assert_eq!(clamp_to_output((900, 500, 400, 300), (None, None), OUT), (880, 420, 400, 300));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_window_is_clamped_on_its_own() {
|
||||
let a = clamp_to_output((0, 0, 1920, 1080), (None, None), OUT);
|
||||
let b = clamp_to_output((100, 100, 300, 200), (Some(5000), None), OUT);
|
||||
assert_eq!(a, (0, 0, 1280, 720));
|
||||
assert_eq!(b, (0, 100, 1280, 200));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ struct Args {
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with_env_filter(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
@@ -83,6 +83,9 @@ pub fn run(sample_rate: u32, channels: u32, frame_size: u32, tx: Sender<Packet>)
|
||||
/// Where the accumulator ends when the graph gives no position: a count
|
||||
/// of the frames received, so packets still carry a continuous timeline.
|
||||
counted_end: u64,
|
||||
/// Frames dropped because the encoder was behind. A stalled encoder
|
||||
/// drops ~50 a second, so only the first and every 250th are logged.
|
||||
dropped: u64,
|
||||
}
|
||||
|
||||
let state = State {
|
||||
@@ -93,6 +96,7 @@ pub fn run(sample_rate: u32, channels: u32, frame_size: u32, tx: Sender<Packet>)
|
||||
sample_rate,
|
||||
next_ticks: None,
|
||||
counted_end: 0,
|
||||
dropped: 0,
|
||||
};
|
||||
|
||||
let _listener = stream
|
||||
@@ -174,7 +178,10 @@ pub fn run(sample_rate: u32, channels: u32, frame_size: u32, tx: Sender<Packet>)
|
||||
};
|
||||
|
||||
if state.tx.try_send(packet).is_err() {
|
||||
tracing::warn!("encoder falling behind, dropping frame");
|
||||
state.dropped += 1;
|
||||
if state.dropped % 250 == 1 {
|
||||
tracing::warn!(dropped = state.dropped, "encoder falling behind, dropping frames");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user