DeviceArray — Stream, Queue and Context
This is the advanced companion to DeviceArray Reference. You need it only if you run your own GPU code against the buffer, for example a CUDA or OpenCL kernel, or a framework such as PyTorch, CuPy, or OpenCV. It covers how the SDK orders its GPU work against your stream or queue, and when you must share a CUDA or OpenCL context.
Stream and Queue Ordering
A stream (CUDA) or command queue (OpenCL) is an ordered list of GPU work: whatever you place on it runs in order, one item after the next. The SDK runs its capture and processing on a stream (or queue) of its own, separate from any you create.
When you call a method that returns a DeviceArray, such as imageDeviceArray() or devicePointsXYZ(), you hand it your own stream or queue, wrapped in StreamOrQueue.
The SDK then makes your stream wait until its own work has finished.
Anything you place on your stream after the call is therefore guaranteed to see finished data, and the CPU never has to wait.
Two streams / queues in play
flowchart LR
subgraph sdk [SDK-owned]
SDKStream["SDK stream / queue"]
captureOp(["capture / processing"])
end
subgraph user [User-owned]
UserStream["User stream / queue"]
userKernelOp(["your kernel or copy"])
end
captureOp --> SDKStream
userKernelOp --> UserStream
acqOp(["imageDeviceArray(userStream)"])
SDKStream --> acqOp
acqOp -.-> UserStream
classDef zividClass fill:#4A8FA4,stroke:#34323D,color:#FFFFFF
classDef api fill:#91D2C8,stroke:#4A8FA4,color:#000000
class SDKStream,UserStream zividClass
class captureOp,userKernelOp,acqOp api
The dashed arrow represents the ordering established by that call. The user stream is made to wait on an event recorded on the SDK stream. No CPU-side sync happens; both streams keep running asynchronously.
Timeline of a typical call sequence
sequenceDiagram
participant Host
participant SDK as SDK stream / queue
participant User as User stream / queue
Host->>SDK: capture / processing work
Note right of SDK: producer runs asynchronously
Host->>SDK: imageDeviceArray(userStream)
SDK-->>User: record event, User waits on it
Host-->>Host: DeviceArray returned (no host sync)
Host->>User: launch consumer kernel(devicePointer)
Note right of User: kernel is ordered after SDK work
Host->>User: copyToHost(dst, userStream)
Note right of User: D2H copy enqueued, call returns
Host->>User: synchronizeStream(userStream)
User-->>Host: blocks until copy completes
Host->>Host: read dst on CPU
Key properties
The call is non-blocking on the host and returns as soon as the ordering is set up.
devicePointer()is a plain accessor; it performs no synchronization.The buffer is already valid to use on the same stream / queue you passed in.
Host-side accessors that copy to CPU (
copyToHost(),toArray2D(),toArray1D(), and the freetoImage(buffer, streamOrQueue)overloads) also return immediately.Call
synchronizeStreamon the same stream / queue before reading the host destination.
Passing the SDK’s own stream / queue
If you do not have a stream or queue of your own, pass computeDevice().sdkStreamOrQueue().
The SDK’s work and your work then share the same stream, so the wait step has nothing to wait for and costs nothing.
This is the recommended pattern when you only need the raw device pointer for a framework that does not expose its own stream handle.
Choosing between StreamOrQueue types
StreamOrQueue is a small tagged wrapper.
Only the member matching the active backend is read; the other must be null.
On a CUDA build, pass
CUDAStreamPtr{ stream }. A null stream is allowed and refers to CUDA’s default / legacy stream.On an OpenCL build, pass
OpenCLCommandQueuePtr{ queue }. The queue handle must be non-null.
Both wrappers convert implicitly to StreamOrQueue, so these methods take a single argument at the call site.
GPU Context Sharing
Note
You only need this section if you create your own CUDA or OpenCL context, or want the SDK to share a context with another framework. With the default setup, where the SDK creates its own context and you use the CUDA runtime in your own code, everything already shares one context and there is nothing to do.
A context is the GPU runtime’s container for allocations, streams, and queues.
A buffer allocated in one context cannot be read by a stream or queue that belongs to a different context.
The buffer behind a DeviceArray lives in the context the SDK uses, so any stream or queue you use to consume it must belong to that same context.
CUDA and OpenCL differ in how much of this they set up for you.
CUDA
The CUDA runtime creates and manages one shared context per device for you, behind the scenes (CUDA calls this the primary context).
As long as everything in your process talks to the same GPU through the CUDA runtime, all streams and device pointers share that one context automatically.
This is what the SDK uses by default.
The simplest setup is therefore to let the SDK create its own CUDA context and to use the CUDA runtime API in your own code as well.
No extra setup is needed, and any CUstream you pass when requesting a device array is compatible.
If you instead have your own CUDA driver-API context (a CUcontext created with cuCtxCreate, or a context owned by another framework), you must give it to the SDK when constructing Application.
Otherwise the SDK creates a separate context, and the DeviceArray it hands back is not usable from your streams.
CUcontext cuContext = ...;
Zivid::Application zivid{ Zivid::CUDAContextPtr{ cuContext } };
OpenCL
OpenCL has no equivalent of a runtime-managed primary context.
Every cl_mem and cl_command_queue is tied to a specific cl_context, and a queue can only enqueue work on buffers that live in the same context.
If you pass an OpenCLCommandQueuePtr whose queue lives in a different context than the SDK’s, the ordering call fails and the device pointer cannot be safely consumed by your kernels.
You therefore have to make one context common to both the SDK and your code. There are two ways to do it.
Option 1: give the SDK your context up front.
Construct Application with OpenCLContextPtr{ clContext }.
The SDK creates its internal queue in that context, and any queue you already own in it is compatible.
cl_context clContext = ...;
Zivid::Application zivid{ Zivid::OpenCLContextPtr{ clContext } };
Option 2: adopt the SDK’s context.
If you can create your OpenCL objects after Application is constructed, retrieve the SDK’s context and create your queues in it.
auto *sdkContext = static_cast<cl_context>(camera.computeDevice().nativeContext());
cl_command_queue userQueue = clCreateCommandQueueWithProperties(sdkContext, device, nullptr, &err);
In both cases you can pass OpenCLCommandQueuePtr{ userQueue } to any method that returns a DeviceArray, and the returned array is safe to use on that queue.
Note
createDeviceArrayView (OpenCL overload) has the same requirement.
The cl_mem you wrap must be created in the SDK’s context, otherwise the SDK cannot enqueue work against it.
See Also
DeviceArray Reference for the data layout of each format.
GPU Access Tutorial for end-to-end usage and framework integration samples.