Expand description
std facade for the tf_tree transform engine.
Re-exports the tf_tree_core engine and adds the ergonomic, allocating
conveniences that do not belong in the no_std core: the TreeBuilder and
the Tree that owns a HeapArena, the plan-cached Tree::lookup, and
Described — a Display wrapper that resolves error ids to frame names by
consulting the arena (the error type itself stays Copy and no_std).
Most users depend on this crate, not on tf_tree_core directly.
use tf_tree::{TreeBuilder, InterpPolicy, Stamp, Iso3};
// Topology is declared on the builder; `build()` sizes the arena from exactly
// these edges (static edges reserve no ring slots).
let tree = TreeBuilder::new()
.static_edge("map", "odom", &Iso3::IDENTITY)
.build()
.expect("layout");
// map -> odom is a static identity, so the lookup is identity at any time.
// A typed binding pins the default `SystemDomain` (method-call inference does
// not apply a type parameter's default, so annotate the stamp once).
let now: Stamp = Stamp::from_nanos(0);
let t = tree.lookup("map", "odom", now).unwrap();
assert_eq!(t, Iso3::IDENTITY);§Minimum supported Rust version
1.87. It is declared in [workspace.package] rust-version and repeated
here because a manifest is not somewhere a user reads: the person deciding
whether they can adopt this crate opens the docs, and cargo refusing to
build is a worse way to find out. just msrv builds --locked on exactly
that toolchain and fails if this line, README.md, SUPPORT.md or any
hand-written rust-version disagrees with the manifest.
An MSRV bump is a minor-version bump pre-1.0 and a breaking change after —
SUPPORT.md is the policy, including why each of the two steps so far was
forced by a dependency rather than chosen.
§Two stability tiers
Everything at this crate’s root is the stable surface: at a published tag
each pub item is a semver promise. The tf_tree::unstable module — behind
the default-off unstable feature, so it is absent from these docs unless
that feature is on — is not, and enabling the feature is the waiver
(docs/API.md §2.6). It mirrors the C ABI’s tf_tree.h /
tf_tree_unstable.h split, which is the same promise spelled as two headers.
What lives there is what the arena layout shapes, because that layout is
scheduled to change (docs/PHASE5.md §1). If you are reading transforms, you
will never need it.
Gating a door is not the same as removing a room. The question the gated
Tree::arena_view used to be the only Rust answer to — what is in this
tree? — is answered on the stable tier by Tree::frames and
Tree::edges, which mirror Python’s tree.frames() / tree.edges()
(docs/API.md §3.2). Names only: the statistics half is docs/PHASE5.md
§4.2’s and is held back on every surface until §3’s counting pass. Enabling
unstable buys the arena-shaped spelling of that answer — record fields,
capacities, counters — not the answer itself.
The three items that moved do not answer at the crate root any more, and this
is what pins that — but only when the feature is on, and where that holds
moved in 0.0.1. It used to be every cargo test here, because the crate
dev-depended on itself to enable unstable; that line did not survive
cargo package and is gone. Today the assertion means “moved to
tf_tree::unstable” under cargo test --doc --workspace, which unifies the
feature in from the four consumers that declare it, and degrades to the
weaker “absent from the crate root” under a bare -p tf_tree. Both readings
are true; just test runs the strong one:
use tf_tree::ArenaView;use tf_tree::EdgeKind;use tf_tree::EdgeMeta;Three blocks and not one use tf_tree::{ArenaView, EdgeKind, EdgeMeta};,
because a single block passes as soon as any one of the three is absent —
it would go on passing after a refactor put two of them back.
E0432 and not a bare compile_fail: an unpinned one passes when the
snippet fails for any reason, and stable rustdoc ignores the code, so
just test-doc-error-codes is this line’s real gate (justfile).
§What the pre-tag audit left alone, and why
The sweep behind the split asked docs/API.md §7 of every pub item here.
Three moved; the rest stay, and two answers are worth recording because they
look like omissions:
EdgeWriterstill carries a lifetime, which §2.1 calls a violation. It is a known one and it is not the bug:OwnedWriteris the storable shape, and a scoped claim whose scope the borrow checker enforces is better when it fits. §2.1 says so in terms.Described’s two fields became private. They promised that theDisplaywrapper is exactly(error, tree)forever, for no caller — the only construction site in the workspace isTree::describe.
tf_tree_core’s crate docs carry the rule the audit applied to
#[non_exhaustive], and the per-type arguments sit on the types.
§no_std / std split
Everything arena-generic — Plan, Step, Guard, Stamp,
Domain, Query, the compile/evaluate engine — lives in the no_std
tf_tree_core. This crate adds only what needs std: the concrete Tree
owning a heap arena, the per-thread plan cache behind Tree::lookup
(thread_local!), and Described’s Display.
§Tree is not Clone, and Arc<Tree> is the embedding idiom
Tree is Send + Sync, so a shared reference is all a reader needs — but
it is deliberately not Clone, and the reason is that a Tree is not just a
handle. It owns its arena backing and holds a registered slot in the
arena’s participant table — a fixed-size table (DEFAULT_MAX_PARTICIPANTS,
64) sized when the arena is created, not an unbounded pool. A derived
Clone would have to pick one of two wrong answers: register a second slot,
and burn a scarce resource every time somebody passed a tree by value; or
share the first one, and report two participants as one to the reaper that
decides whether a slot’s owner is still alive.
So share it with an Arc:
use std::sync::Arc;
use tf_tree::{Iso3, Stamp, TreeBuilder};
let tree = Arc::new(
TreeBuilder::new()
.static_edge("map", "odom", &Iso3::IDENTITY)
.build()
.expect("layout"),
);
let reader = Arc::clone(&tree);
let joined = std::thread::spawn(move || {
let now: Stamp = Stamp::from_nanos(0);
reader.lookup("map", "odom", now)
})
.join()
.expect("reader thread");
assert_eq!(joined.unwrap(), Iso3::IDENTITY);This is not new advice, which is the point of writing it down: tests/tsan.rs
shares a tree between threads this way, tf_tree_c hands out
Arc<TreeShare> (a one-field wrapper around a Tree, so the refcount is on
the wrapper rather than on the Tree itself), and PyO3’s Py<PyTree> is the
same refcount spelled in CPython’s allocator. Three surfaces arrived here
independently and none of them said so where an embedder would look
(docs/API.md §2.2).
§Set lto = "thin" and codegen-units = 1 in your release profile
[profile.release]
lto = "thin"
codegen-units = 1This is worth about 25% of a depth-3 lookup, and it is not cargo-cult
advice — it is a property of where this engine’s code lives. Plan::at
sits across a crate boundary from every consumer, and it and the fold beneath
it live one crate further down still, in tf_tree_core. Five functions on
the evaluate path carry #[inline] for exactly that reason (Plan::at, the
scalar fold, and the three Guard sampling entry points), but what an
attribute buys depends on your profile, not on ours: cargo’s
--release defaults are lto = false, codegen-units = 16, and this
workspace’s are not, so every latency number this project publishes is taken
under whole-program optimisation and your node’s is not.
Measured rather than asserted, because the last claim made here about this
mechanism was wrong in a way only a probe could show. One program — a
depth-3 map <- imu_link lookup, LerpSlerp, off-grid stamps so the
interpolation runs, one lookup per non-inlinable call — built twice and
pinned to one core, nine rounds each, three consecutive runs:
| downstream profile | ns/lookup |
|---|---|
lto = false, codegen-units = 16 (cargo’s --release default) | 240 |
lto = "thin", codegen-units = 1 | 193–195 |
On a 4-physical-core AMD EPYC-Milan VM under moderate load, 2026-08-02, so read it as “about a quarter”, not as three digits — the ratio itself moved between 1.19× and 1.24× across those runs.
The same runs also say why, which is the part that makes this advice
rather than folklore. They time a second, identical body compiled inside
tf_tree_core, and compare it against the one outside:
| downstream profile | from outside the engine | from inside it |
|---|---|---|
lto = false, codegen-units = 16 | 240 ns | 191 ns |
lto = "thin", codegen-units = 1 | 193 ns | 194 ns |
At cargo’s defaults the crate boundary costs about a quarter of the lookup;
with thin LTO it costs nothing measurable, because the boundary is gone at
link time. just embed-cost in this repository re-measures both, and
docs/PHASE5.md §9.2 makes the second one a standing, gated benchmark row so
the next change to those attributes moves a number somebody sees.
The cost of taking this advice is build time: thin LTO adds a link-time
optimisation pass, and codegen-units = 1 gives up intra-crate build
parallelism. Both are compile-time costs and neither changes what the shipped
binary computes. How the 25% splits between the two settings has not been
measured here, so if your release builds are slow enough that you want to
take only one of them, measure your own case rather than trusting a guess
from this paragraph.
Modules§
- dualquat
- Fast SE(3) screw interpolation via unit dual-quaternion powers.
- unstable
- The unstable tier —
docs/API.md§2.6. Enabling theunstablefeature is the waiver; read the module’s own documentation for what it waives. The unstable tier. Nothing in this module is covered by semver.
Structs§
- Adaptive
Scratch - Caller-provided scratch storage for
Plan::at_adaptive, sized to hold the maximum knot set. Allocated once by the caller (its allocation is not counted againstat_adaptive, which never allocates globally). - Capacity
- Ring capacity for one dynamic edge, always a power of two.
- Described
- A
LookupErrorpaired with theTreethat can resolve its ids to names. - EdgeCfg
- Per-edge configuration for
TreeBuilder::dynamic_edge. - EdgeId
- Stable identity of an edge (index into the edge table).
- Edge
Writer - A claimed edge: the arena record, and the lease that makes its holder’s
death observable (
docs/PHASE2.md§6.1,docs/decisions/0005§5). - ErrBound
- The per-component error tolerance for
Plan::at_adaptive. - Extrapolated
- A pose, and how far past the plan’s newest common sample it was extrapolated.
- FrameId
- Stable identity of a frame.
- Frozen
Header - The
.tftcontainer header —docs/PHASE5.md§2.3, NORMATIVE. - Guard
- A batch-evaluation handle: it borrows the arena and pins the topology generation once, so a run of lookups validates against a single snapshot.
- Iso3
- A rigid-body transform in SE(3): a rotation
qfollowed by a translationt.T_parent_child— applying it to a point inchildyields the point inparent. - Lerp
Slerp - tf2-compatible interpolation: translation LERP + rotation SLERP.
- Open
- The
docs/PHASE2.md§3.2 builder. - Owned
Writer - An
EdgeWriterthat owns its tree — the claim shape for a writer that is stored rather than scoped (docs/decisions/0017,docs/API.md§2.1). - Plan
- A compiled
lookup(target, source)path. - Publisher
- Exclusive writer handle for one edge.
- Quat
- Unit quaternion, Hamilton convention, scalar (
w) first. - Sample
- A pose and its derivatives at one instant —
docs/PHASE4.md§2.2. - ScLerp
- SE(3) screw-geodesic interpolation — the default policy.
- Sensor
Domain - A sensor’s own clock (e.g. a lidar or camera timestamp), tag
1. Distinct fromSystemDomainso a stamp from one cannot be used to query the other. - SimDomain
- Simulated time — a
/clockpublisher, a bag replay, or a physics engine — tag2. - Stamp
- A nanosecond timestamp in domain
D. - Steady
Domain - A steady, monotone clock (
CLOCK_MONOTONIC-like), tag3. - System
Domain - The default domain: the host system clock (
CLOCK_REALTIME-like), tag0. - Tree
- A transform tree: a fixed-capacity arena plus the ergonomic operations for
publishing samples and looking up transforms. Build one with
TreeBuilder. - Tree
Builder - Builder for a
Tree. - Twist
- A body-frame (right) twist: angular velocity
ω(rad/s) and linear velocityv(m/s), both expressed in the moving frame. - Vec3
- A point or vector in R³.
Enums§
- Attach
Mode - Shared-memory attachment surface (Phase 2). Linux-only, behind
--features shm. How a process attaches to an existing segment. - Await
Error - Why
Tree::await_framescould not produce ids. - Build
Error - Failure building a
Treefrom aTreeBuilder. - Claim
ApiError - Failure claiming an edge for writing.
- Claim
Error - A failed attempt to claim exclusive write access to an edge.
- Create
Policy - Re-exported so a caller does not have to depend on
tf_tree_ipcdirectly just to name a policyopen()already takes. Whatopen()should do when no arena exists. - Extrap
Policy - What to do when the requested stamp is newer than every published sample.
- Frame
Error - A failed frame interning.
- Frozen
Error - Why a
.tftcould not be written or opened. - Frozen
File Error - Why a
.tftcould not be opened or written. - Inheritance
- How
Tree::inherit_ownershipresolved (§3.5). - Interp
Policy - Selects an interpolation policy at runtime from an edge’s stored discriminant.
- Layout
- How a transform is written into a caller’s buffer.
- Lookup
Error - A lookup or sample failure.
- Open
Error - Why
Open::opencould not produce aTree. - Push
Error - A failed
pushonto an edge’s sample ring. - Query
- A temporal query against a compiled
Plan. - Reparent
Error - Failure re-parenting a frame at runtime.
- ShmError
- Shared-memory attachment surface (Phase 2). Linux-only, behind
--features shm. Everything that can go wrong obtaining or validating a shared segment. - Step
- One step of a compiled plan.
Constants§
- ARENA_
FILE_ ALIGN - Alignment of the arena image within the file (§2.3).
- CRASH_
SITES - Every
docs/PHASE2.md§11.3 crash point compiled into this crate. - MAX_
ADAPTIVE_ DEPTH - Maximum bisection recursion depth in
Plan::at_adaptive. - MAX_
DEPTH - Maximum length of a compiled plan: the number of
plan::Stepslots aplan::Plancarries, counted after constant folding. - MAX_
KNOTS - Maximum number of knots
Plan::at_adaptivemay emit. - MAX_
PATH_ EDGES - Maximum number of raw path edges
plan::compilewill walk, counted across both sides of the lowest common ancestor before folding.
Traits§
- Domain
- A time domain: a compile-time marker carrying a runtime
Domain::TAGbyte. - Interp
- Interpolate between two poses
a(ats = 0) andb(ats = 1).
Functions§
- arena_
format_ version - This build’s arena format version (
docs/PHASE5.md§1). - arena_
layout_ hash - This build’s arena layout hash — the geometry, as distinct from the format version’s set of fields. Both are checked on attach.
- counters_
compiled_ in - Whether this build compiled
docs/PHASE5.md§5’s diagnostic counters in. - exp_se3
- SE(3) exponential: map a twist
ξ = [ω, v]to a rigid transform. - exp_so3
- SO(3) exponential: map a rotation vector
ω(axis × angle) to a unit quaternion. - log_se3
- SE(3) logarithm: map a rigid transform to its twist
ξ = [ω, v]. - log_so3
- SO(3) logarithm: map a unit quaternion to its rotation vector
ωin the principal branch|ω| ∈ [0, π]. - open
- Join the running arena, read-only.
- quat_
from_ rot3 - The unit quaternion of a row-major 3×3 rotation matrix.
- slerp
- Shortest-arc spherical linear interpolation of two unit quaternions.
- write_
affine32 - Write
isoas a row-major 3x4f32affine. - write_
mat4 - Write
isoas a row-major 4x4f64matrix. - write_
quat - Write
isoas[qw qx qy qz tx ty tz]. - write_
quat_ twist - Write
isoandtwistas[qw qx qy qz tx ty tz | ωx ωy ωz vx vy vz].