Skip to content

Compositor incorrectly marks screensaver window as obscured on HiDPI multi-monitor, breaking animation

To be transparent, this issue report was written by Claude Code (it is obviously AI). I have examined the report myself and it seems to me to be accurate. I have a PR that fixes the issue ready — will submit momentarily.


Summary

xfwm4 compositor incorrectly marks fullscreen screensaver window as obscured on HiDPI scaled multi-monitor setup, causing xfce4-screensaver animation to only display on one monitor.

Environment

  • xfwm4 version: 4.20.0 (tested both packaged and latest git master from 2025-11-15)
  • xfce4-screensaver version: 4.20.1-1
  • OS: Debian testing
  • Desktop: XFCE 4.20
  • Display configuration:
    • HDMI-0: 5461x2880 (primary) at position 3840+0
    • DP-2: 3840x2160 at position 0+343
    • HiDPI scaling: 2x (/xsettings/Gdk/WindowScalingFactor: 2)

Bug Description

When xfce4-screensaver activates with an animated screensaver, the compositor incorrectly sends GDK_VISIBILITY_FULLY_OBSCURED events to the fullscreen screensaver window on the primary monitor (HDMI-0). This causes xfce4-screensaver to immediately stop the animation job for that window, leaving only the secondary monitor (DP-2) displaying the screensaver animation. The primary monitor shows only a black screen.

Steps to Reproduce

  1. Set up dual monitors with 2x HiDPI scaling on at least one monitor
  2. Enable xfwm4 compositor
  3. Configure xfce4-screensaver with an animated screensaver (e.g., popsquares)
  4. Set screensaver delay to 1 minute
  5. Wait for screensaver to activate

Expected: Both monitors show screensaver animation Actual: Secondary monitor shows animation, primary monitor is black

Debug Evidence

From xfce4-screensaver debug output (xfce4-screensaver --debug):

[gs_manager_create_window_for_monitor] Creating a Window [1920,0] (2731x1440) for monitor HDMI-0
[gs_manager_create_window_for_monitor] Creating a Window [0,171] (1920x1080) for monitor DP-2
[window_show_cb] Handling window show
[gs_job_set_command] Setting command for job: '/usr/libexec/xfce4-screensaver/popsquares'
[window_map_event_cb] Handling window map_event event
[manager_maybe_start_job_for_window] Starting job for window
[gs_job_start] Starting job
[window_obscured_cb] Handling window obscured: obscured
[gs_job_stop] Stopping job
[gs_job_died] Job finished

The critical issue: immediately after starting the job for the HDMI-0 window, a window_obscured_cb event marks it as obscured, stopping the animation.

Root Cause Analysis

xfce4-screensaver uses GDK visibility events (GDK_VISIBILITY_FULLY_OBSCURED) via gs_window_real_visibility_notify_event() in src/gs-window.c to detect if its fullscreen windows are covered. The xfwm4 compositor appears to be incorrectly reporting one of the fullscreen screensaver windows as obscured in HiDPI scaled multi-monitor configurations.

The visibility detection code in xfce4-screensaver (gs-window.c):

static gboolean
gs_window_real_visibility_notify_event (GtkWidget *widget,
                                         GdkEventVisibility *event) {
    switch (event->state) {
        case GDK_VISIBILITY_FULLY_OBSCURED:
            gs_window_set_obscured (GS_WINDOW (widget), TRUE);
            break;
        case GDK_VISIBILITY_PARTIAL:
            break;
        case GDK_VISIBILITY_UNOBSCURED:
            gs_window_set_obscured (GS_WINDOW (widget), FALSE);
            break;
    }
    return FALSE;
}

Workarounds That Work

  1. Disable xfwm4 compositor entirely
  2. Use picom compositor instead of xfwm4 compositor (works perfectly with no issues)
  3. Use blank screen mode instead of animated screensaver

Additional Information

  • The issue is reproducible with latest git master (commit as of 2025-11-15)
  • Window geometries are correctly detected (scaled coordinates match expected values)
  • Both screensaver windows are created and mapped properly
  • The issue appears to be compositor-specific as picom does not trigger the false obscured event
  • xdotool confirms both fullscreen windows exist at correct positions and sizes:
    • Window at X=3840 Y=0 WIDTH=5462 HEIGHT=2880 (HDMI-0)
    • Window at X=0 Y=342 WIDTH=3840 HEIGHT=2160 (DP-2)

