Skip to content

Advertise _NET_SUPPORTED / _NET_SUPPORTING_WM_CHECK on the root - #79

Merged
jserv merged 5 commits into
mainfrom
wm-net
Aug 10, 2026
Merged

Advertise _NET_SUPPORTED / _NET_SUPPORTING_WM_CHECK on the root#79
jserv merged 5 commits into
mainfrom
wm-net

Conversation

@jserv

@jserv jserv commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary by cubic

Announces EWMH support on the root by creating a supporting-WM-check window and publishing an exact _NET_SUPPORTED list, so EWMH-aware toolkits send the _NET_WM_STATE and _NET_WM_WINDOW_TYPE requests we already handle. Centralizes honored atom tables, hides internal windows, honors pre-map _NET_WM_WINDOW_TYPE, and adds tests to pin the contract.

  • New Features

    • Publish EWMH handshake on XOpenDisplay: create an internal, unmapped InputOnly check window; set its _NET_WM_NAME; write root _NET_SUPPORTING_WM_CHECK and _NET_SUPPORTED; idempotent reuse on subsequent opens.
    • Build _NET_SUPPORTED from single sources of truth: netWmStateHonored (offsetof-backed flags, _NET_WM_STATE_ABOVE gated to SDL >= 2.0.16) and netWmWindowTypeHonored (borderless types). Deliberately exclude _NET_ACTIVE_WINDOW until the root property is maintained.
    • Honor _NET_WM_WINDOW_TYPE set before map: decode from the stored property and apply during realize replay; add netWmWindowTypeWantsBorderless() and use it in the write handler; testable headlessly.
    • Hide internal windows (clipboard, EWMH check) from XQueryTree and snapshots; free root properties, name, colormap windows, and GLX/present buffers on teardown.
    • Tests: verify root handshake, exact supported-set equality (including ABOVE gating and ACTIVE absence), reuse across nested displays, borderless decoding (including pre-map case and SDL flag when available), and popup-type subset.
  • Bug Fixes

    • Fix SDL_ttf thunk for TTF_GlyphIsProvided: use const for versions < 2.0.18, avoiding conflicting types on hosts like 2.0.14/2.0.15.
    • Prevent teardown crashes: filter destroyed children in sibling VisibilityNotify posting and exposure-reset walks.

Written for commit 47f9227. Summary will update on new commits.

Review in cubic

