Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<!--
SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->

<!--
MANDATORY FOR ALL AUTOMATED CODING AGENTS (Cursor, Copilot, etc.):

Before modifying OR creating ANY file under this repository, you MUST use your
file-reading tools to load EVERY AGENTS.md on the directory paths you will touch
(see "CRITICAL — mandatory preflight" below). This is NON-OPTIONAL. Skipping it,
or relying only on chat context without reading those files, is incorrect.

Before you treat work as finished, you MUST run pre-commit as in
"Pre-commit — match CI before you stop" and fix all failures. Declaring the task
done without that run is incorrect.

If pre-commit/CI fails, or the user corrects you, or you repeat the same class of
mistake, you MUST record the lesson in AGENTS.md or source comments per
"Mandatory learning loop" in the same session—not only when the user asks.

Repository owners: keep this block at the top of this file; do not delete it.
-->

# IsaacTeleop — agent notes

## CRITICAL — mandatory preflight (do not skip)

**Hard requirement:** Do **not** edit or add code under `IsaacTeleop/` until you have **read** (e.g. with the Read tool) **every** relevant `AGENTS.md` file as defined below. This is **not** optional, **not** "only for large tasks," and **not** satisfiable by inferring from prior conversation. If you have not read those files in **this** session for **this** task, stop and read them first.

**You must:**

1. List the **directories** you expect to edit or create files under (e.g. `src/core/live_trackers/cpp/`, `src/core/deviceio_base/cpp/inc/…`).
2. For **each** such directory, **read** **`AGENTS.md` in that directory** if it exists.
3. Walk **up** toward the **IsaacTeleop repo root** and **read** **`AGENTS.md` in every ancestor directory** that has one (e.g. `live_trackers/` → `src/core/` → **`IsaacTeleop/AGENTS.md` (this file)**).

**You must not** assume that reading the single "closest" `AGENTS.md` is enough. **Multiple** `AGENTS.md` files can apply to one change (e.g. both `live_trackers` and `deviceio_base`).

**Listing every `AGENTS.md` in this repo** (no curated table here—add new files under the tree without editing this document):

```bash
# Run from the IsaacTeleop repository root (the directory that contains this file):
find . -name AGENTS.md ! -path '*/.git/*' | sort
```

If you cannot run a shell, use your search/glob tools on the pattern `**/AGENTS.md` from the same root. That inventory is for orientation; you must still **read** every file that applies to the directories you will touch (steps 1–3 above).

Optional context index: [`src/core/AGENTS.md`](src/core/AGENTS.md) (also on the ancestor walk—read it when working under `src/core/`).

## Pre-commit — match CI before you stop

- From the **IsaacTeleop repo root** (this directory), run pre-commit and **fix all failures** before you treat a change as finished (do not only rely on “should pass” reasoning).
- **Use the same hook set as GitHub Actions:** `.github/workflows/pre-commit.yaml` runs pre-commit with **`SKIP=check-copyright-year`**. Mirror that locally:

```bash
SKIP=check-copyright-year pre-commit run --all-files
```

- **REUSE:** files covered by the REUSE hook need **`SPDX-FileCopyrightText`** and **`SPDX-License-Identifier`** in the form the repo already uses (for example the HTML comment block at the top of `README.md` also applies to **`AGENTS.md`** and similar docs).
- If a hook failure shows **missing or non-obvious repo policy** (not a one-off typo), you **must** add a **short** reminder under **Mandatory learning loop** rules to the right `AGENTS.md` or adjacent **`//` comments** so the next run does not repeat it—unless it is already documented.

## Mandatory learning loop (AGENTS.md and comments)

**Hard requirement:** When **any** of the following happens, you **must** complete steps 1–3 **before** you end the session or move on as if the work were complete:

1. The **user** is dissatisfied or corrects your approach (wrong layer, scope, style, or a fix that does not stick).
2. **Pre-commit** or **CI** fails on something you or another agent could hit again (linters, REUSE/SPDX, formatting, policy hooks, etc.).
3. You **repeat** the same **category** of error after a correction.

**You must:**

