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.