Focus

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.

StructCore wrapperNotable fields
_cl_platform_id—dispatch only (singleton)
_cl_device_id—globalMemSize, localMemSize, maxWGSize
_cl_contextContext*notify callback, refCount
_cl_command_queueQueue*properties, refCount
_cl_memsize_t addressflags, hostPtr, parent, callbacks
_cl_programProgram*refCount
_cl_kernelKernel*memArgs map, imageArgs
_cl_eventEvent*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;
}
Note: The reference counting is not atomic — Oclgrind is a single-device simulator, so thread-safety is handled at the queue level, not per-object.

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)
Caution: Memory sizes are read from 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:

  1. Validates context, flags, and size
  2. Allocates memory via Context::getGlobalMemory()->allocateBuffer()
  3. Optionally copies from host_ptr if CL_MEM_COPY_HOST_PTR is set
  4. Stores the address, size, and flags in the _cl_mem struct
Sub-buffers: 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:

All are overridable via environment variables.

Thread safety: The runtime layer itself is not thread-safe. Concurrent 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:

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 CodeReturned WhenTypical Cause
CL_INVALID_CONTEXTContext is NULL or mismatchedPassing an object from a different context
CL_INVALID_MEM_OBJECTcl_mem handle is NULL or freedUsing a buffer after clReleaseMemObject
CL_INVALID_KERNELcl_kernel is NULL or invalidKernel creation failed; check build log
CL_INVALID_ARG_INDEXArgument index ≥ kernel arg countOff-by-one error in clSetKernelArg
CL_INVALID_WORK_GROUP_SIZEWG size does not divide NDRange evenlyMismatched local/global sizes
CL_INVALID_EVENT_WAIT_LISTWait list is inconsistentnum_events > 0 but list is NULL
CL_OUT_OF_RESOURCESAllocation exceeds simulated memoryIncrease OCLGRIND_GLOBAL_MEM_SIZE
CL_BUILD_PROGRAM_FAILUREKernel source fails to compileSyntax error; check clGetProgramBuildInfo

Edge Cases & Common Pitfalls

ScenarioBehaviourResolution
Double-release of a contextRefcount underflows → use-after-free (no guard)Ensure retain/release calls are balanced
Sub-buffer outlives parentParent refcount kept alive by sub-buffer retainRelease sub-buffers before releasing parent
CL_MEM_USE_HOST_PTR with stack memorySimulated memory copies data at creation; host pointer not trackedSafe in Oclgrind, but undefined in real implementations
Querying unsupported device info paramReturns CL_INVALID_VALUEOnly query parameters listed in the OpenCL 3.0 spec
Enqueue after queue releaseUndefined — queue struct may be freedAlways finish the queue before releasing
Concurrent cl* calls from multiple threadsUndefined — runtime is not thread-safeSerialise 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