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