Skip to content

fix(wallpaper): paint correctly when output layout has a negative origin#484

Open
raven2cz wants to merge 5 commits into
trip-zip:mainfrom
raven2cz:upstream-submit/wallpaper-negative-origin
Open

fix(wallpaper): paint correctly when output layout has a negative origin#484
raven2cz wants to merge 5 commits into
trip-zip:mainfrom
raven2cz:upstream-submit/wallpaper-negative-origin

Conversation

@raven2cz

@raven2cz raven2cz commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Description

awful.wallpaper is inherited from AwesomeWM where X11 guarantees the root window is anchored at (0, 0), so root.size() alone was sufficient and every screen.geometry.x/y was non-negative. On Wayland, wlr_output_layout happily places outputs at negative coordinates: a portrait monitor mounted to the left of the primary, or a vertical stack with a monitor above y=0. In that case the AwesomeWM-shaped Lua paint path silently clips the affected screens off the cairo surface, so the wallpaper ends up blank on those outputs after the buffer reaches the compositor.

Reproduction

Any multi-monitor configuration where at least one output has a negative position in the layout. Simplest case: portrait monitor to the left of a landscape primary:

-- inside output.connect_signal("added", ...)
o.transform = "90"
o.position  = { x = -2160, y = 0 }   -- 2160x3840 after rotation

Set wallpapers via awful.wallpaper { screen = s, widget = ... } on both screens. Before: wallpaper renders on the primary only, the negative-x output shows blank. After: wallpaper renders on both outputs in a single paint pass.

Root cause

The cairo surface in awful.wallpaper's paint() is allocated at root.size() (the layout bounding box) with an implicit pixel origin at (0, 0). Drawing code then uses s.geometry.x / s.geometry.y (absolute layout coordinates) directly as cairo user-space coordinates. For an output at negative layout x, the draws fall outside the surface pixel range and are discarded. The same mismatch propagates into the C-side wallpaper cache path (create_wallpaper_cache_entry) and the legacy fallback path in root_set_wallpaper_cached, which translate the cairo pattern assuming pixel (0, 0) corresponds to layout (0, 0).

Fix

Teach the pipeline the pattern invariant pattern_pixel(u, v) == layout(u + layout_x, v + layout_y) and apply it consistently:

  • root.c: new root.geometry() Lua getter returning {x, y, width, height} from wlr_output_layout_get_box. Complements the existing root.size(). On X11 it always returns x=0, y=0, width=..., height=... (no behavior change), on Wayland it exposes the real origin.
  • lua/awful/wallpaper.lua: fetch root.geometry() instead of just root.size(), keep the target surface at the bounding-box dimensions, and cr:translate(-root_x, -root_y) right after creating the cairo context so every subsequent s.geometry.x / geo.x (still absolute layout coordinates) lands on a valid surface pixel. The source-to-target copy compensates with cr:set_source_surface(source, root_x, root_y) because source is already in surface-pixel space and needs the outer translate cancelled out.
  • root.c: create_wallpaper_cache_entry now takes the layout origin so its pattern-extract translate carves out the right region for each screen (cairo_translate(cr, layout_x - x, layout_y - y) instead of cairo_translate(cr, -x, -y)). The legacy fallback path allocates a bbox-sized surface whose pixel origin already matches the pattern, so it drops the old -x, -y translate and copies 1:1; the scene buffer node is positioned at layout_box.x, layout_box.y so negative origins still land correctly in the scene graph.

No new API surface besides root.geometry(); existing root.size() is unchanged. External Lua code (themes, widgets, third-party awful.wallpaper wrappers) keeps working unchanged. The fix is purely coordinate bookkeeping; no new allocations, no extra paint passes.

Test plan

  • Single monitor at (0, 0): no regression, wallpaper renders as before (the new root.geometry() returns {x=0, y=0, ...} so the cairo translate is a no-op).
  • Dual monitor with one portrait output to the left of primary (position = {x=-2160, y=0}, transform = "90", logical 2160x3840): portrait wallpaper renders on the rotated output, landscape wallpaper on the primary, in a single awful.wallpaper paint pass.
  • Build: clean on top of main; no new warnings.

raven2cz and others added 2 commits April 20, 2026 14:35
awful.wallpaper's paint() was inherited from AwesomeWM where the X11
root window is guaranteed to start at (0, 0), so root.size() alone was
enough and per-screen geometry.x/y were always non-negative. On Wayland
wlr_output_layout can place outputs at negative coordinates — e.g. a
portrait monitor left of the primary at x=-2160 — and the
AwesomeWM-shaped code quietly clipped those screens off the cairo
surface, leaving their wallpaper blank after the buffer reached the
compositor.

Fix the whole paint pipeline to honor the layout origin:

* root.c: add root.geometry() Lua getter returning {x, y, width, height}
  from wlr_output_layout_get_box (complements root.size()).
* lua/awful/wallpaper.lua: fetch root.geometry(), keep allocating the
  target surface at the bbox size but cr:translate(-root_x, -root_y) so
  that absolute layout coordinates used by every s.geometry.x / geo.x
  draw below still land on-surface. Compensate the source-to-target
  copy's set_source_surface offset by (root_x, root_y) since the source
  is already in surface-pixel space.
* root.c: update the cache and legacy-fallback paths that extract the
  pattern into per-screen / full-layout buffers.
  create_wallpaper_cache_entry now receives the layout origin and uses
  it when computing the cairo translate that carves each screen region
  out of the pattern. The fallback path copies 1:1 (both surfaces are
  bbox-sized with matching pixel origin), so it drops the old -x,-y
  translate.

Verified live on a three-monitor layout (HP U28 portrait at transform
"90", x=-2160; Dell G3223Q primary at 0,0): awful.wallpaper now renders
the portrait wallpaper on HP and the landscape wallpaper on Dell in a
single paint pass.
raven2cz added a commit to raven2cz/somewm that referenced this pull request May 14, 2026
Phase 5 — the fork-only Lua features. Upstream did not touch any of
these four files in the sync window, so each is taken wholesale.

- lua/awful/mouse/snap.lua: aerosnap dwell-time gate (PR trip-zip#522) — defers
  the snap placeholder by a dwell timer to kill cross-monitor flicker.
- lua/awful/wallpaper.lua: paint() negative-origin fix (PR trip-zip#484) —
  translates by -root_x/-root_y so wallpaper paints correctly when the
  output layout has a negative origin (depends on root.c luaA_root_geometry,
  Phase 3b).
- lua/lockscreen.lua: background-image feature (PR trip-zip#476) — bg_image,
  bg_image_overlay, bg_image_blur, multipass blur, surface cache.
- lua/somewm/init.lua: register the tag_slide submodule (the fork-only
  lua/somewm/tag_slide.lua came in Phase 1).

KEEP-UPSTREAM (verified empty diff, no edit): lua/awful/input.lua,
ipc.lua, screenshot.lua (fork is behind upstream's 48e19a0 / ipc / HiDPI
work); lua/awful/permissions/init.lua (PR trip-zip#516 nil-screen Lua fix
dropped — upstream's C-layer 7c932c7 supersedes it); the icon-resolution
unit lua/awful/client.lua + widget/clienticon.lua + widget/tasklist.lua
(decision D1 — adopt upstream's resolve_icon, drop the fork's
get_icon_path API).

Sandbox-verified: headless startup clean, all four fork Lua modules
require() successfully, no Lua errors.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants