compositing/
screenshot.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::RefCell;
6use std::rc::Rc;
7
8use base::Epoch;
9use base::id::{PipelineId, WebViewId};
10use embedder_traits::ScreenshotCaptureError;
11use euclid::{Point2D, Size2D};
12use image::RgbaImage;
13use rustc_hash::FxHashMap;
14use webrender_api::units::{DeviceIntRect, DeviceRect};
15
16use crate::IOCompositor;
17use crate::compositor::RepaintReason;
18
19pub(crate) struct ScreenshotRequest {
20    webview_id: WebViewId,
21    rect: Option<DeviceRect>,
22    callback: Box<dyn FnOnce(Result<RgbaImage, ScreenshotCaptureError>) + 'static>,
23    phase: ScreenshotRequestPhase,
24}
25
26/// Screenshots requests happen in three phases:
27#[derive(PartialEq)]
28pub(crate) enum ScreenshotRequestPhase {
29    /// A request is sent to the Constellation, asking each Pipeline in a WebView,
30    /// to report the display list epoch to render for the screenshot. Each Pipeline
31    /// will wait to send an epoch that happens after the Pipeline is ready in a
32    /// variety of ways:
33    ///
34    ///  - The `load` event has fired.
35    ///  - All render blocking elements are no longer blocking the rendering.
36    ///  - All images are loaded and displayed.
37    ///  - All web fonts are loaded.
38    ///  - The `reftest-wait` and `test-wait` classes have been removed from the root element.
39    ///  - The rendering is up-to-date
40    ///
41    /// When all Pipelines have reported this epoch to the Constellation it sends a
42    /// ScreenshotReadinessResponse back to the renderer.
43    ConstellationRequest,
44    /// The renderer has received the ScreenshotReadinessReponse from the Constellation
45    /// and is now waiting for all display lists to be received from the Pipelines and
46    /// sent to WebRender.
47    WaitingOnPipelineDisplayLists(Rc<FxHashMap<PipelineId, Epoch>>),
48    /// Once the renderer has received all of the Pipeline display lists necessary to take
49    /// the screenshot and uploaded them to WebRender, it waits for an appropriate frame to
50    /// be ready. Currently this just waits for the [`FrameDelayer`] to stop delaying frames
51    /// and for there to be no pending WebRender frames (ones sent to WebRender that are not
52    /// ready yet). Once that happens, and a potential extra repaint is triggered, the renderer
53    /// will take the screenshot and fufill the request.
54    WaitingOnFrame,
55}
56
57#[derive(Default)]
58pub(crate) struct ScreenshotTaker {
59    /// A vector of pending screenshots to be taken. These will be resolved once the
60    /// pages have finished loading all content and the rendering reflects the finished
61    /// state. See [`ScreenshotRequestPhase`] for more information.
62    requests: RefCell<Vec<ScreenshotRequest>>,
63}
64
65impl ScreenshotTaker {
66    pub(crate) fn request_screenshot(
67        &self,
68        webview_id: WebViewId,
69        rect: Option<DeviceRect>,
70        callback: Box<dyn FnOnce(Result<RgbaImage, ScreenshotCaptureError>) + 'static>,
71    ) {
72        self.requests.borrow_mut().push(ScreenshotRequest {
73            webview_id,
74            rect,
75            callback,
76            phase: ScreenshotRequestPhase::ConstellationRequest,
77        });
78    }
79
80    pub(crate) fn handle_screenshot_readiness_reply(
81        &self,
82        webview_id: WebViewId,
83        expected_epochs: FxHashMap<PipelineId, Epoch>,
84        renderer: &IOCompositor,
85    ) {
86        let expected_epochs = Rc::new(expected_epochs);
87
88        for screenshot_request in self.requests.borrow_mut().iter_mut() {
89            if screenshot_request.webview_id != webview_id ||
90                screenshot_request.phase != ScreenshotRequestPhase::ConstellationRequest
91            {
92                continue;
93            }
94            screenshot_request.phase =
95                ScreenshotRequestPhase::WaitingOnPipelineDisplayLists(expected_epochs.clone());
96        }
97
98        // Maybe when the message is received, the renderer already has the all of the necessary
99        // display lists from the Pipelines. In that case, the renderer should move immediately
100        // to the next phase of the screenshot state machine.
101        self.prepare_screenshot_requests_for_render(renderer);
102    }
103
104    pub(crate) fn prepare_screenshot_requests_for_render(&self, renderer: &IOCompositor) {
105        let mut any_became_ready = false;
106
107        for screenshot_request in self.requests.borrow_mut().iter_mut() {
108            let ScreenshotRequestPhase::WaitingOnPipelineDisplayLists(pipelines) =
109                &screenshot_request.phase
110            else {
111                continue;
112            };
113
114            let Some(webview) = renderer.webview_renderer(screenshot_request.webview_id) else {
115                continue;
116            };
117
118            if pipelines.iter().all(|(pipeline_id, expected_epoch)| {
119                webview
120                    .pipelines
121                    .get(pipeline_id)
122                    .and_then(|pipeline| pipeline.display_list_epoch)
123                    .is_some_and(|epoch| epoch >= *expected_epoch)
124            }) {
125                screenshot_request.phase = ScreenshotRequestPhase::WaitingOnFrame;
126                any_became_ready = true;
127            }
128        }
129
130        // If there are now screenshots waiting on a frame, and there are no pending frames,
131        // immediately trigger a repaint so that screenshots can be taken when the repaint
132        // is done.
133        if any_became_ready {
134            self.maybe_trigger_paint_for_screenshot(renderer);
135        }
136    }
137
138    pub(crate) fn maybe_trigger_paint_for_screenshot(&self, renderer: &IOCompositor) {
139        if renderer.has_pending_frames() {
140            return;
141        }
142
143        if self.requests.borrow().iter().any(|screenshot_request| {
144            matches!(
145                screenshot_request.phase,
146                ScreenshotRequestPhase::WaitingOnFrame
147            )
148        }) {
149            renderer.set_needs_repaint(RepaintReason::ReadyForScreenshot);
150        }
151    }
152
153    pub(crate) fn maybe_take_screenshots(&self, renderer: &IOCompositor) {
154        if renderer.has_pending_frames() {
155            return;
156        }
157
158        let mut requests = self.requests.borrow_mut();
159        if requests.is_empty() {
160            return;
161        }
162
163        // TODO: This can eventually just be `extract_if`. We need to have ownership
164        // of the ScreenshotRequest in order to call the `FnOnce` callabck.
165        let screenshots = requests.drain(..);
166        *requests = screenshots
167            .filter_map(|screenshot_request| {
168                if !matches!(
169                    screenshot_request.phase,
170                    ScreenshotRequestPhase::WaitingOnFrame
171                ) {
172                    return Some(screenshot_request);
173                }
174
175                let callback = screenshot_request.callback;
176                let Some(webview_renderer) =
177                    renderer.webview_renderer(screenshot_request.webview_id)
178                else {
179                    callback(Err(ScreenshotCaptureError::WebViewDoesNotExist));
180                    return None;
181                };
182
183                let viewport_rect = webview_renderer.rect.to_i32();
184                let viewport_size = viewport_rect.size();
185                let rect = screenshot_request.rect.map_or(viewport_rect, |rect| {
186                    // We need to convert to the bottom-left origin coordinate
187                    // system used by OpenGL
188                    // If dpi > 1, y can be computed to be -1 due to rounding issue, resulting in panic.
189                    // https://github.com/servo/servo/issues/39306#issuecomment-3342204869
190                    let x = rect.min.x as i32;
191                    let y = 0.max(
192                        (viewport_size.height as f32 - rect.min.y - rect.size().height) as i32,
193                    );
194                    let w = rect.size().width as i32;
195                    let h = rect.size().height as i32;
196
197                    DeviceIntRect::from_origin_and_size(Point2D::new(x, y), Size2D::new(w, h))
198                });
199                let result = renderer
200                    .rendering_context()
201                    .read_to_image(rect)
202                    .ok_or(ScreenshotCaptureError::CouldNotReadImage);
203                callback(result);
204                None
205            })
206            .collect();
207    }
208}