Tutorial 135: A Native X11 Build
What you’ll learn: how to configure CNA with CNA_PLATFORM=X11 and CNA_ENABLE_SDL=OFF, build the demos, prove the binary contains no SDL, run them under a real or virtual X server, and point your own game at the same configuration.
Before you start — Tutorial 02: Setting Up Your Dev Environment for the workspace layout (CNA and sharp-runtime on their next branches, plus easy-gl and meta-gl) and Tutorial 03: Your First CNA Window for the small game project used in step 7. You need a Linux machine with an X server: Xorg, or Xwayland on a Wayland desktop. Without a display you can still follow along with xvfb-run.
Every CNA tutorial so far used the default platform, SDL3. In this snapshot CNA also has a native X11 backend written directly against Xlib, and a configuration switch that removes SDL from the build entirely. This tutorial builds CNA's own 2D and 3D demos that way, proves the result contains no SDL, runs them, and then points your own game at the same configuration. The commands mirror the ones CNA's continuous integration is configured to run (workflow job x11-sdl-free-gpu), so each flag has a reason.
What you are building
| Axis | Value | Why |
|---|---|---|
| Platform | CNA_PLATFORM=X11 | Windows, events, input and clipboard come from Xlib, XKB and XInput2 instead of SDL3. |
| SDL switch | CNA_ENABLE_SDL=OFF | SDL is not fetched, built, found or linked. Anything that needs SDL is refused at configure time. |
| Audio | CNA_AUDIO_PLATFORM=NULL | Silent. The default audio (SDL3) needs SDL, so you must pick another value; Tutorial 137 swaps in real ALSA sound. |
| Renderer | CNA_GRAPHICS_RENDERER=OPENGL33, plus VULKAN;SOFTWARE;HEADLESS selectable at run time | OpenGL 3.3 through GLX. It must be a renderer that does not link SDL (not SDL_RENDERER, SDL_GPU, FNA3D or FREEDIRECT). |
1. Install the packages
On Debian or Ubuntu 24.04 (CI's image; it uses GCC 14):
sudo apt-get install -y g++-14 cmake ninja-build pkg-config git \
libx11-dev libxext-dev libxrandr-dev libxi-dev libxcursor-dev libxfixes-dev libxss-dev \
libgl1-mesa-dev libglx-dev libgl1-mesa-dri libvulkan-dev mesa-vulkan-drivers \
libdbus-1-dev xvfb x11-utils openbox
export CC=gcc-14 CXX=g++-14
Only libx11-dev and libxext-dev are mandatory (plus the XKBlib.h header that ships with libX11). Every other package is optional and switches off exactly one capability when absent:
| Package | What you lose without it |
|---|---|
libxi-dev | Relative mouse, input-device enumeration, touch and pen. |
libxrandr-dev | Multiple-display enumeration; exclusive fullscreen degrades to borderless. |
libxcursor-dev, libxfixes-dev, libxss-dev | Custom cursor images; pointer hiding; screen-saver suspension. |
libgl-dev / libglx-dev | The GLX context, so no OpenGL renderer can open a window. |
libvulkan-dev | The Vulkan surface, so no VULKAN renderer. |
libdbus-dev | File dialogs and OpenUrl through the desktop portal. |
xvfb, x11-utils, openbox, Mesa drivers | Only needed to run without a real X session (step 6). |
No SDL packages are needed — and the SDL submodules are never used in this configuration.
2. Clone the workspace
mkdir cna-workspace && cd cna-workspace
git clone -b next https://github.com/libcna/cna.git
git clone -b next https://github.com/libcna/sharp-runtime.git
git clone https://github.com/libcna/easy-gl.git # OPENGL33 is an EasyGL profile
git clone https://github.com/libcna/meta-gl.git
cd cna
git submodule update --init
Both cna and sharp-runtime must be their next branch (the default branches are alpha.1-era). easy-gl and meta-gl are needed because OPENGL33 is one of the five EasyGL renderer identities. The non-recursive submodule update fetches five submodules; with SDL off, only third_party/draco matters (and vendor/googletest if you turn tests on). If you would rather skip Draco, add -DCNA_ENABLE_DRACO=OFF below.
3. Configure
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCNA_ENABLE_SDL=OFF \
-DCNA_PLATFORM=X11 \
-DCNA_GRAPHICS_RENDERER=OPENGL33 \
-DCNA_GRAPHICS_RENDERERS="OPENGL33;VULKAN;SOFTWARE;HEADLESS" \
-DCNA_AUDIO_PLATFORM=NULL \
-DCNA_BUILD_TESTS=OFF \
-DCNA_ENABLE_NET=OFF
These are CI's flags with tests switched off to keep the build small (CI builds with tests on, and uses ALSA audio; networking is off there too and is not needed for this tutorial). Because the plural option CNA_GRAPHICS_RENDERERS is set, the build contains all four renderers and the singular one is the default; you can pick another at run time in step 6. The configure log should contain lines like these (the list of optional pieces depends on the packages you installed):
-- CNA: Using X11 platform implementation
-- CNA: X11 platform dependencies -- X11 + Xext; optional present: Xi, Xrandr, Xcursor, Xfixes, Xau, Xss, XShm, GLX, Vulkan headers, D-Bus headers
-- CNA: Using NULL audio platform implementation
-- CNA: SDL is NOT configured (CNA_ENABLE_SDL=OFF). No SDL source is fetched, built, found or linked by this configuration.
Read the second line: it is the honest list of what your X11 build can do. GLX must be there for the OpenGL renderer; Vulkan headers for VULKAN.
4. Build
cmake --build build --target cna_demo_2d cna_house3d_demo --parallel
cna_demo_2d is the 2D sprite demo (it works with every renderer) and cna_house3d_demo is the walkable 3D house. The first is built for any renderer; the second exists because the default renderer, OPENGL33, supports 3D. Do not build the target CNA: it is an umbrella interface library, not a buildable target.
5. Prove there is no SDL
ldd build/cna_demo_2d | grep -i sdl # nothing
readelf -d build/cna_demo_2d | grep -i 'NEEDED.*sdl' # nothing
nm -D --undefined-only build/cna_demo_2d | grep ' SDL_' # nothing
find build -iname '*SDL2*' -o -iname '*SDL3*' # no SDL artifact in the build tree
CI runs the equivalent checks and additionally asserts that libasound is not a NEEDED entry (it is loaded at run time if you choose ALSA audio), so a machine without it still starts. If any command above prints something, an SDL dependency has crept into your selection — but configuration would already have refused that.
6. Run it
The demo finds its Content/ folder next to the executable, so run from the build directory.
cd build
# On your own X session (or Xwayland): a real window
./cna_demo_2d
./cna_house3d_demo # W/S/A/D or arrows to move, Space to jump, Tab toggles fly mode, Esc quits
# Pick another renderer at run time (the build contains all four)
CNA_GRAPHICS_RENDERER=VULKAN ./cna_demo_2d
# No display? Give it a private virtual X server and stop after six frames
xvfb-run -a ./cna_demo_2d --smoke 6
CNA_GRAPHICS_RENDERER=SOFTWARE xvfb-run -a ./cna_demo_2d --smoke 6
The CNA_GRAPHICS_RENDERER environment variable is read once, before the first device is created; it must name a renderer compiled into the binary or the game fails at start-up with a message saying so. The 2D demo also stops when you press Esc.
No window? Check the renderer. SOFTWARE, HEADLESS and STUB (and PORTABLEGL) are CPU or no-output renderers: on X11, Wayland, Win32 and SDL3 they render off-screen and open no window at all. The SOFTWARE command above runs and exits cleanly precisely because it draws nothing on screen. Use OPENGL33 or VULKAN to see a window.
If your desktop is Wayland, the window appears through Xwayland: the compositor owns the screen, so the global pointer is exact only over your own windows, minimising and focus follow the compositor's rules, and scaling is done by the compositor (X11 reports a display scale of 1 by design). For a native Wayland client, see Tutorial 136.
7. Build your own game the same way
The platform is a build-time choice; your game code does not change. Take the project from Tutorial 03 and configure it with the same selections (the flags on the command line win over the defaults inside its CMakeLists.txt):
cd my-first-game
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCNA_ENABLE_SDL=OFF -DCNA_PLATFORM=X11 -DCNA_AUDIO_PLATFORM=NULL \
-DCNA_GRAPHICS_RENDERER=OPENGL33
cmake --build build
./build/MyFirstGame
Use the same source files; this is the same configuration CNA's own demos build with, but note that we validated the demos, not a separate consumer project. To confirm at run time which platform your binary was built for, print the default platform name before Run():
#include <iostream>
#include "CNA/Platform/PlatformFactory.hpp"
int main() {
std::cout << "platform: " << CNA::Platform::PlatformFactory::GetDefaultName() << '\n';
// ... MyGame game; game.Run();
return 0;
}
It prints X11 for this build (SDL3 for the default one). There is no CNA_PLATFORM environment variable: the platform is fixed at configure time.
8. Ask the platform what it can do
Every platform reports 32 capability flags, and a false one means the call refuses with PlatformNotSupportedException. This snippet lists the implementations compiled into your binary and a few flags of the active one (it compiles against this snapshot's headers):
#include <iostream>
#include "CNA/Platform/IPlatform.hpp"
#include "CNA/Platform/PlatformCapabilities.hpp"
#include "CNA/Platform/PlatformFactory.hpp"
int main() {
using CNA::Platform::PlatformFactory;
// Every platform implementation compiled into this binary, and the default one.
for (const std::string& name : PlatformFactory::GetAvailable()) {
std::cout << "available: " << name << '\n';
}
std::cout << "default: " << PlatformFactory::GetDefaultName() << '\n';
auto platform = PlatformFactory::Create();
const auto caps = platform->GetCapabilities();
std::cout << std::boolalpha
<< "openGlContext=" << caps.openGlContext
<< " vulkanSurface=" << caps.vulkanSurface
<< " highDpi=" << caps.highDpi
<< " gamepad=" << caps.gamepad
<< " ime=" << caps.ime
<< " globalPointer=" << caps.globalPointer << '\n';
return 0;
}
On X11 the list is X11, Headless and Terminal (the last two are always compiled on POSIX); highDpi is false by design; openGlContext is true only if GLX was available at build and the server provides GLX 1.3; gamepad comes from the kernel's evdev nodes and does not need a display. With no reachable X server, the platform still constructs, every display capability reads false, and starting a window reports the original connection error. The full matrix is on Platform Support.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Configure fails: “CNA_PLATFORM=X11 was requested but this machine cannot build it” | The message names the missing package (libx11-dev, libxext-dev, or XKBlib.h). CNA never falls back to SDL3 for X11. |
| Configure fails: “CNA_ENABLE_SDL=OFF, but this configuration genuinely requires SDL” | Something in your selection needs SDL. The default platform and audio are both SDL3, so pass CNA_PLATFORM and CNA_AUDIO_PLATFORM explicitly, and avoid SDL_RENDERER, SDL_GPU, FNA3D, FREEDIRECT. |
Configure fails: missing sibling easy-gl or meta-gl | Clone both next to cna, or choose VULKAN (needs only the Vulkan headers/loader) or OPENGL4 for the renderer. |
| Configure fails: pinned Draco source not found | Run git submodule update --init third_party/draco, or add -DCNA_ENABLE_DRACO=OFF. |
| The program cannot connect to the X server | DISPLAY is not set or points nowhere. Run under a session, or use xvfb-run -a. |
| OpenGL context creation fails | No GLX at build time (the configure summary lacks GLX) or no GL driver at run time. Install libgl-dev/libglx-dev and Mesa (libgl1-mesa-dri), or try LIBGL_ALWAYS_SOFTWARE=1. |
| No window appears and no error | You selected a CPU renderer; see the warning in step 6. |
| Wayland desktop: odd focus or pointer behaviour | You are running through Xwayland. Use SDL3, or try the native Wayland backend in Tutorial 136. |
What CI covers — and what it does not
The CI job x11-sdl-free-gpu is configured to build exactly this selection (with ALSA audio and tests on), build both demos, checks that no SDL or libasound is linked, runs the 3D demo's smoke and exclusive-fullscreen tests under every compiled-in renderer, and runs cna_demo_2d --smoke 6 once per renderer. All of it happens on a virtual X server (Xvfb) with Mesa's software GL and Vulkan drivers. It does not prove behaviour on a real GPU, a real compositor, multiple monitors or hot-plug, or real input devices. CNA's own notes describe manual validation on real desktops; that is not a gate. See Native Platforms — CI evidence for the exact job list.
Where to go next
- Tutorial 137: Native Linux Audio with ALSA — replace
NULLwith real sound and no SDL. - Tutorial 138: Building Without SDL — the rules behind
CNA_ENABLE_SDL, on every OS. - Tutorial 136: A Native Wayland Build
- Native Platforms and Platform Support — dependency tables, the capability matrix and CI evidence.