diff --git a/src/backend/kms/mod.rs b/src/backend/kms/mod.rs index 6da04aace..f7987b9e0 100644 --- a/src/backend/kms/mod.rs +++ b/src/backend/kms/mod.rs @@ -214,7 +214,7 @@ fn init_libinput( state.process_input_event(event, crate::input::InputBackendId::Normal); for output in state.common.shell.read().outputs() { - state.backend.kms().schedule_render(output); + state.backend.kms().schedule_render(output, false); } }) .map_err(|err| err.error) @@ -700,14 +700,14 @@ impl KmsState { Ok(node) } - pub fn schedule_render(&mut self, output: &Output) { + pub fn schedule_render(&mut self, output: &Output, is_fullscrenn: bool) { for surface in self .drm_devices .values() .flat_map(|d| d.inner.surfaces.values()) .filter(|s| s.output == *output || s.output.mirroring().is_some_and(|o| &o == output)) { - surface.schedule_render(); + surface.schedule_render(is_fullscrenn); } } @@ -804,14 +804,14 @@ impl KmsState { } impl KmsGuard<'_> { - pub fn schedule_render(&mut self, output: &Output) { + pub fn schedule_render(&mut self, output: &Output, is_fullscrenn: bool) { for surface in self .drm_devices .values() .flat_map(|d| d.inner.surfaces.values()) .filter(|s| s.output == *output || s.output.mirroring().is_some_and(|o| &o == output)) { - surface.schedule_render(); + surface.schedule_render(is_fullscrenn); } } diff --git a/src/backend/kms/surface/mod.rs b/src/backend/kms/surface/mod.rs index cbc893813..4b11473ea 100644 --- a/src/backend/kms/surface/mod.rs +++ b/src/backend/kms/surface/mod.rs @@ -12,7 +12,7 @@ use crate::{ state::SurfaceDmabufFeedback, utils::prelude::*, wayland::handlers::{ - compositor::recursive_frame_time_estimation, + compositor::{FULLSCREEN_IMMEDIATE_RENDER, recursive_frame_time_estimation}, image_copy_capture::{FrameHolder, PendingImageCopyData, SessionData, submit_buffer}, }, }; @@ -74,7 +74,7 @@ use smithay::{ }, wayland_server::protocol::wl_surface::WlSurface, }, - utils::{Clock, Monotonic, Physical, Point, Rectangle, Transform}, + utils::{Clock, IsAlive, Monotonic, Physical, Point, Rectangle, Transform}, wayland::{ dmabuf::{DmabufFeedbackBuilder, get_dmabuf}, image_copy_capture::{ @@ -92,7 +92,7 @@ use std::{ collections::{HashMap, HashSet, hash_map}, mem, sync::{ - Arc, RwLock, + Arc, LazyLock, RwLock, atomic::{AtomicBool, Ordering}, mpsc::{Receiver, SyncSender}, }, @@ -105,6 +105,16 @@ pub use self::timings::Timings; use super::{drm_helpers, render::gles::GbmGlowBackend}; +static FULLSCREEN_SKIP_OTHER_SURFACE: LazyLock = LazyLock::new(|| { + crate::utils::env::bool_var("COSMIC_FULLSCREEN_SKIP_OTHER_SURFACE").unwrap_or(true) +}); + +static FULLSCREEN_SKIP_OTHER_SURFACE_ALWAYS: LazyLock = LazyLock::new(|| { + crate::utils::env::bool_var("COSMIC_FULLSCREEN_SKIP_OTHER_SURFACE_ALWAYS").unwrap_or(false) +}); + +const _30_HZ: Duration = Duration::from_nanos(1_000_000_000 / 30); + #[cfg(feature = "debug")] use smithay_egui::EguiState; @@ -153,6 +163,9 @@ pub struct SurfaceThreadState { loop_handle: LoopHandle<'static, Self>, clock: Clock, + min_vrr: Option, + min_vrr_frame_time: Option, + #[cfg(feature = "debug")] egui: EguiState, @@ -186,7 +199,10 @@ pub enum QueueState { /// A redraw is queued. Queued(RegistrationToken), /// We submitted a frame to the KMS and waiting for it to be presented. - WaitingForVBlank { redraw_needed: bool }, + WaitingForVBlank { + redraw_needed: bool, + fullscreen_request: bool, + }, /// We did not submit anything to KMS and made a timer to fire at the estimated VBlank. WaitingForEstimatedVBlank(RegistrationToken), /// A redraw is queued on top of the above. @@ -215,7 +231,7 @@ pub enum ThreadCommand { UpdateMirroring(Option), UpdateScreenFilter(ScreenFilter), VBlank(Option), - ScheduleRender, + ScheduleRender(bool), AdaptiveSyncAvailable(SyncSender>), UseAdaptiveSync(AdaptiveSync), AllowFrameFlags(bool, FrameFlags), @@ -386,9 +402,11 @@ impl Surface { let _ = self.thread_command.send(ThreadCommand::VBlank(metadata)); } - pub fn schedule_render(&self) { + pub fn schedule_render(&self, is_fullscreen: bool) { if self.dpms { - let _ = self.thread_command.send(ThreadCommand::ScheduleRender); + let _ = self + .thread_command + .send(ThreadCommand::ScheduleRender(is_fullscreen)); } } @@ -455,7 +473,7 @@ impl Surface { if self.dpms != on { self.dpms = on; if on { - self.schedule_render(); + self.schedule_render(false); } else { let _ = self.thread_command.send(ThreadCommand::DpmsOff); } @@ -550,6 +568,10 @@ fn surface_thread( shell, loop_handle: event_loop.handle(), clock: Clock::new(), + + min_vrr: None, + min_vrr_frame_time: None, + #[cfg(feature = "debug")] egui, @@ -587,12 +609,12 @@ fn surface_thread( Event::Msg(ThreadCommand::VBlank(metadata)) => { state.on_vblank(metadata); } - Event::Msg(ThreadCommand::ScheduleRender) => { + Event::Msg(ThreadCommand::ScheduleRender(is_fullscreen)) => { if !startup_done.load(Ordering::SeqCst) { return; } - state.queue_redraw(false); + state.queue_redraw(false, is_fullscreen); } Event::Msg(ThreadCommand::UpdateMirroring(mirroring_output)) => { state.update_mirroring(mirroring_output); @@ -700,18 +722,21 @@ impl SurfaceThreadState { .flatten(), ) }); + self.min_vrr = min_hz; let interval = Duration::from_secs_f64(1_000. / drm_helpers::calculate_refresh_rate(mode) as f64); self.timings.set_refresh_interval(Some(interval)); const SAFETY_MARGIN: u32 = 2; // Magic two frames margin taken from kwin to not trigger low-framerate-compensation let min_min_refresh_interval = Duration::from_secs_f64(1. / 30.); // 30Hz - self.timings.set_min_refresh_interval(Some( + self.min_vrr_frame_time = Some( min_hz .map(|min| Duration::from_secs_f64(1. / (min + SAFETY_MARGIN) as f64)) .unwrap_or(min_min_refresh_interval) // alternatively use 30Hz - .max(min_min_refresh_interval), - )); + .min(min_min_refresh_interval), + ); + self.timings + .set_min_refresh_interval(self.min_vrr_frame_time); if crate::utils::env::bool_var("COSMIC_DISABLE_DIRECT_SCANOUT").unwrap_or(false) { self.frame_flags.remove(FrameFlags::ALLOW_SCANOUT); @@ -872,10 +897,13 @@ impl SurfaceThreadState { } } - let redraw_needed = match mem::replace(&mut self.state, QueueState::Idle) { + let (redraw_needed, is_fullscreen) = match mem::replace(&mut self.state, QueueState::Idle) { QueueState::Idle => unreachable!(), QueueState::Queued(_) => unreachable!(), - QueueState::WaitingForVBlank { redraw_needed } => redraw_needed, + QueueState::WaitingForVBlank { + redraw_needed, + fullscreen_request, + } => (redraw_needed, fullscreen_request), QueueState::WaitingForEstimatedVBlank(_) => unreachable!(), QueueState::WaitingForEstimatedVBlankAndQueued { .. } => unreachable!(), }; @@ -886,7 +914,7 @@ impl SurfaceThreadState { .non_continuous_frame(self.vblank_frame_name); self.vblank_frame = Some(vblank_frame); - self.queue_redraw(false); + self.queue_redraw(false, is_fullscreen); } self.send_frame_callbacks(); } @@ -908,21 +936,44 @@ impl SurfaceThreadState { self.frame_callback_seq = self.frame_callback_seq.wrapping_add(1); if force || self.shell.read().animations_going() { - self.queue_redraw(false); + self.queue_redraw(false, false); } self.send_frame_callbacks(); } - fn queue_redraw(&mut self, force: bool) { + fn queue_redraw(&mut self, mut force: bool, is_fullscreen: bool) { let Some(_compositor) = self.compositor.as_mut() else { return; }; - if let QueueState::WaitingForVBlank { .. } = &self.state { + let is_fullscreen_skip_other = *FULLSCREEN_SKIP_OTHER_SURFACE + && (self.timings.vrr() || *FULLSCREEN_SKIP_OTHER_SURFACE_ALWAYS) + && self.output.is_foreground_fullscreen_occupied().is_some() + && !force + && !is_fullscreen; + + if *FULLSCREEN_SKIP_OTHER_SURFACE && is_fullscreen { + force = true; + } + + let immediate = if *FULLSCREEN_IMMEDIATE_RENDER && self.timings.vrr() && is_fullscreen { + force = true; + true + } else { + false + }; + + if let QueueState::WaitingForVBlank { + fullscreen_request, .. + } = &self.state + { // We're waiting for VBlank, request a redraw afterwards. - self.state = QueueState::WaitingForVBlank { - redraw_needed: true, - }; + if !fullscreen_request { + self.state = QueueState::WaitingForVBlank { + redraw_needed: true, + fullscreen_request: is_fullscreen, + }; + } return; } @@ -939,7 +990,15 @@ impl SurfaceThreadState { } let estimated_presentation = self.timings.next_presentation_time(&self.clock); - let render_start = self.timings.next_render_time(&self.clock); + let render_start = if is_fullscreen_skip_other { + // To prevent the fullscreen surface from unexpectedly stopping updates, register a fallback redraw request. + // If the fullscreen surface commits an update within the min_vrr interval, it will replace this fallback request. + self.min_vrr_frame_time.unwrap_or(_30_HZ) + } else if immediate { + Duration::ZERO + } else { + self.timings.next_render_time(&self.clock) + }; let timer = if render_start.is_zero() { trace!("Running late for frame."); @@ -955,7 +1014,7 @@ impl SurfaceThreadState { if let Err(err) = state.redraw(estimated_presentation) { let name = state.output.name(); warn!(?name, "Failed to submit rendering: {:?}", err); - state.queue_redraw(true); + state.queue_redraw(true, false); } TimeoutAction::Drop }) @@ -1027,24 +1086,17 @@ impl SurfaceThreadState { let shell = self.shell.read(); let animations_going = shell.animations_going(); let output = self.mirroring.as_ref().unwrap_or(&self.output); - if let Some((_, workspace)) = shell.workspaces.active(output) { - let seat = shell.seats.last_active(); - if let Some(fullscreen_surface) = workspace.get_fullscreen(seat) { - const _30_FPS: Duration = Duration::from_nanos(1_000_000_000 / 30); - ( - true, - fullscreen_surface - .surface - .wl_surface() - .is_some_and(|surface| { - recursive_frame_time_estimation(&self.clock, &surface) - .is_some_and(|dur| dur <= _30_FPS) - }), - animations_going, - ) - } else { - (false, false, animations_going) - } + if let Some(fullscreen_surface) = output.is_foreground_fullscreen_occupied() + && fullscreen_surface.alive() + { + let min_vrr_frame_time = self + .min_vrr_frame_time + .unwrap_or(Duration::from_nanos(1_000_000_000 / 30)); + let drives_refresh_rate = fullscreen_surface.wl_surface().is_some_and(|surface| { + recursive_frame_time_estimation(&self.clock, &surface) + .is_some_and(|dur| dur <= min_vrr_frame_time) + }); + (true, drives_refresh_rate, animations_going) } else { (false, false, animations_going) } @@ -1340,6 +1392,7 @@ impl SurfaceThreadState { if x.is_ok() { let new_state = QueueState::WaitingForVBlank { redraw_needed: false, + fullscreen_request: false, }; match mem::replace(&mut self.state, new_state) { QueueState::Idle => unreachable!(), diff --git a/src/backend/mod.rs b/src/backend/mod.rs index b9586b5ad..2bafd14d1 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -155,7 +155,7 @@ pub fn init_backend_auto( .startup_done .store(true, std::sync::atomic::Ordering::SeqCst); for output in state.common.shell.read().outputs() { - state.backend.schedule_render(output); + state.backend.schedule_render(output, false); } } } diff --git a/src/backend/render/cursor.rs b/src/backend/render/cursor.rs index da01b270a..530103a3b 100644 --- a/src/backend/render/cursor.rs +++ b/src/backend/render/cursor.rs @@ -481,6 +481,6 @@ fn hide_cursor(state: &mut State, seat: &Seat) { } let outputs: Vec<_> = state.common.shell.read().outputs().cloned().collect(); for output in outputs { - state.backend.schedule_render(&output); + state.backend.schedule_render(&output, false); } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 4a2500fd2..6a5df3e6f 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -965,7 +965,7 @@ fn config_changed(config: cosmic_config::Config, keys: Vec, state: &mut state.common.config.cosmic_conf.appearance_settings = new; state.common.update_config(); for output in state.common.shell.read().outputs() { - state.backend.schedule_render(output); + state.backend.schedule_render(output, false); } } } @@ -983,7 +983,7 @@ fn config_changed(config: cosmic_config::Config, keys: Vec, state: &mut let outputs: Vec<_> = state.common.shell.read().outputs().cloned().collect(); for output in outputs { - state.backend.schedule_render(&output); + state.backend.schedule_render(&output, false); } } } diff --git a/src/lib.rs b/src/lib.rs index aa31a4641..88ed51b13 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -213,7 +213,7 @@ pub fn run(hooks: crate::hooks::Hooks) -> Result<(), Box> { let shell = state.common.shell.read(); if shell.animations_going() { for output in shell.outputs().cloned().collect::>().into_iter() { - state.backend.schedule_render(&output); + state.backend.schedule_render(&output, false); } } } diff --git a/src/libei.rs b/src/libei.rs index ad3a674d4..efaac511e 100644 --- a/src/libei.rs +++ b/src/libei.rs @@ -142,7 +142,7 @@ pub fn setup_ei( data.process_input_event(other, backend_id); if matches!(data.backend, BackendData::Kms(_)) { for output in data.common.shell.read().outputs() { - data.backend.kms().schedule_render(output); + data.backend.kms().schedule_render(output, false); } } } diff --git a/src/shell/focus/mod.rs b/src/shell/focus/mod.rs index cab37a858..c687090ed 100644 --- a/src/shell/focus/mod.rs +++ b/src/shell/focus/mod.rs @@ -6,7 +6,7 @@ use crate::{ }; use indexmap::IndexSet; use smithay::{ - desktop::{PopupUngrabStrategy, layer_map_for_output}, + desktop::{PopupUngrabStrategy, layer_map_for_output, space::SpaceElement}, input::{Seat, pointer::MotionEvent}, output::Output, reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface}, @@ -311,6 +311,7 @@ impl Shell { } let workspace = &mut set.workspaces[set.active]; + for fs in workspace.get_fullscreen_surfaces() { let is_focused = self.seats.iter().any(|seat| { if let Some(KeyboardFocusTarget::Fullscreen(s)) = @@ -324,6 +325,58 @@ impl Shell { fs.surface.set_activated(is_focused); fs.surface.send_configure(); } + + let overview_is_open = crate::utils::quirks::workspace_overview_is_open(&output); + let has_foreground_fullscreen = !overview_is_open + && self.seats.iter().any(|seat| { + if let Some(fs) = workspace.get_fullscreen(seat) { + let focus_stack = workspace.focus_stack.get(seat); + let last_focused = focus_stack.last(); + let focus_stack_is_valid_fullscreen = + last_focused.is_some_and(|target| match target { + FocusTarget::Fullscreen(s) => s == &fs.surface, + FocusTarget::Window(w) => w.active_window() == fs.surface, + }); + + let seat_active_here = seat + .focused_output() + .is_some_and(|active_output| active_output == output); + + let is_foreground = { + let is_focused = if seat_active_here { + seat.get_keyboard() + .and_then(|k| k.current_focus()) + .and_then(|focus| focus.active_window()) + .is_some_and(|surface| surface == fs.surface) + } else { + focus_stack_is_valid_fullscreen + }; + is_focused + }; + + let current_geo = SpaceElement::geometry(&fs.surface).as_local(); + let output_geo = output.geometry().to_local(&output); + + let is_really_fullscreen = current_geo.loc.x <= 1 + && current_geo.loc.y <= 1 + && (current_geo.size.w - output_geo.size.w).abs() <= 1 + && (current_geo.size.h - output_geo.size.h).abs() <= 1; + + if is_foreground && is_really_fullscreen { + output.set_fullscreen_occupied(Some(fs.surface.clone())); + true + } else { + false + } + } else { + false + } + }); + + if !has_foreground_fullscreen { + output.set_fullscreen_occupied(None); + } + for focused in focused_windows.iter() { raise_with_children(&mut workspace.floating_layer, focused); } diff --git a/src/state.rs b/src/state.rs index 5ccc7a7b0..358484d21 100644 --- a/src/state.rs +++ b/src/state.rs @@ -385,13 +385,13 @@ impl BackendData { } } - pub fn schedule_render(&mut self, output: &Output) { + pub fn schedule_render(&mut self, output: &Output, is_fullscreen: bool) { match self { BackendData::Winit(_) => {} // We cannot do this on the winit backend. // Winit has a very strict render-loop and skipping frames breaks atleast the wayland winit-backend. // Swapping with damage (which should be empty on these frames) is likely good enough anyway. BackendData::X11(state) => state.schedule_render(output), - BackendData::Kms(state) => state.schedule_render(output), + BackendData::Kms(state) => state.schedule_render(output, is_fullscreen), _ => unreachable!("No backend was initialized"), } } @@ -607,7 +607,7 @@ impl LockedBackend<'_> { // Winit has a very strict render-loop and skipping frames breaks atleast the wayland winit-backend. // Swapping with damage (which should be empty on these frames) is likely good enough anyway. LockedBackend::X11(state) => state.schedule_render(&output), - LockedBackend::Kms(state) => state.schedule_render(&output), + LockedBackend::Kms(state) => state.schedule_render(&output, false), } } diff --git a/src/utils/prelude.rs b/src/utils/prelude.rs index 2baae7724..872f15054 100644 --- a/src/utils/prelude.rs +++ b/src/utils/prelude.rs @@ -1,4 +1,5 @@ use cosmic_comp_config::output::comp::{AdaptiveSync, OutputConfig, OutputState}; +use parking_lot::RwLock; use smithay::{ backend::drm::VrrSupport as Support, output::{Output, WeakOutput}, @@ -9,7 +10,10 @@ pub use super::geometry::*; pub use crate::shell::{SeatExt, Shell, Workspace}; pub use crate::state::{Common, State}; pub use crate::wayland::handlers::xdg_shell::popup::update_reactive_popups; -use crate::{config::EdidProduct, shell::zoom::OutputZoomState}; +use crate::{ + config::EdidProduct, + shell::{CosmicSurface, element::surface::WeakCosmicSurface, zoom::OutputZoomState}, +}; use std::{ cell::{Ref, RefCell, RefMut}, @@ -36,11 +40,15 @@ pub trait OutputExt { fn config_mut(&self) -> RefMut<'_, OutputConfig>; fn edid(&self) -> Option<&EdidProduct>; + + fn set_fullscreen_occupied(&self, surface: Option); + fn is_foreground_fullscreen_occupied(&self) -> Option; } struct Vrr(AtomicU8); struct VrrSupport(AtomicU8); struct Mirroring(Mutex>); +struct FullscreenOccupied(RwLock>); impl OutputExt for Output { fn is_internal(&self) -> bool { @@ -167,4 +175,21 @@ impl OutputExt for Output { fn edid(&self) -> Option<&EdidProduct> { self.user_data().get() } + + fn set_fullscreen_occupied(&self, surface: Option) { + let user_data = self.user_data(); + user_data + .insert_if_missing_threadsafe(|| FullscreenOccupied(parking_lot::RwLock::new(None))); + let lock = &user_data.get::().unwrap().0; + if lock.read().as_ref().and_then(|weak| weak.upgrade()) == surface { + return; + } + *lock.write() = surface.map(|s| s.downgrade()); + } + + fn is_foreground_fullscreen_occupied(&self) -> Option { + self.user_data() + .get::() + .and_then(|state| state.0.read().as_ref().and_then(|weak| weak.upgrade())) + } } diff --git a/src/wayland/handlers/compositor.rs b/src/wayland/handlers/compositor.rs index 12bc2e565..b5a966604 100644 --- a/src/wayland/handlers/compositor.rs +++ b/src/wayland/handlers/compositor.rs @@ -28,7 +28,11 @@ use smithay::{ }, xwayland::XWaylandClientData, }; -use std::{collections::VecDeque, sync::Mutex, time::Duration}; +use std::{collections::VecDeque, sync::LazyLock, sync::Mutex, time::Duration}; + +pub static FULLSCREEN_IMMEDIATE_RENDER: LazyLock = LazyLock::new(|| { + crate::utils::env::bool_var("COSMIC_FULLSCREEN_IMMEDIATE_RENDER").unwrap_or(true) +}); fn toplevel_ensure_initial_configure( toplevel: &ToplevelSurface, @@ -271,7 +275,13 @@ impl CompositorHandler for State { // schedule a new render if let Some(output) = shell.visible_output_for_surface(surface) { - self.backend.schedule_render(output); + let is_fullscreen = + output + .is_foreground_fullscreen_occupied() + .is_some_and(|cosmic_surface| { + cosmic_surface.has_surface(surface, WindowSurfaceType::ALL) + }); + self.backend.schedule_render(output, is_fullscreen); } if mapped { diff --git a/src/wayland/handlers/image_copy_capture/mod.rs b/src/wayland/handlers/image_copy_capture/mod.rs index 85a01c9a5..269f75d12 100644 --- a/src/wayland/handlers/image_copy_capture/mod.rs +++ b/src/wayland/handlers/image_copy_capture/mod.rs @@ -295,7 +295,7 @@ impl ImageCopyCaptureHandler for State { }; output.add_frame(session.clone(), frame); - self.backend.schedule_render(&output); + self.backend.schedule_render(&output, false); } ImageCaptureSourceKind::Workspace(handle) => { render_workspace_to_buffer(self, session, frame, handle) diff --git a/src/wayland/handlers/layer_shell.rs b/src/wayland/handlers/layer_shell.rs index 9653557d7..0555c1029 100644 --- a/src/wayland/handlers/layer_shell.rs +++ b/src/wayland/handlers/layer_shell.rs @@ -77,7 +77,7 @@ impl WlrLayerShellHandler for State { shell.workspaces.recalculate(); - self.backend.schedule_render(&output); + self.backend.schedule_render(&output, false); } } } diff --git a/src/wayland/handlers/session_lock.rs b/src/wayland/handlers/session_lock.rs index 39e1e3759..4c81cf046 100644 --- a/src/wayland/handlers/session_lock.rs +++ b/src/wayland/handlers/session_lock.rs @@ -38,7 +38,7 @@ impl SessionLockHandler for State { }); for output in shell.outputs() { - self.backend.schedule_render(output); + self.backend.schedule_render(output, false); } } @@ -47,7 +47,7 @@ impl SessionLockHandler for State { shell.session_lock = None; for output in shell.outputs() { - self.backend.schedule_render(output); + self.backend.schedule_render(output, false); } } diff --git a/src/wayland/handlers/xdg_shell/mod.rs b/src/wayland/handlers/xdg_shell/mod.rs index b4f16c4c2..2a4365192 100644 --- a/src/wayland/handlers/xdg_shell/mod.rs +++ b/src/wayland/handlers/xdg_shell/mod.rs @@ -385,7 +385,7 @@ impl XdgShellHandler for State { } if let Some(output) = output.as_ref() { - self.backend.schedule_render(output); + self.backend.schedule_render(output, false); } } diff --git a/src/xwayland.rs b/src/xwayland.rs index da3fb36b8..d8a480741 100644 --- a/src/xwayland.rs +++ b/src/xwayland.rs @@ -906,7 +906,7 @@ impl XwmHandler for State { } for output in outputs.into_iter() { - self.backend.schedule_render(&output); + self.backend.schedule_render(&output, false); } }