diff --git a/crates/hearthspace-compositor/src/compositor/mod.rs b/crates/hearthspace-compositor/src/compositor/mod.rs index 1fa2bad..8cbc96d 100644 --- a/crates/hearthspace-compositor/src/compositor/mod.rs +++ b/crates/hearthspace-compositor/src/compositor/mod.rs @@ -194,12 +194,12 @@ struct App { pending_dmabuf_imports: Vec<(Dmabuf, ImportNotifier)>, loop_handle: LoopHandle<'static, CalloopData>, popups: PopupManager, - /// Cached texture for the repeated background dot tile. - background_dot_tile: Option, + /// Cached texture for the repeated background image tile. + background_image_tile: Option, /// Stable render-element ids for visible background tile instances. Reusing /// ids across frames keeps the damage tracker from treating every tile as /// new on each redraw. - background_dot_ids: Vec, + background_tile_ids: Vec, software_cursor_visible: bool, software_cursor: SoftwareCursor, } @@ -455,8 +455,8 @@ pub(in crate::compositor) fn initialize_app( pending_dmabuf_imports: Vec::new(), loop_handle: handle.clone(), popups: PopupManager::default(), - background_dot_tile: None, - background_dot_ids: Vec::new(), + background_image_tile: None, + background_tile_ids: Vec::new(), software_cursor_visible: false, software_cursor: cursor::standard_software_cursor(), }; diff --git a/crates/hearthspace-compositor/src/compositor/rendering.rs b/crates/hearthspace-compositor/src/compositor/rendering.rs index a417385..9202ece 100644 --- a/crates/hearthspace-compositor/src/compositor/rendering.rs +++ b/crates/hearthspace-compositor/src/compositor/rendering.rs @@ -25,20 +25,29 @@ use smithay::{ use wayland_server::protocol::wl_surface; use super::{App, ManagedWindowKind, masonry_titlebar, windows::toplevel_title}; -use crate::config::{BACKGROUND_DOT_SIZE, BACKGROUND_DOT_SPACING}; +use crate::config::{ + DEFAULT_BACKGROUND_MOVES_WITH_CANVAS, DEFAULT_BACKGROUND_PATTERN_MARK_SIZE, + DEFAULT_BACKGROUND_PATTERN_SPACING, +}; type GlesTexture = ::TextureId; -const BACKGROUND_DOT_TILE_WIDTH: i32 = 1_920; -const BACKGROUND_DOT_TILE_HEIGHT: i32 = 1_080; +const DEFAULT_BACKGROUND_TILE_WIDTH: i32 = 1_920; +const DEFAULT_BACKGROUND_TILE_HEIGHT: i32 = 1_080; const MAX_BACKGROUND_TILE_ELEMENTS: usize = 512; -pub(super) struct BackgroundDotTile { +pub(super) struct BackgroundImageTile { context_id: ContextId, texture: GlesTexture, size: Size, } +struct BackgroundImageData { + pixels: Vec, + format: Fourcc, + size: Size, +} + render_elements! { pub(super) HearthspaceRenderElement<=GlesRenderer>; Surface = RescaleRenderElement>, @@ -110,7 +119,7 @@ impl App { framebuffer, age, &elements, - // White canvas; the gray dot grid is drawn on top of it. + // White canvas; the repeated background image is drawn on top of it. Color32F::new(1.0, 1.0, 1.0, 1.0), )?; Ok(result.damage.cloned()) @@ -161,10 +170,11 @@ impl App { } } - // The dot grid sits behind every window, directly on top of the white - // clear color, so it is appended last in this front-to-back ordering. - for dot in self.background_dot_elements(renderer, output_rect) { - elements.push(HearthspaceRenderElement::from(dot)); + // The repeated background image sits behind every window, directly on + // top of the white clear color, so it is appended last in this + // front-to-back ordering. + for tile in self.background_image_elements(renderer, output_rect) { + elements.push(HearthspaceRenderElement::from(tile)); } elements } @@ -245,13 +255,13 @@ impl App { elements } - /// Build repeated background dot-tile render elements for the current viewport. + /// Build repeated background image render elements for the current viewport. /// /// The tile is laid out in *canvas* space, so it pans and zooms with the - /// viewport while keeping the render-element count proportional to visible - /// tiles rather than visible dots. This is also the shape future user-defined - /// repeating backgrounds can reuse. - fn background_dot_elements( + /// viewport by default while keeping the render-element count proportional + /// to visible image tiles, not to details inside the image. This is also the + /// shape future user-defined repeating backgrounds can reuse. + fn background_image_elements( &mut self, renderer: &mut GlesRenderer, output_rect: Rectangle, @@ -259,24 +269,15 @@ impl App { if self.viewport_scale <= 0.0 || !self.viewport_scale.is_finite() { return Vec::new(); } + let moves_with_canvas = DEFAULT_BACKGROUND_MOVES_WITH_CANVAS; - let (context_id, texture, tile_size) = match self.background_dot_tile(renderer) { + let (context_id, texture, tile_size) = match self.background_image_tile(renderer) { Ok(tile) => (tile.context_id.clone(), tile.texture.clone(), tile.size), Err(_) => return Vec::new(), }; - let top_left = self.screen_to_canvas(Point::from(( - f64::from(output_rect.loc.x), - f64::from(output_rect.loc.y), - ))); - let bottom_right = self.screen_to_canvas(Point::from(( - f64::from(output_rect.loc.x) + f64::from(output_rect.size.w), - f64::from(output_rect.loc.y) + f64::from(output_rect.size.h), - ))); - let min_x = top_left.x.min(bottom_right.x); - let max_x = top_left.x.max(bottom_right.x); - let min_y = top_left.y.min(bottom_right.y); - let max_y = top_left.y.max(bottom_right.y); + let (min_x, max_x, min_y, max_y) = + self.background_image_visible_bounds(output_rect, moves_with_canvas); let Some((first_x, last_x, x_count)) = background_tile_axis_bounds(min_x, max_x, f64::from(tile_size.w)) @@ -293,24 +294,30 @@ impl App { return Vec::new(); } - while self.background_dot_ids.len() < tile_count { - self.background_dot_ids.push(Id::new()); + while self.background_tile_ids.len() < tile_count { + self.background_tile_ids.push(Id::new()); } let mut elements = Vec::with_capacity(tile_count); let src = Rectangle::from_size(tile_size.to_f64()); for gy in first_y..=last_y { for gx in first_x..=last_x { - let canvas = Point::from(( + let tile_origin = Point::from(( gx as f64 * f64::from(tile_size.w), gy as f64 * f64::from(tile_size.h), )); - let next_canvas = Point::from(( + let next_tile_origin = Point::from(( (gx + 1) as f64 * f64::from(tile_size.w), (gy + 1) as f64 * f64::from(tile_size.h), )); - let screen = self.canvas_to_screen(canvas); - let next_screen = self.canvas_to_screen(next_canvas); + let (screen, next_screen) = if moves_with_canvas { + ( + self.canvas_to_screen(tile_origin), + self.canvas_to_screen(next_tile_origin), + ) + } else { + (tile_origin, next_tile_origin) + }; let left = screen.x.round() as i32; let top = screen.y.round() as i32; let right = next_screen.x.round() as i32; @@ -319,7 +326,7 @@ impl App { let size = Size::from(((right - left).max(1), (bottom - top).max(1))); let slot = elements.len(); elements.push(TextureRenderElement::from_static_texture( - self.background_dot_ids[slot].clone(), + self.background_tile_ids[slot].clone(), context_id.clone(), location, texture.clone(), @@ -336,35 +343,61 @@ impl App { elements } - fn background_dot_tile( + fn background_image_tile( &mut self, renderer: &mut GlesRenderer, - ) -> Result<&BackgroundDotTile, ::Error> { + ) -> Result<&BackgroundImageTile, ::Error> { let context_id = renderer.context_id(); let needs_rebuild = self - .background_dot_tile + .background_image_tile .as_ref() .map(|tile| tile.context_id != context_id) .unwrap_or(true); if needs_rebuild { - let size = - Size::::from((BACKGROUND_DOT_TILE_WIDTH, BACKGROUND_DOT_TILE_HEIGHT)); - let pixels = rasterize_background_dot_tile(size); + let data = default_background_image_data(); let texture = renderer.import_memory( - &pixels, - Fourcc::Abgr8888, - (size.w, size.h).into(), + &data.pixels, + data.format, + (data.size.w, data.size.h).into(), false, )?; - self.background_dot_tile = Some(BackgroundDotTile { + self.background_image_tile = Some(BackgroundImageTile { context_id, texture, - size, + size: data.size, }); } - Ok(self.background_dot_tile.as_ref().unwrap()) + Ok(self.background_image_tile.as_ref().unwrap()) + } + + fn background_image_visible_bounds( + &self, + output_rect: Rectangle, + moves_with_canvas: bool, + ) -> (f64, f64, f64, f64) { + let top_left = Point::from((f64::from(output_rect.loc.x), f64::from(output_rect.loc.y))); + let bottom_right = Point::from(( + f64::from(output_rect.loc.x) + f64::from(output_rect.size.w), + f64::from(output_rect.loc.y) + f64::from(output_rect.size.h), + )); + + let (top_left, bottom_right) = if moves_with_canvas { + ( + self.screen_to_canvas(top_left), + self.screen_to_canvas(bottom_right), + ) + } else { + (top_left, bottom_right) + }; + + ( + top_left.x.min(bottom_right.x), + top_left.x.max(bottom_right.x), + top_left.y.min(bottom_right.y), + top_left.y.max(bottom_right.y), + ) } fn window_render_elements_at( @@ -492,7 +525,19 @@ fn background_tile_axis_bounds(min: f64, max: f64, tile_size: f64) -> Option<(i6 Some((first, last, count)) } -fn rasterize_background_dot_tile(size: Size) -> Vec { +fn default_background_image_data() -> BackgroundImageData { + let size = Size::::from(( + DEFAULT_BACKGROUND_TILE_WIDTH, + DEFAULT_BACKGROUND_TILE_HEIGHT, + )); + BackgroundImageData { + pixels: rasterize_default_background_image(size), + format: Fourcc::Abgr8888, + size, + } +} + +fn rasterize_default_background_image(size: Size) -> Vec { let len = size .w .checked_mul(size.h) @@ -500,30 +545,30 @@ fn rasterize_background_dot_tile(size: Size) -> Vec { .and_then(|bytes| usize::try_from(bytes).ok()) .unwrap_or_default(); let mut pixels = vec![0; len]; - let spacing = BACKGROUND_DOT_SPACING.max(1) as usize; - let dot_size = BACKGROUND_DOT_SIZE.max(1); + let spacing = DEFAULT_BACKGROUND_PATTERN_SPACING.max(1) as usize; + let mark_size = DEFAULT_BACKGROUND_PATTERN_MARK_SIZE.max(1); for cy in (0..=size.h).step_by(spacing) { for cx in (0..=size.w).step_by(spacing) { - rasterize_background_dot(&mut pixels, size, cx, cy, dot_size); + rasterize_default_background_mark(&mut pixels, size, cx, cy, mark_size); } } pixels } -fn rasterize_background_dot( +fn rasterize_default_background_mark( pixels: &mut [u8], size: Size, center_x: i32, center_y: i32, - dot_size: i32, + mark_size: i32, ) { - let half = dot_size / 2; + let half = mark_size / 2; let x0 = (center_x - half).max(0); let y0 = (center_y - half).max(0); - let x1 = (center_x - half + dot_size).min(size.w); - let y1 = (center_y - half + dot_size).min(size.h); + let x1 = (center_x - half + mark_size).min(size.w); + let y1 = (center_y - half + mark_size).min(size.h); for y in y0..y1 { for x in x0..x1 { @@ -603,7 +648,7 @@ mod tests { use smithay::utils::{Logical, Point, Rectangle, Size}; use super::{ - background_tile_axis_bounds, point_in_logical_rect, rasterize_background_dot_tile, + background_tile_axis_bounds, point_in_logical_rect, rasterize_default_background_image, }; #[test] @@ -633,9 +678,9 @@ mod tests { } #[test] - fn background_dot_tile_rasterizes_seam_dots() { + fn default_background_image_rasterizes_tile_edges() { let size = Size::::from((240, 120)); - let pixels = rasterize_background_dot_tile(size); + let pixels = rasterize_default_background_image(size); assert_eq!(pixel_alpha(&pixels, size, 0, 0), 255); assert_eq!(pixel_alpha(&pixels, size, 239, 0), 255); diff --git a/crates/hearthspace-compositor/src/config.rs b/crates/hearthspace-compositor/src/config.rs index 7784c98..a9d79af 100644 --- a/crates/hearthspace-compositor/src/config.rs +++ b/crates/hearthspace-compositor/src/config.rs @@ -52,9 +52,8 @@ pub const WINDOW_IDLE_THRESHOLDS: [Duration; 3] = [ pub const ANIMATION_FRAME_INTERVAL: Duration = Duration::from_millis(16); -/// Background dot grid. The dots live in canvas space, so they pan and zoom with -/// the viewport and frame canvas movement even when no windows are open. -/// `BACKGROUND_DOT_SPACING` is the (intentionally large) gap between dots in -/// canvas pixels; `BACKGROUND_DOT_SIZE` is the side length of each square dot. -pub const BACKGROUND_DOT_SPACING: i32 = 120; -pub const BACKGROUND_DOT_SIZE: i32 = 4; +/// Default generated repeating background image. The renderer treats this as a +/// generic image tile; these values only define the built-in fallback pattern. +pub const DEFAULT_BACKGROUND_PATTERN_SPACING: i32 = 120; +pub const DEFAULT_BACKGROUND_PATTERN_MARK_SIZE: i32 = 4; +pub const DEFAULT_BACKGROUND_MOVES_WITH_CANVAS: bool = true;