Version 1.0.0

One shader. CPU and GPU pipelines.

Write a compute shader in GLSL, build it once, then run it on the CPU, a supported OpenGL GPU, or both. The C++11 runtime connects typed data to named shader ports. The build helper creates the CPU adapter and embeds a portable GPU program in the application.

A single runtime header and an automatic shader build. shdr.h provides the C++11 runtime. Native GLSL builds use the included CMake helper and Python 3.8+. Deployed applications use embedded CPU and GPU programs. CPU execution uses host resources. The optional EGL backend creates headless GPU devices.

Choose an execution path What you need
CPU A C++11 compiler
GPU OpenGL ES 3.1/3.2 or desktop OpenGL 4.3+ and a compatible driver
Headless GPU EGL development files at build time and an EGL-capable driver
Native shader build CMake 3.12+ and Python 3.8+

Understand the pieces

A shader computes one invocation at a time. A kernel is a shader instance with its own resource bindings and uniform values. A buffer or image holds data shared by stages. A dispatch gives a kernel its workgroup count. A pipeline runs those dispatches in order.

For example, a first kernel can scale an input array into an intermediate buffer. A second kernel reads that buffer and writes the final result. Connecting both kernels to the same intermediate resource makes the dependency explicit. The runtime handles transfers when execution moves between CPU and GPU.

Build and try a pipeline

# Configure the example build directory.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
# Compile the configured targets and embedded shaders.
cmake --build build -j
# Run this example on the CPU.
./build/example_pipeline

This runs an affine transform, a clamp, and a reduction on the CPU. The same executable can run the same shaders on a GPU:

# Run this example on the requested headless GPU profile.
./build/example_pipeline --gpu es31

GPU examples accept es31, es32, gl43, gl45, and gl46. Choose a profile supported by the driver. If EGL development files are unavailable, CMake builds the CPU examples. -DSHDR_BUILD_GPU=OFF explicitly selects a CPU-only build. SHDR_BUILD_EXAMPLES and SHDR_BUILD_TESTS control those respective targets.

Your first linked shaders

Create a quickstart directory in the checkout and add these three files. The application will run two instances of one shader, passing the first result to the second through a shared resource.

quickstart/scale.cpp is a native compute shader:

#version 310 es // Declare the original shader language version for standalone GLSL compilation.
#ifdef __cplusplus // Expose the runtime header only to the generated C++ adapter.
#include "shdr.h" // Provide portable math types and kernel registration to C++.
#endif // Keep the C++ header inactive for the GLSL compiler.
precision highp float; // Use high precision for floating-point arithmetic on OpenGL ES.
precision highp int; // Use high precision for integer indices on OpenGL ES.

layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; // Launch 64 independent invocations in each workgroup.
layout(std430, binding = 0) readonly buffer Inputs { float input_values[]; }; // Read the input array from storage-buffer binding zero.
layout(std430, binding = 1) writeonly buffer Outputs { float output_values[]; }; // Write the result array to storage-buffer binding one.
uniform uint count; // Limit processing to the allocated element count.
uniform float gain; // Set the multiplier independently for each kernel instance.

void main() { // Compute the output belonging to this invocation.
    uint i = gl_GlobalInvocationID.x; // Find this invocation's element index.
    if (i < count) // Ignore padded invocations beyond the input length.
        output_values[i] = input_values[i] * gain; // Scale one input value into its matching output position.
} // Finish this independent element computation.

quickstart/main.cpp creates the resources and connects the stages:

#include "shdr.h" // Use resources, factories and dispatches from the single runtime header.
#include <iostream> // Print the final host-visible values.

