<!-- SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -->
<!-- SPDX-License-Identifier: BSD-3-Clause -->

# 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.

## 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 through `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

```python
import glob
import os
import tempfile
from pathlib import Path

from ovphysx import PhysX, PhysXConfig


def attach_scene(physx, usd_path, stage_name):
    import ovstage

    if not ovstage.population.available():
        raise RuntimeError("ovstage population bridge is unavailable")

    stage = ovstage.Stage(stage_name)
    ordinal = 1
    try:
        ovstage.population.open_usd(stage, str(usd_path), ordinal=ordinal, domains=ovstage.PopulationDomain.PHYSICS)
        # Population does not seal: the caller owns ordinal lifecycle, and
        # attach_ovstage() reads at a sealed ordinal.
        stage.advance_write_floor(ordinal=ordinal).wait()
        physx.attach_ovstage(stage, read_ordinal=ordinal)
        return stage
    except Exception:
        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}")

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

stage = attach_scene(physx, usd_path, "ovphysx-omnipvd-sample")
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)

print("Simulation complete.")

# Destroying the instance finalizes the recording:
# the runtime renames tmp.ovd → <timestamp>_rec.ovd.
physx.detach_ovstage()
stage.destroy()
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
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)
endif()
```

**Source**

```cpp
#include "ovphysx/ovphysx.h"
#include "ovphysx/ovphysx_config.h"
#include "ovstage_sample.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;
}

static int run(void)
{
#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_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_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 : "");
        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;
    }

    // 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");

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

    // 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;
}

int main(void) {
    int rc = run();
    return rc;
}
```

## 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 (for example, USD Composer, Isaac Sim).
2. Enable the **OmniPVD** extension from **Window > Extensions**.
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, refer to the PhysX Visual Debugger documentation included with the Kit application you use for inspection.

## 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.
