Skip to main content

gpui_runtime/
pipeline.rs

1//! A [`FramePipeline`] that times the passes a frame is made of.
2//!
3//! This is also the proof that the pipeline SPI crosses the crate boundary:
4//! everything here is built from `gpui_authoring`'s public surface, so a host
5//! program or an embedder can do the same from outside the framework.
6
7use std::{
8    cell::RefCell,
9    rc::Rc,
10    time::{Duration, Instant},
11};
12
13use gpui_authoring::{App, FocusId, FramePipeline, PreparedRoots, Window, WindowMetrics};
14
15/// What an [`InstrumentedPipeline`] has measured.
16///
17/// Durations accumulate across frames, so divide by [`frames`](Self::frames) for
18/// an average. A frame drives the root passes once unless the pipeline defers it,
19/// so `evaluate_passes` and [`frames`](Self::frames) differ only for a frame that
20/// opened without drawing its roots.
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22pub struct PhaseMetrics {
23    /// Frames driven.
24    pub frames: usize,
25    /// Times the roots were gathered.
26    pub evaluate_passes: usize,
27    /// Times the roots were laid out and prepainted.
28    pub layout_passes: usize,
29    /// Times the roots were painted.
30    pub paint_passes: usize,
31    /// Time spent gathering the roots.
32    pub evaluate: Duration,
33    /// Time spent laying out and prepainting the roots.
34    pub layout: Duration,
35    /// Time spent painting the roots.
36    pub paint: Duration,
37}
38
39impl PhaseMetrics {
40    /// The time the root passes took in total.
41    pub fn root_passes(&self) -> Duration {
42        self.evaluate + self.layout + self.paint
43    }
44}
45
46/// Composition for pipelines.
47///
48/// A pipeline can wrap another one and leave everything it does not change to it,
49/// which is how independent concerns — capping the frame rate, timing the passes,
50/// inspecting what a frame is about to draw — stack in any order.
51///
52/// A decorator forwards the passes it does not change to the pipeline it wraps;
53/// the default [`draw`](FramePipeline::draw) then runs those forwards as one
54/// frame. A pass it forgets to forward falls back to the standard implementation
55/// of that pass, not the inner pipeline's, so a decorator must forward every pass
56/// it wants the inner pipeline to decide.
57pub trait FramePipelineExt: FramePipeline + Sized {
58    /// Caps this pipeline at `max_fps` frames a second.
59    ///
60    /// See [`ThrottledPipeline`].
61    fn max_fps(self, max_fps: u32) -> ThrottledPipeline<Self> {
62        ThrottledPipeline::new(self, max_fps)
63    }
64
65    /// Times this pipeline's root passes, recording into `metrics`.
66    ///
67    /// See [`InstrumentedPipeline`].
68    fn instrumented(self, metrics: Rc<RefCell<PhaseMetrics>>) -> InstrumentedPipeline<Self> {
69        InstrumentedPipeline::new(self, metrics)
70    }
71}
72
73impl<P: FramePipeline> FramePipelineExt for P {}
74
75/// Defers frames that arrive sooner than a target rate.
76///
77/// Changes only [`should_render`](FramePipeline::should_render) and forwards every
78/// other pass to the pipeline it wraps, so the wrapped pipeline draws exactly as it
79/// would have. Wrap the standard pipeline to cap an application's frame rate, or
80/// wrap another decorator — `.max_fps(30)` reads the same on an
81/// [`InstrumentedPipeline`] as on a
82/// [`StandardImmediatePipeline`][gpui_authoring::StandardImmediatePipeline].
83///
84/// A deferred frame is not a dropped one. It leaves the window dirty and leaves
85/// what the frame would have done pending, so the next frame draws it — including
86/// when the platform asks for a frame on its own, which it does while the window is
87/// dirty. Deferring does not need to schedule that next frame: `should_render` is
88/// asked before a frame's work starts, with no window to schedule one with.
89pub struct ThrottledPipeline<P> {
90    inner: P,
91    min_interval: Duration,
92    last_frame: Option<Instant>,
93}
94
95impl<P> ThrottledPipeline<P> {
96    /// Limits `inner` to `max_fps` frames a second.
97    ///
98    /// Zero is treated as one frame a second. A pipeline that should never draw is
99    /// what answering `false` from `should_render` is for.
100    pub fn new(inner: P, max_fps: u32) -> Self {
101        Self {
102            inner,
103            min_interval: Duration::from_secs_f64(1.0 / f64::from(max_fps.max(1))),
104            last_frame: None,
105        }
106    }
107
108    /// The pipeline this one wraps.
109    pub fn inner(&self) -> &P {
110        &self.inner
111    }
112}
113
114impl<P: FramePipeline> FramePipeline for ThrottledPipeline<P> {
115    fn should_render(&mut self, is_dirty: bool, metrics: &WindowMetrics) -> bool {
116        // Ask the pipeline underneath first: it may have its own reason to defer,
117        // and a frame it defers should not spend this one's slot.
118        if !self.inner.should_render(is_dirty, metrics) {
119            return false;
120        }
121
122        let now = Instant::now();
123        // Frames are only delivered at refresh boundaries, so a target interval that
124        // is an exact multiple of the refresh period (30 fps on a 60 Hz display) sits
125        // on a knife's edge: sub-millisecond jitter in delivery would defer a frame
126        // that should have drawn, dropping the rate to the next-lower multiple
127        // (20 fps). Allowing a frame to arrive up to a quarter of the interval early
128        // absorbs that jitter and keeps the cap on its intended boundary.
129        let min_interval = self.min_interval - self.min_interval / 4;
130        if let Some(last_frame) = self.last_frame
131            && now.duration_since(last_frame) < min_interval
132        {
133            return false;
134        }
135
136        self.last_frame = Some(now);
137        true
138    }
139
140    fn begin_frame(&mut self, window: &mut Window<'_>, cx: &mut App) {
141        self.inner.begin_frame(window, cx);
142    }
143
144    fn evaluate_roots(&mut self, window: &mut Window<'_>, cx: &mut App) -> PreparedRoots {
145        self.inner.evaluate_roots(window, cx)
146    }
147
148    fn layout_roots(&mut self, window: &mut Window<'_>, roots: &mut PreparedRoots, cx: &mut App) {
149        self.inner.layout_roots(window, roots, cx);
150    }
151
152    fn paint_roots(&mut self, window: &mut Window<'_>, roots: PreparedRoots, cx: &mut App) {
153        self.inner.paint_roots(window, roots, cx);
154    }
155
156    fn finish_frame(&mut self, window: &mut Window<'_>, cx: &mut App) {
157        self.inner.finish_frame(window, cx);
158    }
159
160    fn complete_frame(&mut self, window: &mut Window<'_>, cx: &mut App) -> Option<FocusId> {
161        self.inner.complete_frame(window, cx)
162    }
163
164    fn end_frame(
165        &mut self,
166        window: &mut Window<'_>,
167        cx: &mut App,
168        focus_before_listeners: Option<FocusId>,
169    ) {
170        self.inner.end_frame(window, cx, focus_before_listeners);
171    }
172}
173
174/// Times the root passes of the pipeline it wraps, recording into the
175/// [`PhaseMetrics`] the caller holds.
176///
177/// Wrap the standard pipeline to time the standard frame, or wrap another
178/// decorator — `.max_fps(30).instrumented(metrics)` stacks a rate cap under the
179/// timings, in either order.
180///
181/// ```no_run
182/// # use std::{cell::RefCell, rc::Rc};
183/// # use gpui_authoring::StandardImmediatePipeline;
184/// # use gpui_runtime::{Application, FramePipelineExt, PhaseMetrics};
185/// # fn example(application: Application) -> Application {
186/// let metrics = Rc::new(RefCell::new(PhaseMetrics::default()));
187/// application.with_frame_pipeline({
188///     let metrics = metrics.clone();
189///     move |_window_id| Box::new(StandardImmediatePipeline.instrumented(metrics.clone()))
190/// })
191/// # }
192/// ```
193///
194/// [app]: crate::Application::with_frame_pipeline
195pub struct InstrumentedPipeline<P> {
196    inner: P,
197    metrics: Rc<RefCell<PhaseMetrics>>,
198}
199
200impl<P> InstrumentedPipeline<P> {
201    /// Wraps `inner`, recording its root passes into `metrics`, which the caller
202    /// keeps in order to read what it measures.
203    pub fn new(inner: P, metrics: Rc<RefCell<PhaseMetrics>>) -> Self {
204        Self { inner, metrics }
205    }
206
207    /// The pipeline this one wraps.
208    pub fn inner(&self) -> &P {
209        &self.inner
210    }
211}
212
213impl<P: FramePipeline> FramePipeline for InstrumentedPipeline<P> {
214    fn should_render(&mut self, is_dirty: bool, metrics: &WindowMetrics) -> bool {
215        self.inner.should_render(is_dirty, metrics)
216    }
217
218    fn begin_frame(&mut self, window: &mut Window<'_>, cx: &mut App) {
219        self.metrics.borrow_mut().frames += 1;
220        self.inner.begin_frame(window, cx);
221    }
222
223    fn evaluate_roots(&mut self, window: &mut Window<'_>, cx: &mut App) -> PreparedRoots {
224        let start = Instant::now();
225        let roots = self.inner.evaluate_roots(window, cx);
226        let mut metrics = self.metrics.borrow_mut();
227        metrics.evaluate += start.elapsed();
228        metrics.evaluate_passes += 1;
229        roots
230    }
231
232    fn layout_roots(&mut self, window: &mut Window<'_>, roots: &mut PreparedRoots, cx: &mut App) {
233        let start = Instant::now();
234        self.inner.layout_roots(window, roots, cx);
235        let mut metrics = self.metrics.borrow_mut();
236        metrics.layout += start.elapsed();
237        metrics.layout_passes += 1;
238    }
239
240    fn paint_roots(&mut self, window: &mut Window<'_>, roots: PreparedRoots, cx: &mut App) {
241        let start = Instant::now();
242        self.inner.paint_roots(window, roots, cx);
243        let mut metrics = self.metrics.borrow_mut();
244        metrics.paint += start.elapsed();
245        metrics.paint_passes += 1;
246    }
247
248    fn finish_frame(&mut self, window: &mut Window<'_>, cx: &mut App) {
249        self.inner.finish_frame(window, cx);
250    }
251
252    fn complete_frame(&mut self, window: &mut Window<'_>, cx: &mut App) -> Option<FocusId> {
253        self.inner.complete_frame(window, cx)
254    }
255
256    fn end_frame(&mut self, window: &mut Window<'_>, cx: &mut App, focus: Option<FocusId>) {
257        self.inner.end_frame(window, cx, focus);
258    }
259}