Run your first GLSL compute kernel on the CPU, then OpenGL
shdr project — Vojtěch VrbaThis example runs two instances of one GLSL compute shader. The first multiplies four numbers by two; the second multiplies the intermediate result by three. The host checks the final values against an independent reference.
Create an empty working directory, then clone the source into vendor/shdr. The checkout below pins the source revision used for these examples:
mkdir shdr-quickstart cd shdr-quickstart git clone https://github.com/vrbadev/shdr.git vendor/shdr git -C vendor/shdr checkout 01652c119dad9662f48e3c056c5f988593c630d8
Create scale.comp, main.cpp, and CMakeLists.txt in shdr-quickstart, using the complete listings below. Then run step 1 from that directory.
1. Build and run on the CPU
You need a C++11 compiler, CMake 3.12 or newer, Python 3.8 or newer, and platform thread support. The default build uses the CPU and needs no GL context:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build -j2 ./build/quickstart
The expected output is:
6 12 18 24 Backend: CPU
These executable paths use the usual Unix single-configuration layout. On a Windows multi-configuration generator, select a configuration at build time and run the corresponding .exe from its configuration directory.
2. Inspect the shader
scale.comp is the only algorithm source. Each invocation handles one element. One workgroup launches 64 invocations; the i < count check keeps the padded invocations away from the four-element buffer.
#version 310 es
#ifdef __cplusplus
#include "shdr.h"
#endif
precision highp float;
precision highp int;
layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in;
layout(std430, binding = 0) readonly buffer Inputs { float input_values[]; };
layout(std430, binding = 1) writeonly buffer Outputs { float output_values[]; };
uniform uint count;
uniform float gain;
void main() {
uint i = gl_GlobalInvocationID.x;
if (i < count)
output_values[i] = input_values[i] * gain;
}
The guarded include lets the generated CPU adapter see shdr's types. The frontend takes care of producing a C++11 translation unit and embedding the GPU program. The .comp extension is supported, as are the project's native .cpp and .glsl shader sources. The stem scale registers the factory name scale_kernel.
3. Inspect the host application
The important connections are input → temporary → output. Each factory call creates an independent kernel instance, so its bindings and uniform values can differ from those of the other instance.
The full main.cpp below also contains the optional OpenGL and mixed modes used in the next step:
#include "shdr.h"
#include <cmath>
#include <iostream>
#include <stdexcept>
#include <string>
int run(shdr::device* gpu, bool mixed) {
shdr::buffer<float> input = {1.f, 2.f, 3.f, 4.f};
shdr::buffer<float> temporary(input.size()), output(input.size());
auto first = shdr::make_kernel("scale_kernel");
auto second = shdr::make_kernel("scale_kernel");
first->bind("input_values", input);
first->bind("output_values", temporary);
first->set("count", unsigned(input.size()));
first->set("gain", 2.f);
second->bind("input_values", temporary);
second->bind("output_values", output);
second->set("count", unsigned(input.size()));
second->set("gain", 3.f);
const shdr::extent groups(unsigned((input.size() + 63) / 64), 1, 1);
auto pipeline = shdr::dispatch(*first, groups)
>> shdr::dispatch(*second, groups);
if (mixed) {
auto hybrid = shdr::dispatch(*first, groups, shdr::backend::gpu)
>> shdr::dispatch(*second, groups, shdr::backend::cpu);
hybrid.run(*gpu);
} else if (gpu) {
pipeline.run(*gpu);
} else {
pipeline.run();
}
const shdr::buffer<float>& result = output;
const float* values = result.data();
for (std::size_t i = 0; i < result.size(); ++i) {
const float expected = float(i + 1) * 6.f;
if (!std::isfinite(values[i]) || std::fabs(values[i] - expected) > 1e-5f)
throw std::runtime_error("Result differs from the independent reference");
std::cout << values[i] << (i + 1 == result.size() ? '\n' : ' ');
}
std::cout << "Backend: " << (mixed ? "OpenGL then CPU" : gpu ? "OpenGL" : "CPU") << '\n';
return 0;
}
int main(int argc, char** argv) {
const std::string mode = argc == 2 ? argv[1] : "";
if (argc > 2 || (!mode.empty() && mode != "--gpu" && mode != "--mixed")) {
std::cerr << "Usage: quickstart [--gpu|--mixed]\n";
return 2;
}
try {
if (mode.empty())
return run(0, false);
#if defined(SHDR_ENABLE_EGL) && SHDR_ENABLE_EGL
shdr::headless_device gpu(shdr::profile::es31);
return run(&gpu, mode == "--mixed");
#else
std::cerr << "Rebuild with -DSHDR_GUIDE_GPU=ON to enable EGL.\n";
return 2;
#endif
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
return 1;
}
}
Use a C++ unsigned value for a shader uint uniform and a float value for a shader float. The code obtains a const view for reading output: mutable resource access marks the host copy modified, while const access reads without doing that.
The dispatch extent counts workgroups. With 64 local invocations, the group count for N elements is (N + 63) / 64. The kernel objects and their resources remain alive until the pipeline has finished using them.
4. See the build wiring
The source checkout supplies the runtime and frontend under vendor/shdr. shdr_target_shaders runs the frontend and attaches its generated adapter to the executable:
cmake_minimum_required(VERSION 3.12)
project(shdr_guide LANGUAGES CXX)
set(SHDR_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/vendor/shdr" CACHE PATH "shdr source directory")
option(SHDR_GUIDE_GPU "Enable the optional EGL backend" OFF)
find_package(Threads REQUIRED)
add_executable(quickstart main.cpp)
target_compile_features(quickstart PRIVATE cxx_std_11)
set_target_properties(quickstart PROPERTIES CXX_EXTENSIONS OFF)
target_include_directories(quickstart PRIVATE "${SHDR_ROOT}")
target_link_libraries(quickstart PRIVATE Threads::Threads)
include("${SHDR_ROOT}/cmake/shdr.cmake")
shdr_target_shaders(quickstart PRIVATE SOURCES scale.comp)
if(SHDR_GUIDE_GPU)
find_path(GUIDE_EGL_INCLUDE_DIR EGL/egl.h)
find_library(GUIDE_EGL_LIBRARY NAMES EGL)
if(NOT GUIDE_EGL_INCLUDE_DIR OR NOT GUIDE_EGL_LIBRARY)
message(FATAL_ERROR "Install EGL development headers and a compatible EGL library")
endif()
target_compile_definitions(quickstart PRIVATE SHDR_ENABLE_EGL=1)
target_include_directories(quickstart PRIVATE "${GUIDE_EGL_INCLUDE_DIR}")
target_link_libraries(quickstart PRIVATE "${GUIDE_EGL_LIBRARY}")
endif()
The SHDR_ROOT CMake setting can also point to another complete external checkout of vrbadev/shdr. The checkout command pins one source revision for reproducibility.
5. Enable OpenGL execution
Install EGL development headers and a compatible EGL driver that can create an OpenGL ES 3.1 compute context. Then build the same application with its optional backend enabled:
cmake -S . -B build-gl -DCMAKE_BUILD_TYPE=Release -DSHDR_GUIDE_GPU=ON cmake --build build-gl -j2 ./build-gl/quickstart --gpu ./build-gl/quickstart --mixed
Both commands should print 6 12 18 24. Their backend labels identify all-OpenGL execution and an OpenGL stage followed by a CPU stage. A context-creation or shader failure is reported as an error; the application does not silently fall back.
An EGL driver can be a software implementation such as Mesa llvmpipe. That exercises the OpenGL execution path, but its timings do not measure a hardware GPU. Consult the upstream requirements for profiles and context ownership.
What to change next
Change gain without rebuilding the shader: it is a uniform. Add more input values and keep the rounded-up dispatch and bounds check. Then try a nonlinear operation while keeping the CPU reference independent of the shader implementation.
Before adding dependencies between invocations, read the portability guide. For a concrete explanation of transfers, read the mixed-pipeline guide.
Prepared with AI assistance on behalf of the shdr project. Source and API claims were checked against shdr 1.0.0, commit 01652c119dad9662f48e3c056c5f988593c630d8. Verification on 15 October 2026: all 23 upstream CPU tests passed; the complete scaling example produced its expected values in CPU, EGL OpenGL ES 3.1, and mixed OpenGL/CPU modes. These are correctness checks, not hardware GPU performance measurements. Upstream source and MIT license.