From ca9fb910e7d0e35e24ca4c1428bf7284ce8008b8 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 10 Feb 2026 12:46:56 -0800 Subject: [PATCH 1/3] [add] policy for setting the event loops control flow --- crates/lambda-rs-platform/src/winit/mod.rs | 75 ++- crates/lambda-rs/README.md | 10 +- crates/lambda-rs/examples/minimal.rs | 6 +- crates/lambda-rs/src/render/mod.rs | 4 +- crates/lambda-rs/src/runtimes/application.rs | 482 ++++++++++--------- crates/lambda-rs/src/runtimes/mod.rs | 1 + 6 files changed, 338 insertions(+), 240 deletions(-) diff --git a/crates/lambda-rs-platform/src/winit/mod.rs b/crates/lambda-rs-platform/src/winit/mod.rs index f4219252..dba3c407 100644 --- a/crates/lambda-rs-platform/src/winit/mod.rs +++ b/crates/lambda-rs-platform/src/winit/mod.rs @@ -1,5 +1,10 @@ //! Winit wrapper to easily construct cross platform windows +use std::time::{ + Duration, + Instant, +}; + use winit::{ dpi::{ LogicalSize, @@ -44,6 +49,24 @@ pub mod winit_exports { }; } +/// Control flow policy for the winit event loop. +/// +/// Lambda defaults to [`EventLoopPolicy::Poll`] for backwards compatibility. +/// Applications that don't require continuous updates (e.g., editors/tools) +/// should prefer [`EventLoopPolicy::Wait`] to reduce CPU usage when idle. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum EventLoopPolicy { + /// Continuous polling for games and real-time applications. + Poll, + /// Sleep until events arrive; ideal for tools and editors. + Wait, + /// Sleep until the next frame deadline to target a fixed update rate. + /// + /// Note: this is not a frame-pacing / vsync guarantee; it only controls how + /// long the event loop waits between wakeups. + WaitUntil { target_fps: u32 }, +} + /// LoopBuilder - Putting this here for consistency. pub struct LoopBuilder; @@ -228,14 +251,62 @@ impl Loop { } /// Uses the winit event loop to run forever - pub fn run_forever(self, mut callback: Callback) + pub fn run_forever(self, callback: Callback) where Callback: 'static + FnMut(Event, &EventLoopWindowTarget), { + self.run_forever_with_policy(EventLoopPolicy::Poll, callback); + } + + /// Uses the winit event loop to run forever with the provided control-flow + /// policy. + pub fn run_forever_with_policy( + self, + policy: EventLoopPolicy, + mut callback: Callback, + ) where + Callback: 'static + FnMut(Event, &EventLoopWindowTarget), + { + let frame_interval = match policy { + EventLoopPolicy::WaitUntil { target_fps } if target_fps > 0 => { + Some(Duration::from_secs_f64(1.0 / target_fps as f64)) + } + _ => None, + }; + let mut next_frame_deadline: Option = None; + self .event_loop .run(move |event, target| { - target.set_control_flow(ControlFlow::Poll); + match policy { + EventLoopPolicy::Poll => { + target.set_control_flow(ControlFlow::Poll); + } + EventLoopPolicy::Wait => { + target.set_control_flow(ControlFlow::Wait); + } + EventLoopPolicy::WaitUntil { target_fps: 0 } => { + target.set_control_flow(ControlFlow::Wait); + } + EventLoopPolicy::WaitUntil { .. } => { + let now = Instant::now(); + let interval = frame_interval.unwrap_or(Duration::from_secs(1)); + + let deadline = match next_frame_deadline { + Some(mut deadline) => { + while deadline <= now { + deadline += interval; + } + deadline + } + None => now + interval, + }; + + next_frame_deadline = Some(deadline); + target.set_control_flow(ControlFlow::WaitUntil(deadline)); + } + } + callback(event, target); }) .expect("Event loop terminated unexpectedly"); diff --git a/crates/lambda-rs/README.md b/crates/lambda-rs/README.md index 7af0f7df..b11a2f47 100644 --- a/crates/lambda-rs/README.md +++ b/crates/lambda-rs/README.md @@ -16,14 +16,18 @@ cargo add lambda-rs ## First window Getting started with lambda is easy. The following example will create a window with the title "Hello lambda!" and a size of 800x600. ```rust -#[macro_use] use lambda::{ - core::runtime::start_runtime, - runtimes::ApplicationRuntimeBuilder, + runtime::start_runtime, + runtimes::{ + ApplicationRuntimeBuilder, + EventLoopPolicy, + }, }; fn main() { let runtime = ApplicationRuntimeBuilder::new("Hello lambda!") + // Tools/editors should prefer `Wait` to reduce CPU usage when idle. + .with_event_loop_policy(EventLoopPolicy::Wait) .with_window_configured_as(move |window_builder| { return window_builder .with_dimensions(800, 600) diff --git a/crates/lambda-rs/examples/minimal.rs b/crates/lambda-rs/examples/minimal.rs index fbf29965..6beb8dc8 100644 --- a/crates/lambda-rs/examples/minimal.rs +++ b/crates/lambda-rs/examples/minimal.rs @@ -7,11 +7,15 @@ use lambda::{ render::PresentMode, runtime::start_runtime, - runtimes::ApplicationRuntimeBuilder, + runtimes::{ + ApplicationRuntimeBuilder, + EventLoopPolicy, + }, }; fn main() { let runtime = ApplicationRuntimeBuilder::new("Minimal Demo application") + .with_event_loop_policy(EventLoopPolicy::Wait) .with_window_configured_as(move |window_builder| { return window_builder .with_dimensions(800, 600) diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index be85d765..f275ccf0 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -574,14 +574,14 @@ impl RenderContext { // surface. We only acquire a surface frame when a surface-backed pass is // requested; offscreen-only command lists can render without a window. let requires_surface = commands.iter().any(|cmd| { - return matches!( + matches!( cmd, RenderCommand::BeginRenderPass { .. } | RenderCommand::BeginRenderPassTo { destination: RenderDestination::Surface, .. } - ); + ) }); let mut frame = if requires_surface { diff --git a/crates/lambda-rs/src/runtimes/application.rs b/crates/lambda-rs/src/runtimes/application.rs index 62e94a4b..0af3de62 100644 --- a/crates/lambda-rs/src/runtimes/application.rs +++ b/crates/lambda-rs/src/runtimes/application.rs @@ -12,6 +12,7 @@ use lambda_platform::winit::{ PhysicalKey as WinitPhysicalKey, WindowEvent as WinitWindowEvent, }, + EventLoopPolicy, LoopBuilder, }; use logging; @@ -52,6 +53,7 @@ pub struct ApplicationRuntimeBuilder { app_name: String, render_context_builder: RenderContextBuilder, window_builder: WindowBuilder, + event_loop_policy: EventLoopPolicy, components: Vec>>, } @@ -62,6 +64,7 @@ impl ApplicationRuntimeBuilder { app_name: app_name.to_string(), render_context_builder: RenderContextBuilder::new(app_name), window_builder: WindowBuilder::new(), + event_loop_policy: EventLoopPolicy::Poll, components: Vec::new(), }; } @@ -95,6 +98,16 @@ impl ApplicationRuntimeBuilder { return self; } + /// Set the winit event loop control-flow policy. + /// + /// - [`EventLoopPolicy::Poll`]: Continuous updates, highest CPU usage, lowest latency + /// - [`EventLoopPolicy::Wait`]: Sleep until events arrive, minimal CPU usage when idle + /// - [`EventLoopPolicy::WaitUntil`]: Wake at a fixed cadence (best effort) + pub fn with_event_loop_policy(mut self, policy: EventLoopPolicy) -> Self { + self.event_loop_policy = policy; + return self; + } + /// Attach a component to the current runnable. pub fn with_component< T: Default + Component + 'static, @@ -116,6 +129,7 @@ impl ApplicationRuntimeBuilder { name: self.app_name, render_context_builder: self.render_context_builder, window_builder: self.window_builder, + event_loop_policy: self.event_loop_policy, component_stack: self.components, } } @@ -127,6 +141,7 @@ pub struct ApplicationRuntime { name: String, render_context_builder: RenderContextBuilder, window_builder: WindowBuilder, + event_loop_policy: EventLoopPolicy, component_stack: Vec>>, } @@ -175,6 +190,7 @@ impl Runtime<(), String> for ApplicationRuntime { /// else relevant to the runtime. fn run(self) -> Result<(), String> { let name = self.name; + let event_loop_policy = self.event_loop_policy; let mut event_loop = LoopBuilder::new().build(); let window = self.window_builder.build(&mut event_loop); let mut component_stack = self.component_stack; @@ -197,269 +213,271 @@ impl Runtime<(), String> for ApplicationRuntime { let mut current_frame = Instant::now(); let mut runtime_result: Box> = Box::new(Ok(())); - event_loop.run_forever(move |event, target| { - let mapped_event: Option = match event { - WinitEvent::WindowEvent { event, .. } => match event { - WinitWindowEvent::CloseRequested => { - // Issue a Shutdown event to deallocate resources and clean up. - target.exit(); - Some(Events::Runtime { - event: RuntimeEvent::Shutdown, - issued_at: Instant::now(), - }) - } - WinitWindowEvent::Resized(dims) => { - active_render_context - .as_mut() - .unwrap() - .resize(dims.width, dims.height); - - Some(Events::Window { - event: WindowEvent::Resize { - width: dims.width, - height: dims.height, + event_loop.run_forever_with_policy( + event_loop_policy, + move |event, target| { + let mapped_event: Option = match event { + WinitEvent::WindowEvent { event, .. } => match event { + WinitWindowEvent::CloseRequested => { + // Issue a Shutdown event to deallocate resources and clean up. + target.exit(); + Some(Events::Runtime { + event: RuntimeEvent::Shutdown, + issued_at: Instant::now(), + }) + } + WinitWindowEvent::Resized(dims) => { + active_render_context + .as_mut() + .unwrap() + .resize(dims.width, dims.height); + + Some(Events::Window { + event: WindowEvent::Resize { + width: dims.width, + height: dims.height, + }, + issued_at: Instant::now(), + }) + } + WinitWindowEvent::ScaleFactorChanged { .. } => None, + WinitWindowEvent::Moved(_) => None, + WinitWindowEvent::Destroyed => None, + WinitWindowEvent::DroppedFile(_) => None, + WinitWindowEvent::HoveredFile(_) => None, + WinitWindowEvent::HoveredFileCancelled => None, + // Character input is delivered via IME; ignore here for now + WinitWindowEvent::Focused(_) => None, + WinitWindowEvent::KeyboardInput { + event: key_event, + is_synthetic, + .. + } => match (key_event.state, is_synthetic) { + (ElementState::Pressed, false) => { + let (scan_code, virtual_key) = match key_event.physical_key { + WinitPhysicalKey::Code(code) => (0, Some(code)), + _ => (0, None), + }; + Some(Events::Keyboard { + event: Key::Pressed { + scan_code, + virtual_key, + }, + issued_at: Instant::now(), + }) + } + (ElementState::Released, false) => { + let (scan_code, virtual_key) = match key_event.physical_key { + WinitPhysicalKey::Code(code) => (0, Some(code)), + _ => (0, None), + }; + Some(Events::Keyboard { + event: Key::Released { + scan_code, + virtual_key, + }, + issued_at: Instant::now(), + }) + } + _ => None, + }, + WinitWindowEvent::ModifiersChanged(_) => None, + WinitWindowEvent::CursorMoved { + device_id: _, + position, + } => Some(Events::Mouse { + event: Mouse::Moved { + x: position.x, + y: position.y, + dx: 0.0, + dy: 0.0, + device_id: 0, }, issued_at: Instant::now(), - }) - } - WinitWindowEvent::ScaleFactorChanged { .. } => None, - WinitWindowEvent::Moved(_) => None, - WinitWindowEvent::Destroyed => None, - WinitWindowEvent::DroppedFile(_) => None, - WinitWindowEvent::HoveredFile(_) => None, - WinitWindowEvent::HoveredFileCancelled => None, - // Character input is delivered via IME; ignore here for now - WinitWindowEvent::Focused(_) => None, - WinitWindowEvent::KeyboardInput { - event: key_event, - is_synthetic, - .. - } => match (key_event.state, is_synthetic) { - (ElementState::Pressed, false) => { - let (scan_code, virtual_key) = match key_event.physical_key { - WinitPhysicalKey::Code(code) => (0, Some(code)), - _ => (0, None), - }; - Some(Events::Keyboard { - event: Key::Pressed { - scan_code, - virtual_key, - }, + }), + WinitWindowEvent::CursorEntered { device_id: _ } => { + Some(Events::Mouse { + event: Mouse::EnteredWindow { device_id: 0 }, issued_at: Instant::now(), }) } - (ElementState::Released, false) => { - let (scan_code, virtual_key) = match key_event.physical_key { - WinitPhysicalKey::Code(code) => (0, Some(code)), - _ => (0, None), + WinitWindowEvent::CursorLeft { device_id: _ } => { + Some(Events::Mouse { + event: Mouse::LeftWindow { device_id: 0 }, + issued_at: Instant::now(), + }) + } + WinitWindowEvent::MouseWheel { + device_id: _, + delta: _, + phase: _, + } => Some(Events::Mouse { + event: Mouse::Scrolled { device_id: 0 }, + issued_at: Instant::now(), + }), + WinitWindowEvent::MouseInput { + device_id: _, + state, + button, + } => { + // Map winit button to our button type + let button = match button { + MouseButton::Left => Button::Left, + MouseButton::Right => Button::Right, + MouseButton::Middle => Button::Middle, + MouseButton::Other(other) => Button::Other(other), + MouseButton::Back => Button::Other(8), + MouseButton::Forward => Button::Other(9), }; - Some(Events::Keyboard { - event: Key::Released { - scan_code, - virtual_key, + + let event = match state { + ElementState::Pressed => Mouse::Pressed { + button, + x: 0.0, + y: 0.0, + device_id: 0, + }, + ElementState::Released => Mouse::Released { + button, + x: 0.0, + y: 0.0, + device_id: 0, }, + }; + + Some(Events::Mouse { + event, issued_at: Instant::now(), }) } + WinitWindowEvent::TouchpadPressure { .. } => None, + WinitWindowEvent::AxisMotion { .. } => None, + WinitWindowEvent::Touch(_) => None, + WinitWindowEvent::ThemeChanged(_) => None, _ => None, }, - WinitWindowEvent::ModifiersChanged(_) => None, - WinitWindowEvent::CursorMoved { - device_id: _, - position, - } => Some(Events::Mouse { - event: Mouse::Moved { - x: position.x, - y: position.y, - dx: 0.0, - dy: 0.0, - device_id: 0, - }, - issued_at: Instant::now(), - }), - WinitWindowEvent::CursorEntered { device_id: _ } => { - Some(Events::Mouse { - event: Mouse::EnteredWindow { device_id: 0 }, - issued_at: Instant::now(), - }) - } - WinitWindowEvent::CursorLeft { device_id: _ } => { - Some(Events::Mouse { - event: Mouse::LeftWindow { device_id: 0 }, - issued_at: Instant::now(), - }) - } - WinitWindowEvent::MouseWheel { - device_id: _, - delta: _, - phase: _, - } => Some(Events::Mouse { - event: Mouse::Scrolled { device_id: 0 }, - issued_at: Instant::now(), - }), - WinitWindowEvent::MouseInput { - device_id: _, - state, - button, - } => { - // Map winit button to our button type - let button = match button { - MouseButton::Left => Button::Left, - MouseButton::Right => Button::Right, - MouseButton::Middle => Button::Middle, - MouseButton::Other(other) => Button::Other(other), - MouseButton::Back => Button::Other(8), - MouseButton::Forward => Button::Other(9), - }; - - let event = match state { - ElementState::Pressed => Mouse::Pressed { - button, - x: 0.0, - y: 0.0, - device_id: 0, - }, - ElementState::Released => Mouse::Released { - button, - x: 0.0, - y: 0.0, - device_id: 0, - }, - }; + WinitEvent::AboutToWait => { + let last_frame = current_frame; + current_frame = Instant::now(); + let duration = ¤t_frame.duration_since(last_frame); - Some(Events::Mouse { - event, - issued_at: Instant::now(), - }) - } - WinitWindowEvent::TouchpadPressure { .. } => None, - WinitWindowEvent::AxisMotion { .. } => None, - WinitWindowEvent::Touch(_) => None, - WinitWindowEvent::ThemeChanged(_) => None, - _ => None, - }, - WinitEvent::AboutToWait => { - let last_frame = current_frame; - current_frame = Instant::now(); - let duration = ¤t_frame.duration_since(last_frame); - - let active_render_context = active_render_context - .as_mut() - .expect("Couldn't get the active render context. "); - for component in &mut component_stack { - let update_result = component.on_update(duration); - if let Err(error) = update_result { - logging::error!("{}", error); - publisher.publish_event(Events::Runtime { - event: RuntimeEvent::ComponentPanic { message: error }, - issued_at: Instant::now(), - }); - continue; + let active_render_context = active_render_context + .as_mut() + .expect("Couldn't get the active render context. "); + for component in &mut component_stack { + let update_result = component.on_update(duration); + if let Err(error) = update_result { + logging::error!("{}", error); + publisher.publish_event(Events::Runtime { + event: RuntimeEvent::ComponentPanic { message: error }, + issued_at: Instant::now(), + }); + continue; + } + let commands = component.on_render(active_render_context); + active_render_context.render(commands); } - let commands = component.on_render(active_render_context); - active_render_context.render(commands); - } - // Warn if frames dropped below 32 ms (30 fps). - match duration.as_millis() > 32 { - true => { + // Warn if frames dropped below 32 ms (30 fps). + if event_loop_policy != EventLoopPolicy::Wait + && duration.as_millis() > 32 + { logging::warn!( "Frame took too long to render: {:?} ms", duration.as_millis() ); } - false => { - // Disable until frametimes can be determined via monitor - // std::thread::sleep(std::time::Duration::from_millis(16 - duration.as_millis() as u64)); - } - } - None - } - // Redraw requests are handled implicitly when AboutToWait fires; ignore explicit requests - WinitEvent::NewEvents(_) => None, - WinitEvent::DeviceEvent { - device_id: _, - event: _, - } => None, - WinitEvent::UserEvent(lambda_event) => match lambda_event { - Events::Runtime { - event, - issued_at: _, - } => match event { - RuntimeEvent::Initialized => { - logging::debug!( - "Initializing all of the components for the runtime: {}", - name - ); - for component in &mut component_stack { - let attach_result = - component.on_attach(active_render_context.as_mut().unwrap()); - if let Err(error) = attach_result { - logging::error!("{}", error); - publisher.publish_event(Events::Runtime { - event: RuntimeEvent::ComponentPanic { message: error }, - issued_at: Instant::now(), - }); + None + } + // Redraw requests are handled implicitly when AboutToWait fires; ignore explicit requests + WinitEvent::NewEvents(_) => None, + WinitEvent::DeviceEvent { + device_id: _, + event: _, + } => None, + WinitEvent::UserEvent(lambda_event) => match lambda_event { + Events::Runtime { + event, + issued_at: _, + } => match event { + RuntimeEvent::Initialized => { + logging::debug!( + "Initializing all of the components for the runtime: {}", + name + ); + for component in &mut component_stack { + let attach_result = component + .on_attach(active_render_context.as_mut().unwrap()); + if let Err(error) = attach_result { + logging::error!("{}", error); + publisher.publish_event(Events::Runtime { + event: RuntimeEvent::ComponentPanic { message: error }, + issued_at: Instant::now(), + }); + } } + None } - None - } - RuntimeEvent::Shutdown => { - for component in &mut component_stack { - let detach_result = - component.on_detach(active_render_context.as_mut().unwrap()); - if let Err(error) = detach_result { - logging::error!("{}", error); - publisher.publish_event(Events::Runtime { - event: RuntimeEvent::ComponentPanic { message: error }, - issued_at: Instant::now(), - }); + RuntimeEvent::Shutdown => { + for component in &mut component_stack { + let detach_result = component + .on_detach(active_render_context.as_mut().unwrap()); + if let Err(error) = detach_result { + logging::error!("{}", error); + publisher.publish_event(Events::Runtime { + event: RuntimeEvent::ComponentPanic { message: error }, + issued_at: Instant::now(), + }); + } } + *runtime_result = Ok(()); + None } - *runtime_result = Ok(()); - None - } - RuntimeEvent::ComponentPanic { message } => { - *runtime_result = Err(message); - None - } + RuntimeEvent::ComponentPanic { message } => { + *runtime_result = Err(message); + None + } + }, + _ => None, }, - _ => None, - }, - WinitEvent::Suspended => None, - WinitEvent::Resumed => None, - WinitEvent::MemoryWarning => None, - // No RedrawEventsCleared in winit 0.29 - WinitEvent::LoopExiting => { - active_render_context - .take() - .expect("[ERROR] The render API has been already taken.") - .destroy(); - - logging::info!("All resources were successfully deleted."); - None - } - }; + WinitEvent::Suspended => None, + WinitEvent::Resumed => None, + WinitEvent::MemoryWarning => None, + // No RedrawEventsCleared in winit 0.29 + WinitEvent::LoopExiting => { + active_render_context + .take() + .expect("[ERROR] The render API has been already taken.") + .destroy(); - if let Some(event) = mapped_event { - logging::trace!("Sending event: {:?} to all components", event); + logging::info!("All resources were successfully deleted."); + None + } + }; - let event_mask = event.mask(); - for component in &mut component_stack { - let event_result = - dispatch_event_to_component(&event, event_mask, component.as_mut()); + if let Some(event) = mapped_event { + logging::trace!("Sending event: {:?} to all components", event); - if let Err(error) = event_result { - logging::error!("{}", error); - publisher.publish_event(Events::Runtime { - event: RuntimeEvent::ComponentPanic { message: error }, - issued_at: Instant::now(), - }); + let event_mask = event.mask(); + for component in &mut component_stack { + let event_result = dispatch_event_to_component( + &event, + event_mask, + component.as_mut(), + ); + + if let Err(error) = event_result { + logging::error!("{}", error); + publisher.publish_event(Events::Runtime { + event: RuntimeEvent::ComponentPanic { message: error }, + issued_at: Instant::now(), + }); + } } } - } - }); + }, + ); return Ok(()); } diff --git a/crates/lambda-rs/src/runtimes/mod.rs b/crates/lambda-rs/src/runtimes/mod.rs index 4968d88b..562ee0d4 100644 --- a/crates/lambda-rs/src/runtimes/mod.rs +++ b/crates/lambda-rs/src/runtimes/mod.rs @@ -6,3 +6,4 @@ pub use application::{ ApplicationRuntime, ApplicationRuntimeBuilder, }; +pub use lambda_platform::winit::EventLoopPolicy; From fa9f48e4a2a752ddafe800086d25169e5ce88a7e Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 10 Feb 2026 13:14:37 -0800 Subject: [PATCH 2/3] [update] how frame warning thresholds are computed. --- crates/lambda-rs/src/runtimes/application.rs | 38 +++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/crates/lambda-rs/src/runtimes/application.rs b/crates/lambda-rs/src/runtimes/application.rs index 0af3de62..1dc1d04f 100644 --- a/crates/lambda-rs/src/runtimes/application.rs +++ b/crates/lambda-rs/src/runtimes/application.rs @@ -2,7 +2,10 @@ //! provides a window and a render context which can be used to render //! both 2D and 3D graphics to the screen. -use std::time::Instant; +use std::time::{ + Duration, + Instant, +}; use lambda_platform::winit::{ winit_exports::{ @@ -191,6 +194,18 @@ impl Runtime<(), String> for ApplicationRuntime { fn run(self) -> Result<(), String> { let name = self.name; let event_loop_policy = self.event_loop_policy; + let frame_warn_threshold: Option = match event_loop_policy { + EventLoopPolicy::Poll => Some(Duration::from_millis(32)), + EventLoopPolicy::Wait => None, + EventLoopPolicy::WaitUntil { target_fps } if target_fps > 0 => { + // Compute an expected frame interval (1 / FPS) and warn only if the + // observed frame time exceeds it by a slack factor (25%) to avoid + // spamming on small scheduling jitter. + let expected_secs = 1.0 / target_fps as f64; + Some(Duration::from_secs_f64(expected_secs * 1.25)) + } + EventLoopPolicy::WaitUntil { .. } => None, + }; let mut event_loop = LoopBuilder::new().build(); let window = self.window_builder.build(&mut event_loop); let mut component_stack = self.component_stack; @@ -378,14 +393,19 @@ impl Runtime<(), String> for ApplicationRuntime { active_render_context.render(commands); } - // Warn if frames dropped below 32 ms (30 fps). - if event_loop_policy != EventLoopPolicy::Wait - && duration.as_millis() > 32 - { - logging::warn!( - "Frame took too long to render: {:?} ms", - duration.as_millis() - ); + // Warn if the time between frames significantly exceeds the expected + // interval for the selected event loop policy. + // + // - Poll: uses a fixed 32 ms threshold (~30 fps). + // - WaitUntil: uses a threshold derived from the target FPS. + // - Wait: disabled (duration includes idle sleep time). + if let Some(threshold) = frame_warn_threshold { + if *duration > threshold { + logging::warn!( + "Frame took too long to render: {:?} ms", + duration.as_millis() + ); + } } None From ae80c458043f15d76de5874bccbe4c01546947ff Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 10 Feb 2026 13:33:59 -0800 Subject: [PATCH 3/3] [add] clamp to 1000 FPS for WaitUntil policies and ensure that the deadline is always greater than now. --- crates/lambda-rs-platform/src/winit/mod.rs | 40 +++++++++++++++----- crates/lambda-rs/src/runtimes/application.rs | 17 ++++++++- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/crates/lambda-rs-platform/src/winit/mod.rs b/crates/lambda-rs-platform/src/winit/mod.rs index dba3c407..f75147c8 100644 --- a/crates/lambda-rs-platform/src/winit/mod.rs +++ b/crates/lambda-rs-platform/src/winit/mod.rs @@ -67,6 +67,32 @@ pub enum EventLoopPolicy { WaitUntil { target_fps: u32 }, } +const MAX_TARGET_FPS: u32 = 1000; + +fn div_ceil_u64(numerator: u64, denominator: u64) -> u64 { + let div = numerator / denominator; + let rem = numerator % denominator; + if rem == 0 { + return div; + } + return div + 1; +} + +fn frame_interval_for_target_fps(target_fps: u32) -> Option { + if target_fps == 0 { + return None; + } + + // Clamp to a sane max to avoid impractically small intervals (which can + // busy-loop or require large catch-up work after sleeps). + let clamped_fps = target_fps.min(MAX_TARGET_FPS) as u64; + + // Compute a non-zero interval in integer nanoseconds (ceil to ensure at + // least 1ns). + let nanos_per_frame = div_ceil_u64(1_000_000_000, clamped_fps); + return Some(Duration::from_nanos(nanos_per_frame)); +} + /// LoopBuilder - Putting this here for consistency. pub struct LoopBuilder; @@ -268,8 +294,8 @@ impl Loop { Callback: 'static + FnMut(Event, &EventLoopWindowTarget), { let frame_interval = match policy { - EventLoopPolicy::WaitUntil { target_fps } if target_fps > 0 => { - Some(Duration::from_secs_f64(1.0 / target_fps as f64)) + EventLoopPolicy::WaitUntil { target_fps } => { + frame_interval_for_target_fps(target_fps) } _ => None, }; @@ -292,14 +318,10 @@ impl Loop { let now = Instant::now(); let interval = frame_interval.unwrap_or(Duration::from_secs(1)); + // Guarantee the deadline always advances and stays in the future. let deadline = match next_frame_deadline { - Some(mut deadline) => { - while deadline <= now { - deadline += interval; - } - deadline - } - None => now + interval, + Some(deadline) if deadline > now => deadline, + _ => now + interval, }; next_frame_deadline = Some(deadline); diff --git a/crates/lambda-rs/src/runtimes/application.rs b/crates/lambda-rs/src/runtimes/application.rs index 1dc1d04f..df63e58c 100644 --- a/crates/lambda-rs/src/runtimes/application.rs +++ b/crates/lambda-rs/src/runtimes/application.rs @@ -186,6 +186,17 @@ fn dispatch_event_to_component( } } +const MAX_TARGET_FPS: u32 = 1000; + +fn div_ceil_u64(numerator: u64, denominator: u64) -> u64 { + let div = numerator / denominator; + let rem = numerator % denominator; + if rem == 0 { + return div; + } + return div + 1; +} + impl Runtime<(), String> for ApplicationRuntime { type Component = Box>; /// Runs the event loop for the Application Runtime which takes ownership @@ -201,8 +212,10 @@ impl Runtime<(), String> for ApplicationRuntime { // Compute an expected frame interval (1 / FPS) and warn only if the // observed frame time exceeds it by a slack factor (25%) to avoid // spamming on small scheduling jitter. - let expected_secs = 1.0 / target_fps as f64; - Some(Duration::from_secs_f64(expected_secs * 1.25)) + let clamped_fps = target_fps.min(MAX_TARGET_FPS) as u64; + let nanos_per_frame = div_ceil_u64(1_000_000_000, clamped_fps); + let expected_interval = Duration::from_nanos(nanos_per_frame); + Some(expected_interval.mul_f64(1.25)) } EventLoopPolicy::WaitUntil { .. } => None, };