Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.4.7](https://github.com/tycho/gloam/compare/0.4.6...0.4.7) - 2026-04-01

### Fixed

- *(vulkan)* ensure we don't leak handle on double-gloamVulkanInitialize

### Other

- cargo dependency update
- update bundled XML specs and headers

## [0.4.6](https://github.com/tycho/gloam/compare/0.4.5...0.4.6) - 2026-03-27

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "gloam"
version = "0.4.6"
version = "0.4.7"
edition = "2024"
description = "Loader generator for Vulkan, OpenGL, OpenGL ES, EGL, GLX, and WGL"
license = "MIT OR Apache-2.0"
Expand Down
137 changes: 76 additions & 61 deletions src/generator/c/templates/header.h.j2
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#define GLOAM_{{ guard }}
{# ---- System-header conflict guards ---------------------------------------
These prevent including both the platform/system GL or Vulkan headers and
this gloam header, which would cause duplicate declarations. Using
this gloam header, which would cause duplicate declarations. Using
reserved identifiers (double-underscore prefixed) requires suppressing the
clang warning for that specific pragma block. #}
{%- if fs.spec_name in ["gl", "gles1", "gles2", "glcore"] %}
Expand Down Expand Up @@ -238,10 +238,11 @@ extern "C" {
#endif

/* Calling convention for GL callback function pointers (e.g. debug callbacks,
blob cache functions). Piggybacks on APIENTRY if already defined so that
code which defines APIENTRY before including this header gets consistent
behaviour. Guard against redefinition so multiple gloam headers included
in the same translation unit don't conflict. */
* blob cache functions). Piggybacks on APIENTRY if already defined so that
* code which defines APIENTRY before including this header gets consistent
* behaviour. Guard against redefinition so multiple gloam headers included
* in the same translation unit don't conflict.
*/
#ifndef GLOAM_API_PTR
# ifdef APIENTRY
# define GLOAM_API_PTR APIENTRY
Expand All @@ -265,18 +266,20 @@ struct _cl_event;
{%- if not (fs.is_vulkan and external_headers) -%}
{%- if fs.features | length > 0 -%}
/* ---- Version feature guards ----------------------------------------------
These mirror the upstream vulkan_core.h / gl.h definitions so that code
guarded by e.g. #ifdef GL_VERSION_3_3 compiles correctly against this
header. */
* These mirror the upstream vulkan_core.h / gl.h definitions so that code
* guarded by e.g. #ifdef GL_VERSION_3_3 compiles correctly against this
* header.
*/
{% for feat in fs.features -%}
#define {{ feat.full_name }} 1
{% endfor -%}
{% endif -%}
{%- if fs.ext_guard_groups | length > 0 %}
/* ---- Extension compile-time guards ---------------------------------------
These mirror the definitions in standard glext.h/gl2ext.h/eglext.h so
that code guarded by e.g. #ifdef GL_ARB_draw_indirect compiles correctly
against this header. */
* These mirror the definitions in standard glext.h/gl2ext.h/eglext.h so
* that code guarded by e.g. #ifdef GL_ARB_draw_indirect compiles correctly
* against this header.
*/
{% for group in fs.ext_guard_groups -%}
{{- u.protect_begin(group.protect) -}}
{% for ext in group.items -%}
Expand Down Expand Up @@ -330,8 +333,9 @@ typedef enum {{ group.name }} {
{% endfor -%}
{% endif %}
/* ---- Types ----------------------------------------------------------------
Emitted in topological dependency order. Consecutive types sharing the
same platform guard are coalesced into a single #ifdef/#endif block. */
* Emitted in topological dependency order. Consecutive types sharing the
* same platform guard are coalesced into a single #ifdef/#endif block.
*/
{% for group in fs.type_groups -%}
{% if group.protect | length > 0 -%}
#if defined({{ group.protect | join(") && defined(") }})
Expand Down Expand Up @@ -369,14 +373,15 @@ typedef {{ cmd.return_type }} (VKAPI_PTR *{{ cmd.pfn_type }})({{ cmd.params_str
{%- endif %}

/* ---- Context struct ------------------------------------------------------
Three anonymous unions give both indexed (array) and named (struct member)
access to the same memory, at zero runtime cost.

featArray / extArray: unsigned char flags, one per feature / extension.
pfnArray: function pointer slots, one per command.

Anonymous structs inside the unions are a C11 / GCC extension; they are
universally supported on our target compilers. */
* Three anonymous unions give both indexed (array) and named (struct member)
* access to the same memory, at zero runtime cost.
*
* featArray / extArray: unsigned char flags, one per feature / extension.
* pfnArray: function pointer slots, one per command.
*
* Anonymous structs inside the unions are a C11 / GCC extension; they are
* universally supported on our target compilers.
*/
typedef struct {{ fs.context_name }} {
union {
unsigned char featArray[{{ fs.features | length }}];
Expand Down Expand Up @@ -418,22 +423,27 @@ typedef struct {{ fs.context_name }} {
{%- endfor %}
};
};
/* Built-in loader library handle. Set by gloamVulkanInitialize*Context /
gloamLoaderLoad*Context when it opens the platform library. If the
caller pre-populates this field, the open is skipped and ownership is
NOT taken (gloam will not close it). Present on all context types so
user code can use a single field name regardless of API. */

/* Built-in loader library handle. Set by gloamVulkanInitialize*Context /
* gloamLoaderLoad*Context when it opens the platform library. If the
* caller pre-populates this field, the open is skipped and ownership is
* NOT taken (gloam will not close it). Present on all context types so
* user code can use a single field name regardless of API.
*/
void *gloam_loader_handle;

/* Non-zero if gloam opened the library handle itself and is responsible
for closing it in gloamVulkanFinalize / gloamLoaderUnload. */
* for closing it in gloamVulkanFinalize / gloamLoaderUnload.
*/
uint8_t gloam_loader_owns_handle;
{% if fs.is_vulkan %}
/* The last VkInstance this context loaded entry points from */
VkInstance vk_loaded_instance;
/* The last VkDevice this context loaded entry points from */
VkDevice vk_loaded_device;
/* Vulkan discovery-path metadata — used by gloamLoaderLoadVulkanContext to make
repeated calls additive without re-enumerating already-cached scopes. */
* repeated calls additive without re-enumerating already-cached scopes.
*/
uint32_t vk_instance_version; /* cached EnumerateInstanceVersion result */
uint32_t vk_device_version; /* cached GetPhysicalDeviceProperties.apiVersion */
uint8_t vk_found_instance_exts; /* set once instance extensions enumerated */
Expand All @@ -442,11 +452,13 @@ typedef struct {{ fs.context_name }} {
} {{ fs.context_name }};

/* Global context instance — a value, not a pointer, so the compiler knows
its address is fixed and does not re-load it on every access. */
* its address is fixed and does not re-load it on every access.
*/
extern {{ fs.context_name }} gloam_{{ fs.spec_name }}_context;

/* ---- Feature presence macros --------------------------------------------
Test whether a versioned feature was detected at load time. */
* Test whether a versioned feature was detected at load time.
*/
{%- for feat in fs.features %}
#define GLOAM_{{ feat.full_name }} (gloam_{{ fs.spec_name }}_context.{{ feat.short_name }})
{%- endfor %}
Expand All @@ -463,7 +475,7 @@ extern {{ fs.context_name }} gloam_{{ fs.spec_name }}_context;
/* ---- Dispatch ------------------------------------------------------------ */
{% if fs.is_vulkan -%}
{# Vulkan: force-inlined function wrappers that dispatch through the global
context struct. Unlike #define macros, these don't pollute the preprocessor
context struct. Unlike #define macros, these don't pollute the preprocessor
namespace (avoids collisions with VMA, etc.). #}
{% for group in fs.cmd_pfn_groups -%}
{% if group.protect | length > 0 -%}
Expand Down Expand Up @@ -512,7 +524,8 @@ GLOAM_FORCE_INLINE {{ cmd.return_type }} {{ cmd.name }}({{ cmd.params_str }}) {
#ifndef GLOAM_DEFINED_CALLBACK_TYPES_
#define GLOAM_DEFINED_CALLBACK_TYPES_
/* Opaque function pointer type — the common return type for all load
callbacks. Callers cast to the specific PFN type they need. */
* callbacks. Callers cast to the specific PFN type they need.
*/
typedef void (*GloamAPIProc)(void);

/* Load function pointer type (GL / EGL / GLX / WGL). */
Expand All @@ -533,31 +546,32 @@ int gloamLoad{{ api | api_display }}Context({{ u.ctx_arg(', ') }}HDC hdc, GloamL
int gloamLoad{{ api | api_display }}(HDC hdc, GloamLoadFunc getProcAddr);
{%- elif fs.spec_name == "vk" %}
/* ---- Vulkan enabled API (Volk-like) ----------------------------------------
Phased loading: Initialize → LoadInstance → LoadDevice.
The caller owns extension discovery and tells gloam what was enabled.

Phase 0 — Initialize: open libvulkan and load the handful of Global-scope
PFNs needed to create an instance (vkCreateInstance, vkEnumerateInstance*).
If library_handle is non-NULL, use it without taking ownership; if NULL,
dlopen the platform default and take ownership.

Phase 1 — LoadInstance: load Global + Instance-scope PFNs for core features
and enabled instance extensions. Set featArray from api_version
(VK_MAKE_API_VERSION or VK_API_VERSION_x_y). Set extArray for enabled
instance extensions. Resolve aliases.

Phase 1.5 (optional) — LoadPhysicalDeviceExtension(s): pre-load
Instance-scope PFNs for device extensions the application wants to query
before creating a VkDevice (e.g. vkGetPhysicalDeviceFragmentShadingRatesKHR
from VK_KHR_fragment_shading_rate). Does NOT set extArray.

Phase 2 — LoadDevice: load PFNs for enabled device extensions (all scopes).
Device-scope commands use vkGetDeviceProcAddr for the fast path; Instance-
scope commands in device extensions use vkGetInstanceProcAddr. Update
featArray from the device's api_version. Set extArray for enabled device
extensions. Resolve aliases.

Finalize: close library handle if gloam owns it, zero the context. */
* Phased loading: Initialize → LoadInstance → LoadDevice.
* The caller owns extension discovery and tells gloam what was enabled.
*
* Phase 0 — Initialize: open libvulkan and load the handful of Global-scope
* PFNs needed to create an instance (vkCreateInstance, vkEnumerateInstance*).
* If library_handle is non-NULL, use it without taking ownership; if NULL,
* dlopen the platform default and take ownership.
*
* Phase 1 — LoadInstance: load Global + Instance-scope PFNs for core features
* and enabled instance extensions. Set featArray from api_version
* (VK_MAKE_API_VERSION or VK_API_VERSION_x_y). Set extArray for enabled
* instance extensions. Resolve aliases.
*
* Phase 1.5 (optional) — LoadPhysicalDeviceExtension(s): pre-load
* Instance-scope PFNs for device extensions the application wants to query
* before creating a VkDevice (e.g. vkGetPhysicalDeviceFragmentShadingRatesKHR
* from VK_KHR_fragment_shading_rate). Does NOT set extArray.
*
* Phase 2 — LoadDevice: load PFNs for enabled device extensions (all scopes).
* Device-scope commands use vkGetDeviceProcAddr for the fast path; Instance-
* scope commands in device extensions use vkGetInstanceProcAddr. Update
* featArray from the device's api_version. Set extArray for enabled device
* extensions. Resolve aliases.
*
* Finalize: close library handle if gloam owns it, zero the context.
*/
{% if loader -%}
int gloamVulkanInitializeContext({{ u.ctx_arg(', ') }}void *library_handle);
int gloamVulkanInitialize(void *library_handle);
Expand Down Expand Up @@ -585,10 +599,11 @@ void gloamVulkanFinalize(void);

{%- if loader -%}
/* Built-in loader: opens the platform library if needed and calls the
appropriate load function for you. Non-Vulkan loaders call the detection-
based gloamLoad* functions. Vulkan loaders handle all extension detection
and PFN loading internally.
Each Load function may be called multiple times (additive). */
* appropriate load function for you. Non-Vulkan loaders call the detection-
* based gloamLoad* functions. Vulkan loaders handle all extension detection
* and PFN loading internally.
* Each Load function may be called multiple times (additive).
*/
{%- for api in fs.apis %}
{%- if fs.spec_name in ["gl", "gles1", "gles2", "glcore"] %}
int gloamLoaderLoad{{ api | api_display }}Context({{ u.ctx_arg() }});
Expand Down
11 changes: 7 additions & 4 deletions src/generator/c/templates/impl_util.j2
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
#endif

/* GLOAM_NO_INLINE — suppress inlining on functions that should stay out of
the hot path (sort, hash, etc.) to avoid code bloat at call sites. */
* the hot path (sort, hash, etc.) to avoid code bloat at call sites.
*/
#ifdef _MSC_VER
# define GLOAM_NO_INLINE __declspec(noinline)
#else
Expand All @@ -20,16 +21,18 @@
#define GLOAM_UNUSED(x) ((void)(x))

/* Contiguous run of pfnArray slots belonging to one feature or extension.
Used by the range-based PFN loading loop. */
* Used by the range-based PFN loading loop.
*/
typedef struct {
uint16_t extension; /* index into featArray or extArray */
uint16_t extension; /* index into featArray or extArray */
uint16_t start; /* first pfnArray index in this run */
uint16_t count; /* number of consecutive slots */
} GloamPfnRange_t;

{% if alias -%}
/* Bijective alias pair: if canonical slot is null but secondary is loaded
(or vice versa), the loaded pointer is propagated to both slots. */
* (or vice versa), the loaded pointer is propagated to both slots.
*/
typedef struct {
uint16_t first; /* canonical (shortest name) pfnArray index */
uint16_t second; /* alias pfnArray index */
Expand Down
36 changes: 22 additions & 14 deletions src/generator/c/templates/loader.j2
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
{% import "utils.j2" as u with context %}
{#- Built-in platform loader. Only included when --loader is passed. -#}
{#- Built-in platform loader. Only included when --loader is passed. -#}
{% include "library.j2" %}
{% if fs.spec_name in ["gl", "gles1", "gles2", "glcore"] %}
/* ---- GL / GLES built-in loader ----------------------------------------- */

/* Transient userptr: set for the duration of a gloamLoaderLoad*Context call.
Holds the library handle and — for desktop GL — the correctly-typed
platform proc-addr function pointer so that the calling convention is
always right. Only valid between entry and return; never accessed
concurrently. */
* Holds the library handle and — for desktop GL — the correctly-typed
* platform proc-addr function pointer so that the calling convention is
* always right. Only valid between entry and return; never accessed
* concurrently.
*/
struct gloam_gl_load_userptr {
void *handle;
#if defined(GLOAM_PLATFORM_WINDOWS)
/* wglGetProcAddress is WINAPI (__stdcall); must not be called through a
plain cdecl function pointer. */
* plain cdecl function pointer.
*/
GloamAPIProc (WINAPI *wgl_get_proc)(const char *);
#elif !defined(__APPLE__) && !defined(__HAIKU__)
GloamAPIProc (*glx_get_proc)(const char *);
Expand All @@ -22,7 +24,8 @@ struct gloam_gl_load_userptr {
static struct gloam_gl_load_userptr gloam_gl_load_state;

/* GL desktop adapter — matches GloamLoadFunc (__cdecl), dispatches through
the correctly-typed platform proc-addr pointer stored in the userptr. */
* the correctly-typed platform proc-addr pointer stored in the userptr.
*/
static GloamAPIProc gloam_gl_get_proc(const char *name)
{
GloamAPIProc result = NULL;
Expand All @@ -46,7 +49,8 @@ static GloamAPIProc gloam_gl_get_proc(const char *name)
}

/* GLES adapter: all symbols (including extensions) are exported directly from
the GLES library so plain dlsym is sufficient — no platform indirection. */
* the GLES library so plain dlsym is sufficient — no platform indirection.
*/
static GloamAPIProc gloam_gles_get_proc(const char *name)
{
struct gloam_gl_load_userptr *u = &gloam_gl_load_state;
Expand Down Expand Up @@ -164,8 +168,9 @@ static const char * const gloam_egl_lib_names[] = {
};

/* eglGetProcAddress has its own calling convention (declared in EGL headers as
PFNEGLGETPROCADDRESSPROC). Store it with the correct type so the call always
uses the right convention, even when the default differs (e.g. Win32). */
* PFNEGLGETPROCADDRESSPROC). Store it with the correct type so the call always
* uses the right convention, even when the default differs (e.g. Win32).
*/
struct gloam_egl_load_userptr {
void *handle;
PFNEGLGETPROCADDRESSPROC get_proc_address;
Expand Down Expand Up @@ -261,7 +266,8 @@ static const char * const gloam_glx_lib_names[] = {
};

/* glXGetProcAddressARB returns __GLXextFuncPtr = void(*)(void); store with the
correct function-pointer type so the call goes through the right ABI. */
* correct function-pointer type so the call goes through the right ABI.
*/
struct gloam_glx_load_userptr {
void *handle;
GloamAPIProc (*get_proc_address)(const char *);
Expand Down Expand Up @@ -353,7 +359,8 @@ void gloamLoaderReset{{ api | api_display }}(void)
static const char * const gloam_wgl_lib_names[] = { "opengl32.dll" };

/* wglGetProcAddress is WINAPI (__stdcall); must not be stored or called through
a plain cdecl pointer. Store with the correct type in a userptr struct. */
* a plain cdecl pointer. Store with the correct type in a userptr struct.
*/
struct gloam_wgl_load_userptr {
void *handle;
GloamAPIProc (WINAPI *wgl_get_proc)(const char *);
Expand Down Expand Up @@ -447,8 +454,9 @@ void gloamLoaderReset{{ api | api_display }}(void)
*
* Opens the Vulkan library into context->gloam_loader_handle if it is not
* already set, stores vkGetInstanceProcAddr in the context, then delegates to
* gloamVulkanDiscoverContext. Follows the same additive multi-call contract
* as the underlying discover function. */
* gloamVulkanDiscoverContext. Follows the same additive multi-call contract
* as the underlying discover function.
*/
int gloamLoaderLoadVulkanContext({{ u.ctx_arg(', ') }}VkInstance instance, VkPhysicalDevice physical_device, VkDevice device)
{
int did_open = 0;
Expand Down
Loading