From e705d3760d4e592ad1db0ada1f7e71ce01c7d428 Mon Sep 17 00:00:00 2001 From: unexpected-soup <283385344+unexpected-soup@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:57:08 +0300 Subject: [PATCH] Add vsync option for fps cap issues --- backends/vulkan/src/lib.rs | 22 ++++++++++------- backends/wgpu/src/lib.rs | 21 ++++++++++------- crates/engine/assets/lang/en_us.lang | 3 +++ crates/engine/assets/lang/fr_fr.lang | 3 +++ crates/engine/assets/lang/ru_ru.lang | 3 +++ crates/engine/src/app.rs | 6 ++--- crates/engine/src/config.rs | 4 +++- crates/engine/src/screens.rs | 5 ++-- crates/engine/src/settings.rs | 29 +++++++++++++++++++++-- crates/render/src/lib.rs | 35 ++++++++++++++++++++++++++-- 10 files changed, 103 insertions(+), 28 deletions(-) diff --git a/backends/vulkan/src/lib.rs b/backends/vulkan/src/lib.rs index f332b230..78050660 100644 --- a/backends/vulkan/src/lib.rs +++ b/backends/vulkan/src/lib.rs @@ -98,7 +98,7 @@ pub struct VulkanBackend { slot: usize, image_index: Option, want_size: (u32, u32), - vsync: bool, + vsync: VsyncMode, dirty_swapchain: bool, buffers: Slab, textures: Slab, @@ -653,14 +653,18 @@ impl VulkanBackend { .surface_fn .get_physical_device_surface_present_modes(self.pdev, self.surface) .map_err(err("present modes"))?; - let mode = if self.vsync { - vk::PresentModeKHR::FIFO - } else if modes.contains(&vk::PresentModeKHR::MAILBOX) { - vk::PresentModeKHR::MAILBOX - } else if modes.contains(&vk::PresentModeKHR::IMMEDIATE) { - vk::PresentModeKHR::IMMEDIATE + use vk::PresentModeKHR as P; + let [first, second] = match self.vsync { + VsyncMode::On => [P::FIFO; 2], + VsyncMode::Off => [P::MAILBOX, P::IMMEDIATE], + VsyncMode::Uncapped => [P::IMMEDIATE, P::MAILBOX], + }; + let mode = if modes.contains(&first) { + first + } else if modes.contains(&second) { + second } else { - vk::PresentModeKHR::FIFO + P::FIFO }; let mut count = caps.min_image_count + 1; if caps.max_image_count > 0 { @@ -1171,7 +1175,7 @@ impl Backend for VulkanBackend { } } - fn set_vsync(&mut self, vsync: bool) { + fn set_vsync(&mut self, vsync: VsyncMode) { if self.vsync != vsync { self.vsync = vsync; self.dirty_swapchain = true; diff --git a/backends/wgpu/src/lib.rs b/backends/wgpu/src/lib.rs index 1234f82a..557c5802 100644 --- a/backends/wgpu/src/lib.rs +++ b/backends/wgpu/src/lib.rs @@ -779,16 +779,19 @@ impl Backend for WgpuBackend { } } - fn set_vsync(&mut self, vsync: bool) { - let modes = &self.present_modes; - let mode = if vsync { - wgpu::PresentMode::Fifo - } else if modes.contains(&wgpu::PresentMode::Mailbox) { - wgpu::PresentMode::Mailbox - } else if modes.contains(&wgpu::PresentMode::Immediate) { - wgpu::PresentMode::Immediate + fn set_vsync(&mut self, vsync: VsyncMode) { + use wgpu::PresentMode as P; + let [first, second] = match vsync { + VsyncMode::On => [P::Fifo; 2], + VsyncMode::Off => [P::Mailbox, P::Immediate], + VsyncMode::Uncapped => [P::Immediate, P::Mailbox], + }; + let mode = if self.present_modes.contains(&first) { + first + } else if self.present_modes.contains(&second) { + second } else { - wgpu::PresentMode::Fifo + P::Fifo }; if self.config.present_mode != mode { self.config.present_mode = mode; diff --git a/crates/engine/assets/lang/en_us.lang b/crates/engine/assets/lang/en_us.lang index 8a7e695a..0729a0d4 100644 --- a/crates/engine/assets/lang/en_us.lang +++ b/crates/engine/assets/lang/en_us.lang @@ -49,6 +49,9 @@ settings.fov_effects = FOV effects: {0}% settings.gui_scale = GUI scale: {0} settings.gui_scale.auto = Auto settings.vsync = VSync: {0} +settings.vsync.off = Off +settings.vsync.on = On +settings.vsync.uncapped = Off (uncapped) settings.renderer = Renderer: {0} settings.renderer.restart = Renderer: {0} (after restart) settings.renderer.vulkan = Vulkan diff --git a/crates/engine/assets/lang/fr_fr.lang b/crates/engine/assets/lang/fr_fr.lang index 1fb1bc73..963efbe8 100644 --- a/crates/engine/assets/lang/fr_fr.lang +++ b/crates/engine/assets/lang/fr_fr.lang @@ -49,6 +49,9 @@ settings.fov_effects = Effets de champ de vision: {0}% settings.gui_scale = Taille de l'interface: {0} settings.gui_scale.auto = Auto settings.vsync = VSync: {0} +settings.vsync.off = Non +settings.vsync.on = Oui +settings.vsync.uncapped = Non (sans limite d'IPS) settings.renderer = Rendu : {0} settings.renderer.restart = Rendu : {0} (au redémarrage) settings.renderer.vulkan = Vulkan diff --git a/crates/engine/assets/lang/ru_ru.lang b/crates/engine/assets/lang/ru_ru.lang index 2a7c772e..b9fffb4b 100644 --- a/crates/engine/assets/lang/ru_ru.lang +++ b/crates/engine/assets/lang/ru_ru.lang @@ -23,6 +23,9 @@ settings.fov_effects = Эффекты поля зрения: {0}% settings.gui_scale = Масштаб интерфейса: {0} settings.gui_scale.auto = Авто settings.vsync = Вертикальная синхронизация: {0} +settings.vsync.off = Выкл +settings.vsync.on = Вкл +settings.vsync.uncapped = Выкл (без лимита FPS) settings.renderer = Рендер: {0} settings.renderer.restart = Рендер: {0} (после перезапуска) settings.renderer.vulkan = Vulkan diff --git a/crates/engine/src/app.rs b/crates/engine/src/app.rs index e28711cf..214b76e8 100644 --- a/crates/engine/src/app.rs +++ b/crates/engine/src/app.rs @@ -6,7 +6,7 @@ use std::time::Instant; use glam::Vec2; use weft_abi as abi; -use weft_render::{Backend, BackendDesc, Cmd, CommandList, Format, Target, TextureDesc, TextureId}; +use weft_render::{Backend, BackendDesc, Cmd, CommandList, Format, Target, TextureDesc, TextureId, VsyncMode}; use winit::application::ApplicationHandler; use winit::event::{DeviceEvent, DeviceId, ElementState, MouseButton, MouseScrollDelta, WindowEvent}; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; @@ -293,7 +293,7 @@ fn engine_assets(dev: bool) -> PathBuf { /// The settings `Running::apply_settings` last put into effect. #[derive(Default, PartialEq)] struct Applied { - vsync: bool, + vsync: VsyncMode, view: i32, language: String, gui_scale: u32, @@ -328,7 +328,7 @@ impl App { crate::settings::migrate(&self.opts.run_dir); crate::config::load(&self.opts.run_dir); if self.opts.no_vsync { - crate::config::override_for_run("core:vsync", "false"); + crate::config::override_for_run("core:vsync", "off"); } if let Some(v) = self.opts.view { crate::config::override_for_run("core:view_distance", &v.to_string()); diff --git a/crates/engine/src/config.rs b/crates/engine/src/config.rs index d754717b..58caa237 100644 --- a/crates/engine/src/config.rs +++ b/crates/engine/src/config.rs @@ -10,6 +10,7 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Mutex, OnceLock, RwLock}; use weft_abi::{config_kind as ck, config_scope as scope}; +use weft_render::VsyncMode; use crate::content::content; use crate::entries::{Entry, RawEntry}; @@ -384,7 +385,8 @@ pub fn engine_configs(keys: &[(String, String)]) -> Vec { number("core:chat_opacity", ck::INT, 50.0, 0.0, 100.0, 1.0), number("core:chat_visibility", ck::CHOICE, 0.0, 0.0, 2.0, 0.0) .set(c::CHOICES, ["all", "system", "hidden"].map(String::from).to_vec()), - number("core:vsync", ck::BOOL, 1.0, 0.0, 1.0, 0.0), + number("core:vsync", ck::CHOICE, VsyncMode::On as u8 as f64, 0.0, 2.0, 0.0) + .set(c::CHOICES, VsyncMode::ALL.map(|m| m.name().to_string()).to_vec()), // Taken at the next start; the others are tried if it can't run. number("core:renderer", ck::CHOICE, 0.0, 0.0, 1.0, 0.0).set(c::CHOICES, ["vulkan", "wgpu"].map(String::from).to_vec()), number("core:fullscreen", ck::BOOL, 0.0, 0.0, 1.0, 0.0), diff --git a/crates/engine/src/screens.rs b/crates/engine/src/screens.rs index 861e6b27..526d9af2 100644 --- a/crates/engine/src/screens.rs +++ b/crates/engine/src/screens.rs @@ -633,8 +633,9 @@ impl Screen for SettingsScreen { let next = if r.settings.gui_scale() >= max { 0 } else { r.settings.gui_scale() + 1 }; r.settings.set("gui_scale", next as f64); }); - d.button("settings.vsync", &[on_off(s.vsync())], 300.0, false, |r| { - r.settings.set("vsync", !r.settings.vsync() as u8 as f64); + d.button("settings.vsync", &[Arg::Key(format!("settings.vsync.{}", s.vsync().name()))], 300.0, false, |r| { + r.settings + .set("vsync", r.settings.vsync().next() as u8 as f64); r.handle(vec![Request::Rebuild]); }); let chosen = s.renderer(); diff --git a/crates/engine/src/settings.rs b/crates/engine/src/settings.rs index dc346fb1..b34db466 100644 --- a/crates/engine/src/settings.rs +++ b/crates/engine/src/settings.rs @@ -4,6 +4,8 @@ use std::path::{Path, PathBuf}; +use weft_render::VsyncMode; + use crate::config; use crate::input::{Binding, Input, Slots}; @@ -87,8 +89,8 @@ impl Settings { pub fn view_distance(&self) -> i32 { number("core:view_distance") as i32 } - pub fn vsync(&self) -> bool { - number("core:vsync") != 0.0 + pub fn vsync(&self) -> VsyncMode { + VsyncMode::from_index(number("core:vsync") as u32) } /// The chosen renderer's name (`core:renderer`). pub fn renderer(&self) -> String { @@ -189,6 +191,29 @@ pub fn migrate(run_dir: &Path) { merge_settings(&config); for dir in [config, run_dir.join("test-config")] { migrate_bindings(&dir.join("core.txt")); + migrate_vsync(&dir.join("core.txt")); + } +} + +/// `vsync` was true or false; it names a mode now. Off always preferred mailbox over immediate, +/// which is what `off` does, so both keep working the same. +/// TODO remove together with `migrate_bindings` +fn migrate_vsync(path: &Path) { + let Ok(src) = std::fs::read_to_string(path) else { return }; + let mut changed = false; + let out: String = crate::kv::pairs(&src) + .map(|(_, k, v)| { + let v = match (k, v.trim()) { + ("vsync", "true" | "1") => "on", + ("vsync", "false" | "0") => "off", + _ => return format!("{k} = {v}\n"), + }; + changed = true; + format!("{k} = {v}\n") + }) + .collect(); + if changed { + let _ = std::fs::write(path, out); } } diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index aea60aa3..0f3ca9a6 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -346,11 +346,42 @@ impl std::error::Error for Error {} pub trait Window: HasWindowHandle + HasDisplayHandle {} impl Window for T {} +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] +pub enum VsyncMode { + /// May torn but sometimes resolves issues with `Off`. + Uncapped = 0, + /// Never torn, fps capped to the refresh rate. + #[default] + On = 1, + /// Never torn, but some players experience an fps cap. + Off = 2, +} + +impl VsyncMode { + pub const ALL: [VsyncMode; 3] = [VsyncMode::Off, VsyncMode::On, VsyncMode::Uncapped]; + + pub fn name(self) -> &'static str { + match self { + VsyncMode::Off => "off", + VsyncMode::On => "on", + VsyncMode::Uncapped => "uncapped", + } + } + + pub fn from_index(i: u32) -> VsyncMode { + Self::ALL.get(i as usize).copied().unwrap_or_default() + } + + pub fn next(self) -> VsyncMode { + Self::from_index((self as u32 + 1) % Self::ALL.len() as u32) + } +} + pub struct BackendDesc<'a> { pub window: &'a dyn Window, pub width: u32, pub height: u32, - pub vsync: bool, + pub vsync: VsyncMode, pub debug: bool, } @@ -363,7 +394,7 @@ pub trait Backend: Send { fn frames_in_flight(&self) -> usize; fn surface_size(&self) -> (u32, u32); fn resize(&mut self, width: u32, height: u32); - fn set_vsync(&mut self, vsync: bool); + fn set_vsync(&mut self, vsync: VsyncMode); fn create_buffer(&mut self, size: u64, usage: u32, label: &str) -> BufferId; /// Only valid for `usage::HOST` buffers.