OpenCL API Layer Deep-Dive
How runtime.cpp implements every cl* function by wrapping lightweight Core objects behind standard OpenCL struct handles.
Architecture Overview
graph LR App[Host App]-->|cl* calls|RT[runtime.cpp] RT-->|wraps|Ctx[Core::Context] RT-->|wraps|Prg[Core::Program] RT-->|wraps|Krn[Core::Kernel] RT-->|wraps|Mem[Core::Memory] RT-->|wraps|Q[Core::Queue] RT-->|events|AQ[async_queue]
Key Structs
Every OpenCL handle is a pointer to one of the structs declared in icd.h. The first field is always void* dispatch — the ICD function-pointer table.
| Struct | Core wrapper | Notable fields |
|---|---|---|
_cl_platform_id | — | dispatch only (singleton) |
_cl_device_id | — | globalMemSize, localMemSize, maxWGSize |
_cl_context | Context* | notify callback, refCount |
_cl_command_queue | Queue* | properties, refCount |
_cl_mem | size_t address | flags, hostPtr, parent, callbacks |
_cl_program | Program* | refCount |
_cl_kernel | Kernel* | memArgs map, imageArgs |
_cl_event | Event* | type, callbacks list, refCount |
Reference Counting
Each struct carries an unsigned int refCount. The clRetain* / clRelease* functions increment or decrement it atomically. When the count hits zero, the struct and its Core counterpart are deleted.
// Simplified retain / release pattern in runtime.cpp
CL_API_ENTRY cl_int CL_API_CALL clRetainContext(cl_context context) {
REGISTER_API;
if (!context) ReturnError(NULL, CL_INVALID_CONTEXT);
context->refCount++;
return CL_SUCCESS;
}
CL_API_ENTRY cl_int CL_API_CALL clReleaseContext(cl_context context) {
REGISTER_API;
if (!context) ReturnError(NULL, CL_INVALID_CONTEXT);
if (--context->refCount == 0) {
delete context->context; // Core::Context
delete[] context->properties;
delete context;
}
return CL_SUCCESS;
}
Error Handling Macros
Four macros centralise error reporting. They call notifyAPIError() which logs to stderr (when OCLGRIND_CHECK_API is set) and fires the context callback.
// ReturnError — returns an error code and optionally notifies
#define ReturnError(context, err) ReturnErrorInfo(context, err, "")
// ReturnErrorArg — adds the argument name to the message
#define ReturnErrorArg(context, err, arg) \
ReturnErrorInfo(context, err, "For argument '" #arg "'")
// SetError / SetErrorArg — sets *errcode_ret instead of returning
#define SetError(context, err) SetErrorInfo(context, err, "")
Platform & Device Initialisation
sequenceDiagram participant App participant RT as runtime.cpp participant ICD as ICD Loader App->>ICD: clGetPlatformIDs() ICD->>RT: clIcdGetPlatformIDsKHR() RT->>RT: lazy-init m_platform & m_device RT-->>ICD: platform handle ICD-->>App: platform handle App->>RT: clGetDeviceIDs(platform) RT-->>App: m_device (singleton)
OCLGRIND_* environment variables at first clGetPlatformIDs call. Changing them afterwards has no effect.API Call Stack Tracking
A thread-local g_apiCallStack vector records the outermost API name
via the RAII helper APICallEntry. This name appears in error messages,
enabling precise diagnostics even when one cl* function calls another
internally.
Context Creation
When the user calls clCreateContext, the runtime allocates a
_cl_context struct, instantiates a new Core::Context,
copies the properties array, and sets the initial reference count to 1.
If the user provides a notification callback, it is stored for later use
by error-reporting macros.
// Simplified clCreateContext from runtime.cpp
cl_context clCreateContext(
const cl_context_properties* properties,
cl_uint num_devices, const cl_device_id* devices,
void (CL_CALLBACK *pfn_notify)(...), void* user_data,
cl_int* errcode_ret) {
REGISTER_API;
// Validate parameters ...
cl_context ctx = new _cl_context;
ctx->dispatch = m_dispatchTable;
ctx->context = new oclgrind::Context();
ctx->notify = pfn_notify;
ctx->data = user_data;
ctx->refCount = 1;
// Copy properties ...
SetError(ctx, CL_SUCCESS);
return ctx;
}
Memory Object Management
The _cl_mem struct wraps an address in the simulated global
memory space. When clCreateBuffer is called, the runtime:
- Validates context, flags, and size
- Allocates memory via
Context::getGlobalMemory()->allocateBuffer() - Optionally copies from
host_ptrifCL_MEM_COPY_HOST_PTRis set - Stores the address, size, and flags in the
_cl_memstruct
clCreateSubBuffer creates a _cl_mem
with a non-null parent pointer and an offset into the
parent's allocation. The parent's reference count is incremented.
Kernel Argument Binding
clSetKernelArg records each argument in the Core Kernel
object. For memory arguments, the cl_mem handle is also stored in
kernel->memArgs so the async queue can retain it during execution.
Image arguments are tracked separately in kernel->imageArgs.
Command Queue Lifecycle
A _cl_command_queue wraps a Core::Queue. Commands flow through the
async queue layer which manages event dependencies. The queue itself is
destroyed only when its reference count reaches zero and all enqueued
commands have completed — ensured by clFinish being called in
clReleaseCommandQueue.
Device Constants
The simulated device reports itself as CL_DEVICE_TYPE_CPU |
CL_DEVICE_TYPE_GPU | CL_DEVICE_TYPE_ACCELERATOR | CL_DEVICE_TYPE_DEFAULT,
matching any device-type query. Memory sizes default to:
- Global: 128 MiB (
DEFAULT_GLOBAL_MEM_SIZE) - Constant: 64 KiB
- Local: 32 KiB
- Max work-group size: 1024
All are overridable via environment variables.
cl* calls from multiple host threads will cause undefined
behaviour. This matches the Oclgrind design goal of deterministic,
single-threaded simulation.
Event Wait List Validation
The helper checkEventWaitList() is called by most enqueue
functions. It verifies that:
num_events > 0impliesevent_wait_list != NULLand vice-versa- Every event in the list is non-null
- Every event belongs to the same context as the command queue
Violations return CL_INVALID_EVENT_WAIT_LIST or
CL_INVALID_CONTEXT.
Sampler Objects
The _cl_sampler struct stores addressing mode, filter mode,
and normalised-coordinate settings. These are combined into a 32-bit
sampler field that the Core interpreter uses when executing
image-read instructions. Samplers follow the same retain/release
pattern as other objects.
Common OpenCL Error Codes
| Error Code | Returned When | Typical Cause |
|---|---|---|
CL_INVALID_CONTEXT | Context is NULL or mismatched | Passing an object from a different context |
CL_INVALID_MEM_OBJECT | cl_mem handle is NULL or freed | Using a buffer after clReleaseMemObject |
CL_INVALID_KERNEL | cl_kernel is NULL or invalid | Kernel creation failed; check build log |
CL_INVALID_ARG_INDEX | Argument index ≥ kernel arg count | Off-by-one error in clSetKernelArg |
CL_INVALID_WORK_GROUP_SIZE | WG size does not divide NDRange evenly | Mismatched local/global sizes |
CL_INVALID_EVENT_WAIT_LIST | Wait list is inconsistent | num_events > 0 but list is NULL |
CL_OUT_OF_RESOURCES | Allocation exceeds simulated memory | Increase OCLGRIND_GLOBAL_MEM_SIZE |
CL_BUILD_PROGRAM_FAILURE | Kernel source fails to compile | Syntax error; check clGetProgramBuildInfo |
Edge Cases & Common Pitfalls
| Scenario | Behaviour | Resolution |
|---|---|---|
| Double-release of a context | Refcount underflows → use-after-free (no guard) | Ensure retain/release calls are balanced |
| Sub-buffer outlives parent | Parent refcount kept alive by sub-buffer retain | Release sub-buffers before releasing parent |
CL_MEM_USE_HOST_PTR with stack memory | Simulated memory copies data at creation; host pointer not tracked | Safe in Oclgrind, but undefined in real implementations |
| Querying unsupported device info param | Returns CL_INVALID_VALUE | Only query parameters listed in the OpenCL 3.0 spec |
| Enqueue after queue release | Undefined — queue struct may be freed | Always finish the queue before releasing |
Concurrent cl* calls from multiple threads | Undefined — runtime is not thread-safe | Serialise all API calls or use one thread |
Troubleshooting
Context callback not firing on errors
The notification callback passed to clCreateContext is only invoked when OCLGRIND_CHECK_API is set in the environment. Without it, errors are returned via error codes but the callback is skipped.
Memory leak warnings at exit
Oclgrind's Context destructor checks for unreleased allocations. If you see leak warnings, trace your clCreateBuffer / clReleaseMemObject calls. Sub-buffers count as separate allocations and must be released independently.
Related Topics
- ICD Dispatch Table — how
cl*calls are routed to the runtime functions - Async Command Queue — event and lifetime management for enqueued commands
- CLI Interface — the
oclgrind-kerneltool that bypasses the API layer - Simulation Lifecycle — direct Core usage without the OpenCL API wrapper
- Runtime Module Overview — parent page with links to all runtime sub-topics