The code generation modules, runtime target bindings, and underlying high-performance InfiniWorkflow Engine Library are proprietary software covered strictly under the Photron USA Inc. Commercial License Agreement. Redistribution, production deployment, or embedding these binaries into commercial applications requires a valid enterprise subscription tier.
CodeGen turns your workflow into application or integration code. Depending on what you need, you can generate a complete sample application, a plugin, an extension, a notebook, or only the underlying processing code.
Creates a complete application with a configurable user interface. This is the option to use when you want to design the application's controls, views, features, appearance, and build behavior.
Creates an OpenFX plugin suitable for hosts such as Resolve and Nuke.
Creates a Photron PFV4 plugin for camera frame buffers.
Creates a MEX extension that can be called from MATLAB.
Creates an interactive Python notebook configured around the workflow.
Generates the core processing code without the sample application's GUI.
The Application Name identifies the generated application. Use a simple name such as my_app. The application name is also used when creating the generated application workspace.
Cancel closes the CodeGen setup without starting generation.
Proceed continues with the selected generation type. You must enter an application name before continuing from the application setup screen.
Creates a C++ sample application with a configurable GUI. Selecting this option takes you to the GUI Designer.
Creates a Python sample application with a configurable GUI. Selecting this option takes you to the GUI Designer.
Creates a C++ OpenFX plugin. This is intended for OpenFX-compatible hosts such as Resolve and Nuke.
Creates a C++ Photron PFV4 plugin for camera frame buffers.
Creates a MATLAB MEX extension that can be called directly from MATLAB scripts.
Creates a Python Jupyter Notebook configured for interactive use.
Generates C++ processing code without the sample application's GUI.
Generates Python processing code without the sample application's GUI.
The available generation choices and their C++/Python modes are defined by the CodeGen configuration.
The GUI Designer compiles your workflow into a standalone application - a generated C++ or Python program with its
own window, viewer, and parameter sidebar - instead of running inside INFINIWORKFLOW itself. It opens as a wizard:
drag parameters from the left tree onto the canvas to place them in the generated app's layout, switch to the
Views tab to choose which viewers show video output, Features to enable optional built-in tools (Zoom,
Measurement, Stabilization, Keystone, Telemetry), and Style to pick a theme preset (Dark, Nord, Cyberpunk,
Custom) and fine-tune colors, spacing, rounding and fonts. Build shows packaging/installer options before
generating the final app. Clicking Proceed sends the full layout/theme configuration as JSON to the backend,
which runs the same node-graph-to-code generator used for exporting group nodes as OFX/PFV4 plugins - see
docs/gui_designer_codegen.md for that JSON payload's full structure if you're extending the generator
itself rather than just using the wizard.
The designer is organized into five main areas:
Back returns to the CodeGen selection screen without proceeding with the current designer configuration.
Proceed accepts the current GUI configuration and starts generation.
The Canvas is where you design the application's control interface. You can drag workflow parameters onto the canvas, move them, resize them, select multiple controls, and organize controls into separate application tabs.
The left side of the designer contains the parameters available from your workflow.
Each workflow node appears as a group containing its available inputs and outputs.
Use the search field at the top of the Parameters area to quickly find a node, input, or output.
Click and drag a control to move it. Select a control to expose its resize handle, then drag the handle to change its size.
Multiple controls can be selected together. Multi-selection is useful for moving groups of controls or aligning them.
| Button | What it does |
|---|---|
| Align Left | Lines up selected controls using their left edges. |
| Align Center | Centers selected controls horizontally. |
| Align Right | Lines up selected controls using their right edges. |
Clear removes the controls from the current canvas so you can rebuild the layout.
Undo reverses recent layout changes. Redo restores a change that was undone.
Canvas tabs let you divide a complex application interface into separate pages of controls.
Click + Tab to create a new page. The new page starts with the name New Tab.
Double-click a tab name to rename it. Use meaningful names such as Camera, Processing, Output, or Advanced.
Click a tab to make it the active control page.
Use the tab's close button to delete it. You will be asked to confirm because all controls on that page are deleted with the tab. The final remaining tab cannot be deleted.
Select a control on the Canvas to edit its properties.
Choose which application page should contain the selected control.
Change the text displayed next to or inside the control.
For text labels, choose the color used by the label.
Text labels can be aligned Left, Center, or Right.
Camera-related controls can have camera-source constraints where applicable.
You can directly edit the control's X, Y, Width, and Height values for precise placement.
Use Remove Selected to delete the selected control from the interface.
The properties shown depend on the selected item. For example, text labels expose text and alignment settings, while camera controls can expose camera-specific settings.
The Views tab controls what the application displays as visual outputs and telemetry.
The Media area contains image/video-style outputs from the workflow.
Give each output a meaningful title. The title is what users see when interacting with the generated application.
Use Set as Default to make a media output the application's default view.
Remove an output from the application's configured views when it is not needed.
Telemetry outputs are numeric or other non-video values that can be displayed by the application.
The Features tab determines which capabilities are available to users of the generated application.
The playback section controls navigation through media or frames. Each toggle below only sets the initial state - the generated app also gets a Preferences > Playback Controls page with the same five checkboxes, so an end user can show or hide any of these buttons at runtime without a rebuild; that choice is saved to settings and restored on the next launch.
| Feature | Purpose |
|---|---|
| Seek First | Move to the first available frame or position. |
| Seek Prior | Move to the previous frame or position. |
| Seek Next | Move to the next frame or position. |
| Seek Last | Move to the final available frame or position. |
| Set Frame Number | Adds a button (crosshair/target glyph in C++, a calculator icon in Python) next to the timeline that opens a small modal with a calculator-style numeric keypad for typing an exact frame number to jump to. Off by default, like the four Seek toggles above. In a Multi-View group with Bidirectional Timeline on, using it also claims playback authority for the whole group, same as any other transport control. |
| Feature | Purpose |
|---|---|
| Zoom | Provides zoom functionality for the displayed content. |
| Image | Provides image-related viewing functionality. |
| Measurement | Provides measurement functionality. |
| Stabilize | Provides stabilization functionality. |
| Key Stone | Provides keystone correction functionality. |
| Pixel Probe | Provides pixel inspection functionality. |
| View | Provides view-related functionality. |
| Undo / Redo | Provides undo/redo related functionality. |
| Load / Save | Provides load/save of settings functionality. |
| Reset All | Provides reset all of settings functionality. |
| Telemetry | Provides telemetry display functionality. |
| Spawn | Provides the Multi-View menu, letting the running application launch and tile extra copies of itself ("followers") alongside the original ("master"). |
Features are managed using two lists:
| Button | Action |
|---|---|
| Move All | Enables all available features. |
| Move | Moves selected features into the Enabled list. |
| Move in the opposite direction | Returns selected enabled features to Available. |
| Move All in the opposite direction | Removes all optional features from Enabled. |
The Style tab controls the visual appearance and layout behavior of the generated application.
| Option | Result |
|---|---|
| Left | Application controls appear on the left side of the interface. |
| Right | Application controls appear on the right side. |
| Floating | Controls use a floating panel arrangement. |
| Option | Result |
|---|---|
| Embedded | The timeline is integrated with the viewing area. |
| Top | The timeline appears above the viewer. |
| Bottom | The timeline appears below the viewer. |
These controls determine how much space is used around windows, frames, items, and nested controls. Adjust them to make the interface more compact or more spacious.
These controls determine the amount of corner rounding and border thickness used throughout the interface, including windows, frames, popups, tabs, scrollbars, and controls.
Colors are grouped into logical sections:
The available presets include:
Use the search field to quickly locate a style setting.
The live preview shows how the application's controls, viewer, and timeline will be arranged. Use it while experimenting with layout settings so you can see the effect before generating the application.
The Build tab provides build and packaging options for C++ Sample Apps.
Enable Automatic Compilation (Build) controls whether the generated C++ application is automatically compiled.
Enable Automatic Execution (Run) controls whether the application is launched automatically after a successful build.
Enable the installer/package option when you want the generated application prepared for distribution.
Installer information includes:
| Setting | What to enter |
|---|---|
| Author / Company | The company or person responsible for the application. |
| Version | The application's release version. |
| App Identifier / Bundle ID | The unique application identifier. |
| Description | A description of the application. |
| License File Path | An optional license file to include with the package. |
| Package all executables | If checked, packages all executables located in the external/bin directory. If unchecked, only the main application executable is packaged. |
The interface describes packaging targets as NSIS Windows / DMG Mac / AppImage Linux.
After configuring the designer, click Proceed.
The generation request includes the selected generation mode, layout configuration, application name, workflow, and plugin/notebook options where applicable.
After generation, the application displays the generated files so you can inspect the result.
When the generated result contains a buildable application, Build & Run is available. The generated result is considered buildable when the expected CMake or Python application files are present.
The build workspace provides a console showing compiler/build messages. Use this output when diagnosing a failed build.
During compilation, the status can progress through workspace creation, CMake configuration, C++ compilation, deployment, and finally a successful running state.
Do not place every parameter on one page. Use Canvas tabs to separate groups of related controls.
Technical workflow parameter names may be useful to developers but confusing to end users. Rename displayed labels to describe what the setting actually does.
If your workflow produces several visual outputs, choose the output users are most likely to need first as the default.
Every enabled feature adds another capability to the generated application. Keep the interface focused on the application's purpose.
First build the controls and views, then use Style to refine the appearance and layout. This makes it easier to distinguish functional changes from visual changes.
For C++ applications, verify that the generated application builds and runs correctly before preparing the installer/package.
| Screen / Tab | Main purpose |
|---|---|
| CodeGen | Choose what kind of code or application to generate. |
| Canvas | Design the application's controls and control pages. |
| Views | Configure visual outputs and telemetry. |
| Features | Choose playback and application capabilities. |
| Style | Configure layout, fonts, spacing, colors, borders, rounding, and theme. |
| Build | Configure automatic C++ build/run and packaging. |
| Proceed | Accept the configuration and generate the application. |
| Back | Return to the previous configuration screen. |
Upon generating your application, you are provided with a Source code explorer interface. This interface allows you to:
The system also provides a Standalone ZIP Packager that compresses your application into a final distributable artifact. To successfully build full executable installers via this backend service, you must install NSIS (Nullsoft Scriptable Install System) on your build machine. Ensure that the makensis executable is explicitly added to your system's PATH environment variable so the automated packaging scripts can invoke it correctly.
If your application depends on the Visual C++ runtime, you can have the installer check for it and install it automatically — no code changes required.
Place your copy of VC_redist.x64.exe in the codegen_installer folder (located at the same level as the app folder). If not found there, the generator will check the assets folder. During generation, the installer:
VC_redist.x64.exe before continuing with your app's installation.This means bundling the redistributable is entirely optional and additive — including or omitting VC_redist.x64.exe is the only thing you need to do.
When you generate your C++ or Python applications, the system automatically includes default branding assets and licensing files so your app is ready to compile immediately. However, you can easily override these defaults to fully customize and white-label your application and its installer.
The code generator uses a hierarchical override directory codegen_installer (located at the same level as the app folder) to search for custom branding and configuration files. Below are the precise search priority paths for each supported asset type:
sidebar.bmp, topbar.bmp)codegen_installer/{{app_name}}/{{filename}}codegen_installer/{{filename}}app/static/assets/EULA.txt, LICENSE.txt)codegen_installer/{{app_name}}/{{filename}}codegen_installer/{{filename}}{{filename}}manifest.txt, localize.txt)codegen_installer/{{app_name}}/{{filename}}codegen_installer/{{filename}}assets/{{filename}} for manifest.txt; assets/localize/{{filename}} for localize.txt and every per-language file (see the Localization section above).favicon.ico, splashscreen.png)codegen_installer/{{app_name}}/{{filename}}codegen_installer/{{filename}}assets/{{filename}}app/static/ or app/static/assets/user_guide.html)codegen_installer/{{app_name}}/user_guide.htmlcodegen_installer/user_guide.htmlapp/static/assets/user_guide.htmlFor safety and traversal protection, entries in manifest.txt must comply with strict validation rules:
.. segments is strictly prohibited and skipped.assets/ or Extensions/ directories.assets or Extensions) to copy the entire repository is rejected.There is no need to modify any code, scripts, or configuration files. Just drop your preferred assets into the override locations, and the next time you trigger the code generation, your custom branding will automatically replace the defaults!
If you have a code-signing process, you can have it applied automatically during generation.
Place a signing script in your project's assets folder:
signing.bat (or signing.cmd).signing.sh (or a script named signing with no extension).The script should accept a single argument — the full path to the file to sign — and sign that file in place (for example, by calling out to signtool on Windows or codesign/your own signing tooling on macOS/Linux).
When a signing script is found:
If no signing script is present in assets, signing is skipped entirely and generation proceeds exactly as it would otherwise — no extra steps, no changes to your build.
Note: The uninstaller that ships inside the installer is not currently signed as part of this process.
To translate or customize text within your generated application, the engine supports drop-in localization dictionaries. Follow these steps to implement custom strings:
assets/localize directory within your project workspace (create it if it doesn't exist yet).localize.txt (or one of the language-specific filenames below).=) or a colon (:) as a delimiter.Example assets/localize/localize.txt formatting:
# This is a comment line
my_app = My Custom App Name
Zoom In : Magnify View
Tracking Progress = Processando o Rastreamento
Note: The engine will automatically load these pairs at runtime and replace corresponding tr("...") text wrapping instances in the UI. Localization files live under an assets/localize/ subfolder (not flat in assets/) to keep that folder from accumulating one file per bundled language - the icons used by "..." buttons live in their own assets/icons/ subfolder for the same reason.
The Style tab of the GUI Designer (for the standalone C++/Python sample-app targets - not shown for PFV4 plugin exports) has a Default Language dropdown next to the theme preset picker. This sets which bundled dictionary the generated app starts with; the end user can still change it afterward via Preferences > Language at runtime, same as always.
Beyond the single always-loaded localize.txt above, the generated application also ships with a Preferences > Language dropdown that lets the end user switch between several bundled dictionaries at runtime, without editing any files themselves:
| Language combo entry | Dictionary file |
|---|---|
| English | localize.txt - the same file described above (empty by default, so every tr("...") call falls back to its own English key text unless you've added overrides). Also the default unless a different Default Language is set in the GUI Designer's Style tab. |
| German | localize_de-DE.txt |
| Spanish | localize_es-ES.txt |
| French | localize_fr-FR.txt |
| Japanese | localize_ja-JP.txt |
| Chinese | localize_zh-CN.txt |
Each of these files uses the exact same key-value format as localize.txt above, lives in the same assets/localize/ subfolder, and is resolved through the same codegen_installer/<app_name> → codegen_installer → workspace assets/localize/ override hierarchy described under "Extra Assets Copy List and Localization" above - a language file that can't be resolved anywhere for a given build simply isn't bundled, and its combo entry falls back to English text at runtime instead of failing to build.
.ini settings file ([Localization] section) and restored automatically the next time the application starts.The generated application's user interface can be extended with application features by adding them in the Features tab of the GUI designer.
= or I to zoom in, - or O to zoom out, and F to reset the view to fit the screen.R/G/B/A hotkeys directly - each shown next to its menu entry. Pressing the hotkey for whichever channel is already active switches back to "RGBA", the same as clicking that menu entry a second time..mp4/.avi video, numeric/matrix ports as .csv. Users pick the node, choose which of its ports to capture (each with its own filename and, for video, codec), set a frame range, and are prompted for a destination folder when export starts (with a single confirmation if any file there would be overwritten). Not available in a spawned Multi-View window.Space key toggles Play/Pause from anywhere in the window.Enabling the "Spawn" feature lets the running application launch up to three extra copies of itself ("followers") and tile them alongside the original window ("master") - useful for showing several views of the same live data side by side.
| Child category | Configures |
|---|---|
| Border | The focus-highlight border color/thickness. |
| Apps | Which executable, if not this one, launches in each of the three spawn slots. |
| Sync | Three independent checkboxes, all on by default - Frame (playback position/play-pause), Pan/Zoom, and Channels (R/G/B/A/RGBA). Turning one off stops just that behavior from propagating to followers. Below these, a Bidirectional Timeline checkbox (off by default) - see below. |
Every generated application (C++ and Python alike) writes a small diagnostic log file on every run, so a user who hits a problem has something concrete to send back rather than a screenshot of a crash.
%LOCALAPPDATA%\<AppName>\logs\<AppName>.log on Windows, ~/Library/Logs/<AppName>/<AppName>.log on macOS, ~/.local/share/<AppName>/logs/<AppName>.log on Linux. The exact path is also shown directly in Preferences > Logging so a user never has to go hunting for it.Regardless of your chosen output language, your code follows a matching workflow lifecycle to manage memory boundaries and thread execution models cleanly:
addMatrixLive and addImageLive.setNodeInputCudaFrame/attachNodeInputCudaFrame to push a live CUDA device frame into a workflow, and addCudaImageLive/add_cuda_image_live to receive one back out without a forced GPU→CPU download. These are library-level capabilities only - no generated project wires them up automatically today, so using them means writing the node names/IDs and calls into your own code by hand. See GPU-Resident (CUDA) Live APIs below.
OVERALL C++ WORKFLOW LIFECYCLE USAGE HANDBOOK:
INITIALIZATION & SETUP:
AppLogic app;
if (!app.initEngine()) { // Initializes Photron Engine
// Handle DLL loading failure safely
return -1;
}
app.setupWorkflow(); // Hydrates pipeline handles, configurations, and topology
LIVE PARAMETER MODIFICATION:
Modifies parameter values type-safely via Port Enums mapped dynamically:
app.updateNodeVal(NodeID::AI_WELDING, AI_WeldingInputs::MODEL, std::string("..."));
LIVE VIEWER API:
Registers a live callback to view output of a node's port
app.addImageLive(NodeID::AI_WELDING, 2, "live_ai_welding_2", on_ai_welding_2_callback);
app.addMatrixLive(NodeID::WELD_ANALYSIS, 0, "live_weld_analysis_0", on_weld_analysis_0_callback);
app.addNumericLive(NodeID::WELD_ANALYSIS, 2, "live_weld_analysis_2", on_weld_analysis_2_numeric_callback);
Below is a typical lifecycle implementation mapping the exported C++ target bindings based on the engine's initialization architecture:
#include "app_logic.h"
#include <iostream>
#include <mutex>
#include <opencv2/opencv.hpp>
// Define a global lock and frame buffer
std::mutex g_data_mutex;
cv::Mat g_ai_welding_2_frame;
// Define the live image callback
void on_ai_welding_2_callback(const cv::Mat& frame) {
std::lock_guard<std::mutex> lock(g_data_mutex);
if (!frame.empty()) {
frame.copyTo(g_ai_welding_2_frame);
}
}
int main() {
AppLogic app;
// 1. Initialize engine library bindings
if (!app.initEngine()) {
std::cerr << "Failed to load photron_engine runtime binary." << std::endl;
return -1;
}
// 2. Hydrate configuration parameters and thread topology
app.setupWorkflow();
// 3. Connect Live View Listeners
app.addImageLive(NodeID::AI_WELDING, 2, "live_ai_welding_2", on_ai_welding_2_callback);
// 4. Update active node attributes type-safely via generated Port Enums
app.updateNodeVal(NodeID::AI_WELDING, AI_WeldingInputs::MODEL, std::string("path/to/model.onnx"));
// Main UI or execution loop would continue here...
return 0;
}
To compile your pipeline using native C++ bindings, ensure your built application can access all engine library links correctly by executing the following steps:
app_logic.h and app_logic.cpp files and add them to your C++ application project layout.| Method | Description & Examples |
|---|---|
__init__ |
Initializes the Photron engine, configures the PythonNodeFactory, and sets up essential FFI callback types and memory management to prevent garbage collection crashes. |
initialize_workflow |
Entry point for building the computational pipeline; instantiates C++ nodes and Python proxies, sets node parameters, and establishes edge connections. |
add_live_viewer |
Creates a live viewer node, connects it to a source, and registers a callback for C++ nodes
Examples: 1. Numeric: app.add_live_viewer(NodeID.NUMERIC_A_B, 0, 'numeric', lambda val: print(f"Value: {val}"))2. String: app.add_live_viewer(NodeID.STR, 0, 'string', lambda s: print(f"String: {s.decode('utf-8')}"))3. Matrix: app.add_live_viewer(NodeID.DARKNET_YOLO7_CLASSIFICATION, 0, 'matrix', lambda mat: print(f"Mean: {mat.mean()}"))4. Image: app.add_live_viewer(NodeID.MOVIE_READER, 0, 'image', lambda img: print(f"Shape: {img.shape}"))
Note: Automatically wraps raw C++ pointers into NumPy arrays and uses arr.copy() to ensure data persistence.
|
execution_loop |
A daemon thread that manages node updates, triggers re-execution for "dirty" Python nodes, and handles stream inspection logging. |
update_node_val |
Updates node parameters; if the target is a Python node, it marks it as "dirty" for re-execution; otherwise, it calls the engine’s parameter setter. |
view_node |
Designates a node/port for real-time inspection. For Python nodes, it enables internal monitoring; for C++ nodes, it triggers native engine rendering. |
update_from_cpp |
Synchronizes Python state with C++ updates, parsing binary streams and mapping raw data buffers to NumPy arrays. |
python_proxy_callback |
A static FFI handler invoked by the C++ engine to flag specific proxy nodes as "dirty" when their port data updates. |
stop |
Signals the execution thread to stop and gracefully shuts down the workflow pipeline. |
setNodeInputCudaFrame |
GPU-resident (CUDA) counterpart to setNodeInputMemoryFrame - pushes a live CUDA device frame (a raw pointer, e.g. from PyCUDA/CuPy/torch's .data_ptr()) into a cuda.memory_source node. Copies (cudaMemcpy2D) into a buffer the engine owns - accepts any input pitch. Caller may reuse/free the pointer the instant this call returns. See GPU-Resident (CUDA) Live APIs below.
|
attachNodeInputCudaFrame |
Zero-copy sibling of setNodeInputCudaFrame - adopts the device pointer directly at push time, no copy. The pointer must already be packed (pitch == width * num_channels * bytesPerPixel) - rejected (returns False) otherwise. The optional release_callback fires once the engine has copied the frame into its own output buffer - that's when it's safe to reuse/free the pointer.
|
add_cuda_image_live |
GPU-resident counterpart to add_live_viewer(..., 'image', ...) - the callback receives a live CUDA device pointer instead of a downloaded NumPy array, so the frame never leaves the GPU.
Example: app.add_cuda_image_live(NodeID.CUDA_NODE, 0, lambda ptr, w, h, pitch, c, depth, device: ...)
Note: This is a synchronous call on the viewer node's own thread - the callback must finish reading the pointer before returning; the engine reuses that buffer on its very next tick. There is no separate release signal like attachNodeInputCudaFrame has on the push-in side.
|
A separate, additive pair of APIs for working with GPU frames without ever forcing a round-trip through CPU
memory. Not used by any generated project automatically - cuda.memory_source and
live.viewer.cuda_image are internal node types with no catalog entry (they can't be dragged onto a
canvas), so using either API means adding the corresponding node to your own code-generated workflow by hand
and calling these methods directly, the same way you'd call addImageLive/add_live_viewer
today. Both APIs are safe no-ops when the engine binary in use wasn't built with CUDA support.
cuda.memory_source node) - two methods, matching the two
ways a GPU frame is typically already available to you:
setNodeInputCudaFrame (C++) / setNodeInputCudaFrame (Python) - copies the frame in, accepting any pitch. The safe default.attachNodeInputCudaFrame - zero-copy at push time, but requires packed rows (no padding) and a release callback to know when the buffer is free again.live.viewer.cuda_image node) - addCudaImageLive
(C++) / add_cuda_image_live (Python), the GPU-resident counterpart of addImageLive/
add_live_viewer(..., 'image', ...). The callback is synchronous and receives a device pointer
valid only for the duration of the call.
// Push a live CUDA frame in (copy path - accepts any pitch)
app.setNodeInputCudaFrame(NodeID::CUDA_SOURCE, devicePtr, width, height, pitch, numChannels, cvDepthBits, cudaDevice);
// Push a live CUDA frame in (zero-copy path - devicePtr must already be packed)
app.attachNodeInputCudaFrame(NodeID::CUDA_SOURCE, devicePtr, width, height, pitch, numChannels, cvDepthBits, cudaDevice,
[](void* userData) { /* devicePtr is now safe to reuse/free */ }, nullptr);
// Get a live CUDA frame out
app.addCudaImageLive(NodeID::CUDA_NODE, 0, "live_cuda_0",
[](const void* devicePtr, int width, int height, int pitch, int numChannels, int cvDepthBits, int cudaDevice) {
// devicePtr is only valid for the duration of this call
});
# Push a live CUDA frame in (copy path - accepts any pitch)
app.setNodeInputCudaFrame(NodeID.CUDA_SOURCE, device_ptr, width, height, pitch, num_channels, cv_depth_bits, cuda_device)
# Push a live CUDA frame in (zero-copy path - device_ptr must already be packed)
app.attachNodeInputCudaFrame(NodeID.CUDA_SOURCE, device_ptr, width, height, pitch, num_channels, cv_depth_bits, cuda_device,
release_callback=lambda: print("device_ptr is now safe to reuse/free"))
# Get a live CUDA frame out
app.add_cuda_image_live(NodeID.CUDA_NODE, 0,
lambda device_ptr, width, height, pitch, num_channels, cv_depth_bits, cuda_device: None) # must finish reading device_ptr before returning