OpenGL4 renderer internals
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page); runtime-observed (recorded by CNA, not re-run here). 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 runtime-observed figures are CNA's own recorded runs on one AMD GPU under Mesa (Wayland/EGL, X11/GLX and a sanitizer tree); Windows, macOS and other drivers are unvalidated, and the compiled-effect route was outside the sanitizer runs.
modules/renderers/opengl4/ implements the one public OPENGL4 identity as a physical renderer family of its own, built as cna_renderer_opengl4. It requests a desktop OpenGL 4.1 core context from the platform, resolves entry points through its own loader and draws immediately, with no command buffer between a public call and the GL call. It is not a sixth EasyGL profile: it shares only EasyGL's meaning of XNA, through two headers. This page follows the family from CMake to context creation, per-device context isolation, resource lifetime, draw and shader routes, the modern additions, capability answers, teardown and tests, and says what the campaign recorded as GL4-0001 to GL4-0037 changed.
Purpose, boundary and the EasyGL relationship
OPENGL4 is declared Supported and Native (GraphicsBackendMaturity.hpp); that is CNA's declaration, not a measurement. Use is covered by the user guide and Tutorial 130. OpenGL4Renderer.cpp requests 4.1 core with 24 depth bits, 8 stencil bits and double buffering (4.1 is the highest core version macOS's GL also provides), then reads GL_MAJOR_VERSION, GL_MINOR_VERSION and GL_CONTEXT_PROFILE_MASK. Below 4.1, or without the core bit, the constructor throws std::runtime_error naming the granted version: a platform may legitimately grant less (a GLX fallback context, a compatibility profile), and the renderer refuses instead of running as some other GL. Anything newer is discovered by GL4::DiscoverModernCapabilities and used only behind a live check.
| Concern | OPENGL4 | Shared with EasyGL? |
|---|---|---|
| Context request | RequestedContext(): 4.1 core, verified after creation | No |
| Function loading | Own gl4_* pointers (GL4Loader.hpp); pre-1.2 entry points link against the system GL library; no sibling checkout | No (EasyGL uses easy-gl and meta-gl) |
| Context ownership | PlatformGlContextOwner, PlatformGlRendererState.hpp | Yes, one class |
| Stock-effect GLSL | GlStockShaderSources.hpp | Yes: one corpus, two binders |
| Presentation transform | GlPresentationSurfaceState, GlPresentationSurfaceState.hpp | Yes (EasyGL aliases it as EasyGLSurfaceState) |
| Textures, targets, formats, wireframe, occlusion | Own classes following EasyGL's measured semantics; native glPolygonMode; exact GL_SAMPLES_PASSED | Semantics copied, code not shared |
| Context loss | None: a desktop 4.x context is not taken away, and no reset notification is requested | EasyGL has a release-and-recreate path |
The independence was checked by reading: nothing in the family includes an easy-gl or meta-gl header or names an easygl:: or metagl symbol (the EasyGL mentions under src/ and include/ are comments), and the OPENGL4 arm of RendererSelection.cmake has no ../easy-gl check; UnitTests.cmake keeps EasyGL's suites out of an OPENGL4-only build for that reason. One comment in Harnesses.cmake still lists OPENGL4 among the identities EasyGL is "the shared implementation behind"; it is stale prose, not behaviour. The examples do compile EasyGL's example sources, but only those naming no SDL and no EasyGL internals (see tests). See EasyGL renderer internals and the family map.
The uniform and attribute names declared by GlStockShaderSources.hpp are a contract with two binders. OPENGL4's is OpenGL4StockProgram::Resolve in OpenGL4StockDraw.cpp, which resolves every name the corpus declares for every program and skips a location a program lacks (-1). A stock-effect fix changes both renderers: build and test both, and read EasyGL's shader section before renaming anything.
CMake wiring and options
CNA_GRAPHICS_RENDERER=OPENGL4 (or a member of CNA_GRAPHICS_RENDERERS)
-> RendererSelection.cmake, OPENGL4 arm: find_package(OpenGL REQUIRED), CNA_RENDERER_OPENGL4,
option CNA_OPENGL4_COMPILED_EFFECTS (ON: ThirdPartyFNA3D.cmake -> cna_configure_mojoshader())
-> RendererRegistry.cmake: row "OPENGL4 OpenGL4" -> OpenGL4::GetDescriptor
-> opengl4/CMakeLists.txt: cna_add_renderer() (src/*.cpp, non-recursive), OpenGL::OpenGL or
OpenGL::GL, [ON: embedded SpriteEffect + cna_renderer_mojoshader_effect], examples/
The arm has no sibling checkout and, without the compiled-effects option, no fetched dependency, and no host gate of its own. TERMINAL is refused earlier by the shared CPU-renderer rule, and the Windows-only and Emscripten-only partitions in RendererCombinations.cmake do not contain OPENGL4, so a configure that finds OpenGL is accepted; whether it then builds or works on Windows, macOS, Android or Emscripten is not established by these gates, and CNA's records validate Linux only. That file's one rule for this family is that PORTABLEGL cannot share a binary with OPENGL4 (both define global gl* symbols). Nothing forbids an EasyGL identity beside it; CNA's records describe a local OPENGL4;OPENGLES3;OPENGL33 tree used for same-binary comparisons (GL4-0006), and no CI job is recorded for the pair (combination rules).
CNA_RENDERER_OPENGL4 always reaches the family's own target but is defined project-wide only when OPENGL4 is the default identity. That decides which tests exist: the GoogleTest sources guard on it and compile to nothing when another renderer is the default, while the example guard CNA_GRAPHICS_RENDERER STREQUAL "OPENGL4" is true for any member, because modules/renderers/CMakeLists.txt re-points that variable while entering each family. Only OpenGL4_Smoke and the three CNA_CNAEXT-only examples (ModernFeatureDiscovery, Gl41Floor, ModernStress) exit 77 at run time unless the device really runs OPENGL4 (GL4-0035); the rest assert OpenGL4 behaviour. See renderer selection internals.
| Switch | Default | What it controls |
|---|---|---|
CNA_OPENGL4_COMPILED_EFFECTS | OFF | Compiled XNA Effect bytecode through MojoShader. The arm includes ThirdPartyFNA3D.cmake, calls cna_configure_mojoshader() and defines the macro with add_compile_definitions (directory-wide). opengl4/CMakeLists.txt then needs Python 3, embeds SpriteEffect.fxb through embed_effects.py into a generated Fna3dStockEffectBlobs.hpp and links cna_renderer_mojoshader_effect. With CNA_ENABLE_SDL=OFF MojoShader is built on the C library without its SDL_GPU adapter. The option raises kMaxSamplerSlots from 16 to 20 (vertex samplers on units 16 to 19). |
| Link line | — | OpenGL::OpenGL, GLVND's window-system-neutral library, where that target exists; OpenGL::GL elsewhere. The renderer needs GL entry points only, because its context comes from the platform; OpenGL::GL is libOpenGL plus libGLX under GLVND and gave a Wayland build a GLX dependency that only --as-needed dropped (GL4-0034). |
CNA_OPENGL4_DEBUG_OUTPUT | unset (environment) | Controls the KHR_debug callback. Unset, it is installed exactly when NDEBUG is not defined; a value starting with anything but 0 turns it on, 0 or empty turns it off, verbose adds informational messages. |
Context creation, per platform
GraphicsDevice: window with OpenGl intent + openGlFramebuffer 24/8/double-buffered
-> descriptor.create(args), args.glContext = platform.GetGlContext()
-> OpenGL4Renderer: PlatformGlContextOwner(RequirePlatformGlContext, RequirePlatformGlWindow,
RequestedContext()) -> CreateContext, MakeCurrent, publish the proc-address loader;
lease control, presentation state
-> body: LoadGL4Functions -> version/profile check -> DiscoverModernCapabilities -> debug output
-> GL_PROGRAM_POINT_SIZE -> pixel-centre scale (GL_SUBPIXEL_BITS) -> MRT limit
-> anisotropy ceiling -> log line -> swap interval -> one sampler object per slot
-> depth test on -> MSAA framebuffer if samples > 1 -> RegisterForWindow
OpenGL4RendererDescriptor.cpp declares RendererWindowKind::OpenGL, needsWindow, needsVideoSubsystem, needsGlContext, AlwaysAvailable and a glFramebuffer of 24/8/double-buffered with no multisample request: window MSAA is a managed framebuffer, not a multisampled visual. The request must be made up front because an X visual is fixed when the window is created; GraphicsDevice.cpp copies it into WindowDescription. A missing service is refused before any GL exists (PlatformNotSupportedException(OpenGlContext)). If a later step throws, the context owner, being a member, is destroyed with the half-built renderer, and RegisterForWindow is last because a throwing constructor never runs its destructor.
| Platform | Context service | Expressing 4.1 core |
|---|---|---|
| X11 | GLX 1.3+; X11GraphicsServices.cpp opens libGLX.so.0 at run time; the platform is built with GLX headers (CNA_X11_HAVE_GLX) | glXCreateContextAttribsARB with version and, from 3.2, the profile mask; if refused, a plain glXCreateNewContext context (why the renderer's own check exists). A visual short of the requested depth or stencil bits prints a stderr line. |
| WAYLAND | EGL; WaylandGraphicsServices.cpp opens libEGL.so.1 and libwayland-egl.so.1 at run time | eglBindAPI(EGL_OPENGL_API), then eglCreateContext with version and core-profile attributes; a refused version is a PlatformException naming it. |
| SDL3, SDL2 | The platform's SDL GL service (Sdl3GraphicsServices.cpp) | SDL_GL_* attributes, then SDL_GL_CreateContext. |
| WIN32 | A WGL service exists; its code was not read for this page | Not validated for OPENGL4 in CNA's records. |
| HEADLESS, TERMINAL | None (GetGlContext() is null) | TERMINAL is refused at configure; HEADLESS reaches the capability refusal at device creation. |
The contract is IPlatformGlContext.hpp; see X11 and Wayland. The renderer names no SDL type, so CNA_ENABLE_SDL=OFF is a real OPENGL4 configuration (the example macro links SDL3::SDL3 only where it exists and otherwise defines CNA_EXAMPLES_NO_SDL). After the version check the constructor logs OpenGL4Renderer initialized with OpenGL <GL_VERSION> on <GL_RENDERER> to the renderer log, never stdout; put it in every bug report.
One context per GraphicsDevice
Two GraphicsDevice objects are two GL contexts (each PlatformGlContextOwner creates its own), and a GL name means something only in the context that created it. The original defect surfaced through GL error output: the renderer made its context current only when creating a resource, so later binds, clears, uploads and deletes for the first device ran in the second device's context and raised GL_INVALID_VALUE or hit an unrelated object with the same name (GL4-0021; CNA's plan records that EasyGL kept the single-context assumption, not audited here). The design now has two halves.
- Renderer entry points. Every GL-issuing method starts with
EnsureCallingThreadContext(), i.e.PlatformGlContextOwner::EnsureCurrent(), which switches only if this owner's context is not already current.GraphicsDevice's constructor also restores the caller's GL binding after building a device. - Resources. Every class owning GL names derives from
OpenGL4ContextResource(OpenGL4Common.hpp): effect programs, queries, vertex and index buffers, the SpriteBatch, textures, render targets and every modern resource. The creating renderer ensures its context, constructs the object (its constructor issues GL there) and callsAttachOwningContext, which stores a weak reference. Each later operation takesEnterOwnContext(), a scope that makes the owning context current if needed and restores the previous binding on exit. It converts to false once the context is gone, and the resource then issues no GL at all (its names died with the context; a destructor abandons its program handle instead of deleting it).
Draw-time binds such as BindGL(unit) take no scope; they run inside a renderer method that already owns the context. Cross-device misuse is caught one layer up (GraphicsDevice, TextureCollection and the CNAEXT ComputeShader refuse foreign objects); the renderer's own dynamic_cast checks test the type, not the owner. PlatformGlContextOwner's destructor unbinds only when its own context is current, so destroying device B cannot unbind device A.
Threading. AcquireThreadContextLeaseEXT serialises a background content load against the frame: a recursive mutex in the shared lease control, a thread_local depth table so leases nest, the previous binding saved on first acquisition and restored (or released, per RendererThreadContextLeaseRelease) on the last release. A lease holds the control and, through it, the context owner, so an unreleased lease keeps the context alive past the renderer; no named test exercises that corner. Only OPENGL4 and EasyGL return a lease (device thread rules, thread and callback map).
OpenGL4MultiDeviceTests.cpp has three cases (interleaved clears land in each device's own target; destroying one device's resources leaves the other's intact; a resource outliving its device issues no GL), counting [OpenGL4 GL Error] lines through a logger sink. CNA's records say all three fail with the fix disabled; they were not run for this page.
Resource ownership, lifetime and destruction order
Buffers. A vertex buffer is one VBO plus its own VAO. ApplyLayout(stride) resets locations 0 to 15 (disabled, divisor 0) and binds the caller's VertexDeclaration with location N equal to element N, or, with none, one of eleven built-in strides; an unknown stride without a declaration throws NotSupportedException instead of binding position-only. Storage follows the buffer's capacity, so a short SetData never shrinks what a later draw may read. An index buffer is an IBO uploaded with no VAO bound; it keeps a CPU copy for the negative-base-vertex fold.
Textures. OpenGL4Textures.cpp and OpenGL4Formats.cpp store Texture2D in the twenty classic formats ClassifySurfaceFormatEXT reports as supported; cube and volume textures use narrower lists. DXT stays native where the context has S3TC (ContextHasS3tc, cached in a process-wide static) and is otherwise decoded on the CPU, the blocks retained for exact readback. Every transfer restores the unit, framebuffer and pixel-store state it touched (EasyGL leaves the default framebuffer or unit 0 bound), and every multisample resolve runs with the scissor test off.
Render targets. OpenGL4RenderTargets.cpp: an FBO whose colour attachment is the texture later sampled, plus an optional depth renderbuffer and, for MSAA, a multisample renderbuffer with a resolve FBO. The renderer owns one heap OpenGL4BoundTarget record (bound_: single target, cube, up to four MRT slots, depth format, extent; 0 means the default framebuffer) and gives each target a weak_ptr to it (REMED-GFX-168). A destroyed target detaches from the record first, before any GL name is released, and deliberately does not resolve into storage about to be freed; a renderer that dies first leaves every target's pointer expired. SetRenderTarget2D finalises an MRT set, unbinds the outgoing target (MSAA resolve, mip regeneration), then binds the next. SetRenderTargets refuses too many targets, non-OpenGL4 bindings, mismatched size or sample count, a repeated subresource and pending GL errors, reuses one mrtFbo_ with every attachment point cleared, and on failure rebinds the default framebuffer and throws. A framebuffer stores the image bottom-up, so readback reverses rows and sampling applies uRtFlipV whenever SampledRowOrderIsBottomUp is true (textures and render targets).
Destruction order (platformContext_ is declared first, so it dies last):
- Unregister the window mapping and make this context current (failure swallowed).
- With compiled effects: detach every live compiled effect, destroy this device's MojoShader context and delete the shared VAO, flipped-source copies and read framebuffer (
ReleaseCompiledEffectResourcesEXT). It runs in the destructor body because the MojoShader context is a raw pointer needing the GL context, and an engine-layer pass can hold a SpriteBatch's compiled effect past the device. - Delete the per-slot samplers, the compute sampler, the four default textures, the negative-base-vertex IBO, the MRT FBO and the MSAA buffers.
- Members in reverse order: the fourteen stock programs (still under a live context), the bound-target record, presentation state, lease control, and last
PlatformGlContextOwner, which unbinds if current and destroys the context.
Held references keep this safe: a dispatch's recorded bindings and an effect's texture-array units hold their resources by shared_ptr, and outliving resources find an expired context reference. Moving effect release after context destruction, or reordering the members, risks GL calls through a dead context (shutdown and lifetime, invariants).
Presentation, resize and readback
surfaceState_ is the shared GlPresentationSurfaceState (virtual resolution; native, fixed-height dynamic width, stretch, letterbox and overscan modes); OnSurfaceChanged throws PlatformException for a different window id. Present() ensures the context, resolves the MSAA framebuffer into framebuffer 0 with the scissor off, swaps, and rebinds the multisample framebuffer if the back buffer is the destination. BindDefaultFramebuffer rebuilds multisample storage lazily when the drawable extent or back-buffer depth format changed; CreateMsaaBuffers clamps to GL_MAX_SAMPLES and throws on an incomplete framebuffer. SetSwapInterval records and forwards the request without proving the driver honoured it. The window framebuffer is RGBA8 whatever the game requests, and CNA's document lists scRGB or HDR10 display output as not implemented. ReadBackbuffer resolves, reads GL_BACK (or the bound target), reverses rows to top-down order and restores the read framebuffer. For a black frame, check the logical-to-drawable mapping, the bound framebuffer, the MSAA resolve and the swap (EasyGL's presentation section has the same checklist).
Draw path and shader routes
The neutral trace (indexed draw trace) reaches OpenGL4StockDraw.cpp. For DrawIndexedPrimitivesEx, in order:
- Ensure the context; apply the stencil topology rule (the counter-clockwise tuple applies to triangles only, as in Direct3D 9).
- Compiled XNA effect (only when built): dispatch to the compiled route before any stock guard, since its layout is arbitrary.
StockParamsFor: a BasicEffect draw whose declarations name no Normal is drawn unlit (REMED-GFX-234); PBR, skinned, environment-map and custom effects are exempt.- Custom effect:
RequireFilterableTextureArraysEXT, bind the VAO, configure every per-vertex stream (InvalidOperationExceptionunless each buffer carries a declaration),Bind(), setWorld,View,Projection, re-point integer-typed inputs, draw. - Stock effect:
RequireDeclarationFitsStockProgrambefore the VAO is touched (REMED-GFX-218);SelectProgrampicks the shape from effect state, never from stride (stride only selects the dual-UV PBR variants: 60, 76, 80 bytes);BindDrawParamsuploads matrices (with the XNA pixel-centre translation, omitted for multisampled destinations), lighting, fog, shadow and IBL state;ConfigureDeclarationForStockProgrambinds each input by semantic from whichever stream declares it; draw, then restore the buffer's own layout. DrawIndexedWithBaseVertexFallback: a negative base vertex is folded into a scratch index buffer built on the CPU; a positive one usesglDrawElementsBaseVertex; a non-zero first instance uses the base-instance variant.
Instanced draws place per-instance streams at locations 12 to 15 for stock programs and directly after the per-vertex streams for a custom effect; a stream with no room in the 16-attribute limit is refused. Indirect draws reuse this setup, because the argument record always carries an instance count.
| Route | Source and compilation |
|---|---|
| Stock effects, SpriteBatch | GLSL ES 3.00 from the shared corpus. EnsureStockProgram compiles lazily: AdaptStockVertexShaderForOpenGL (Direct3D clip depth z*2-w, gl_PointSize), then AdaptGlslEs300ForDesktopCore (#version 300 es becomes #version 410 core; the following precision line is blanked, keeping line numbers). A failing stock program logs [OpenGL4 GL Error] stock program '<label>' failed to build and throws. SpriteBatch submits CPU-built quads in chunks of 2,048 sprites under the device's own state. |
Custom ShaderEffect | Desktop GLSL, or ES 3.00 adapted as above; errors carry VS:, FS: or Link:. A shader declaring uniform vec4 uRtFlipV gets the render-target orientation flags; integer-typed inputs use glVertexAttribIPointer, and a float-stored element bound to one is refused by name (GL4-0023). |
| Compute | #version 430 core; a failed build still returns the object so its CS: or Link: log reaches the caller. |
| Compiled XNA effects | D3D9 Effect bytecode through MojoShader's OpenGL adapter, asked for MOJOSHADER_PROFILE_GLSL120 explicitly; only with CNA_OPENGL4_COMPILED_EFFECTS. |
Compiled effects (OpenGL4CompiledEffects.cpp, OpenGL4CompiledEffect.cpp) follow EasyGL's desktop route. One MojoShader context is created lazily per renderer; it keeps its current context in process-global state, so each entry makes this device's GL context current first and then names the MojoShader one. The explicit profile replaces MOJOSHADER_glBestProfile, whose glspirv choice cannot finalise a pixel-only pass. All compiled draws share one VAO, and every bound vertex buffer must carry a declaration. A render-target source is sampled through a row-order-corrected copy per sampler slot, and a pixel-shader-only SpriteBatch pass inherits the embedded SpriteEffect vertex shader. A pass whose shader pair fails to link is refused by name, an indirect draw refuses a compiled effect, and an effect outliving its renderer is detached. MojoShader emits GLSL 1.20 into a 4.1 core context, so acceptance is the driver's choice, and CNA's records name only Mesa.
Compute, storage, indirect and the other modern additions
OpenGL4Modern.cpp holds everything after the classic surface; each answer is asked of the live context.
- Compute.
SupportsComputeShadersEXTneeds a 4.3+ context and native compute and shader-storage facts: a 4.2 context withGL_ARB_compute_shadercould dispatch but cannot compile the#version 430 corepayloads. Bindings are only recorded;DispatchEXTinstalls them (sampled inputs through a nearest/clamp sampler), runs, then restores the program, active unit, each unit's 2D texture and sampler and every generic and indexed SSBO and UBO binding, so a dispatch never disturbs XNA draw state. Each dispatch ends with one GPU-sideglMemoryBarrier(GL_ALL_BARRIER_BITS); nothing waits on the CPU. With debug output on it is wrapped in a debug group. - Buffers. One GL buffer per storage buffer; usage and CPU-access masks are validated; transfers use the copy binding points and restore them; overlapping same-buffer copies are refused; indirect-argument-only buffers exist without compute.
- Storage textures. A mutable 2D texture with every level allocated; fifteen formats, each an exact image-unit format.
ByteEXTandUShortEXTare mapped here but refused one layer up, because the CNAEXT wrapper (StorageTexture2D.cpp) requires theTextureStorageusage, which OPENGL4 does not report for them. - Texture arrays.
GL_TEXTURE_2D_ARRAYin the same twenty formats, zero-filled at creation, DXT only with native S3TC. Array unit N is GL unit N's array binding, owned by the effect and reinstalled on eachBind. A non-Filterablearray sampled through anything butTextureFilter.Pointis refused, as Vulkan refuses it, although GL would filter.MaxTextureArrayLayersisGL_MAX_ARRAY_TEXTURE_LAYERS; arrays are core since 3.0, so the floor is unchanged. - Indirect and base instance. Indirect drawing is core 4.0, so
SupportsIndirectDrawEXTis true on every accepted context once both entry points resolve; the indirect binding is restored. Base instance is the native 4.2 fact. CNA's ledger records that a first instance past the stream is deliberately not refused, since draw ranges reach the native API unvalidated. - Timers and markers. A timer is two
GL_TIMESTAMPqueries, notGL_TIME_ELAPSED: a context allows one active elapsed query, so overlapping timers would raiseGL_INVALID_OPERATION. Reads are 64-bit, the period is 1000 ps, and support needs a non-zeroGL_QUERY_COUNTER_BITS.SetStringMarkerEXTinserts aKHR_debugmarker. - Shadows, IBL, format usages. The shared stock programs sample shadows and image-based light: shadow map on unit 7, punctual cube and spot map on 8 and 9, irradiance, specular and BRDF table on 10 to 12, neutral defaults when absent.
GetSurfaceFormatUsageSupportEXTasksglGetInternalformativper format (cached; unknown without the query);StorageAtomicis known false everywhere, because image atomics needr32iorr32uiand no CNA format is one, whatever Mesa answers.
The engine-layer side is on graphics extension internals; the other families are compared in modern features by renderer.
Capability answers
OpenGL4Renderer::SupportsCapability is a switch with no default: arm. It answers true for ThreeD, DepthStencilBuffer, MultiSampleAntiAliasing, MultipleRenderTargets, WireFrame, OcclusionQuery, CustomEffects, Texture3D, MultiStreamVertexInput, Instancing, StencilBuffer and AdditiveBlending, and AnisotropicFiltering from the probed ceiling (an extension before 4.6). Six members are answered false there and never asked of it: GraphicsDevice::SupportsCapability derives CompiledEffects from SupportsCompiledEffects() (true only when the option is built), the two float render-target members from probes of Vector4 and HdrBlendable, HalfFloatTextureLinearFiltering from a virtual (true, core since 3.0), and ComputeShaders and IndirectDraw from the modern queries. Unlike Vulkan's CMakeLists.txt, this family adds no -Werror=switch, so an appended GraphicsCapability member is at most a compiler warning here (whether the default flags enable it was not established).
Format verdicts are runtime-shaped: ClassifyRenderTargetFormatEXT answers Color directly and probes every other colour target by attaching a 1x1 image to a framebuffer (cached in formatSupport_). SupportsShaderLanguageEXT is true only for desktop GLSL vertex and fragment, plus compute where promised. Limits are read from GL, except the vertex-stream ceiling (16) and the MRT ceiling (at most four). See RendererCapabilityProfile and the capability matrix.
Tests, registration and their limits
GoogleTest sources under tests/ match the modules/renderers/*/tests/*.cpp glob in UnitTests.cmake and join the CnaRendererTests group and the CnaTests aggregate. They guard on CNA_RENDERER_OPENGL4 (see build); the storage-texture and array files also on CNA_CNAEXT, the compiled-effect file on the option. Most skip at run time when the device is not OPENGL4 or lacks the capability.
OpenGL4MultiDeviceTestsandOpenGL4ModernLifetimeTests: isolation; every modern resource releases its GL names (asked of the driver withglIs*); an effect destroyed afterEndleaves nothing to replay; two devices compute in their own contexts; resources made on a loading thread under the lease serve the frame thread.OpenGL4ComputeIsolationTests,OpenGL4BaseInstanceTests,OpenGL4IntegerAttributeTests,OpenGL4StorageTextureTests,OpenGL4TextureArrayTests,OpenGL4TimingAndMarkerTests: a dispatch leaves draw state bound; instance one reads instance one's record; integer inputs; exact bytes per storage format; theFilterablerefusal and per-effect array bindings; overlapping timers and markers.OpenGL4CompiledEffectTests: 78TEST/TEST_Pdefinitions (shared conformance contracts, Direct3D 9 semantics, golden-pixel draw and SpriteBatch cases).
CTest registrations are in examples/CMakeLists.txt, under CNA_BUILD_TESTS whenever OPENGL4 is in the renderer set: 26 own registrations (one reusing an EasyGL source), the 32 shared parity fixtures as OpenGL4_Parity_<fixture> (ParityFixtures.cmake), the EasyGL corpus, and, only with CNA_CNAEXT (default OFF), OpenGL4_ModernFeatureDiscovery, OpenGL4_Gl41Floor and OpenGL4_ModernStress. That is at most 404 registrations, 407 with the engine layer, counted from the CMake files (three corpus entries need the CNA_GamerServices target). The stress test runs 3,000 cycles (timeout 900) of compute, storage textures, render-compute-render ordering, indirect and base-instance draws and a timer, checking GL errors, memory, descriptors and threads (excluding Mesa's shader-compiler pool by thread name). OpenGL4_Gl41Floor sets MESA_GL_VERSION_OVERRIDE=4.1 and skips (77) where it is not honoured.
The EasyGL parity corpus. generate_easygl_parity_corpus.py reads EasyGL's own example registrations and writes EasyGLParityCorpus.cmake: every registration whose source names no SDL API and no EasyGL internals, with EasyGL's timeout, working directory, libraries and include directories. The header records 346 entries and skips (4 harness registrations, 5 SDL-naming sources, 1 EasyGL-internal). Each becomes OpenGL4_EasyGLParity_<name>. Sources with per-renderer contract tables carry an OPENGL4 branch holding EasyGL's desktop contract verbatim, so a failure is a parity gap (CNA's ledger counts 35 when the corpus was added; 47 example sources mention CNA_RENDERER_OPENGL4 now). The oracle is each program's own assertions, not real XNA; --check reports a stale file, and nothing in the tree runs it automatically.
The GL error gate. When the callback is on (see the options table), the renderer logs [OpenGL4 GL Error] for every error, undefined-behaviour or high-severity message except those the shader compiler raises, and TestHelpers.cmake fails any OPENGL4 CTest whose output contains that line (the exemption list is empty). No registration sets the environment variable, so in an NDEBUG build the gate has nothing to match.
| CNA's records (one AMD Radeon 780M, Mesa radeonsi, 4.6 core) | Wayland (EGL) | X11 (GLX) |
|---|---|---|
ctest -R '^OpenGL4_' | 407 / 407 | 406 / 406, plus OpenGL4_Gl41Floor |
CnaGraphicsTests; CnaRendererTests | 2,833 / 0 / 57; 338 / 0 / 10 | 2,800 / 0 / 71; 232 / 0 / 0 |
CnaGraphicsExtTests | 960 / 0 / 7 | 956 / 0 / 6 (before the array cases) |
ASan, UBSan, LSan (build-asan) | OpenGL4 tests 18 / 0 / 0; CnaGraphicsExtTests 956 / 0 / 6; stress 6 / 6 | — |
These are pass, fail and skip counts as recorded in docs/opengl4-renderer.md and the ledger plan_opengl4_modern_graphics.md, produced through run_gpu_tests_private.sh; they were not repeated here and no test was executed for this page. Limits when citing them: one GPU and driver; no Windows or macOS run; no workflow names OPENGL4 (how renderers are verified); the sanitizer tree had compiled effects off, so MojoShader and that route were outside those runs, and the stress run needs ASan's quarantine off to pass its memory check. CNA's records classify a 2,112-byte Mesa leak seen without eglTerminate and suppress it in lsan_x11_mesa.supp; the default platform in CurrentPlatform.cpp is deliberately not destroyed implicitly at exit, so a standalone device process never terminates its EGL display. See test architecture and what to test after changing X.
What the campaign changed, and how to change this family
The plan file is a ledger with one row per task: read it for history and evidence, this page for behaviour. Its shape: GL4-0008 and 0010 shared the stock GLSL and presentation transform; GL4-0009 to 0018 rebuilt the renderer on EasyGL's measured semantics; GL4-0016, 0020 and 0021 added the error gate, compiled effects and context isolation; GL4-0025 to 0030 and 0037 the modern surface; GL4-0031 to 0036 the lifetime, stress, sanitizer and platform evidence, including the GLVND link.
When changing the family: decide first whether a fault lives in the shared corpus (both renderers), a binder, a resource class or a modern route. Every new GL-issuing renderer method calls EnsureCallingThreadContext(); every new resource class derives from OpenGL4ContextResource, enters its context in each operation and joins the destruction walk. Promise a capability only with an implementation and a test. Run ctest -N -R OpenGL4 to see what is registered, then the targeted tests, and the EasyGL tests if a shared header changed.
Limits and gaps
- The
gl4_*pointers, the published proc-address loader and the S3TC answer are process-wide and re-resolved at each construction; two devices on different GL implementations are not addressed in the code read for this page. - No robustness or reset notification is requested, so a driver reset is not detected; CNA's document classes recovery as not applicable to a desktop context.
OpenGL4_ModernStress(900-second timeout) exists only whenCNA_CNAEXTis ON, its default being OFF, so a default configure does not contain it or the other two engine-layer examples.
Read in this order
RendererSelection.cmake(OPENGL4 arm),opengl4/CMakeLists.txtandOpenGL4RendererDescriptor.cpp.PlatformGlRendererState.hppandOpenGL4Common.hpp: context ownership and the per-resource scope.OpenGL4Renderer.hpp, then the constructor, destructor,Presentand target switching inOpenGL4Renderer.cpp.OpenGL4StockDraw.cppwithGlStockShaderSources.hpp, one draw route at a time.OpenGL4Resources.hppand the texture, target and format files, thenOpenGL4Modern.cpp.examples/CMakeLists.txtand the test whose preconditions match the change;docs/opengl4-renderer.mdlast, as a summary to check against the code.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- easy-gl and meta-gl: the two-library GL stack beneath the EasyGL family — How meta-gl and easy-gl split loading, typed calls, ownership and failure beneath CNA's five GL identities: revisions CNA needs, feature gating, per-thread state, context loss, tests and build inheritance.
- EasyGL: five GL profiles, one implementation, and the other GL-named renderers — What OPENGLES2, OPENGLES3, OPENGL33, WEBGL1 and WEBGL2 share in EasyGL, where they differ, how far evidence carries between them, and how OPENGL4 and PORTABLEGL differ.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-VGAP-010: No CI workflow builds or tests the OPENGL4 renderer — OPENGL4, declared Supported and merged at this snapshot with several hundred family CTests, is named by none of the 20 workflows, so its evidence is CNA's recorded runs on one Radeon 780M under Mesa, and Windows and macO
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Desktop OpenGL 4: the OPENGL4 renderer · Tutorial 130: OpenGL 4 on the desktop · Capability matrix
- Architecture
- Graphics architecture
- Internals
- EasyGL renderer internals (shared stock shaders) · SDL_gpu renderer internals · Vulkan renderer internals · Renderer selection internals · GraphicsDevice internals · Graphics backends
- Maintainer workflow
- Fix a renderer bug · Debug shutdown and lifetime
- Tests and validation
- Test architecture · What to test after changing X
- Reference
- Selection axes index · CMake option index · Test target index