cubic-dev-ai[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

jserv added 5 commits August 10, 2026 11:28
The dlwrap thunks define the symbol they wrap, so a signature that
disagrees with the installed SDL_ttf.h is a hard conflicting-types error
rather than a warning. SDL_ttf carried a const font argument on
TTF_GlyphIsProvided until 2.0.18 dropped it, and the thunk here declares
it non-const unconditionally, so any host still on the const spelling
fails to build the wrapper at all:

  src/wrapper/sdl-ttf-wrapper.c:60:15: error: conflicting types for
  'TTF_GlyphIsProvided'
  /usr/include/SDL2/SDL_ttf.h:160:29: note: previous declaration ...
  (const TTF_Font *font, Uint16 ch)

That is the differential host, which ships 2.0.15, so its compat-side
build could not start. Upstream headers put the boundary at 2.0.18:
2.0.12, 2.0.14 and 2.0.15 all prototype const TTF_Font *, while 2.0.18
and 2.24 do not. Split on that single upper bound.

TTF_GlyphIsProvided32 arrived in 2.0.18, after the revert, so it is
non-const for its whole life and needs no gate.
destroyWindow leaves a destroyed child in its parent's array typed
CLOSED_WINDOW with a NULL struct, and the screen teardown relies on
that: destroyScreenWindowImpl passes freeParentData=False so the ids
linger until the whole array is freed. Walkers over that array are
therefore expected to filter on the type slot, which
hasPendingWindowPresent (src/drawing.c) and
repaintTopLevelsOverlappingRect (src/window.c) already do.

Two walkers were missing the filter and dereference the struct:

postVisibilityForWindowAndSiblings crashes when a client calls
XCloseDisplay while a popup is still mapped. Destroying the popup's
parent drains the anchored popup, whose unmap posts VisibilityNotify to
every sibling, and postEvent then dereferences a window that is already
gone:

  postEvent
  postVisibilityForWindowAndSiblings
  unmapWindowInternal
  drainAnchoredPopups
  destroyWindow
  destroyScreenWindowImpl
  XCloseDisplay

The faulting address is the struct field offset off a NULL base.
Whether it fires depends on which child is destroyed first, so it needs
only one more window ahead of the client's own in the root's list.

enqueueResetExposures has the same shape: a render-targets or
render-device reset arriving mid-teardown reads ->sdlWindow off a
CLOSED_WINDOW slot.
destroyScreenWindowImpl frees the root WindowStruct directly instead of
going through destroyWindow, and it released only the children array and
the pixman regions. Everything else destroyWindow frees for an ordinary
window leaked once per XOpenDisplay/XCloseDisplay cycle: the property
list (the root already carries one, since compatPublishHiDpiScaleProperty
writes the HiDPI scale there), a name if a client ever called XStoreName
on the root, the colormap-window list, the present readback scratch and
the GLX buffers.

A process that cycles displays (the Motif and Xt probes, the check
suite, an embedding host) grew its heap monotonically with no way to
reclaim it, and a leak checker that runs a single cycle cannot see it.
An EWMH-aware toolkit (GTK, Qt, recent Xt) probes the root for
_NET_SUPPORTING_WM_CHECK before it speaks EWMH at all. We run an
in-process WM and already act on _NET_WM_STATE add/remove/toggle,
_NET_CLOSE_WINDOW and _NET_WM_WINDOW_TYPE, but the root advertised
nothing, so those clients concluded no compliant manager was running and
fell back to self-decorating, skipping the very requests we honor.

XOpenDisplay now publishes the two-property handshake: an internal,
never-mapped InputOnly child of the root carries _NET_SUPPORTING_WM_CHECK
pointing at itself plus a UTF8_STRING _NET_WM_NAME, and the root points
at the same window. createInternalWindow marks it internal, which keeps
it out of CreateNotify, XQueryTree and the state snapshot, and it is
unmapped so pointer routing skips it. The root property is the only
record of it, so a second display adopts the existing window instead of
stacking up one per open. The four writes are one short-circuit chain
with the root _NET_SUPPORTING_WM_CHECK last as the commit point; any
failure destroys the check window and clears both root properties.

_NET_SUPPORTED lists only what we act on, and rather than hand-maintain
that list beside the handlers, the honored atoms are extracted into
tables the handlers themselves read: netWmStateHonored pairs each
substate with the NetWmStateSet field it sets via offsetof (replacing an
if-else chain, and letting both MAXIMIZED axes share one field), and
netWmWindowTypeHonored holds the borderless types. Advertised and
honored cannot drift.

Three atoms are deliberately absent. _NET_WM_ICON_NAME, because nothing
reads it. _NET_ACTIVE_WINDOW, because EWMH defines it as a root property
the manager keeps current as well as a ClientMessage and only the
message is handled here, so advertising it would leave a client reading
the root property (gdk_x11_screen_get_active_window, xdotool
getactivewindow) seeing no active window at all. _NET_WM_STATE_ABOVE
below SDL 2.0.16, where its only consumer is compiled out; the table
entry carries the same gate as the apply. _NET_CLOSE_WINDOW has no
property side, so it stays.
EWMH has clients set the type before the map request, which is the order
GTK, Qt and Motif all use. The SDL window does not exist yet at that
point, so XChangeProperty handler bailed on IS_MAPPED_TOP_LEVEL_WINDOW
and nothing replayed the property afterwards: realizeTopLevelWindow
derives borderless only from overrideRedirect, and
replayDeferredWmProperties covered _MOTIF_WM_HINTS, WM_NORMAL_HINTS,
_NET_WM_STATE and WM_TRANSIENT_FOR but not this one. A splash or menu
that set its type the standard way came up fully decorated, while the
root _NET_SUPPORTED told the client the type was honored, so the client
had no reason to fall back to _MOTIF_WM_HINTS or override-redirect.

Move the decode into applyNetWmWindowTypeFromProperty next to the other
appliers and call it from both the property write and the realize
replay, so the write order stops mattering. The write handler now just
calls it, which also drops the duplicated mapped-state check and the
open-coded data cast.

Scope: only the eight honored types change behavior, and only for
windows that previously kept a border they asked not to have. A window
whose type is NORMAL, DIALOG, UTILITY, TOOLBAR or DESKTOP decodes as
not-borderless and is untouched, so ordinary top-levels are unaffected.
@jserv
jserv merged commit ed80598 into main Aug 10, 2026
56 checks passed
@jserv
jserv deleted the wm-net branch August 10, 2026 04:08
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.

1 participant