Use EventLoopProxy for efficient frame wake instead of busy-poll

Replace ControlFlow::Poll (CPU-hungry busy loop) with an
EventLoopProxy that the network thread uses to wake the winit
event loop only when a new frame arrives. Zero CPU when idle,
instant wake on new frames.
This commit is contained in:
Till Wegmueller 2026-04-07 19:49:33 +02:00
parent b805f9f6c8
commit a7ad184774

View file

@ -14,7 +14,7 @@ use std::sync::mpsc;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use winit::application::ApplicationHandler; use winit::application::ApplicationHandler;
use winit::event::WindowEvent; use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop}; use winit::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy};
use winit::window::{Window, WindowAttributes, WindowId}; use winit::window::{Window, WindowAttributes, WindowId};
use wayray_protocol::messages::InputMessage; use wayray_protocol::messages::InputMessage;
@ -102,12 +102,10 @@ impl ApplicationHandler for App {
); );
} }
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
// Use Poll mode so the event loop continuously checks for new frames
// instead of blocking until user input arrives.
event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
// Drain any pending frames from the network thread. // Drain any pending frames from the network thread.
// The network thread wakes us via EventLoopProxy when frames arrive,
// so we don't need to busy-poll.
let mut got_frame = false; let mut got_frame = false;
while let Ok(frame) = self.frame_rx.try_recv() { while let Ok(frame) = self.frame_rx.try_recv() {
if let Some(display) = &self.display { if let Some(display) = &self.display {
@ -121,6 +119,22 @@ impl ApplicationHandler for App {
} }
} }
fn user_event(&mut self, _event_loop: &ActiveEventLoop, _event: ()) {
// Woken by the network thread — new frame available.
// The actual frame processing happens in about_to_wait.
// Just request a redraw check.
let mut got_frame = false;
while let Ok(frame) = self.frame_rx.try_recv() {
if let Some(display) = &self.display {
display.update_frame(&frame.pixels);
got_frame = true;
}
}
if got_frame && let Some(window) = &self.window {
window.request_redraw();
}
}
fn window_event( fn window_event(
&mut self, &mut self,
event_loop: &ActiveEventLoop, event_loop: &ActiveEventLoop,
@ -214,10 +228,18 @@ fn main() {
// dimensions back before entering its frame-receive loop. // dimensions back before entering its frame-receive loop.
let (dim_tx, dim_rx) = mpsc::channel::<(u32, u32)>(); let (dim_tx, dim_rx) = mpsc::channel::<(u32, u32)>();
// Create the event loop early so we can get a proxy for cross-thread wake.
let event_loop = EventLoop::<()>::with_user_event()
.build()
.expect("failed to create event loop");
let proxy: EventLoopProxy<()> = event_loop.create_proxy();
// Spawn the network thread with its own tokio runtime. // Spawn the network thread with its own tokio runtime.
let net_proxy = proxy.clone();
std::thread::Builder::new() std::thread::Builder::new()
.name("wrclient-network".into()) .name("wrclient-network".into())
.spawn(move || { .spawn(move || {
let proxy = net_proxy;
let rt = tokio::runtime::Builder::new_current_thread() let rt = tokio::runtime::Builder::new_current_thread()
.enable_all() .enable_all()
.build() .build()
@ -312,6 +334,9 @@ fn main() {
break; break;
} }
// Wake the winit event loop to process the new frame.
let _ = proxy.send_event(());
// Acknowledge the frame. // Acknowledge the frame.
if let Err(e) = conn.send_frame_ack(update.sequence).await { if let Err(e) = conn.send_frame_ack(update.sequence).await {
warn!(error = %e, "failed to send frame ack"); warn!(error = %e, "failed to send frame ack");
@ -342,7 +367,8 @@ fn main() {
info!(width, height, "starting display"); info!(width, height, "starting display");
// Run the winit event loop on the main thread. // Run the winit event loop on the main thread.
let event_loop = EventLoop::new().expect("failed to create event loop"); // The event loop was created earlier (before spawning the network thread)
// so we could pass an EventLoopProxy to wake it on new frames.
let mut app = App::new(width, height, frame_rx, input_tx); let mut app = App::new(width, height, frame_rx, input_tx);
event_loop.run_app(&mut app).expect("event loop error"); event_loop.run_app(&mut app).expect("event loop error");
} }