Skip to content

Commit 8191149

Browse files
authored
feat(lambda-rs): Expose vsync and present modes in RenderContextBuilder
2 parents cf3f472 + 8a9ec6b commit 8191149

File tree

4 files changed

+172
-23
lines changed

4 files changed

+172
-23
lines changed

crates/lambda-rs-platform/src/wgpu/surface.rs

Lines changed: 85 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -261,21 +261,10 @@ impl<'window> Surface<'window> {
261261
.unwrap_or_else(|| *capabilities.formats.first().unwrap());
262262

263263
let requested_present_mode = present_mode.to_wgpu();
264-
config.present_mode = if capabilities
265-
.present_modes
266-
.contains(&requested_present_mode)
267-
{
268-
requested_present_mode
269-
} else {
270-
capabilities
271-
.present_modes
272-
.iter()
273-
.copied()
274-
.find(|mode| {
275-
matches!(mode, wgpu::PresentMode::Fifo | wgpu::PresentMode::AutoVsync)
276-
})
277-
.unwrap_or(wgpu::PresentMode::Fifo)
278-
};
264+
config.present_mode = select_present_mode(
265+
requested_present_mode,
266+
capabilities.present_modes.as_slice(),
267+
);
279268

280269
if capabilities.usages.contains(usage.to_wgpu()) {
281270
config.usage = usage.to_wgpu();
@@ -321,6 +310,54 @@ impl<'window> Surface<'window> {
321310
}
322311
}
323312

313+
fn select_present_mode(
314+
requested: wgpu::PresentMode,
315+
available: &[wgpu::PresentMode],
316+
) -> wgpu::PresentMode {
317+
if available.contains(&requested) {
318+
return requested;
319+
}
320+
321+
let candidates: &[wgpu::PresentMode] = match requested {
322+
wgpu::PresentMode::Immediate | wgpu::PresentMode::AutoNoVsync => &[
323+
wgpu::PresentMode::Immediate,
324+
wgpu::PresentMode::Mailbox,
325+
wgpu::PresentMode::AutoNoVsync,
326+
wgpu::PresentMode::Fifo,
327+
wgpu::PresentMode::AutoVsync,
328+
],
329+
wgpu::PresentMode::Mailbox => &[
330+
wgpu::PresentMode::Mailbox,
331+
wgpu::PresentMode::Fifo,
332+
wgpu::PresentMode::AutoVsync,
333+
],
334+
wgpu::PresentMode::FifoRelaxed => &[
335+
wgpu::PresentMode::FifoRelaxed,
336+
wgpu::PresentMode::Fifo,
337+
wgpu::PresentMode::AutoVsync,
338+
],
339+
wgpu::PresentMode::Fifo | wgpu::PresentMode::AutoVsync => &[
340+
wgpu::PresentMode::Fifo,
341+
wgpu::PresentMode::AutoVsync,
342+
wgpu::PresentMode::FifoRelaxed,
343+
wgpu::PresentMode::Mailbox,
344+
wgpu::PresentMode::Immediate,
345+
wgpu::PresentMode::AutoNoVsync,
346+
],
347+
};
348+
349+
for candidate in candidates {
350+
if available.contains(candidate) {
351+
return *candidate;
352+
}
353+
}
354+
355+
return available
356+
.first()
357+
.copied()
358+
.unwrap_or(wgpu::PresentMode::Fifo);
359+
}
360+
324361
/// A single acquired frame and its default `TextureView`.
325362
#[derive(Debug)]
326363
pub struct Frame {
@@ -345,3 +382,36 @@ impl Frame {
345382
self.texture.present();
346383
}
347384
}
385+
386+
#[cfg(test)]
387+
mod tests {
388+
use super::*;
389+
390+
#[test]
391+
fn select_present_mode_prefers_requested() {
392+
let available = &[wgpu::PresentMode::Fifo, wgpu::PresentMode::Immediate];
393+
let selected = select_present_mode(wgpu::PresentMode::Immediate, available);
394+
assert_eq!(selected, wgpu::PresentMode::Immediate);
395+
}
396+
397+
#[test]
398+
fn select_present_mode_falls_back_from_immediate_to_mailbox() {
399+
let available = &[wgpu::PresentMode::Fifo, wgpu::PresentMode::Mailbox];
400+
let selected = select_present_mode(wgpu::PresentMode::Immediate, available);
401+
assert_eq!(selected, wgpu::PresentMode::Mailbox);
402+
}
403+
404+
#[test]
405+
fn select_present_mode_falls_back_from_mailbox_to_fifo() {
406+
let available = &[wgpu::PresentMode::Fifo, wgpu::PresentMode::Immediate];
407+
let selected = select_present_mode(wgpu::PresentMode::Mailbox, available);
408+
assert_eq!(selected, wgpu::PresentMode::Fifo);
409+
}
410+
411+
#[test]
412+
fn select_present_mode_uses_auto_no_vsync_when_available() {
413+
let available = &[wgpu::PresentMode::AutoNoVsync, wgpu::PresentMode::Fifo];
414+
let selected = select_present_mode(wgpu::PresentMode::Immediate, available);
415+
assert_eq!(selected, wgpu::PresentMode::AutoNoVsync);
416+
}
417+
}

crates/lambda-rs/examples/minimal.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//! applications correctly.
66
77
use lambda::{
8+
render::PresentMode,
89
runtime::start_runtime,
910
runtimes::ApplicationRuntimeBuilder,
1011
};
@@ -16,6 +17,9 @@ fn main() {
1617
.with_dimensions(800, 600)
1718
.with_name("Minimal window");
1819
})
20+
.with_renderer_configured_as(|render_context_builder| {
21+
return render_context_builder.with_present_mode(PresentMode::Mailbox);
22+
})
1923
.build();
2024

