Status: implemented. This document describes the C output — what gloam generates, how to build it into a project, and the runtime API it exposes. The Rust output is covered by rust-loader.md; the
.gloam/manifest.jsonwritten alongside every tree is covered by manifest.md.
The C backend emits one header/source pair per requested spec, plus the auxiliary Khronos headers those files include:
<out-path>/
.gloam/manifest.json # provenance + the exact command line (see manifest.md)
include/
gloam/gl.h # one public header per spec stem
gloam/vk.h
...
KHR/khrplatform.h # auxiliary headers, only those the output needs
vk_platform.h
xxhash.h
src/
gl.c # one implementation file per spec stem
vk.c
...
The stem is the spec name (gl, gles2, vk, egl, glx, wgl); a
merged GL + GLES2 request (--merge) produces a single gl pair serving
both APIs. Everything in the tree is self-contained — no system Khronos
headers are required (Vulkan can opt back into them with
--external-headers).
To integrate: add include/ to the include path, compile the src/*.c
files with the rest of the project, and include <gloam/gl.h> (or the
stem you generated). On POSIX platforms --loader output needs -ldl;
nothing else links against anything.
Each spec gets a context struct (GloamGLContext, GloamVulkanContext,
...) holding every function pointer, the feature/extension presence flags,
and the detected version. A global instance of each is defined
(gloam_gl_context, gloam_vk_context, ...), and the API names dispatch
through it:
glClear(GL_COLOR_BUFFER_BIT); /* macro → gloam_gl_context.Clear(...) */
vkCmdDraw(cmd, 3, 1, 0, 0); /* force-inlined fn → gloam_vk_context */GL, GLES, EGL, GLX, and WGL dispatch uses #define macros. Vulkan
dispatch uses static force-inlined functions instead, so the names
survive coexistence with the upstream Vulkan headers and macro-heavy
libraries such as the Vulkan Memory Allocator. Either way a call compiles
to one load from the global context and an indirect call — there is no
per-call presence check, so calling a function the context did not load
jumps through a null pointer. Check the presence flags first (§3).
Every loader entry point comes in two forms: the plain form operating on
the global context, and a *Context form taking an explicit
Gloam*Context * for applications juggling several contexts at once
(gloamLoadGLContext(&ctx, ...), gloamVulkanLoadInstanceContext(&ctx, ...), ...). Dispatch macros always route through the global context;
with an explicit context you call the members directly
(ctx.Clear(...)).
Loading writes to the context are plain unsynchronized stores. Do the load phases before fanning out to threads (or provide your own happens-before edge); afterwards all dispatch is read-only and freely concurrent.
Each windowing/GL spec loads from a GetProcAddress-style callback you
provide, detecting the version first and extensions second:
typedef void (*GloamAPIProc)(void);
typedef GloamAPIProc (*GloamLoadFunc)(const char *name);
int gloamLoadGL (GloamLoadFunc getProcAddr);
int gloamLoadGLES2(GloamLoadFunc getProcAddr); /* merged gl output */
int gloamLoadEGL (EGLDisplay display, GloamLoadFunc getProcAddr);
int gloamLoadWGL (HDC hdc, GloamLoadFunc getProcAddr);
int gloamLoadGLX (Display *display, int screen, GloamLoadFunc getProcAddr);The return value is the detected version packed as major << 8 | minor
(0x0303 for GL 3.3), or 0 when no usable API was found. The extra
parameters are what each API's extension query needs: EGL queries client
extensions when display is EGL_NO_DISPLAY and display extensions
otherwise (call it again after eglInitialize to pick those up), WGL
needs a device context for wglGetExtensionsStringARB, and GLX needs the
display and screen for glXQueryExtensionsString.
Load calls are additive and may be repeated — e.g. re-run gloamLoadEGL
with a real display after initializing it, or reload after switching
contexts. A matching context must be current on the calling thread for
GL/GLES/WGL, since version and extension detection go through the driver.
Extension detection never string-compares at load time: the loader hashes each advertised extension name with XXH3 and binary-searches a pre-baked sorted hash table. This is the main reason context initialization is fast; it is invisible in the API.
The context records what was detected, exposed through generated macros that read the global context (explicit-context users read the members directly):
if (GLOAM_GL_VERSION_3_3) { /* GL 3.3 core available */ }
if (GLOAM_GL_ES_VERSION_3_0) { /* OpenGL ES 3.0 available */ }
if (GLOAM_GL_KHR_debug) { /* extension detected */ }For GL, EGL, GLX, and WGL, presence means usable — no enable step exists in those APIs. For Vulkan, presence flags are only set for extensions you actually enabled (§4), matching Vulkan's validity rules.
Vulkan loads in phases mirroring the object lifecycle, and most applications should use the enabled-list flow: tell gloam exactly what you enabled, and it loads exactly those pointers — no enumeration calls, no heap allocation, no ambiguous "supported but not enabled" pointers.
gloamVulkanInitialize(NULL); /* phase 0: open libvulkan, global PFNs */
vkCreateInstance(&instanceCreateInfo, NULL, &instance);
gloamVulkanLoadInstance(instance, /* phase 1: instance-scope PFNs */
appInfo.apiVersion,
instanceCreateInfo.enabledExtensionCount,
instanceCreateInfo.ppEnabledExtensionNames);
vkCreateDevice(physicalDevice, &deviceCreateInfo, NULL, &device);
gloamVulkanLoadDevice(device, physicalDevice, /* phase 2: device-scope PFNs */
deviceCreateInfo.enabledExtensionCount,
deviceCreateInfo.ppEnabledExtensionNames);
/* ... */
gloamVulkanFinalize(); /* close library, zero context */The version and extension lists are the same values you passed to
VkApplicationInfo / Vk*CreateInfo — no separate bookkeeping. Each
phase returns nonzero on success. Details:
gloamVulkanInitialize(NULL)finds and opens the platform Vulkan library (vulkan-1.dll,libvulkan.so.1,libvulkan.dylib; on macOS it also tries/usr/local/liband/opt/homebrew/libby path, since dyld no longer searches them for bare names); pass your own library handle instead to control the open. If you already have avkGetInstanceProcAddr, skip the library entirely withgloamVulkanInitializeCustom(getInstanceProcAddr).gloamVulkanGetInstanceVersion()wrapsvkEnumerateInstanceVersionafter phase 0 (reporting 1.0 on loaders that predate it), for choosingapiVersionbefore instance creation.- The applied version is capped by what the driver reports, so requesting 1.3 against a 1.2 device loads the 1.2 command set.
- Commands load through the right entry point automatically: gloam infers
global/instance/device scope from each command's first parameter type
and uses
vkGetInstanceProcAddrorvkGetDeviceProcAddraccordingly, so device-scope calls skip the loader trampoline.
Extension flags are scope-exact: gloamVulkanLoadInstance assigns
every instance-scope flag from its list (absent means cleared) and never
touches device-scope flags; gloamVulkanLoadDevice does the reverse.
Reloading a scope can therefore never leave stale flags behind.
Phase 1.5 (rare): some device extensions carry instance-scope query
commands that must be callable before device creation —
vkGetPhysicalDeviceFragmentShadingRatesKHR is the canonical example.
gloamVulkanLoadPhysicalDeviceExtension(name) (singular) and
gloamVulkanLoadPhysicalDeviceExtensions(count, names) (plural) pre-load
those commands between phases 1 and 2. They do not set presence
flags; only gloamVulkanLoadDevice does, once the extension is actually
enabled.
Extension probing. Presence questions — which extensions does this
implementation advertise? — are answered by the probe API without
touching the context. GloamVulkanExtensions is a standalone snapshot
with the same named flags as the context; one enumeration fills it for
every extension the loader knows:
GloamVulkanExtensions exts;
gloamVulkanQueryInstanceExtensions(&exts); /* after phase 0 */
gloamVulkanQueryDeviceExtensions(pd, &exts); /* after phase 1 */
gloamVulkanHashExtensionProperties(n, props, &exts); /* from your own
enumeration; pure */
if (exts.KHR_swapchain) { /* advertised (not necessarily enabled) */ }This makes device selection cheap: probe each candidate once (a single
vkEnumerateDeviceExtensionProperties call answers all extensions the
selection cares about), pick a winner, and hand the winner's snapshot to
gloamVulkanLoadDeviceFromQuery(device, pd, &exts) — a phase-2 variant
that copies the snapshot's device-scope flags instead of hashing an
enabled-name list, so device extensions are never enumerated twice.
(gloamVulkanLoadInstanceFromQuery is the phase-1 counterpart.) On a
context loaded this way the presence flags mean advertised rather than
enabled — the application chooses which semantics it wants.
Discovery mode (generated with --loader) enumerates instead of
being told:
gloamLoaderLoadVulkan(NULL, VK_NULL_HANDLE, VK_NULL_HANDLE);
gloamLoaderLoadVulkan(instance, VK_NULL_HANDLE, VK_NULL_HANDLE);
gloamLoaderLoadVulkan(instance, physical_device, device);
gloamLoaderUnloadVulkan();Each call loads what the passed handles allow, calling
vkEnumerate*ExtensionProperties to detect extensions; each scope is
enumerated once and cached, and passing a different physical device
than the previous call invalidates and re-queries all device-derived
state (device version, device-scope flags). Two caveats make
enabled-list the recommended flow: detection means supported, not
enabled (using a detected-but-unenabled extension violates Vulkan
validity), and the enumeration calls are expensive — the Khronos loader
rescans ICDs and layers on each one.
With --loader, each spec also gets a convenience layer that opens the
platform library and resolves proc addresses itself, so an application
with no windowing-toolkit GetProcAddress can still load:
int gloamLoaderLoadGL(void); /* dlopen/LoadLibrary + gloamLoadGL */
void gloamLoaderUnloadGL(void); /* close library, zero context */
void gloamLoaderResetGL(void); /* zero context, keep library open */The same triple exists for every stem (...LoadGLES2, ...LoadEGL,
...LoadWGL, ...LoadGLX, ...LoadVulkan), each with a *Context
variant. Load calls are additive and repeatable, like the underlying
gloamLoad* functions; Reset clears detection state without the
library-close/reopen cost, for context switches.
Many commands exist under both a core and an extension spelling
(glDebugMessageCallback / glDebugMessageCallbackKHR). With --alias,
after loading, any pointer present under one spelling is propagated to
the other — bijectively, in both directions — so application code can use
whichever name it prefers regardless of which one the driver exports.
Without --alias each slot holds exactly what the driver returned for
that exact name.
--merge with gl and gles2 produces one loader — one context, one
header, one set of dispatch macros — serving whichever API the current
context speaks. Call gloamLoadGL after creating a desktop context or
gloamLoadGLES2 after an ES context; shared commands occupy the same
slots either way, and extension detection is scoped per-API, so a
GL-only extension is never reported present under GLES. This is the
production pattern for applications that run on desktop GL and on GLES
(natively or via ANGLE) from one binary — see
examples/c/gl-triangle.
Every tree records the command line that produced it in
.gloam/manifest.json, so gloam regen <tree> reproduces it and
gloam regen --fresh <tree> advances it to current spec content. See
manifest.md for the format and
README for the
pinning workflow.