Skip to content

Wayland/feature parity m0 m5#80

Open
DMJC wants to merge 28 commits into
gnustep:masterfrom
DMJC:wayland/feature-parity-m0-m5
Open

Wayland/feature parity m0 m5#80
DMJC wants to merge 28 commits into
gnustep:masterfrom
DMJC:wayland/feature-parity-m0-m5

Conversation

@DMJC
Copy link
Copy Markdown

@DMJC DMJC commented May 16, 2026

This adds support for NSOpenGL on Wayland as well as fixes some issues with drag and drop and Keyboard and Mouse input handling. I'm looking for assistance/guidance on what needs to be done to get this merged. I'm expecting to have to go back and edit this.

For the OpenGL support I had Claude compare the X11/Wayland drivers and implement the Wayland GL support based on the X11 implementation. This is working in the MyGL.app test application as well as other GL applications running on Wayland.

I had ChatGPT run a report against the X11/Wayland drivers and then had Claude implement missing features from Wayland. This fixed Keyboard handling in terminal.app as I can now use tab-autocompletion where it was broken before. It also fixed drag/drop support on Wayland. I can now drag/drop tabs in Firefox properly and It partially fixed drag/drop support in Gorm.app.

DMJC and others added 24 commits April 18, 2026 13:39
Wayland: lazily attach EGL/OpenGL context to window when view becomes available
Add Wayland vs X11 feature-gap report and Wayland implementation plan
Updated the integration harness script to use ambrosia Wayland compositor instead of Weston.
Milestone 0 — Baseline instrumentation:
- Replace bare NSDebugLog with targeted NSDebugMLLog/NSDebugFLLog categories
  (WaylandDnD, WaylandIME, WaylandPointer, WaylandScroll, WaylandOutput)
  across WaylandDragView, WaylandInputServer, WaylandServer+Cursor,
  WaylandServer+Keyboard, and WaylandServer+Output.
- Add default: case logging for unhandled pointer buttons (BTN_SIDE/EXTRA etc.)
  and stub logging for pointer_handle_frame/axis_stop/axis_discrete.
- Add Tools/wayland-smoke-test.sh: integration harness that starts Ambrosia
  compositor nested in the current Wayland session, discovers its socket from
  the log, and runs 11 smoke checks covering compositor liveness, protocol
  globals, source code snapshots for each feature bucket, and log sanity.
- Fix pre-existing WaylandCairoShmSurface.m ivar redeclaration build error.

Milestone 1 — Inter-process DnD via wl_data_device:
- WaylandServer.h: add wl_data_device_manager, wl_data_device, and full DnD
  state (dnd_source, dnd_offer, MIME list, action, position, target) to
  WaylandConfig.
- WaylandServer.m: bind wl_data_device_manager in registry handler (v≤3),
  create wl_data_device and attach listener after the initial roundtrip.
- WaylandDragView.m: complete implementation —
  Outbound (GNUstep → external): dragImage: creates wl_data_source, offers
  all MIME types from the pasteboard, calls wl_data_device_start_drag, then
  runs GSDragView's event loop; data_source_send writes pasteboard data to
  the pipe FD; cancelled/dnd_finished post a fake NSLeftMouseUp to exit the
  loop cleanly.
  Inbound (external → GNUstep): device_data_offer accumulates MIME types;
  device_enter accepts the best type, sets wl_data_offer_set_actions (v3),
  sets up NSDraggingInfo on the shared drag view, and posts
  GSAppKitDraggingEnter; device_motion posts GSAppKitDraggingUpdate;
  device_leave posts GSAppKitDraggingExit and destroys the offer;
  device_drop reads data via pipe, populates NSDragPboard, posts
  GSAppKitDraggingDrop, and calls wl_data_offer_finish (v3).
  MIME mapping: text/plain↔NSStringPboardType, text/uri-list↔NSFilenamesPboardType,
  application/rtf↔NSRTFPboardType, image/tiff↔NSTIFFPboardType,
  image/png↔NSPNGPboardType.
  Action mapping: wl copy/move ↔ NSDragOperationCopy/Move.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Protocol plumbing:
- Generate text-input-unstable-v3 client header and protocol .c file via
  wayland-scanner; add the .c to wayland_C_FILES in GNUmakefile.
- Include text-input-unstable-v3-client-protocol.h from WaylandServer.h.
- Add zwp_text_input_manager_v3 and zwp_text_input_v3 pointers plus
  preedit state (pending_preedit, pending_commit, preedit_rect/spot,
  text_input_active, ime_serial) to WaylandConfig.
