stride3d/stride · error · InvalidOperationException

Skeleton nodes are not sorted

Error message

Skeleton nodes are not sorted

What it means

ShapeCacheSystem.ExtractNodeTransforms walks a skeleton's nodes in order, computing world matrices by composing each node with its parent at an earlier index. If a node references a ParentIndex >= its own position, the parent transform has not been computed yet, so the system throws InvalidOperationException('Skeleton nodes are not sorted'). This enforces the library invariant that skeleton nodes must be stored parent-before-child.

Solutions

  1. Sort skeleton nodes topologically so every ParentIndex is strictly less than the node's own index
  2. Fix the asset/exporter so parents are emitted before children
  3. After modifying node parenting at runtime, rebuild/re-sort the node list and remap indices
  4. Detect cycles: a valid sorted skeleton has exactly one node with ParentIndex == -1 (the root) and no index >= position

Example fix

// before
nodes.Add(new Node { ParentIndex = 2 }); // index 0 depends on later node
nodes.Add(new Node { ParentIndex = -1 });
nodes.Add(new Node { ParentIndex = 1 });
// after
// topologically sort so parents come first, then remap ParentIndex
nodes.Sort((a, b) => CompareDepth(a, b)); // root (-1) first
RemapParentIndices(nodes);
Defensive patterns

Strategy: validation

Validate before calling

for (int i = 0; i < nodes.Count; ++i)
    if (nodes[i].ParentIndex >= i)
        throw new InvalidOperationException($"Node {i} references parent {nodes[i].ParentIndex}; sort nodes parent-first.");

Type guard

static bool IsSorted(IReadOnlyList<SkeletonNode> nodes) { for (int i = 0; i < nodes.Count; ++i) if (nodes[i].ParentIndex >= i) return false; return true; }

Try / catch

try { ExtractNodeTransforms(...); } catch (InvalidOperationException ex) when (ex.Message.Contains("not sorted")) { /* topologically sort skeleton and retry */ }

Prevention

When it happens

Trigger: Assigning a Skeleton/NodeInformation with nodes whose ParentIndex points to a later node (or forming a cycle); building or editing skeleton asset data programmatically without topologically ordering nodes; deserializing hand-authored or corrupted skeleton assets.

Common situations: Procedurally generated skeletons where nodes were appended child-first; content pipeline exports with unsorted node lists; editing bone parenting in code after loading and forgetting to re-sort.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14). Data as JSON: /api/errors/42ae7d7d52243a22. Report an issue: GitHub.

Appendix: source

Thrown at sources/engine/Stride.BepuPhysics/Stride.BepuPhysics/Systems/ShapeCacheSystem.cs:245

    private static Matrix[]? ExtractNodeTransforms(Model model)
    {
        Matrix[]? nodeTransforms = null;
        if (model.Skeleton == null)
            return nodeTransforms;

        var nodesLength = model.Skeleton.Nodes.Length;
        nodeTransforms = new Matrix[nodesLength];
        nodeTransforms[0] = Matrix.Identity;
        for (var i = 0; i < nodesLength; i++)
        {
            var node = model.Skeleton.Nodes[i];
            Matrix.Transformation(ref node.Transform.Scale, ref node.Transform.Rotation, ref node.Transform.Position, out var localMatrix);

            Matrix worldMatrix;
            if (node.ParentIndex != -1)
            {
                if (node.ParentIndex >= i)
                    throw new InvalidOperationException("Skeleton nodes are not sorted");
                var nodeTransform = nodeTransforms[node.ParentIndex];
                Matrix.Multiply(ref localMatrix, ref nodeTransform, out worldMatrix);
            }
            else
            {
                worldMatrix = localMatrix;
            }

            if (i != 0)
            {
                nodeTransforms[i] = worldMatrix;
            }
        }

        return nodeTransforms;
    }

    /// <summary>

View on GitHub (pinned to 96fad776d2)