Fix rendering efficiency and screencopy Y-flip
Rendering: Skip the full bind/clear/draw/finish/submit cycle when nothing visual has changed (no new Electron frames, no dirty Wayland surfaces, no cursor movement). In the DRM path, also skip when no screencopy or portal streams are active. Frame callbacks are always sent unconditionally to prevent Wayland client stalls. Screencopy: Remove the incorrect row-flip after glReadPixels in both the wlr-screencopy fulfillment and portal stream readback paths. Smithay already applies a Y-correction when rendering to GBM-backed FBOs, so glReadPixels returns top-down data. The manual flip was inverting an already-correct image, producing upside-down output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
0bede114a6
commit
0a72ec60bf
2 changed files with 180 additions and 150 deletions
48
src/main.rs
48
src/main.rs
|
|
@ -90,6 +90,8 @@ struct EventLoopData {
|
|||
frame_timestamps: std::collections::HashMap<String, Instant>,
|
||||
gles_tex_cache: std::collections::HashMap<String, smithay::backend::renderer::gles::GlesTexture>,
|
||||
input_events_pending: bool,
|
||||
/// Track previous cursor position to detect cursor movement for damage tracking.
|
||||
last_cursor_pos: (f64, f64),
|
||||
session: smithay::backend::session::libseat::LibSeatSession,
|
||||
active: bool,
|
||||
vblank_pending: bool,
|
||||
|
|
@ -478,6 +480,32 @@ fn render_frame(data: &mut EventLoopData) {
|
|||
data.first_render = false;
|
||||
}
|
||||
|
||||
// Detect cursor movement for damage tracking
|
||||
let cursor_pos = (data.state.pointer_location.x, data.state.pointer_location.y);
|
||||
let cursor_moved = cursor_pos != data.last_cursor_pos;
|
||||
if cursor_moved {
|
||||
data.last_cursor_pos = cursor_pos;
|
||||
}
|
||||
|
||||
// Check for pending screencopy requests that require a fresh render
|
||||
let has_screencopy = data.state.screencopy_state.as_ref()
|
||||
.map_or(false, |s| s.has_pending_frames());
|
||||
let has_portal_streams = data.state.portal.as_ref()
|
||||
.map_or(false, |p| !p.get_active_sessions().is_empty());
|
||||
|
||||
// Skip the expensive render+flip cycle when nothing visual has changed.
|
||||
// Dirty surface exports (SHM/DMABUF to Electron) are handled separately
|
||||
// and still run when dirty_surfaces is non-empty.
|
||||
let frame_dirty = !data.state.damaged_windows.is_empty()
|
||||
|| !data.state.dirty_surfaces.is_empty()
|
||||
|| cursor_moved
|
||||
|| has_screencopy
|
||||
|| has_portal_streams;
|
||||
|
||||
if !frame_dirty {
|
||||
return;
|
||||
}
|
||||
|
||||
// Acquire swapchain slot
|
||||
let slot = match data.swapchain.acquire() {
|
||||
Ok(Some(slot)) => slot,
|
||||
|
|
@ -966,15 +994,7 @@ fn render_frame(data: &mut EventLoopData) {
|
|||
});
|
||||
|
||||
if read_ok.is_ok() {
|
||||
// GL returns bottom-up rows; flip to top-down.
|
||||
let stride = fb_w as usize * 4;
|
||||
for y in 0..(fb_h as usize / 2) {
|
||||
let top = y * stride;
|
||||
let bot = (fb_h as usize - 1 - y) * stride;
|
||||
for x in 0..stride {
|
||||
fb_pixels.swap(top + x, bot + x);
|
||||
}
|
||||
}
|
||||
// GBM-backed FBO stores pixels top-down; no row flip needed.
|
||||
|
||||
// Convert RGBA→ARGB (wl_shm expects ARGB8888)
|
||||
for pixel in fb_pixels.chunks_exact_mut(4) {
|
||||
|
|
@ -1060,14 +1080,7 @@ fn render_frame(data: &mut EventLoopData) {
|
|||
);
|
||||
});
|
||||
if read_ok.is_ok() {
|
||||
let stride = fb_w as usize * 4;
|
||||
for y in 0..(fb_h as usize / 2) {
|
||||
let top = y * stride;
|
||||
let bot = (fb_h as usize - 1 - y) * stride;
|
||||
for x in 0..stride {
|
||||
fb_pixels.swap(top + x, bot + x);
|
||||
}
|
||||
}
|
||||
// GBM-backed FBO stores pixels top-down; no row flip needed.
|
||||
for pixel in fb_pixels.chunks_exact_mut(4) {
|
||||
let (r, g, b, a) = (pixel[0], pixel[1], pixel[2], pixel[3]);
|
||||
pixel[0] = b;
|
||||
|
|
@ -1869,6 +1882,7 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> {
|
|||
wayland_socket,
|
||||
frame_timestamps: std::collections::HashMap::new(),
|
||||
input_events_pending: false,
|
||||
last_cursor_pos: (0.0, 0.0),
|
||||
session,
|
||||
active: true,
|
||||
vblank_pending: false,
|
||||
|
|
|
|||
280
src/nested.rs
280
src/nested.rs
|
|
@ -296,6 +296,8 @@ pub fn run_nested(config: Config) -> Result<()> {
|
|||
// Frame tracking for backpressure management
|
||||
let mut frame_timestamps: HashMap<String, Instant> = HashMap::new();
|
||||
let _max_frame_age = Duration::from_millis(33); // Drop frames older than 2 frames @60Hz
|
||||
// Track previous cursor position to detect movement
|
||||
let mut last_cursor_pos: (f64, f64) = (0.0, 0.0);
|
||||
|
||||
info!("Nested compositor running");
|
||||
|
||||
|
|
@ -551,6 +553,16 @@ pub fn run_nested(config: Config) -> Result<()> {
|
|||
let output_rect: Rectangle<i32, Physical> =
|
||||
Rectangle::new((0, 0).into(), (size.w as i32, size.h as i32).into());
|
||||
|
||||
// Determine if anything visual changed since last frame.
|
||||
let cursor_pos = (state.pointer_location.x, state.pointer_location.y);
|
||||
let cursor_moved = cursor_pos != last_cursor_pos;
|
||||
if cursor_moved {
|
||||
last_cursor_pos = cursor_pos;
|
||||
}
|
||||
let has_new_frames = !latest_frames.is_empty();
|
||||
let has_dirty_surfaces = !state.dirty_surfaces.is_empty();
|
||||
let frame_dirty = has_new_frames || has_dirty_surfaces || cursor_moved;
|
||||
|
||||
// Render in a scope to release borrows before submission.
|
||||
// Use a labeled block so early exits (bind/render failures) still reach
|
||||
// the frame-callback section below — Wayland clients MUST receive frame
|
||||
|
|
@ -558,6 +570,12 @@ pub fn run_nested(config: Config) -> Result<()> {
|
|||
let mut render_ok = false;
|
||||
let render_t0 = std::time::Instant::now();
|
||||
'render: {
|
||||
// Skip the expensive bind/render/submit cycle when nothing visual changed.
|
||||
// Dirty surface exports (SHM/DMABUF) still happen via the separate path below.
|
||||
if !frame_dirty {
|
||||
break 'render;
|
||||
}
|
||||
|
||||
// Bind backend for rendering (this returns (renderer, target))
|
||||
let bind_t0 = std::time::Instant::now();
|
||||
let (renderer, mut target) = match backend.bind() {
|
||||
|
|
@ -602,83 +620,115 @@ pub fn run_nested(config: Config) -> Result<()> {
|
|||
|
||||
let imported_textures = &gles_tex_cache;
|
||||
|
||||
// Prepare cursor elements before starting the frame
|
||||
let cursor_elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> =
|
||||
if let smithay::input::pointer::CursorImageStatus::Surface(ref cursor_surface) = state.cursor_status {
|
||||
use smithay::backend::renderer::element::{surface::render_elements_from_surface_tree, Kind};
|
||||
use smithay::utils::Scale as ScaleF;
|
||||
// Only run the full compositing pass when the output needs updating.
|
||||
if frame_dirty {
|
||||
// Prepare cursor elements before starting the frame
|
||||
let cursor_elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> =
|
||||
if let smithay::input::pointer::CursorImageStatus::Surface(ref cursor_surface) = state.cursor_status {
|
||||
use smithay::backend::renderer::element::{surface::render_elements_from_surface_tree, Kind};
|
||||
use smithay::utils::Scale as ScaleF;
|
||||
|
||||
let scale = ScaleF::from(1.0);
|
||||
render_elements_from_surface_tree(
|
||||
renderer,
|
||||
cursor_surface,
|
||||
(state.pointer_location.x as i32, state.pointer_location.y as i32),
|
||||
scale,
|
||||
1.0,
|
||||
Kind::Cursor,
|
||||
)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let scale = ScaleF::from(1.0);
|
||||
render_elements_from_surface_tree(
|
||||
renderer,
|
||||
cursor_surface,
|
||||
(state.pointer_location.x as i32, state.pointer_location.y as i32),
|
||||
scale,
|
||||
1.0,
|
||||
Kind::Cursor,
|
||||
)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Load themed cursor texture for Named cursor status
|
||||
let themed_cursor = if let smithay::input::pointer::CursorImageStatus::Named(ref icon) = state.cursor_status {
|
||||
state.cursor_theme_manager.get_cursor(icon.name(), renderer).cloned()
|
||||
} else if cursor_elements.is_empty() {
|
||||
if !matches!(state.cursor_status, smithay::input::pointer::CursorImageStatus::Hidden) {
|
||||
state.cursor_theme_manager.get_cursor("default", renderer).cloned()
|
||||
// Load themed cursor texture for Named cursor status
|
||||
let themed_cursor = if let smithay::input::pointer::CursorImageStatus::Named(ref icon) = state.cursor_status {
|
||||
state.cursor_theme_manager.get_cursor(icon.name(), renderer).cloned()
|
||||
} else if cursor_elements.is_empty() {
|
||||
if !matches!(state.cursor_status, smithay::input::pointer::CursorImageStatus::Hidden) {
|
||||
state.cursor_theme_manager.get_cursor("default", renderer).cloned()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
};
|
||||
|
||||
// Wayland windows are exclusively rendered by comraw via off-screen buffers.
|
||||
// The native space.elements() rendering pass has been removed here.
|
||||
// Wayland windows are exclusively rendered by comraw via off-screen buffers.
|
||||
// The native space.elements() rendering pass has been removed here.
|
||||
|
||||
let mut frame = match renderer.render(
|
||||
&mut target,
|
||||
Size::from((size.w as i32, size.h as i32)),
|
||||
Transform::Flipped180,
|
||||
) {
|
||||
Ok(frame) => frame,
|
||||
Err(e) => {
|
||||
warn!("Failed to begin frame: {}", e);
|
||||
if is_egl_context_or_surface_lost(&e.to_string()) {
|
||||
error!(
|
||||
"EGL context/surface lost while starting frame; shutting down so context can be recreated"
|
||||
);
|
||||
state.running = false;
|
||||
let mut frame = match renderer.render(
|
||||
&mut target,
|
||||
Size::from((size.w as i32, size.h as i32)),
|
||||
Transform::Flipped180,
|
||||
) {
|
||||
Ok(frame) => frame,
|
||||
Err(e) => {
|
||||
warn!("Failed to begin frame: {}", e);
|
||||
if is_egl_context_or_surface_lost(&e.to_string()) {
|
||||
error!(
|
||||
"EGL context/surface lost while starting frame; shutting down so context can be recreated"
|
||||
);
|
||||
state.running = false;
|
||||
}
|
||||
break 'render;
|
||||
}
|
||||
};
|
||||
|
||||
// Clear background
|
||||
if let Err(e) = frame.clear(bg.into(), &[output_rect]) {
|
||||
warn!("Failed to clear frame: {}", e);
|
||||
break 'render;
|
||||
}
|
||||
};
|
||||
|
||||
// Clear background
|
||||
if let Err(e) = frame.clear(bg.into(), &[output_rect]) {
|
||||
warn!("Failed to clear frame: {}", e);
|
||||
break 'render;
|
||||
}
|
||||
let mut windows: Vec<_> = state
|
||||
.config
|
||||
.windows
|
||||
.iter()
|
||||
.chain(state.config.root.iter())
|
||||
.collect();
|
||||
windows.sort_by_key(|w| w.z_order);
|
||||
|
||||
let mut windows: Vec<_> = state
|
||||
.config
|
||||
.windows
|
||||
.iter()
|
||||
.chain(state.config.root.iter())
|
||||
.collect();
|
||||
windows.sort_by_key(|w| w.z_order);
|
||||
for win in windows {
|
||||
let mapped = state.window_mapped.get(&win.name).copied().unwrap_or(true);
|
||||
if !mapped {
|
||||
continue;
|
||||
}
|
||||
|
||||
for win in windows {
|
||||
let mapped = state.window_mapped.get(&win.name).copied().unwrap_or(true);
|
||||
if !mapped {
|
||||
continue;
|
||||
if let Some(tex) = imported_textures.get(&win.name) {
|
||||
if let Err(e) = frame.render_texture_at(
|
||||
tex,
|
||||
(win.x, win.y).into(),
|
||||
1,
|
||||
1.0,
|
||||
Transform::Normal,
|
||||
&[output_rect],
|
||||
&[],
|
||||
1.0,
|
||||
) {
|
||||
warn!("Failed to render texture for {}: {}", win.name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(tex) = imported_textures.get(&win.name) {
|
||||
// Render cursor on top of everything
|
||||
if !cursor_elements.is_empty() {
|
||||
for element in &cursor_elements {
|
||||
use smithay::utils::Scale as ScaleF;
|
||||
let scale = ScaleF::from(1.0);
|
||||
let src = element.src();
|
||||
let dst = element.geometry(scale);
|
||||
if let Err(e) = element.draw(&mut frame, src, dst, &[output_rect], &[]) {
|
||||
trace!("Failed to render cursor element: {}", e);
|
||||
}
|
||||
}
|
||||
} else if let Some(ref cur) = themed_cursor {
|
||||
use smithay::backend::renderer::Frame as _;
|
||||
let cx = state.pointer_location.x as i32 - cur.xhot as i32;
|
||||
let cy = state.pointer_location.y as i32 - cur.yhot as i32;
|
||||
if let Err(e) = frame.render_texture_at(
|
||||
tex,
|
||||
(win.x, win.y).into(),
|
||||
&cur.texture,
|
||||
(cx, cy).into(),
|
||||
1,
|
||||
1.0,
|
||||
Transform::Normal,
|
||||
|
|
@ -686,59 +736,25 @@ pub fn run_nested(config: Config) -> Result<()> {
|
|||
&[],
|
||||
1.0,
|
||||
) {
|
||||
warn!("Failed to render texture for {}: {}", win.name, e);
|
||||
trace!("Failed to render themed cursor: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (Native rendering loop was removed)
|
||||
|
||||
// NOTE: Wayland surfaces are rendered via offscreen GL to SHM for comraw,
|
||||
// allowing the client to fully control window compositing and layout.
|
||||
|
||||
// Render cursor on top of everything
|
||||
if !cursor_elements.is_empty() {
|
||||
for element in &cursor_elements {
|
||||
use smithay::utils::Scale as ScaleF;
|
||||
let scale = ScaleF::from(1.0);
|
||||
let src = element.src();
|
||||
let dst = element.geometry(scale);
|
||||
if let Err(e) = element.draw(&mut frame, src, dst, &[output_rect], &[]) {
|
||||
trace!("Failed to render cursor element: {}", e);
|
||||
match frame.finish() {
|
||||
Ok(_) => {
|
||||
render_ok = true;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to finish frame: {}", e);
|
||||
if is_egl_context_or_surface_lost(&e.to_string()) {
|
||||
error!(
|
||||
"EGL context/surface lost while finishing frame; shutting down so context can be recreated"
|
||||
);
|
||||
state.running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(ref cur) = themed_cursor {
|
||||
use smithay::backend::renderer::Frame as _;
|
||||
let cx = state.pointer_location.x as i32 - cur.xhot as i32;
|
||||
let cy = state.pointer_location.y as i32 - cur.yhot as i32;
|
||||
if let Err(e) = frame.render_texture_at(
|
||||
&cur.texture,
|
||||
(cx, cy).into(),
|
||||
1,
|
||||
1.0,
|
||||
Transform::Normal,
|
||||
&[output_rect],
|
||||
&[],
|
||||
1.0,
|
||||
) {
|
||||
trace!("Failed to render themed cursor: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
match frame.finish() {
|
||||
Ok(_) => {
|
||||
render_ok = true;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to finish frame: {}", e);
|
||||
if is_egl_context_or_surface_lost(&e.to_string()) {
|
||||
error!(
|
||||
"EGL context/surface lost while finishing frame; shutting down so context can be recreated"
|
||||
);
|
||||
state.running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // end if frame_dirty
|
||||
let render_ms = render_t0.elapsed().as_millis();
|
||||
if render_ms > 50 {
|
||||
warn!("render phase took {}ms!", render_ms);
|
||||
|
|
@ -1080,7 +1096,7 @@ pub fn run_nested(config: Config) -> Result<()> {
|
|||
}
|
||||
|
||||
state.dirty_surfaces.clear();
|
||||
} // 'render block ends here, backend borrow is released
|
||||
} // inner bind scope ends here, backend borrow is released
|
||||
|
||||
|
||||
let did_bind = render_ok;
|
||||
|
|
@ -1105,31 +1121,31 @@ pub fn run_nested(config: Config) -> Result<()> {
|
|||
warn!("backend.submit() (eglSwapBuffers) took {}ms! This may indicate a blocked vsync/EGL fence.", submit_ms);
|
||||
}
|
||||
}
|
||||
} // 'render block ends here
|
||||
|
||||
let presented_at = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default();
|
||||
let windows_snapshot: Vec<_> = state.space.elements().cloned().collect();
|
||||
for window in windows_snapshot {
|
||||
window.send_frame(&state.output, presented_at, None, |_, _| {
|
||||
Some(state.output.clone())
|
||||
});
|
||||
}
|
||||
// Frame callbacks must be sent unconditionally so Wayland clients
|
||||
// don't stall — even when the compositor skipped rendering.
|
||||
let presented_at = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default();
|
||||
let windows_snapshot: Vec<_> = state.space.elements().cloned().collect();
|
||||
for window in windows_snapshot {
|
||||
window.send_frame(&state.output, presented_at, None, |_, _| {
|
||||
Some(state.output.clone())
|
||||
});
|
||||
}
|
||||
|
||||
// SHM exports already written directly to mmap'd memfds and broadcast above.
|
||||
let render_total_ms = render_t_start.elapsed().as_millis();
|
||||
if render_total_ms > 50 {
|
||||
warn!("render+submit+callbacks total took {}ms!", render_total_ms);
|
||||
}
|
||||
|
||||
let render_total_ms = render_t_start.elapsed().as_millis();
|
||||
if render_total_ms > 50 {
|
||||
warn!("render+submit+callbacks total took {}ms!", render_total_ms);
|
||||
}
|
||||
std::thread::sleep(Duration::from_micros(100));
|
||||
|
||||
std::thread::sleep(Duration::from_micros(100));
|
||||
|
||||
// Log if the entire loop iteration was slow.
|
||||
let total_loop_ms = loop_t0.elapsed().as_millis();
|
||||
if total_loop_ms > 50 {
|
||||
warn!("SLOW LOOP iter={} total={}ms", loop_iter, total_loop_ms);
|
||||
}
|
||||
// Log if the entire loop iteration was slow.
|
||||
let total_loop_ms = loop_t0.elapsed().as_millis();
|
||||
if total_loop_ms > 50 {
|
||||
warn!("SLOW LOOP iter={} total={}ms", loop_iter, total_loop_ms);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue