Tutorial 130: Run CNA on Desktop OpenGL 4 (OPENGL4)
What you’ll learn: how OPENGL4 differs from the EasyGL-backed OpenGL identities, how to build it on the SDL3 platform and with no SDL at all on native X11, how to probe the granted context and its capability answers, how to run CNA’s own OpenGL4 tests, and what to do when the driver grants less than 4.1.
Before you start — Tutorial 02 (toolchain and the sharp-runtime sibling), Tutorial 20 (building CNA itself) and Tutorial 102 (the five EasyGL-backed OpenGL identities, so you can see what makes OPENGL4 different). This tutorial documents CNA snapshot 009d40f5, which lives on the next branch; git clone https://github.com/libcna/cna.git without -b next gives you the alpha.1 tag, whose OPENGL4 is a much smaller renderer (see below) and which has none of the compute, float-target, shadow or compiled-effect support described here.
Most of CNA's OpenGL choices are profiles of one implementation, EasyGL, that speaks OpenGL ES 2/3, WebGL or desktop GL 3.3. OPENGL4 is the exception: a renderer family of its own, with its own hand-written function loader, its own draw code and no dependency on the easy-gl or meta-gl sibling checkouts. It asks the platform for a desktop OpenGL 4.1 core-profile context, runs on whatever newer core context the driver grants, and refuses anything older. On a 4.3+ context it also unlocks compute shaders and storage buffers.
In this tutorial you will build CNA with OPENGL4 twice — once on the default SDL3 platform and once with no SDL at all, on native X11 — write a small program that prints what the renderer reports, run CNA's own OPENGL4 tests, and see what to do when the driver grants less than 4.1.
Do not confuse OPENGL4 with OPENGL33. OPENGL33 is desktop OpenGL 3.3 core through EasyGL (declared Production). OPENGL4 is a separate implementation targeting 4.1+ (declared Supported, CNA's own classification, not a measurement). They share CNA's stock-effect GLSL corpus, but not their driver-facing code, their dependencies or their capability answers.
What OPENGL4 is, precisely
| Question | Answer at this snapshot |
|---|---|
| Context requested | Desktop OpenGL 4.1, core profile, 24-bit depth, 8-bit stencil, double-buffered. A newer core context is accepted and used. |
| What is refused | A context below 4.1, or a non-core (compatibility) profile. The renderer throws std::runtime_error whose text begins OpenGL4: this renderer requires a desktop OpenGL 4.1 core profile context; the platform granted OpenGL … rather than quietly running as some other GL. |
| Function loading | Its own loader (GL4Loader). No glad, GLEW or EasyGL. |
| Where the context comes from | The selected CNA_PLATFORM: SDL3, SDL2, native X11 (GLX), native Wayland (EGL) or native Win32 (WGL). All five ask for a core-profile context of the requested version in source. HEADLESS and TERMINAL provide no GL context. |
| SDL | Not required. OPENGL4 is not one of the four renderers that link SDL3 by identity, so -DCNA_ENABLE_SDL=OFF is a valid configuration for it. |
| Sibling repositories | Only ../sharp-runtime (its next branch). No easy-gl, meta-gl, free-direct or fetched renderer library. |
| System dependency | The platform's OpenGL library and headers, located with find_package(OpenGL REQUIRED). |
| Windows / macOS gate | None in CMake. (Not being gated is not the same as being validated there; see the status note below.) |
| Compiled XNA effects | Opt-in: -DCNA_OPENGL4_COMPILED_EFFECTS=ON, off by default. See Tutorial 128. |
What changed since alpha.1. OPENGL4 already existed in the alpha.1 tag, with the same 4.1 core request, but as a classic-XNA renderer: no compute, no storage buffers, no indirect draw, no GPU timers, no compiled-effect option, no shadow or image-based-lighting claims, and multi-stream vertex input reported as unsupported. The “modern graphics” work merged into next after the tag added all of those; the renderer's source directory grew from 3 to 11 source files. It also gained its own example and parity test registrations and, through the native platform layer, runs without SDL. Everything in this tutorial describes the snapshot, not the tag.
Get the sources and packages
Lay the checkouts out side by side. sharp-runtime must be its next branch too; the default branches lack the Resources and Xml.Serialization components this CNA snapshot needs.
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
cd cna
git submodule update --init # SDL3, SDL_image, SDL_mixer, googletest, draco
You do not clone easy-gl or meta-gl for this renderer. (An ordinary Linux build with the default OPENGLES3 renderer does; that is the difference.)
On a Debian-family system the package set CNA's own SDL-free X11 workflow installs for its GL leg is a good starting point. Only libX11, libXext and XKBlib.h are mandatory for the native X11 platform; each of the other X libraries switches on one capability.
sudo apt-get install --no-install-recommends \
g++ cmake ninja-build pkg-config \
libx11-dev libxext-dev libxrandr-dev libxi-dev libxcursor-dev libxfixes-dev \
libxss-dev libxtst-dev libxkbcommon-dev \
libgl1-mesa-dev libglx-dev libgl1-mesa-dri
The GLX headers matter. The native X11 platform offers an OpenGL context only when find_package(OpenGL COMPONENTS OpenGL GLX) found them; without GLX the platform builds, but it reports no OpenGL-context capability and OPENGL4 fails at device creation with a “platform capability not supported” error naming OPENGL4. Toolchain requirements (CMake 3.20 or newer, a C++23 compiler) are the same as for every other CNA configuration.
Configure and build: the default SDL3 platform
The shortest route changes one option. SDL3 (built from CNA's vendored submodule) supplies the window and the GL context; OPENGL4 supplies everything after that.
cmake -S . -B build -DCNA_GRAPHICS_RENDERER=OPENGL4
cmake --build build --target cna_demo_2d --parallel
Near the top of the configure output you should see CNA: Using OPENGL4 (real desktop OpenGL 4.x core profile) graphics renderer. Run CNA's renderer-agnostic 2D sprite demo for a handful of frames and exit:
./build/cna_demo_2d --smoke 6
--smoke N makes the demo exit cleanly after N drawn frames, so a zero exit status means the whole path — window, GL 4.1 core context, function loading, sprite draws and swap — worked.
The 3D house demo is not built for OPENGL4. At this snapshot the cna_house3d_demo target is created only for the five GL-profile identities, VULKAN, WEBGPU and FNA3D, so --target cna_house3d_demo does not exist in this configuration. That is a decision about which demo targets exist, not a statement that OPENGL4 has no 3D pipeline; it reports ThreeD as supported (see below).
Configure and build: no SDL at all (native X11 or Wayland)
Because OPENGL4 gets its context from the platform layer, it can run on CNA's native platforms, which talk to Xlib or the Wayland protocol directly. This is CNA's “CNA without SDL” configuration:
cmake -S . -B build \
-DCNA_GRAPHICS_RENDERER=OPENGL4 \
-DCNA_PLATFORM=X11 \
-DCNA_ENABLE_SDL=OFF \
-DCNA_AUDIO_PLATFORM=NULL \
-DCNA_ENABLE_NET=OFF
cmake --build build --target cna_demo_2d --parallel
Every option other than the renderer is what CNA's own SDL-free workflows pass. CNA_ENABLE_SDL=OFF means no SDL source is fetched, built, found or linked, so the audio platform must not be SDL either (NULL here; ALSA on Linux gives you sound). CNA_ENABLE_NET=OFF drops the networking module; leave it out if you want it. Re-run the command with different -D values in the same build directory to change platform; CMake updates its cache.
CNA_PLATFORM | GL context comes from | Needs | Automatic CI for OPENGL4 |
|---|---|---|---|
SDL3 (default) | SDL3 | Vendored SDL3 submodule | None |
X11 | GLX, through the native X11 backend | libX11, libXext, XKBlib.h; GLX headers (GL development package) | None |
WAYLAND | EGL, through the native Wayland backend (EGL and wayland-egl are opened at run time) | wayland-client ≥ 1.18, xkbcommon ≥ 0.5, wayland-scanner, wayland-protocols | None (no workflow selects WAYLAND at all) |
SDL2 | SDL2 | Also needs -DCNA_AUDIO_PLATFORM=SDL2; SDL2 and SDL3 do not mix | None |
WIN32 | WGL (a core profile through wglCreateContextAttribsARB) | Windows, or a mingw-w64 cross-build | None; unverified for OPENGL4 |
“None” in the last column is deliberate: no GitHub workflow in this snapshot names OPENGL4. CNA's SDL-free X11 workflow builds the same shape with OPENGL33, VULKAN, SOFTWARE and HEADLESS, not with OPENGL4. The renderer's own test suite is registered and is described in Run CNA's own OpenGL4 tests.
What CMake refuses, and why that helps
-DCNA_ENABLE_SDL=OFFwith an SDL platform, SDL audio or one of the four SDL-linking renderers is a configureFATAL_ERRORthat names what needs SDL. Nothing is substituted.-DCNA_PLATFORM=X11orWAYLANDon a machine that cannot build it is aFATAL_ERRORnaming the packages to install; it never falls back to SDL3.CNA_PLATFORM=TERMINALaccepts only CPU renderers, soOPENGL4is refused there.HEADLESShas no GL context service.- Renderer names are case-sensitive in CMake:
-DCNA_GRAPHICS_RENDERER=opengl4is an unknown graphics renderer error; only the 25 exact spellings are accepted. OPENGL4cannot share a binary withPORTABLEGL. PortableGL defines the globalgl*symbols itself, so linking it beside a real-GL renderer would be a duplicate-symbol error; CMake refuses the combination up front.
Write a probe program
Place a small consumer project beside the checkouts, using the same add_subdirectory pattern as Tutorial 03:
cna-workspace/
cna/
sharp-runtime/
gl4-probe/ <-- new
CMakeLists.txt
main.cpp
cmake_minimum_required(VERSION 3.20)
project(Gl4Probe LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Choose the renderer before CNA is added. Platform, audio and SDL options are passed
# on the command line so the same file serves the SDL3 and the SDL-free build.
set(CNA_GRAPHICS_RENDERER "OPENGL4" CACHE STRING "CNA graphics renderer")
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../cna ${CMAKE_BINARY_DIR}/cna)
add_executable(gl4_probe main.cpp)
target_link_libraries(gl4_probe PRIVATE CNA)
The program opens a window, asks the graphics device which renderer it really got and how it answers five capability questions that matter for OpenGL 4, draws one frame, and exits:
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDevice.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsProfile.hpp"
#include "CNA/GraphicsCapability.hpp"
#include <cstdio>
#include <string_view>
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
using CNA::GraphicsCapability;
class GlFourProbe final : public Game {
public:
GlFourProbe() : graphics_(this) {
getWindowProperty().setTitleProperty("OPENGL4 probe");
graphics_.setPreferredBackBufferWidthProperty(640);
graphics_.setPreferredBackBufferHeightProperty(360);
// Reach is the default profile and allows one render target; HiDef allows four.
graphics_.setGraphicsProfileProperty(GraphicsProfile::HiDef);
}
protected:
void Initialize() override {
Game::Initialize();
auto& device = getGraphicsDeviceProperty();
const std::string_view name = device.GetGraphicsRendererName();
std::printf("renderer: %.*s\n", static_cast<int>(name.size()), name.data());
const auto yesNo = [&](GraphicsCapability capability) {
return device.SupportsCapability(capability) ? "yes" : "no";
};
std::printf("MSAA %s, MRT %s, occlusion %s, compute %s, indirect draw %s\n",
yesNo(GraphicsCapability::MultiSampleAntiAliasing),
yesNo(GraphicsCapability::MultipleRenderTargets),
yesNo(GraphicsCapability::OcclusionQuery),
yesNo(GraphicsCapability::ComputeShaders),
yesNo(GraphicsCapability::IndirectDraw));
}
void Update(GameTime& gameTime) override {
(void)gameTime;
Exit(); // one frame is enough for a probe
}
void Draw(const GameTime& gameTime) override {
(void)gameTime;
auto& device = getGraphicsDeviceProperty();
device.Clear(Color::CornflowerBlue);
// No device.Present(): Game presents in EndDraw, after Draw() returns.
}
private:
GraphicsDeviceManager graphics_;
};
int main() {
GlFourProbe game;
game.Run();
return 0;
}
Configure and build it. Use the SDL3 flags from the first section, or the native ones from the second:
cd ../gl4-probe
# SDL3 platform (default)
cmake -S . -B build
# ... or native X11, no SDL
cmake -S . -B build -DCNA_PLATFORM=X11 -DCNA_ENABLE_SDL=OFF -DCNA_AUDIO_PLATFORM=NULL -DCNA_ENABLE_NET=OFF
cmake --build build --target gl4_probe --parallel
./build/gl4_probe
The first output line is renderer: OPENGL4. The name is exactly the CNA_GRAPHICS_RENDERER spelling, and it is the device's real renderer, not a compile-time guess (see Tutorial 126 for why that distinction exists). CNA also logs CNA: graphics renderer: OPENGL4 once, at Info level on stderr, and the renderer logs the driver's GL_VERSION and GL_RENDERER strings when it initialises.
Status of the commands and code above. We checked every option, target, class and method in this tutorial against the CNA headers and CMake files at snapshot 009d40f5, and the C++ compiles as a syntax-only check against those headers. We did not configure, build or run CNA for this page, so the values printed on your machine come from your driver. What the code says should be printed is in the next section.
What OPENGL4 reports
These answers come from the renderer's own SupportsCapability switch plus the device-level derivations described in Tutorial 101. “Probe” means the answer is read from the live context, so it can differ between two machines running the same binary.
GraphicsCapability | Answer | Condition |
|---|---|---|
ThreeD, DepthStencilBuffer, StencilBuffer | Yes | — |
MultiSampleAntiAliasing | Yes | Core 4.x guarantees at least 4 samples. |
MultipleRenderTargets | Profile | The renderer allows up to four targets, but the device ANDs the answer with the XNA profile: Reach (the default) permits one, HiDef four. Set setGraphicsProfileProperty(GraphicsProfile::HiDef) as the probe does. |
AnisotropicFiltering | Probe | True when the driver reports a maximum anisotropy above 1 (an extension before GL 4.6). |
WireFrame | Yes | Native glPolygonMode, not a line-list re-expansion, so culling, clipping, depth bias and MSAA still apply. |
OcclusionQuery | Yes | Exact passed-sample counts. |
CustomEffects, Texture3D, MultiStreamVertexInput, Instancing, AdditiveBlending | Yes | — |
CompiledEffects | Build option | True only in a build configured with -DCNA_OPENGL4_COMPILED_EFFECTS=ON. |
FloatRenderTargets, HalfFloatRenderTargets | Probe | Answered from the render-target format probe (SupportsSurfaceFormatAsRenderTargetEXT). |
HalfFloatTextureLinearFiltering | Yes | Core since GL 3.0. |
ComputeShaders | 4.3+ | True only on a 4.3+ core context with native compute and shader-storage buffers. CNA's compute payloads are #version 430 core, so a 4.2 context that merely exposes the extension is refused. |
IndirectDraw | Probe | True when the native indirect-draw entry points exist (core since 4.0), so it is available on the 4.1 floor. |
Beyond the 19 legacy capabilities, the renderer answers true for shadow sampling (directional, cascaded, point and spot) and image-based lighting in CNA's lit stock shaders, reports a real GL_TEXTURE_3D sampler path for custom effects, supports GPU timers (timestamp pairs, when the driver reports a non-zero counter width) and base-instance drawing (4.2+ or GL_ARB_base_instance), and stores all 20 classic XNA surface formats. Tutorial 133 shows how to read every one of these answers, plus limits and per-format usage, from the device's RendererCapabilityProfile.
See the 4.1 floor for yourself
Mesa normally answers a 4.1 core request with a 4.6 context, so you rarely see the floor. Mesa can be told to hand out a 4.1 context instead, which is exactly how CNA's own OpenGL4_Gl41Floor test (built when CNA_CNAEXT is on) gets one. Run the probe that way and compute should flip to no while indirect draw stays yes:
MESA_GL_VERSION_OVERRIDE=4.1 MESA_GLSL_VERSION_OVERRIDE=410 ./build/gl4_probe
Those two variables belong to Mesa, not to CNA, and other drivers ignore them.
Custom shaders on OPENGL4
A ShaderEffect on this renderer is desktop GLSL: the vertex and fragment sources reach the driver's compiler. There is one convenience: if a source's very first line is #version 300 es — the dialect CNA authors its own stock shaders in — the renderer rewrites it to #version 410 core and blanks the precision statement that follows, keeping line numbers intact so compiler diagnostics still name the line you wrote. Any other source is compiled as written, so you can also write #version 410 core yourself. Compute payloads must be #version 430 core and need the 4.3+ context above. See Tutorial 52 for the ShaderEffect API itself.
In the capability profile this shows up as ShaderDialectGlslDesktop being supported and the other dialect flags (OpenGL ES, Vulkan GLSL, HLSL, MSL, WGSL) being unsupported; Tutorial 133 shows how to ask.
Run CNA's own OPENGL4 tests
When CNA_BUILD_TESTS (on by default) is set, an OPENGL4 configuration registers its own tests, all labelled OpenGL4. They create a real GL context, so they need a display: either your desktop session, or a private virtual X server that CNA's launcher script starts for you.
# list what is registered
ctest --test-dir build -N -L OpenGL4
# build the smallest test (window + context + a 60-frame Clear/Present loop)
cmake --build build --target cna_test_opengl4_smoke
# run it against a private virtual X server that CNA's launcher starts and stops
sh tools/platform/x11_test_server.sh ./build/cna_test_opengl4_smoke
# or, from your own desktop session (the window opens on your screen)
ctest --test-dir build -R '^OpenGL4_Smoke$' --output-on-failure
The launcher needs Xvfb installed; without it the script exits with status 77 (“skipped”) instead of failing. The tests' DISPLAY is whatever you export, because CNA_TEST_DISPLAY defaults to empty and CNA no longer forces the live desktop display.
What the registrations cover, all verified from the renderer's example CMakeLists.txt:
- Own tests such as
OpenGL4_Smoke,OpenGL4_Readback(pixel read-back),OpenGL4_3D,OpenGL4_RenderTarget2D,OpenGL4_MSAAandOpenGL4_Texture3D. Three more —OpenGL4_ModernFeatureDiscovery,OpenGL4_Gl41Floor(the 4.1-floor oracle described above) and a long-runningOpenGL4_ModernStresssoak — are registered only when the engine layer is on (-DCNA_CNAEXT=ON). - 32 shared parity fixtures, registered as
OpenGL4_Parity_<fixture>. These are renderer-neutral programs whose expected result is stated by the fixture's own assertions. They are not compared against real XNA. Only the EasyGL-backed identities,WEBGPU,SDL_GPUandOPENGL4register them. - A copy of the EasyGL example corpus rebuilt against
OPENGL4and registered asOpenGL4_EasyGLParity_<name>, on the reasoning that a program that proves a capability on EasyGL should pass here too. - A GL-error gate: with
KHR_debugavailable (Debug builds, orCNA_OPENGL4_DEBUG_OUTPUT=1in the environment) the renderer prints[OpenGL4 GL Error] …for every error or high-severity message, and its CTests fail on that line.CNA_OPENGL4_DEBUG_OUTPUT=verbosealso logs informational messages.
CNA's own renderer documentation records local runs of these suites on Wayland (EGL) and X11 (GLX). Those runs are CNA's account of its own machines and are not part of any automatic workflow; neither this page nor the site has re-run them.
When creation fails
| Symptom | Cause | Fix |
|---|---|---|
OpenGL4: this renderer requires a desktop OpenGL 4.1 core profile context; the platform granted OpenGL 3.x … | The driver granted less than 4.1 core (an old GPU or driver, or a driver that only offers a compatibility profile). | Update or change the driver; check the core-profile version your driver reports (for example with glxinfo -B from Mesa's utilities). Use OPENGL33 (EasyGL, 3.3 core) if the machine genuinely cannot do 4.1. |
A “platform capability not supported” exception naming OPENGL4 and the OpenGL-context capability | The selected CNA_PLATFORM has no GL context service: HEADLESS, TERMINAL, or native X11 built without GLX headers. | Install the GL development package and reconfigure, or choose a platform that provides a context. |
| Configure stops with CNA_ENABLE_SDL=OFF, but this configuration genuinely requires SDL | You left the default SDL3 platform or SDL audio in place. | Pass -DCNA_PLATFORM=X11 (or WAYLAND) and -DCNA_AUDIO_PLATFORM=NULL or ALSA. |
Configure stops asking for ../easy-gl | OPENGL4 was not selected, so the per-host default was: OPENGLES3 on Linux, which is an EasyGL identity and needs those siblings. | Pass -DCNA_GRAPHICS_RENDERER=OPENGL4 (in the probe project, the set(CNA_GRAPHICS_RENDERER …) line does the same). |
Program starts but the capability line says compute no | The granted context is below 4.3. | Expected on a 4.1 or 4.2 context; guard compute code with SupportsCapability(GraphicsCapability::ComputeShaders). |
With a multi-renderer build (Tutorial 126) you can keep OPENGL4 as the default and list OPENGLES3 or SOFTWARE after it in an explicit fallback chain. Fallback is off unless you opt in, and a renderer that cannot start is an error otherwise.
Next steps
- Tutorial 133: Read the renderer capability profile — the 32 features, 22 limits and per-format usage that go beyond
SupportsCapability() - Tutorial 131: Use the SDL_GPU renderer and Tutorial 132: WebGPU, native and in the browser — the other two renderers that received the same “modern graphics” work
- Tutorial 128: Load compiled XNA effects — the
CNA_OPENGL4_COMPILED_EFFECTSoption in context - Tutorial 102: The OpenGL family — the EasyGL profiles, for comparison
- Tutorial 127: Choose platform, renderer and audio independently
- Renderers reference: the OPENGL4 entry