Skip to content

fix: pointer constraints, xwayland positioning, and physics tick - #8

Open
sevenluckz wants to merge 14 commits into
iluaii:mainfrom
sevenluckz:upstream-fixes
Open

fix: pointer constraints, xwayland positioning, and physics tick#8
sevenluckz wants to merge 14 commits into
iluaii:mainfrom
sevenluckz:upstream-fixes

Conversation

@sevenluckz

@sevenluckz sevenluckz commented Jul 31, 2026

Copy link
Copy Markdown

This PR addresses Xwayland coordinate sync issues and hardens the compositor's core pointer constraint logic.

1. Xwayland default cursor

Fetches the default xcursor buffer and sets it via wlr_xwayland_set_cursor to prevent the legacy X11 fallback cursor.

2. Xwayland Positioning/Physics Tick

Translates world coordinates to screen coordinates during view_set_size to restore hit-testing on non-primary desktops.

Xwayland coordinates are also continuously synced (view_sync_position) during physics glides and drags to prevent detached popups and dropdowns. To support desktop switching, this synchronization caches the computed sx/sy screen coordinates, naturally treating coordinate translation failures as un-cached.

3. Pointer Constraints & Crash Fixes

Hardens both LOCKED and CONFINED pointer constraints:

  • Locked constraints: Replaced an early return with a locked boolean check to keep the cursor visually frozen and suppress motion events while still allowing focus/hit-testing to process.
  • Confined constraints: Replaced manual boundary-dropping with wlr_region_confine to strictly clamp the pointer.
  • CSD Geometry Insets: Constraint regions now subtract XDG surface geometry offsets to align with true surface coordinates.
  • Window tracking: Actively warps the locked cursor alongside the window during physics glides to maintain its relative position (gated to LOCKED constraints).
  • Off-screen focus & Cursor freeze: If a confined/locked window drops out of bounds or leaves the active display, the pointer is permitted to move and naturally update focus, breaking the lock via constraints_follow_focus(server, NULL) without forcing the seat's pointer focus.
  • Double-Free Crash fix: Fixed a SIGSEGV in server_seat.c caused by a double wl_list_remove. The constraint listener is now unhooked before sending the deactivated signal to prevent memory corruption.

@iluaii

iluaii commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Thanks — I went through all three commits and built the branch against wlroots 0.20. It compiles clean and merges cleanly on current main. Two of the three I'd take as they are; one needs a change before I merge.

1. xwayland: set fallback cursor — correct, and the comment you deleted was simply wrong.

fwm does draw the pointer itself, but it takes the image from the client (handle_request_set_cursor, src/server_seat.c:85). An X11 client that never sets a cursor inherits the root window's, which defaults to the black X, and Xwayland forwards exactly that to us. So the root cursor does have to be set. Good catch.

2. xwayland: fix pointer coordinates on non-primary desktops — correct, and it makes the code match what the tree already claimed.

Both the comment in view_set_size ("send screen coords") and src/server_shell.c:133 ("X11 windows are always configured at screen coords") said this was already happening, while view->x went out in world coordinates. Override-redirect placement is built on that assumption, so the bug was real and this is the right fix. Your error handling is right too: server_world_to_screen (src/server_output.c:483) leaves the outputs untouched when it fails, and you pre-seed sx/sy with the world values, so a desktop that is off every monitor falls back to the old behaviour rather than to garbage.

One gap this leaves, which is not a regression and does not need to be in this PR — view_sync_position runs on events (server_camera_settled, desktop change, drag, expo), not per frame. A window flying under physics, or any window while the camera slides, still reports stale coordinates to X. It used to be wrong always; now it is wrong only in motion.

3. pointer: correctly handle locked pointer constraints — right diagnosis, but it lets in something I'd rather keep out.

The part I agree with entirely: the early return meant constraints_follow_focus never ran while the pointer was locked, so the active constraint was never re-evaluated. That is a genuine hole.

But the same change now also runs focus-follows-pointer on every motion event under a lock. The cursor is frozen, yes — the windows are not. fwm's windows drift and get thrown around on their own, and when one lands under the frozen cursor, server_focus_view (src/server.c:135) raises it to the top and takes the keyboard away from the game in the middle of mouse-look. The old early return ruled that out. It won't fire spuriously (server_focus_view returns early on an unchanged view), but the focus can change, and during mouse-look it must not.