int main() { // Build and run two linked compute stages.
    shdr::buffer<float> input = {1.f, 2.f, 3.f, 4.f}; // Start with four host-resident input values.
    shdr::buffer<float> temporary(4), output(4); // Allocate a buffer between stages and one for the final result.
    auto first = shdr::make_kernel("scale_kernel"); // Create the first instance of the embedded scale shader.
    auto second = shdr::make_kernel("scale_kernel"); // Create an independent instance of the same shader.

    first->bind("input_values", input); // Connect the original input to the first stage.
    first->bind("output_values", temporary); // Connect the first stage's output to the intermediate buffer.
    first->set("count", unsigned(input.size())); // Set the number of valid elements for the first stage.
    first->set("gain", 2.f); // Multiply every input value by two.

    second->bind("input_values", temporary); // Read the first stage's results in the second stage.
    second->bind("output_values", output); // Write the second stage's results into the final buffer.
    second->set("count", unsigned(input.size())); // Set the valid element count for the second stage.
    second->set("gain", 3.f); // Multiply the intermediate values by three.

    const shdr::extent groups(1, 1, 1); // One workgroup covers all four values.
    auto pipeline = shdr::dispatch(*first, groups) // Dispatch the first kernel before its consumer.
                 >> shdr::dispatch(*second, groups); // Append the second kernel to the ordered pipeline.
    pipeline.run(); // Execute both stages on the CPU.

    const shdr::buffer<float>& result = output; // Use a const view when reading the result.
    for (std::size_t i = 0; i < result.size(); ++i) // Visit every valid output value.
        std::cout << result.data()[i] << ' '; // Print each completed pipeline result.
    std::cout << '\n'; // 6 12 18 24
} // Release kernels and buffers after their final use.

quickstart/CMakeLists.txt attaches the shader to the executable:

