Skip to content

Commit 02443e8

Browse files
committed
[update] the render context to support configuring vsync and present modes.
1 parent cf3f472 commit 02443e8

File tree

4 files changed

+185
-23
lines changed

4 files changed

+185
-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: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,33 @@ 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+
93+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94+
enum PresentModeOverride {
95+
Vsync(bool),
96+
Explicit(PresentMode),
97+
}
98+
7299
/// Builder for configuring a `RenderContext` tied to one window.
73100
///
74101
/// Purpose
@@ -90,6 +117,7 @@ pub struct RenderContextBuilder {
90117
/// Reserved for future timeout handling during rendering (nanoseconds).
91118
/// Not currently enforced; kept for forward compatibility with runtime controls.
92119
_render_timeout: u64,
120+
present_mode: Option<PresentModeOverride>,
93121
}
94122

95123
impl RenderContextBuilder {
@@ -98,6 +126,7 @@ impl RenderContextBuilder {
98126
Self {
99127
name: name.to_string(),
100128
_render_timeout: 1_000_000_000,
129+
present_mode: None,
101130
}
102131
}
103132

@@ -107,6 +136,27 @@ impl RenderContextBuilder {
107136
self
108137
}
109138

139+
/// Enable or disable vertical sync.
140+
///
141+
/// When enabled, the builder requests `PresentMode::Vsync` (FIFO).
142+
///
143+
/// When disabled, the builder requests a non‑vsync mode (immediate
144+
/// presentation) and falls back to a supported low-latency mode if needed.
145+
pub fn with_vsync(mut self, enabled: bool) -> Self {
146+
self.present_mode = Some(PresentModeOverride::Vsync(enabled));
147+
return self;
148+
}
149+
150+
/// Explicitly select a presentation mode.
151+
///
152+
/// The requested mode is validated against the adapter's surface
153+
/// capabilities. If unsupported, the renderer falls back to a supported
154+
/// mode with similar behavior.
155+
pub fn with_present_mode(mut self, mode: PresentMode) -> Self {
156+
self.present_mode = Some(PresentModeOverride::Explicit(mode));
157+
return self;
158+
}
159+
110160
/// Build a `RenderContext` for the provided `window` and configure the
111161
/// presentation surface.
112162
///
@@ -116,7 +166,9 @@ impl RenderContextBuilder {
116166
self,
117167
window: &window::Window,
118168
) -> Result<RenderContext, RenderContextError> {
119-
let RenderContextBuilder { name, .. } = self;
169+
let RenderContextBuilder {
170+
name, present_mode, ..
171+
} = self;
120172

121173
let instance = instance::InstanceBuilder::new()
122174
.with_label(&format!("{} Instance", name))
@@ -141,11 +193,33 @@ impl RenderContextBuilder {
141193
})?;
142194

143195
let size = window.dimensions();
196+
let requested_present_mode = match present_mode {
197+
Some(PresentModeOverride::Vsync(enabled)) => {
198+
if enabled {
199+
PresentMode::Vsync
200+
} else {
201+
PresentMode::Immediate
202+
}
203+
}
204+
Some(PresentModeOverride::Explicit(mode)) => mode,
205+
None => {
206+
if window.vsync_requested() {
207+
PresentMode::Vsync
208+
} else {
209+
PresentMode::Immediate
210+
}
211+
}
212+
};
213+
let platform_present_mode = match requested_present_mode {
214+
PresentMode::Vsync => targets::surface::PresentMode::Fifo,
215+
PresentMode::Immediate => targets::surface::PresentMode::Immediate,
216+
PresentMode::Mailbox => targets::surface::PresentMode::Mailbox,
217+
};
144218
surface
145219
.configure_with_defaults(
146220
&gpu,
147221
size,
148-
targets::surface::PresentMode::default(),
222+
platform_present_mode,
149223
texture::TextureUsages::RENDER_ATTACHMENT,
150224
)
151225
.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)