Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 85 additions & 15 deletions crates/lambda-rs-platform/src/wgpu/surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
}
}
4 changes: 4 additions & 0 deletions crates/lambda-rs/examples/minimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! applications correctly.

use lambda::{
render::PresentMode,
runtime::start_runtime,
runtimes::ApplicationRuntimeBuilder,
};
Expand All @@ -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);
Expand Down
65 changes: 63 additions & 2 deletions crates/lambda-rs/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<PresentMode>,
}

impl RenderContextBuilder {
Expand All @@ -98,6 +120,7 @@ impl RenderContextBuilder {
Self {
name: name.to_string(),
_render_timeout: 1_000_000_000,
present_mode: None,
}
}

Expand All @@ -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.
///
Expand All @@ -116,7 +164,9 @@ impl RenderContextBuilder {
self,
window: &window::Window,
) -> Result<RenderContext, RenderContextError> {
let RenderContextBuilder { name, .. } = self;
let RenderContextBuilder {
name, present_mode, ..
} = self;

let instance = instance::InstanceBuilder::new()
.with_label(&format!("{} Instance", name))
Expand All @@ -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| {
Expand Down
26 changes: 20 additions & 6 deletions crates/lambda-rs/src/render/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl WindowBuilder {
return Self {
name: String::from("Window"),
dimensions: (480, 360),
vsync: false,
vsync: true,
};
}

Expand All @@ -54,29 +54,35 @@ 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;
}

// TODO(vmarcella): Remove new call for window and construct the window directly.
pub fn build(self, event_loop: &mut Loop<Events>) -> 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<Events>,
) -> Self {
let window_properties = WindowProperties {
Expand All @@ -89,7 +95,10 @@ impl Window {
.build();

logging::debug!("Created window: {}", name);
return Self { window_handle };
return Self {
window_handle,
vsync,
};
}

/// Redraws the window.
Expand All @@ -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;
}
}
Loading