GL Function Loading
Overview
OpenGL is loaded at runtime, never linked. Each platform shell opens the
system GL library, hands a symbol-resolution closure to load_gl_context,
and gets back a GenericGlContext with ~800 function pointers populated.
WebRender, the SVG renderer, and the FXAA pass all run against that single
struct. WIP — GenericGlContext may move to a slimmer subset; the rest
of the loader interface is stable.
The shared loader lives at dll/src/desktop/shell2/common/gl_loader.rs.
Per-platform code wraps load_gl_context with a closure that knows where
the system GL symbols live (EGL on Linux, the OpenGL framework on macOS,
WGL plus opengl32.dll on Windows). The loader cannot fail per-symbol —
missing entries stay null, and calling them is undefined behaviour, so the
platform layer must verify the GL version before invoking
extension-only functions.
load_gl_context
pub fn load_gl_context(
get_func: impl Fn(&str) -> *mut gl_context_loader::c_void,
) -> GenericGlContext {
GenericGlContext {
glAccum: get_func("glAccum"),
glActiveTexture: get_func("glActiveTexture"),
// ...~800 more entries
glEndTilingQCOM: get_func("glEndTilingQCOM"),
}
}
The struct definition is in the gl_context_loader crate. Every field is
a raw function pointer (*mut c_void) cast at the call site.
Per-platform resolvers
Each platform owns one file that constructs a GlFunctions wrapper around
the Rc<GenericGlContext> and a handle to whatever needs to stay alive
(DLL, framework, or Library).
Linux
pub fn initialize(egl: &Rc<Egl>) -> Result<Self, String> {
let opengl_lib = Library::load("libGL.so.1").ok();
let context = load_gl_context(|s| {
let symbol_name = CString::new(s).unwrap();
let addr = unsafe { (egl.eglGetProcAddress)(symbol_name.as_ptr()) };
if !addr.is_null() {
return addr as *mut gl_context_loader::c_void;
}
if let Some(lib) = &opengl_lib {
if let Ok(addr) = unsafe { lib.get_symbol::<*mut c_void>(s) } {
return addr as *mut gl_context_loader::c_void;
}
}
std::ptr::null_mut()
});
// ...
}
eglGetProcAddress is the primary path — it works for all GL/GLES
extensions and modern entry points. libGL.so.1 is the fallback for
legacy 1.x core symbols that some EGL implementations don't expose through
eglGetProcAddress. Used by both the X11 and Wayland backends.
macOS
pub fn initialize() -> Result<Self, String> {
const RTLD_NOW: i32 = 2;
const RTLD_GLOBAL: i32 = 8;
let framework_path = CString::new(
"/System/Library/Frameworks/OpenGL.framework/OpenGL"
).unwrap();
let handle = unsafe { dlopen(framework_path.as_ptr(), RTLD_NOW | RTLD_GLOBAL) };
if handle.is_null() {
return Err("Could not dlopen OpenGL.framework/OpenGL".to_string());
}
let context = load_gl_context(|s| {
let c_string = CString::new(s).unwrap();
(unsafe { dlsym(handle, c_string.as_ptr()) }) as *mut _
});
// ...
}
There is no eglGetProcAddress on macOS; every symbol comes from a single
dlopen of the framework binary. The hardcoded path bypasses dyld's
search logic — the framework lives at a fixed location on every supported
macOS version. dlclose runs in Drop. The framework is deprecated by
Apple but still present on every macOS release; migration to Metal would
replace this entire file (and the WebRender backend), not just the
loader.
Windows
pub fn load(&mut self) {
let opengl32_dll = self._opengl32_dll_handle;
self.functions = Rc::new(load_gl_context(|s| {
use winapi::um::{
libloaderapi::GetProcAddress,
wingdi::wglGetProcAddress,
};
let mut func_name = super::encode_ascii(s);
let addr1 = unsafe {
wglGetProcAddress(func_name.as_mut_ptr() as *const i8)
};
(if addr1 != ptr::null_mut() {
addr1
} else if let Some(opengl32_dll) = opengl32_dll {
unsafe {
GetProcAddress(opengl32_dll, func_name.as_mut_ptr() as *const i8)
}
} else {
addr1
}) as *mut gl_context_loader::c_void
}));
}
wglGetProcAddress only resolves entry points beyond OpenGL 1.1 — the 1.1
core (glClear, glViewport, etc.) must come from opengl32.dll
directly. That's why the DLL handle is loaded eagerly in initialize()
and the loader falls back to GetProcAddress on it. Any function that
returns null after both paths is unavailable on this driver.
load() is split from initialize() because wglGetProcAddress requires
a current WGL context. The platform shell creates a dummy context,
calls load(), then destroys the dummy context.
Initialization order
- The platform shell creates the native GL surface (GLX FBConfig, NSOpenGLContext, or WGL pixel format + dummy context).
- The shell makes that context current.
- The shell calls
GlFunctions::initialize(...)(or.load()on Windows), which callsload_gl_contextto resolve every entry point. - The shell hands the resulting
Rc<GenericGlContext>toGlContextPtr::new, which compiles the SVG and FXAA shader programs and stores the program IDs inGlContextPtrInner. - WebRender is initialized against the same
Rc<GenericGlContext>.
If any of steps 1 through 3 are out of order (particularly making the
context current after loading on Windows), the resulting
GenericGlContext is full of nulls. Every subsequent call returns a black
frame or crashes in WebRender.
Why one giant struct
GenericGlContext sits in the gl_context_loader crate (third-party
fork) and exposes every GL entry point as a public field. Three reasons
it's shaped this way:
- No global state. Static GL bindings (the typical C/C++ pattern) require a single context per process. Azul wants per-window contexts and headless test contexts to coexist; one struct per context is the only design that scales.
- Ad-hoc context substitution. Tests and
cpurenderswap a realGenericGlContextfor a stub by constructing the struct directly withmem::zeroed()(Windows takes this path duringinitialize()before the real context is current). - Lifetimes are obvious.
Rc<GenericGlContext>is the GL context. When the last refcount drops, the struct is freed and so is every cached pointer — no orphaned function pointers that outlive their library.
GlContextPtr wraps the Rc and adds the compiled SVG and FXAA program
IDs. The wrapper is repr(C) and exposed across the FFI boundary. The
compiled shaders live alongside the function pointers because they share
the same lifetime.
glconst.rs
core/src/glconst.rs declares 1624 OpenGL enum constants — every named
GL_* value the implementation may need. The file is purely declarative:
pub const ACTIVE_ATTRIBUTES: types::GLenum = 0x8B89;
pub const ACTIVE_ATTRIBUTE_MAX_LENGTH: types::GLenum = 0x8B8A;
// ...
pub use crate::glconst::* in core/src/gl.rs re-exports them so call
sites use gl::TEXTURE_2D and similar without further qualification. The
constants are GL-spec-defined. If you need a value that isn't here, look
it up in the OpenGL registry and add a single pub const line.
What's not in GenericGlContext
A few functions the renderer needs are not in GenericGlContext:
- WGL extensions.
wglCreateContextAttribsARB,wglSwapIntervalEXT, andwglChoosePixelFormatARB. Loaded into a separateExtraWglFunctionsstruct indll/src/desktop/shell2/windows/gl.rsvia a dummy context. - GLX/EGL setup.
eglGetDisplay,eglCreateContext, and friends. Loaded directly through the EGLLibrary(X11 and Wayland) before any GL function pointer exists. - Platform context-creation primitives.
wglMakeCurrent,[NSOpenGLContext makeCurrentContext],eglMakeCurrent. Called by the platform shell, never bycoreorlayout.
Anything else missing is a bug in gl_context_loader. File a PR there
rather than working around it in the loader closure.
Failure modes
- All draws produce a black frame. The loader closure ran without a
current GL context. On Windows,
load()was called beforewglMakeCurrent. - Some draws work, others crash on entry. The driver doesn't expose
that entry point. Check for null in the consuming code, or guard with
GlApiVersion. - Linux backend fails entirely.
libGL.so.1isn't installed (themesaornvidia-driverpackage is missing). EGL alone cannot resolve every legacy symbol. - macOS backend fails to dlopen. The OpenGL framework was removed. Rare; no shipping macOS version has done this.
- Windows backend works in dev but fails in installer.
opengl32.dllisn't on the load path of the installed binary, or the installer renamed the DLL.
The Linux fallback to libGL.so.1 is the only path that swallows a
missing symbol silently. The others either return null (per-symbol
failure) or refuse to construct the loader at all (whole-context
failure). If a single function pointer is null and you call it, the
program segfaults. There is no runtime guard. The OpenGL ES tile-control
entries (glStartTilingQCOM, glEndTilingQCOM) are present in the
struct for completeness but only populate on Adreno-class mobile GPUs.
Desktop drivers leave them null.
Coming Up Next
- Rendering — display list to pixels
- WebRender Bridge — transactions, pipelines, IFrames
- Image Pipeline — texture caches, ExternalImageId