1. **Distill** what went wrong into a **short, reusable pattern** (a rule or boundary, not a chat transcript).
2. **Update** the right artifact in the **same working pass** as the fix: prefer the **most local** `AGENTS.md` for package- or subtree-level rules; use the **repo root** `AGENTS.md` only for expectations that span the whole tree; put **volatile or line-specific** detail in **`//` comments** next to the code.
3. Respect **scope vs `main`** (below): only add `AGENTS.md` bullets for **this branch’s delta** to **`main`** (or the agreed base), not for behavior that already exists on the base branch.

**You must not** skip documentation updates because the user did not say “update AGENTS.md”—failed checks and repeated mistakes **trigger** this loop automatically.

**What belongs in `AGENTS.md`**

- **High-level** expectations: boundaries between layers, what to avoid, naming or structural conventions, CMake/include policy at a glance, “always / never” rules that stay true across refactors.
- **Style of work**: how minimal to keep diffs, when to ask for clarification, how this subsystem should relate to OpenXR/schema/deviceio, etc.

**What does *not* belong in `AGENTS.md`**

- **Low-level or volatile detail**: exact call sequences, field-by-field semantics, long checklists tied to one function, anything that will go stale the next time the code moves.
- Put that in **comments in the source files** where the behavior lives (short `//` notes: intent, invariants, or “do not X because …”).

**Scope — document what *this* branch changes, not main**

- New or updated bullets should capture **learnings from the delta** between **this branch (or commit)** and **`main`** (or whatever long-lived base you are targeting—release branch, parent MR, etc.).
- Do **not** restate facts that already hold on **`main`** just because this branch touches nearby files. If behavior landed in the **base** history, it belongs in **code comments** next to that code or in **main’s** docs—not as “new” guidance in an `AGENTS.md` introduced only to support a smaller follow-up change.
- Example: schema- or API-level choices that merged **before** this branch are **out of scope** for `AGENTS.md` edits tied to this branch; they add churn and read like stale noise after merge.
12 changes: 12 additions & 0 deletions src/core/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!--
SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->

# Agent notes — IsaacTeleop `src/core` (index)

**CRITICAL:** The mandatory multi-file `AGENTS.md` preflight in **[`../../AGENTS.md`](../../AGENTS.md)** applies here too. Before changing anything under `src/core/`, you must have read **this file**, **the repo root `AGENTS.md`**, and **every other `AGENTS.md` on the directory paths you will touch**. Do not skip any of them.

To see **all** `AGENTS.md` files in the IsaacTeleop repo, use the **`find` command (or `**/AGENTS.md` glob) documented in the repo root [`AGENTS.md`](../../AGENTS.md)**—do not rely on a hand-maintained list in this file.

If work under **`src/core/`** went wrong—**user** correction, **pre-commit/CI** failure, or **repeated** same-class mistakes—you **must** follow the repo root **[`AGENTS.md`](../../AGENTS.md)** **Mandatory learning loop**: distill a short rule and **update** the **nearest** relevant `AGENTS.md` (this file or a package file) or **source comments** in the same session (including **delta vs `main`** scope).
21 changes: 21 additions & 0 deletions src/core/deviceio_base/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!--
SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->

# Agent notes — `deviceio_base`

**CRITICAL (non-optional):** Before editing this package, complete the mandatory **`AGENTS.md` preflight** in [`../../../AGENTS.md`](../../../AGENTS.md) (read every applicable `AGENTS.md` on your paths, not just this file).

## API

- **`ITrackerImpl::update`** takes **`int64_t monotonic_time_ns`** (system monotonic clock, same domain as `core::os_monotonic_now_ns()`).
- **Do not** use `XrTime`, `<openxr/openxr.h>`, or OpenXR link targets in this library. Keep the tracker abstraction runtime-agnostic.

## CMake

- **`deviceio_base`** is an **INTERFACE** library: list only what the headers actually need (e.g. `isaacteleop_schema`). Do **not** link `OpenXR::headers` or `oxr::oxr_utils` here.

## Fallout for dependents

