srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/winit/capture.rs
blob: 6e03dd824976b5daf37e8ac2ad5fcd2167f9054c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
use super::*;

impl WaylandPlatform {

    /// Re-renders the current scene into an offscreen GLES renderbuffer and
    /// serves the queued screencopy captures from it. See the call site for
    /// why capture cannot read the window surface directly.
    pub(super) fn capture_offscreen(&mut self, captures: Vec<screencopy::PendingCapture>) -> PlatformResult<()> {
        use smithay::backend::renderer::{Bind, Offscreen};

        let size = self.output.current_mode().map(|m| m.size).unwrap_or_default();
        if size.w <= 0 || size.h <= 0 {
            return Ok(());
        }
        let renderer = self.backend.renderer();
        let mut target: smithay::backend::renderer::gles::GlesRenderbuffer =
            renderer.create_buffer(Fourcc::Abgr8888, (size.w, size.h).into()).map_err(err)?;
        let mut framebuffer = renderer.bind(&mut target).map_err(err)?;

        // WHAT THIS PASS STILL DOES NOT DRAW: border strips, and the
        // desktop icon grid. Everything else the on-screen loop draws is
        // covered - content, titlebars, shadows, layer-shell surfaces,
        // popups, both menus, the Snap-Layouts flyout and the drag snap
        // preview.
        //
        // Keep this list honest. The whole reason the nested backend exists
        // is checking behaviour with `grim`, and a tier missing here makes
        // a working feature photograph as broken. That has now cost four
        // separate investigations in one day (popups, shadows, the drag
        // snap preview, the desktop menu), every one of them starting from
        // a screenshot that was quietly lying.
        // Content (with each window's own `opacity`, unlike the
        // `self.state.space`-based single-alpha call this replaced) and the
        // bar/dock now render into the capture, at least: a screenshot used
        // to only ever show titlebars for windows that had one, and never
        // any layer-shell surface at all.
        let hide_top_layers = self.wm.borrow().visible_windows_front_to_back().any(|w| w.fullscreen);
        let mut custom_elements: Vec<crate::elements::OverlayElement<GlesRenderer>> = Vec::new();
        // Popups first, so they land above everything else - exactly the
        // order both on-screen render loops already use.
        //
        // This pass had no popup step at all, so no tooltip, dropdown or
        // right-click menu could ever appear in a screenshot taken on this
        // backend, no matter how correctly it was drawn on screen. That is
        // a capture-only gap, not a rendering one: the DRM backend serves
        // screencopy out of its own on-screen frame (`udev/render.rs`
        // drains `screencopy_pending` and hands `service_pending` the same
        // framebuffer it just drew), so it never had the gap; this backend
        // renders the scene a second time into an offscreen buffer, and
        // that second scene was missing a tier.
        //
        // It cost real time to find. The whole point of the nested backend
        // is validating behaviour with `grim`, and this made `grim` state
        // the opposite of the truth about every popup: a menu that drew
        // perfectly on screen photographed as absent, which reads exactly
        // like the client never opened one.
        let popup_targets = crate::elements::popup_targets(&self.state);
        custom_elements.extend(crate::elements::popup_render_elements(&popup_targets, renderer, (0, 0)));
        // The titlebar context menu and the desktop/desktop-icon menu, both
        // topmost, in the same order the on-screen loop draws them.
        for (menu_pos, buffer) in [
            (self.state.context_menu.as_ref().map(|m| m.pos), self.state.context_menu_buffer.as_ref()),
            (self.state.desktop_menu.as_ref().map(|m| m.pos), self.state.desktop_menu_buffer.as_ref()),
        ] {
            let (Some(pos), Some(buffer)) = (menu_pos, buffer) else { continue };
            match MemoryRenderBufferRenderElement::from_buffer(renderer, (pos.0 as f64, pos.1 as f64), buffer, None, None, None, Kind::Unspecified) {
                Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
                Err(e) => log::warn!("screencopy: failed to import menu buffer: {e}"),
            }
        }
        // The Snap-Layouts flyout and the drag snap preview, in the same
        // order the on-screen loop draws them. Both are drag-time overlays,
        // and a screenshot that omits them cannot be used to check either
        // one - which is exactly how this pass's missing popup tier wasted
        // a session already.
        if let (Some(flyout), Some(buffer)) = (self.state.snap_flyout.as_ref(), self.state.snap_flyout_buffer.as_ref()) {
            let pos = (flyout.pos.0 as f64, flyout.pos.1 as f64);
            match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, buffer, None, None, None, Kind::Unspecified) {
                Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
                Err(e) => log::warn!("screencopy: failed to import snap flyout buffer: {e}"),
            }
        }
        if let Some(rect) = self.wm.borrow().drag_snap_preview() {
            let accent = self.wm.borrow().theme.default_border_color;
            custom_elements.extend(
                crate::elements::snap_preview_elements(&mut self.state.snap_preview_buffers, rect, accent, (0, 0))
                    .into_iter()
                    .map(crate::elements::OverlayElement::Solid),
            );
        }
        if !hide_top_layers {
            custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, |layer| matches!(layer, Layer::Top | Layer::Overlay)));
        }
        // Front-to-back, so a window already pushed occludes everything a
        // later one draws - accumulated for the shadow clip below, exactly
        // as both on-screen render loops do it.
        let monitor_bounds: Vec<srdwm_core::Rect> = self.wm.borrow().monitors().iter().map(|m| m.full_geometry).collect();
        let mut occluders: Vec<srdwm_core::Rect> = Vec::new();
        let focused = self.wm.borrow().focused_id();
        for id in self.wm.borrow().visible_windows_front_to_back().map(|w| w.id).collect::<Vec<_>>() {
            // Matches the render loops: a capture must not show a frame the
            // screen does not (see `window_has_content`).
            if !crate::state::CompState::has_content(&self.state.windows_shown_once, &self.state.id_to_window, id) {
                continue;
            }
            let Some(w) = self.wm.borrow().window(id).cloned() else { continue };
            if let Some(deco) = self.state.decorations.get(&id) {
                if let Ok(elem) = MemoryRenderBufferRenderElement::from_buffer(renderer, (w.geometry.x as f64, w.geometry.y as f64), deco, None, None, None, Kind::Unspecified) {
                    custom_elements.push(crate::elements::OverlayElement::Memory(elem));
                }
            }
            if let Some(dwindow) = self.state.id_to_window.get(&id) {
                if let Some(surface) = crate::elements::window_wl_surface(dwindow) {
                    let band = if w.decorated { srdwm_core::TITLEBAR_HEIGHT as i32 } else { 0 };
                    // `content_offset`: same `xdg_surface.set_window_geometry`
                    // subtraction every other render/capture path in this
                    // codebase already does (`udev/render.rs`, `winit/
                    // render.rs`, `udev/capture.rs`) - missed here
                    // specifically. A CSD client's invisible shadow margin
                    // landed at `w.geometry.x, w.geometry.y + band` instead
                    // of its real visible content, so a screenshot taken on
                    // this backend showed the same content_offset-sized gap
                    // the on-screen render loops already had fixed.
                    let content_offset = dwindow.geometry().loc;
                    let pos = (w.geometry.x - content_offset.x, w.geometry.y + band - content_offset.y);
                    custom_elements.extend(crate::elements::surface_content_elements(renderer, &surface, pos, w.opacity));
                }
            }
            // The drop shadow, last in this window's own group so it sits
            // under its own decoration and content but still over every
            // window behind it. `shadow_buffers` only holds an entry for a
            // window that is meant to have one at all (see
            // `state/lifecycle.rs`), so no separate floating/maximized
            // check belongs here.
            //
            // Shadows were the other half of this pass's missing tier: a
            // screenshot taken on this backend showed no shadow on any
            // window, which is exactly the thing a shadow bug gets reported
            // and re-checked by. Border strips are still absent - a real
            // remaining gap, called out here rather than left silent.
            if let Some(shadow) = self.state.shadow_buffers.get(&id) {
                // `effective_frame`, not `w.geometry` - the same correction
                // both on-screen render loops apply before sizing any
                // decoration bitmap (see its own doc comment). The shadow
                // buffer was rasterised at the *frame's* size, and `src`
                // below indexes into that buffer, so measuring from
                // `w.geometry` instead reads the wrong region whenever the
                // two differ. They differ for exactly the windows that
                // matter here: a CSD client whose committed surface is not
                // the size this compositor asked for. Found by sampling
                // pixels - Alacritty's shadow appeared in a capture and
                // Nemo's did not, and the only difference was that gap.
                let frame = self.state.effective_frame(id, w.geometry);
                let full = crate::decoration::shadow_rect(frame);
                let rect = crate::decoration::shadow_rect_clipped(frame, &monitor_bounds);
                for fragment in crate::elements::visible_border_fragments(rect, &occluders) {
                    let src = Rectangle::new(
                        Point::from(((fragment.x - full.x) as f64, (fragment.y - full.y) as f64)),
                        Size::from((fragment.width as f64, fragment.height as f64)),
                    );
                    match MemoryRenderBufferRenderElement::from_buffer(renderer, (fragment.x as f64, fragment.y as f64), shadow, None, Some(src), None, Kind::Unspecified) {
                        Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
                        Err(e) => log::warn!("screencopy: failed to import shadow buffer for window {id}: {e}"),
                    }
                }
            }
            // Border strips, as plain solid fills.
            //
            // The on-screen loop draws the top and bottom strips from
            // cached bitmaps so their corners round into the titlebar's
            // curve, and only the sides as fills. This pass approximates
            // all four with fills: corner rounding is not reproduced, so a
            // capture is not pixel-exact at the four corners. Everything
            // that matters for checking a border - whether one is drawn at
            // all, where, how thick, and in what colour - is faithful.
            //
            // Without this a screenshot could not answer "is there a border
            // here", which is exactly the question a border bug asks. That
            // gap was documented and then walked into twice: a maximize
            // border fix was checked against a capture that never draws
            // borders, and the control silently passed for the wrong
            // reason both times.
            if w.border_width > 0 && !w.maximized {
                let color = crate::state::effective_border_color(w.border_color, focused == Some(id), self.wm.borrow().theme.border_inactive_dim);
                let frame = self.state.effective_frame(id, w.geometry);
                for (index, strip) in crate::decoration::border_strips(frame, w.border_width).into_iter().enumerate() {
                    for fragment in crate::elements::visible_border_fragments(strip, &occluders) {
                        let buf = crate::elements::border_fragment_buffer(&mut self.state.capture_border_buffers, index);
                        custom_elements.push(crate::elements::OverlayElement::Solid(crate::elements::border_side_render_element(buf, fragment, color, (0, 0))));
                    }
                }
            }
            occluders.push(w.geometry);
        }
        custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, |layer| matches!(layer, Layer::Background | Layer::Bottom)));

        // A throwaway damage tracker, so this pass always draws the whole
        // scene (age 0) and never perturbs the on-screen tracker's history.
        let mut tracker = OutputDamageTracker::from_output(&self.output);
        tracker
            .render_output(renderer, &mut framebuffer, 0, &custom_elements, [0.05, 0.05, 0.08, 1.0])
            .map_err(err)?;

        screencopy::service_pending(captures, renderer, &framebuffer);
        Ok(())
    }
}