Could you keep the constraint bookkeeping and leave focus alone:

if (view && !locked) server_focus_view(server, view);

Same class of thing, lower stakes, up to you: drag_icon_update_position, expo_handle_motion, launcher_handle_motion and server_drag_motion now also run while locked. They see unchanged coordinates, so in practice they do nothing — it just no longer reads as "the pointer is frozen".

Ping me when you've marked it ready and I'll merge. Nice digging — you found two places where the comments and the code had drifted apart.

Seven Menta added 8 commits July 31, 2026 22:47
Translate the world coordinates to screen coordinates before sending them to wlr_xwayland_surface_configure. Otherwise, X11 clients on any desktop besides the first one think they are positioned outside the X11 root window, resulting in dead clicks and failed hit-testing.
As noted in review, the pointer constraint lock prevented motion events
from being sent to clients, but it still allowed fwm's focus-follows-pointer
logic to trigger if a window slid under the cursor. This could cause the
focused client to lose focus during interactions like mouse-look.

This patch bypasses focus evaluation and drag/hover interactions while
the pointer is locked, ensuring the locked client retains focus.
@sevenluckz
sevenluckz marked this pull request as ready for review July 31, 2026 21:49
@sevenluckz

Copy link
Copy Markdown
Author

Hey, this is all I have for the moment!

I've hopefully solved the focus issue you mentioned, and I also added logic to track the cursor in relation to the window when it's locked (and instantly drop the lock if the window goes off-screen). I also moved the CONFINED constraint handling over to wlr_region_confine to prevent the cursor from escaping its boundaries during fast mouse flicks.

Finally, I added more position updates for Xwayland while its windows are moving to fix an issue I noticed where detached UI elements (like dropdown menus) were not updating their positions dynamically.

@iluaii

iluaii commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Thanks for this — I built the branch against wlroots 0.20 and it compiles clean, all 7 ctest targets pass. Replacing the manual boundary drop with wlr_region_confine and dropping the early return for LOCKED are both clearly right, and the fallback cursor commit is self-contained and good to go.

Three things I'd like fixed before merging, all in the constraint/physics commits:

1. The view_sync_position cache defeats the coordinate fix it ships with (src/view.c:168)

The early-out keys on view->x/y, i.e. world coordinates. But server_camera_settled() (src/server_tick.c:869) re-syncs every view after the camera moves precisely because the screen coordinates changed while view->x/y did not — server_world_to_screen() subtracts o->camera_x. So after a desktop switch, an X11 window that didn't move never gets a new configure, and it keeps the root coordinates from the old camera. That's the exact case point 4 of the description sets out to fix.

Cache the computed sx/sy instead of view->x/y (and treat a server_world_to_screen() failure as "not cached", since the fallback there is the raw world position).

2. The cursor warp during glides isn't gated on LOCKED (src/server_tick.c:1157)

The condition is just server->active_constraint, so a CONFINED constraint drags the user's cursor along with a window that flew off — the pointer was never handed over in that case. Please check constraint->type == WLR_POINTER_CONSTRAINT_V1_LOCKED.

Also worth a comment: dx/dy are world deltas passed to wlr_cursor_move(), which takes layout coordinates. They agree only while the camera is standing still, which is not true during a slide.

3. Breaking the lock off-screen does more than break the lock (src/server_tick.c:1170)

Besides deactivating the constraint it force-sets the default xcursor and calls wlr_seat_pointer_clear_focus(), even though the pointer may legitimately be sitting over some other window at that moment. constraints_follow_focus(server, NULL) alone already deactivates the constraint, and the next motion event restores focus and the correct cursor image on its own.

Minor, not a blocker and pre-existing: the region offset is computed from cv->x/cv->y, the window origin, while the constraint region is in surface coordinates — those diverge for CSD geometry insets.

One process note: the PR title reads as a cursor fix, but only d7fc7c8 is that; the other seven commits reach into the physics tick and Xwayland positioning. If you'd rather land something quickly, I'm happy to take the fallback-cursor commit on its own and review the constraint work separately.

The cache introduced for view_sync_position tracked the static
'world' coordinate of the view rather than the dynamic 'screen'
coordinate. When the camera panned to a new desktop, the screen
coordinates changed, but since the world coordinate remained identical,
the update was skipped. This resulted in Xwayland retaining stale
coordinates for the main window, causing popups (like context menus)
to appear on entirely wrong physical monitors.

