OmniPVD Recording: Capture Physics Internals to .ovd#

This tutorial shows how to record OmniPVD data from ovphysx to .ovd files for offline inspection in a Kit application. OmniPVD captures the full internal state of the PhysX simulation each frame — shapes, contacts, solver data — so you can debug and visualize physics behavior after the fact.

Prerequisites#

  • Install ovphysx and confirm native libraries load.

  • Prepare a USD file with physics objects.

  • For offline inspection: a Kit application with the OmniPVD extension (omni.physx.pvd).

Required Config#

OmniPVD recording is controlled by two typed config fields:

Config field (Python)

C builder

Description

omnipvd_ovd_recording_directory

ovphysx_config_entry_omnipvd_ovd_recording_directory()

Writable directory where .ovd files are saved

omnipvd_output_enabled

ovphysx_config_entry_omnipvd_output_enabled()

Enables OmniPVD data capture

Both must be configured before the PhysX instance is created, because the recording pipeline is initialized during physics engine startup. Pass them via PhysXConfig (Python) or config_entries in ovphysx_create_args (C/C++).

The runtime auto-creates the recording directory if it does not exist.

Code#

Python#

import glob
import os
import tempfile
from pathlib import Path

from ovphysx import PhysX, PhysXConfig

# 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}")

# Initialize PhysX with OmniPVD recording enabled.
# IMPORTANT: Both config fields must be passed at initialization, before the
# physics engine is created internally. The recording directory must be
# a valid, writable path.
physx = PhysX(
    config=PhysXConfig(
        omnipvd_ovd_recording_directory=output_dir,
        omnipvd_output_enabled=True,
    )
)

# Load a USD scene with physics objects
script_dir = Path(__file__).resolve().parent
usd_path = script_dir / ".." / "data" / "links_chain_sample.usda"
print(f"Loading USD scene: {usd_path}")

physx.add_usd(str(usd_path))
physx.wait_all()

# Run 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, i * dt)

print("Simulation complete.")

# Destroying the instance finalizes the recording:
# the runtime renames tmp.ovd → <timestamp>_rec.ovd.
physx.release()
print("Cleanup complete")

# List the produced .ovd files
ovd_files = glob.glob(os.path.join(output_dir, "*_rec.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

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)
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)
endif()

Source

#include "ovphysx/ovphysx.h"
#include "ovphysx/ovphysx_config.h"

#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <string>

namespace fs = std::filesystem;

// Count files matching *_rec.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() > 8 && name.substr(name.size() - 8) == "_rec.ovd")
            ++count;
    }
    return count;
}

int main() {
#if defined(__aarch64__) || defined(_M_ARM64)
    printf("OmniPVD is not supported on aarch64, skipping.\n");
    return 0;
#endif

    // Create a temporary output directory for OVD recording
    fs::path output_dir = fs::temp_directory_path() / "ovphysx_pvd_cpp_sample";
    std::error_code ec;
    fs::create_directories(output_dir, ec);
    if (ec) {
        fprintf(stderr, "Failed to create directory '%s': %s\n",
                output_dir.string().c_str(), ec.message().c_str());
        return 1;
    }
    std::string dir_str = output_dir.string();
    printf("OmniPVD recording directory: %s\n", dir_str.c_str());

    // Configure OmniPVD recording via typed config entries.
    // Both must be set before instance creation — the recording pipeline
    // initializes during physics engine startup.
    ovphysx_config_entry_t config[] = {
        ovphysx_config_entry_omnipvd_ovd_recording_directory(ovphysx_cstr(dir_str.c_str())),
        ovphysx_config_entry_omnipvd_output_enabled(true),
    };

    ovphysx_create_args create_args = OVPHYSX_CREATE_ARGS_DEFAULT;
    create_args.config_entries = config;
    create_args.config_entry_count = 2;
    ovphysx_handle_t handle = 0;

    ovphysx_result_t r = ovphysx_create_instance(&create_args, &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 : "");
        return 1;
    }

    // Load USD scene
    ovphysx_string_t path_str = ovphysx_cstr(OVPHYSX_TEST_DATA "/simple_physics_scene.usda");
    ovphysx_string_t prefix_str = {nullptr, 0};
    ovphysx_usd_handle_t usd_handle = 0;

    ovphysx_enqueue_result_t add_result = ovphysx_add_usd(handle, path_str, prefix_str, &usd_handle);
    if (add_result.status != OVPHYSX_API_SUCCESS) {
        ovphysx_string_t err = ovphysx_get_last_error();
        fprintf(stderr, "Failed to load USD: %.*s\n",
                (int)(err.ptr ? err.length : 0), err.ptr ? err.ptr : "");
        ovphysx_destroy_instance(handle);
        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++) {
        float current_time = static_cast<float>(i) * dt;
        ovphysx_result_t step_r = ovphysx_step_sync(handle, dt, current_time);
        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_destroy_instance(handle);
            return 1;
        }
    }

    printf("Simulation complete.\n");

    // Destroying the instance finalizes the recording:
    // tmp.ovd is renamed to a timestamped *_rec.ovd file.
    ovphysx_destroy_instance(handle);

    // Verify that at least one OVD file was produced.
    // Return non-zero if not, so CI can catch regressions.
    int ovd_count = count_ovd_files(output_dir);
    if (ovd_count > 0) {
        printf("Recorded %d OVD file(s) in %s\n", ovd_count, dir_str.c_str());
        printf("Cleanup complete\n");
        return 0;
    }

    fprintf(stderr, "FAIL: No OVD files found in %s\n", dir_str.c_str());
    return 1;
}

What Happens at Runtime#

  1. When PhysX() (or ovphysx_create_instance) is called with both config fields set, the runtime creates an OmniPVD writer backed by a file stream.

  2. A temporary file tmp.ovd is created in the recording directory.

  3. Each simulation step records a frame of physics state into tmp.ovd.

  4. When the instance is destroyed (release() / ovphysx_destroy_instance), the runtime renames tmp.ovd to a timestamped file: YYYY_MM_DD_HH_MM_SS_CC_rec.ovd (where CC is a disambiguation counter).

If the recording directory is unset (empty string) or omnipvd_output_enabled is false, no recording takes place and no files are written.

Inspecting .ovd Files in Kit#

  1. Open any Kit-based application (e.g., USD Composer, Isaac Sim).

  2. Enable the OmniPVD extension: Window > Extensions, search for omni.physx.pvd, and enable it.

  3. Use File > Open or the OmniPVD panel to load the .ovd file.

  4. Use the timeline scrubber to step through recorded frames and inspect shapes, contacts, and solver state.

For more details on the Kit-side OmniPVD workflow, see the PhysX Visual Debugger documentation.

Troubleshooting#

Symptom

Cause

Fix

No .ovd file after simulation

Config not set before instance creation

Pass both fields in PhysXConfig / config_entries at init

Empty recording directory

omnipvd_output_enabled is false

Set to true

tmp.ovd exists but no *_rec.ovd

Instance not properly destroyed

Ensure release() / ovphysx_destroy_instance is called

Runtime error about directory

Directory path is invalid or not writable

Use an absolute path to a writable location

Result#

After this tutorial you can capture .ovd recordings from any ovphysx simulation and inspect them offline in Kit.