// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! GLES (EGL + FBO) draw paths. //! //! The GPU counterparts to [`super::software::draw_surface_full`] and //! [`super::software::draw_surface_partial`]. Same DrawCtx flow; the //! differences are: //! //! * Canvas is `Canvas::new_gles` backed by a persistent FBO rather //! than a CPU pixmap → FBO pixels survive across frames naturally, //! so the partial path only needs a scissor. //! * Presentation goes through `egl_ctx.swap_buffers_with_damage` //! which attaches + damages + commits atomically. The partial path //! translates the top-left dirty rects into EGL's bottom-left //! convention before passing them in. //! * No `wl_shm` pool; no `pick_shm_format`; no byte-order swap. //! //! The Y-flip for the swap-with-damage call lives here (only the GPU //! path needs it — SHM buffers are already top-down). use std::sync::Arc; use smithay_client_toolkit::compositor::CompositorState; use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface; use crate::egl_context::EglContext; use crate::event_loop::SurfaceState; use crate::render::Canvas; use crate::types::{ Color, Rect }; use crate::widget::Element; use super::{ DrawCtx, layout_and_draw }; use super::chrome::{ apply_input_region, draw_fallback_banner, draw_titlebar }; /// GPU full-redraw path. Mirrors [`super::software::draw_surface_full`] /// but writes into the EGL surface's persistent FBO instead of an SHM /// buffer: /// /// 1. `eglMakeCurrent` so subsequent GL calls hit this surface. /// 2. (Lazily) build the [`Canvas::Gles`], or resize its FBO if the /// surface dimensions changed. /// 3. Clear, run layout+draw, blit the FBO to the default framebuffer. /// 4. Set buffer_scale + input_region (state attached to the implicit /// commit done by `eglSwapBuffers`). /// 5. `eglSwapBuffers` — performs the wl attach/damage/commit atomically. pub( crate ) fn draw_surface_full_gpu( ss: &mut SurfaceState, compositor: &CompositorState, egl_ctx: &Arc, view: &Element, bg: Color, input_region: Option<&[Rect]>, debug_layout: bool, pw: u32, ph: u32, scale: u32, request_frame: &dyn Fn( &WlSurface ), ) { let Some( es ) = ss.egl_surface.as_ref() else { return }; if egl_ctx.make_current( es ).is_err() { return; } let canvas = ss.canvas.get_or_insert_with( || { let mut c = Canvas::new_gles( Arc::clone( egl_ctx.gl() ), egl_ctx.version, pw, ph, ); c.set_dpi_scale( scale as f32 ); if let Some( reg ) = crate::theme::build_font_registry() { c.set_font_registry( Arc::new( reg ) ); } c } ); if canvas.size() != ( pw, ph ) { canvas.resize( pw, ph ); canvas.set_dpi_scale( scale as f32 ); } canvas.clear_clip(); let sf = scale as f32; let tb_h = ss.titlebar_height * sf; let screen_rect = Rect { x: 0.0, y: tb_h, width: pw as f32, height: ( ph as f32 - tb_h ).max( 0.0 ) }; if bg.a > 0.0 { canvas.fill( bg ); } else { canvas.clear(); } ss.titlebar_close_rect = draw_titlebar( canvas, &ss.titlebar_title, pw, tb_h, sf ); let mut ctx: DrawCtx = DrawCtx { focused_idx: ss.focused_idx, hovered_idx: ss.hovered_idx, pressed_idx: ss.gesture.pressed_idx, cursor_state: std::mem::take( &mut ss.cursor_state ), selection_anchor: std::mem::take( &mut ss.selection_anchor ), widget_rects: Vec::new(), debug_layout, scroll_offsets: std::mem::take( &mut ss.scroll_offsets ), scroll_rects: Vec::new(), scroll_canvases: std::mem::take( &mut ss.scroll_canvases ), scroll_navigable_items: std::mem::take( &mut ss.scroll_navigable_items ), previous_widget_rects: ss.widget_rects.clone(), accessible_extras: Vec::new(), live_depth: 0, }; layout_and_draw::( view, canvas, screen_rect, &mut ctx, 0 ); if ctx.debug_layout { for w in &ctx.widget_rects { canvas.stroke_rect( w.rect, Color::rgb( 1.0, 0.0, 0.0 ), 1.5, 0.0 ); } } if let Some( ref menu ) = ss.context_menu { super::draw_context_menu( canvas, menu ); } // Fallback-theme warning. Painted last so it sits on top of every // widget; no-op when the real theme is loaded. draw_fallback_banner( canvas, pw, sf ); canvas.present(); ss.prev_focused = ss.focused_idx; ss.prev_hovered = ss.hovered_idx; ss.prev_pressed = ss.gesture.pressed_idx; ss.widget_rects = ctx.widget_rects; ss.scroll_rects = ctx.scroll_rects; ss.scroll_canvases = std::mem::take( &mut ctx.scroll_canvases ); ss.cursor_state = ctx.cursor_state; ss.selection_anchor = ctx.selection_anchor; ss.scroll_offsets = ctx.scroll_offsets; ss.accessible_extras = ctx.accessible_extras; ss.scroll_navigable_items = ctx.scroll_navigable_items; ss.content_dirty = false; let wl_surface = ss.surface.wl_surface(); apply_input_region( wl_surface, compositor, input_region, scale ); // Frame callback request must precede the implicit commit done by // `swap_buffers_with_damage`, so the compositor can attach it to this // frame and not the previous one. request_frame( wl_surface ); // Full-surface damage. (0,0,w,h) is identical in EGL bottom-left and // top-left coords, so no Y-flip needed. let _ = egl_ctx.swap_buffers_with_damage( es, &[ ( 0, 0, pw as i32, ph as i32 ) ] ); ss.frame_pending = true; } /// GPU partial-redraw path. Same flow as /// [`super::software::draw_surface_partial`] (clip mask → repaint /// background + widgets → present), but the clip is implemented with /// `glScissor` (bbox union) inside the canvas, and presentation goes /// through `eglSwapBuffers`. The FBO is preserved between frames so /// the unclipped pixels naturally carry over. pub( crate ) fn draw_surface_partial_gpu( ss: &mut SurfaceState, compositor: &CompositorState, egl_ctx: &Arc, view: &Element, bg: Color, input_region: Option<&[Rect]>, dirty_rects: Vec, pw: u32, ph: u32, scale: u32, request_frame: &dyn Fn( &WlSurface ), ) { let Some( es ) = ss.egl_surface.as_ref() else { return }; if egl_ctx.make_current( es ).is_err() { return; } let canvas = ss.canvas.as_mut().expect( "partial path requires existing canvas" ); if canvas.size() != ( pw, ph ) { canvas.resize( pw, ph ); canvas.set_dpi_scale( scale as f32 ); } canvas.set_clip_rects( &dirty_rects ); let sf = scale as f32; let tb_h = ss.titlebar_height * sf; let screen_rect = Rect { x: 0.0, y: tb_h, width: pw as f32, height: ( ph as f32 - tb_h ).max( 0.0 ) }; if bg.a > 0.0 { canvas.fill( bg ); } else { canvas.clear_rects_transparent( &dirty_rects ); } ss.titlebar_close_rect = draw_titlebar( canvas, &ss.titlebar_title, pw, tb_h, sf ); let mut ctx: DrawCtx = DrawCtx { focused_idx: ss.focused_idx, hovered_idx: ss.hovered_idx, pressed_idx: ss.gesture.pressed_idx, cursor_state: std::mem::take( &mut ss.cursor_state ), selection_anchor: std::mem::take( &mut ss.selection_anchor ), widget_rects: Vec::new(), debug_layout: false, scroll_offsets: std::mem::take( &mut ss.scroll_offsets ), scroll_rects: Vec::new(), scroll_canvases: std::mem::take( &mut ss.scroll_canvases ), scroll_navigable_items: std::mem::take( &mut ss.scroll_navigable_items ), previous_widget_rects: ss.widget_rects.clone(), accessible_extras: Vec::new(), live_depth: 0, }; layout_and_draw::( view, canvas, screen_rect, &mut ctx, 0 ); if let Some( ref menu ) = ss.context_menu { super::draw_context_menu( canvas, menu ); } // Fallback-theme warning. Painted last so it sits on top of every // widget; no-op when the real theme is loaded. draw_fallback_banner( canvas, pw, sf ); canvas.clear_clip(); canvas.present(); ss.prev_focused = ss.focused_idx; ss.prev_hovered = ss.hovered_idx; ss.prev_pressed = ss.gesture.pressed_idx; ss.widget_rects = ctx.widget_rects; ss.scroll_rects = ctx.scroll_rects; ss.scroll_canvases = std::mem::take( &mut ctx.scroll_canvases ); ss.cursor_state = ctx.cursor_state; ss.selection_anchor = ctx.selection_anchor; ss.scroll_offsets = ctx.scroll_offsets; ss.accessible_extras = ctx.accessible_extras; ss.scroll_navigable_items = ctx.scroll_navigable_items; ss.content_dirty = false; let wl_surface = ss.surface.wl_surface(); apply_input_region( wl_surface, compositor, input_region, scale ); request_frame( wl_surface ); // Convert top-left dirty rects to EGL bottom-left coords for the // swap-with-damage call. let damage: Vec<( i32, i32, i32, i32 )> = dirty_rects.iter().map( |r| { let x = r.x.floor() as i32; let w = r.width.ceil() as i32; let h = r.height.ceil() as i32; let y_top = r.y.floor() as i32; let y_bottom = ph as i32 - y_top - h; ( x, y_bottom.max( 0 ), w.max( 0 ), h.max( 0 ) ) } ).collect(); let _ = egl_ctx.swap_buffers_with_damage( es, &damage ); ss.frame_pending = true; }