fix: clear IPC socket timeout after handshake, add vblank recovery and render diagnostics
Three bugs addressed: 1. SO_RCVTIMEO(5s) on the IPC socket was never cleared after the HELLO handshake. When the web page becomes static (no paint events for >5s), the blocking read_exact in recv_message times out, the IPC reader thread exits, and the socket is closed — causing ECONNRESET on the Electron side and permanently stalling frame delivery. Fix: clear SO_RCVTIMEO to zero (infinite) immediately after successful HELLO exchange. 2. If a VBlank event is lost or delayed (e.g. during DRM master transitions), vblank_pending stays true and all subsequent render_frame calls bail out permanently. Fix: if vblank_pending has been stuck for >500ms, force-clear it with a warning so rendering can resume. making it impossible to diagnose the black screen from logs. Fix: add INFO-level logging for first render, first page flip, first texture import, swapchain exhaustion on first frame, and vblank recovery events. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
ae528b5132
commit
b82a9ca9c3
2 changed files with 88 additions and 9 deletions
|
|
@ -810,6 +810,18 @@ fn handle_connection(
|
||||||
});
|
});
|
||||||
clients.lock().unwrap().insert(name.clone(), client.clone());
|
clients.lock().unwrap().insert(name.clone(), client.clone());
|
||||||
|
|
||||||
|
// Clear the handshake-only SO_RCVTIMEO so the reader thread blocks
|
||||||
|
// indefinitely between messages. A static web page may produce no paint
|
||||||
|
// events for many seconds; a 5-second timeout would incorrectly kill the
|
||||||
|
// connection during these normal idle periods.
|
||||||
|
{
|
||||||
|
let borrowed = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) };
|
||||||
|
let no_timeout = TimeVal::new(0, 0);
|
||||||
|
if let Err(e) = setsockopt(&borrowed, ReceiveTimeout, &no_timeout) {
|
||||||
|
warn!("Failed to clear SO_RCVTIMEO on IPC socket: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Stream Messages (frames, syscalls, positions, etc.)
|
// 2. Stream Messages (frames, syscalls, positions, etc.)
|
||||||
let mut msg_count = 0u32;
|
let mut msg_count = 0u32;
|
||||||
loop {
|
loop {
|
||||||
|
|
|
||||||
85
src/main.rs
85
src/main.rs
|
|
@ -86,9 +86,13 @@ struct EventLoopData {
|
||||||
session: smithay::backend::session::libseat::LibSeatSession,
|
session: smithay::backend::session::libseat::LibSeatSession,
|
||||||
active: bool,
|
active: bool,
|
||||||
vblank_pending: bool,
|
vblank_pending: bool,
|
||||||
|
/// When vblank_pending was first set; used to detect stuck vblank state.
|
||||||
|
vblank_since: Option<Instant>,
|
||||||
consecutive_flip_failures: u32,
|
consecutive_flip_failures: u32,
|
||||||
/// DRI render node path for spawning Electron processes (e.g. `/dev/dri/renderD128`).
|
/// DRI render node path for spawning Electron processes (e.g. `/dev/dri/renderD128`).
|
||||||
electron_render_node: String,
|
electron_render_node: String,
|
||||||
|
/// Total successful page flips (for startup diagnostics).
|
||||||
|
total_flips: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_CONSECUTIVE_FLIP_FAILURES: u32 = 5;
|
const MAX_CONSECUTIVE_FLIP_FAILURES: u32 = 5;
|
||||||
|
|
@ -307,14 +311,32 @@ fn main() -> Result<(), anyhow::Error> {
|
||||||
|
|
||||||
/// Render a single frame - called by VBlank events
|
/// Render a single frame - called by VBlank events
|
||||||
fn render_frame(data: &mut EventLoopData) {
|
fn render_frame(data: &mut EventLoopData) {
|
||||||
// Bail if session is paused (VT switched away) or a page flip is pending
|
// Bail if session is paused (VT switched away)
|
||||||
if !data.active {
|
if !data.active {
|
||||||
trace!("Skipping render: session inactive");
|
trace!("Skipping render: session inactive");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vblank-pending gate: skip if a page flip is outstanding.
|
||||||
|
// Safety net: if vblank_pending has been stuck for >500ms, the VBlank event
|
||||||
|
// was likely lost (e.g. during DRM master transitions). Force-clear so
|
||||||
|
// rendering can resume.
|
||||||
if data.vblank_pending {
|
if data.vblank_pending {
|
||||||
trace!("Skipping render: vblank pending");
|
if let Some(since) = data.vblank_since {
|
||||||
return;
|
if since.elapsed() > Duration::from_millis(500) {
|
||||||
|
warn!(
|
||||||
|
"vblank_pending stuck for {:?} (total_flips={}), force-clearing",
|
||||||
|
since.elapsed(),
|
||||||
|
data.total_flips,
|
||||||
|
);
|
||||||
|
data.vblank_pending = false;
|
||||||
|
data.vblank_since = None;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Frame pacing: respect adaptive timing for input responsiveness
|
// Frame pacing: respect adaptive timing for input responsiveness
|
||||||
|
|
@ -330,8 +352,14 @@ fn render_frame(data: &mut EventLoopData) {
|
||||||
data.last_render = std::time::Instant::now();
|
data.last_render = std::time::Instant::now();
|
||||||
data.input_events_pending = false;
|
data.input_events_pending = false;
|
||||||
|
|
||||||
// First render: mark all windows damaged
|
// First render: mark all windows damaged and log diagnostic state
|
||||||
if data.first_render {
|
if data.first_render {
|
||||||
|
info!(
|
||||||
|
"First render: output={}x{}, windows={}, bg={:?}",
|
||||||
|
data.width, data.height,
|
||||||
|
data.state.config.windows.len(),
|
||||||
|
data.bg,
|
||||||
|
);
|
||||||
for win_cfg in &data.state.config.windows {
|
for win_cfg in &data.state.config.windows {
|
||||||
data.state.damaged_windows.insert(win_cfg.name.clone());
|
data.state.damaged_windows.insert(win_cfg.name.clone());
|
||||||
}
|
}
|
||||||
|
|
@ -341,7 +369,12 @@ fn render_frame(data: &mut EventLoopData) {
|
||||||
// Acquire swapchain slot
|
// Acquire swapchain slot
|
||||||
let slot = match data.swapchain.acquire() {
|
let slot = match data.swapchain.acquire() {
|
||||||
Ok(Some(slot)) => slot,
|
Ok(Some(slot)) => slot,
|
||||||
Ok(None) => return,
|
Ok(None) => {
|
||||||
|
if data.total_flips == 0 {
|
||||||
|
warn!("Swapchain: no free slots on first frame");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Error acquiring swapchain slot: {e}");
|
warn!("Error acquiring swapchain slot: {e}");
|
||||||
return;
|
return;
|
||||||
|
|
@ -508,7 +541,9 @@ fn render_frame(data: &mut EventLoopData) {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
if !render_ok {
|
if !render_ok {
|
||||||
trace!("Rendering failed, skipping page flip");
|
if data.total_flips == 0 {
|
||||||
|
warn!("Rendering failed on first frame, skipping page flip");
|
||||||
|
}
|
||||||
data.swapchain.submitted(&slot);
|
data.swapchain.submitted(&slot);
|
||||||
data.state.damaged_windows.clear();
|
data.state.damaged_windows.clear();
|
||||||
if context_lost {
|
if context_lost {
|
||||||
|
|
@ -543,16 +578,24 @@ fn render_frame(data: &mut EventLoopData) {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let tex_count = data.gles_tex_cache.len();
|
||||||
|
|
||||||
// Schedule page flip (or full modeset commit for the first frame).
|
// Schedule page flip (or full modeset commit for the first frame).
|
||||||
// smithay's DrmSurface tracks pending state (mode, connectors). When
|
// smithay's DrmSurface tracks pending state (mode, connectors). When
|
||||||
// commit_pending() is true (e.g. first frame after surface creation or
|
// commit_pending() is true (e.g. first frame after surface creation or
|
||||||
// VT switch), we must use commit() which includes ALLOW_MODESET.
|
// VT switch), we must use commit() which includes ALLOW_MODESET.
|
||||||
// page_flip() only does a non-blocking flip without modesetting and will
|
// page_flip() only does a non-blocking flip without modesetting and will
|
||||||
// fail with EINVAL if the CRTC doesn't have a mode set yet.
|
// fail with EINVAL if the CRTC doesn't have a mode set yet.
|
||||||
let result = if data.drm_surface.commit_pending() {
|
let commit_pending = data.drm_surface.commit_pending();
|
||||||
info!("Performing initial modeset commit");
|
let result = if commit_pending {
|
||||||
|
info!("Performing initial modeset commit (textures={})", tex_count);
|
||||||
data.drm_surface.commit([plane_state.clone()], true)
|
data.drm_surface.commit([plane_state.clone()], true)
|
||||||
} else {
|
} else {
|
||||||
|
// On the very first page_flip (no prior flips), log at INFO level
|
||||||
|
// so we can confirm the rendering pipeline is working.
|
||||||
|
if data.total_flips == 0 {
|
||||||
|
info!("First page_flip (commit_pending=false, textures={})", tex_count);
|
||||||
|
}
|
||||||
data.drm_surface.page_flip([plane_state.clone()], true)
|
data.drm_surface.page_flip([plane_state.clone()], true)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -560,6 +603,11 @@ fn render_frame(data: &mut EventLoopData) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
data.consecutive_flip_failures = 0;
|
data.consecutive_flip_failures = 0;
|
||||||
data.vblank_pending = true;
|
data.vblank_pending = true;
|
||||||
|
data.vblank_since = Some(Instant::now());
|
||||||
|
data.total_flips += 1;
|
||||||
|
if data.total_flips == 1 {
|
||||||
|
info!("First successful page flip (commit_pending={})", commit_pending);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
data.consecutive_flip_failures = data.consecutive_flip_failures.saturating_add(1);
|
data.consecutive_flip_failures = data.consecutive_flip_failures.saturating_add(1);
|
||||||
|
|
@ -589,6 +637,7 @@ fn render_frame(data: &mut EventLoopData) {
|
||||||
);
|
);
|
||||||
write_flip_failure_report(&commit_report);
|
write_flip_failure_report(&commit_report);
|
||||||
data.vblank_pending = false;
|
data.vblank_pending = false;
|
||||||
|
data.vblank_since = None;
|
||||||
if data.consecutive_flip_failures >= MAX_CONSECUTIVE_FLIP_FAILURES {
|
if data.consecutive_flip_failures >= MAX_CONSECUTIVE_FLIP_FAILURES {
|
||||||
let report = format!(
|
let report = format!(
|
||||||
"Exceeded consecutive DRM flip failures ({}). Last page_flip error: {e}; last commit error: {commit_err}. Exiting compositor.",
|
"Exceeded consecutive DRM flip failures ({}). Last page_flip error: {e}; last commit error: {commit_err}. Exiting compositor.",
|
||||||
|
|
@ -604,6 +653,8 @@ fn render_frame(data: &mut EventLoopData) {
|
||||||
} else {
|
} else {
|
||||||
data.consecutive_flip_failures = 0;
|
data.consecutive_flip_failures = 0;
|
||||||
data.vblank_pending = true;
|
data.vblank_pending = true;
|
||||||
|
data.vblank_since = Some(Instant::now());
|
||||||
|
data.total_flips += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -648,6 +699,7 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
|
||||||
info!("Session paused (VT switched away). Pausing DRM device and halting rendering.");
|
info!("Session paused (VT switched away). Pausing DRM device and halting rendering.");
|
||||||
data.active = false;
|
data.active = false;
|
||||||
data.vblank_pending = false;
|
data.vblank_pending = false;
|
||||||
|
data.vblank_since = None;
|
||||||
data.drm.pause();
|
data.drm.pause();
|
||||||
}
|
}
|
||||||
Event::ActivateSession => {
|
Event::ActivateSession => {
|
||||||
|
|
@ -656,10 +708,12 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
|
||||||
error!("Failed to activate DRM device on session resume: {e}");
|
error!("Failed to activate DRM device on session resume: {e}");
|
||||||
data.active = false;
|
data.active = false;
|
||||||
data.vblank_pending = false;
|
data.vblank_pending = false;
|
||||||
|
data.vblank_since = None;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
data.active = true;
|
data.active = true;
|
||||||
data.vblank_pending = false;
|
data.vblank_pending = false;
|
||||||
|
data.vblank_since = None;
|
||||||
if let Err(e) = data.display.flush_clients() {
|
if let Err(e) = data.display.flush_clients() {
|
||||||
warn!("Failed to flush Wayland clients on activation: {e}");
|
warn!("Failed to flush Wayland clients on activation: {e}");
|
||||||
}
|
}
|
||||||
|
|
@ -1121,9 +1175,11 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
|
||||||
session,
|
session,
|
||||||
active: true,
|
active: true,
|
||||||
vblank_pending: false,
|
vblank_pending: false,
|
||||||
|
vblank_since: None,
|
||||||
consecutive_flip_failures: 0,
|
consecutive_flip_failures: 0,
|
||||||
gles_tex_cache: std::collections::HashMap::new(),
|
gles_tex_cache: std::collections::HashMap::new(),
|
||||||
electron_render_node: electron_render_node,
|
electron_render_node: electron_render_node,
|
||||||
|
total_flips: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let signal_for_handler = loop_signal.clone();
|
let signal_for_handler = loop_signal.clone();
|
||||||
|
|
@ -1140,6 +1196,7 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
|
||||||
if !data.active {
|
if !data.active {
|
||||||
if let DrmEvent::VBlank(_) = event {
|
if let DrmEvent::VBlank(_) = event {
|
||||||
data.vblank_pending = false;
|
data.vblank_pending = false;
|
||||||
|
data.vblank_since = None;
|
||||||
}
|
}
|
||||||
trace!("Ignoring DRM event while session is inactive");
|
trace!("Ignoring DRM event while session is inactive");
|
||||||
return;
|
return;
|
||||||
|
|
@ -1147,6 +1204,7 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
|
||||||
if let DrmEvent::VBlank(_) = event {
|
if let DrmEvent::VBlank(_) = event {
|
||||||
// The screen just refreshed! We are clear to render the next frame.
|
// The screen just refreshed! We are clear to render the next frame.
|
||||||
data.vblank_pending = false;
|
data.vblank_pending = false;
|
||||||
|
data.vblank_since = None;
|
||||||
render_frame(data);
|
render_frame(data);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -1274,14 +1332,23 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
|
||||||
// Import latest frames
|
// Import latest frames
|
||||||
for (_name, frame) in latest_frames {
|
for (_name, frame) in latest_frames {
|
||||||
let frame_name = frame.name.clone();
|
let frame_name = frame.name.clone();
|
||||||
|
let frame_dims = (frame.width, frame.height);
|
||||||
if let Some(renderer_arc) = data.state.backend.renderer() {
|
if let Some(renderer_arc) = data.state.backend.renderer() {
|
||||||
match renderer_arc.lock() {
|
match renderer_arc.lock() {
|
||||||
Ok(ref mut renderer) => {
|
Ok(ref mut renderer) => {
|
||||||
match import_electron_frame(renderer, frame) {
|
match import_electron_frame(renderer, frame) {
|
||||||
Ok((tex, cached)) => {
|
Ok((tex, cached)) => {
|
||||||
|
let is_first = !data.gles_tex_cache.contains_key(&frame_name);
|
||||||
data.state.damaged_windows.insert(frame_name.clone());
|
data.state.damaged_windows.insert(frame_name.clone());
|
||||||
data.state.texture_cache.insert_dmabuf(cached);
|
data.state.texture_cache.insert_dmabuf(cached);
|
||||||
data.gles_tex_cache.insert(frame_name, tex.texture);
|
data.gles_tex_cache.insert(frame_name.clone(), tex.texture);
|
||||||
|
if is_first {
|
||||||
|
info!(
|
||||||
|
"First DMABUF imported for '{}' ({}x{}, total_textures={})",
|
||||||
|
frame_name, frame_dims.0, frame_dims.1,
|
||||||
|
data.gles_tex_cache.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => warn!("DMABUF import failed for '{}': {e:#}", frame_name),
|
Err(e) => warn!("DMABUF import failed for '{}': {e:#}", frame_name),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue