Skip to content

Add an XCB compatibility layer - #83

Merged
jserv merged 6 commits into
mainfrom
xcb
Aug 31, 2026
Merged

jserv merged 6 commits into
mainfrom
xcb

Conversation

@jserv

@jserv jserv commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Adds an XCB compatibility layer over the in-process Xlib shim, so an XCB
client runs against libx11-compat with no X server present.

The layer is opt-in. XCB=1 builds libxcb-compat.so, the
libX11-xcb-compat.so bridge, their tests, pkg-config files and example
clients; a default build produces none of them. The CI jobs pass XCB=1,
so the tests and the 143-symbol ABI gate run on every push.

Shared code

Three gaps were closed in src/ rather than worked around in the shim,
so Xlib callers get the same fixes: XCreateWindow could not be handed a
caller-chosen resource id, which is how every XCB client names its
windows; restack honored only Above and Below, silently rejecting TopIf,
BottomIf and Opposite; and XGetWindowAttributes left border width,
class, gravities and screen unset while configure ignored CWBorderWidth.

The XID table moved from a growing array to a chunked table under a
mutex. xcb_generate_id is callable from any thread, and the old array
handed out interior pointers a concurrent realloc could invalidate. The
advertised base and mask now come from one definition shared by the table
and the setup record, so a client deriving ids as base | (n & mask)
cannot collide with the allocator.

Known gaps

  • check-xcb-reference compiles its probe against system XCB and diffs
    it against a file that same binary produced, so it asserts nothing
    about this shim yet.
  • PutImage and GetImage accept ZPixmap only; XYBitmap and XYPixmap answer
    BadValue rather than guessing at a conversion.
  • Graphics contexts keep display, owner and depth in a side table the
    shim owns, because the shared XID table records none of them. The
    lookup is linear under one mutex.
  • XCB=0 is a packaging gate, not a compile-out: the core library still
    carries the caller-chosen-id paths and the hook setters.

Close #81

cubic-dev-ai[bot]

This comment was marked as resolved.

XCB clients compile against exact structure layouts, enum values and
inline accessors, so the public headers cannot be hand-written
approximations without silently breaking callers. They also cannot come
from libxcb's release tarball: xproto.h is generated from xcb-proto's XML
during that build, so only a binary package carries the generated result.

Extend scripts/sync-upstream-headers.py to stage them into
build/upstream/include/xcb/ alongside the Xorg headers, from a pinned
libxcb1-dev 1.15-1ubuntu2 whose archive digest and per-header digests are
both verified. Tracking 13k lines of generated upstream code instead
would put it through this repository's formatting and whitespace gates,
which upstream style does not satisfy, and would carry it in every clone
forever.

A .deb is an ar archive, so the script reads its data.tar member directly
rather than shelling out to dpkg-deb, which exists only on Debian
derivatives and would make the fetch fail on the macOS development host.
@jserv
jserv force-pushed the xcb branch 3 times, most recently from a2b8da6 to 7209bde Compare August 31, 2026 15:06
jserv added 4 commits August 31, 2026 23:24
An XCB client needs a connection handle, the server setup record, XID
allocation and an event queue before a single request can be expressed,
and none of those parts can be exercised without the others. An
unchecked request reports its protocol error through the event queue,
and the queue only changes hands through XSetEventQueueOwner, so
splitting this into three commits would produce two that cannot be
tested.

The connection is a thin handle over an existing Display rather than a
transport, so XGetXCBConnection returns the connection already bound to
that display and both APIs observe one event source. Ownership follows
libX11-xcb: after XSetEventQueueOwner(XCBOwnsEventQueue), the Xlib
dequeue paths stop consuming. Hooks let the connection observe display
close and cancelled event waits without src/ ever depending on the XCB
layer, which keeps the core library free of a reverse dependency on a
sibling shim.

xcb_get_file_descriptor returns -1 because there is no socket to hand
out. Clients that poll a connection FD have no equivalent here and must
use the wait and poll entry points instead.

XIDs move from a growing array to a chunked table under a mutex.
xcb_generate_id is callable from any thread, and the old array handed
out interior pointers that a concurrent realloc could invalidate. The
chunked form keeps allocated slots pinned and lets a client reserve an
id it chose itself, which the protocol expects: a client derives ids as
base | (n & mask), so the advertised base and mask must not share a bit
and the whole range has to fit the protocol's 29 bits. Both now come
from one definition that the table and the setup record share.

Installing xcb.pc and x11-xcb.pc drags x11.pc along, because x11-xcb.pc
says "Requires: x11 xcb" and pkg-config would otherwise resolve x11
against the host's real libX11 and link it beside the shim. The
generated files now spell their Libs and Cflags with the ${libdir} and
${includedir} variables instead of absolute build paths, so install can
relocate them by rewriting the variable lines; pkg-config expands them,
so an in-tree consumer reads exactly the flags it read before.

The layer is opt-in through XCB=1. No in-tree workload needs it yet, and
a default build should not grow two libraries, two pkg-config files and
an installed include/xcb that nothing asks for. The CI jobs pass XCB=1,
so the tests and the ABI gate still run on every push.

No core request is advertised yet, so a client linking against an
unimplemented opcode fails at link time instead of receiving a cookie
that silently never completes.
Translate the window, property and selection opcodes (1-4, 7-8, 10, 12,
14-24) onto the existing Xlib implementation, so an XCB client can
create and configure windows, read geometry and the window tree, and
exchange properties and selections.

Each request validates its arguments before touching shared state,
because the protocol answer for a bad request is a specific error code,
not whatever the Xlib path happens to do with an out-of-range value. A
checked request stores its error for xcb_request_check; an unchecked
one queues the error for the event loop, which is where an XCB client
expects to find it. A request that reserved the client's chosen id and
then failed validation gives the id back, so a client can retry with it.

Three gaps in the shared code had to be closed rather than worked around
in the shim, so Xlib callers get the same fixes: XCreateWindow could not
be handed a caller-chosen resource id, which is how every XCB client
names its windows; restack honored only Above and Below, leaving TopIf,
BottomIf and Opposite silently rejected; and XGetWindowAttributes left
border width, class, gravities and screen unset while configure ignored
CWBorderWidth.

Drawing, pixmap and graphics-context requests are still deferred and
remain absent from the export manifest.
Translate the remaining core rendering opcodes (53-57, 60-68, 72-73,
76-77) so an XCB client can allocate pixmaps and graphics contexts,
draw points, lines, segments, rectangles, arcs and text, copy between
drawables, and move images in and out.

Drawing is where a wrong argument corrupts output instead of failing
loudly, so value masks, coordinate modes and image geometry are
validated up front and answered with the protocol error the client
expects. Depth compatibility is checked against the drawable rather
than assumed: a GC or copy spanning mismatched depths is BadMatch, not
a silently wrong blit. GetImage rejects a rectangle that leaves the
drawable instead of returning a reply padded with invented pixels, and
CopyPlane rejects a plane the source depth does not contain.

Graphics contexts need a display, owner and depth that the shared XID
table does not record, so the shim keeps that state alongside the id
and releases it when the owning connection disconnects. This leaves the
GC lookup linear and the state duplicated; folding it into the shared
table means changing what src/gc.c stores per XID, which is a larger
change than this translation layer should carry.

XCreatePixmap and XCreateGC gain caller-chosen id variants, matching
the window change in the previous commit, because an XCB client names
every resource itself.
The XCB tests drive the shim through assertions, which proves the
return values but never renders a frame. These two programs are XCB
clients with no Xlib calls at all, so they exercise the paths a real
client takes: connection setup, a mapped window, a graphics context,
image upload and the primitive calls, ending on the event loop.

They build with the other clients under make examples rather than a
target of their own, since XCB=1 already decides whether they exist and
a second entry point only gives the same set two names.

Both accept --smoke for a headless run under SDL's dummy video driver,
so make check-xcb-showcases keeps them building and running rather than
rotting as documentation. Smoke mode drains the event queue before
exiting, because the drawing requests are unchecked and a run that
failed every one of them would otherwise still exit successfully.
cubic-dev-ai[bot]

This comment was marked as resolved.

Three WebKitGTK paint jobs failed once each on a single pull request and
passed on re-run with no code change: two with "driver request timeout"
and one with a blank first frame. The harness had no tolerance for a
loaded runner. Every driver request carried a hardcoded five second
budget, and a run that never got a frame failed outright.

Give the per-request budget its own knob at fifteen seconds, and retry a
failed attempt once. Only a transient outcome is retried: losing the
driver, or never painting at all, says nothing about the build. A run
that painted and then failed an input, layout or visual assertion is a
real result and is reported on the first attempt, so this cannot mask a
regression.

The four app recipes ran the same node smoke plus paint check with
slightly different wrappers, and had drifted: xnedit skipped the paint
check silently when node was missing rather than honoring
WASM_PAINT_REQUIRED, so a runner without node passed it while the other
three failed. One macro now covers all four, and xnedit gains the gate.
@jserv
jserv merged commit 26d5a7f into main Aug 31, 2026
56 checks passed
@jserv
jserv deleted the xcb branch August 31, 2026 16:22
@hardBSDk

hardBSDk commented Sep 1, 2026

Copy link
Copy Markdown

Wonderful work, thanks a lot!!

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.

(Feature request) XCB support to become a XWayland alternative with limitations

2 participants