ICD Dispatch Table Deep-Dive
How icd.h defines the function-pointer dispatch table, how oclgrind.cpp injects Oclgrind via LD_PRELOAD, and how the ICD model routes every OpenCL call to the simulator.
What is ICD?
The Installable Client Driver (ICD) model lets multiple OpenCL implementations coexist. An ICD loader (e.g. libOpenCL.so) dispatches each cl* call through a per-platform function-pointer table. Every OpenCL struct's first field is void* dispatch, pointing to this table.
graph LR App[Host Application] -->|clCreateBuffer| Loader[ICD Loader libOpenCL.so] Loader -->|dispatch table[42]| OG[oclgrind-rt.so] Loader -->|dispatch table[42]| NV[nvidia-opencl.so] Loader -->|dispatch table[42]| AMD[amdocl64.so]
The Rename Macro Block
When built with -DOCLGRIND_ICD, icd.h renames every public cl* symbol to _cl*. This avoids linker clashes with the ICD loader — Oclgrind's functions are only reachable through the dispatch table.
// icd.h — symbol renaming (excerpt)
#ifdef OCLGRIND_ICD
#define clCreateContext _clCreateContext
#define clCreateBuffer _clCreateBuffer
#define clEnqueueNDRangeKernel _clEnqueueNDRangeKernel
// ... ~90 more definitions
#endif
m_dispatchTable[256] is declared as extern in icd.h and populated at library load time. Each slot maps to the corresponding OpenCL API ordinal defined by the Khronos ICD specification.Struct Layout — dispatch First
Every struct in icd.h starts with void* dispatch. The ICD loader casts the handle, reads the first pointer, and indexes into it. Oclgrind sets this pointer to m_dispatchTable in every new _cl_* allocation.
// Creating a new event — dispatch is always first
cl_event _event = new _cl_event;
_event->dispatch = m_dispatchTable; // ICD slot pointer
_event->context = queue->context;
_event->queue = queue;
_event->type = type;
_event->event = event; // Core::Event*
_event->refCount = 1;
oclgrind.cpp — The Injection Launcher
The oclgrind executable does not implement OpenCL. It launches the user's application with LD_PRELOAD (Linux), DYLD_INSERT_LIBRARIES (macOS), or DLL injection (Windows) pointing to liboclgrind-rt.
sequenceDiagram participant User as $ oclgrind ./myapp participant OG as oclgrind.cpp participant ENV as Environment participant Target as myapp User->>OG: parseArguments() OG->>OG: map --data-races → OCLGRIND_DATA_RACES=1 OG->>ENV: setenv(LD_PRELOAD, liboclgrind-rt.so) OG->>ENV: setenv(LD_LIBRARY_PATH, ...) OG->>Target: execvp(appArgs) Note over Target: Loads with oclgrind-rt intercepting OpenCL
Windows DLL Injection
On Windows, oclgrind.cpp takes a different approach: it creates the target process in a suspended state, allocates memory in the child process for the DLL path, writes it, and launches a remote thread calling LoadLibraryA. After the DLL loads, it calls initOclgrind via another remote thread, then resumes the main thread.
checkWow64() verifies that parent and child match (both 32-bit or both 64-bit). A mismatch triggers a clear error message telling the user to switch binaries.Platform Enumeration
The ICD loader discovers Oclgrind by calling
clIcdGetPlatformIDsKHR (exported from the runtime library). This
function lazily creates the singleton m_platform and
m_device, reading memory sizes from environment variables like
OCLGRIND_GLOBAL_MEM_SIZE.
Platform Info Queries
clGetPlatformInfo returns metadata about the Oclgrind platform.
The implementation is a switch statement over param_name
that returns static strings and version numbers:
// clGetPlatformInfo excerpt
switch (param_name) {
case CL_PLATFORM_NAME: data = "Oclgrind"; break;
case CL_PLATFORM_VENDOR: data = "Oclgrind"; break;
case CL_PLATFORM_VERSION:
data = "OpenCL 3.0 (Oclgrind " PACKAGE_VERSION ")";
break;
case CL_PLATFORM_EXTENSIONS:
data = "cl_khr_icd";
break;
case CL_PLATFORM_ICD_SUFFIX_KHR:
data = "oclg";
break;
// ...
}
clGetExtensionFunctionAddress only recognises two names —
"clIcdGetPlatformIDsKHR" and "clGetPlatformInfo".
All other queries return NULL.
Device Initialisation Detail
The singleton m_device is created alongside the platform. Its
memory sizes are populated from environment variables with defaults:
OCLGRIND_GLOBAL_MEM_SIZE→ 128 MiBOCLGRIND_CONSTANT_MEM_SIZE→ 64 KiBOCLGRIND_LOCAL_MEM_SIZE→ 32 KiBOCLGRIND_MAX_WGSIZE→ 1024
These values are read once via oclgrind::getEnvInt() and cached
in the device struct for the lifetime of the process.
Linux vs macOS vs Windows
The injection mechanism differs by platform:
| OS | Mechanism | Library |
|---|---|---|
| Linux | LD_PRELOAD | liboclgrind-rt.so |
| macOS | DYLD_INSERT_LIBRARIES | liboclgrind-rt.dylib |
| Windows | Remote DLL injection | oclgrind-rt.dll |
DYLD_FORCE_FLAT_NAMESPACE=1
is also set to ensure the injected library's symbols override the
system OpenCL framework. Without this, symbol interposition would fail.
Library Path Resolution
getLibDirPath() in oclgrind.cpp resolves the path to
the library directory by reading /proc/self/exe (Linux),
_NSGetExecutablePath (macOS), or GetModuleFileName
(Windows), stripping the binary name, then appending
/lib (or staying in-place during testing).
ICD Dispatch Table Layout
The table is a 256-slot void* array. Each slot corresponds
to the ordinal defined by the Khronos ICD specification. For example
slot 0 is clGetPlatformIDs, slot 4 is clCreateContext,
etc. Oclgrind fills every slot it supports; unsupported slots are
NULL (the ICD loader returns CL_INVALID_OPERATION
if called).
How Dispatch Works at Runtime
When the ICD loader receives a call like clCreateBuffer(ctx, ...),
it dereferences ctx->dispatch (which is
m_dispatchTable), indexes to the clCreateBuffer slot,
and calls the function pointer. This indirection costs one memory load
per API call — negligible for a simulator.
Supported OpenCL Version
Oclgrind advertises OpenCL 3.0 at both the platform and
device level. The ICD suffix is "oclg" — this is used by
the loader to distinguish Oclgrind from other drivers in
/etc/OpenCL/vendors/.
The cl_khr_icd Extension
The only extension Oclgrind reports is cl_khr_icd. This
extension defines two entry points:
clIcdGetPlatformIDsKHR— discovers the platformclGetExtensionFunctionAddressForPlatform— looks up extension functions by name
Oclgrind's implementation of the latter returns NULL for
everything except "clIcdGetPlatformIDsKHR" and
"clGetPlatformInfo".
/etc/OpenCL/vendors/oclgrind.icd containing the path to
liboclgrind-rt.so. The oclgrind launcher bypasses
this by using LD_PRELOAD instead.
Security Implications
The LD_PRELOAD injection technique requires that the
target binary is not setuid/setgid — the dynamic linker ignores
LD_PRELOAD for privileged executables. On Windows, the
remote-thread DLL injection requires the launching process to have
PROCESS_ALL_ACCESS rights on the child, which is granted
automatically since oclgrind.exe creates the child itself.
Testing Mode
When the environment variable OCLGRIND_TESTING is set,
getLibDirPath() skips the ../lib suffix and looks
for the runtime library in the same directory as the executable. This
allows the test suite to run against an uninstalled build.
Edge Cases & Common Pitfalls
| Scenario | Behaviour | Resolution |
|---|---|---|
| LD_PRELOAD path is wrong | App loads system OpenCL instead of Oclgrind — runs on real GPU | Verify liboclgrind-rt.so path; use ldd to confirm |
| Target binary is setuid | Dynamic linker ignores LD_PRELOAD; injection silently fails | Run the binary without setuid, or use the ICD vendor file approach |
| 32/64-bit architecture mismatch (Windows) | checkWow64() detects mismatch → error message | Use matching architecture for oclgrind.exe and the target |
| Multiple OpenCL platforms installed | ICD loader returns all platforms; app must select Oclgrind by name | Filter by CL_PLATFORM_NAME == "Oclgrind" |
| Unsupported dispatch table slot called | NULL function pointer → ICD loader returns CL_INVALID_OPERATION | Only use OpenCL functions that Oclgrind implements |
Missing .icd vendor file | ICD loader doesn't discover Oclgrind; clGetPlatformIDs omits it | Create /etc/OpenCL/vendors/oclgrind.icd or use LD_PRELOAD |
| DYLD_INSERT_LIBRARIES on macOS SIP | System Integrity Protection strips the variable for protected binaries | Disable SIP for testing, or use the ICD vendor file method |
Troubleshooting
Application does not see Oclgrind platform
Run clinfo under the same environment. If Oclgrind is missing, either the .icd file is absent or LD_PRELOAD was not set. On Linux, verify with ls /etc/OpenCL/vendors/ and check that the path inside the .icd file resolves to an existing liboclgrind-rt.so.
Windows injection fails with access denied
If oclgrind.exe cannot open the child process with PROCESS_ALL_ACCESS, anti-virus software may be blocking remote thread creation. Add an exception for oclgrind.exe or run from an elevated command prompt.
Symbol clashes when not using ICD mode
If Oclgrind is built without -DOCLGRIND_ICD, the cl* symbols are exported directly and will clash with the system ICD loader. Always build with ICD mode enabled when installing alongside other OpenCL drivers.
Platform Discovery Flow
When an application calls clGetPlatformIDs, the ICD loader scans /etc/OpenCL/vendors/*.icd for vendor files. Each file contains the path to a shared library. The loader opens the library and calls clIcdGetPlatformIDsKHR to retrieve the platform handle. Oclgrind's implementation lazily creates the singleton m_platform and m_device at this point, reading memory configuration from environment variables.
Vendor file registration
To register without using LD_PRELOAD, create /etc/OpenCL/vendors/oclgrind.icd containing the absolute path to liboclgrind-rt.so. This approach is preferred for system-wide installation and avoids the security restrictions of LD_PRELOAD on setuid binaries.
Related Topics
- OpenCL API Layer — the
cl*implementations behind the dispatch table - Async Command Queue — how enqueued commands flow after dispatch
- CLI Interface — the
oclgrind-kerneltool that bypasses ICD entirely - Runtime Module Overview — parent page with links to all runtime sub-topics