To solve this systematically and prevent translating coordinates twice,
the caching logic has been consolidated directly into view_set_size.

Additionally, we explicitly treat server_world_to_screen failures
as "not cached" by bypassing the early-out and invalidating the cache
(setting it to -999999) when the window is parked off-screen. This
ensures that the raw world position is always correctly sent as a
fallback, and when the window comes back on-screen, Xwayland is
guaranteed to receive the fresh screen coordinates immediately.

@iluaii iluaii left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marking this as changes-requested so the status is visible in the UI — my earlier feedback went out as plain comments, so GitHub never flagged the PR and it may well have been lost in your inbox. Nothing new here, this is just the checklist from #8 (comment):

  1. src/view.c:168 — cache the computed sx/sy rather than view->x/y, and treat a server_world_to_screen() failure as "not cached". As written the early-out skips the reconfigure after a camera move, which is the case the coordinate fix exists for.
  2. src/server_tick.c:1157 — gate the cursor warp on constraint->type == WLR_POINTER_CONSTRAINT_V1_LOCKED; a CONFINED constraint shouldn't drag the pointer along.
  3. src/server_tick.c:1170 — on breaking the lock, constraints_follow_focus(server, NULL) is enough; drop the forced default xcursor and the wlr_seat_pointer_clear_focus().

No rush on my end, and no need to rebase or reshape anything. The standing offer also holds: if you'd rather land something now, I'll take d7fc7c8 (fallback cursor) on its own and we can keep the constraint work in this PR.

Seven Menta added 5 commits August 2, 2026 19:13
Remove explicit cursor and focus clearing when breaking constraints off-screen. constraints_follow_focus(server, NULL) alone already deactivates the constraint, and the next motion event restores focus and the correct cursor image on its own.
The constraint region is in surface coordinates, but we were computing the region offset from cv->x/cv->y (the window origin). For XDG surfaces with CSD geometry insets, we now subtract the geometry offset to properly map world coordinates to surface coordinates.
wlr_pointer_constraint_v1_send_deactivated can cause the constraint to be destroyed immediately, which triggers the destroy listener and removes the link. Calling wl_list_remove afterwards results in a double-remove and memory corruption. Unlink the listener first.
Remove early return when pointer coordinates fall outside the confined region. Now that the constraint listener double-free crash is fixed, we can safely let the pointer move out so focus can update and break the constraint properly.
@sevenluckz sevenluckz changed the title xwayland: Fix fallback cursor and pointer constraint interactions fix: pointer constraints, xwayland positioning, and physics tick Aug 2, 2026
@sevenluckz

sevenluckz commented Aug 2, 2026

Copy link
Copy Markdown
Author

I've pushed updates to address your feedback:

  1. view_sync_position cache: Updated to cache the computed sx/sy screen coordinates instead of the view->x/y world coordinates, properly treating a server_world_to_screen() failure as not-cached.
  2. Cursor warp during glides: Gated the cursor warp on constraint->type == WLR_POINTER_CONSTRAINT_V1_LOCKED. Also added a comment noting that dx/dy are world deltas which only agree with layout coordinates while the camera is stationary.
  3. Off-screen constraint breaking: Removed the forced wlr_seat_pointer_clear_focus() and default cursor setting. It now relies purely on constraints_follow_focus(server, NULL) to deactivate the constraint.

Minor (CSD Geometry Insets): Updated handle_cursor_motion to subtract the XDG surface geometry offset, so constraint regions use true surface coordinates.

I've also included fixes for related edge cases found during testing:

  • Cursor Freeze: Fixed an issue where the cursor freezes if a confined window drops out of bounds (e.g., due to gravity). The pointer is now allowed to move outside the region, triggering a normal focus update that breaks the constraint.
  • Double-Free Crash: Fixed a SIGSEGV in server_seat.c caused by a double wl_list_remove. The listener is now removed before wlr_pointer_constraint_v1_send_deactivated to prevent memory corruption if the constraint is destroyed synchronously.

@sevenluckz
sevenluckz requested a review from iluaii August 2, 2026 20:38

@iluaii iluaii left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — all three checklist items are addressed, and I rebuilt the branch against wlroots 0.20.2: it merges cleanly onto current main, compiles without warnings, and all 10 ctest targets pass.