- Targets that need OpenXR/oxr for compilation must declare those dependencies themselves (they are **not** implied by `deviceio_base`). See e.g. [`../live_trackers/AGENTS.md`](../live_trackers/AGENTS.md). **`deviceio_trackers`** intentionally stays OpenXR-free—see [`../deviceio_trackers/AGENTS.md`](../deviceio_trackers/AGENTS.md).
2 changes: 0 additions & 2 deletions src/core/deviceio_base/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ target_include_directories(deviceio_base
target_link_libraries(deviceio_base
INTERFACE
isaacteleop_schema
oxr::oxr_utils
OpenXR::headers
)

add_library(deviceio::deviceio_base ALIAS deviceio_base)
13 changes: 6 additions & 7 deletions src/core/deviceio_base/cpp/inc/deviceio_base/tracker.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,14 @@

#pragma once

#include <openxr/openxr.h>

#include <cstdint>
#include <memory>
#include <string>
#include <string_view>

namespace core
{

// Forward declarations
struct OpenXRSessionHandles;

// Base interface for tracker implementations.
// The actual worker objects updated each frame by DeviceIOSession.
class ITrackerImpl
Expand All @@ -23,13 +19,16 @@ class ITrackerImpl
virtual ~ITrackerImpl() = default;

/**
* @brief Updates tracker state for the specified OpenXR time.
* @brief Updates tracker state for the current frame.
*
* @param monotonic_time_ns Current time from the system monotonic clock, in nanoseconds
* (same domain as core::os_monotonic_now_ns()).
*
* @throws std::runtime_error On critical tracker/runtime failures.
* @note A thrown exception indicates a fatal condition; the application is
* expected to terminate rather than continue running.
*/
virtual void update(XrTime time) = 0;
virtual void update(int64_t monotonic_time_ns) = 0;
};

/**
Expand Down
22 changes: 22 additions & 0 deletions src/core/deviceio_session/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!--
SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->

# Agent notes — `deviceio_session`

**CRITICAL (non-optional):** Before editing this package, complete the mandatory **`AGENTS.md` preflight** in [`../../../AGENTS.md`](../../../AGENTS.md) (read every applicable `AGENTS.md` on your paths, not just this file).

## Update loop

- **`DeviceIOSession::update`** reads the clock once with **`core::os_monotonic_now_ns()`** (via `#include <oxr_utils/os_time.hpp>`) and passes that value to **`ITrackerImpl::update(int64_t)`** for every registered impl.
- **No** session-owned **`XrTimeConverter`** is required solely to drive that loop (OpenXR conversion stays inside live impls).

## Implementation / includes

- **`deviceio_session.cpp`**: if the TU uses **`XR_NULL_HANDLE`** or other OpenXR macros, include **`<openxr/openxr.h>`** explicitly after the session header so **`XR_NO_PROTOTYPES`** is already established by **`oxr_utils/oxr_funcs.hpp`** pulled in through **`deviceio_session.hpp`**.

## Related docs

- Tracker interface contract: [`../deviceio_base/AGENTS.md`](../deviceio_base/AGENTS.md)
- Live factory + impls: [`../live_trackers/AGENTS.md`](../live_trackers/AGENTS.md)
8 changes: 5 additions & 3 deletions src/core/deviceio_session/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ target_include_directories(deviceio_session
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/inc>
)

# Public headers include oxr_funcs.hpp, which pulls in NVIDIA OpenXR extension
# headers (e.g. XR_NVX1_action_context.h). Propagate Teleop::openxr_extensions so
# consumers (Python bindings, apps) can compile without linking deviceio_trackers.
# Public headers include oxr_funcs.hpp and oxr_session_handles.hpp. Propagate
# oxr::oxr_utils (OpenXR headers + oxr inc/) and Teleop::openxr_extensions (NVIDIA
# extension headers) so consumers (Python bindings, apps) compile without linking
# live_trackers.
target_link_libraries(deviceio_session
PUBLIC
deviceio::deviceio_base
oxr::oxr_utils
Teleop::openxr_extensions
PRIVATE
deviceio::deviceio_trackers
Expand Down
8 changes: 5 additions & 3 deletions src/core/deviceio_session/cpp/deviceio_session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

#include <live_trackers/live_deviceio_factory.hpp>
#include <mcap/writer.hpp>
#include <openxr/openxr.h>
#include <oxr_utils/os_time.hpp>

#include <cassert>
#include <iostream>
Expand All @@ -24,7 +26,7 @@ namespace core
DeviceIOSession::DeviceIOSession(const std::vector<std::shared_ptr<ITracker>>& trackers,
const OpenXRSessionHandles& handles,
std::optional<McapRecordingConfig> recording_config)
: handles_(handles), time_converter_(handles)
: handles_(handles)
{
std::vector<std::pair<const ITracker*, std::string>> tracker_names;

Expand Down Expand Up @@ -103,11 +105,11 @@ std::unique_ptr<DeviceIOSession> DeviceIOSession::run(const std::vector<std::sha

void DeviceIOSession::update()
{
XrTime current_time = time_converter_.os_monotonic_now();
const int64_t monotonic_ns = os_monotonic_now_ns();

for (auto& kv : tracker_impls_)
{
kv.second->update(current_time);
kv.second->update(monotonic_ns);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
#include <deviceio_base/tracker.hpp>
#include <oxr_utils/oxr_funcs.hpp>
#include <oxr_utils/oxr_session_handles.hpp>
#include <oxr_utils/oxr_time.hpp>

#include <memory>
#include <optional>
Expand Down Expand Up @@ -88,7 +87,6 @@ class DeviceIOSession : public ITrackerSession

const OpenXRSessionHandles handles_;
std::unordered_map<const ITracker*, std::unique_ptr<ITrackerImpl>> tracker_impls_;
XrTimeConverter time_converter_;

// Owned MCAP writer; null when recording is not configured.
std::unique_ptr<mcap::McapWriter> mcap_writer_;
Expand Down
16 changes: 16 additions & 0 deletions src/core/deviceio_trackers/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!--
SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->

# Agent notes — `deviceio_trackers`

**CRITICAL (non-optional):** Before editing this package, complete the mandatory **`AGENTS.md` preflight** in [`../../../AGENTS.md`](../../../AGENTS.md) (read every applicable `AGENTS.md` on your paths, not just this file).

## No OpenXR dependency

- **`deviceio_trackers`** must **not** link **`OpenXR::headers`**, **`oxr::oxr_utils`**, or vendor extension targets, and must **not** `#include` OpenXR headers. Public API stays schema + **`deviceio_base`** only.

## Related docs

- Base interface: [`../deviceio_base/AGENTS.md`](../deviceio_base/AGENTS.md)
34 changes: 34 additions & 0 deletions src/core/live_trackers/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!--
SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->

# Agent notes — `live_trackers`

**CRITICAL (non-optional):** Before editing this package, complete the mandatory **`AGENTS.md` preflight** in [`../../../AGENTS.md`](../../../AGENTS.md) (read every applicable `AGENTS.md` on your paths, not just this file).

## Time and OpenXR

- Store **`last_update_time_` as `int64_t`** (monotonic ns), not **`XrTime`**.
- **Once per `update` call:** `const XrTime xr_time = time_converter_.convert_monotonic_ns_to_xrtime(monotonic_time_ns);` then use **`xr_time`** for every **`xrLocate*`** / hand / body call **and** for MCAP (see below). **Do not** call **`convert_monotonic_ns_to_xrtime`** again in the MCAP block.
- **Full-body limp mode:** if the body tracker handle is null and you **return early**, **do not** compute **`xr_time`** first—only convert after you know you will call OpenXR.

## `DeviceDataTimestamp` (MCAP)

- **Fields 1–2:** monotonic ns (e.g. **`last_update_time_`, `last_update_time_`**).
- **Field 3 (`sample_time_raw_device_clock`):** the **same** **`xr_time`** variable used for OpenXR this frame (not a second conversion).

## Includes

- In headers that need both: **`#include <oxr_utils/oxr_funcs.hpp>`** comes **before** any bare **`#include <openxr/openxr.h>`**. `oxr_funcs.hpp` defines **`XR_NO_PROTOTYPES`** then includes OpenXR; including **`openxr.h`** first fights that policy.
- In **`.cpp`** files that construct **`DeviceDataTimestamp`**, include **`#include <schema/timestamp_generated.h>`** explicitly.
- **`.cpp`** files should include headers for **symbols the TU uses** (e.g. **`oxr_funcs.hpp`** for **`createReferenceSpace`**), not only what the matching **`.hpp`** happens to pull in.

## CMake

- **`live_trackers`** should **`PUBLIC` link `oxr::oxr_utils`** (OpenXR headers come through that INTERFACE target) because headers/sources use OpenXR / oxr types.

## Related docs

- Session update loop: [`../deviceio_session/AGENTS.md`](../deviceio_session/AGENTS.md)
- No OpenXR in base API: [`../deviceio_base/AGENTS.md`](../deviceio_base/AGENTS.md)
1 change: 1 addition & 0 deletions src/core/live_trackers/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ target_link_libraries(live_trackers
deviceio::deviceio_base
deviceio::deviceio_trackers
mcap::mcap_core
oxr::oxr_utils
Teleop::openxr_extensions
)

Expand Down
14 changes: 7 additions & 7 deletions src/core/live_trackers/cpp/live_controller_tracker_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

#include <mcap/recording_traits.hpp>
#include <oxr_utils/oxr_funcs.hpp>
#include <oxr_utils/oxr_time.hpp>
#include <schema/controller_bfbs_generated.h>
#include <schema/timestamp_generated.h>

#include <cassert>
#include <cmath>
Expand Down Expand Up @@ -314,9 +314,10 @@ LiveControllerTrackerImpl::LiveControllerTrackerImpl(const OpenXRSessionHandles&
std::cout << "ControllerTracker initialized (left + right) with action context" << std::endl;
}

void LiveControllerTrackerImpl::update(XrTime time)
void LiveControllerTrackerImpl::update(int64_t monotonic_time_ns)
{
last_update_time_ = time;
last_update_time_ = monotonic_time_ns;
const XrTime xr_time = time_converter_.convert_monotonic_ns_to_xrtime(monotonic_time_ns);

// Sync actions via xrSyncActions2NV with our session action context
XrActiveActionSet active_action_set{ action_set_.get(), XR_NULL_PATH };
Expand Down Expand Up @@ -352,7 +353,7 @@ void LiveControllerTrackerImpl::update(XrTime time)
ControllerPose aim_pose{};

XrSpaceLocation grip_location{ XR_TYPE_SPACE_LOCATION };
result = core_funcs_.xrLocateSpace(grip_space.get(), base_space_, time, &grip_location);
result = core_funcs_.xrLocateSpace(grip_space.get(), base_space_, xr_time, &grip_location);
if (XR_FAILED(result))
{
tracked.data.reset();
Expand All @@ -373,7 +374,7 @@ void LiveControllerTrackerImpl::update(XrTime time)
}

XrSpaceLocation aim_location{ XR_TYPE_SPACE_LOCATION };
result = core_funcs_.xrLocateSpace(aim_space.get(), base_space_, time, &aim_location);
result = core_funcs_.xrLocateSpace(aim_space.get(), base_space_, xr_time, &aim_location);
if (XR_FAILED(result))
{
tracked.data.reset();
Expand Down Expand Up @@ -419,8 +420,7 @@ void LiveControllerTrackerImpl::update(XrTime time)

if (mcap_channels_)
{
int64_t monotonic_ns = time_converter_.convert_xrtime_to_monotonic_ns(last_update_time_);
DeviceDataTimestamp timestamp(monotonic_ns, monotonic_ns, last_update_time_);
DeviceDataTimestamp timestamp(last_update_time_, last_update_time_, xr_time);
mcap_channels_->write(0, timestamp, left_tracked_.data);
mcap_channels_->write(1, timestamp, right_tracked_.data);
}
Expand Down
Loading
Loading