cmake_minimum_required(VERSION 3.12) # Require the CMake version supported by the build helper.
project(shdr_quickstart LANGUAGES CXX) # Configure a project containing C++ translation units.
find_package(Threads REQUIRED) # Find the platform thread library used by the runtime.
add_executable(quickstart main.cpp) # Build the small host application.
target_compile_features(quickstart PRIVATE cxx_std_11) # Compile the host and generated adapter as standard C++11.
target_link_libraries(quickstart PRIVATE Threads::Threads) # Link the runtime thread support on every supported platform.
target_include_directories(quickstart PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..") # Find shdr.h in the checkout containing this quickstart directory.
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/shdr.cmake") # Load the automatic CPU-adapter and GPU-embedding helper.
shdr_target_shaders(quickstart PRIVATE SOURCES scale.cpp) # Compile and register scale.cpp as part of the executable.

Build and run from the checkout:

# Configure the example build directory.
cmake -S quickstart -B quickstart/build
# Compile the configured targets and embedded shaders.
cmake --build quickstart/build -j
# Run this example on the CPU.
./quickstart/build/quickstart

The filename scale.cpp registers the factory scale_kernel. Each factory call returns an independent kernel object. bind connects typed buffer storage. set assigns a uniform of its declared type. In C++, use unsigned for shader uint values and f suffixes for float uniform values.

A dispatch extent counts workgroups. With 64 local invocations and extent(1), the shader executes 64 invocations. Its bounds check limits buffer access to the four valid elements. For count elements, dispatch extent((count+63)/64).

The build frontend translates native GLSL interfaces and swizzles into C++ grammar. GCC compiles the C++11 adapter, and the GL driver compiles the embedded GLSL. Deploy the executable together with its linked libraries.

Run on a GPU or combine backends

Enable the optional EGL backend to create a headless compute context. For an installed package, link the shdr::egl CMake target. It enables SHDR_ENABLE_EGL=1 and supplies the EGL headers and link library.

target_link_libraries(app PRIVATE shdr::egl) # Enable the optional EGL backend and link its dependencies.

The application uses the same kernels, buffers and pipeline:

#include "shdr.h" // SHDR_ENABLE_EGL=1 is supplied by the build target.

// Inside main(), after constructing the pipeline:
shdr::headless_device gpu(shdr::profile::es31); // Create a headless context using the ES 3.1 profile.
pipeline.run(gpu); // Execute the same ordered pipeline on that GPU device.

For a manual build, define SHDR_ENABLE_EGL=1 before including the header and link EGL. The backend uses a 1-by-1 pbuffer, prefers a surfaceless EGL display, and otherwise tries EGL's default display. backend_unavailable reports an unavailable driver or requested context. Shader and dispatch failures use shdr::error and retain their diagnostics.

Applications with an existing GL context can make it current and construct shdr::device(resolve_gl, profile) with their function resolver. This path integrates with the application's context management.

Automatic stages use the CPU with pipeline.run() and the GPU with pipeline.run(gpu). A stage may select its own backend:

auto mixed = shdr::dispatch(*first, groups, shdr::backend::gpu) // Choose GPU execution explicitly for the first stage.
           >> shdr::dispatch(*second, groups, shdr::backend::cpu); // Choose CPU execution explicitly for its consumer.
mixed.run(gpu); // Run the mixed pipeline and transfer intermediate data when needed.

A shared resource links the stages and transfers its latest contents when a CPU/GPU transition needs them. GPU stages insert memory barriers. Host access to a GPU-produced resource waits for its data. device.synchronize() waits for completion and keeps the data resident on its current backend.

A pipeline retains references to its kernels. Keep those objects alive until execution finishes. Rebind resources or update uniforms before another run to reuse the pipeline. pipeline += dispatch(...) appends a stage, and pipeline += another_pipeline appends an existing sequence. Two pipelines can also be linked with first_pipeline >> second_pipeline. Their kernels retain the same lifetime requirements.

Submit work and poll completion

Compile and upload a GPU pipeline before a processing loop, then enqueue work and poll its fence:

pipeline.prepare(gpu); // Compile programs and upload resources before the submission loop.
auto completion = pipeline.submit(gpu); // Enqueue GPU work and retain its completion handle.
// Perform other application work on this thread.
if (completion.ready()) { // Query GPU completion with a zero-timeout fence poll.
 // GPU work has completed. This resource slot can be read or reused.
} // Continue application work while the submission is pending.

ready() polls completion. wait(duration) supplies a timeout budget to the driver, and wait() waits until completion. GPU submission accepts GPU stages. Schedule host readbacks at deliberate transfer points and give each in-flight job its own resource slot.

For background CPU processing, shdr::executor workers(2, 8) starts two workers with up to eight pending jobs. workers.submit(pipeline) returns a std::future<void> and snapshots shader uniforms and bindings. A full queue throws immediately. Inputs must already be available on the host, and concurrent jobs must have distinct writable resources. Call workers.shutdown() at an explicit shutdown point. It finishes accepted jobs and joins the workers.

The asynchronous execution guide explains resource lifetimes, polling, queue limits and latency budgets. Plan for the variable latency of allocation, operating-system scheduling and graphics-driver calls.

Continue with a complete example

Buffers and execution

Start with three linked stages, switch one stage to the CPU, or load a stage from a dynamic library.

Images and vision

Build Bayer, convolution, blur, edge and corner pipelines from reusable kernels.

Signals

Run batched FFTs and connect frequency masks, FIR filters or persistent biquad streams.

Event-camera streams

Decode compact EVT3 data and link normal optical flow while keeping intermediate events on the selected backend.

Neural models

Train small models with native shaders and run a seven-stage LeNet-style digit classifier.

Public inputs and results

Try prepared photographs, ECG samples, event streams and MNIST digits. Learn what each check establishes.

Import a compute shader

Keep the shader body in a .cpp, .comp or .glsl file. Add the guarded shdr.h include shown above and attach the file with shdr_target_shaders. The file stem becomes the kernel factory name with _kernel appended. Bind its declared resource names and set its uniforms in C++.

The build frontend adapts supported compute interfaces and expressions into standard C++11 and a portable GPU variant. It selects the shader version for the requested profile. Dual-backend shaders use independent compute invocations, supported storage-buffer or image ports, uniforms and portable math. Express coordination as ordered dispatches with shared buffers.

The portability guide explains interface layouts, invocation state, numeric behavior and device limits. The public-shader collection records compiler results for 664 pinned inputs. Validate an imported algorithm with its intended data, dispatch geometry and numerical references.

Keep data and lifetimes clear

Resources and bindings explains when data is uploaded or downloaded, how resource copies share storage, and how to connect an existing GL buffer or texture. Asynchronous execution explains completion handles and reusable resource slots. The API reference documents the public runtime declarations and portable GLSL math.