Confirming what's now settled:

  1. src/view.c:156 — the cache keys on the computed sx/sy and a server_world_to_screen() failure invalidates it, so a camera move does reach X now. server_world_to_screen tolerates NULL out-params (src/server_output.c:483), so the NULL, NULL probe in the tick is fine too.
  2. src/server_tick.c:1164 — gated on LOCKED, with the world-vs-layout caveat written down.
  3. src/server_tick.c:1175 — down to constraints_follow_focus(server, NULL) alone.

And the CSD inset is handled via xdg_toplevel->base->current.geometry, which is what I meant.

38d8cc8 is a genuine find and the fix is the right one — the header says it outright: "Deactivate the constraint. May destroy the constraint." With lifetime == ONESHOT, wlr_pointer_constraint_v1_send_deactivated() destroys the object synchronously, handle_constraint_destroy removes the listener, and constraint_set_active then removes it a second time. Unhooking before the signal is correct.

Two things left, then I'll merge.

1. Off-screen X11 windows now get a configure every tick (src/view.c:156, src/server_tick.c:1162)

view_sync_position() runs unconditionally for every unpinned body in the physics tick, which includes every window sitting on a desktop no monitor is showing. For those server_world_to_screen() fails, so the cache is stamped with -999999, the early-out can never hit, and wlr_xwayland_surface_configure() goes out at 60 Hz per window — carrying raw world coordinates, which is the value the early-out was there to suppress in the first place. Clients that relayout on ConfigureNotify (Chrome, anything AWT) will do it 60 times a second while parked on another desktop.

Cheap fix: keep a bool last_sync_onscreen alongside the coordinates, cache whatever was actually sent in both cases, and compare all of it. That also retires the -999999 sentinel.

2. The comment under locked says the opposite of what the code does (src/server_pointer.c:145)

/* Mouse-look: the cursor does not move at all, but we still need
 * to send motion events with constant coordinates. */

The code deliberately skips wlr_seat_pointer_notify_enter/motion while locked, which is right — the client is getting relative motion from the relative-pointer protocol, and absolute motion under a lock is exactly what we're suppressing. The reason to fall through is hit-testing and constraints_follow_focus. Could you reword it to say that?

Neither of these needs a reshape, and no rush.

Two non-blockers for the record, no action needed here:

  • If wlr_region_confine() ever returns false the pointer is freed, but should it still be over the same surface, constraints_follow_focus re-arms the same constraint while every subsequent motion also starts outside the region — so confinement stays broken until focus actually leaves. In practice the pointer is inside the region at activation, so this only bites after a set_region while the pointer is elsewhere. I'll open an issue rather than grow this PR.
  • af1900d duplicates a8ec5a6, which landed on main after you branched. Identical change, merges clean, ends up a no-op — nothing to do, I'll just drop it from the squash message.
  • Seven of the added lines carry trailing whitespace.

@iluaii

iluaii commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Heads up: main moved under this branch and it now conflicts, in the one file this PR lives in.

1727912 gives the pointer an implicit grab — a button press pins the pointer to the surface it landed on until the button comes back up, because every motion re-asking the scene what was underneath meant a client dragging a splitter or a panel divider lost the pointer the moment the hand ran past the thing it was pushing. It adds ~90 lines above handle_cursor_motion and rewrites the delivery tail of that function, which is exactly where your constraint work sits. Git reports the conflict in src/server_pointer.c alone; the rest of the branch still applies.

It is worth resolving by hand rather than by taking a side, because the two changes meet on the same question. handle_cursor_motion now ends:

if (server_drag_motion(server, lx, ly, &now)) return;
if (pointer_grab_deliver(server, lx, ly, event->time_msec)) return;
pointer_update_focus(server, lx, ly, event->time_msec);

Your locked flag suppresses wlr_seat_pointer_notify_enter/motion and falls through for the sake of hit-testing and constraints_follow_focus. pointer_grab_deliver is a second reason to not re-ask what is under the cursor, and it returns true when it has handled the motion. Under a lock they want the same thing, so the merge should end up with one path, not two that each partly suppress delivery — the grab is the more general statement of "the pointer is spoken for".

Also note pointer_update_focus is now where focus-follows-pointer lives, so the if (view && !locked) guard we agreed on earlier has moved into that function.

No rush, and nothing about the review changes — the two items from my last comment still stand (the 60 Hz configure storm for off-screen X11 windows, and the comment at what is now a different line number in server_pointer.c). I would just rather you rebase before doing them than do them twice.

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