Skip to content

Support Motif on the compat stack#6

Merged
jserv merged 2 commits into
mainfrom
motif
Jun 5, 2026
Merged

Support Motif on the compat stack#6
jserv merged 2 commits into
mainfrom
motif

Conversation

@jserv
Copy link
Copy Markdown
Contributor

@jserv jserv commented Jun 4, 2026

This adds lib{Xmu,Xpm,Xext}-compat shared libraries for the active Motif fork[1] alongside the existing libXt-compat, and rework the Xrm resource cascade, event layer, and font surface so its widgets function.

Xrm: matchEntry returns a per-component specificity vector compared with memcmp, replacing the flat-sum scoring that violated Xt precedence; XrmEnumerateDatabase honors loose-bound entries so *background-style resources surface under any prefix; parseLine and entryPatternToString decode and re-encode \n, \t, \r, \, and octal escapes so XtParseTranslationTable consumers see the byte values their grammars expect.

Events and input: convertModifierState maps KMOD_ALT to Mod1Mask and KMOD_NUM to Mod2Mask; XK_Alt_R joins XK_Alt_L on Mod1Mask in XkbKeysymToModifiers and the XGetModifierMapping table. XSetInputFocus drives keyboardFocus instead of warning; XGrabKey / XUngrabKey track passive grabs; XGrabKeyboard / XUngrabKeyboard track a modal grab; the event layer routes key events through grabs before falling back to focus. The implicit setKeyboardFocus calls in XSelectInput and XMapWindow are removed so XSetInputFocus is the sole focus authority. postEvent ClientMessage uses calloc, sets send_event = True, and carries a timestamp in data.l[1] for ICCCM compliance. getEventQueueLength lifts its peek buffer from 25 to 256 so expose and configure bursts during Motif geometry negotiation are not silently truncated.

Bounds and threading: parsePositiveInt guards INT_MAX before each XLFD digit multiply; decodeString bounds-checks every \x, \u, and trailing-backslash payload; the text-render cache wraps lookup, insert, eviction, and the dependent SDL_RenderCopy under one SDL_mutex so concurrent evictions cannot free a texture mid-draw.

XSetRGBColormaps and XGetRGBColormaps replace WARN_UNIMPLEMENTED stubs with the ICCCM 6.4 ten-CARD32 wire format (visualid, killid, colormap, red_max, red_mult, green_max, green_mult, blue_max, blue_mult, base_pixel). XmuLookupStandardColormap uses them to synthesize a 24bpp RGB cube on the root window when the requested visual is not already installed. destroyWindow loses its vestigial WARN_UNIMPLEMENTED that was firing on every Motif demo teardown.

Build glue: mk/libxpm.mk pins libXpm-3.5.19; mk/xcompat-libs.mk links the new compat libraries against libXt-compat and libX11-compat; mk/pkgconfig.mk emits x11/xt/xpm/xmu/xext .pc files so motif's autotools configure resolves the compat stack instead of host X11; mk/motif.mk clones the pinned motif fork, runs autoreconf and configure quietly (V=1 to surface details, per-stage log files redirected on failure), and stages lib{Xm,Mrm}.so plus the upstream demos. A new shared_lib_rpath_ldflags macro in mk/common.mk consolidates the Linux $ORIGIN / Darwin @rpath+@loader_path conditionals; mk/deps.mk aggregates per-fragment dep files via $(ALL_DEPS).

Tests: XSet/GetRGBColormaps round-trip exercising all ten wire fields, XkbKeysymToModifiers including XK_Alt_R on Mod1Mask, libXt resource cascade enumeration with mixed tight and loose bindings, Xmu converter address pinning so a dropped converter fails at link, plus lib{xpm,xt,xmu} and motif link-and-resource binaries. make check reports 31 ok markers in release and debug (CFLAGS_EXTRA=-DDEBUG_LIBX11_COMPAT) builds.

[1] https://github.com/thentenaar/motif


Summary by cubic

Adds Motif support to the compat stack with new Xmu/Xpm/Xext/Xinerama shims, pkg-config metadata, and a make motif build target. Implements full Shape masks with per‑pixel compositing and upgrades Xrm/XIM/events so Motif widgets link and render correctly.

  • New Features

    • Compat libs and tooling: libXmu-compat, libXpm-compat, libXext-compat, libXinerama-compat; pkg-config for x11, xt, xpm, xmu, xext, xinerama; make motif builds the pinned fork and stages libXm, libMrm, demos, and helper scripts (validate, profile, diff, screenshots).
    • Shape: bounding/clip masks with XShape* combine/query and per‑pixel compositing across all draw paths (copy, fills, lines, text, clears, XPutImage); tests cover single/intersection/combine semantics.
    • Events/input: Mod1/Mod2 mapping, passive XGrabKey and modal XGrabKeyboard, XSetInputFocus as sole focus authority, OSF keysyms, larger queue, deferred/coalesced presents and child expose propagation; per‑Display last request code with mutex‑guarded tracking for accurate XErrorEvent.
    • Xrm/XIM/Fonts: per‑component specificity and loose‑binding; escape parsing (\n, \t, \r, \\, octal); UTF‑8/XIM/XOM (Xutf8*, XSetOCValues/XGetOCValues, XOMOfOC) and text‑extent APIs; GC font ref‑counting and cache invalidation; bounds checks in XLFD/string decoders.
    • ICCCM/Colormaps and utils: XSet/GetRGBColormaps; XmuLookupStandardColormap synthesizes a 24bpp RGB cube; XReadBitmapFileData; link tests for xmu, xpm, xinerama; Motif link/resource and visible rendering tests; Motif examples (hello, simpleapp, togglebox).
    • Build/runtime: rpath on Linux ($ORIGIN) and macOS (install_name @rpath/<self> + @loader_path); SDL wrapper libs libSDL2-x11compat and libSDL2_ttf-x11compat with sanitizer‑safe loading (auto‑disable RTLD_DEEPBIND) and atomic dlsym caching; upstream headers synced to libX11 1.8.13; stub X11/SM/SM.h.
    • CI: GitHub Actions runs lint/build/debug/sanitize/motif in parallel with content‑addressed caches (upstream headers, Motif clone, per‑job ccache); the Motif job builds all demos and validates under SDL_VIDEODRIVER=dummy (Linux sets MOTIF_YACC="bison -y").
  • Migration

    • Set PKG_CONFIG_PATH=$PWD/build/pkgconfig so autotools resolve x11, xt, xpm, xmu, xext, xinerama to the compat libs.
    • Run make motif to build Motif against the compat stack; demos are staged and can be validated/profiled with the provided scripts.
    • No LD_LIBRARY_PATH needed when linking via the provided .pc files; rpath is set for Linux/macOS as above.

Written for commit 4886eae. Summary will update on new commits.

Review in cubic

@jserv jserv force-pushed the motif branch 2 times, most recently from 11529b5 to 69ae575 Compare June 4, 2026 11:24
cubic-dev-ai[bot]

This comment was marked as resolved.

@jserv
Copy link
Copy Markdown
Contributor Author

jserv commented Jun 4, 2026

Steps to build Motif demo programs:

make motif-demos

Run one of demo programs:

build/motif-demos/demos/programs/piano/piano
piano

cubic-dev-ai[bot]

This comment was marked as resolved.

@jserv jserv force-pushed the motif branch 3 times, most recently from 7ead0a6 to 74bbb8f Compare June 5, 2026 12:44
@sysprog21 sysprog21 deleted a comment from cubic-dev-ai Bot Jun 5, 2026
cubic-dev-ai[bot]

This comment was marked as resolved.

This adds lib{Xmu,Xpm,Xext}-compat shared libraries for the active Motif
fork[1] alongside the existing libXt-compat, and rework the Xrm resource
cascade, event layer, and font surface so its widgets function.

Xrm: matchEntry returns a per-component specificity vector compared with
memcmp, replacing the flat-sum scoring that violated Xt precedence;
XrmEnumerateDatabase honors loose-bound entries so *background-style
resources surface under any prefix; parseLine and entryPatternToString
decode and re-encode \n, \t, \r, \\, and octal escapes so
XtParseTranslationTable consumers see the byte values their grammars
expect.

Events and input: convertModifierState maps KMOD_ALT to Mod1Mask and
KMOD_NUM to Mod2Mask; XK_Alt_R joins XK_Alt_L on Mod1Mask in
XkbKeysymToModifiers and the XGetModifierMapping table. XSetInputFocus
drives keyboardFocus instead of warning; XGrabKey / XUngrabKey track
passive grabs; XGrabKeyboard / XUngrabKeyboard track a modal grab; the
event layer routes key events through grabs before falling back to
focus. The implicit setKeyboardFocus calls in XSelectInput and
XMapWindow are removed so XSetInputFocus is the sole focus authority.
postEvent ClientMessage uses calloc, sets send_event = True, and carries
a timestamp in data.l[1] for ICCCM compliance. getEventQueueLength lifts
its peek buffer from 25 to 256 so expose and configure bursts during
Motif geometry negotiation are not silently truncated.

Bounds and threading: parsePositiveInt guards INT_MAX before each XLFD
digit multiply; decodeString bounds-checks every \\x, \\u, and
trailing-backslash payload; the text-render cache wraps lookup, insert,
eviction, and the dependent SDL_RenderCopy under one SDL_mutex so
concurrent evictions cannot free a texture mid-draw.

XSetRGBColormaps and XGetRGBColormaps replace WARN_UNIMPLEMENTED stubs
with the ICCCM 6.4 ten-CARD32 wire format (visualid, killid, colormap,
red_max, red_mult, green_max, green_mult, blue_max, blue_mult, base_pixel).
XmuLookupStandardColormap uses them to synthesize a 24bpp RGB cube on the
root window when the requested visual is not already installed.
destroyWindow loses its vestigial WARN_UNIMPLEMENTED that was firing on
every Motif demo teardown.

The shape extension was previously a probe-only stub; this lands a
per-pixel snapshot+composite at every destination-side draw entry point
so XCopyArea, XCopyPlane, XDrawRectangle/XFillRectangles, XDrawLine(s),
XDrawSegments, XDrawPoint(s), XDrawArc(s)/XFillArc(s), XFillPolygon,
XClearArea, XPutImage, and the text renderText path all honor the
window's installed shape masks.

Shape semantics: split the single shapeMask field into shapeBoundingMask
and shapeClipMask with independent offsets, matching X's two slots. The
composite intersects them: a pixel is admitted iff every installed mask
admits it (pixelInsideShape / ShapeMaskView). XShapeQueryExtents reports
each kind separately. XShapeCombineMask's black-pixel visual indicator
paints only for ShapeBounding (clip masks don't define the window
outline). New tests cover the single-mask, intersection, and combine
semantics.

Shape integration: ShapeGuard (begin/end pair) consolidates the
snapshot+composite wrap across the draw primitives so each entry point
gets one declaration and one end call that handles all early returns.
applyShapeMaskOverDrawnRect returns Bool so callers can suppress
presentDrawableIfVisible / repaintMappedChildrenInRect when the SDL
readback or texture upload fails, keeping mask-violating output off the
visible surface. captureShapeMaskBaseline pre-clips the request to the
window bounds so pathological coordinates can't allocate huge readback
surfaces.

Overflow hardening: every bbox-arithmetic site now runs in int64 with
saturating clampToInt. CoordModePrevious accumulators in XFillPolygon
and XDrawLines clamp at INT_MIN/INT_MAX before storing into SDL_Point;
unionRect, lineDamageRect, polylineDamageRect, arcDamageRect, and the
XDrawRectangle damage+corner math all use int64 with DAMAGE_PAD_CAP on
stroke padding. XCopyPlane gained a missing BadGC check on gc and a
BadMatch return when a shape mask is installed but the destination
readback failed (avoids mixing real prior pixels with synthetic GC
background in masked-out positions). Wide-line fallback now LOGs the
silent 1-pixel downgrade when strokeLineOnRenderer / rasterStrokePath
fails so a regression in the path rasterizer isn't invisible.

Supporting infrastructure: libXinerama compat shim covering the
Xt/Motif probe surface (XineramaQueryExtension/Version/IsActive,
QueryScreens enumerates all dpy->screens with SHRT_MAX clamp).
libSDL2-x11compat / libSDL2_ttf-x11compat dlopen wrapper libraries so
the Motif demo link line doesn't double-bind real SDL2. Mapping-list
mutex with eager init from initScreenWindow on the single-threaded
XOpenDisplay path and NULL-tolerant lock/unlock wrappers so a
SDL_CreateMutex failure degrades to unsynchronized rather than crashes.
Motif build wiring propagates LIBS / iconv / runtime LD_LIBRARY_PATH
through every sub-make (config, lib/Xm, lib/Mrm, tools/wml, clients/uil,
demos).

Scripts and build artifacts: capture-motif-demo-screenshots.sh honors
MOTIF_DEMO_SCREENSHOT_RESULT_FILE env override post log_dir resolve;
compare-motif-reference.py groups by status-row comparison;
sync-upstream-headers.py switched from x.org/individual tarballs to
gitlab.freedesktop.org archives with corresponding sha256s; the new
motif-demos-check, motif-demos-screenshots, and motif-differential-tests
make targets wire up the demo validation pipeline.

ASan refuses dlopen() with RTLD_DEEPBIND because deep binding bypasses
its symbol interception; the CI sanitize job aborts at the first test
binary launch with:

  You are trying to dlopen ... with RTLD_DEEPBIND flag which is
  incompatible with sanitizer runtime

Detect __SANITIZE_ADDRESS__ / __SANITIZE_THREAD__ at compile time
(plus __has_feature() for Clang's address/memory/thread sanitizers)
and force RTLD_DEEPBIND to 0 in that case. RTLD_LOCAL was already on
both dlopen() sites, so the only thing lost is the belt-and-suspenders
protection against the wrapper resolving symbols back into itself.

Promote two unresolved review findings to real sync

The two annotations from d27706e where the prior pass left only a
comment got pushed back as still-unresolved by the PR reviewer. Land
actual synchronization for both.

src/error.c: guard the per-Display side table with an SDL_mutex,
lazily allocated on first use (NULL-tolerant lock/unlock if
SDL_CreateMutex fails). Read getLastRequestCode's byte under the lock
rather than returning the entry pointer — releaseLastRequestCode can
otherwise free the node between our unlock and the caller's deref.

src/wrapper/sdl-wrapper.c, src/wrapper/sdl-ttf-wrapper.c: load and
store the shared dlopen handle plus every per-wrapper realFunc cache
via __atomic_load_n / __atomic_store_n with ACQUIRE/RELEASE ordering.
The races were always benign (dlopen ref-counts and dlsym is
idempotent so the value written is always the same) but C's memory
model still classified the unsynchronized accesses as UB; the atomics
let the compiler reason about them as ordinary loads/stores rather
than fold the cache check into a single read.

include/X11/Xmu/Misc.h: MAXDIMENSION was computed as ((1 << 31) - 1)
which shifts into the sign bit of a signed int, which C99 6.5.7p4
leaves undefined. Replace with the equivalent hex literal 0x7FFFFFFF.
This header gets pulled in by Xmu consumers; UBSan -fsanitize=shift
would flag it at every translation unit that includes it.

mk/library.mk: the Darwin link line set the dylib install_name to
@rpath/<self> but did not add an LC_RPATH entry. A consumer linked
against libX11-compat that loads sibling compat dylibs (libXt-compat,
libXpm-compat, etc.) via the same @rpath relies on the loader having
some @rpath registered. Add -Wl,-rpath,@loader_path so the dylib
resolves its siblings relative to its own directory without forcing
the consumer to bake an absolute rpath in.

Verified via otool -l on the rebuilt libX11-compat.so:
    cmd LC_RPATH
    path @loader_path
This rewrites the workflow to fan out into five parallel top-level jobs
and add long-lived caches so CI doesn't redo deterministic work.

Topology. Previously lint / build / sanitize ran in parallel and the
build job did a release pass plus a make clean + DEBUG_LIBX11_COMPAT
rebuild serially. Split debug into its own debug-build job so the
release and debug paths run concurrently. Add a motif job that builds
the thentenaar/motif libXm and libMrm against the compat stack, builds
every Motif demo, and runs scripts/validate-motif-demos.sh under
SDL_VIDEODRIVER=dummy. This is the local half of the motif-differential
make target; the remote ssh comparison half needs a separate physical
host and isn't reproducible in a stock GitHub runner.

Caching. Three layers, all keyed so cache content can never substitute
for differently-flagged content from another job:
  * upstream-src (shared across build / debug-build / sanitize / motif)
    caches the deterministic outputs of sync-upstream-headers.py: the
    tarball download cache, extracted X11 headers, and extracted
    upstream .c source slices. Excludes build/upstream/**/*.o and *.d
    so CFLAGS-sensitive objects from $(OUT)/upstream/src/%.o rules
    don't cross-contaminate release vs debug vs sanitizer builds.
    Also excludes build/upstream/motif-src which has its own cache.
  * motif-src (motif job only) caches the Motif git clone and the
    autoreconf output. Exact-match key on mk/motif.mk + patches with
    no restore-keys fallback: the .source-stamp / .autogen-stamp files
    inside motif-src don't depend on those inputs in the makefile, so
    a stale prefix restore would let make skip a fresh clone+patch+
    autoreconf after inputs changed.
  * ccache per-job (build / debug-build / sanitize / motif) with
    distinct keys so the sanitizer-instrumented objects, debug-flag
    objects, and release objects each get their own pool. CC=ccache
    clang is exported via $GITHUB_ENV; mk/toolchain.mk's
    "ifeq ($(origin CC),default)" check correctly treats the
    env-supplied value as authoritative.

Concurrency. cancel-in-progress fires only when github.ref is not
main, so PR pushes still cancel stale runs but main commits get a
complete CI record and cache-save pass.

Motif autotools. Ubuntu's bison package ships /usr/bin/bison but no
/usr/bin/yacc. mk/motif.mk defaults MOTIF_YACC to yacc on Linux;
set MOTIF_YACC=bison -y at job env so the Mrm parser generation
works without adding byacc as a build dep. The configure script
bakes the resolved YACC value into the generated Makefile so the
recursive submakes pick it up.
@jserv jserv merged commit f7b7f90 into main Jun 5, 2026
5 checks passed
@jserv jserv deleted the motif branch June 5, 2026 14:24
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