Skip to main content

Animation output and ragdolls

Animation generator output is callback-owned transient memory. Graph managers are retained engine objects protected by their own lock. hknp ragdoll world changes additionally require a world write epoch. Keep these three lifetime domains separate.

View a generator track

#include <RPS/Runtime/AnimationPose.h>
#include <RPS/Runtime/GeneratorOutput.h>

using namespace RPS::Runtime::Animation;

const TrackView poseTrack = viewTrack(
generatorOutput,
PoseTrack,
sizeof(HkQsTransform),
TrackAccess::ActiveRead);

if (!poseTrack) {
return;
}

viewTrack() validates the master header, track count, blob size, element size, data range, palette/sparse range, disabled state, and requested access. Returned pointers and spans are valid only until the current synchronous graph callback returns.

Never cache a TrackView, TrackHeader*, data, indices, generator output, driver, context, skeleton, or graph pointer.

Capture and copy poses

#include <RPS/Runtime/AnimationPose.h>

std::array<HkQsTransform, 256> poseStorage{};
const auto captured = capturePose(generatorOutput, poseStorage);
if (!captured) {
return;
}

const auto pose = std::span{poseStorage}.first(captured.count);

The functions use caller-provided storage and report the required count:

  • capturePose() and captureWorldFromModel() copy callback data out;
  • copyPoseToOutput() and copyWorldFromModelToOutput() write validated values into mutable track storage;
  • blendPoseToOutput() and blendWorldFromModelToOutput() blend into output;
  • blendTransform() is a pure transform helper.

All transforms must be finite. Pose operations reject undersized spans and use shortest-hemisphere quaternion interpolation.

Inspect or tune motor tracks

#include <RPS/Runtime/AnimationMotor.h>

const MotorInspectionResult before =
inspectDriveToPoseMotorState(generatorOutput);

MotorControlSettings settings{};
settings.forceKeyframedControls = true;
settings.forcePoweredControls = true;
settings.poweredMaxForce = 500.0f;

const MotorApplyResult applied = applyDriveToPoseMotorControl(
generatorOutput,
settings,
activeDriveSeconds,
ragdollBodyCount,
suppressPoweredForThisFrame);

This API works only in the synchronous generator callback that owns the output. It validates keyframed-control, keyframed-bone, powered-control, and world-from-model-mode tracks before changing them. Use the detailed result flags; tracksResolved alone does not mean every requested control was applied.

sanitizeMotorControlSettings() clamps invalid/non-finite tuning to the framework's supported domain. Policy—when to drive, which bodies to control, and how to transition out—remains in the consuming mod.

Acquire and inspect actor ragdolls

#include <RPS/Runtime/Ragdoll.h>

using namespace RPS::Runtime::Physics;

GraphManagerLease manager = acquireActorGraphManager(actor);
if (!manager) {
return;
}

std::array<RagdollPointers, 8> ragdolls{};
{
GraphManagerLock lock{manager};
if (!lock.active()) {
return;
}

const auto collected = collectRagdolls(manager, lock, ragdolls);
if (!collected || collected.completeCount == 0) {
return;
}

std::array<BodyId, 256> bodyIds{};
const auto copied = copyRagdollBodyIds(
manager,
lock,
ragdolls[0],
bodyIds);
}

acquireActorGraphManager() returns one retained reference. The lock protects the manager's current native graph array. All pointers in RagdollPointers are borrowed and valid only while both the matching lease and lock remain alive on the current thread. Copy body IDs or other value data before leaving the scope.

Use writtenCount, completeCount, validCount, and truncated instead of assuming the fixed output captured every graph/body.

Activate or remove the current ragdoll

After read-only collection is complete and the graph lock has been released, move the retained lease into RagdollManagerApi:

RagdollManagerApi ragdollApi{
module,
std::move(manager),
hknpWorld};

WorldWriteGuard write{module, hknpWorld};
if (!write.active()) {
return;
}

const auto activated = ragdollApi.activate(write, bhkWorld);
if (!activated) {
logActivationStage(activated.stage, activated.error);
return;
}

Activation preserves the proven native sequence:

  1. query whether the graph has a ragdoll;
  2. set the physics world;
  3. remove attachments;
  4. add the ragdoll to the world;
  5. update constraints.

RagdollActivationResult records the last stage and each native operation. Removal uses the same world-write contract and reports removal and constraint update separately. Graph sync/cache setters are narrow native calls; they do not implement a high-level active-ragdoll state machine.

Root-pose primitives

RootPoseApi wraps callback-scoped driver/context operations:

#include <RPS/Runtime/RootPose.h>

RootPoseApi roots{module, driver, context};

std::array<std::int16_t, 256> parents{};
const auto skeleton = roots.copyLowSkeletonParents(parents);
if (!skeleton) {
return;
}

const auto anchor = roots.readBodyAnchor(0);

It can map high-to-low poses, apply/copy scale, convert local poses to world, and read checked body anchors. The pure root helpers apply local body offsets, compute/apply translation bias, and sample translation/horizontal/yaw deltas.

The framework deliberately does not expose live world-from-model output compensation. Existing runtime evidence shows feedback and cross-space yaw hazards; root sampling and math are reusable, but the unstable mutation lane is not a stable API.

Callback checklist

  • Preallocate pose, parent, body-ID, and diagnostic buffers.
  • Do not allocate, perform I/O, or emit repeated formatted logs in the callback.
  • Validate the track/result needed by the current operation, not just the generator-output pointer.
  • Copy durable values before returning.
  • Release graph locks before world mutation or other subsystem calls.
  • Keep drive/activation policy and deterministic cleanup in the consumer.