Skip to content

Commit a4b5f9e

Browse files
authored
feat(lambda-rs): Add policy to control the event loops control-flow (#182)
## Summary Add an opt-in, configurable winit event loop control-flow policy so apps that don’t need continuous rendering (tools/editors) can avoid `ControlFlow::Poll` and reduce CPU/power usage when idle. ## Related Issues - Resolves #98 ## Changes - Add `EventLoopPolicy` (`Poll`, `Wait`, `WaitUntil { target_fps }`) to the winit wrapper and implement `Loop::run_forever_with_policy(...)`. - Add `ApplicationRuntimeBuilder::with_event_loop_policy(...)` (defaults to `Poll` for backwards compatibility) and wire it into `ApplicationRuntime` execution. - Re-export `EventLoopPolicy` from `lambda::runtimes` for easy consumption. - Update docs + minimal example to demonstrate `EventLoopPolicy::Wait` for event-driven apps. - Fix nightly clippy `never_loop` lint failure by removing an unnecessary `return` inside an iterator adapter closure. ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] Feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation (updates to docs, specs, tutorials, or comments) - [ ] Refactor (code change that neither fixes a bug nor adds a feature) - [x] Performance (change that improves performance) - [ ] Test (adding or updating tests) - [ ] Build/CI (changes to build process or CI configuration) ## Affected Crates - [x] `lambda-rs` - [x] `lambda-rs-platform` - [ ] `lambda-rs-args` - [ ] `lambda-rs-logging` - [ ] Other: ## Checklist - [x] Code follows the repository style guidelines (`cargo +nightly fmt --all`) - [x] Code passes clippy (`cargo clippy --workspace --all-targets -- -D warnings`) - [x] Tests pass (`cargo test --workspace`) - [x] New code includes appropriate documentation - [x] Public API changes are documented - [ ] Breaking changes are noted in this PR description ## Testing **Commands run:** ```bash cargo +nightly fmt --all rustup run nightly cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace ``` **Manual verification steps (if applicable):** 1. (Optional) Run an example app and verify CPU usage drops when idle with `EventLoopPolicy::Wait`. ## Screenshots/Recordings N/A ## Platform Testing - [x] macOS - [ ] Windows - [ ] Linux ## Additional Notes - Default remains `Poll` to avoid behavior changes for existing applications unless they opt in. - `WaitUntil` is best-effort wakeup scheduling (control-flow only), not frame pacing/vsync.
2 parents d93fa76 + ae80c45 commit a4b5f9e

6 files changed

Lines changed: 398 additions & 245 deletions

File tree

crates/lambda-rs-platform/src/winit/mod.rs

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
//! Winit wrapper to easily construct cross platform windows
22
3+
use std::time::{
4+
Duration,
5+
Instant,
6+
};
7+
38
use winit::{
49
dpi::{
510
LogicalSize,
@@ -44,6 +49,50 @@ pub mod winit_exports {
4449
};
4550
}
4651

52+
/// Control flow policy for the winit event loop.
53+
///
54+
/// Lambda defaults to [`EventLoopPolicy::Poll`] for backwards compatibility.
55+
/// Applications that don't require continuous updates (e.g., editors/tools)
56+
/// should prefer [`EventLoopPolicy::Wait`] to reduce CPU usage when idle.
57+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58+
pub enum EventLoopPolicy {
59+
/// Continuous polling for games and real-time applications.
60+
Poll,
61+
/// Sleep until events arrive; ideal for tools and editors.
62+
Wait,
63+
/// Sleep until the next frame deadline to target a fixed update rate.
64+
///
65+
/// Note: this is not a frame-pacing / vsync guarantee; it only controls how
66+
/// long the event loop waits between wakeups.
67+
WaitUntil { target_fps: u32 },
68+
}
69+
70+
const MAX_TARGET_FPS: u32 = 1000;
71+
72+
fn div_ceil_u64(numerator: u64, denominator: u64) -> u64 {
73+
let div = numerator / denominator;
74+
let rem = numerator % denominator;
75+
if rem == 0 {
76+
return div;
77+
}
78+
return div + 1;
79+
}
80+
81+
fn frame_interval_for_target_fps(target_fps: u32) -> Option<Duration> {
82+
if target_fps == 0 {
83+
return None;
84+
}
85+
86+
// Clamp to a sane max to avoid impractically small intervals (which can
87+
// busy-loop or require large catch-up work after sleeps).
88+
let clamped_fps = target_fps.min(MAX_TARGET_FPS) as u64;
89+
90+
// Compute a non-zero interval in integer nanoseconds (ceil to ensure at
91+
// least 1ns).
92+
let nanos_per_frame = div_ceil_u64(1_000_000_000, clamped_fps);
93+
return Some(Duration::from_nanos(nanos_per_frame));
94+
}
95+
4796
/// LoopBuilder - Putting this here for consistency.
4897
pub struct LoopBuilder;
4998

@@ -228,14 +277,58 @@ impl<E: 'static + std::fmt::Debug> Loop<E> {
228277
}
229278

230279
/// Uses the winit event loop to run forever
231-
pub fn run_forever<Callback>(self, mut callback: Callback)
280+
pub fn run_forever<Callback>(self, callback: Callback)
232281
where
233282
Callback: 'static + FnMut(Event<E>, &EventLoopWindowTarget<E>),
234283
{
284+
self.run_forever_with_policy(EventLoopPolicy::Poll, callback);
285+
}
286+
287+
/// Uses the winit event loop to run forever with the provided control-flow
288+
/// policy.
289+
pub fn run_forever_with_policy<Callback>(
290+
self,
291+
policy: EventLoopPolicy,
292+
mut callback: Callback,
293+
) where
294+
Callback: 'static + FnMut(Event<E>, &EventLoopWindowTarget<E>),
295+
{
296+
let frame_interval = match policy {
297+
EventLoopPolicy::WaitUntil { target_fps } => {
298+
frame_interval_for_target_fps(target_fps)
299+
}
300+
_ => None,
301+
};
302+
let mut next_frame_deadline: Option<Instant> = None;
303+
235304
self
236305
.event_loop
237306
.run(move |event, target| {
238-
target.set_control_flow(ControlFlow::Poll);
307+
match policy {
308+
EventLoopPolicy::Poll => {
309+
target.set_control_flow(ControlFlow::Poll);
310+
}
311+
EventLoopPolicy::Wait => {
312+
target.set_control_flow(ControlFlow::Wait);
313+
}
314+
EventLoopPolicy::WaitUntil { target_fps: 0 } => {
315+
target.set_control_flow(ControlFlow::Wait);
316+
}
317+
EventLoopPolicy::WaitUntil { .. } => {
318+
let now = Instant::now();
319+
let interval = frame_interval.unwrap_or(Duration::from_secs(1));
320+
321+
// Guarantee the deadline always advances and stays in the future.
322+
let deadline = match next_frame_deadline {
323+
Some(deadline) if deadline > now => deadline,
324+
_ => now + interval,
325+
};
326+
327+
next_frame_deadline = Some(deadline);
328+
target.set_control_flow(ControlFlow::WaitUntil(deadline));
329+
}
330+
}
331+
239332
callback(event, target);
240333
})
241334
.expect("Event loop terminated unexpectedly");

crates/lambda-rs/README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@ cargo add lambda-rs
1616
## First window
1717
Getting started with lambda is easy. The following example will create a window with the title "Hello lambda!" and a size of 800x600.
1818
```rust
19-
#[macro_use]
2019
use lambda::{
21-
core::runtime::start_runtime,
22-
runtimes::ApplicationRuntimeBuilder,
20+
runtime::start_runtime,
21+
runtimes::{
22+
ApplicationRuntimeBuilder,
23+
EventLoopPolicy,
24+
},
2325
};
2426

2527
fn main() {
2628
let runtime = ApplicationRuntimeBuilder::new("Hello lambda!")
29+
// Tools/editors should prefer `Wait` to reduce CPU usage when idle.
30+
.with_event_loop_policy(EventLoopPolicy::Wait)
2731
.with_window_configured_as(move |window_builder| {
2832
return window_builder
2933
.with_dimensions(800, 600)

crates/lambda-rs/examples/minimal.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77
use lambda::{
88
render::PresentMode,
99
runtime::start_runtime,
10-
runtimes::ApplicationRuntimeBuilder,
10+
runtimes::{
11+
ApplicationRuntimeBuilder,
12+
EventLoopPolicy,
13+
},
1114
};
1215

1316
fn main() {
1417
let runtime = ApplicationRuntimeBuilder::new("Minimal Demo application")
18+
.with_event_loop_policy(EventLoopPolicy::Wait)
1519
.with_window_configured_as(move |window_builder| {
1620
return window_builder
1721
.with_dimensions(800, 600)

crates/lambda-rs/src/render/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -574,14 +574,14 @@ impl RenderContext {
574574
// surface. We only acquire a surface frame when a surface-backed pass is
575575
// requested; offscreen-only command lists can render without a window.
576576
let requires_surface = commands.iter().any(|cmd| {
577-
return matches!(
577+
matches!(
578578
cmd,
579579
RenderCommand::BeginRenderPass { .. }
580580
| RenderCommand::BeginRenderPassTo {
581581
destination: RenderDestination::Surface,
582582
..
583583
}
584-
);
584+
)
585585
});
586586

587587
let mut frame = if requires_surface {

0 commit comments

Comments
 (0)