Phase 82: wlroots 0.19 compatibility + native Wayland client support

- Add wlr_subcompositor global (required by all modern Wayland clients)
- Switch from xdg_shell new_surface to new_toplevel signal (wlroots 0.19
  fires new_surface with role=NONE before get_toplevel assigns the role)
- Defer initial xdg_surface configure to first client commit via
  surface->events.commit listener (surface must be initialized first)
- Defer xdg_decoration set_mode to first commit (set_mode internally
  calls schedule_configure which asserts initialized)
- Wire destroy_listener to toplevel->events.destroy instead of
  xdg_surface->events.destroy (wlroots asserts toplevel event lists
  are empty before the toplevel is freed)
- Log DISPLAY and WAYLAND_DISPLAY env vars at startup
- Add WAYLAND_ROADMAP.md

Native Wayland clients (foot, etc.) can now connect, render, and
disconnect without crashing the compositor.
This commit is contained in:
Levi Neely 2026-05-14 11:57:15 +02:00
parent da4f662844
commit 288d2ad3a7
2 changed files with 343 additions and 45 deletions

245
WAYLAND_ROADMAP.md Normal file
View File

@ -0,0 +1,245 @@
# WindowMaker Wayland Port — Roadmap
Last updated: 2026-05-14
## Architecture
WindowMaker 0.96.0 uses a vtable-based backend abstraction (`WMBackend` struct in
`src/backend/backend.h`) with ~150+ function pointer slots. Policy code calls
`wm_backend->operation(...)` and never touches X11/Wayland APIs directly.
Two backends:
- **X11**: `src/backend/x11/x11_backend.c` (150K)
- **Wayland**: `src/backend/wayland/wl_backend.c` (316K, wlroots-based compositor)
Activation: `WMAKER_USE_WAYLAND=1` env var. Nested mode: `WMAKER_WAYLAND_NESTED=1`.
Three major libraries:
- **src/** — WM core (policy layer, zero Xlib calls remaining)
- **WINGs/** — Widget toolkit ("WINGs Is Not GNUstep"), split into `_x11.c` / `_wl.c` backends
- **wrlib/** — Raster graphics library (image loading, manipulation, rendering)
---
## Completed (as of 2026-05-14)
- Backend vtable abstraction (150+ slots)
- Policy layer fully X11-free (zero direct Xlib calls in WM core)
- wlroots compositor (headless + nested modes)
- XDG shell + XWayland bridge
- Frame rendering (titlebar, buttons, resizebar) via pixman → wlr_scene_buffer
- Menu rendering + highlighting + item activation
- Root menu from config file
- Dock rendering (SHM panel, icon slots, app tracking)
- Clip rendering
- Background rendering
- Mouse input (motion, buttons, scroll, grabs)
- Keyboard input (XKB, modifiers, keysym translation)
- Interactive move/resize with edge detection + cursor changes
- Click-to-focus via hit-testing
- Run dialog (WINGs modal loop with compositor tick)
- Window manage/unmanage lifecycle (XDG toplevel + XWayland)
- Focus delivery (wlr_seat_keyboard_notify_enter)
- WINGs Wayland backend (views, fonts/PangoFT2, events/ring buffer, rendering/pixman, colors)
- Smoke test suite (30+ tests in tests/wl-smoke-test.sh)
---
## Phase A: Core Window Management Gaps
Priority: HIGH — needed for daily-driver use.
| # | Task | Key Files | Complexity |
|---|------|-----------|------------|
| A1 | Stacking (raise/lower via scene graph) | stacking.c, wlr_scene_node_raise/lower | Low |
| A2 | Window maximize (full/half/vertical/horizontal) | actions.c, usable area calc | Low-Medium |
| A3 | Window shade (titlebar collapse) | actions.c, frame buffer resize | Low |
| A4 | Workspace switching (create/destroy/switch) | workspace.c, scene tree reparenting | Medium |
| A5 | Alt-Tab window cycling + switch panel | cycling.c, switchpanel.c, vtable switchpanel_enter | Medium |
| A6 | Window iconification (minimize) + miniwindow | icon.c, appicon.c, vtable icon_paint/icon_update_pixmap | Medium |
| A7 | Window placement algorithms (smart/cascade/random) | placement.c (may already work via vtable) | Low |
---
## Phase B: Dock & App Icons
| # | Task | Key Files | Complexity |
|---|------|-----------|------------|
| B1 | App icon rendering (load .tiff/.png, render into dock) | appicon.c, wrlib loaders, vtable appicon_show | Medium |
| B2 | Dock icon drag & drop (reorder, add/remove) | dock.c DnD handlers | Medium |
| B3 | Dock auto-launch on login | dock.c session restore | Low |
| B4 | Clip workspace indicator (number, arrows) | dock.c clip paint | Low |
| B5 | Drawer support (horizontal dock extension) | dock.c drawer subsystem | Medium |
| B6 | Icon bounce on launch | vtable appicon_bounce | Low |
---
## Phase C: Visual Polish & Animations
| # | Task | Key Files | Complexity |
|---|------|-----------|------------|
| C1 | Slide animations (iconify/deiconify zoom) | vtable slide_window | Medium |
| C2 | Menu scrolling (long menus) | menu.c scroll logic | Low |
| C3 | Balloon tooltips (dock icon hover) | vtable balloon_show | Low |
| C4 | Cursor themes (xcursor loading) | wlr_xcursor_manager | Low |
| C5 | Window opacity (NETWM alpha) | vtable netwm_set_opacity | Low |
---
## Phase D: WINGs Widgets (for Preferences & Inspectors)
Core rendering backends exist. Complex widgets need verification/porting:
| Widget | Needed For | Notes |
|--------|-----------|-------|
| WMList | File panels, icon chooser | Test |
| WMBrowser | File browser in dialogs | Test |
| WMColorPanel | Color picker in prefs | May need porting |
| WMFontPanel | Font selection | May need porting |
| WMFilePanel | Open/save dialogs | Test |
| WMTabView | Preferences app tabs | Test |
| WMScroller/WMScrollView | Scrollable content | Test |
| WMSlider | Preference sliders | Test |
| WMPopUpButton | Dropdown menus in prefs | Test |
| WMTextField (advanced) | Multi-line editing | Test |
| Drag & Drop | Icon DnD, file DnD | Likely needs porting |
---
## Phase E: Dialogs & Panels
| # | Task | Key Files | Complexity |
|---|------|-----------|------------|
| E1 | Window Inspector (per-window attributes) | winspector.c | High (many widgets) |
| E2 | Info Panel (about dialog) | dialog.c | Low |
| E3 | Crash Dialog (recovery UI) | dialog.c | Low |
| E4 | Icon Chooser (browse icon files) | dialog.c | Medium |
| E5 | Preferences App (full GUI) | WPrefs.app/ | High |
---
## Phase F: wrlib Wayland Integration
| # | Task | Description | Complexity |
|---|------|-------------|------------|
| F1 | RContext for Wayland | pixman-backed RContext without X11 Display | Medium |
| F2 | RImage → pixman_image conversion | Direct path without XImage | Low (may exist) |
| F3 | Texture rendering verification | Solid, gradient, pixmap textures | Verify |
| F4 | Image format loaders | PNG/JPEG/GIF/WebP (X11-independent) | Verify |
---
## Phase G: Session & Persistence
| # | Task | Key Files | Complexity |
|---|------|-----------|------------|
| G1 | Session save/restore (positions, workspaces) | session.c | Medium |
| G2 | Dock state persistence (icon layout) | dock.c state files | Low (may work) |
| G3 | Defaults database (~/GNUstep/Defaults/) | defaults.c | Verify |
| G4 | Autostart (launch apps from session) | startup.c | Low |
---
## Phase H: Advanced Compositor Features
| # | Task | Description | Complexity |
|---|------|-------------|------------|
| H1 | Multi-monitor (output layout, per-output workspaces) | wlr_output_layout, screen.c | High |
| H2 | HiDPI / output scaling | wlr_output_set_scale, buffer scaling | Medium |
| H3 | Gamma control (night light) | Already advertised, wire up | Low |
| H4 | Screen lock integration | ext-session-lock-v1 protocol | Medium |
| H5 | Clipboard (copy/paste between clients) | wl_data_device (may work via wlroots) | Verify |
| H6 | Primary selection | Already advertised | Verify |
| H7 | Screencopy (screenshots) | Already advertised | Verify |
| H8 | DMA-BUF (GPU buffer sharing) | Already advertised | Verify |
| H9 | Input method (IME support) | Already advertised | Medium |
---
## Phase I: EWMH / Foreign Toplevel
| # | Task | Description | Complexity |
|---|------|-------------|------------|
| I1 | Foreign toplevel list (taskbar integration) | Already advertised, verify events | Low |
| I2 | XDG activation (focus stealing prevention) | Wire to policy | Low |
| I3 | Idle inhibit (prevent blank during video) | Already advertised | Low |
---
## Phase J: DRM/KMS Backend (Native)
For running without a host compositor:
| # | Task | Description | Complexity |
|---|------|-------------|------------|
| J1 | DRM backend | wlr_backend_autocreate | Medium |
| J2 | libinput (real hardware input) | wlroots handles this | Low |
| J3 | VT switching | wlr_session | Low |
| J4 | Multi-GPU | Secondary GPU rendering | High |
---
## Suggested Implementation Order
```
NOW (daily-driver basics):
A1. Stacking (raise/lower)
A2. Maximize
A3. Shade
A4. Workspace switching
A5. Alt-Tab cycling
A6. Icon/minimize
A7. Placement (verify)
NEXT (visual completeness):
B1. App icon rendering (wrlib → pixman)
F1. wrlib RContext for Wayland
C1. Slide animations
C3. Balloon tooltips
THEN (full feature parity):
D*. Test all WINGs widgets
E1. Window Inspector
E4. Icon Chooser
G*. Session persistence
B2-B6. Full dock features
LATER (production-ready):
H1. Multi-monitor
H2. HiDPI
J*. DRM/KMS native backend
E5. WPrefs app
```
---
## Technical Notes
1. **Rendering pattern**: pixman_image_t → wlr_scene_buffer → position in scene graph
2. **wrlib is mostly X11-independent** — only context.c and convert.c touch X11
3. **WINGs complex widgets** are the hardest remaining work (subtle X11 assumptions in events/coords)
4. **wlroots handles compositor plumbing** — protocols advertised, work is wiring to policy
5. **wl_backend.c (316K) should be split** into wl_frame.c, wl_menu.c, wl_input.c, wl_toplevel.c, wl_output.c as complexity grows
6. **Many vtable slots are no-ops** — stubs exist, need implementation following the pixman→scene pattern
---
## Key Source Files Reference
| File | Size | Purpose |
|------|------|---------|
| src/backend/backend.h | 74K | Vtable definition |
| src/backend/backend_types.h | 16K | Backend-neutral types |
| src/backend/wayland/wl_backend.c | 316K | Wayland compositor |
| src/backend/x11/x11_backend.c | 150K | X11 backend |
| src/window.c | 83K | Window management |
| src/dock.c | 130K | Dock/Clip/Drawers |
| src/menu.c | 61K | Menu system |
| src/actions.c | 68K | Window actions |
| src/defaults.c | 102K | Preferences/config |
| src/event.c | 61K | Event dispatch |
| src/moveres.c | 67K | Move/resize |
| src/wmspec.c | 61K | EWMH compliance |
| src/main.c | 22K | Entry point |
| WINGs/WINGs.h | 56K | Toolkit public API |
| wrlib/wraster.h | 17K | Raster lib public API |

View File

@ -119,6 +119,7 @@
#include <wlr/render/wlr_renderer.h>
#include <wlr/interfaces/wlr_output.h>
#include <wlr/types/wlr_compositor.h>
#include <wlr/types/wlr_subcompositor.h>
#include <wlr/types/wlr_data_device.h>
#include <wlr/types/wlr_output.h>
#include <wlr/types/wlr_output_layout.h>
@ -241,6 +242,7 @@ struct wl_toplevel_view {
struct wl_listener request_fullscreen_listener;/* Phase 16 */
struct wl_listener request_minimize_listener; /* Phase 16 */
struct wl_listener set_app_id_listener; /* Phase 16 */
struct wl_listener commit_listener; /* initial configure */
/* Phase 17: XWayland — set for X11 windows, NULL for XDG toplevels. */
struct wlr_xwayland_surface *xw_surface;
/* Path 1: geometry listener — keeps scene node in sync when X11 window moves. */
@ -1091,6 +1093,7 @@ handle_toplevel_destroy(struct wl_listener *listener, void *data)
wl_list_remove(&view->request_fullscreen_listener.link);
wl_list_remove(&view->request_minimize_listener.link);
wl_list_remove(&view->set_app_id_listener.link);
wl_list_remove(&view->commit_listener.link);
wl_list_remove(&view->link);
/* Phase 18: remove scene node. */
if (view->scene_node) {
@ -1110,17 +1113,47 @@ handle_toplevel_destroy(struct wl_listener *listener, void *data)
/*
* Phase 64: new_toplevel_decoration listener.
* Immediately respond with CLIENT_SIDE so clients draw their own
* title bars and borders (the Wayland path has no X11 frame rendering).
* Respond with CLIENT_SIDE so clients draw their own decorations.
* Must defer until the surface is initialized (wlroots 0.19).
*/
struct wl_deferred_decoration {
struct wl_listener commit_listener;
struct wlr_xdg_toplevel_decoration_v1 *deco;
};
static void
handle_deferred_decoration_commit(struct wl_listener *listener, void *data)
{
struct wl_deferred_decoration *dd =
wl_container_of(listener, dd, commit_listener);
struct wlr_xdg_surface *surface = dd->deco->toplevel->base;
(void)data;
if (!surface->initial_commit)
return;
wl_list_remove(&dd->commit_listener.link);
wlr_xdg_toplevel_decoration_v1_set_mode(dd->deco,
WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE);
wfree(dd);
}
static void
handle_new_toplevel_decoration(struct wl_listener *listener, void *data)
{
struct wlr_xdg_toplevel_decoration_v1 *deco = data;
(void)listener;
wlr_xdg_toplevel_decoration_v1_set_mode(deco,
WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE);
wwarning("wl_backend: decoration mode=client_side for new toplevel");
struct wlr_xdg_surface *surface = deco->toplevel->base;
if (surface->initialized) {
wlr_xdg_toplevel_decoration_v1_set_mode(deco,
WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE);
} else {
struct wl_deferred_decoration *dd = wmalloc(sizeof(*dd));
dd->deco = deco;
dd->commit_listener.notify = handle_deferred_decoration_commit;
wl_signal_add(&surface->surface->events.commit, &dd->commit_listener);
}
}
/* ------------------------------------------------------------------ */
@ -1523,30 +1556,43 @@ handle_xwayland_new_surface(struct wl_listener *listener, void *data)
}
/*
* Phase 36 / 38: new XDG surface listener.
* Allocates a wl_toplevel_view, assigns a synthetic WNativeWindow ID,
* wires the three lifecycle listeners, and schedules the initial configure.
* handle_xdg_toplevel_initial_commit -- wlroots 0.19 requires that the
* initial configure is sent in response to the client's first (empty) commit,
* not at new_surface time. The surface is only "initialized" after this
* commit, so schedule_configure is safe here.
*/
static void
handle_xdg_new_surface(struct wl_listener *listener, void *data)
handle_xdg_toplevel_initial_commit(struct wl_listener *listener, void *data)
{
struct wlr_xdg_surface *xdg_surface = data;
struct wl_toplevel_view *view =
wl_container_of(listener, view, commit_listener);
(void)data;
if (!view->xdg_surface || !view->xdg_surface->initial_commit)
return;
/* Only need this once — remove ourselves. */
wl_list_remove(&view->commit_listener.link);
wl_list_init(&view->commit_listener.link);
wlr_xdg_toplevel_set_size(view->xdg_surface->toplevel, 0, 0);
wlr_xdg_surface_schedule_configure(view->xdg_surface);
}
/*
* Phase 36 / 38: new XDG toplevel listener (wlroots 0.19+).
* Fires when the client calls get_toplevel on an xdg_surface.
* Allocates a wl_toplevel_view, assigns a synthetic WNativeWindow ID,
* wires lifecycle listeners, and sets up the initial commit handler.
*/
static void
handle_xdg_new_toplevel(struct wl_listener *listener, void *data)
{
struct wlr_xdg_toplevel *toplevel = data;
struct wlr_xdg_surface *xdg_surface = toplevel->base;
struct wl_toplevel_view *view;
(void)listener;
if (xdg_surface->role == WLR_XDG_SURFACE_ROLE_POPUP) {
/* Phase 66: schedule the initial configure for XDG popups so clients
* that use popups (context menus, autocomplete, tooltips) can commit
* their first buffer without stalling. We do not manage popups in
* the policy layer — they are self-contained transient surfaces. */
wlr_xdg_surface_schedule_configure(xdg_surface);
wwarning("wl_backend: new XDG popup (scheduled configure)");
return;
}
if (xdg_surface->role != WLR_XDG_SURFACE_ROLE_TOPLEVEL)
return;
view = wmalloc(sizeof(*view));
memset(view, 0, sizeof(*view));
view->xdg_surface = xdg_surface;
@ -1556,34 +1602,36 @@ handle_xdg_new_surface(struct wl_listener *listener, void *data)
view->unmap_listener.notify = handle_toplevel_unmap;
view->destroy_listener.notify = handle_toplevel_destroy;
view->title_listener.notify = handle_toplevel_title;
view->request_move_listener.notify = handle_toplevel_request_move; /* Phase 15 */
view->request_resize_listener.notify = handle_toplevel_request_resize; /* Phase 15 */
view->request_maximize_listener.notify = handle_toplevel_request_maximize; /* Phase 16 */
view->request_fullscreen_listener.notify = handle_toplevel_request_fullscreen; /* Phase 16 */
view->request_minimize_listener.notify = handle_toplevel_request_minimize; /* Phase 16 */
view->set_app_id_listener.notify = handle_toplevel_set_app_id; /* Phase 16 */
view->request_move_listener.notify = handle_toplevel_request_move;
view->request_resize_listener.notify = handle_toplevel_request_resize;
view->request_maximize_listener.notify = handle_toplevel_request_maximize;
view->request_fullscreen_listener.notify = handle_toplevel_request_fullscreen;
view->request_minimize_listener.notify = handle_toplevel_request_minimize;
view->set_app_id_listener.notify = handle_toplevel_set_app_id;
wl_signal_add(&xdg_surface->surface->events.map, &view->map_listener);
wl_signal_add(&xdg_surface->surface->events.unmap, &view->unmap_listener);
wl_signal_add(&xdg_surface->events.destroy, &view->destroy_listener);
wl_signal_add(&xdg_surface->toplevel->events.set_title, &view->title_listener);
wl_signal_add(&xdg_surface->toplevel->events.request_move, &view->request_move_listener);
wl_signal_add(&xdg_surface->toplevel->events.request_resize, &view->request_resize_listener);
wl_signal_add(&xdg_surface->toplevel->events.request_maximize, &view->request_maximize_listener);
wl_signal_add(&xdg_surface->toplevel->events.request_fullscreen, &view->request_fullscreen_listener);
wl_signal_add(&xdg_surface->toplevel->events.request_minimize, &view->request_minimize_listener);
wl_signal_add(&xdg_surface->toplevel->events.set_app_id, &view->set_app_id_listener);
wl_signal_add(&toplevel->events.destroy, &view->destroy_listener);
wl_signal_add(&toplevel->events.set_title, &view->title_listener);
wl_signal_add(&toplevel->events.request_move, &view->request_move_listener);
wl_signal_add(&toplevel->events.request_resize, &view->request_resize_listener);
wl_signal_add(&toplevel->events.request_maximize, &view->request_maximize_listener);
wl_signal_add(&toplevel->events.request_fullscreen, &view->request_fullscreen_listener);
wl_signal_add(&toplevel->events.request_minimize, &view->request_minimize_listener);
wl_signal_add(&toplevel->events.set_app_id, &view->set_app_id_listener);
/* wlroots 0.19: send initial configure on the client's first commit,
* when the surface becomes initialized. */
view->commit_listener.notify = handle_xdg_toplevel_initial_commit;
wl_signal_add(&xdg_surface->surface->events.commit, &view->commit_listener);
wl_list_insert(&wl_state.toplevel_list, &view->link);
wl_state.managed_surface_count++;
wwarning("wl_backend: new XDG toplevel id=0x%lx app_id=%s title=%s",
view->id,
xdg_surface->toplevel->app_id ? xdg_surface->toplevel->app_id : "(none)",
xdg_surface->toplevel->title ? xdg_surface->toplevel->title : "(none)");
/* Phase 37: initial configure so the client can commit its first buffer. */
wlr_xdg_surface_schedule_configure(xdg_surface);
toplevel->app_id ? toplevel->app_id : "(none)",
toplevel->title ? toplevel->title : "(none)");
}
/* ------------------------------------------------------------------ */
@ -2157,6 +2205,7 @@ wl_display_open(const char *name)
/* Phase 35: advertise core Wayland protocol globals. */
wl_state.compositor =
wlr_compositor_create(wl_state.display, 6, wl_state.renderer);
wlr_subcompositor_create(wl_state.display);
wl_state.seat = wlr_seat_create(wl_state.display, "seat0");
wl_state.data_device_mgr =
wlr_data_device_manager_create(wl_state.display);
@ -2432,9 +2481,9 @@ wl_display_open(const char *name)
wl_state.next_synthetic_id = 0x10000000UL;
wl_state.managed_surface_count = 0;
/* Phase 36: listen for new XDG toplevel surfaces. */
wl_state.xdg_new_surface_listener.notify = handle_xdg_new_surface;
wl_signal_add(&wl_state.xdg_shell->events.new_surface,
/* Phase 36: listen for new XDG toplevels (wlroots 0.19+). */
wl_state.xdg_new_surface_listener.notify = handle_xdg_new_toplevel;
wl_signal_add(&wl_state.xdg_shell->events.new_toplevel,
&wl_state.xdg_new_surface_listener);
/* Path 1: XWayland bridge — start Xwayland eagerly (lazy=false) so that
@ -2949,6 +2998,10 @@ wl_event_loop_run(void)
wwarning("wl_backend: X11 event source registered for WINGs dispatch");
}
wwarning("wl_backend: DISPLAY=%s WAYLAND_DISPLAY=%s",
getenv("DISPLAY") ? getenv("DISPLAY") : "(unset)",
getenv("WAYLAND_DISPLAY") ? getenv("WAYLAND_DISPLAY") : "(unset)");
/* Phase A: register compositor dispatch callback so WINGs modal loops
* (WMRunModalLoop, color panels, etc.) can tick the event loop. */
{