- Bind zwp_text_input_manager_v3 in registry handler.
- Create zwp_text_input_v3 from manager+seat after the initial roundtrip
  and attach the listener.

Keyboard / text_input_v3 listener (WaylandServer+Keyboard.m):
- Fix longstanding XKB bug: replace the broken `&sym` (keysym address cast
  to UTF-8) with xkb_state_key_get_utf8, which correctly handles the full
  dead-key composition pipeline (e.g. dead_acute + e → é).
- Add zwp_text_input_v3_listener with all 9 callbacks (6 v1 + 3 v2 stubs).
- keyboard_handle_enter/leave: call enable()/disable() + commit() on the
  text input object so the compositor IM knows when focus changes.
- text_input_enter/leave: track active state; clear preedit on leave.
- text_input_preedit_string / commit_string: buffer pending IM events.
- text_input_done: apply buffered preedit via setMarkedText:selectedRange:
  and commit string via insertText: (with key-event fallback); clear marked
  text when preedit is NULL.
- delete_surrounding_text, action, language, preedit_hint: logged stubs.

WaylandInputServer (WaylandInputServer.m / .h):
- Add wlconfig back-pointer; wire it from _initWaylandContext.
- statusArea: return the bottom strip of the focused window.
- preeditArea/preeditSpot: return the stored geometry values or fall back
  to the client window rect / window centre.
- setPreeditSpot:/setPreeditArea:: store geometry and forward to the
  compositor via zwp_text_input_v3_set_cursor_rectangle + commit so the
  IM candidate window tracks the text cursor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Button mapping:
- Handle BTN_SIDE (0x113), BTN_EXTRA (0x114), BTN_FORWARD (0x115),
  BTN_BACK (0x116), BTN_TASK (0x117) and any further evdev button codes
  as NSOtherMouseDown/Up instead of silently discarding them.
- Fix buttonNumber: was passing the raw evdev code (272, 273, …); now
  uses button - BTN_LEFT (0=left, 1=right, 2=middle, 3=side, 4=extra, …)
  matching AppKit convention and the X11 backend's XGetPointerMapping logic.
- pointer_handle_motion: default case now produces NSOtherMouseDragged
  for any held button beyond right, covering middle/side/extra drags.

Per-frame scroll accumulation:
- Bump wl_seat binding from v1 to v5 so the compositor delivers
  wl_pointer.frame, axis_source, axis_stop, and axis_discrete events.
- Add frame accumulation fields to struct pointer in WaylandConfig:
  frame_has_axis, frame_deltaX/Y, frame_discrete_x/y, frame_time.
- pointer_handle_axis: accumulate delta into the frame state instead of
  dispatching immediately; avoids two separate NSScrollWheel events when
  both vertical and horizontal axes arrive in the same compositor frame.
- pointer_handle_axis_discrete: store the integer step count per axis in
  the frame state (logged alongside the smooth delta in frame dispatch).
- pointer_handle_axis_source: unchanged logic; now also logs source.
- pointer_handle_frame: dispatch the accumulated axes as one NSScrollWheel
  event then reset all per-frame state; includes debug log with deltas,
  discrete steps, and source for traceability under WaylandScroll.
- pointer_handle_axis_stop: emit a zero-delta NSScrollWheel so AppKit
  sees that a touchpad gesture ended (basis for future momentum support).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
struct output additions (WaylandServer.h):
- effective_width / effective_height: logical pixel dimensions after
  applying scale factor and 90°/270° rotation transform.
- name / description: human-readable strings from wl_output v4 events.
- configured: YES after handle_done has fired at least once.

WaylandServer+Output.m — full rewrite:
- output_compute_effective_size(): divide physical mode dimensions by
  the scale factor, then swap width/height for 90° and 270° transforms
  (including the flipped variants).
- handle_done(): rewritten as the integration point for the full
  configuration batch —
    1. Recompute effective_width/height.
    2. Call reposition_windows_for_output() to clamp any window whose
       pos_x/y fell outside the new logical bounds; setFrameOrigin: is
       called on the NSWindow so AppKit tracks the corrected position.
    3. Post NSApplicationDidChangeScreenParametersNotification so
       NSScreen reloads its geometry (guarded against pre-NSApp calls).
    4. Skip steps 2-3 if nothing changed (suppresses spurious redraws).
- handle_name() / handle_description(): store v4 connector name and
  human-readable description; logged under WaylandOutput.
- Listener updated to all 6 entries (adds handle_name, handle_description).
- wl_output bound at version 4 (was 2) to receive name/description.

