# Developer Guide

Use this guide when you integrate, build, and operate ovphysx in your own application. You learn how to build the SDK, run samples, and apply core runtime rules for synchronization, threading, and resource ownership.

## Relationship and Consumption Model

ovphysx provides a stable C API and Python bindings on top of the Omni PhysX runtime, which itself uses the PhysX SDK.
Use ovphysx when you need USD-based physics simulation outside Kit with a small, self-contained SDK.

- Python: `pip install ovphysx` and `from ovphysx import PhysX`
- C/C++: include headers from `include/ovphysx/` and link against `ovphysx::ovphysx` via CMake

## Samples and Tutorials

The ovphysx samples are runnable references for SDK and wheel usage, designed to run in a clean environment matching how end users consume the wheel or SDK.

**Python samples** (`tests/python_samples/`):

| Sample | Tutorial | Feature |
|---|---|---|
| `hello_world.py` | {doc}`tutorials/hello_world` | Load USD + step (minimal workflow) |
| `tensor_bindings.py` | {doc}`tutorials/tensor_bindings` | Read/write simulation data via tensor bindings |
| `clone.py` | {doc}`tutorials/cloning` | Replicate environments with the clone API |
| `tensor_bindings_views.py` | | Build "TensorAPI-like" view wrappers (advanced) |

**C/C++ samples** (`tests/c_samples/`):

| Sample | Tutorial | Feature |
|---|---|---|
| `hello_world_c/` | {doc}`tutorials/hello_world` | Minimal C hello world |
| `tensor_bindings_c/` | {doc}`tutorials/tensor_bindings` | CPU tensor read/write |
| `tensor_bindings_gpu_c/` | | GPU tensor read/write with CUDA |
| `clone_c/` | {doc}`tutorials/cloning` | Scene cloning |

For SDK setup, see {doc}`tutorials/quickstart`.

For tensor binding shape/read/write semantics, see the Tensor Type Reference in {doc}`tutorials/tensor_bindings`.
Canonical enum-level definitions are in `include/ovphysx/ovphysx_types.h` (`ovphysx_tensor_type_t`).

## Execution Model

ovphysx uses a stream-ordered execution model:

- Calls are enqueued in submission order and observe prior writes without extra sync.
- Asynchronous calls return an `op_index`; wait on it before consuming results outside the stream.
- Synchronous calls complete before returning and do not yield an `op_index`.

Use `ovphysx_wait_op()` (or `PhysX.wait_op()` in Python) to:
- synchronize before reading or modifying tensors on CPU/GPU if they're currently accessed by asynchronous operations inside ovphysx
- ensure correctness before external side-effects (logging, rendering, network I/O)

## Multi-Instance Support

The SDK supports multiple independent PhysX instances running concurrently or sequentially.
Settings are internally handled by Carbonite and therefore per-process, so different instances cannot use different settings in the same process.

## Operation Indices and Polling

`op_index` values are single-use. After a successful wait, the index is consumed and must not be used again.
Polling is supported by passing `timeout_ns = 0` to `wait_op`.

## Threading

- Multiple ovphysx instances are safe to use concurrently across threads.
- A single instance is **not** thread-safe. Serialize access externally.
- Do not wait on the same `op_index` from multiple threads.

## Ownership and Lifetimes

- Error strings returned by the API must be destroyed with `ovphysx_destroy_error()` or `ovphysx_destroy_errors()`.
- Tensor bindings and attribute bindings own internal resources and must be destroyed when no longer needed.

## Dependency Management

For SDK and wheel users, dependencies are bundled with the package:

- Runtime dependencies are loaded from `libDir/deps/` in the installed layout.
- The wheel includes the native runtime stack and required plugins.
- No additional dependency fetch step is required for normal package usage.
- Auto-detects library location via `getLibraryDirectory()`
- Sets `PYTHONHOME` for scripting plugin support
- Pre-loads shared libraries with `RTLD_GLOBAL` for plugin symbol resolution
- Offline-capable

