From 9af2ae9a0f29a9a105036ad67aa6331b423a2bf4 Mon Sep 17 00:00:00 2001 From: SixSolid <48584146+WhiteNoiseGuy@users.noreply.github.com> Date: Fri, 30 Oct 2026 13:24:30 +0000 Subject: [PATCH] Agent chat window: an off-screen chat window no longer stalls the main window (#9118) (#9119) * claim: #9118 * Agent chat window: an off-screen chat window no longer stalls the main window (#9118) The chat popped out into its own window is an immediate viewport: eframe draws and presents it inside the main window's frame. Both surfaces waited for vsync, and a Wayland compositor sends no frame callbacks to a window it isn't showing, so the main window stalled behind the chat window's present. A surface other than the main window's is now configured with Mailbox, else Immediate, when the configured mode waits for vsync and the surface offers one (viewport_present_mode in the vendored egui-wgpu). The main window keeps vsync and still paces both. --- crates/ui-egui/src/surface_clamp_tests.rs | 27 ++++++++++++++++++++-- vendor/README.md | 5 ++++ vendor/egui-wgpu/src/lib.rs | 28 +++++++++++++++++++++++ vendor/egui-wgpu/src/winit.rs | 16 ++++++++++++- 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/surface_clamp_tests.rs b/crates/ui-egui/src/surface_clamp_tests.rs index 84b4aaa8e1..b635d4739d 100644 --- a/crates/ui-egui/src/surface_clamp_tests.rs +++ b/crates/ui-egui/src/surface_clamp_tests.rs @@ -1,8 +1,10 @@ //! The window surface is clamped to the device's largest texture (#869): a window moved between //! monitors of different scale asked for 11512 x 6808 against a maximum of 8192 and the -//! surface configure was a validation panic. The clamp lives in the vendored egui-wgpu. +//! surface configure was a validation panic. The clamp lives in the vendored egui-wgpu, and so +//! does the present mode of a window other than the main one (the agent chat popped out). -use egui_wgpu::clamp_surface_extent; +use egui_wgpu::wgpu::PresentMode; +use egui_wgpu::{clamp_surface_extent, viewport_present_mode}; #[test] fn a_window_that_fits_is_unchanged() { @@ -39,3 +41,24 @@ fn extreme_sizes_never_give_zero_or_exceed_the_limit() { assert!(s > 0.0 && s <= 1.0); } } + +#[test] +fn a_popped_out_window_presents_without_waiting_for_the_display() { + // Mesa's Vulkan on Wayland: the chat's window took the main window down to a crawl while + // both waited for vsync in one frame. + let wayland = [PresentMode::Fifo, PresentMode::Mailbox, PresentMode::Immediate]; + assert_eq!(viewport_present_mode(false, PresentMode::AutoVsync, &wayland), PresentMode::Mailbox); + assert_eq!(viewport_present_mode(false, PresentMode::Fifo, &[PresentMode::Fifo, PresentMode::Immediate]), PresentMode::Immediate); + // The main window keeps its vsync: it paces both. + assert_eq!(viewport_present_mode(true, PresentMode::AutoVsync, &wayland), PresentMode::AutoVsync); +} + +#[test] +fn a_surface_that_only_waits_and_a_chosen_mode_without_vsync_are_kept() { + assert_eq!(viewport_present_mode(false, PresentMode::AutoVsync, &[PresentMode::Fifo]), PresentMode::AutoVsync); + assert_eq!(viewport_present_mode(false, PresentMode::Fifo, &[]), PresentMode::Fifo); + for configured in [PresentMode::AutoNoVsync, PresentMode::Immediate, PresentMode::Mailbox] { + assert_eq!(viewport_present_mode(false, configured, &[PresentMode::Fifo, PresentMode::Mailbox]), configured); + assert_eq!(viewport_present_mode(true, configured, &[PresentMode::Fifo]), configured); + } +} diff --git a/vendor/README.md b/vendor/README.md index 14380b98bd..f1c3ebc11a 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -313,6 +313,11 @@ SolidCraft changes: and the frame is skipped with a repaint asked for when it is due. The streak and the deadline survive a recreate; a configure the device takes ends the streak, a recreate it asked for included. A recreate for a lost surface goes ahead as before. +- `src/lib.rs`, `src/winit.rs`: a window other than the main one (an immediate viewport, drawn + and presented inside the main window's frame) is configured with Mailbox, else Immediate, + when the configured mode waits for vsync and the surface offers one (`viewport_present_mode`). + Waiting on both surfaces in one frame halved the frame rate, and on Wayland a window the + compositor wasn't showing stalled the main window behind its present. - `src/lib.rs`, `src/winit.rs`: the device's lost callback marks `RenderState::device_lost` (`DeviceLost`), and the painter draws nothing on a lost device. Before a frame's uploads it submits nothing (`DeviceLost::probe`), so a device the driver lost is found by that diff --git a/vendor/egui-wgpu/src/lib.rs b/vendor/egui-wgpu/src/lib.rs index 1e65ac7a0b..439e5400f8 100644 --- a/vendor/egui-wgpu/src/lib.rs +++ b/vendor/egui-wgpu/src/lib.rs @@ -1058,6 +1058,34 @@ pub enum Attempt { Refused(RetryPlan, E), } +/// SolidCraft: the present mode a viewport's surface is configured with. The main window keeps +/// `configured`. Another window (the agent chat popped out into its own window) is an immediate +/// viewport: eframe draws and presents it inside the main window's frame, one surface after the +/// other. With vsync on both, every frame waited for the display twice, and on a compositor that +/// sends no frame callbacks to a window it isn't showing (Wayland: on another workspace, scrolled +/// away, covered) the main window stalled behind the other one's present. So another window +/// presents without waiting where its surface can (Mailbox, which doesn't tear, before +/// Immediate); the main window's vsync still paces both. A `configured` mode without vsync is +/// kept everywhere. +pub fn viewport_present_mode( + root: bool, + configured: wgpu::PresentMode, + supported: &[wgpu::PresentMode], +) -> wgpu::PresentMode { + use wgpu::PresentMode; + let waits = matches!( + configured, + PresentMode::AutoVsync | PresentMode::Fifo | PresentMode::FifoRelaxed + ); + if root || !waits { + return configured; + } + [PresentMode::Mailbox, PresentMode::Immediate] + .into_iter() + .find(|mode| supported.contains(mode)) + .unwrap_or(configured) +} + /// SolidCraft: the surface size for a window of `width` x `height` pixels on a device whose /// textures are at most `max_side` on a side, and the scale it is drawn at. A window that fits is /// unchanged (scale 1); a larger one shrinks uniformly, keeping its aspect ratio, so the image diff --git a/vendor/egui-wgpu/src/winit.rs b/vendor/egui-wgpu/src/winit.rs index aa9f948c09..bacb3f8a99 100644 --- a/vendor/egui-wgpu/src/winit.rs +++ b/vendor/egui-wgpu/src/winit.rs @@ -111,6 +111,7 @@ impl Painter { /// device, or a surface the adapter doesn't support, neither of which configured it (#7007). /// Called only through [`Self::try_configure`], which says what follows. fn configure_surface( + viewport_id: ViewportId, surface_state: &SurfaceState, render_state: &RenderState, config: &SurfaceConfig, @@ -141,6 +142,17 @@ impl Painter { else { return Err(crate::ConfigureError::Unsupported); }; + // SolidCraft: a window other than the main one presents without waiting for the display + // where its surface can (`crate::viewport_present_mode`). + let supported = surface_state + .surface + .get_capabilities(&render_state.adapter) + .present_modes; + let present_mode = crate::viewport_present_mode( + viewport_id == ViewportId::ROOT, + present_mode, + &supported, + ); let mut surf_config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: render_state.target_format, @@ -177,7 +189,9 @@ impl Painter { ) -> bool { // A copy, as the configure borrows the whole surface state. let mut retry = state.retry; - let attempt = retry.attempt(now, || Self::configure_surface(state, render_state, config)); + let attempt = retry.attempt(now, || { + Self::configure_surface(viewport_id, state, render_state, config) + }); state.retry = retry; match attempt { crate::Attempt::Took(refusals) => {