WaylandServer.m:
- handle_global_remove(): was empty; now finds the removed output by
  server_output_id, reassigns all its windows to the first remaining
  output (preventing NULL-deref in coordinate helpers), removes the
  output from output_list, destroys the wl_output proxy, frees all
  heap strings, and posts NSApplicationDidChangeScreenParametersNotification.
- boundsForScreen:: now finds the specific output matching the screen ID
  and returns NSMakeRect(alloc_x, alloc_y, effective_w, effective_h),
  falling back to the first output only when the ID is not found.
  Previously always returned the first output at (0,0).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pool_buffer struct (WaylandCairoShmSurface.h):
- Add needs_repaint (bool): set when handleExposeRect skips attach because
  the compositor still holds the buffer; cleared in release callback.
- Add owner_surface / owner_display: allow the release callback to
  re-attach and commit without going through the ObjC object.

WaylandCairoShmSurface.m — buffer lifecycle rewrite:
- finishBuffer: add close(buf->poolfd) to fix a file-descriptor leak that
  accumulated one open FD per window allocation over the session lifetime.
- createShmBuffer: use calloc so all fields start zeroed; initialise
  poolfd = -1 so the close() in finishBuffer is safe on error paths.
  Destroy wl_shm_pool immediately after buffer creation (correct and
  documented in the protocol); set buf->pool = NULL to avoid confusion.
  Return NULL on buffer creation failure instead of crashing.
- buffer_handle_release: if needs_repaint is set, re-attach + damage +
  commit the buffer immediately on release so a frame missed during a
  busy period is never permanently lost; only calls finishBuffer when
  no repaint is pending.
- initWithDevice: set owner_surface/owner_display back-pointers; add
  wl_surface_damage before the initial commit (without it the compositor
  treats the commit as a no-op); mark busy = true on attach.
- handleExposeRect: add three guards before attaching:
    1. Size mismatch: if pbuffer dimensions differ from window size, set
       needs_repaint and return — AppKit will allocate a correctly-sized
       surface; avoids attaching a wrong-sized buffer during resize churn.
    2. Busy check: if the compositor still holds the buffer, set
       needs_repaint and return; prevents a Wayland protocol error.
    3. Use the actual exposed NSRect (Y-flipped into Wayland coords) for
       wl_surface_damage instead of always damaging the full surface.
- dealloc: clear owner_surface/owner_display/needs_repaint before calling
  finishBuffer so the release callback cannot write to freed memory.
  Also clear window->wcs to avoid a dangling ObjC pointer.
- Add clearOwnerSurface method: called by destroySurfaceRole: just before
  wl_surface_destroy so the async release callback cannot dereference a
  destroyed Wayland proxy.

WaylandServer.m:
- destroySurfaceRole: call [wcs clearOwnerSurface] before destroying the
  wl_surface; then destroy wl_surface itself (role objects destroyed
  first per protocol — xdg_surface/layer_surface were already handled).
  Add #include for WaylandCairoShmSurface.h.

WaylandServer+Xdgshell.m:
- xdg_surface_on_configure terminated path: remove the duplicate
  wl_list_remove call. termwindow: already removed the window from
  window_list; calling wl_list_remove a second time corrupts the list
  in debug builds and is logically wrong regardless.  Now only free().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Header and banner updated from "Milestone 0" to "M0–M5" to reflect
cumulative coverage.

T9 pattern fixed: the "Milestone 3" TODO comment was replaced with real
button-mapping code in M3; the check now looks for BTN_SIDE which is
present in the buttonNumber documentation block.

New M5 tests (T12–T19):

T12 — Buffer FD lifecycle: verify finishBuffer closes poolfd (FD-leak
      fix), pool_buffer is zero-initialised via calloc, needs_repaint
      and owner back-pointer fields are present.

T13 — handleExposeRect safety: verify busy guard is present (prevents
      attaching a compositor-held buffer), repaint-on-release path exists
      in buffer_handle_release, and size-mismatch guard is present.

T14 — Precise damage rect: verify handleExposeRect uses NSMinX/NSMaxY
      for the damaged region instead of always the full surface, and
      that the initial commit in initWithDevice is preceded by damage.

T15 — Use-after-free prevention: verify clearOwnerSurface is called
      before wl_surface_destroy in destroySurfaceRole, and that
      wl_surface_destroy itself is present in that function.