Root Cause - Coordinate Space Mismatch (2025-11-15 Investigation)

The bug is caused by a 1-pixel coordinate mismatch between GDK and X11/RandR in HiDPI configurations:

The Mismatch

GDK reports (after scaling by 2):

  • HDMI-0: 3840,0 5462×2880
  • DP-2: 0,342 3840×2160

X11/RandR reports:

  • HDMI-0: 3840+0 5461×2880 (1 pixel narrower!)
  • DP-2: 0+343 3840×2160 (1 pixel lower!)

Verified with:

python3 -c "
import gi
gi.require_version('Gdk', '3.0')
from gi.repository import Gdk
display = Gdk.Display.get_default()
for i in range(display.get_n_monitors()):
    monitor = display.get_monitor(i)
    geo = monitor.get_geometry()
    scale = monitor.get_scale_factor()
    print(f'Monitor {i}: Logical {geo.x},{geo.y} {geo.width}x{geo.height} → Physical {geo.x*scale},{geo.y*scale} {geo.width*scale}x{geo.height*scale}')
"

The Bug Flow

  1. xfce4-screensaver creates windows in logical pixels (2731×1440 for HDMI-0)

  2. GTK/GDK maps to X11 which uses X11/RandR geometry → window becomes 5461 pixels wide

  3. xfwm4 compositor checks fullscreen in src/compositor.c:254 (is_fullscreen() function)

  4. is_fullscreen() compares:

    • Window attributes from X11: cw->attr (XGetWindowAttributes) = 5461 width
    • Monitor geometry from GDK: rect from myScreenFindMonitorAtPoint() = 5462 width
  5. Exact comparison fails:

    return ((cw->attr.x == rect.x) &&
            (cw->attr.y == rect.y) &&
            (cw->attr.width + 2 * cw->attr.border_width == rect.width) &&
            (cw->attr.height + 2 * cw->attr.border_width == rect.height));
    • HDMI-0: 5461 ≠ 5462 → returns FALSE
  6. Window not recognized as fullscreen → compositor sends incorrect GDK_VISIBILITY_FULLY_OBSCURED

  7. xfce4-screensaver stops animation

Code Analysis

common/xfwm-common.c:87 - xfwm_get_monitor_geometry():

gboolean xfwm_get_monitor_geometry (GdkScreen *screen, gint monitor_num,
                                     GdkRectangle *geometry, gboolean scaled) {
    scale = gdk_monitor_get_scale_factor (monitor);
    gdk_monitor_get_geometry (monitor, geometry);  // Returns LOGICAL pixels

    if (scaled && scale != 1)
        xfwm_geometry_convert_to_device_pixels (geometry, scale);  // Multiply by scale

    return TRUE;
}

When scaled=TRUE, this multiplies GDK's logical pixels by the scale factor, but GDK's logical pixels don't always match X11/RandR's physical pixels exactly due to rounding.

src/screen.c:804 - myScreenFindMonitorAtPoint() calls with scaled=TRUE src/compositor.c:3324 - Window attributes from XGetWindowAttributes() (always physical pixels)

Proper Fix Implemented (2025-11-15)

Status: FIXED

A proper fix has been implemented that uses X11 RandR to query monitor geometry directly in physical pixels, avoiding GDK's rounding errors entirely.

Solution: Created new function myScreenFindMonitorAtPointPhysical() in src/screen.c that:

  • Uses XRRGetScreenResources() and XRRGetCrtcInfo() to query CRTC geometry
  • Returns exact physical pixel coordinates matching X11/RandR
  • Avoids GDK's logical pixel calculation and rounding
  • Falls back to GDK method if RandR query fails

Changes:

  • src/screen.c:838-917 - New myScreenFindMonitorAtPointPhysical() function
  • src/screen.h:301-304 - Function declaration added
  • src/compositor.c:264-269 - Modified is_fullscreen() to use new function

Testing: Tested successfully on dual HiDPI (2x scaling) setup:

  • Both monitors now display screensaver animation correctly
  • No GDK_VISIBILITY_FULLY_OBSCURED events sent incorrectly
  • Screensaver jobs run on both monitors without being stopped

Branch: fix-hidpi-screensaver-rounding Commit: 06cb5a057

The root issue was mixing coordinate spaces: X11 window attributes (always physical) vs GDK monitor geometry (logical, then scaled). The fix ensures both use physical pixels from X11/RandR.