Link CPU and OpenGL compute stages with shdr
shdr project — Vojtěch VrbaA processing pipeline does not always belong entirely on one backend. A GPU-friendly elementwise operation may feed a CPU-only stage, or a small CPU operation may sit between larger GPU stages. shdr lets each dispatch select a backend while bound resources connect the stages.
This guide uses the two-stage scaling application from the first-kernel guide. That guide includes all three source files and build commands needed to run the code.
The data flow stays explicit
The first kernel reads input and writes temporary. The second reads temporary and writes output. Both use the same scale.comp, with gains two and three respectively.
auto pipeline = shdr::dispatch(*first, groups)
>> shdr::dispatch(*second, groups);
pipeline.run();
With no explicit per-stage selection, run() executes the stages on the CPU. Supplying a device to run(gpu) executes these automatic stages on that device.
To make the boundary explicit, build the dispatches this way instead:
auto mixed = shdr::dispatch(*first, groups, shdr::backend::gpu)
>> shdr::dispatch(*second, groups, shdr::backend::cpu);
mixed.run(gpu);
This is the code behind the sample's --mixed mode. The output remains 6 12 18 24.
Account for the boundary
In the mixed example, the first dispatch produces the latest temporary contents on the GL device. The CPU consumer needs those contents in host memory. shdr synchronizes the shared resource for that transition.
This transfer and the associated wait are part of the workload. Moving a tiny stage to the GPU can cost more than it saves. Conversely, keeping a long chain of stages on the GPU can avoid intermediate host reads. The right boundary depends on the data size, algorithm, driver, and hardware.
A GL memory barrier makes writes visible to later GL operations. Waiting for completion is a different operation. For measurement, wait for completed work using an appropriate readback or device.synchronize(), and state whether transfers are included. Do not compare a CPU completion time with only a GPU enqueue time.
Read results deliberately
const shdr::buffer<float>& result = output; const float* values = result.data();
Const access synchronizes newer GPU contents when needed and preserves the modification state. Mutable data() also makes host changes authoritative. Obtain a fresh host pointer after GPU work before reading host storage.
The sample uses a tolerance and checks every result against 6 * input[i]. Larger floating-point algorithms need tolerances suited to their value range and numerical behavior. CPU/GPU results are not promised to be bitwise identical.
Keep ownership simple
For the reference-based dispatches above, keep both kernels alive until execution completes. Keep a borrowed host allocation alive while a resource or bound port can use it. Resources belong to one GL device; perform GPU operations and final resource destruction on its context thread.
The complete sample constructs its headless device before entering the function that owns kernels and resources. Those objects are destroyed before that function returns and before the device leaves scope. This makes the lifetime relationship visible in the host program.
For an application-owned GL context, make it current and supply a GL resolver to shdr::device. Keep the context alive while the resource objects use it. The runtime changes GL bindings, so restore the application's own GL state where necessary.
Add asynchronous execution only when needed
The runtime also provides GPU completion handles and a bounded CPU executor. GPU submit is for GPU stages; a mixed pipeline that enters a CPU consumer has different waiting behavior. Use separate writable resource slots for work in flight, and preserve their lifetimes through completion.
The upstream asynchronous guide explains preparation, fences, polling, queue limits, and shutdown. Start with the synchronous sample so correctness and data movement are clear before introducing concurrency.
For larger examples, inspect the upstream hybrid pipeline and benchmark methodology. Measurements there are evidence for the recorded workloads, rather than a promise that one backend is always faster.
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.