OmniPVD Recording – Capture Physics Internals to FILE or TCP#
This tutorial shows how to record OmniPVD data from ovphysx to .ovd files
for offline inspection or stream it live to a TCP listener. OmniPVD captures
selected PhysX object and contact state each frame so you can debug and
visualize physics behavior.
Prerequisites#
Install ovphysx and confirm native libraries load.
A USD scene with physics objects. This tutorial uses
links_chain_sample.usda, which ships with every package underovphysx/samples/data/in the wheel,<sdk-root>/samples/data/in the C/C++ SDK, andtests/data/in a repository checkout.For offline inspection: a compatible Kit application with the OmniPVD extension.
Required Config#
Set GlobalConfigKey.OVD_RECORDING_CAPABLE to True with module-level
set_global_config() before PhysX(). In C, initialize and set
OVPHYSX_GLOBAL_CONFIG_OVD_RECORDING_CAPABLE with
ovphysx_config_value_bool(true) before plain ovphysx_create_instance().
This process-wide BOOL bootstrap option installs the provider and sampler when
PxPhysics is created. It defaults to false; without it, late start fails.
Capability alone leaves output idle but retains sampler, listener, mutex, and
scene readback costs. Only the default disabled configuration avoids those costs.
The scoped setter is rejected after the first successful create until successful
shutdown with no live instances, including after all native instances are destroyed.
Python’s PhysX.destroy() releases its lifecycle owner; releasing the last Python
owner performs that shutdown, provided no native instances remain, and reopens
the setter. Its configured value survives shutdown.
The getter reports the configured bool. Failed creates do not freeze the setter.
Attach and initialize the scene, then call start_recording() with an
OmniPvdDestination. To capture from the first simulated frame, start before
the first step. FILE uses one exact path whose parent exists; TCP requires a
ready listener. Stop finalizes the output and allows a new destination on the
same instance. TCP is synchronous trusted plaintext.
Creation, attach, and reattach never start output automatically. Old startup configuration is rejected; see the API migration guide.
Code#
Python#
This sample provisions capability, attaches links_chain_sample.usda, records
to an exact FILE destination before the first step, then stops and tears down:
# @implements REQ-CAPI-OVSTAGE-XFORM-001
# @covers AC-1
import glob
import os
import tempfile
from pathlib import Path
import ovphysx
from ovphysx.samples.python_samples.schema_support import physx_schema_paths
from ovphysx import GlobalConfigKey, OmniPvdDestination, PhysX, set_global_config
_physx_schemas_registered = False
def attach_scene(physx, usd_path, stage_name):
import ovstage
if not ovstage.population.available():
raise RuntimeError("ovstage population bridge is unavailable")
# Register external PhysX and Newton schemas before the first population.
global _physx_schemas_registered
if not _physx_schemas_registered:
ovstage.population.register_usd_schemas(
[*[str(p) for p in physx_schema_paths()], str(ovphysx.newton_schema_root())]
)
_physx_schemas_registered = True
stage = ovstage.Stage(stage_name)
ordinal = 1
try:
ovstage.population.open_usd(stage, str(usd_path), ordinal=ordinal, domains=ovstage.PopulationDomain.ALL)
stage.advance_write_floor(ordinal=ordinal).wait()
physx.wait_task(physx.attach_ovstage(stage, read_ordinal=ordinal))
return stage
except Exception:
if physx.is_stage_attached(stage):
physx.wait_task(physx.detach_ovstage())
stage.destroy()
raise
# Use a temporary directory for recording output.
# Replace with your own path for persistent recordings.
output_dir = tempfile.mkdtemp(prefix="ovphysx_pvd_")
print(f"OmniPVD recording directory: {output_dir}")
# Capability must exist before the shared runtime creates PxPhysics.
set_global_config(GlobalConfigKey.OVD_RECORDING_CAPABLE, True)
physx = PhysX()
# Prefer package data so a copied sample works. Fall back to the checked-in
# sample's adjacent data directory when package data is absent.
usd_path = (
Path(ovphysx.__file__).resolve().parent
/ "samples"
/ "data"
/ "links_chain_sample.usda"
)
if not usd_path.is_file():
usd_path = Path(__file__).resolve().parent.parent / "data" / "links_chain_sample.usda"
if not usd_path.is_file():
raise FileNotFoundError(f"ovphysx sample data is missing: {usd_path}")
print(f"Loading USD scene: {usd_path}")
stage = attach_scene(physx, usd_path, "ovphysx-omnipvd-sample")
physx.wait_all()
physx.start_recording(OmniPvdDestination.file(str(Path(output_dir) / "capture.ovd")))
# Run the simulation. OmniPVD captures each frame automatically.
dt = 1.0 / 60.0
n_steps = 120 # 2 seconds at 60 Hz
print(f"Simulating {n_steps} steps...")
for i in range(n_steps):
physx.step_sync(dt)
print("Simulation complete.")
# Stop finalizes the exact destination before releasing the borrowed stage.
physx.stop_recording()
physx.wait_task(physx.detach_ovstage())
stage.destroy()
physx.destroy()
print("Runtime cleanup complete; recording retained for inspection")
# List the produced .ovd files
ovd_files = glob.glob(os.path.join(output_dir, "*.ovd"))
if ovd_files:
print(f"\nRecorded {len(ovd_files)} OVD file(s):")
for f in ovd_files:
size_kb = os.path.getsize(f) / 1024
print(f" {os.path.basename(f)} ({size_kb:.1f} KB)")
print("\nOpen these files in a Kit app with OmniPVD to inspect simulation data.")
else:
print("\nWARNING: No .ovd files found. Check runtime logs for errors.")
C++#
CMakeLists.txt
The C++ sample uses the same find_package(ovphysx) and ovphysx::ovphysx
target as every other sample:
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.16)
project(OmniPvdRecordingCpp CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(ovphysx REQUIRED)
add_executable(omnipvd_recording_cpp main.cpp)
target_link_libraries(omnipvd_recording_cpp PRIVATE ovphysx::ovphysx ovphysx::ovstage)
target_include_directories(omnipvd_recording_cpp PRIVATE "${CMAKE_CURRENT_LIST_DIR}/../common")
get_filename_component(OVPHYSX_TEST_DATA_DIR "${CMAKE_CURRENT_LIST_DIR}/../../data" ABSOLUTE)
target_compile_definitions(omnipvd_recording_cpp PRIVATE
OVPHYSX_TEST_DATA="${OVPHYSX_TEST_DATA_DIR}"
)
if(WIN32)
ovphysx_copy_runtime_dlls(omnipvd_recording_cpp)
# OVStage is not copied (it loads its plugins relative to itself), so its
# directories have to reach the process through PATH.
ovphysx_set_ovstage_runtime_env(omnipvd_recording_cpp)
endif()
Source
The C++ sample starts an exact FILE recording after attaching the scene and before the first simulation step:
#include "ovphysx/ovphysx.h"
#include "ovphysx/ovphysx_config.h"
#include "ovstage_sample.h"
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <string>
namespace fs = std::filesystem;
// Count files matching *.ovd in the given directory.
static int count_ovd_files(const fs::path& dir) {
int count = 0;
std::error_code ec;
for (const auto& entry : fs::directory_iterator(dir, ec)) {
const auto name = entry.path().filename().string();
if (name.size() > 4 && name.substr(name.size() - 4) == ".ovd")
++count;
}
return count;
}
static int run(void)
{
// Create a fresh output directory for this invocation in the caller-owned working directory.
std::error_code ec;
const fs::path working_dir = fs::current_path(ec);
if (ec) {
const std::string reason = ec.message();
fprintf(stderr, "Failed to resolve the working directory: %s\n", reason.c_str());
return 1;
}
const std::chrono::steady_clock::rep unique_suffix =
std::chrono::steady_clock::now().time_since_epoch().count();
fs::path output_dir =
working_dir / ("ovphysx_pvd_cpp_sample_" + std::to_string(unique_suffix));
if (!fs::create_directory(output_dir, ec)) {
const std::string reason = ec ? ec.message() : "directory already exists";
fprintf(stderr, "Failed to create directory '%s': %s\n",
output_dir.string().c_str(), reason.c_str());
return 1;
}
std::string dir_str = output_dir.string();
printf("OmniPVD recording directory: %s\n", dir_str.c_str());
ovphysx_instance_handle_t handle = 0;
ovphysx_result_t r = ovphysx_initialize();
if (r.status != OVPHYSX_API_SUCCESS) {
ovphysx_string_t err = ovphysx_get_last_error();
fprintf(stderr, "Failed to initialize ovphysx: %.*s\n",
(int)(err.ptr ? err.length : 0), err.ptr ? err.ptr : "");
return 1;
}
r = ovphysx_set_global_config(
OVPHYSX_GLOBAL_CONFIG_OVD_RECORDING_CAPABLE, ovphysx_config_value_bool(true));
if (r.status != OVPHYSX_API_SUCCESS) {
const ovphysx_string_t err = ovphysx_get_last_error();
fprintf(stderr, "Failed to enable OVD recording capability: %.*s\n",
static_cast<int>(err.length), err.ptr ? err.ptr : "");
ovphysx_shutdown();
return 1;
}
r = ovphysx_create_instance(&handle);
if (r.status != OVPHYSX_API_SUCCESS) {
ovphysx_string_t err = ovphysx_get_last_error();
fprintf(stderr, "Failed to create PhysX instance: %.*s\n",
(int)(err.ptr ? err.length : 0), err.ptr ? err.ptr : "");
ovphysx_shutdown();
return 1;
}
ovphysx_sample_stage_attachment_t stage_attachment = {};
if (!ovphysx_sample_attach_usd_with_ovstage(
handle, OVPHYSX_TEST_DATA "/simple_physics_scene.usda", &stage_attachment)) {
ovphysx_destroy_instance(handle);
ovphysx_shutdown();
return 1;
}
const std::string file_path = (output_dir / "capture.ovd").string();
ovphysx_omnipvd_destination_t destination = {};
destination.transport = OVPHYSX_OMNIPVD_TRANSPORT_FILE;
destination.file_path = ovphysx_cstr(file_path.c_str());
r = ovphysx_ovd_start_recording(handle, &destination);
if (r.status != OVPHYSX_API_SUCCESS) {
const ovphysx_string_t err = ovphysx_get_last_error();
fprintf(stderr, "Failed to start OVD recording: %.*s\n", static_cast<int>(err.length), err.ptr);
ovphysx_sample_destroy_stage(handle, &stage_attachment);
ovphysx_destroy_instance(handle);
ovphysx_shutdown();
return 1;
}
// Run simulation steps. OmniPVD records each frame.
const float dt = 1.0f / 60.0f;
const int n_steps = 10;
printf("Running %d simulation steps...\n", n_steps);
for (int i = 0; i < n_steps; i++) {
ovphysx_result_t step_r = ovphysx_step_sync(handle, dt);
if (step_r.status != OVPHYSX_API_SUCCESS) {
ovphysx_string_t err = ovphysx_get_last_error();
fprintf(stderr, "Step %d failed: %.*s\n", i,
(int)(err.ptr ? err.length : 0), err.ptr ? err.ptr : "");
ovphysx_sample_destroy_stage(handle, &stage_attachment);
ovphysx_destroy_instance(handle);
ovphysx_shutdown();
return 1;
}
}
printf("Simulation complete.\n");
r = ovphysx_ovd_stop_recording(handle);
const bool stopped = r.status == OVPHYSX_API_SUCCESS;
// Finalize before releasing the borrowed stage.
ovphysx_sample_destroy_stage(handle, &stage_attachment);
ovphysx_destroy_instance(handle);
ovphysx_shutdown();
// At least one OVD file has to exist. A non-zero return lets CI catch regressions.
int ovd_count = count_ovd_files(output_dir);
if (stopped && ovd_count > 0) {
printf("Recorded %d OVD file(s) in %s\n", ovd_count, dir_str.c_str());
printf("Runtime cleanup complete; recording retained for inspection\n");
return 0;
}
fprintf(stderr, "FAIL: No OVD files found in %s\n", dir_str.c_str());
return 1;
}
int main(void) {
int rc = run();
return rc;
}
Late TCP Recording#
Opt in before creating the first instance, before attaching and stepping the
scene. Start the trusted-plaintext TCP listener before calling
start_recording(); the connect is synchronous. This complete sequence
simulates before recording, records to TCP, stops, continues simulating, and
then records to a FILE destination. Replace scene.usda with a path to your own
USD scene, such as the links_chain_sample.usda file named in the
prerequisites:
import ovstage
from ovphysx import GlobalConfigKey, OmniPvdDestination, PhysX, newton_schema_root, set_global_config
from ovphysx.samples.python_samples.schema_support import physx_schema_paths
set_global_config(GlobalConfigKey.OVD_RECORDING_CAPABLE, True)
physx = PhysX()
# Register PhysX and Newton schemas before the first USD schema read (population or export).
ovstage.population.register_usd_schemas([*[str(p) for p in physx_schema_paths()], str(newton_schema_root())])
stage = ovstage.Stage("late-recorded-scene")
ovstage.population.open_usd(
stage, "scene.usda", ordinal=1, domains=ovstage.PopulationDomain.ALL
)
stage.advance_write_floor(ordinal=1).wait()
physx.wait_task(physx.attach_ovstage(stage, read_ordinal=1))
physx.step_sync(1 / 60) # work before recording is allowed
physx.start_recording(
OmniPvdDestination.tcp("127.0.0.1", 5425, timeout_ms=3000)
)
assert physx.is_recording()
physx.step_sync(1 / 60) # streamed to the TCP listener
physx.stop_recording()
physx.step_sync(1 / 60) # simulation continues without recording
physx.start_recording(OmniPvdDestination.file("capture.ovd"))
physx.step_sync(1 / 60) # written to capture.ovd
physx.stop_recording()
physx.wait_task(physx.detach_ovstage())
physx.destroy()
stage.destroy()
C and C++#
To adapt the complete C++ sample in Code, keep its initialization,
stage attachment, and cleanup. The sample already enables
recording capability and starts recording explicitly. Keep this configuration
when replacing its FILE destination and recording loop with the TCP sequence
below. Use create_recording_instance() for instance creation, checking its
returned status. Remove the sample’s final file-count check; a TCP session
writes no local file:
#include <ovphysx/ovphysx.h>
#include <ovphysx/ovphysx_config.h>
static ovphysx_result_t create_recording_instance(ovphysx_instance_handle_t* handle)
{
const ovphysx_result_t configured = ovphysx_set_global_config(
OVPHYSX_GLOBAL_CONFIG_OVD_RECORDING_CAPABLE, ovphysx_config_value_bool(true));
if (configured.status != OVPHYSX_API_SUCCESS)
return configured;
return ovphysx_create_instance(handle);
}
Call this helper after ovphysx_initialize() and before creating any instance.
After it succeeds and a physics stage is attached,
replace the sample’s FILE recording sequence with record_tcp(handle),
then perform the sample’s cleanup and return its result.
The preliminary step demonstrates starting after simulation has already begun;
omit it to record from the first simulation frame:
#include <ovphysx/ovphysx.h>
static int record_tcp(ovphysx_instance_handle_t handle)
{
const ovphysx_omnipvd_destination_t destination = {
OVPHYSX_OMNIPVD_TRANSPORT_TCP,
OVPHYSX_LITERAL(""),
OVPHYSX_LITERAL("127.0.0.1"),
5425,
3000,
};
ovphysx_result_t result = ovphysx_step_sync(handle, 1.0f / 60.0f);
if (result.status != OVPHYSX_API_SUCCESS)
return 1;
result = ovphysx_ovd_start_recording(handle, &destination);
if (result.status != OVPHYSX_API_SUCCESS)
return 1;
bool recording = false;
result = ovphysx_ovd_is_recording(handle, &recording);
if (result.status != OVPHYSX_API_SUCCESS || !recording)
return 1;
result = ovphysx_step_sync(handle, 1.0f / 60.0f);
if (result.status != OVPHYSX_API_SUCCESS)
return 1;
result = ovphysx_ovd_stop_recording(handle);
if (result.status != OVPHYSX_API_SUCCESS)
return 1;
return 0;
}
If the listener is not ready, start_recording() fails without consuming the
session; start the listener and retry. After stop_recording(), the same
instance can later record to another TCP listener or to an exact FILE path.
FILE uses the exact requested path, and TCP connects synchronously to an already-ready listener. Failed opens can be retried. Only one stream can be active in the shared runtime, and an active stream cannot be replaced. After stop, the owning instance can start another FILE or TCP session. The instance that explicitly starts recording owns it; peers report inactive and cannot stop its session. Detaching the active stage finalizes the shared session and clears its public owner, including when a peer handle started it. Reattach remains dormant until another explicit start.
These synchronous APIs follow the ovphysx same-thread contract. The caller must serialize recording calls on each handle and recording/attach/detach/destroy transitions across all handles sharing the runtime; concurrent calls are not supported. Recording is supported on Windows x86_64 and Linux x86_64/aarch64. Initial and restarted sessions each capture the current core PhysX, PhysXExtensions (including joints and custom geometry), and PhysXVehicle state. Stopping releases only the session’s telemetry handles; the next start takes a fresh full-state snapshot.
What Happens at Runtime#
FILE opens its exact path or TCP connects before sampling begins.
Start snapshots the current world; each subsequent step records its state.
Stop finalizes output. Another recording can start on the same capable runtime.
Inspecting .ovd Files in Kit#
Before you begin, stop the FILE recording to finalize the exact path you requested. If output is missing or incomplete, check the runtime log and verify that the parent directory exists and is writable. You also need the compatible Kit-based application with the OmniPVD extension named in the prerequisites.
Then inspect the recording:
Open a compatible Kit-based application (for example, USD Composer or Isaac Sim with a compatible OmniPVD extension generation).
Enable the OmniPVD extension (
omni.physx.pvd) from Window > Extensions.Use File > Open or the OmniPVD panel to load the
.ovdfile.Use the timeline scrubber to step through recorded frames and inspect shapes, contacts, and solver state.
Inspection succeeded when the OmniPVD panel lists your .ovd file as imported
and the timeline scrubber advances through recorded frames, with shapes,
contacts, and solver state updating as you scrub. A file the panel refuses to
import can be unreadable, malformed, truncated, or incompatible with the reader.
Check the reader’s reported error first. If it reports a format or version
mismatch, refer to Capture Format and Compatibility
for the two version checks the reader applies.
For more details on the Kit-side OmniPVD workflow, refer to the PhysX Visual Debugger documentation included with the Kit application you use for inspection.
Capture Format and Compatibility#
ovphysx OmniPVD capture is optional. When enabled, the runtime writes PhysX OmniPVD recordings in the OVD binary format. FILE output keeps its exact requested path.
OVD capture format and compatibility policy
This table records the format, version, and platform policy for ovphysx OmniPVD capture:
Item |
Policy |
|---|---|
Format |
PhysX OmniPVD OVD command stream (not USD, not a general capture/replay API) |
OmniPVD stream version |
Each recording begins with a 12-byte OmniPVD version header. The writer in the PhysX SDK pinned by this ovphysx build emits 0.4.0 ( |
PhysX OVD integration version |
The stream’s |
Canonical reader |
Kit OmniPVD extension |
Reader compatibility |
Both versions must be compatible. The OmniPVD runtime reader rejects a command-stream version newer than its own |
ovphysx version coupling |
ovphysx does not define a separate OVD version. Compatibility follows the PhysX / OmniPVD runtime pinned by that ovphysx build and the Kit OmniPVD extension that reads the file. Prefer matching generations: inspect with a Kit build whose OmniPVD extension supports both the recorded OmniPVD stream version and the PhysX OVD integration major. |
Platform |
OmniPVD recording is supported on Windows x86_64 and Linux x86_64/aarch64. PVDRuntime is statically linked into the shipped native library; no separate PVDRuntime |
This is the public compatibility story for ovphysx OmniPVD capture. If you need a guaranteed cross-release matrix beyond the two Reader compatibility checks in Capture Format and Compatibility, confirm with the OmniPVD owners before treating an older Kit reader as supported against a newer writer.
Troubleshooting#
These symptoms cover the configuration and lifecycle mistakes that stop a recording from appearing:
OmniPVD recording symptoms and fixes
Symptom |
Cause |
Fix |
|---|---|---|
No |
Capability or explicit start was omitted |
Set |
Output is incomplete |
Recording was not finalized |
Call |
FILE |
Parent directory is missing or not writable |
Create the parent directory first (FILE does not create directories), then retry |
TCP start fails |
Listener was not ready or address/port was wrong |
Start the trusted plaintext listener before calling |
Result#
After this tutorial you can stream live OmniPVD data over TCP or capture .ovd
recordings from an ovphysx simulation for offline inspection in Kit.