2125
start_runtime(runtime);

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

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,27 @@ use self::{
6969
targets::surface::RenderTarget,
7070
};
7171

72+
/// High-level presentation mode selection for window surfaces.
73+
///
74+
/// The selected mode is validated against the adapter's surface capabilities
75+
/// during `RenderContextBuilder::build`. If the requested mode is not
76+
/// supported, Lambda selects a supported fallback.
77+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78+
pub enum PresentMode {
79+
/// VSync enabled, capped to display refresh rate (FIFO).
80+
Vsync,
81+
/// VSync disabled, immediate presentation (may tear).
82+
Immediate,
83+
/// Triple buffering, low latency without tearing if supported.
84+
Mailbox,
85+
}
86+
87+
impl Default for PresentMode {
88+
fn default() -> Self {
89+
return PresentMode::Vsync;
90+
}
91+
}
92+
7293
/// Builder for configuring a `RenderContext` tied to one window.
7394
///
7495
/// Purpose
@@ -90,6 +111,7 @@ pub struct RenderContextBuilder {
90111
/// Reserved for future timeout handling during rendering (nanoseconds).
91112
/// Not currently enforced; kept for forward compatibility with runtime controls.
92113
_render_timeout: u64,
114+
present_mode: Option<PresentMode>,
93115
}
94116

