Practical guide
Asynchronous pipeline execution
Separate submission, completion and result access when an application needs to keep responding while compute work runs. GPU pipelines return a fence handle. CPU pipelines can run on a bounded worker pool. The shader source and resource bindings remain the same.
The async example processes thirty-two frames through three reusable slots and verifies each result on both backends:
# Run this example on the CPU.
./build/example_async
# Run this example on the requested headless GPU profile.
./build/example_async --gpu es31
Enqueue a GPU pipeline
Create the device on the thread that will issue GPU calls. The optional EGL
backend needs SHDR_ENABLE_EGL=1 and an EGL link library. The CMake shdr::egl
target supplies both.
shdr::headless_device gpu; // Create a headless ES 3.1 device.
pipeline.prepare(gpu); // Compile GPU programs and upload initial resources.
auto completion = pipeline.submit(gpu); // Enqueue work and keep its completion handle.
// Do other application work, then poll on this same context thread.
if (completion.ready()) { // Poll the completion fence with a zero timeout.
// Queued dispatches have completed.
} // Continue application work while GPU work is pending.
prepare compiles GPU programs and creates or uploads their bound resources. Call it before a processing loop, and prepare newly bound or host-modified resources as needed. submit queues GPU stages, inserts a fence, flushes the command stream and returns a completion handle. Use run(device) for pipelines that include explicit CPU stages.
submission is movable. ready() uses a zero-timeout fence query. For an
explicitly bounded wait:
const auto status = completion.wait(std::chrono::milliseconds(1)); // Request a one-millisecond fence-wait timeout.
if (status == shdr::wait_status::timeout) { // Handle work that is still pending after the time limit.
// Keep this slot in flight and try again later.
} // The timeout leaves the submission and its resources alive.
Calling wait() waits until completion. An empty submission is already complete. Driver failures throw shdr::error.
Releasing a submission handle leaves its pending programs and resources retained until the device retires the fence. Call device.collect_garbage() on the context thread during an application loop to poll completion and release retired objects. Device operations also perform this collection.
Use a finite ring of resource slots
Allocate a small fixed number of input, intermediate and output resource sets. Each slot has a pipeline bound to that set and its own completion handle:
- Select a free slot. Skip or defer the incoming item when none is free.
- Update its inputs, prepare newly bound or modified resources if appropriate,
and call
submit. - Poll existing handles on the context thread.
- Read or recycle a slot only after its submission completes.
Use stable inputs and distinct writable storage for every in-flight slot. Resource-wrapper copies share the same storage, so allocate a separate resource set for each slot. Kernel uniform and binding changes after submit affect later submissions. Issued GL commands retain their original bindings.
A ready fence indicates shader completion. buffer.data() and image2d.data() transfer newer GPU contents to host memory when needed. Include that transfer cost in the application budget. Place readbacks, allocation and shader compilation at planned points around a latency-sensitive processing loop.
Run CPU jobs on background workers
A CPU executor accepts pipelines with host-ready resources and returns futures:
shdr::executor workers(2, 8); // Create two CPU workers and allow eight queued jobs.
auto finished = workers.submit(cpu_pipeline); // Submit an independent CPU pipeline snapshot.
if (finished.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { // Poll the CPU future with a zero timeout.
finished.get(); // Report a shader exception, or consume the completed result.
} // Keep doing application work while the job is pending.
// At application shutdown:
workers.shutdown(); // Explicitly join workers when application processing ends.
The first constructor argument sets the worker count. The second limits pending jobs, while running jobs are tracked separately. Use positive counts and handle shdr::error for a full queue or closed executor. Submission snapshots shader instances, uniforms and bindings before enqueueing. Each worker has independent invocation state and shared resource storage. Later kernel changes affect subsequent jobs.
CPU submissions require host-ready inputs and CPU-compatible stages. Download newer GPU contents on the device context thread before enqueueing a CPU job. Concurrent jobs can share immutable inputs and require distinct writable resources. Read or modify a job's output after its future becomes ready. Keep GL calls on the thread that owns the context.
executor.enqueue(function) places ordinary CPU work on the same queue. Accepted jobs remain queued or running after their returned futures are released. shutdown() drains accepted jobs and joins workers. Executor destruction also joins. Call shutdown from an application thread at an explicit lifecycle point, and reserve that join time in the application budget.
Plan lifetimes and latency
GPU operations, completion queries and final release of GPU resource owners use
the device's context thread. A headless_device retains its EGL context with
the device state. An application-owned context must remain valid until all GPU
objects are released. Stop CPU workers before destroying devices they can retain,
and keep a dynamically loaded shader module loaded until its kernel snapshots
and jobs finish.
The CPU queue has a fixed pending capacity. Submission checks that capacity and reports a full queue as an error. Queue access uses short mutex-protected sections. Allocation, operating-system scheduling and driver calls have variable latency. Control GPU submissions with a finite resource ring and budget these costs separately from shader execution.
See the API reference for submission, executor and pipeline,
and the portability guide for independent-invocation rules.