Practical guide
Shader compatibility and portability
shdr translates a compute shader into a C++11 CPU implementation and embeds a portable GPU variant for a graphics driver. Portable sources fit both the CPU adapter and selected GLSL profile. Check compiler compatibility, resource layout, dispatch dimensions and numerical behavior with the application's intended inputs.
Choose a profile
| Backend | Shader language and API | Context requirement |
|---|---|---|
| CPU | C++11 generated adapter | None |
profile::es31 |
GLSL ES 3.10 / OpenGL ES 3.1 | Current ES context |
profile::es32 |
GLSL ES 3.20 / OpenGL ES 3.2 | Current ES context |
profile::gl43 |
GLSL 4.30 / OpenGL 4.3 | Current desktop context |
profile::gl45 |
GLSL 4.50 / OpenGL 4.5 | Current desktop context |
profile::gl46 |
GLSL 4.60 / OpenGL 4.6 | Current desktop context |
A newer context can run an older supported shader profile. Select language features available in the requested profile. Core compute shaders and storage buffers establish the ES 3.1 / GL 4.3 baseline. CPU execution uses C++11 host code and resources. The API requirements are specified by OpenGL ES 3.1 and OpenGL 4.3 core.
Create a headless GPU device
The EGL backend is part of the runtime header. Define SHDR_ENABLE_EGL=1 and link EGL, or link the CMake shdr::egl target. shdr::headless_device(profile) creates a 1-by-1 pbuffer context, preferring a surfaceless display and falling back to EGL's default display. Select an API provided by the graphics driver. backend_unavailable identifies display, configuration, context or entry-point setup failures.
Create and use a device on its context thread. The backend makes its context current for runtime operations and shares initialized display ownership across living contexts. Use headless_device.make_current() for external GL calls. GPU objects retain the context through their device state. Release their final owners on that thread before shutdown, and let the backend manage its EGL display lifetime. See the EGL specification for context and display lifetime rules.
Applications with an existing GL context use device(resolver, profile) and keep that context alive and current. This integration uses the application's GL function resolver. Enable the EGL backend specifically when the library should create the context.
Build a self-contained compute source
Attach native files to an executable or shared module with shdr_target_shaders(target PRIVATE SOURCES shader.cpp). The build frontend converts GLSL resource declarations and expressions into a C++11 translation unit and embeds the portable GPU program. The guarded #include "shdr.h" line integrates the CPU adapter and is removed from the GPU text.
The helper accepts .cpp, .comp and .glsl sources. The selected profile supplies the exported shader version. Provide a self-contained shader with its helper functions in the same source, and choose features supported by both the adapter and that profile.
GPU preparation can assign missing storage-buffer bindings and normalize interface declarations. It also spells out desktop numeric promotions with explicit scalar or vector constructors where ES requires them. Supported conversions cover initializers, arithmetic, comparisons, assignments, ternary branches and by-value helper arguments. Output and inout arguments remain lvalues. The expression evaluation order is retained.
Global constants and local compile-time constants keep their qualifiers. A local
const initialized from a runtime value loses that qualifier in the GPU variant,
because GLSL ES requires a constant expression there. A compatibility helper
handles integer boolean-selector mix on ES 3.1 and GL 4.3. Ordinary floating
mix still uses the shader builtin. Inspect kernel::glsl(profile) when checking
the final program passed to a driver.
Mutable globals in a native shader are private to each invocation. Initialized globals receive fresh CPU state for each call to main(), and helper functions share that invocation's state. CPU initializers can read bound uniforms. For the ES 3.1 baseline, use constant global initializers and place uniform-dependent initialization inside main(). Extension-enabled sources follow their applicable initializer rules. Initialize every private variable before reading it. Use bound buffers or images for state shared across invocations or dispatches. Uniforms and resource bindings retain their application-supplied values. See GLSL ES 3.10 section 4.3 for initializer rules.
A manually authored SHDR_BEGIN class is an ordinary C++ object. Put mutable
per-invocation variables inside SHDR_MAIN when using that declaration interface
for a dual CPU/GPU shader.
Factories use the file stem plus _kernel. Bind declared array/image names with
kernel.bind, and assign uniform names with kernel.set. Uniform types must
match exactly: C++ unsigned corresponds to shader uint, and C++ float
corresponds to shader float.
Attach adapters directly to the final executable or loaded shared module for automatic factory registration. When using a static archive, configure the application linker to retain registration objects.
Match interfaces and memory layout
Portable storage-buffer elements are 32-bit float, int or uint, and matching two- or four-component vectors. Their array strides match std430. Use three-component vectors, matrices and booleans in supported local math or uniforms. Convert structured data into supported scalar or vector arrays before binding it to storage ports.
| Source construct | Adapter support |
|---|---|
| Runtime-sized or fixed-size single-array buffer block | Supported with matching element layout |
layout(std430) on a block or as a global buffer default |
Supported |
| Explicit or omitted buffer binding | Supported, omitted bindings are assigned and added to GPU declarations |
| Literal, object-macro, or constant-integer layout expression | Supported integer arithmetic |
| Individual uniform with optional explicit location | Supported |
| Fixed-size private/local arrays and array constructors | Supported through glsl::array<T,N> |
| Fixed-size array function parameters | Supported with matching declared size |
| Scalar conversion from vector, swizzle, or matrix | Supported using the first component |
| Vulkan buffer descriptor set zero | Imported to ordinary OpenGL buffer bindings |
| Specialization constant with an initializer | Imported using its declared default value |
| Supported push-constant block fields | Exposed as named uniforms such as params.count |
A fixed-size shader buffer requires at least its declared element count in the bound resource. The shader length remains that declared count when the resource has extra capacity. Native array .length() expressions return GLSL's signed int on both backends, including boundary arithmetic and unsigned comparisons. The build assigns GPU binding numbers for the baseline GL interfaces.
Single-field storage blocks can be uninstanced, or accessed through a block
instance. For a declaration buffer B { float values[]; } data;, bind the port
with "data.values". Allocate enough resource elements for every index the
shader can access. Resource sizes, dimensions, and bound shader interfaces must
agree.
Storage images use one or four interleaved 32-bit scalar components: r32f, r32i, r32ui, rgba32f, rgba32i and rgba32ui. Match the GLSL image type to that format. Portable RGBA declarations use separate readonly and writeonly ports. Single-component r32 images also support read/write access. Supply image values in their native scalar representation.
Use individual scalar, vector or float-matrix uniforms. Supported Vulkan push-constant members become independently set named GL uniforms. Descriptor set zero maps to OpenGL resource bindings, and specialization constants use their declared default values. This interface supplies OpenGL resource semantics for the imported source.
Use supported storage-buffer and storage-image ports with individual uniforms. Adapt sampler reads, uniform blocks or arrays, structured storage elements and graphics-stage interfaces into that compute interface before importing them. Match every bound array to its declared scalar or vector element layout.
Keep invocations independent
Each invocation should write its own output elements and read stable input. Use separate resources for neighborhood filters and separate dispatches for producer/consumer dependencies. An in-place image blur is a GPU data race even when a serial CPU loop happens to produce a plausible image.
The CPU backend runs independent invocations serially. Portable kernels use local state, supported math and explicit storage ports. Express communication through ordered dispatches and bound resources. Adapt workgroup shared storage, barriers, atomics, subgroup operations, derivatives, sampling and graphics stages to this independent-invocation interface before adding a source to a dual-backend target.
Use function-local mutable variables for invocation-local state. Native helper functions, scalar/vector math, swizzles, loops and branches form the common subset. Give each destination component exactly one position in a writable swizzle.
Dispatch within device limits
A shader's layout(local_size_x=..., local_size_y=..., local_size_z=...) in;
defines its local group. extent(gx,gy,gz) gives group counts. Each dimension
executes local_size * group_count invocations. Round up the group count and
keep an explicit bounds check for incomplete groups.
Small groups such as (64,1,1) or (8,8,1) fit broad ES 3.1 devices. Guaranteed
ES 3.1 minima include 128 invocations per group, axis limits of at least
(128,128,64), four storage blocks, and four image uniforms per compute shader.
Larger groups and resource counts require checking the actual implementation.
The ordinary compute builtins gl_GlobalInvocationID, gl_LocalInvocationID,
gl_WorkGroupID, gl_NumWorkGroups, gl_WorkGroupSize, and
gl_LocalInvocationIndex have their standard meanings.
The runtime inserts GL memory barriers between its dispatches and synchronizes
resource contents at CPU/GPU transitions. A memory barrier establishes visibility
for later operations. Host readback or device.synchronize() establishes a
completion wait. Application-issued GL commands need barriers matching their
own consumers and modification notifications for shared resources. See the
Khronos memory model guide.
pipeline.submit(device) queues GPU stages and returns a completion fence. executor.submit(pipeline) snapshots CPU work for a bounded worker pool. Use independent writable resources for concurrent jobs. The asynchronous guide explains preparation, polling, queue limits and explicit shutdown.
Compare numbers appropriately
The portable scalar types are 32-bit float, int and uint. CPU and GPU floating-point contraction, rounding, denormals, exceptional values and transcendental approximations can differ. Choose absolute and relative tolerances for the algorithm. Keep signed results representable, use defined shift operands and counts, use nonzero divisors and keep indices within initialized storage.
Use float for the common ES 3.1/3.2 math subset. Host math also provides double-precision types. For fma, select ES 3.2, a supporting desktop profile or an applicable extension. See the GLSL ES 3.20 specification. The build frontend supplies integer boolean-selector mix through a compatibility helper for the baseline portable GPU profiles.
Validate an imported shader
Build the CPU adapter, inspect diagnostics and run a small input with known reference output. Check each stage before composing a larger pipeline. Export kernel.glsl(profile) for an independent GLSL compiler check, then execute the driver path and compare resource layout, dispatch coverage and numerical results.
The API reference documents runtime functions and math helpers. The imaging guide lists kernels with explicit formats, border rules and dispatch sizes. The public-source report records 664 inputs, including 443 accepted by reference compilers and 258 with strict C++11 adapters and all five GPU profiles. Check imported algorithms against numerical references and their intended inputs.