Jump to content

Reading and Writing Values and Samples

From RidgeRun Developer Wiki

🚧 Documentation is under development

The Holoscan SDK guide is currently under active development. Some sections may be incomplete or change without notice.

Questions? Contact RidgeRun or email to support@ridgerun.com.

Contents Pin





Reading and Writing Values and Samples

The minimal operator reads a number and publishes another number. In a larger pipeline, the value may also have a capture time, a frame identity, or retained Tensor storage that downstream work still needs. Choose the read and publication APIs together: that choice determines which metadata survives, what ownership you retain, and which errors the callback must handle.


Related Concepts


Choose the read and publish path

Choose the read and publication pair from the information the next operator needs:

Need Read with Publish with
Payload only, with no sample metadata receive_data() A new value through Output<T>::emit(value).
Payload plus capture time, frame identity, or flags receive() The owning sample through emit(sample).
A new value with selected metadata receive() A new value with EmitOptions.
Several values selected by a batch contract selected_batch() A new aggregate value, or one output per result when the contract allows it.
A new runtime-owned Tensor The selected sample, then allocate_tensor() on the output Fill, commit, and publish the output loan through emit(loan).
An existing Tensor and its metadata should pass through receive() The owning sample through emit(sample), with no write to the received backing.

Every accessor returns a checked result or a bounded view. Check it before dereferencing it, and keep borrowed views inside the callback. The Writing Tensor Operators page follows the Tensor row through a complete host and CUDA transform. For the official overview, see Your First Operator in the NVIDIA Holoscan SDK documentation.

Choose a payload before declaring the ports

Keep Input<T> and Output<T> as stable operator members and register them in setup(). The name supplied there identifies the port in wiring and diagnostics. Constructing the port member declares an endpoint; runtime creates its queue when the application runs.

The PublishedPayload concept admits built-in arithmetic and enum scalars, supported generated schema roots, explicitly contracted raw POD types, and exactly Tensor. An arbitrary Result struct is not admitted merely because it is copyable or trivially copyable. Prefer a generated schema for an application-defined message. Raw POD requires both ChannelTypeIdentity<T> and ChannelPodLayout<T>, with supported ordered fields covering every object byte without gaps, overlap, or padding. It also requires coordinated identity/version and binary-layout compatibility. The Ports page above provides the complete POD declaration and its limits.

Use Tensor storage for image or array bytes, with scalar or schema metadata alongside it. Declare the accepted input representation through expects_tensor() and the output storage, representation, and finite bounds through produces_tensor(). Tensor Contracts and Memory Types explains those contracts.

Read the selection prepared for this activation

Runtime freezes the input selection before compute() begins. Reading it again accesses the same selected data; it does not advance to the next queued sample. Select an accessor that matches the contract's scalar or batch shape:

The table above matches the accessor to the selected shape. A missing selection, wrong cardinality, stale binding, exhausted read credit, or backend failure is still an accessor error even when the payload's C++ type is correct.

An owning sample returned by receive() can outlive the callback. Copies retain its existing claim, and a move transfers it. Each separate successful receive reserves an independent finite claim, so repeatedly reading and retaining the same selection can exhaust bounded capacity. Release retained samples when their work finishes. Constructing a new Sample from payload and metadata does not manufacture an input read claim.

A Tensor value has its own backing ownership: copying the handle can retain the allocation without copying image bytes, including when obtained through receive_data(). That retention is distinct from a sample read claim. Received backing remains subject to its ownership and readiness contract; mutable pointer access alone grants no permission to write upstream storage.

Forward the sample when its identity must survive

The following is a complete operator class, with its includes. Add it to an application graph with matching uint32 producer and consumer ports. It deliberately forwards the selected sample unchanged:

#include <cstdint>
#include <utility>

#include <holoscan/core/errors.hpp>
#include <holoscan/core/execution_context.hpp>
#include <holoscan/core/expected.hpp>
#include <holoscan/core/operator.hpp>
#include <holoscan/core/operator_spec.hpp>
#include <holoscan/core/port.hpp>
#include <holoscan/core/sample.hpp>
#include <holoscan/core/temporal_contract.hpp>

class ForwardScalar final : public holoscan::Operator<> {
 public:
  void setup(holoscan::OperatorSpec& spec) override {
    spec.input(input, "input").queue_depth(4U);
    spec.output(output, "output");
  }

  [[nodiscard]] holoscan::Contract contract() const override {
    holoscan::Contract result;
    result.trigger(holoscan::OnEach{input});
    return result;
  }

  [[nodiscard]] holoscan::expected<void, holoscan::Error> compute(
      holoscan::ExecutionContext&) override {
    auto sample = input.receive();
    if (!sample) {
      return holoscan::make_unexpected(std::move(sample).error());
    }
    return output.emit(std::move(*sample));
  }

  holoscan::Input<std::uint32_t> input;
  holoscan::Output<std::uint32_t> output;
};

Output<T>::emit(sample) preserves capture time and source clock, frame identity, trace, and semantic flags. The destination output assigns its own publication sequence, time, byte size, and classification. The overload takes no EmitOptions argument because the sample already carries the semantic metadata to forward.

For this scalar class, forwarding moves an owning copied value. With a Tensor payload, sharing compatible backing can preserve the allocation's identity while downstream lifetime requirements remain active. Publishing a received Tensor does not authorize changing its contents.

Publish a new value with truthful metadata

The value overload emit(value, options) creates a fresh metadata record using EmitOptions. Supply capture time only when it represents the time the data was acquired or created, expressed in an admitted clock domain. For a transformed camera frame, the current activation time generally does not replace the original acquisition time.

Omitted capture time remains absent; runtime does not invent it from arrival, activation, or publication time. SampleMetadata::capture_time() and SampleMetadata::publish_time() expose those separate times. The minimal filter's value-only emission therefore starts new semantic metadata even though the application configured a default publication clock. Age-based selection later in a graph needs real capture evidence and comparable clocks.

Both scalar emit overloads consume their by-value argument on every outcome. Check and propagate the result. A retryable publication error does not restore a moved sample or value; any retry needs a newly supplied equivalent argument and must respect the admitted publication semantics. Generic output loans and Tensor output loans have their own authority rules.

Read a batch with a batch-shaped contract

This complete operator class sums the values selected since the previous driver activation. Its uint32 driver provides pacing; the second input supplies uint32 values, and the result is uint64. Connect both inputs and its output in an application graph before compiling:

#include <cstdint>
#include <utility>

#include <holoscan/core/errors.hpp>
#include <holoscan/core/execution_context.hpp>
#include <holoscan/core/expected.hpp>
#include <holoscan/core/operator.hpp>
#include <holoscan/core/operator_spec.hpp>
#include <holoscan/core/port.hpp>
#include <holoscan/core/temporal_contract.hpp>

class BatchSum final : public holoscan::Operator<> {
 public:
  void setup(holoscan::OperatorSpec& spec) override {
    spec.input(driver, "driver").queue_depth(4U);
    spec.input(values, "values").queue_depth(16U);
    spec.output(sum, "sum");
  }

  [[nodiscard]] holoscan::Contract contract() const override {
    holoscan::Contract result;
    result.trigger(holoscan::OnEach{driver})
        .input(values, holoscan::DrainAll{});
    return result;
  }

  [[nodiscard]] holoscan::expected<void, holoscan::Error> compute(
      holoscan::ExecutionContext&) override {
    auto batch = values.selected_batch();
    if (!batch) {
      return holoscan::make_unexpected(std::move(batch).error());
    }
    std::uint64_t total{};
    for (const auto& sample : *batch) {
      total += sample.data;
    }
    return sum.emit(total);
  }

  holoscan::Input<std::uint32_t> driver;
  holoscan::Input<std::uint32_t> values;
  holoscan::Output<std::uint64_t> sum;
};

DrainAll selects a batch, so this callback uses selected_batch(). The OnEach driver is pinned implicitly, and the callback need not read its payload to use it as a trigger. There is no separate driver selector. An empty batch emits zero, so no missing policy is attached to DrainAll. A scalar receive accessor would have the wrong cardinality for this input.

The input has an explicit 16-entry teaching buffer. Its effective depth and retention still belong to the accepted graph plan; a real application must choose those bounds for its producer rate and trigger behavior. Keep the batch view inside the callback, and avoid assuming that copying a borrowed view creates independent ownership.

Handle missing data through the declared branch

For a failure-capable side selector, inspect selection() or is_missing() before reading. The selection reports both InputState and SelectionReason. A SafeDefault declaration lets the operator's degraded branch run; it does not create a default T value for receive_data() to return.

Selection state Available value Callback action
kSelected Current selected value Use the accessor matching the selected shape and check its result.
kReused Bounded reused value Read it through the normal accessor and preserve the fact that it was reused when that matters to the algorithm.
kSafeDefault No side-input value Execute the operator-authored fallback branch; do not attempt to read a fabricated default value.
kAbsent or kNotSelected No usable selection Do not read that input. Whether the callback runs is governed by the full contract and the other inputs.

The SDK operator_anatomy example below combines an implicit driver, DrainAll, and a LatestWithin + SafeDefault side branch. It shows the frozen branch inspection without hiding missing-data behavior in hand-written queue reads.

Move to Tensor writes through the output loan

For a new runtime-owned Tensor, the sequence is allocate_tensor() → TensorOutputLoan::write() → fill → TensorOutputWriteGuard::commit() → Output<Tensor>::emit(loan). Check each fallible result and publish the loan only after the write phase is committed. Commit consumes the write guard but does not publish the sample. Writing Tensor Operators supplies the full host/CUDA recipes and the required output contracts.


Source reference map and official documentation


Related Examples


Cookies help us deliver our services. By using our services, you agree to our use of cookies.