From 02443e800a2d70c11d13f8ddd6c945e9add20c52 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 26 Jan 2026 14:42:44 -0800 Subject: [PATCH 1/2] [update] the render context to support configuring vsync and present modes. --- crates/lambda-rs-platform/src/wgpu/surface.rs | 100 +++++++++++++++--- crates/lambda-rs/examples/minimal.rs | 4 + crates/lambda-rs/src/render/mod.rs | 78 +++++++++++++- crates/lambda-rs/src/render/window.rs | 26 +++-- 4 files changed, 185 insertions(+), 23 deletions(-) diff --git a/crates/lambda-rs-platform/src/wgpu/surface.rs b/crates/lambda-rs-platform/src/wgpu/surface.rs index a97338f0..77017f5c 100644 --- a/crates/lambda-rs-platform/src/wgpu/surface.rs +++ b/crates/lambda-rs-platform/src/wgpu/surface.rs @@ -261,21 +261,10 @@ impl<'window> Surface<'window> { .unwrap_or_else(|| *capabilities.formats.first().unwrap()); let requested_present_mode = present_mode.to_wgpu(); - config.present_mode = if capabilities - .present_modes - .contains(&requested_present_mode) - { - requested_present_mode - } else { - capabilities - .present_modes - .iter() - .copied() - .find(|mode| { - matches!(mode, wgpu::PresentMode::Fifo | wgpu::PresentMode::AutoVsync) - }) - .unwrap_or(wgpu::PresentMode::Fifo) - }; + config.present_mode = select_present_mode( + requested_present_mode, + capabilities.present_modes.as_slice(), + ); if capabilities.usages.contains(usage.to_wgpu()) { config.usage = usage.to_wgpu(); @@ -321,6 +310,54 @@ impl<'window> Surface<'window> { } } +fn select_present_mode( + requested: wgpu::PresentMode, + available: &[wgpu::PresentMode], +) -> wgpu::PresentMode { + if available.contains(&requested) { + return requested; + } + + let candidates: &[wgpu::PresentMode] = match requested { + wgpu::PresentMode::Immediate | wgpu::PresentMode::AutoNoVsync => &[ + wgpu::PresentMode::Immediate, + wgpu::PresentMode::Mailbox, + wgpu::PresentMode::AutoNoVsync, + wgpu::PresentMode::Fifo, + wgpu::PresentMode::AutoVsync, + ], + wgpu::PresentMode::Mailbox => &[ + wgpu::PresentMode::Mailbox, + wgpu::PresentMode::Fifo, + wgpu::PresentMode::AutoVsync, + ], + wgpu::PresentMode::FifoRelaxed => &[ + wgpu::PresentMode::FifoRelaxed, + wgpu::PresentMode::Fifo, + wgpu::PresentMode::AutoVsync, + ], + wgpu::PresentMode::Fifo | wgpu::PresentMode::AutoVsync => &[ + wgpu::PresentMode::Fifo, + wgpu::PresentMode::AutoVsync, + wgpu::PresentMode::FifoRelaxed, + wgpu::PresentMode::Mailbox, + wgpu::PresentMode::Immediate, + wgpu::PresentMode::AutoNoVsync, + ], + }; + + for candidate in candidates { + if available.contains(candidate) { + return *candidate; + } + } + + return available + .first() + .copied() + .unwrap_or(wgpu::PresentMode::Fifo); +} + /// A single acquired frame and its default `TextureView`. #[derive(Debug)] pub struct Frame { @@ -345,3 +382,36 @@ impl Frame { self.texture.present(); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn select_present_mode_prefers_requested() { + let available = &[wgpu::PresentMode::Fifo, wgpu::PresentMode::Immediate]; + let selected = select_present_mode(wgpu::PresentMode::Immediate, available); + assert_eq!(selected, wgpu::PresentMode::Immediate); + } + + #[test] + fn select_present_mode_falls_back_from_immediate_to_mailbox() { + let available = &[wgpu::PresentMode::Fifo, wgpu::PresentMode::Mailbox]; + let selected = select_present_mode(wgpu::PresentMode::Immediate, available); + assert_eq!(selected, wgpu::PresentMode::Mailbox); + } + + #[test] + fn select_present_mode_falls_back_from_mailbox_to_fifo() { + let available = &[wgpu::PresentMode::Fifo, wgpu::PresentMode::Immediate]; + let selected = select_present_mode(wgpu::PresentMode::Mailbox, available); + assert_eq!(selected, wgpu::PresentMode::Fifo); + } + + #[test] + fn select_present_mode_uses_auto_no_vsync_when_available() { + let available = &[wgpu::PresentMode::AutoNoVsync, wgpu::PresentMode::Fifo]; + let selected = select_present_mode(wgpu::PresentMode::Immediate, available); + assert_eq!(selected, wgpu::PresentMode::AutoNoVsync); + } +} diff --git a/crates/lambda-rs/examples/minimal.rs b/crates/lambda-rs/examples/minimal.rs index 638eef5a..fbf29965 100644 --- a/crates/lambda-rs/examples/minimal.rs +++ b/crates/lambda-rs/examples/minimal.rs @@ -5,6 +5,7 @@ //! applications correctly. use lambda::{ + render::PresentMode, runtime::start_runtime, runtimes::ApplicationRuntimeBuilder, }; @@ -16,6 +17,9 @@ fn main() { .with_dimensions(800, 600) .with_name("Minimal window"); }) + .with_renderer_configured_as(|render_context_builder| { + return render_context_builder.with_present_mode(PresentMode::Mailbox); + }) .build(); start_runtime(runtime); diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index a4d9fc54..14ab720e 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -69,6 +69,33 @@ use self::{ targets::surface::RenderTarget, }; +/// High-level presentation mode selection for window surfaces. +/// +/// The selected mode is validated against the adapter's surface capabilities +/// during `RenderContextBuilder::build`. If the requested mode is not +/// supported, Lambda selects a supported fallback. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PresentMode { + /// VSync enabled, capped to display refresh rate (FIFO). + Vsync, + /// VSync disabled, immediate presentation (may tear). + Immediate, + /// Triple buffering, low latency without tearing if supported. + Mailbox, +} + +impl Default for PresentMode { + fn default() -> Self { + return PresentMode::Vsync; + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum PresentModeOverride { + Vsync(bool), + Explicit(PresentMode), +} + /// Builder for configuring a `RenderContext` tied to one window. /// /// Purpose @@ -90,6 +117,7 @@ pub struct RenderContextBuilder { /// Reserved for future timeout handling during rendering (nanoseconds). /// Not currently enforced; kept for forward compatibility with runtime controls. _render_timeout: u64, + present_mode: Option, } impl RenderContextBuilder { @@ -98,6 +126,7 @@ impl RenderContextBuilder { Self { name: name.to_string(), _render_timeout: 1_000_000_000, + present_mode: None, } } @@ -107,6 +136,27 @@ impl RenderContextBuilder { self } + /// Enable or disable vertical sync. + /// + /// When enabled, the builder requests `PresentMode::Vsync` (FIFO). + /// + /// When disabled, the builder requests a non‑vsync mode (immediate + /// presentation) and falls back to a supported low-latency mode if needed. + pub fn with_vsync(mut self, enabled: bool) -> Self { + self.present_mode = Some(PresentModeOverride::Vsync(enabled)); + return self; + } + + /// Explicitly select a presentation mode. + /// + /// The requested mode is validated against the adapter's surface + /// capabilities. If unsupported, the renderer falls back to a supported + /// mode with similar behavior. + pub fn with_present_mode(mut self, mode: PresentMode) -> Self { + self.present_mode = Some(PresentModeOverride::Explicit(mode)); + return self; + } + /// Build a `RenderContext` for the provided `window` and configure the /// presentation surface. /// @@ -116,7 +166,9 @@ impl RenderContextBuilder { self, window: &window::Window, ) -> Result { - let RenderContextBuilder { name, .. } = self; + let RenderContextBuilder { + name, present_mode, .. + } = self; let instance = instance::InstanceBuilder::new() .with_label(&format!("{} Instance", name)) @@ -141,11 +193,33 @@ impl RenderContextBuilder { })?; let size = window.dimensions(); + let requested_present_mode = match present_mode { + Some(PresentModeOverride::Vsync(enabled)) => { + if enabled { + PresentMode::Vsync + } else { + PresentMode::Immediate + } + } + Some(PresentModeOverride::Explicit(mode)) => mode, + None => { + if window.vsync_requested() { + PresentMode::Vsync + } else { + PresentMode::Immediate + } + } + }; + let platform_present_mode = match requested_present_mode { + PresentMode::Vsync => targets::surface::PresentMode::Fifo, + PresentMode::Immediate => targets::surface::PresentMode::Immediate, + PresentMode::Mailbox => targets::surface::PresentMode::Mailbox, + }; surface .configure_with_defaults( &gpu, size, - targets::surface::PresentMode::default(), + platform_present_mode, texture::TextureUsages::RENDER_ATTACHMENT, ) .map_err(|e| { diff --git a/crates/lambda-rs/src/render/window.rs b/crates/lambda-rs/src/render/window.rs index 6e963c41..3023c65e 100644 --- a/crates/lambda-rs/src/render/window.rs +++ b/crates/lambda-rs/src/render/window.rs @@ -36,7 +36,7 @@ impl WindowBuilder { return Self { name: String::from("Window"), dimensions: (480, 360), - vsync: false, + vsync: true, }; } @@ -54,9 +54,8 @@ impl WindowBuilder { /// Request vertical sync behavior for the swapchain. /// - /// Note: present mode is ultimately selected when configuring the rendering - /// surface in `RenderContextBuilder`. This flag is reserved to influence - /// that choice and is currently a no‑op. + /// This value is consumed when building a `RenderContext` if no explicit + /// present mode is provided to `RenderContextBuilder`. pub fn with_vsync(mut self, vsync: bool) -> Self { self.vsync = vsync; return self; @@ -64,19 +63,26 @@ impl WindowBuilder { // TODO(vmarcella): Remove new call for window and construct the window directly. pub fn build(self, event_loop: &mut Loop) -> Window { - return Window::new(self.name.as_str(), self.dimensions, event_loop); + return Window::new( + self.name.as_str(), + self.dimensions, + self.vsync, + event_loop, + ); } } /// Window implementation for rendering applications. pub struct Window { window_handle: WindowHandle, + vsync: bool, } impl Window { fn new( name: &str, dimensions: (u32, u32), + vsync: bool, event_loop: &mut Loop, ) -> Self { let window_properties = WindowProperties { @@ -89,7 +95,10 @@ impl Window { .build(); logging::debug!("Created window: {}", name); - return Self { window_handle }; + return Self { + window_handle, + vsync, + }; } /// Redraws the window. @@ -109,4 +118,9 @@ impl Window { self.window_handle.size.height, ); } + + /// Returns the requested vertical sync preference for presentation. + pub fn vsync_requested(&self) -> bool { + return self.vsync; + } } From 8a9ec6b5693a223f069adc076110e5e3ad0d1ec3 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 26 Jan 2026 15:34:51 -0800 Subject: [PATCH 2/2] [remove] override and add a warning when the requested mode doesn't match the actual one. --- crates/lambda-rs/src/render/mod.rs | 37 ++++++++++-------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 14ab720e..824d3716 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -90,12 +90,6 @@ impl Default for PresentMode { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum PresentModeOverride { - Vsync(bool), - Explicit(PresentMode), -} - /// Builder for configuring a `RenderContext` tied to one window. /// /// Purpose @@ -117,7 +111,7 @@ pub struct RenderContextBuilder { /// Reserved for future timeout handling during rendering (nanoseconds). /// Not currently enforced; kept for forward compatibility with runtime controls. _render_timeout: u64, - present_mode: Option, + present_mode: Option, } impl RenderContextBuilder { @@ -143,7 +137,11 @@ impl RenderContextBuilder { /// When disabled, the builder requests a non‑vsync mode (immediate /// presentation) and falls back to a supported low-latency mode if needed. pub fn with_vsync(mut self, enabled: bool) -> Self { - self.present_mode = Some(PresentModeOverride::Vsync(enabled)); + self.present_mode = Some(if enabled { + PresentMode::Vsync + } else { + PresentMode::Immediate + }); return self; } @@ -153,7 +151,7 @@ impl RenderContextBuilder { /// capabilities. If unsupported, the renderer falls back to a supported /// mode with similar behavior. pub fn with_present_mode(mut self, mode: PresentMode) -> Self { - self.present_mode = Some(PresentModeOverride::Explicit(mode)); + self.present_mode = Some(mode); return self; } @@ -193,23 +191,12 @@ impl RenderContextBuilder { })?; let size = window.dimensions(); - let requested_present_mode = match present_mode { - Some(PresentModeOverride::Vsync(enabled)) => { - if enabled { - PresentMode::Vsync - } else { - PresentMode::Immediate - } + let requested_present_mode = present_mode.unwrap_or_else(|| { + if window.vsync_requested() { + return PresentMode::Vsync; } - Some(PresentModeOverride::Explicit(mode)) => mode, - None => { - if window.vsync_requested() { - PresentMode::Vsync - } else { - PresentMode::Immediate - } - } - }; + return PresentMode::Immediate; + }); let platform_present_mode = match requested_present_mode { PresentMode::Vsync => targets::surface::PresentMode::Fifo, PresentMode::Immediate => targets::surface::PresentMode::Immediate,