## Error Handling

For C:
- Check `result.status` on every call.
- If `result.error.ptr` is non-null, destroy it with `ovphysx_destroy_error()`.
- For `ovphysx_wait_op()`, destroy errors with `ovphysx_destroy_errors()`.

For Python:
- Runtime errors are raised on failed calls.
- Use try/finally or context managers to ensure bindings are destroyed.

## GPU Warmup and Determinism

GPU tensor reads require a warmup step that initializes DirectGPU buffers.
This is done automatically on the first tensor operation.
If deterministic initial state matters, call `ovphysx_warmup_gpu()` explicitly after USD load and before the first tensor read.

## Scene Cloning

The SDK provides a clone API for replicating sub-sections of a scene:

- Clones are created in the internal representation (no USD prims)
- Optimized for large-scale replication and simulation throughput
- Preserves physics properties, materials, and constraints
- Non-blocking async execution with explicit completion tracking

For examples see the {doc}`tutorials/cloning` tutorials.

Requirements:

- Source hierarchy must exist in the loaded USD stage
- Source prims must have physics components
- Target paths must not already exist


## Logging

ovphysx uses [Carbonite](https://docs.omniverse.nvidia.com/kit/docs/carbonite/) as its internal logging backend.
By default, the global log level is `OVPHYSX_LOG_WARNING` — only warnings and errors are emitted.

### Controlling the Log Level

::::{tab-set}

:::{tab-item} Python
```python
import ovphysx

ovphysx.set_log_level(ovphysx.OVPHYSX_LOG_VERBOSE)
print(ovphysx.get_log_level())
```
:::

:::{tab-item} C
```c
#include <ovphysx/ovphysx.h>

// Set before or after instance creation — applies globally to all outputs.
ovphysx_set_log_level(OVPHYSX_LOG_VERBOSE);

uint32_t current = ovphysx_get_log_level();
```
:::

::::

### Custom Log Callbacks (C)

Register one or more callbacks to receive log messages programmatically.
The caller must ensure the callback and any resources it references remain
valid until it is unregistered. If the callback and its resources naturally
outlive the process (e.g. a static function with no `user_data`), calling
`ovphysx_unregister_log_callback()` is not required. When called, it
guarantees the callback is not running on any thread and will never be
invoked again.

```c
void my_logger(uint32_t level, const char* message, void* user_data) {
    fprintf(stderr, "[%u] %s\n", level, message);
}

ovphysx_register_log_callback(my_logger, NULL);
// ... run simulation ...
ovphysx_unregister_log_callback(my_logger, NULL);
// Safe to destroy any resources referenced by the callback here.
```

### Controlling Default Console Output

By default, Carbonite logs to the console. When a custom callback is registered
that also writes to the console, output may be doubled. Use
`ovphysx_enable_default_log_output()` to suppress the built-in console logger:

::::{tab-set}

:::{tab-item} Python
```python
ovphysx.enable_python_logging()
ovphysx.enable_default_log_output(False)
```
:::

:::{tab-item} C
```c
ovphysx_register_log_callback(my_logger, NULL);
ovphysx_enable_default_log_output(false);  // only my_logger receives messages now
```
:::

::::

### Python Logging Bridge

Route native log messages into Python's standard `logging` module:

```python
import logging
import ovphysx

# Route native messages to the "ovphysx" Python logger
ovphysx.enable_python_logging()

# Add a handler to see the output
logging.getLogger("ovphysx").addHandler(logging.StreamHandler())
logging.getLogger("ovphysx").setLevel(logging.DEBUG)

# ... run simulation — native messages appear in Python logging ...

ovphysx.disable_python_logging()
```

You now have the core integration rules for building, running, and operating ovphysx. For the full C API reference, see the {doc}`api`. For Python, see {doc}`python_api`.
