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..824d3716 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -69,6 +69,27 @@ 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; + } +} + /// Builder for configuring a `RenderContext` tied to one window. /// /// Purpose @@ -90,6 +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, } impl RenderContextBuilder { @@ -98,6 +120,7 @@ impl RenderContextBuilder { Self { name: name.to_string(), _render_timeout: 1_000_000_000, + present_mode: None, } } @@ -107,6 +130,31 @@ 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(if enabled { + PresentMode::Vsync + } else { + PresentMode::Immediate + }); + 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(mode); + return self; + } + /// Build a `RenderContext` for the provided `window` and configure the /// presentation surface. /// @@ -116,7 +164,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 +191,22 @@ impl RenderContextBuilder { })?; let size = window.dimensions(); + let requested_present_mode = present_mode.unwrap_or_else(|| { + if window.vsync_requested() { + return PresentMode::Vsync; + } + return 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; + } }