Skip to content

NSScreens and XRANDR #67

@sheffler

Description

@sheffler

PROBLEM: when libxrandr-dev is installed, HAVE_XRANDR is triggered which enables some code that modifies NSWindows to adjust their position when there is a screen change.

The computations seem to not work correctly. One thing I observed is that menus are repositioned partly offscreen because of some of the adjustments.

The problem seems to be in the value of

[NSScreen screens]

When libxrandr-dev is not installed, there is one NSScreen that corresponds to the entire DISPLAY of the Xserver. When libxrandr-dev is installed, one NSScreen per monitor will appear, but their sizes and offsets do not match the output of the shell command xrandr.

DISCUSSION: I believe there are some inconsistencies in the computing of NSScreens (which correspond to monitors) and the XDisplay (which can span the union of a couple of monitors.) The X11 XRANDR extension provides ways to get details about monitor configurations and GS has some support for watching for XRANDR events, but it seems to me that the way in which NSScreens are configured using XRANDR is incorrect.

When libxrandr-dev is installed, an XRANDR event is handled by
libs-back and libs-gui.

Chain of events:

  XGServerEvent.m
    if xEvent is a RANDR event, then
      XRRUpdateConfiguration() // to update the client's X config state
      [NSScreen resetScreens] // destroy existing list of screens
      [NSNotify NSApplicationDidChangeScreenParameters] // tell application 

  in NSWindow.:
    - applicationDidChangeScreenParameters
      - calls [NSScreen screens] which regenerates list

  in NSScreen.m
    - observe [NSScreen screens]
    - put this print statement
        NSLog(@"TOM: SCREENS:%@", screenArray)
      before last line of [NSScreen screens]

  in XGServerWindow.m
    put this print statement
      NSLog(@"TOM: BoundsForScrreen:%@ %@", screen, NSScreenFromRect(boundsRect)
    before last line of boundsForScreen:(int)screen

    NOTE: method screenList in XGServerWindow.m has HAVE_XRANDR code
    that is building an array called monitors[] that is examined
    by boundsForScreen:(int)screen.  So the problem is somewhere in
    that code, I believe.

With those print statements in, I see this traces below.

It seems that the :boundsForScreen:(int)screen method is not correctly accessing screen number N. It seems to be incorrect whether it is called as the result of an xrandr event, OR when it is the initial call in the applicaiton. (This seems to rule out Xevent race conditions in a running program due to the xrandr event updates).

$ openapp SystemPreferences
// initial size of screen is correct - builtin display only
2026-01-03 08:42:31.352 SystemPreferences[59609:59609] TOM: BoundsForScreen:0  {x = 0; y = 0; width = 1920; height = 1080}
2026-01-03 08:42:31.355 SystemPreferences[59609:59609] TOM: SCREENS:("<NSScreen: 0x55c620dc7e48> number: 0 frame: {x = 0; y = 0; width = 1920; height = 1080}")
// enable external 1920x1200 display.  Number of screens seems ok.  Sizes are not.
2026-01-03 08:42:58.937 SystemPreferences[59609:59609] TOM: BoundsForScreen:0  {x = 0; y = 0; width = 1920; height = 1080}
2026-01-03 08:42:58.937 SystemPreferences[59609:59609] TOM: BoundsForScreen:1  {x = 0; y = 0; width = 1920; height = 1080}
2026-01-03 08:42:58.937 SystemPreferences[59609:59609] TOM: SCREENS:("<NSScreen: 0x55c6211b96a8> number: 0 frame: {x = 0; y = 0; width = 1920; height = 1080}", "<NSScreen: 0x55c6213df828> number: 1 frame: {x = 0; y = 0; width = 1920; height = 1080}")
// disable external 1920x1200 display.  width is incorrect.
2026-01-03 08:43:18.582 SystemPreferences[59609:59609] TOM: BoundsForScreen:0  {x = 0; y = 0; width = 3840; height = 1200}
2026-01-03 08:43:18.582 SystemPreferences[59609:59609] TOM: SCREENS:("<NSScreen: 0x55c6213afe48> number: 0 frame: {x = 0; y = 0; width = 3840; height = 1200}")

Here is another test where I start with two monitors and then change them. The initial sizes of the monitors seems incorrect.

$ openapp SystemPreferences
// initial configuration is two screens.  The sizes are not right.
2026-01-03 08:48:33.733 SystemPreferences[59769:59769] TOM: BoundsForScreen:0  {x = 0; y = 0; width = 3840; height = 1200}
2026-01-03 08:48:33.735 SystemPreferences[59769:59769] TOM: BoundsForScreen:1  {x = 0; y = 0; width = 3840; height = 1200}
2026-01-03 08:48:33.735 SystemPreferences[59769:59769] TOM: SCREENS:("<NSScreen: 0x5597821dee28> number: 0 frame: {x = 0; y = 0; width = 3840; height = 1200}", "<NSScreen: 0x5597821f2fb8> number: 1 frame: {x = 0; y = 0; width = 3840; height = 1200}")
// disable one screen.  The remaining builtin display has the correct bounds
2026-01-03 08:48:46.743 SystemPreferences[59769:59769] TOM: BoundsForScreen:0  {x = 0; y = 0; width = 1920; height = 1080}
2026-01-03 08:48:46.744 SystemPreferences[59769:59769] TOM: SCREENS:("<NSScreen: 0x559782815f78> number: 0 frame: {x = 0; y = 0; width = 1920; height = 1080}")
// enable the external 1920x1200 display.  things are strange.
2026-01-03 08:49:12.119 SystemPreferences[59769:59769] TOM: BoundsForScreen:0  {x = 0; y = 0; width = 3840; height = 1200}
2026-01-03 08:49:12.119 SystemPreferences[59769:59769] TOM: BoundsForScreen:1  {x = 0; y = 0; width = 3840; height = 1200}
2026-01-03 08:49:12.120 SystemPreferences[59769:59769] TOM: SCREENS:("<NSScreen: 0x55978278b9b8> number: 0 frame: {x = 0; y = 0; width = 3840; height = 1200}", "<NSScreen: 0x5597827f1e58> number: 1 frame: {x = 0; y = 0; width = 3840; height = 1200}")
// disable the external display.  The remaining builtin display is correct
2026-01-03 08:49:24.248 SystemPreferences[59769:59769] TOM: BoundsForScreen:0  {x = 0; y = 0; width = 1920; height = 1080}
2026-01-03 08:49:24.248 SystemPreferences[59769:59769] TOM: SCREENS:("<NSScreen: 0x559782774f78> number: 0 frame: {x = 0; y = 0; width = 1920; height = 1080}")

If I revert to no Xrandr support by removing and rebuilding

$ sudo apt remove libxrandr-dev
 $ cd libs-back
 $ make
 $ sudo -E make install

and recompile libs-back, a simpler behavior is implemented. In this case there
is always just one "screen", and its size represents the size of the X11 "Display"
which is the union of the monitors areas. Using arandr to change the displays
has no effect on the running program.

// both monitors are enabled.  start the program
$ openapp SystemPreferences
2026-01-03 09:17:47.975 SystemPreferences[64810:64810] TOM: BoundsForScreen:0  {x = 0; y = 0; width = 3840; height = 1200}
2026-01-03 09:17:47.975 SystemPreferences[64810:64810] TOM: SCREENS:("<NSScreen: 0x5631f62b96c8> number: 0 frame: {x = 0; y = 0; width = 3840; height = 1200}")

// only one monitor is enabled.  start the program.
$ openapp SystemPreferences
2026-01-03 09:18:30.651 SystemPreferences[64883:64883] TOM: BoundsForScreen:0  {x = 0; y = 0; width = 1920; height = 1080}
2026-01-03 09:18:30.652 SystemPreferences[64883:64883] TOM: SCREENS:("<NSScreen: 0x55a419ee96c8> number: 0 frame: {x = 0; y = 0; width = 1920; height = 1080}")

The program arandr was used to alter the screen setup between these two configures.

Image Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions