Skip to content

C and C++ SDK

The Linux platform wheel includes the public C headers, a header-only C++ wrapper, libpixtreme.so.2, and a CMake package. A C11 or C++17 application can link the SDK and run without embedding Python. Python is used here to install the wheel and locate the CMake package. The CMake consumer does not require Python development headers, nanobind, or the CUDA toolkit.

Install pixtreme into your Python environment, then obtain the package directory:

python -m pip install pixtreme
python -c 'import pixtreme; print(pixtreme.core.get_cmake_dir())'

Use that value for pixtreme_DIR. The lookup also works with editable installs. Applications should use the lookup function instead of constructing an SDK path. pixtreme.core remains the existing Python module and also exports this function.

Create CMakeLists.txt:

cmake_minimum_required(VERSION 3.28)
project(example LANGUAGES C CXX)
find_package(pixtreme CONFIG REQUIRED)
add_executable(example main.cpp)
target_compile_features(example PRIVATE cxx_std_17)
target_link_libraries(example PRIVATE pixtreme::pixtreme)
add_executable(c_example main.c)
set_target_properties(c_example PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED YES)
target_link_libraries(c_example PRIVATE pixtreme::pixtreme)

Create main.cpp:

#include <pixtreme/pixtreme.hpp>
#include <iostream>

int main() {
    try {
        pixtreme::require_abi();
        std::cout << "Compatible pixtreme SDK\n";
    } catch (const pixtreme::error &error) {
        std::cerr << error.what() << '\n';
        return 1;
    }
}

Create main.c:

#include <pixtreme/pixtreme.h>
#include <stdio.h>

int main(void) {
    pixtreme_error_t *error = NULL;
    pixtreme_status_t status = pixtreme_check_abi(
        PIXTREME_ABI_MAJOR, PIXTREME_ABI_MINOR, &error);
    if (status != PIXTREME_STATUS_SUCCESS) {
        const char *reason = NULL, *target = NULL, *remedy = NULL;
        pixtreme_error_get_reason(error, &reason, NULL);
        pixtreme_error_get_target(error, &target, NULL);
        pixtreme_error_get_remedy(error, &remedy, NULL);
        fprintf(stderr, "why=%s; what=%s; how=%s\n", reason, target, remedy);
        pixtreme_error_release(error, NULL);
        return 1;
    }
    return 0;
}

Build and run the programs:

cmake -S . -B build \
  -Dpixtreme_DIR="$(python -c 'import pixtreme; print(pixtreme.core.get_cmake_dir())')"
cmake --build build
./build/example
./build/c_example

The imported target supplies the header and shared library locations. CMake keeps that library discoverable in the build directory. For installed applications, keep the SDK library directory in the application's runtime search path, for example with CMake's INSTALL_RPATH. Keep the Python environment containing the SDK installed while those applications use it. Distribute a dependency on pixtreme; do not copy libpixtreme into your own wheel or application distribution. Python and C++ consumers in one process use the same library.

Versions and compatibility

find_package(pixtreme 2.0 CONFIG REQUIRED) can impose a lower bound on the installed distribution version. Distribution versions and ABI versions have separate purposes. The SDK ABI starts at major 2, minor 0.

PIXTREME_ABI_MAJOR and PIXTREME_ABI_MINOR describe the build headers. pixtreme_abi_version(&major, &minor, &error) returns the loaded library's ABI. pixtreme_check_abi(header_major, header_minor, &error) accepts equal majors when the library minor is at least the header minor. A different major or a newer header minor returns PIXTREME_STATUS_INCOMPATIBLE_ABI with diagnostics. pixtreme::require_abi() receives the consumer's header values through default arguments evaluated at each call site. Libraries built with different headers each check their own versions when loaded into the same process. Applications can call it after loading the library; it does not store a global state that changes later calls. The Python native module also checks its build headers when imported.

The shared library's SONAME includes its ABI major (libpixtreme.so.2). Within a major, existing C functions retain their meanings. New functions or trailing struct fields increment the ABI minor in the release that adds them. Previous minor consumers can run against later minors without being rebuilt.

Extensible structs start with size_t struct_size. Their initialization macros provide the size published by the header and default field values. A library treats omitted input fields as defaults and ignores unknown input tails. It rejects zero sizes and sizes smaller than the required prefix. Pass these structs by pointer. Outputs preserve struct_size and write only complete fields that fit, leaving padding and unknown trailing bytes untouched. Added fields start at or beyond the previous struct's sizeof; trailing padding is never reused. The allocator, Frame, token, color, and LUT structs follow this rule. The _MIN_SIZE macros give each mandatory prefix; the _INIT macros initialize the published size and defaults.

Status and errors

Every C function returns pixtreme_status_t, an int32_t. Its optional final argument receives a pixtreme_error_t *. All other output pointers are required. The status constants are:

Constant Value Meaning
PIXTREME_STATUS_SUCCESS 0 The call succeeded
PIXTREME_STATUS_INVALID_ARGUMENT 1 An input or required output pointer is invalid
PIXTREME_STATUS_INCOMPATIBLE_ABI 2 The header requires an incompatible ABI
PIXTREME_STATUS_OUT_OF_MEMORY 3 An allocation failed
PIXTREME_STATUS_INTERNAL_ERROR 4 An internal operation failed
PIXTREME_STATUS_FILE_NOT_FOUND 5 The requested image file does not exist
PIXTREME_STATUS_IO_ERROR 6 An operating-system file I/O cause

On success, a supplied error output is set to NULL. Passing NULL for that output returns the status without allocating diagnostics. On failure, the caller owns the returned error and releases it with pixtreme_error_release(error, NULL). Release accepts NULL. Release an existing object before reusing its output variable; assigning a new error does not release the old one.

The accessors pixtreme_error_get_status, pixtreme_error_get_reason, pixtreme_error_get_target, and pixtreme_error_get_remedy receive the source error, a required output pointer, and an optional error output for accessor failures. The status accessor returns the classification stored in the object. The other accessors return strings containing the reason, affected input or operation, and correction. The strings remain valid until the object is released. Later calls, calls from other threads, and reentrant calls do not change an existing object. An error can be read and released on another thread; its owner must ensure all readers finish before releasing it.

pixtreme_error_get_cause(source, &cause, &error) returns a borrowed const pixtreme_error_t *. A successful query returns NULL when the error has no cause. A cause can itself have a cause; inspect the chain with the same status, reason, target, remedy, and cause accessors. Every borrowed cause remains valid until its parent error is released. Release only the owned outer error; do not release borrowed causes separately. The Python EXR binding maps this chain to exception __cause__ values using each error's status. EXR file I/O failures return PIXTREME_STATUS_INTERNAL_ERROR at the outer level and retain an IO_ERROR cause. The cause's reason contains the OS error text and code; its target identifies the path. Python translates that cause to OSError and the outer error to RuntimeError. Malformed header and payload causes remain INVALID_ARGUMENT / ValueError. A missing input file still returns FILE_NOT_FOUND / FileNotFoundError. The C error interface adds no errno attribute or additional accessor. Corrupt pixel payloads normally return INTERNAL_ERROR. Sampled DWA Huffman code-length table failures and short legacy sampled lossless planes retain their existing INVALID_ARGUMENT / ValueError classification and original diagnostic; dense DWA retains its existing classification.

If allocating error details fails, the call returns PIXTREME_STATUS_OUT_OF_MEMORY and a static error object with a reason and correction. Releasing that object is a no-op. No C++ exception crosses the C boundary. pixtreme::check_status(status, error) takes ownership of a C error and throws pixtreme::error for failures. Its what() includes the reason, target, and correction; status() retains the status code. cause() borrows a const pixtreme_error_t *, or returns null when there is no cause. Inspect it with the C status and diagnostic accessors; do not release it separately. Copies of the C++ exception share ownership of the C error, so borrowed causes remain valid until the last exception copy is destroyed. The wrapper releases the C object even if constructing the exception or its shared ownership control block fails.

Tokens and color definitions

pixtreme_token_families returns the 30 family names from the Token Reference's Literal alias table, such as Colorspace, Gamma, and Interpolation. pixtreme_token_spellings(family, &tokens, &count, &error) returns that family's canonical strings. Both lists and their strings are immutable, remain valid until process exit, and must not be freed. pixtreme_token_aliases returns parallel lists for the four permanent aliases that require an explicit mapping. The other 26 earlier spellings match canonical tokens through casefold and separators.

<pixtreme/tokens.h>, included by both SDK headers, provides a string constant for every canonical token. Constants use an uppercase family prefix, such as PIXTREME_COLORSPACE_SRGB, PIXTREME_GAMMA_ARRI_LOGC4, and PIXTREME_REFERENCE_WHITE_D65. They carry the same strings used by Python.

const char *color = pixtreme::match_token("Colorspace", u8"ſ_R.G-B");
// color is the process-lifetime canonical string "sRGB".
pixtreme_token_options_t options = PIXTREME_TOKEN_OPTIONS_INIT;
const char *subset[] = {"trilinear", "tetrahedral"};
options.accepted = subset;
options.accepted_count = 2;
const char *interpolation = pixtreme::match_token("Interpolation", "TETRA_HEDRAL", &options);

The equivalent C function is pixtreme_token_match(family, input, options, &canonical, &error). Null options select the whole family. An explicit subset must be a nonempty list of exact canonical spellings from one family. A null family infers a containing family from that subset. Names from other families or outside the subset are rejected. The options struct can also supply axis, received, reason, and remedy diagnostic text. received preserves a caller's representation of a non-string input when input is null. A nonzero exact field requires a canonical string without normalization. The default uses the Token Reference's Unicode casefold, separator, and permanent-alias rules. Errors preserve the rejected value and provide a reason and canonical recovery candidates. These functions use no GPU resources. Token queries and matching leave non-error outputs unchanged on failure.

pixtreme_colorspace_get(canonical, &info, &error) returns primaries[3][2] and white[2] in pixtreme_colorspace_info_t. pixtreme_reference_white_get returns xy[2] in pixtreme_reference_white_info_t. Initialize outputs with PIXTREME_COLORSPACE_INFO_INIT or PIXTREME_REFERENCE_WHITE_INFO_INIT. Queries require exact canonical names and return standard CIE 1931 xy coordinates at double precision. They use no GPU resources and preserve outputs on failure. The C++ wrappers are pixtreme::colorspace_definition(name, &info) and pixtreme::reference_white_coordinates(name, &info).

LUT validation

pixtreme_lut_validate and pixtreme_lut1d_validate validate borrowed 3D and 1D RGB tables. They do not create persistent LUT handles or apply a LUT to pixels. Their arguments are a pixtreme_lut_desc_t *, an optional allocator, a CUDA stream, a required pixtreme_lut_info_t *, and an optional error output. C++ uses pixtreme::validate_lut(desc, &info, allocator, stream) or pixtreme::validate_lut1d with the same arguments.

Initialize descriptions, array views, explicit domains, and outputs with PIXTREME_LUT_DESC_INIT, PIXTREME_LUT_ARRAY_INIT, PIXTREME_LUT_DOMAIN_INIT, and PIXTREME_LUT_INFO_INIT. The description has these fields:

Field Meaning
data Required borrowed CUDA array view; float32 (N,N,N,3) for 3D or (N,3) for 1D, with N >= 2
domain_min, domain_max Optional domain structs with a borrowed double *values and uint64_t count; an explicit domain has exactly three finite RGB values
shaper Optional float32 (N,) shared output table for 3D LUTs, on the data's CUDA device; 1D LUTs reject this field

Each array view supplies data, rank, shape[4], byte strides[4], dtype, and device. Keep arrays and their metadata alive through validation. Validation does not copy or take ownership of either array. Table values may decrease or lie outside 0..1. Pixel table values are not read or checked for finiteness. Without a shaper, validation checks metadata on the CPU and allocates no device memory. A shaper may have positive, negative, or zero byte stride; its float32 sample addresses must be aligned and covered by the caller's CUDA allocation.

Omitted domains default to (0,0,0) and (1,1,1). Every lower endpoint must be strictly less than its upper endpoint. On success, the output contains normalized domain_min[3], domain_max[3], and edge. On failure, output fields remain unchanged. Size rules apply to every input and output struct.

Shaper validation reads samples after prior work on the supplied stream. It allocates scratch through the supplied allocator, or stream-ordered CUDA memory when the allocator is null. It synchronizes that stream to obtain the finite-value result and releases scratch before returning. It reports allocator failures with the existing callback ownership rules. Python retains its frozen Lut and Lut1D dataclasses and their original CuPy arrays; it passes CuPy's current stream and allocator to this same C++ validation.

EXR header inspection

pixtreme_io_read_exr_header(path, &header, &error) reads an EXR file and returns an owned pixtreme_exr_header_t *. The path is a UTF-8 filesystem string. The call clears its output to NULL at entry, including on failure; release any previously owned header before reusing its output variable. Inspection checks container and header data on the CPU and allocates no CUDA context or device memory. Deep parts can be inspected without reading their pixels. Header inspection also succeeds during CUDA stream capture.

The header queries use caller-owned output structures:

Function Output and initialization
pixtreme_exr_header_get_info(header, &info, &error) pixtreme_exr_header_info_t, initialized with PIXTREME_EXR_HEADER_INFO_INIT
pixtreme_exr_header_get_part(header, part, &info, &error) pixtreme_exr_part_info_t, initialized with PIXTREME_EXR_PART_INFO_INIT
pixtreme_exr_header_get_channel(header, part, channel, &info, &error) pixtreme_exr_channel_info_t, initialized with PIXTREME_EXR_CHANNEL_INFO_INIT

Part and channel positions are zero-based. Queries preserve outputs on failure. On success they preserve struct_size and write only complete fields that fit. PIXTREME_EXR_HEADER_INFO_MIN_SIZE includes every field through mappable, including width, height, and part_count. Part and channel information have a _MIN_SIZE of sizeof(size_t).

Header information supplies width, height, part_count, mappable, has_chromaticities, chromaticities[8], has_aces_flag, aces_flag, colorspace, and gamma. Dimensions come from the first part's data window. The eight chromaticity values contain the stored red, green, blue, and white xy coordinates, promoted from their file float32 values to doubles. Presence flags distinguish absent attributes from stored zero values. A true ACES container flag maps to ACES2065-1/linear; otherwise known chromaticities map to a colorspace, with sRGB primaries reported as Rec.709. Inspection supplies no fallback tokens. mappable is -1 when color metadata is absent, 0 when it cannot be mapped, and 1 when mapped. Missing tokens are null pointers.

Part information supplies name, deep, and channel_count. Channel information supplies name and the native dtype: float16, float32, or uint32. Part and channel names are borrowed until pixtreme_exr_header_release(header, &error); canonical color and dtype strings have library lifetime. Release accepts NULL. The header owns its storage independently of the path string and output structures.

auto header = pixtreme::io::read_exr_header("image.exr");
pixtreme_exr_header_info_t info = PIXTREME_EXR_HEADER_INFO_INIT;
header.get_info(&info);
pixtreme_exr_part_info_t part = PIXTREME_EXR_PART_INFO_INIT;
header.get_part(0, &part);
pixtreme_exr_channel_info_t channel = PIXTREME_EXR_CHANNEL_INFO_INIT;
header.get_channel(0, 0, &channel);

The move-only pixtreme::io::exr_header wrapper releases its handle on destruction. Missing files return PIXTREME_STATUS_FILE_NOT_FOUND; malformed or unreadable files return PIXTREME_STATUS_INTERNAL_ERROR. Parser failures retain the original diagnostic as an INVALID_ARGUMENT cause. Invalid query arguments return PIXTREME_STATUS_INVALID_ARGUMENT. Python read_header uses these same queries to construct its existing ImageHeader model.

EXR pixel reads

pixtreme_io_read_exr(path, options, allocator, stream, info, &frame, &error) reads selected channels into an owned GPU Frame. info is optional. Initialize pixtreme_exr_read_options_t with PIXTREME_EXR_READ_OPTIONS_INIT, or pass null options to select every default below:

Field Meaning and default
channels Optional pixtreme_channels_desc_t; null selects unique flat R, G, B and optional A
unchanged Zero promotes native samples to float32; nonzero retains homogeneous selected HALF, FLOAT, or UINT storage
colorspace, gamma Optional token claims; null uses mapped file metadata followed by the ACES2065-1 / linear defaults

Explicit channel labels retain their requested order and can use part.channel qualification. Ambiguous labels require qualification. Selecting a deep part or incompatible sampled dimensions fails with a reason and correction. An unchanged selection must contain one native dtype. UINT samples convert literally to float32 in default reads; they are not normalized. Color claims set metadata without transforming samples. The resulting Frame has no matrix claim.

For an ordinary stream, the reader selects that stream's CUDA device for the operation and restores the caller's previous device. Null, legacy and per-thread streams use the calling thread's current device. Result pixels and device scratch use the supplied allocator; null selects stream-ordered CUDA allocation. CPU codec work and host transfers can wait for the supplied stream. Synchronize the stream before consuming results on the host, and apply the existing Frame lifetime rules.

Pixel reads require host transfers and completion waits, so they cannot run during CUDA stream capture. After validating inputs, the call rejects capture before allocating device storage or transferring pixels. It returns INTERNAL_ERROR with a reason, the affected stream, and a correction to call outside capture. The rejection preserves the graph and leaves no temporary device allocations. Allocator and CUDA execution failures retain their status, reason, affected operation, and correction, and expose the original failure through the error cause.

pixtreme_exr_read_options_t options = PIXTREME_EXR_READ_OPTIONS_INIT;
options.unchanged = 1;
auto frame = pixtreme::io::read_exr("image.exr", &options, allocator, stream);
frame.release(stream);

The Frame output is cleared at entry, including on failure. A supplied info follows the header information size rules. Reinitialize it with PIXTREME_EXR_HEADER_INFO_INIT before each read: part_count == 0 means the reader did not write it; a positive count means the reader wrote all complete fields that fit. Both part_count and mappable fit in the mandatory prefix. The reader writes info after header parsing and colorspace/gamma validation, before channel selection. Header failures, invalid color claims and invalid output capacity preserve the initial info. Channel selection and codec failures retain the written header facts. Info owns no resources and needs no release; its canonical strings have library lifetime. Python uses this mappability result to preserve its warning before a channel error, while invalid color claims fail without that warning.

EXR pixel writes

pixtreme_io_write_exr(path, frame, options, allocator, stream, &error) writes one Frame. Initialize pixtreme_exr_write_options_t with PIXTREME_EXR_WRITE_OPTIONS_INIT, or pass null options for the defaults:

Field Meaning and default
compression An EXR compression token; null selects zip
dwa_level Borrowed const double *; null selects 45.0 for DWAA/DWAB. An explicit value must remain positive and finite after float32 conversion and requires DWA compression
dtype float16, float32, or uint32; null selects uint32 for a uint32 Frame and float16 otherwise
compression_received, dwa_level_received, dtype_received Optional diagnostic representations; null uses the corresponding input value

Tokens use the shared casefold, separator and alias rules. Missing trailing option fields use their defaults; the mandatory prefix is sizeof(size_t). Device dtype conversion uses the same full-range semantics as recode_dtype. Writing leaves the input's samples and metadata unchanged.

pixtreme_io_write_exr_channels(path, frames, count, options, allocator, stream, &error) combines a nonempty array of borrowed Frame handles. Its separate pixtreme_exr_channels_options_t, initialized with PIXTREME_EXR_CHANNELS_OPTIONS_INIT, has compression, dwa_level, compression_received, and dwa_level_received with the meanings above. It has no dtype override. Each input must store float16, float32, or uint32; those channels enter the codec as HALF, FLOAT, or UINT without conversion. All Frames must have the same width, height, CUDA device and colorspace.

Both writers take channel names from Frame labels, sort them for the file, and require globally unique, nonempty UTF-8 names of at most 255 encoded bytes without embedded nulls. Every Frame must have one label per data channel and a colorspace. Missing colorspace or mismatched label counts return INVALID_ARGUMENT; set them with pixtreme_frame_set_metadata before writing. Gamma and matrix may be unset: writing neither applies nor stores them. The common colorspace supplies chromaticities and the ACES2065-1 container flag.

The writers read samples and allocate device scratch on the supplied stream through the supplied allocator. Null selects stream-ordered CUDA allocation. Keep the input Frames and stream alive through the call. Encoding and file I/O can wait for that stream; they do not synchronize unrelated streams. The call returns after writing the file and releases temporary storage on the same stream. Both writers reject CUDA stream capture with INTERNAL_ERROR before dtype conversion or device scratch allocation. The diagnostic explains the required host transfers and completion waits and directs the caller to write outside capture. Rejection preserves the graph and leaves no temporary device allocations.

pixtreme_exr_write_options_t options = PIXTREME_EXR_WRITE_OPTIONS_INIT;
options.compression = PIXTREME_EXR_COMPRESSION_PIZ;
pixtreme::io::write_exr("output.exr", frame, &options, allocator, stream);

const pixtreme_frame_t *inputs[] = {color.get(), object_ids.get()};
pixtreme::io::write_exr_channels("mixed.exr", inputs, 2, nullptr, allocator, stream);

Input validation finishes before creating or truncating the destination. An encoding or I/O failure after validation carries a cause; this guarantee does not make writes atomic. An unwritable path or missing parent directory returns INTERNAL_ERROR with an IO_ERROR cause identifying the path and correction. The writers do not create parent directories. They return no new handles; the caller retains ownership of every input Frame.

Asynchronous GPU operations

For operations that submit GPU work, a successful status means that validation and submission to the supplied CUDA stream succeeded. It does not mean that GPU work has finished. The caller must synchronize that stream, for example with cudaStreamSynchronize(stream), and check CUDA's result before treating the work as complete. Synchronization may report a deferred GPU failure.

A returned error object never receives later failures. pixtreme does not promise to associate a deferred failure with a particular earlier operation. It does not start a persistent failure-monitoring thread or synchronize all streams behind the caller's back. CPU workers finish their call's work and collect failures inside C++; a public C entry reports one resulting error object.

Frame pixels and CUDA streams

pixtreme_frame_t owns writable, C-contiguous HWC pixels on one CUDA device. Height, width, and channels must be positive. Storage accepts the five canonical dtype tokens: float32, float16, uint8, uint16, and uint32. New allocations contain uninitialized pixels. Each Frame also holds color metadata and channel labels, which can be set through the metadata interface below.

Frame dtype inputs follow the Token Reference's casefold and separator rules. For example, FLOAT_32 and fLoAt.32 both return canonical float32 in Frame information. Tabs and other whitespace remain significant. The C header provides PIXTREME_DTYPE_FLOAT32, PIXTREME_DTYPE_FLOAT16, PIXTREME_DTYPE_UINT8, PIXTREME_DTYPE_UINT16, and PIXTREME_DTYPE_UINT32 as canonical string constants. Invalid dtype diagnostics preserve the rejected input and list the accepted canonical spellings. The library uses Unicode 15.0.0 full default casefold data; its Unicode License V3 is included at pixtreme/core/licenses/Unicode.txt.

#include <pixtreme/pixtreme.hpp>
#include <cuda_runtime_api.h>

void allocate_pixels(cudaStream_t stream) {
    pixtreme_frame_desc_t desc = PIXTREME_FRAME_DESC_INIT;
    desc.height = 1080;
    desc.width = 1920;
    desc.channels = 3;
    desc.dtype = "float32";
    desc.device = 0;
    auto frame = pixtreme::frame::create(desc, nullptr, stream);

    pixtreme_frame_info_t info = PIXTREME_FRAME_INFO_INIT;
    frame.get_info(&info);
    // Enqueue work using info.data on stream, then release in that order.
    frame.release(stream);
}

This example uses the caller's CUDA toolkit to operate on streams. The base SDK headers do not include CUDA headers. pixtreme_stream_t is a pointer to the forward-declared struct CUstream_st, compatible with CUDA 13.x cudaStream_t. The stream's device must match the Frame's device. For a special default stream, select that device on the calling host thread before the operation.

A null stream handle means CUDA's legacy default stream. pixtreme replaces null with cudaStreamLegacy before passing it to CUDA or allocator callbacks, including callbacks built with per-thread default streams. cudaStreamPerThread denotes the calling host thread's default stream; callbacks run on that same thread. Keep a supplied stream alive until the call returns and all work submitted to it has completed. pixtreme retains no stream handle after the call.

The C entry points are:

Function Ownership and result
pixtreme_frame_create Allocates HWC pixels and returns one owned Frame handle
pixtreme_frame_get_info Writes complete fields that fit in the caller's output struct
pixtreme_frame_release Takes a pointer to the handle, releases on the supplied stream, and clears the handle
pixtreme_frame_destroy Takes a pointer to the handle, releases without waiting, and clears the handle
pixtreme_frame_from_dlpack Takes ownership of a versioned managed tensor on success
pixtreme_frame_to_dlpack Returns a versioned managed tensor owning a reference to the pixels

pixtreme_frame_create, pixtreme_frame_from_dlpack, and pixtreme_frame_to_dlpack clear their output variable to NULL at the start of the call, including when the call fails. Pass an empty output variable. Release any handle or managed tensor you own before reusing its output variable; these calls do not release the previous value.

The information struct returns data, shape[3], dtype, device, and bytes. The pixel pointer is borrowed while a Frame or exported tensor retains the allocation. The dtype string has static library lifetime. Calls do not retain the description or output struct. Transfer a handle between threads when needed; finish all access to that handle before releasing it. Separate exported tensors may be released concurrently. Releasing an already cleared handle is a no-op.

Frame color metadata and channel labels

pixtreme_frame_set_metadata(frame, &metadata, &error) updates selected fields of pixtreme_frame_metadata_t. Initialize it with PIXTREME_FRAME_METADATA_INIT and combine PIXTREME_FRAME_COLORSPACE, PIXTREME_FRAME_GAMMA, PIXTREME_FRAME_MATRIX, and PIXTREME_FRAME_CHANNELS in fields. The corresponding fields are colorspace, gamma, matrix, and a pointer to a pixtreme_channels_desc_t. Unselected fields remain unchanged. Color names use the Token Reference matching and aliases. Selecting matrix with a null value removes the matrix. Invalid input leaves every field unchanged. C++ uses frame.set_metadata(metadata). Optional colorspace_received, gamma_received, and matrix_received strings affect only the received-value portion of diagnostics; they do not change matching or validation. When omitted, the ordinary C++ token diagnostics are retained.

Initialize channel descriptions with PIXTREME_CHANNELS_DESC_INIT. Supply either compact, such as "YCbCrA", or labels and count. Compact text is parsed greedily using the known, case-sensitive channel labels. Explicit sequences permit arbitrary nonempty labels, including duplicates and multi-character names, without sorting or normalization. Optional lengths describe each label in bytes; omitted lengths use null-terminated strings. compact_length can supply an explicit byte length. An optional received string supplies only the received-value representation in diagnostics. Frame metadata requires exactly as many labels as the pixel channel dimension.

The CPU-only pixtreme_channels_parse also normalizes channels independently of a Frame. It returns an owned pixtreme_channels_t *; its output is cleared to null on failure. pixtreme_channels_get borrows label pointers, byte lengths, and a count until pixtreme_channels_release. The query preserves outputs on failure. Release accepts null. The C++ equivalent is pixtreme::channels::parse(desc) followed by get(&labels, &lengths, &count); its destructor releases the labels.

pixtreme_frame_get_info additionally returns colorspace, gamma, matrix, labels, label_lengths, and label_count. Newly allocated or DLPack-imported Frames initially have null color names and no labels. Color names returned after assignment have canonical spelling and remain valid until process exit. Channel pointers and byte lengths remain valid until that Frame is released or its channels are successfully reassigned. Changing other fields leaves those pointers valid. Every output follows the struct capacity rules.

Borrowing and copying Frame pixels

The optional array tail of pixtreme_frame_desc_t points to a pixtreme_frame_array_t: data, HWC rank and shape[3], byte strides[3], dtype, device, and an optional owner. This view describes the source instead of the description's original height, width, channel count, dtype, and device fields. Initialize it with PIXTREME_FRAME_ARRAY_INIT. Supply writable CUDA storage covering every addressed sample; copied views can use negative or zero strides. Without an array, creation retains its original allocation behavior.

The description's copy field selects PIXTREME_FRAME_COPY_BORROW to borrow contiguous storage, PIXTREME_FRAME_COPY_ALWAYS to always copy, or PIXTREME_FRAME_COPY_IF_NEEDED to copy only when needed for contiguous storage. Copies use the supplied allocator and stream. Outside stream capture, a copy waits for its stream before returning, so the source view is borrowed only during the call and its owner is not retained. When the supplied stream is being captured, the copy is recorded in the graph and returns without waiting. The caller must keep both source and result pixel storage alive until all graph launches using them have completed. This applies to creation, data replacement, and deep copies; the source owner is not retained during capture either. Borrowing allocates no pixels and preserves the source pointer.

pixtreme_owner_t starts with struct_size, followed by value, context, and paired retain(context, value) and release(context, value, stream) callbacks. Initialize it with PIXTREME_OWNER_INIT. A successful borrow retains the owner once; a failed creation does not retain it. Final pixel cleanup releases it once, even when exported consumers or shallow copies outlive the original Frame. Callbacks run synchronously on the calling thread, acquire any required GIL themselves, and must not throw. pixtreme does not select a device before these callbacks; contexts must identify the owning device when their cleanup needs to select it. Explicit release forwards the supplied stream without waiting on the host; the release callback relinquishes pixels after work ordered onto that stream. Streamless destruction and a final DLPack deleter pass cudaStreamLegacy after the caller has confirmed completion on the host. The owner struct is borrowed only during creation; its known fields are copied. Without an owner, the caller keeps the pixels alive.

The owner output of pixtreme_frame_get_info borrows the external owner's value, or the allocator's owner value for an allocation. A default CUDA allocation has a null owner. The query transfers no ownership; the returned pointer remains valid until the Frame is released or its data is replaced.

pixtreme_frame_set_data(frame, &desc, allocator, stream, previous_stream, &error) and C++ frame.set_data(desc, allocator, stream, previous_stream) replace pixels without changing the handle or metadata. Validation, allocation, or copy failure preserves the old pixels and owner. Existing channel labels constrain the replacement channel count. After preparing the new pixels, replacement commits and relinquishes the old reference on previous_stream. The new stream belongs to the new pixels' device; previous_stream belongs to the previous pixels' device. The C++ three-argument overload passes one stream for both, for replacements on the same device. Replacement can change devices while retaining the same handle and metadata. Replacing an imported DLPack producer waits for previous_stream before invoking its streamless producer deleter. A final old owner is released once; outstanding consumers defer that cleanup. Cleanup after the committed replacement suppresses failures like destructor cleanup. Order uses of the old pixels onto previous_stream and source writes onto the new stream before replacement.

pixtreme_frame_clone(source, deep, allocator, stream, &result, &error) and C++ frame.clone(deep, allocator, stream) create distinct handles with independent metadata. With deep=0, pixels and their owner are shared; deep=1 copies pixels through the supplied allocator. The output is cleared to null on failure. The same final-release and stream-ordering rules apply to borrowed, allocated, and copied pixels.

Python Frame uses these interfaces and returns the original contiguous CuPy array from data. Non-contiguous input and deep copies allocate through the caller's current CuPy allocator; the resulting ndarray retains its MemoryPointer. The binding supplies CuPy's current stream, including when it explicitly releases its C++ handle. Construction failures release on the construction stream. Release callbacks relinquish CuPy references; CuPy's memory pool controls when blocks are returned and reused. The binding does not synchronize the device or use the streamless Frame destructor. Pixel copies outside capture wait only for their supplied stream. During capture, callers retain source and result arrays through completion of every graph launch using them. Python callers retain the ordinary CuPy rules for external array and stream use. Cleanup preserves an already-active Python exception; additional cleanup failures are reported through Python's unraisable-exception hook.

Pixel value operations

The six value operations return a new Frame with independent pixels and a copy of the input's colorspace, gamma, matrix, and channel labels. Even conversion to the current dtype allocates new pixels. The input remains unchanged.

C function Argument after the input Frame Pixel conversion
pixtreme_values_cast_dtype const char *dtype Literal numeric cast, following CuPy astype semantics without additional scaling, clipping, or rounding
pixtreme_values_recode_dtype const char *dtype Preserves normalized meaning across the full ranges of unsigned containers
pixtreme_values_quantize int64_t bit_depth Clips float32 to [0, 1], scales to the declared maximum, and rounds half away from zero
pixtreme_values_dequantize int64_t bit_depth Multiplies unsigned codes by the reciprocal of the declared maximum and returns float32
pixtreme_values_full_to_legal int64_t bit_depth Maps full-range float32 values to H.273 legal positions without clipping
pixtreme_values_legal_to_full int64_t bit_depth Maps H.273 legal positions to full-range float32 values without clipping

Each C function has this argument order: input const pixtreme_frame_t *, dtype or bit depth, const pixtreme_allocator_t *, pixtreme_stream_t, output pixtreme_frame_t **, and optional pixtreme_error_t **. All six clear the Frame output to NULL at entry, including on failure. Pass an empty output variable and release any previous handle before reusing it. Rejected inputs produce an error with the reason, affected input, and correction, without producing a result Frame.

Dtype arguments use the five dtype tokens, including their casefold and separator matching rules. recode_dtype maps unsigned integer containers to normalized [0, 1] values. Float-to-unsigned conversion clips to [0, 1] and rounds scaled half ties away from zero. Float-to-float conversion is a literal cast. Conversion to uint32 maps NaN to 2147483648. Integer-to-integer recoding uses the complete source and target code ranges.

Bit depth accepts 8, 10, 12, 14, or 16. Maximum code is 2^bit_depth - 1; the container is uint8 for 8 bits and uint16 for the other depths. quantize requires float32 input. dequantize requires the matching unsigned container and preserves codes above the declared maximum as values above 1. Both range operations require float32 input and channel labels drawn from R, G, B, Y, Cb, and Cr. R, G, B, and Y use the luma upper code 235; Cb and Cr use the chroma upper code 240, with lower code 16. These positions scale with the declared bit depth. Range conversion preserves negative values and values above 1. Frames without channel labels cannot select range semantics.

C++ calls the corresponding functions in pixtreme::values with a const pixtreme::frame & and receives an owned pixtreme::frame:

void convert_values(const pixtreme::frame &input,
                    const pixtreme_allocator_t *allocator,
                    pixtreme_stream_t stream) {
    auto codes = pixtreme::values::quantize(input, 10, allocator, stream);
    auto normalized = pixtreme::values::dequantize(codes, 10, allocator, stream);
    auto half = pixtreme::values::cast_dtype(
        normalized, PIXTREME_DTYPE_FLOAT16, allocator, stream);
    half.release(stream);
    normalized.release(stream);
    codes.release(stream);
}

The C++ range wrappers default to bit depth 8. Other dtype and bit-depth arguments are required. Allocator and stream default to null in all six C++ wrappers. Result pixels and any device scratch use the supplied allocator; null selects CUDA stream-ordered allocation. Work follows earlier operations on the supplied stream. Order input writes onto that stream before the call, and apply the existing Frame release rules to both input and result. A source Frame can be explicitly released on that stream immediately after submission. During capture, callers retain the source and result pixel storage until all graph launches using it have completed. Calls and result release preserve capture without waiting on the host.

Three CPU-only C queries expose the shared value definitions:

Function Result
pixtreme_values_bit_depths(&values, &count, &error) Borrows the supported int32_t depths and their count
pixtreme_values_get_domain(bit_depth, received, &info, &error) Validates an int64_t depth and writes its float32 numeric parameters
pixtreme_values_float32_guidance(dtype, &text, &error) Returns the existing conversion guidance for a supported storage dtype

Initialize pixtreme_value_domain_info_t with PIXTREME_VALUE_DOMAIN_INFO_INIT. Its fields are maximum, scale, container_dtype, legal_lower, legal_luma_extent, and legal_chroma_extent. scale is the reciprocal of maximum; the legal extents are upper position minus lower position. The query preserves struct_size, writes only complete fields that fit, and preserves the entire output on failure. received can supply a diagnostic representation; null uses the integer argument. It does not change validation. The list and returned strings are immutable library storage and require no release. All three queries preserve non-error outputs on failure and require no CUDA context.

The C++ equivalents are pixtreme::values::bit_depths(&values, &count), pixtreme::values::get_domain(bit_depth, &info, received), and pixtreme::values::float32_guidance(dtype). Python's value-domain helpers read these same definitions through the binding. Python bit-depth arguments retain their strict built-in int requirement; booleans, floating-point values, and strings are rejected with the supported-depth list.

Channel shuffle

pixtreme_channel_shuffle(outputs, count, allocator, stream, &result, &error) creates a float32 Frame by copying source channels or writing literal float32 fills. Its C++ wrapper is pixtreme::channel::shuffle(outputs, count, allocator, stream), returning an owned pixtreme::frame. Allocator and stream default to null in C++. No Python runtime is required.

Each output uses one pixtreme_shuffle_channel_t, initialized with PIXTREME_SHUFFLE_CHANNEL_INIT. The sequence preserves output order and permits duplicate output labels. At least one output must use a source Frame. Output height, width, colorspace and gamma come from the first source Frame, even when fills precede it. Every source must have float32 pixels, matching dimensions, colorspace, gamma, and CUDA device. Fills and copied pixels are never clipped; copying preserves float32 bits, including signed zeros and NaN payloads.

Field Meaning and omitted-field default
label Required nonempty literal output label
source Source Frame; null selects a fill in automatic mode
source_label Source label; its first exact byte match is selected
source_index Zero-based position when source_label is null; defaults to zero
fill Float32 fill when no Frame is selected; defaults to zero
source_kind PIXTREME_SHUFFLE_AUTO by default; PIXTREME_SHUFFLE_LABEL, PIXTREME_SHUFFLE_INDEX, and PIXTREME_SHUFFLE_FILL explicitly select those representations
provenance Optional original Frame supplying matrix provenance after an adapter converted the source; null uses source
label_length, source_label_length Explicit byte lengths, or UINT64_MAX for null-terminated strings
received Optional diagnostic representation of the source declaration

Each element starts with struct_size. Its mandatory prefix ends at label; omitted trailing fields use the defaults above. The next element starts struct_size bytes after the preceding element, allowing different historical layouts in one sequence. Allocate enough bytes for every declared element and initialize each element's size. Unknown tails are ignored. The sequence, labels, source handles and provenance handles are borrowed only during the call.

void route_channels(const pixtreme::frame &input,
                    const pixtreme_allocator_t *allocator,
                    pixtreme_stream_t stream) {
    pixtreme_shuffle_channel_t outputs[] = {
        PIXTREME_SHUFFLE_CHANNEL_INIT,
        PIXTREME_SHUFFLE_CHANNEL_INIT,
        PIXTREME_SHUFFLE_CHANNEL_INIT
    };
    outputs[0].label = "blue";
    outputs[0].source = input.get();
    outputs[0].source_label = "B";
    outputs[1].label = "first";
    outputs[1].source = input.get();
    outputs[1].source_index = 0;
    outputs[2].label = "negative";
    outputs[2].fill = -1.25f;
    auto result = pixtreme::channel::shuffle(outputs, 3, allocator, stream);
    result.release(stream);
}

Label selection always uses the first occurrence; position selection can select later duplicates. Python's internal composition of merge and stack uses positions to preserve each duplicate occurrence. Public Python shuffle keeps its keyword-based (frame, label) syntax and strict boolean adapt option.

RGB output labels clear matrix provenance, including mixed RGB/YCbCr output. Output without Y, Cb or Cr also clears provenance. Otherwise, only Y/Cb/Cr outputs that read a Frame contribute matrix claims. Equal non-null claims are preserved literally, including native; any missing contributing claim clears provenance. Different non-null claims are rejected, even when another claim is missing. Fills do not contribute claims. An adapter can set provenance to the original Frame so that conversion of its pixels does not change those claims.

C and C++ shuffle do not convert colorspaces or gamma. Mismatched color metadata is rejected with a reason and correction. Python callers can pass adapt=True to px.channel.shuffle; it converts mismatched sources once with px.color.rgb_to_rgb before calling C++ shuffle and derives matrix provenance from the original sources. Conversion requires R, G and B. Geometry must still match; resize explicitly before shuffle.

The C function clears its Frame output to null at entry, including on failure. Release any previous result before reusing the output variable. Invalid inputs produce no result and report the reason, affected output or source, and correction. Errors include absent labels, positions outside the source channel range, non-float32 sources, constants without a Frame, mismatched dimensions or color metadata, and conflicting matrix claims.

Result pixels use the supplied allocator, or CUDA stream-ordered allocation when it is null. Routing uses no device scratch: at most 32 channel descriptors are passed by value per kernel launch; larger sequences use successive launches on the same stream. Work follows prior operations on that stream. Explicit source and result release follow the existing Frame lifetime rules. During capture, keep source and result pixel storage alive through every graph launch using them. Shuffle and result release preserve capture without a host wait. Python supplies CuPy's current stream and allocator and keeps the ordinary CuPy array and memory-pool lifetime rules.

Allocator callbacks

Pass nullptr for the allocator to use cudaMallocAsync and cudaFreeAsync on the supplied stream. A custom pixtreme_allocator_t has these fields after struct_size:

Field Contract
context Caller-defined allocator context
allocate Receives context, byte count, and stream; writes a device pointer and an opaque owner value
deallocate Receives the same context, pointer, byte count, owner value, and release stream
retain_context Optional context retention callback
release_context Optional context release callback

Both allocation callbacks return pixtreme_status_t. A successful allocation transfers one pointer and owner value to pixtreme. Failed allocation outputs are ignored and never deallocated. A deallocation call consumes the owner value once, even if it fails; pixtreme never retries that owner. A failure returns a pixtreme error identifying the allocation or deallocation callback and its numeric status. Status 3 remains an allocation failure; other callback failures are classified as internal errors. Callbacks must contain C++ exceptions and return a status. The C boundary also contains an escaping callback exception and reports it.

Callbacks run synchronously inside the pixtreme call, on its calling host thread. A streamless final cleanup invokes them on the thread releasing the final reference. pixtreme does not call them on a worker pool or inside cudaLaunchHostFunc. It does not acquire Python's GIL or change the current CUDA device before a custom callback. The context must identify its target device; callbacks using Python values must handle the GIL themselves. The deallocation callback must preserve the supplied stream's ordering when relinquishing memory.

The context lifetime callbacks are optional as a pair. If present, pixtreme retains the context before allocation and releases it after the final deallocation, including a failed deallocation. Exported DLPack consumers keep the same context and owner alive. If the callbacks are omitted, the caller keeps the context alive until final cleanup. The allocator struct itself is borrowed only during creation; pixtreme copies its known fields.

Releasing pixels

The release path determines what the caller must guarantee:

Release path Caller guarantee Final cleanup
Explicit frame.release(stream) or pixtreme_frame_release Order allocation and every pixel use onto the supplied stream, including other streams and external pointer or DLPack users Calls the allocator deallocation callback with that stream; default CUDA deallocation is stream ordered
C++ destructor, move assignment replacing an existing Frame, or pixtreme_frame_destroy Confirm on the host that allocation and every GPU use have finished Uses cudaStreamLegacy; the C++ destructor and move assignment suppress cleanup failures
Exported DLPack tensor's deleter Confirm on the host that allocation and every GPU use have finished Uses cudaStreamLegacy; cleanup failures cannot be reported to the consumer

A Frame release leaves pixels alive while exported DLPack references remain. Only the final reference performs cleanup, using that final release path. A failed explicit cleanup still consumes and clears the handle; an invalid stream leaves the handle intact so the caller can correct it and retry.

An imported DLPack producer has a deleter without a stream argument. When an explicit Frame release drops its final reference, pixtreme synchronizes only the supplied stream on the host and then invokes the producer's deleter once on the calling thread. This call waits for that stream's work. If synchronization fails, pixtreme still calls the deleter and consumes ownership, then returns an error recording CUDA's status and that cleanup occurred. A nonfinal imported Frame release neither waits nor calls the producer's deleter. A streamless final release does not wait; its caller must already have confirmed completion.

DLPack exchange

The base C header forward-declares struct DLManagedTensorVersioned and does not include DLPack definitions. Include your own DLPack header when constructing tensors, or include <pixtreme/dlpack.h>. The SDK includes the unmodified DLPack v1.2 header at include/pixtreme/dlpack.h, with its Apache-2.0 license in include/pixtreme/LICENSE.DLPack and source information in ORIGIN.DLPack. CUDA and DLPack headers can precede or follow either base SDK header. A legacy DLPack 0.8 header can coexist with the base SDK headers, but exchanging tensors requires the 1.x versioned structure.

Import accepts DLPack major 1. A newer minor is accepted when its fields and flags can be interpreted. Unknown majors are rejected before reading dl_tensor. Import requires writable CUDA device memory, scalar lanes (lanes=1), one of the five supported storage dtypes, and positive HWC dimensions. Strides must be [width*channels, channels, 1] in elements. For every minor version of DLPack major 1, import also accepts a null strides pointer as C-contiguous HWC storage. Export always returns a non-null strides pointer. A nonzero byte offset is added to the data pointer and must give an aligned, nonoverflowing pixel address. The caller must supply enough accessible storage for all pixels.

Read-only tensors, unknown flags, unsupported dtypes or lanes, other device types, invalid dimensions, and unsupported strides return actionable errors. The IS_COPIED flag is understood. Failed import leaves the managed tensor owned by the caller and never calls its deleter. Successful import transfers that ownership to the Frame and eventually calls the producer's deleter once. Before import, the caller orders producer writes onto the supplied stream.

Export receives both the producer stream containing the pixel writes and the consumer stream that will read them. pixtreme records an event on the producer stream and makes the consumer wait; identical streams need no extra ordering. The caller first orders any outside writes onto the producer stream. Exported shape and strides remain valid until the consumer invokes its deleter once. Either the Frame or the consumer may be released first. Before the consumer's deleter, the caller confirms completion on the host as described above.