T16 — No double wl_list_remove: verify the terminated path in
      xdg_surface_on_configure calls free(window) (pattern escaped
      as "free.window." to avoid ERE capturing-group confusion) and
      does NOT call wl_list_remove( (tightened to the actual call
      rather than the identifier, so comments mentioning the function
      name do not trigger a false fail).

T17 — wl_shm_pool lifecycle: verify the pool is destroyed immediately
      after buffer creation (wl_shm_pool_destroy present) and that
      buf->pool is not stored for later use.

T18 — Sustained-operation stress: fire wayland-info 20 times in rapid
      succession; compositor must survive without crash or hang.

T19 — Post-stress health: compositor process still alive; log contains
      no crash, abort, SIGSEGV, double-free, or disconnect markers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bumping the wl_seat binding from v1 to v5 in Milestone 3 caused
wl_seat.name (opcode 1, introduced in v2) to be delivered during the
initial roundtrip.  The seat_listener struct only declared one slot
(capabilities), leaving the name slot NULL.  libwayland-client aborts
with "listener function for opcode 1 of wl_seat is NULL" when it
encounters a NULL dispatcher.

Fix: add seat_handle_name which logs the seat name under the WaylandIME
debug category and satisfies the listener struct contract.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
wayland-client 1.24 defines wl_pointer_listener with 11 event slots:
  0  enter          (v1)
  1  leave          (v1)
  2  motion         (v1)
  3  button         (v1)
  4  axis           (v1)
  5  frame          (v5)
  6  axis_source    (v5)
  7  axis_stop      (v5)
  8  axis_discrete  (v5)
  9  axis_value120  (v8)  ← was NULL → abort
  10 axis_relative_direction (v9) ← was NULL → abort

We bound wl_seat at v5 (Milestone 3) which should gate v8/v9 events, but
wlroots 0.20 / Ambrosia sends these events regardless when the hardware
generates high-resolution scroll input.  libwayland-client aborts on any
NULL dispatcher slot when that event arrives during wl_display_roundtrip.

Fixes:
  pointer_handle_axis_value120: converts 1/120-unit steps to logical
    scroll deltas (÷120 × mouse_scroll_multiplier) and accumulates them
    in the per-frame state alongside the smooth axis value.
  pointer_handle_axis_relative_direction: logs the natural-scroll hint
    under WaylandScroll; direction inversion is a future improvement.
  Both handlers added to pointer_listener in correct opcode order.

Also fixes wl_seat_listener which had only one slot (capabilities) after
the v5 bump added the wl_seat.name event (opcode 1).  seat_handle_name
was already committed in the previous fix; this commit ensures both
listener arrays are complete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs prevented click-and-drag from working correctly within the
same GNUstep application on Wayland while it worked fine on X11:

1. postDragEvent: discarded all events except NSLeftMouseUp
   Once _waylandExternalDragActive was set (which happened for every drag
   when wl_data_device is available), the override returned immediately
   for any event type.  This meant the GSAppKitDraggingEnter/Update/Drop
   events posted by the device_enter/motion/drop callbacks were dequeued
   by GSDragView's event loop and then silently thrown away, so the target
   view's draggingEntered:/draggingUpdated:/performDragOperation: methods
   were never called.

   Fix: pass NSAppKitDefined events through to [super postDragEvent:] so
   they are forwarded to the target window.  NSLeftMouseUp still exits the
   loop; all other types (pointer motion, which stops arriving during a
   Wayland drag) are suppressed.

2. device_enter/motion/leave posted events to the app queue for in-process
   drops.  GSDragView's running event loop dequeued them, called
   postDragEvent:, and (before fix 1) discarded them.  Even with fix 1
   the queue path adds latency and could interact with GSDragView logic.

   Fix: detect in-process drops (dnd_source != NULL means we are the drag
   source, so the target is one of our own windows).  For in-process,
   send events directly to the target NSWindow via [nswindow sendEvent:]
   bypassing the app queue entirely.

3. device_enter called setupInboundDragWithPasteboard: unconditionally.
   This overwrote dragPasteboard from the live source pasteboard to an
   empty NSDragPboard, which would have corrupted data_source_send for
   any concurrent external receiver.  For in-process drops the shared
   WaylandDragView already carries the correct source info from dragImage:.

   Fix: skip setupInboundDragWithPasteboard: when inProcess == YES.

4. device_drop used a pipe for in-process data transfer.  Same-client
   DnD deadlocks: wl_data_offer_receive asks the compositor to call
   wl_data_source.send back to us, but we are blocked in read() inside
   a dispatch callback and cannot dispatch that event.

   Fix: for in-process drops, copy types and data directly from the source
   pasteboard (WaylandDragView.dragPasteboard) to NSDragPboard in memory,
   with no pipe and no roundtrip.  The drop event is then delivered via
   [nswindow sendEvent:] so performDragOperation: sees the correct data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…views

Pointer events for a wl_subsurface arrive with sx/sy relative to that
subsurface's own origin, not the parent window origin.  The previous
approach stored the subsurface offset in a single gl_subsurface_x/y
pair on struct window, which was overwritten by whichever WaylandGLContext
ran last — breaking coordinate translation for all other GL views in the
same window (e.g. the three NSOpenGLViews in MyGL.app each picking up the
wrong view's offset, causing hitTest: to always route mouse events to the
same view regardless of which one was clicked).

Replace the per-window offset fields with a struct wl_surface_binding
stored as the wl_surface user data for every GNUstep-owned surface:
- Main window surfaces embed their binding (offset 0,0) directly inside
  struct window — no allocation required.
- Each WaylandGLContext mallocs its own binding with its actual subsurface
  position; the binding is updated on resize and freed with the surface.

pointer_handle_enter reads the per-surface offset via surface_get_offset()
and stores it in pointer.focus_offset_x/y.  pointer_handle_motion applies
that offset unconditionally (it is zero for main surfaces).  All other
wl_surface_get_user_data call sites are updated to surface_get_window().

Also folds in the earlier keyboard_handle_enter NULL-surface guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@DMJC DMJC requested a review from fredkiefer as a code owner May 16, 2026 01:49
Comment thread wayland_feature_implementation_plan.md Outdated
@@ -0,0 +1,173 @@
# Wayland Backend Feature Implementation Plan (vs X11 parity)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It is surely helpful for you to have these two files, but we should store them someplace else. Either in the Documentation folder or in the wayland folder itself.

Copy link
Copy Markdown
Author

@DMJC DMJC May 16, 2026

Choose a reason for hiding this comment

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

I've removed the automated testing files from the folder. The Smoke test was specifically used to verify that the changes to text input/feature parity with the X11 backend were working. The planning.MD files were created by Chat GPT when I asked it to compare the X11 backend and the Wayland backend for feature differences. Those plan files are no longer required as the plans were executed by claude and were successful.

@@ -0,0 +1,1815 @@
#! /bin/sh
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you please explain what this and the next file are for and why they live in a folder with a rather strange name? And why this went unnoticed? This could give the impression that you never even looked through the changes your AI did produce. And if you didn't , why should I?
I really would like to see the changes from this PR to be applied but this will require more commitment from your side.

Copy link
Copy Markdown
Author

@DMJC DMJC May 16, 2026

Choose a reason for hiding this comment

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

That entire folder is unnecessary. I've removed it from the Source tree. It appears that it was the $GNUSTEP_MAKEFILES folder e.g /usr/GNUstep/System/Library/Makefiles. At some point rather than link/refer to the folder the folder contents were copied into that location. Subsequent runs never used it.

# Additional library directories the linker should search
#ADDITIONAL_LIB_DIRS +=
CONFIG_SYSTEM_LIB_DIR += $(GRAPHIC_LFLAGS)
CONFIG_SYSTEM_LIB_DIR += $(GRAPHIC_LFLAGS) -lGL -lEGL -lwayland-egl
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should not add libraries here. This should rather be done in the configure script when the libraries are requested and present.

Comment thread configure.ac

AC_INIT
AC_PREREQ([2.73])
AC_PREREQ([2.72])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why did you go back to the older version here? You otherwise had no changes to this file and so a regeneration of the configure file should not be necessary.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I manually regenerated the file and when I was merging it I was getting a mismatch preventing it from merging. I'm on Debian 14 with Autoconf 2.72.

@fredkiefer
Copy link
Copy Markdown
Member

In its current form this PR is almost unreviewable. We need to split it up. It looks like you are building up on already existing other changes. Should we try to get this other changes in one by one first? Are these mergable by themselves or are there changes required?
And after that we still need to split up your changes even further to be able to get them in. You asked for help on the mailing list. Did anybody reply? If not we could try to do this together, but my time is rather limited.

@DMJC
Copy link
Copy Markdown
Author

DMJC commented May 16, 2026

Thanks Fred for taking some time on this, I will try to split the changes up by feature and resubmit the requests. Now that I know some of the problems I'll try to make this easier to merge.

There are still issues in the Wayland backend, but they are mostly around popup/right click menus, and popup windows like file open/save dialogs disappearing. This was the case before I made my changes.

@fredkiefer
Copy link
Copy Markdown
Member

I will try to split the changes up by feature and resubmit the requests. Now that I know some of the problems I'll try to make this easier to merge.

Sounds great. Looking forward to help you with this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants