X11 platform internals

CNA snapshot 009d40f5  ·  Development › Platform internals  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. The CTest entries and CI jobs named here were read, not run. Behaviour on a live X server matrix (real window managers, physical GPUs and GLX drivers, input-method engines, multi-monitor layouts and hot-plug) is not established by this page.

modules/platform/src/X11 is a native Xlib implementation of CNA::Platform::IPlatform, not SDL's X11 video driver. Windows come from XCreateWindow, events from XPending/XNextEvent, physical keys from XKB key names, text from XIM, monitors from XRandR, OpenGL from GLX loaded at run time, Vulkan from VK_KHR_xlib_surface, and on Linux controllers from the kernel's evdev nodes. This page is for the maintainer who has to change that code: it traces the connection and service lifetimes, window creation, the event multiplexer, the input snapshots, the three graphics seams and the test entries, and says where each failure mode lives.

ℹ

The user-facing view of this backend (what it needs, what it can do, how to build it SDL-free) is in Native Platforms: Native X11, the seven-platform capability matrix in Platform Support: capability matrix, and a step-by-step build in Tutorial 135. This page does not repeat those tables; it explains the code behind them. The contract every backend implements is described in Platform architecture, and the other backends are compared in Platform backends.

A stable capability snapshot requires an early connection

X11Platform (X11Platform.hpp) opens its X connection in the constructor, not in AcquireSubsystem(Video). Half of its capabilities are answers about one particular X server (does the window manager advertise EWMH fullscreen, is XRandR 1.2 there, does GLX 1.3 answer), and the contract says a capability set is read once and cached by its owners (Game copies it in its constructor, see Game.cpp). A set that changed when video was acquired would silently invalidate every cached copy. The constructor in X11Platform.cpp therefore runs in this order:

X11Platform::X11Platform()
  1. DesktopPortal::Connect()            (only with CNA_PLATFORM_HAVE_DBUS; session bus, not X)
  2. systemInfo_ = LinuxSystemInfo       (with evdev; StandardSystemInfo otherwise)
  3. OpenConnection()                    X11Connection: XOpenDisplay($DISPLAY)
       → X11ErrorPolicy::Register, atoms (one XInternAtoms), XKB / XI2 / RandR / Xwayland probes
       → keyboard, mouse, touch, XIM text input, CLIPBOARD + PRIMARY selections,
         drag-and-drop, dialogs, tray (if a tray owns _NET_SYSTEM_TRAY_Sn),
         XI2 input devices (if XI2), displays, GLX context service, Vulkan surface service
     a PlatformException here is caught and stored in connectionError_, never thrown
  4. controllers_ (EvdevControllerHub)   only if /dev/input is a directory; nothing opened yet
  5. capabilities_ = ComputeCapabilities()   frozen for the instance's lifetime

Game → AcquireSubsystem(Video)   throws the saved connectionError_ if step 3 failed
     → CreateWindow(...) → PollEvents() every frame
~X11Platform → DetachHost() on registered windows → CloseConnection() → controllers_.reset()

A missing DISPLAY or an unreachable server is recorded, not thrown: a process with no display still constructs a platform to reach StorageDevice and TitleContainer through the portable filesystem (StandardFileSystem rooted as cna-x11) and system-information services. With no connection every display-dependent capability reads false and each corresponding accessor returns null. AcquireSubsystem(PlatformSubsystem::Video) is then the call that reports the original error text (for example "no X server: the DISPLAY environment variable is not set"). Every other subsystem is pure reference counting, except that the first Gamepad acquisition starts the evdev hub. Releasing Video to zero deliberately does not close the connection: the connection is the instance's own resource, opened in the constructor and closed in the destructor, because closing it would flip every display-dependent capability to false mid-life. An unpaired release is a no-op, as the contract requires. By contrast, SDL3 acquires its video subsystem lazily and balances host-owned SDL references (SDL3 platform internals); the two implement the same IPlatform API under different native lifetime constraints.

Where each capability comes from is visible in X11Platform::ComputeCapabilities. The code sets a flag only in the same place that wires its accessor, so a maintainer adding a service must change both together:

CapabilityWhat sets it at this snapshot
gamepad, joystick, gamepadRumble, gamepadSensors, hapticscontrollers_ != nullptr: built with evdev (linux/input.h found) and /dev/input exists. Independent of the X connection.
powerInfoCompiled with evdev (LinuxSystemInfo reads sysfs). Also independent of X.
multipleWindows, nativeWindowHandle, surfacePresentation, textInput, exactKeyboardState, pixelAccurateMouse, cursorShapes, globalPointer, clipboard, clipboardData, dragAndDrop, primarySelectionA connection exists.
highDpiAlways false: one coordinate space.
multipleDisplaysX11Connection::HasRandr() (RandR 1.2 or newer negotiated).
borderlessFullscreenThe window manager listed _NET_WM_STATE_FULLSCREEN in _NET_SUPPORTED when the platform was built.
openGlContextX11GlContext::IsAvailable(): GLX headers at configure time, libGLX.so.0 or libGL.so.1 opened at run time, and the server answers GLX 1.3 or newer.
vulkanSurfaceA connection exists (the service is always constructed; see the graphics section).
relativeMouseX11Mouse::HasRawMotion() (XInput2).
imeX11TextInput::HasCompositionEvents(): on-the-spot composition was asked for and negotiated.
inputDeviceEnumerationXInput2 opcode present.
messageBox / nativeFileDialogDialogs service exists / and a desktop portal was found on the session bus.
trayA client owned _NET_SYSTEM_TRAY_S<screen> when the platform was built.
sensors, camera, managedEntrypointAlways false; GetSensors() and GetCamera() return null.

Teardown order

The connection must outlive every Xlib-backed service. X11Platform::CloseConnection first offers the owned CLIPBOARD contents to a clipboard manager (X11Clipboard::HandOverToClipboardManager; X has no clipboard storage, so without this a copy dies with the process), then destroys the services in reverse construction order: Vulkan surface, GLX context service (GLX contexts must be destroyed while their display is alive), displays, tray, dialogs, input devices, drag-and-drop, PRIMARY, CLIPBOARD (each owns a window on the connection), XIM text input, touch, mouse, keyboard, and only then the X11Connection. The connection's own destructor (X11Display.cpp) first destroys the mode switcher, which gives back every display mode exclusive fullscreen still holds, then calls XCloseDisplay, and only after that unregisters from the error policy: closing flushes and can deliver a late protocol error, which must still reach CNA's non-fatal handler rather than Xlib's default one that calls exit().

The platform destructor calls X11Window::DetachHost() on every window still in its registry before any of that, so a window destroyed later cannot call back into a dead X11Platform. That is defensive, not permission to let Game windows outlive their platform. Linux evdev controller nodes are closed last and separately (closing a node also stops any rumble still playing). Conversely, X11Window::~X11Window unregisters from the platform first, then gives back an exclusive display mode, destroys its XIM input context before the window (an XIC holds a reference to the window inside the input method), calls XDestroyWindow only if it owns the window, and frees its own colormap last. The shutdown sequence on the runtime side is in Ownership and shutdown.

Process-global state the backend touches

  • X error handler. X11ErrorPolicy (X11Error.cpp) installs one handler on the first registered connection and restores the previous one when the last CNA connection unregisters. Errors on a display CNA did not open are forwarded unchanged to the host's handler; untrapped errors on CNA's own connections are printed to stderr and swallowed instead of reaching Xlib's default exit(). XSetIOErrorHandler is deliberately left alone. The policy's state is allocated once and never destroyed, because the ambient default platform created by CurrentPlatform.cpp is itself never destroyed at process exit and may unregister after ordinary statics are gone. X11ErrorTrap brackets a request, calls XSync and turns an asynchronous error into a synchronous answer; traps nest and are tracked per thread.
  • Threads. XInitThreads() is not called: it must run before any other Xlib call in the process, and CNA does not own process startup. A host that wants process-wide Xlib thread safety calls it before constructing the platform. The class comment in X11Platform.hpp says every Xlib call "goes through one per-instance mutex", but no such lock exists in src/X11 at this snapshot (the only mutex there guards the error policy's list of owned displays). The effective rule is the contract's: poll events and call the platform from one thread, once per frame. Message boxes and the tray open connections of their own for that reason.
  • Locale. Opening the input method calls setlocale(LC_CTYPE, "") only if LC_CTYPE is still "C" or "POSIX", so XIM can deliver multi-byte text. No other category is touched and the change is not reverted; a host that set its own LC_CTYPE keeps it.
  • Nothing else. No signal handler and no environment variable are set in the game process. The exclusive-fullscreen mode guardian is a separate forked process (see window creation).

Creation is a visual and window-manager transaction

X11Platform::CreateWindow requires a connection and a positive size, then:

  1. Chooses the visual before the window exists. An X window's visual is fixed for its lifetime, which is why WindowDescription carries renderIntent and openGlFramebuffer rather than a post-creation setter. For WindowRenderIntent::OpenGl, a server without GLX 1.3 is refused with PlatformNotSupportedException(OpenGlContext), and X11GlContext::ChooseVisual searches for a GLX framebuffer configuration. Every other intent uses the screen's default visual.
  2. Creates a colormap and an explicit border pixel when the visual is not the default one. Without both, XCreateWindow fails with BadMatch across depth differences.
  3. Creates the window inside an X11ErrorTrap and syncs, so an asynchronous protocol error becomes a local PlatformException with a description, instead of a failure that shows up much later without an explanation.
  4. Wraps the XID in an owning X11Window with a separate CNA WindowId, sets WM_DELETE_WINDOW (so the close button produces a message the application can answer instead of the window manager killing the connection and every other window with it), _NET_WM_PID, WM_CLIENT_MACHINE and WM_CLASS (class CNA, instance = title), the title, the size hints and, for a borderless window, the Motif decoration hint.
  5. Registers it in windows_ and with the mouse, text input, GLX and Vulkan services, attaches an XIM input context, drag-and-drop (XDND) and touch selection, and seeds the window's content scale from the display it is on.
  6. Maps it (if visible), then applies fullscreen. A window manager only starts managing a window at MapRequest, so a _NET_WM_STATE message sent earlier has no receiver; SetNetWmState writes the property directly while the window has never been asked to map.

Adoption. AdoptWindow(WindowId) returns a BorrowedWindow that forwards every call to the live owning wrapper, so the two views cannot disagree about cached state such as focus or fullscreen mode. AdoptWindowHandle(uintptr_t) interprets the token as an XID. If the XID belongs to a registered window, the result is again a BorrowedWindow. Otherwise the XID is validated on this display with XGetWindowAttributes under an error trap and wrapped in a non-owning X11Window. That wrapper is deliberately not added to the registry the event pump resolves against, and CNA selects no input on it, so a foreign window yields a native handle for rendering but no CNA events. The XID is an integer resource identifier, and GetNativeHandle puts it in NativeWindowHandle::windowId with the Display* in display, never in a pointer field.

Geometry and scale. X11 has one coordinate space. GetPixelSize equals the client size, GetDisplayScale always returns 1, and highDpi is false. The session's scale (Xft.dpi from RESOURCE_MANAGER, the XSETTINGS manager's Gdk/WindowScalingFactor or Xft/DPI, GDK_SCALE, KDE's per-screen QT_SCREEN_SCALE_FACTORS, read in SDL3's order by X11ContentScale) is a content scale for sizing an interface, reported per display, and a change emits WindowEventKind::DisplayScaleChanged. Do not copy SDL3's pixel-density assumptions into this backend. GetClientBounds translates the client origin to root coordinates, because a reparenting window manager puts the window inside a decoration frame. Sync() calls XSync and, if a SetSize is pending, polls the geometry for up to 500 ms: under a window manager a resize is a redirected request that the window manager may apply later or refuse, so XSync alone would read the old size. The pending size is cleared either way, so a refusal does not stall every later Sync. This polls instead of waiting for ConfigureNotify, so that event still reaches PollEvents.

Fullscreen. Both borderless and exclusive fullscreen are requests to the window manager, so X11Window::SetFullscreenMode refuses both with PlatformNotSupportedException(BorderlessFullscreen) when _NET_WM_STATE_FULLSCREEN is not advertised, and the message tells a missing window manager apart from one without the hint. Exclusive mode additionally asks X11ModeSwitcher (X11ModeSwitch.cpp) for a RandR mode, choosing it by SDL's rule (smallest mode that fits, closest aspect ratio, refresh rate closest to the desktop's). If no mode fits, the window stays fullscreen on the desktop mode and reports BorderlessFullscreen. The switcher starts an X11ModeGuardian (X11ModeGuardian.cpp): a forked, never-exec'd process that watches a socket pair and, if the game dies in any way (including SIGKILL or the OOM killer), restores the CRTC over a raw connection of its own using only async-signal-safe calls, but only if the CRTC is still in the mode the game set. On focus loss an exclusive window iconifies and gives the mode back, and takes it again when focus returns. A focus loss within 400 ms of a mode change is held back (some window managers move focus while they re-lay out the screen) and decided by X11Window::CheckPendingFocusLoss from the event pump. Under Xwayland the mode is only emulated inside the game's own window, so a focus loss gives nothing back there.

