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}