95117
impl RenderContextBuilder {
@@ -98,6 +120,7 @@ impl RenderContextBuilder {
98120
Self {
99121
name: name.to_string(),
100122
_render_timeout: 1_000_000_000,
123+
present_mode: None,
101124
}
102125
}
103126

@@ -107,6 +130,31 @@ impl RenderContextBuilder {
107130
self
108131
}
109132

133+
/// Enable or disable vertical sync.
134+
///
135+
/// When enabled, the builder requests `PresentMode::Vsync` (FIFO).
136+
///
137+
/// When disabled, the builder requests a non‑vsync mode (immediate
138+
/// presentation) and falls back to a supported low-latency mode if needed.
139+
pub fn with_vsync(mut self, enabled: bool) -> Self {
140+
self.present_mode = Some(if enabled {
141+
PresentMode::Vsync
142+
} else {
143+
PresentMode::Immediate
144+
});
145+
return self;
146+
}
147+
148+
/// Explicitly select a presentation mode.
149+
///
150+
/// The requested mode is validated against the adapter's surface
151+
/// capabilities. If unsupported, the renderer falls back to a supported
152+
/// mode with similar behavior.
153+
pub fn with_present_mode(mut self, mode: PresentMode) -> Self {
154+
self.present_mode = Some(mode);
155+
return self;
156+
}
157+
110158
/// Build a `RenderContext` for the provided `window` and configure the
111159
/// presentation surface.
112160
///
@@ -116,7 +164,9 @@ impl RenderContextBuilder {
116164
self,
117165
window: &window::Window,
118166
) -> Result<RenderContext, RenderContextError> {
119-
let RenderContextBuilder { name, .. } = self;
167+
let RenderContextBuilder {
168+
name, present_mode, ..
169+
} = self;
120170

121171
let instance = instance::InstanceBuilder::new()
122172
.with_label(&format!("{} Instance", name))
@@ -141,11 +191,22 @@ impl RenderContextBuilder {
141191
})?;
142192

143193
let size = window.dimensions();
194+
let requested_present_mode = present_mode.unwrap_or_else(|| {
195+
if window.vsync_requested() {
196+
return PresentMode::Vsync;
197+
}
198+
return PresentMode::Immediate;
199+
});
200+
let platform_present_mode = match requested_present_mode {
201+
PresentMode::Vsync => targets::surface::PresentMode::Fifo,
202+
PresentMode::Immediate => targets::surface::PresentMode::Immediate,
203+
PresentMode::Mailbox => targets::surface::PresentMode::Mailbox,
204+
};
144205
surface
145206
.configure_with_defaults(
146207
&gpu,
147208
size,
148-
targets::surface::PresentMode::default(),
209+
platform_present_mode,
149210
texture::TextureUsages::RENDER_ATTACHMENT,
150211
)
151212
.map_err(|e| {

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ impl WindowBuilder {
3636
return Self {
3737
name: String::from("Window"),
3838
dimensions: (480, 360),
39-
vsync: false,
39+
vsync: true,
4040
};
4141
}
4242

@@ -54,29 +54,35 @@ impl WindowBuilder {
5454

5555
/// Request vertical sync behavior for the swapchain.
5656
///
57-
/// Note: present mode is ultimately selected when configuring the rendering
58-
/// surface in `RenderContextBuilder`. This flag is reserved to influence
59-
/// that choice and is currently a no‑op.
57+
/// This value is consumed when building a `RenderContext` if no explicit
58+
/// present mode is provided to `RenderContextBuilder`.
6059
pub fn with_vsync(mut self, vsync: bool) -> Self {
6160
self.vsync = vsync;
6261
return self;
6362
}
6463

6564
// TODO(vmarcella): Remove new call for window and construct the window directly.
6665
pub fn build(self, event_loop: &mut Loop<Events>) -> Window {
67-
return Window::new(self.name.as_str(), self.dimensions, event_loop);
66+
return Window::new(
67+
self.name.as_str(),
68+
self.dimensions,
69+
self.vsync,
70+
event_loop,
71+
);
6872
}
6973
}
7074

7175
/// Window implementation for rendering applications.
7276
pub struct Window {
7377
window_handle: WindowHandle,
78+
vsync: bool,
7479
}
7580

7681
impl Window {
7782
fn new(
7883
name: &str,
7984
dimensions: (u32, u32),
85+
vsync: bool,
8086
event_loop: &mut Loop<Events>,
8187
) -> Self {
8288
let window_properties = WindowProperties {
@@ -89,7 +95,10 @@ impl Window {
8995
.build();
9096

9197
logging::debug!("Created window: {}", name);
92-
return Self { window_handle };
98+
return Self {
99+
window_handle,
100+
vsync,
101+
};
93102
}
94103

95104
/// Redraws the window.
@@ -109,4 +118,9 @@ impl Window {
109118
self.window_handle.size.height,
110119
);
111120
}
121+
122+
/// Returns the requested vertical sync preference for presentation.
123+
pub fn vsync_requested(&self) -> bool {
124+
return self.vsync;
125+
}
112126
}

0 commit comments

Comments
 (0)