From f166df4d979162ae09b439c87cdc64f2b0981308 Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:34:00 +0200 Subject: Split crates/wayland/src/winit.rs (889 lines) into winit/ Pure reorganization, no behavior change - verified by diffing the function-name and struct-name sets before/after (both identical) plus a full cargo test pass. mod.rs keeps the module doc comment, imports, WaylandPlatform's struct definition, and TARGET_FRAME_TIME, plus mod declarations. The rest splits by concern: - connect.rs: connect(), the ~200-line setup/init function. - run.rs: accept_clients, pump_winit - the small per-poll pair. - render.rs: render_frame, the per-frame render loop (left as one intact ~330-line function, same reasoning as udev's render.rs: its structure is deliberate and already documented inline, not a target for further decomposition in a pure reorganization pass). - capture.rs: capture_offscreen, the screencopy path. - events.rs: handle_winit_event. - platform.rs: `impl Platform for WaylandPlatform`. No tests module existed in the original file, so none was split out. A handful of methods/functions (accept_clients, pump_winit, render_frame, capture_offscreen, handle_winit_event) went from private to pub(super): called across what are now sibling submodules, which Rust's privacy model doesn't let see each other's private items. --- crates/wayland/src/winit/render.rs | 331 +++++++++++++++++++++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 crates/wayland/src/winit/render.rs (limited to 'crates/wayland/src/winit/render.rs') diff --git a/crates/wayland/src/winit/render.rs b/crates/wayland/src/winit/render.rs new file mode 100644 index 0000000..3dda278 --- /dev/null +++ b/crates/wayland/src/winit/render.rs @@ -0,0 +1,331 @@ +use super::*; + +impl WaylandPlatform { + + pub(super) fn render_frame(&mut self) -> PlatformResult<()> { + self.state.tick_animations(); + self.state.tick_dirty_broadcasts(); + let size = self.backend.window_size(); + let resized = self.output.current_mode().map(|m| m.size) != Some(size); + if resized { + // Only push a new output mode - and thus emit `wl_output.mode`/ + // `done` - when the size actually changed. This used to run + // unconditionally every frame; harmless with no Wayland-native + // client connected (the only way this was ever exercised before + // real layer-shell clients existed), but a real client bound to + // `wl_output` would otherwise be flooded with duplicate + // mode/done events at the render loop's full frame rate. + self.output.change_current_state(Some(OutputMode { size, refresh: 60_000 }), None, None, None); + layer_map_for_output(&self.output).arrange(); + } + + let age = self.backend.buffer_age().unwrap_or(0); + let (renderer, mut framebuffer) = self.backend.bind().map_err(err)?; + + // Locked: the lock surface over an opaque black clear, and nothing + // else - no windows, no decorations, no layer surfaces. + if self.state.lock.locked { + let lock_surface = self.state.lock_surface_for(&self.output).cloned(); + let elements = lock_render_elements(lock_surface.as_ref(), renderer); + self.damage_tracker + .render_output(renderer, &mut framebuffer, age, &elements, [0.0, 0.0, 0.0, 1.0]) + .map_err(err)?; + drop(framebuffer); + self.backend.submit(None).map_err(err)?; + send_lock_frame(lock_surface.as_ref(), &self.output, self.state.start_time.elapsed()); + self.state.confirm_lock_if_presented(&self.output); + screencopy::fail_pending(std::mem::take(&mut self.state.screencopy_pending)); + return Ok(()); + } + + // `WinitElement`, not `OverlayElement` directly - see + // `rounded_corners.rs`'s own doc comment on why content that gets + // rounded needs a wider element type than everything else here, + // and why that couldn't just be added as a new `OverlayElement` + // variant instead. Every existing push below wraps its + // `OverlayElement` in `WinitElement::Base`; only the new + // rounded-content push (further down) uses `WinitElement::Rounded` + // directly. + let mut custom_elements: Vec = Vec::new(); + // The right-click titlebar menu, if open - pushed first so it's + // topmost over every window (this backend draws no cursor of its + // own, see this module's doc comment, so there's no "stay under + // the pointer" ordering concern like udev.rs's matching push has). + if let (Some(menu), Some(buffer)) = (self.state.context_menu.as_ref(), self.state.context_menu_buffer.as_ref()) { + let pos = (menu.pos.0 as f64, menu.pos.1 as f64); + match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, buffer, None, None, None, Kind::Unspecified) { + Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))), + Err(e) => log::warn!("failed to import context menu buffer: {e}"), + } + } + // Content now renders here too, one window at a time, not through + // `render_output`'s own `spaces` argument - see this function's + // own call to `damage_tracker.render_output` further down for why, + // and `elements.rs`'s `surface_content_elements` doc comment for + // what it's actually for (per-window opacity, impossible through + // `spaces`, which takes one `alpha` for the whole frame). + // + // An earlier version of this exact change was reverted: with two or + // more native Wayland toplevels on screen, whichever was created + // *first* always painted in front of later ones regardless of real + // focus/stacking order. That bug's real root cause (found by + // instrumenting a locally vendored smithay copy directly) turned + // out to be `sync_geometry`'s `Space::map_element` call silently + // re-stacking windows to the top of `Space`'s *own* internal + // order as a side effect of updating position - see `state.rs`'s + // `resync_stacking_order` doc comment for the full story and the + // fix that landed for it (called after every `map_element` since). + // This loop never reads `Space`'s order at all: `ids` below comes + // from `WindowManager.order` (`visible_windows_front_to_back`), + // srdwm's own stacking model, the same source `hit_test` already + // trusts - so the specific bug that sank the earlier attempt + // can't recur here regardless of whether `resync_stacking_order` + // ever drifts again. `self.state.space` stays mapped and + // `resync_stacking_order`-maintained exactly as before; only the + // render step stopped reading from it. + let ids: Vec = self.wm.borrow().visible_windows_front_to_back().map(|w| w.id).collect(); + let focused = self.wm.borrow().focused_id(); + // Popups next: always above every window's own content - see the + // matching comment in `udev.rs`'s render loop for why this has to + // be pushed ahead of both the bar/dock and every window now that + // content shares this same list. + let popup_targets = crate::elements::popup_targets(&self.state); + custom_elements.extend(crate::elements::popup_render_elements(&popup_targets, renderer, (0, 0)).into_iter().map(crate::rounded_corners::WinitElement::Base)); + // The bar/dock/launcher, skipped entirely for a fullscreen window -- + // see `udev.rs`'s matching push for the full reasoning. + let hide_top_layers = self.wm.borrow().visible_windows_front_to_back().any(|w| w.fullscreen); + // `None` (the user's config never touched `general.rounded_corners`) + // defaults to *on* here - this backend has an actual GPU shader + // behind the feature (see `rounded_corners.rs`), no untested + // per-frame CPU cost to weigh the way the udev backend's own + // default has to. + let rounded_corners_enabled = self.wm.borrow().rounded_corners_enabled.unwrap_or(true); + if !hide_top_layers { + custom_elements.extend( + crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Top | Layer::Overlay)) + .into_iter() + .map(crate::rounded_corners::WinitElement::Base), + ); + } + // Windows stacked in front of whichever one border/decoration is + // being built right now - `ids` is already front-to-back, so this + // only ever needs appending to, not recomputing. A window's own + // *content*, pushed inside this same loop below, needs no separate + // occlusion test - see the matching comment in `udev.rs`'s render + // loop for why ordinary front-to-back push order already occludes + // it correctly. The border strips and titlebar bitmap are + // different: outside `geometry`, so they still need `occluders`' + // explicit clip against whichever window is stacked in front. + let mut occluders: Vec = Vec::with_capacity(ids.len()); + for id in ids { + let Some(w) = self.wm.borrow().window(id).cloned() else { continue }; + // `w.geometry` is the animation's target, not necessarily where + // the window is actually drawn this frame - see the matching + // comment in `udev.rs`'s render loop for the full story + // (reported live as the border "not flush" with the window + // during an animated maximize/fullscreen/open-slide transition). + let geom = self.state.window_anims.get(&id).map(crate::state::WindowAnim::current_rect).unwrap_or(w.geometry); + // Same reasoning as udev.rs's matching push: positioned from + // `geom`, not `w.geometry`, and not fragment-clipped against + // `occluders` - see that comment. + if let Some(shadow) = self.state.shadow_buffers.get(&id) { + let rect = decoration::shadow_rect(geom); + let pos = (rect.x as f64, rect.y as f64); + match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, shadow, None, None, None, Kind::Unspecified) { + Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))), + Err(e) => log::warn!("failed to import shadow buffer for window {id}: {e}"), + } + } + if let Some(deco) = self.state.decorations.get(&id) { + // Fragment-clipped, same as udev.rs's matching titlebar + // push - see that comment for why all-or-nothing (skip + // only once *fully* covered) wasn't enough: a titlebar + // only partially covered, the common case for cascaded + // windows, still bled through the covered part. + let titlebar_rect = srdwm_core::Rect::new(geom.x, geom.y, geom.width, srdwm_core::TITLEBAR_HEIGHT); + for fragment in crate::elements::visible_border_fragments(titlebar_rect, &occluders) { + let pos = (fragment.x as f64, fragment.y as f64); + let src = Rectangle::new( + Point::from(((fragment.x - titlebar_rect.x) as f64, (fragment.y - titlebar_rect.y) as f64)), + Size::from((fragment.width as f64, fragment.height as f64)), + ); + match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, deco, None, Some(src), None, Kind::Unspecified) { + Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))), + Err(e) => log::warn!("failed to import titlebar buffer for window {id}: {e}"), + } + } + } + // Border strips sit entirely outside `geometry` (see + // `decoration::border_strips`), so they never overlap this same + // window's own decoration/content pixels - draw order relative + // to those doesn't matter, only relative to other windows'. + if w.border_width > 0 { + let color = crate::state::effective_border_color(w.border_color, focused == Some(id)); + let strips = decoration::border_strips(geom, w.border_width); + // Strip 0 (top) is rounded to match the titlebar underneath + // it - see `render_border_top`'s doc comment - so it's a + // cached bitmap (rebuilt only in `redraw_decoration_buffer`, + // same as the titlebar itself), not rasterized fresh here + // every frame; the other three don't touch a rounded corner + // and stay persistent solid-colour buffers instead - see + // `elements::border_side_render_element`'s doc comment for + // why a per-frame rebuild of either was a real, continuous + // cost, not a cosmetic one. Not fragment-clipped like the + // other three below - see the matching comment in + // `udev.rs` for why the top strip only gets the cheaper + // all-or-nothing occlusion check. + if strips[0].width > 0 && strips[0].height > 0 && !strips[0].subtract_all(&occluders).is_empty() { + if let Some(buffer) = self.state.border_top_decorations.get(&id) { + match MemoryRenderBufferRenderElement::from_buffer(renderer, (strips[0].x as f64, strips[0].y as f64), buffer, None, None, None, Kind::Unspecified) { + Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))), + Err(e) => log::warn!("failed to import top border buffer for window {id}: {e}"), + } + } + } + let pool = self.state.border_side_buffers.entry(id).or_default(); + let mut buf_index = 0; + for strip in &strips[1..] { + if strip.width == 0 || strip.height == 0 { + continue; + } + for fragment in crate::elements::visible_border_fragments(*strip, &occluders) { + let buf = crate::elements::border_fragment_buffer(pool, buf_index); + buf_index += 1; + custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Solid(crate::elements::border_side_render_element(buf, fragment, color, (0, 0))))); + } + } + } + // The window's own content, at its own `opacity` - see the + // matching push in `udev.rs`'s render loop for why. Single + // output at the global origin, so no offset to subtract (see + // `elements.rs`'s doc comment on why `udev.rs`'s per-head call + // does). Rounded via `rounded_corners::rounded_content_element` + // when the feature's on and the shader compiled - a decorated + // window only rounds its bottom two corners (the top two are + // already rounded, on the titlebar's own bitmap, by + // `decoration.rs`), an undecorated/CSD one rounds all four, + // since its content *is* the window's whole visible extent. + // Falls back to the plain, unrounded `surface_content_elements` + // on any failure - no committed buffer yet, a single-pixel + // solid-colour buffer, or the feature simply being off - same + // "always show something over a prettier maybe-nothing" + // reasoning `cursor.rs`'s built-in-arrow fallback already uses. + 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 }; + let pos = (geom.x, geom.y + band); + let rounded = rounded_corners_enabled.then_some(self.state.rounded_corners_program.as_ref()).flatten().and_then(|program| { + let corners = if w.decorated { crate::rounded_corners::RoundedCorners::BOTTOM_ONLY } else { crate::rounded_corners::RoundedCorners::ALL }; + crate::rounded_corners::rounded_content_element(renderer, program, &surface, pos, w.opacity, crate::decoration::CORNER_RADIUS as f32, corners) + }); + match rounded { + Some(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Rounded(elem)), + None => custom_elements.extend(crate::elements::surface_content_elements(renderer, &surface, pos, w.opacity).into_iter().map(crate::rounded_corners::WinitElement::Base)), + } + } + } + occluders.push(geom); + } + // Background/bottom layer-shell (wallpaper engines) last -- + // bottommost, matching smithay's own `space_render_elements` + // ordering, which this whole custom loop now replaces. + custom_elements.extend( + crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Background | Layer::Bottom)) + .into_iter() + .map(crate::rounded_corners::WinitElement::Base), + ); + + // Not `smithay::desktop::space::render_output`: see `udev.rs`'s + // matching call site for why (per-window opacity, fullscreen-aware + // layer-shell inclusion - `custom_elements` above already carries + // everything that wrapper would have built). + let result = self + .damage_tracker + .render_output(renderer, &mut framebuffer, age, &custom_elements, [0.05, 0.05, 0.08, 1.0]) + .map_err(err)?; + let damage_rects: Vec> = result.damage.cloned().unwrap_or_default(); + let has_damage = !damage_rects.is_empty(); + drop(framebuffer); + // Both the buffer swap and the frame-callback notification are + // conditional on real damage now - this used to run + // unconditionally on every call to this function (every ~16ms + // regardless of activity), which told every window it could render + // its next frame whether or not the screen had actually changed. + // Any client using the standard wait-for-frame-callback render + // pattern (most of them) had no reason not to redraw at whatever + // rate this loop cycled, forever - confirmed live on the udev + // backend: wezterm-gui pinned at 140%+ CPU sitting on a fully idle, + // unchanged terminal, from the identical bug there. + // + // That output-wide gate wasn't enough on its own: cursor motion + // alone damages the small region around the pointer, which still + // marked the *whole output* damaged and sent every mapped window a + // callback regardless of whether the cursor was anywhere near it. + // `windows_touched_by_damage` narrows this to windows the actual + // damage rectangles overlap - see its doc comment in elements.rs. + if has_damage { + self.backend.submit(None).map_err(err)?; + let scale = Scale::from(self.output.current_scale().fractional_scale()); + let now = self.state.start_time.elapsed(); + for w in crate::elements::windows_touched_by_damage(&self.state.space, &damage_rects, scale) { + w.send_frame(&self.output, now, None, |_, _| Some(self.output.clone())); + } + } + // Deliberately outside `if has_damage`: the whole point of + // `always_notify` is covering the case where the output has *no* + // damage at all (a fully idle desktop, cursor not moving) but the + // focused/hovered window still has a pending callback it needs + // answered to unblock an input-driven redraw. Nesting this inside + // `if has_damage` (the first version of this fix) meant it only + // ever ran on a tick that already had damage from something else + // happening - i.e. never in the exact scenario it exists for. + // Reported live as clicks in Firefox still doing nothing at all, + // not just intermittently, after the first version of this fix. + { + let pointer_pos = last_pointer_pos(&self.state); + let now = self.state.start_time.elapsed(); + let wm = self.wm.borrow(); + let always_notify = [wm.focused_id(), wm.window_at(pointer_pos.x as i32, pointer_pos.y as i32)]; + drop(wm); + for w in always_notify.into_iter().flatten().filter_map(|id| self.state.id_to_window.get(&id)) { + w.send_frame(&self.output, now, None, |_, _| Some(self.output.clone())); + } + } + // Layer-shell surfaces get their callback every pass, unconditionally + // - NOT folded into the `has_damage` gate above. See the matching + // (much longer) comment in udev.rs's `render_udev_frame`: many + // layer-shell clients (GTK4/AGS among them) drive their entire + // repaint loop off frame callbacks with no independent timer + // fallback, so withholding the callback until *something* on the + // desktop happens to produce damage deadlocks them permanently + // after their first frame - confirmed live, AGS and waybar both + // froze exactly this way. Toplevel windows keep the damage gate + // (that's what fixed the wezterm-gui CPU-burn bug); layer surfaces + // are few, cheap to redraw, and are exactly the periodic-UI-chrome + // case frame callbacks exist to pace. + for layer in layer_map_for_output(&self.output).layers() { + layer.send_frame(&self.output, self.state.start_time.elapsed(), None, |_, _| Some(self.output.clone())); + } + + // Screencopy is serviced *after* the on-screen frame is submitted, + // into its own offscreen buffer - never by reading back the window + // surface. Reading the winit backend's EGL window surface (what an + // earlier version did) reliably killed the GL context: the first + // `grim` capture produced `eglSwapBuffers: BAD_SURFACE` followed by + // `BAD_ALLOC` and "context has been lost", confirmed by A/B-ing the + // same build with only the readback call removed. The offscreen + // detour costs a second scene render, but only on frames where a + // capture was actually requested. + // + // Deliberately placed after the locked-session early return above, + // so a capture requested while the screen is locked can never see + // client content. + let captures = std::mem::take(&mut self.state.screencopy_pending); + if !captures.is_empty() { + if let Err(e) = self.capture_offscreen(captures) { + log::warn!("screencopy: offscreen capture pass failed: {e}"); + } + } + Ok(()) + } +} -- cgit v1.2.3