Polling is an ordered protocol multiplexer

X11Platform::PollEvents clears the caller's vector and runs the following sequence, once per frame:

PollEvents(destination)
  ├─ PollXEvents                    (only with a connection)
  │    while XPending(display) > 0:     XPending flushes and counts, so XNextEvent never blocks
  │      XNextEvent
  │      XFilterEvent  ── key events offered only if text input is active for their window;
  │                       everything else (the IM's own traffic) always offered
  │      TakeEditingEvents  → composition events, in order with the rest
  │      if filtered: continue
  │      CLIPBOARD.HandleEvent / PRIMARY.HandleEvent   (both see every event)
  │      if a selection consumed it: continue
  │      TranslateEvent  → zero or more PlatformEvents
  │      TakeEditingEvents
  │    mouse.RefreshRelativeMode(); touch.TakePendingEvents(); window.CheckPendingFocusLoss()
  ├─ evdev hub Pump + TakeEvents    (only once the Gamepad subsystem was started)
  ├─ desktop portal Pump            (file-dialog answers and their callbacks)
  └─ tray Pump                      (tray clicks and menu callbacks, own connection)

An X event is not immediately a game event. XIM may consume the key presses that make up a composition, and delivering them as keys too would type the composition twice. Key events are offered to XFilterEvent only while text input is started for their window: an input-method server such as ibus processes every key of a window with a context, focused or not, and would otherwise turn a game's WASD keys into Hangul or Japanese composition. The selection handlers may consume ownership-transfer traffic (SelectionRequest, INCR chunks). Only then does X11Platform::TranslateEvent run, in this order:

X inputHandling in TranslateEvent
XInput2 generic eventXI_RawMotion: walks the valuator bitmask to find axes 0 and 1 (the values are packed, and indexing them as if every axis were present reads the wrong axis whenever only one moved) and adds them to the mouse's relative delta. Touch and pen events go to X11Touch; XI_HierarchyChanged refreshes pens and emits device events. Consumed.
RandR RRScreenChangeNotifyXRRUpdateConfiguration, display cache invalidated, one DisplayChanged per window. Consumed.
Content-scale sourcesRESOURCE_MANAGER or XSETTINGS change: DisplayScaleChanged for windows whose scale changed. Not consumed.
XKB state, map or new-keyboard notifyRebuilds the keyboard mapping only when the layout group or keymap really changed. Consumed.
MappingNotifyCore keymap refresh (xmodmap, or a server without XKB). Consumed.
Expose (count 0), ConfigureNotifyExposed; on a size change both Resized and PixelSizeChanged (identical on X11, but a renderer listens for the latter); Moved only from the window manager's synthetic configure, the only one with root-relative coordinates.
MapNotify, UnmapNotify, PropertyNotify on _NET_WM_STATE/WM_STATEMinimized/Maximized/Restored, derived from the difference against the last observed state so that one user action produces exactly one event. A root _NET_SUPPORTED change re-reads the window manager's hints.
FocusIn/FocusOutGrab-related and inferior focus changes are ignored. Otherwise: exclusive-mode handling, relative-mode grab, XIC focus, and on loss X11Keyboard::ReleaseAllKeys() so no key sticks down.
ClientMessageXDND messages go to X11DragAndDrop (DropEvent). WM_DELETE_WINDOW becomes CloseRequested, and a QuitEvent follows only if this is the last registered window. The window is not destroyed.
DestroyNotifyX11Window::MarkDestroyedByServer, so its destructor does not destroy a released XID, then the window is dropped from the registry and services.
KeyPress/KeyReleaseKeycode 0 is an input-method commit, delivered as text only. Without detectable auto-repeat, a release immediately followed by a matching press is collapsed into one repeat. Otherwise a KeyEvent, plus TextInputEvent from Xutf8LookupString while text input is active.
ButtonPress/ButtonRelease, MotionNotify, EnterNotify/LeaveNotifyButtons 4 to 7 become MouseWheelEvent (press only); buttons 8 and higher are renumbered down. Double clicks are counted by the backend (500 ms, 4 px), because X has no double-click concept. Enter and leave only move the mouse snapshot's reference window and reset the motion baseline.

This ordering prevents both duplicate IME text and lost selection requests. A "no input after fullscreen" bug therefore belongs in this event, filter and focus chain before the public Keyboard::GetState facade is changed.

What the Game does with the batch. Game::PollEvents passes every event to PlatformInputBridge::ProcessEvent before its own handling, so events after a quit in the same batch still reach input state. It calls Exit() on CloseRequested itself (the backend's conditional QuitEvent is not relied on), and invalidates the renderer surface on Resized, PixelSizeChanged, DisplayScaleChanged, FocusGained, Exposed, Minimized, Maximized, Restored and DisplayChanged. At the end of the same function it calls Update() on the keyboard and mouse services and, once the Gamepad subsystem is initialized, on gamepad and joystick. There is no separate Game::UpdateInput at this snapshot, although a comment in X11Platform.cpp still names one. The full event-to-public-state trace is in Input internals, and the frame order in One frame source trace.

Snapshots, optional devices and thread context

Keyboard. X11Keyboard (X11Keyboard.cpp) owns two different mappings. keycode → Scancode is derived from XKB key names (AD01 is the physical position of US Q on every ruleset), so it does not move when the user switches to AZERTY. keycode → KeyCode is re-read on every layout change using SDL3's rules: a non-Latin layout gets the US meaning for character keys so WASD bindings still work, and a layout whose number row types digits only when shifted (AZERTY, Czech QWERTZ) still reports D1 to D0. The shared XKB tables are in src/Xkb. Update() does not accumulate events: it reads the server's key vector with XQueryKeymap and the effective modifiers with XkbGetState, so the snapshot is correct after missed events. The event path tracks held keys only to flag repeats. XKB detectable auto-repeat is requested per connection; if the server refuses, the release/press pairs it sends are coalesced by looking one event ahead, never by a timeout. Key releases are real, which is why exactKeyboardState is true.

Mouse. X11Mouse (X11Mouse.cpp) answers level queries from XQueryPointer relative to the window the snapshot refers to. Buttons 1 to 3 come from the server's pointer mask. The extra buttons X1 and X2 come from tracked press and release events, because core mask bits 4 and 5 are the wheel. Relative mode selects XI_RawMotion on the root (deltas before acceleration and clipping) and holds a confining grab, but only while the window has focus, or is viewable when no window manager exists. Raw motion is selected only while the grab is held, because an ungrabbed XI2 client receives raw motion from the whole desktop. The state is refreshed on focus, map and unmap, and after every pump. Without XInput2, SetRelativeMode refuses rather than imitating relative mode with pointer warps. Custom ARGB cursors need libXcursor at build time.

Text input. X11TextInput is a separate XIM service with per-window active state, not an alias for key presses. Committed text comes from Xutf8LookupString on a per-window XIC. Composition events (ime true) exist only when the application sets CNA_IME_IMPLEMENTED_UI=composition (read once at platform creation) and the input method accepts the on-the-spot style. The candidate list is never delivered, because XIM has no protocol for it. With no input-method server, XOpenIM fails and the backend falls back to XLookupString (Latin-1, no dead keys), with textInput still true.

Touch and pens. X11Touch selects XInput 2.2 touch events on every window CNA created and turns each contact into TouchEvent. The contact the server marks as pointer-emulating also drives the mouse, because a window that selects touch no longer receives the emulated pointer events. Pens are slave pointers with an "Abs Pressure" valuator and count as touches while the tip is down. When a window disappears in the middle of a touch, its contacts are cancelled on the next pump.

Controllers. X has no gamepad API. With evdev compiled in (Linux, linux/input.h) and /dev/input present, the constructor builds an EvdevControllerHub (EvdevControllers.hpp) but opens nothing. The first GetGamepad(), GetJoystick() or input-device enumeration calls EnsureControllerSubsystem, which acquires Gamepad (starting the hub) and runs one immediate update. Hot-plug is one inotify watch on the input directory, with no libudev, and everything runs on the pumping thread. A node without read permission means "no device", which is a normal answer. Releasing Gamepad to zero stops the hub and clears the snapshots and rumble without changing the frozen capability set. Releasing Haptic closes the force-feedback devices. The same nodes provide rumble, gamepad motion sensors and haptics, so controllers work even in a process that could not reach an X server. GetSensors() and GetCamera() are null here; service presence is never inferred from another platform's features.

Thread context. None of these services locks. The event pump, the snapshot updates and the service calls are serial game-loop operations on the thread that owns the platform. X11Dialogs draws a message box on a connection of its own and blocks the calling thread until it is answered, and X11Tray reads its own connection from PollEvents. Neither touches the game's event queue. The cross-module thread map is in Thread and callback map.

GLX, Vulkan and CPU presentation are different seams

GLX. X11GlContext (X11GraphicsServices.cpp) implements IPlatformGlContext for the GL-family renderers (EasyGL, OpenGL4). Its entry points are resolved with dlopen("libGLX.so.0"), falling back to libGL.so.1, rather than linked, so a HEADLESS or SOFTWARE X11 build does not need a GL implementation installed to start. The GLX headers are still needed at configure time for the service to be compiled in (CNA_X11_HAVE_GLX). ChooseVisual defaults to what an XNA back buffer needs (double-buffered, 24-bit depth, 8-bit stencil) and degrades step by step only where the server offers less. It never lowers a value the caller stated explicitly, and it drops multisampling before it drops the depth buffer. The chosen GLXFBConfig travels with the window, because the context can only be made current on a window whose visual matches the context's config. The service creates contexts, makes them current, swaps buffers and returns proc addresses. SetSwapInterval tries GLX_EXT_swap_control (negative intervals need GLX_EXT_swap_control_tear), then the MESA and SGI extensions, matching extension names as whole tokens. GLX availability depends on the actual server, so the existence of this source file is not a capability.

Vulkan. X11VulkanSurface names VK_KHR_surface and VK_KHR_xlib_surface as the required instance extensions. It creates the surface for a registered WindowId through vkCreateXlibSurfaceKHR, resolved through the vkGetInstanceProcAddr already present in the process, or else from libvulkan.so.1 (then libvulkan.so) (Posix/VulkanLoader.cpp). The Vulkan struct is declared locally, so this service needs no Vulkan headers. CMake still records CNA_X11_HAVE_VULKAN_HEADERS when it finds them, but no X11 source reads that definition at this snapshot, and vulkanSurface is true whenever a connection exists. A missing loader or a missing instance extension is reported when CreateSurface is called. The surface is not the window: the Vulkan renderer must destroy it with the same instance, before the platform closes the display.

CPU presentation. X11SurfacePresenter puts a finished RGBA8 SurfaceFrame into the window with XPutImage. It uses MIT-SHM (XShmPutImage) when the extension is compiled in and the server accepts the attach, and otherwise the plain path, for example over a network connection. The frame is validated, including its stride, by the shared ValidateSurfaceFrame. Scaling and conversion to the visual's own channel masks (X11PixelPacker, correct for 5-6-5 and BGRX visuals and for either server byte order) happen in one pass. SetVSync returns false. CreateSurfacePresenter rejects a window this platform did not create. At this snapshot no renderer reaches this presenter through GraphicsDevice on X11. The software renderer's descriptor sets needsSurfacePresenter, but GraphicsDevice creates a window and presenter for it only on a platform that reports surfacePresentation without nativeWindowHandle (TERMINAL on a TTY). On X11, as on the other windowing platforms, SOFTWARE stays off-screen. The presenter is exercised by the platform's own live tests and is available to direct IPlatform users.

When diagnosing "window opens but nothing draws", tell the following cases apart: no GLX visual or context (the window was created without OpenGl intent, or the server has no GLX 1.3); a Vulkan instance created without VK_KHR_xlib_surface, or no loader; an ordinary renderer frame-path problem; and "no window at all", which with SOFTWARE, PORTABLEGL, HEADLESS or STUB is by design. A platform capability says the service exists; the graphics backend still has to acquire its own context or surface and present. Changes to WindowDescription::renderIntent affect window construction before any renderer exists, so test them with at least one GL and one non-GL renderer.

Timing, errors and extension boundaries

The performance counter is Posix::MonotonicNanoseconds() (Posix/MonotonicClock.cpp) with a fixed frequency of 1,000,000,000 Hz. GetTicksMilliseconds measures from a steady_clock epoch taken at construction, and Delay is Posix::SleepMilliseconds. Fixed or variable timestep policy stays in Game, not here.

Expected failures take one of three forms. A PlatformException means the thing exists but failed (no connection when video is acquired, XCreateWindow or GLX config failure, an unknown XID). A PlatformNotSupportedException names the missing capability (OpenGL intent without GLX, fullscreen without EWMH, relative mode without XInput2, file dialogs without a portal). A false capability paired with a null accessor covers the rest. There is no SDL fallback when X11 is selected. cmake/PlatformSelection.cmake refuses CNA_PLATFORM=X11 when cmake/PlatformX11.cmake reports the backend unavailable, with the package to install. The code separately handles a successful build started without a reachable X server, by keeping the non-video services. The configure log's line CNA: X11 platform dependencies -- X11 + Xext; optional present: …; absent: … is the quickest way to see what a build will have:

Found at configure timeDefinitionWhat it enables
libX11, libXext, X11/XKBlib.h(mandatory)The backend itself; without them X11 is not offered. Also never offered for Windows, Emscripten, Android or iOS targets.
libXi, libXrandr, libXcursor, libXfixes, libXau, libXssCNA_X11_HAVE_XI … CNA_X11_HAVE_XSSRaw motion, touch and device enumeration; displays and exclusive modes; ARGB cursors; pointer hiding; the mode guardian's authorisation; per-client screen-saver suspension.
X11/extensions/XShm.hCNA_X11_HAVE_XSHMThe shared-memory presenter path.
GLX headers (FindOpenGL GLX)CNA_X11_HAVE_GLXThe GLX service (the library itself is opened at run time).
dbus-1 headersCNA_PLATFORM_HAVE_DBUSDesktop portal (file dialogs, OpenUrl) and session-bus screen-saver inhibition; libdbus is opened at run time.
linux/input.h (Linux only, checked in modules/platform/CMakeLists.txt)CNA_PLATFORM_HAVE_EVDEVControllers, rumble, gamepad sensors, haptics, sysfs power information.

Several optional boundaries deserve separate attention when modifying them. They are not one monolithic "X11 feature": XRandR for displays and modes (plus the mode guardian); XKB and XIM for keys, layouts and composition; XInput2 for raw motion, touch, pens and device enumeration; ICCCM selections for CLIPBOARD and PRIMARY, including INCR transfers in both directions and any MIME target; XDND 5 (target side) for drops; the freedesktop portal (src/Freedesktop) for file dialogs; the XEmbed system tray; MIT-SCREEN-SAVER or the session bus for keeping the screen on; and Linux evdev (src/Linux) for gamepads and haptics. Check the relevant capability and test, and keep host process state intact as described under process-global state.

Validation and reading order

The X11 suites compile only into a CNA_PLATFORM=X11 build (cmake/UnitTests.cmake filters X11*.cpp out otherwise). They are split into CTest entries by what each one needs, and they run in the binary named by CNA_PLATFORM_CTEST_BINARY (CnaTests by default, or the focused CnaPlatformModuleTests). Entries that need a server go through tools/platform/x11_test_server.sh, which starts a private Xvfb on a display number it searches for and exits 77 (CTest's skip code) where Xvfb, a window manager or ibus is missing.

CTest entrySuites (source files)Needs
CnaX11MappingTestsKey, modifier, button, focus-filter and auto-repeat tables (X11KeyboardMappingTests.cpp), X11IsSdlFree, pixel packing, exclusive-mode choice, screen plan and guardian wire format, touch math, content-scale parsing, text encoding, input-device classification, message-box geometry, portal requests, screen-saver names, XKB and evdev mapping and layoutNo display.
CnaX11EvdevTestsX11EvdevVirtualDeviceEvdev compiled in; a writable /dev/uinput and readable event nodes, otherwise each test skips.
CnaX11IntegrationTestsX11Live (X11PlatformIntegrationTests.cpp), X11ClipboardInterop, X11VulkanSurfaceTest, and the live drag-and-drop, touch-selection, content-scale, selection, input-device, message-box, portal, screen-saver and tray suitesPrivate Xvfb; some cases need a Vulkan loader, D-Bus or a second client process and skip without them.
CnaX11WindowManagerTestsX11WithWindowManagerPrivate Xvfb plus an openbox binary; the fixture starts and stops the window manager for each test.
CnaX11InputMethodTestsX11InputMethodPrivate Xvfb plus a private ibus with its XIM server.
CnaX11TouchscreenTestsX11TouchscreenCNA_X11_TEST_TOUCHSCREEN=1 (set by the entry), uinput, and a rootless Xorg with dummy and evdev drivers that takes the virtual device exclusively.
CnaX11ExclusiveFullscreenTestsX11ExclusiveFullscreenThe launcher's own server with a window manager; skips anywhere CNA_X11_PRIVATE_TEST_SERVER is not set, so it never changes a real desktop's mode.

Several suites start the test binary again as a separate X client through DISABLED_ helper cases (X11SelectionPeer, X11DragSource, X11ScreenSaverPeer, X11ExclusiveFullscreenHelper), so selections, drags and guardian recovery after SIGKILL are tested between real clients. cna_platform_x11_exit_harness (tools/platform/x11_exit_harness.cpp) covers static destruction order at process exit, which no in-process test can reach. The implementation-neutral suites in CnaPlatformTests (*PlatformConformance*, parameterised over PlatformFactory::GetAvailable(), which lists X11 in an X11 build) also apply. cna_x11_desktop_validation (tools/platform/x11_desktop_validation, scenarios in docs/testing-x11-desktop.md) is a standalone program for a real desktop, not a CTest entry. Some of its scenarios inject input or overwrite the clipboard.

In .github/workflows/platform-ci.yml, the job x11-sdl-free is configured to build CnaPlatformModuleTests with CNA_ENABLE_SDL=OFF, HEADLESS and NULL audio on Xvfb with openbox. It asserts that no SDL artifact or SDL link exists, runs the platform and all seven X11 CTest entries, and, when the runner has uinput, re-runs the uinput, D-Bus and private-server suites and fails if any case skipped. The job x11-sdl-free-gpu is configured to build cna_demo_2d and cna_house3d_demo with OPENGL33;VULKAN;SOFTWARE;HEADLESS and ALSA audio, to check that neither SDL nor libasound is a NEEDED entry, and to run the House3D smoke and exclusive-fullscreen tests and cna_demo_2d --smoke 6 under each renderer on Mesa's software drivers. That is what the workflow file configures; it is not a record of passing runs, and both jobs run on Xvfb, not on a real desktop or GPU.

The suites have different host prerequisites (X server, window manager, XI2, evdev/uinput, ibus, D-Bus, a portal), and a skipped test is not evidence. Check the selected build's test target index and the test source's skip conditions before reporting coverage. For a window or GLX fix, run a real X11 renderer smoke test in addition to the unit suites. The source and the test registrations were checked by reading them at 009d40f5; no X server-dependent suite was executed for this page. The general change-and-verify loop is in I need to modify a platform backend and What to test after changing X.

Read in this order

  1. PlatformSelection.cmake and PlatformX11.cmake: when the backend is offered, why a request is refused, and which optional definition switches on which capability.
  2. X11Platform.hpp: the owned services, the frozen capabilities_ and the stated process-state policy (and compare the threading comment with the code).
  3. X11Platform.cpp and X11Display.cpp: constructor, ComputeCapabilities, CloseConnection, CreateWindow, adoption, PollXEvents and TranslateEvent, before diving into feature helpers.
  4. X11Error.cpp: the error policy and traps every other file relies on.
  5. X11Window.cpp, X11ModeSwitch.cpp and X11ModeGuardian.cpp: window-manager requests, Sync, fullscreen and display-mode recovery.
  6. X11GraphicsServices.cpp: the three graphics routes.
  7. X11Keyboard.cpp, X11Mouse.cpp, X11TextInput.cpp and X11Clipboard.cpp: event-to-snapshot state, XIM and selections.
  8. X11PlatformIntegrationTests.cpp and X11KeyboardMappingTests.cpp: what live-host and table evidence actually assert, and the rest of the X11*Tests.cpp files for the narrower protocols.

The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.