Skip to content
Open
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
14 changes: 7 additions & 7 deletions .yamato/global.metafile
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
test_editors:
- version: 2021.3
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we also remove this and upgrade the minimum unity version to 6000.0?

Same with romotion_test_editors

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the minimum version of the package, so we have to keep the job for promotion.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe its allow to bump the minimum version of the package if said version is not supported anymore without being considered a new major release. But if that would increase the scope of this PR, we can do it separately

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say it's indeed out of scope, so let's log a task to update this separately.

- version: 2022.3
- version: 6000.0
- version: 6000.2
- version: 6000.3
- version: 6000.4
- version: 6000.5
- version: trunk

nightly_test_editors:
- version: 2022.3
- version: 6000.0
- version: 6000.2
- version: 6000.3
- version: 6000.4
- version: trunk
- version: 6000.5
- version: trunk

clean_console_test_editors:
- version: 6000.0
- version: 6000.2
- version: trunk
- version: 6000.3
- version: 6000.4
- version: 6000.5
- version: trunk

promotion_test_editors:
- version: 2021.3
Expand Down
4 changes: 4 additions & 0 deletions com.unity.formats.alembic/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this package will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.4.5] - 2026-04-02
### Changed
- Update deprecated and obsolete APIs available in Unity 6000.4 and above.

## [2.4.4] - 2026-01-29
### Changed
- Signed binaries on Windows and Mac.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ static IEnumerable<T> GetTargets<T>(this AlembicRecorderSettings settings) where
{
return settings.TargetBranch != null ? settings.TargetBranch.GetComponentsInChildren<T>() : Enumerable.Empty<T>();
}
#if UNITY_2023_1_OR_NEWER
#if UNITY_6000_4_OR_NEWER
// Analytics only counts component instances; order does not affect the result.
return Object.FindObjectsByType<T>();
Copy link
Copy Markdown
Collaborator Author

@mfe mfe Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FindObjectsSortMode.InstanceID used in UNITY_2023_1_OR_NEWER code path is not needed as the result of GetTargets is only used with Any(), so I just removed the sorting mode here.

I was tempted to remove it in UNITY_2023_1_OR_NEWER code path too but on a very legacy package in basic support, the less we touch, the better.
Feel free to push back.

#elif UNITY_2023_1_OR_NEWER
return Object.FindObjectsByType<T>(FindObjectsSortMode.InstanceID);
#else
return Object.FindObjectsOfType<T>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IO;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
#if UNITY_EDITOR
using UnityEditor;
#endif
Expand Down Expand Up @@ -546,7 +547,11 @@ public override void Setup(Component c)
m_target = null;
return;
}
#if UNITY_6000_4_OR_NEWER
abcObject = parent.abcObject.NewXform(target.name + " (" + EntityId.ToULong(target.GetEntityId()).ToString("X16") + ")", timeSamplingIndex);
#else
abcObject = parent.abcObject.NewXform(target.name + " (" + target.GetInstanceID().ToString("X8") + ")", timeSamplingIndex);
#endif
m_target = target;
}

Expand Down Expand Up @@ -834,7 +839,11 @@ public void Dispose()

class CaptureNode
{
#if UNITY_6000_4_OR_NEWER
public ulong entityId;
#else
public int instanceID;
#endif
public Type componentType;
public CaptureNode parent;
public Transform transform;
Expand Down Expand Up @@ -872,9 +881,15 @@ class CapturerRecord

aeContext m_ctx;
ComponentCapturer m_root;
#if UNITY_6000_4_OR_NEWER
Dictionary<ulong, CaptureNode> m_nodes;
List<CaptureNode> m_newNodes;
List<ulong> m_entityIdsToRemove;
#else
Dictionary<int, CaptureNode> m_nodes;
List<CaptureNode> m_newNodes;
List<int> m_iidToRemove;
#endif
int m_lastTimeSamplingIndex;
int m_startFrameOfLastTimeSampling;

Expand Down Expand Up @@ -947,16 +962,96 @@ public static void ForceDisableBatching()

#endif

#if UNITY_6000_4_OR_NEWER
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explaination about why we need this:
⚠️ In previous versions of the code, a sorting by FindObjectsSortMode.instanceID was used to have the determinism needed for UpdateCaptureNodes. But that was an inconsistant way of sorting. Furthermore, with entityID, the small amount of creation-order semantics that was accidentally present in instanceID is removed. That's why this PR instroduces another way of sorting with determinism,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A concern that jumped out to me is performance.
When and how often is this sorting taking place? We are doing a lot more computations including string operations to get this determinism.

Also out of curiosity why is determinism important here? What would it mean not having determinism?

Copy link
Copy Markdown
Collaborator Author

@mfe mfe Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good questions :D

Put on your seat belt 😄

Some context

GetTargets (where this func is used) is the scene discovery mechanism. Its results feed ConstructTree, which inserts nodes into m_nodes aka builds the in-memory capture tree and ultimately determines the order in which Alembic nodes (NewXform, NewPolyMesh, etc.) are created inside the archive.

GetTargets is called at every capture frame during recording.

Why we want determinism

The tree topology is naturaly done and deterministic for parents/children because of the recursiveness used (aka we always resolves parents before children).

But what is not naturally stable is the sibling ordering, aka the order in which NewXform/NewPolyMesh is called on the children of a given parent, which determines the order those children appear in the Alembic file. If no sorting is done, sibling order will not be stable and produce .abc files that bit-wise/ascii-wise looks different when they technically are not (noise during versioning, unnessary cache invalidation...).

BTW the sorting by InstanceID was actually not great either, because from a Unity session to another, the instanceID could change and so the ordering of the sibblings.

The new sorting is based on the GO scene path and root-to-leaf sibling indices (that order is serialized into the scene file and restored identically every time the scene loads), this is now a structural and session-independent sorting.

Perf concern

At every captured frame, we unconditionally calls UpdateCaptureNodes, which calls GetTargets once per enabled capturer type (up to 4: Transform, Camera, MeshRenderer, SkinnedMeshRenderer) = SortComponentsByStableSceneHierarchy runs up to 4× per frame for the entire duration of recording 😅

So, ya, not great, the new sorting is probably worse than native InstanceID sorting.
That said there is room for improv, let me try.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I tried for few hours but the improvements I could get (without breaking everything) are almost anecdoctical and there is still this idea of minimalizing impact / halo in the changes we do in this package.

To mitigate, this is only run when we record. So I would stop there.
Feel free to push back if you feel I should persevere in trying to improve the perf.

How about that @LeoUnity ?

/// <summary>
/// Sorts <paramref name="components"/> in place into a stable order derived from each object’s scene and
/// transform hierarchy (see <see cref="AppendStableHierarchySortKey(StringBuilder, Component, List{int})"/>).
/// </summary>
/// <remarks>
/// <para>
/// On Unity 6.4 and newer, sort mode is deprecated for <c>FindObjectsByType</c>, so the returned array has no
/// defined order. Sorting by <c>InstanceID</c> / <c>EntityId</c> is also discouraged. This method reorders the
/// array so exporter iteration (for example from <c>GetTargets</c>) is deterministic and aligned with scene and
/// hierarchy rather than identity allocation.
/// </para>
/// <para>
/// Implementation: allocate a parallel <c>string[]</c> of the same length, fill each entry by clearing a shared
/// <see cref="StringBuilder"/> and a shared <c>List&lt;int&gt;</c> scratchpad, calling
/// <see cref="AppendStableHierarchySortKey(StringBuilder, Component, List{int})"/>, and assigning
/// <c>keys[i] = sb.ToString()</c>. Then <c>Array.Sort(keys, components, StringComparer.Ordinal)</c>
/// reorders <paramref name="components"/> to match ascending key order. Arrays of length 0 or 1, or a null
/// reference, are left unchanged without allocating.
/// </para>
/// </remarks>
internal static void SortComponentsByStableSceneHierarchy(Component[] components)
{
if (components == null || components.Length <= 1)
return;

var keys = new string[components.Length];
var sb = new StringBuilder(128);
var scratchpad = new List<int>(8);
for (int i = 0; i < components.Length; i++)
{
sb.Clear();
scratchpad.Clear();
AppendStableHierarchySortKey(sb, components[i], scratchpad);
keys[i] = sb.ToString();
}

Array.Sort(keys, components, StringComparer.Ordinal);
}

/// <summary>
/// Appends a deterministic sort key for <paramref name="c"/> to <paramref name="sb"/>.
/// </summary>
/// <remarks>
/// <para>
/// Unity 6.4+ no longer guarantees an ordering for <c>FindObjectsByType</c>, and engine guidance is to avoid
/// sorting by <c>InstanceID</c> / <c>EntityId</c>. This key instead fixes a stable order from scene identity
/// and transform hierarchy so export iteration is reproducible.
/// </para>
/// <para>
/// The key is built as: <c>scene.path</c>, a U+0001 separator, then the path from scene root to the component’s transform as dot-separated
/// <see cref="Transform.GetSiblingIndex"/> values, each formatted as eight digits (<c>D8</c>) so lexical
/// string order matches numeric sibling order. Walking from leaf to root and emitting indices root-first yields
/// depth-first hierarchy order when keys are compared with <see cref="StringComparer.Ordinal"/>.
/// </para>
/// </remarks>
internal static void AppendStableHierarchySortKey(StringBuilder sb, Component c, List<int> scratchpad)
{
var scene = c.gameObject.scene;
sb.Append(scene.path);
sb.Append('\x1');
var t = c.transform;
while (t != null)
{
scratchpad.Add(t.GetSiblingIndex());
t = t.parent;
}
for (int i = scratchpad.Count - 1; i >= 0; i--)
{
if (i < scratchpad.Count - 1)
sb.Append('.');
sb.Append(scratchpad[i].ToString("D8"));
}
}
#endif

Component[] GetTargets(Type type)
{
if (m_settings.Scope == ExportScope.TargetBranch && TargetBranch != null)
return TargetBranch.GetComponentsInChildren(type);
else
#if UNITY_2023_1_OR_NEWER
return Array.ConvertAll<UnityEngine.Object, Component>(GameObject.FindObjectsByType(type, FindObjectsSortMode.InstanceID), e => (Component)e);

#if UNITY_6000_4_OR_NEWER
var objects = GameObject.FindObjectsByType(type);
var components = Array.ConvertAll<UnityEngine.Object, Component>(objects, e => (Component)e);
SortComponentsByStableSceneHierarchy(components);
return components;
#elif UNITY_2023_1_OR_NEWER
return Array.ConvertAll<UnityEngine.Object, Component>(GameObject.FindObjectsByType(type, FindObjectsSortMode.InstanceID), e => (Component)e);
#else
return Array.ConvertAll<UnityEngine.Object, Component>(GameObject.FindObjectsOfType(type), e => (Component)e);
return Array.ConvertAll<UnityEngine.Object, Component>(GameObject.FindObjectsOfType(type), e => (Component)e);
#endif
}

Expand All @@ -974,15 +1069,29 @@ CaptureNode ConstructTree(Transform node)
{
if (node == null) { return null; }

#if UNITY_6000_4_OR_NEWER
ulong entityId = EntityId.ToULong(node.gameObject.GetEntityId());
#else
int iid = node.gameObject.GetInstanceID();
#endif
CaptureNode cn;
#if UNITY_6000_4_OR_NEWER
if (m_nodes.TryGetValue(entityId, out cn)) { return cn; }

cn = new CaptureNode();
cn.entityId = entityId;
cn.transform = node;
cn.parent = ConstructTree(node.parent);
m_nodes.Add(entityId, cn);
#else
if (m_nodes.TryGetValue(iid, out cn)) { return cn; }

cn = new CaptureNode();
cn.instanceID = iid;
cn.transform = node;
cn.parent = ConstructTree(node.parent);
m_nodes.Add(iid, cn);
#endif
m_newNodes.Add(cn);
return cn;
}
Expand Down Expand Up @@ -1028,9 +1137,15 @@ void SetupComponentCapturer(CaptureNode node)
if (parent != null && parent.transformCapturer == null)
{
SetupComponentCapturer(parent);
#if UNITY_6000_4_OR_NEWER
if (!m_nodes.ContainsKey(parent.entityId) || !m_newNodes.Contains(parent))
{
m_nodes.Add(parent.entityId, parent);
#else
if (!m_nodes.ContainsKey(parent.instanceID) || !m_newNodes.Contains(parent))
{
m_nodes.Add(parent.instanceID, parent);
#endif
m_newNodes.Add(parent);
}
}
Expand Down Expand Up @@ -1143,9 +1258,15 @@ public bool BeginRecording()
}

m_root = new RootCapturer(this, m_ctx.topObject);
#if UNITY_6000_4_OR_NEWER
m_nodes = new Dictionary<ulong, CaptureNode>();
m_newNodes = new List<CaptureNode>();
m_entityIdsToRemove = new List<ulong>();
#else
m_nodes = new Dictionary<int, CaptureNode>();
m_newNodes = new List<CaptureNode>();
m_iidToRemove = new List<int>();
#endif
m_lastTimeSamplingIndex = 1;
m_startFrameOfLastTimeSampling = 0;

Expand All @@ -1170,7 +1291,11 @@ public void EndRecording()
{
if (!m_recording) { return; }

#if UNITY_6000_4_OR_NEWER
m_entityIdsToRemove = null;
#else
m_iidToRemove = null;
#endif
m_newNodes = null;
m_nodes = null;
m_root = null;
Expand Down Expand Up @@ -1213,14 +1338,24 @@ public void ProcessRecording()
var node = kvp.Value;
node.Capture();
if (node.transform == null)
#if UNITY_6000_4_OR_NEWER
m_entityIdsToRemove.Add(node.entityId);
#else
m_iidToRemove.Add(node.instanceID);
#endif
}
m_ctx.MarkFrameEnd();

// remove deleted GameObjects
#if UNITY_6000_4_OR_NEWER
foreach (ulong entityId in m_entityIdsToRemove)
m_nodes.Remove(entityId);
m_entityIdsToRemove.Clear();
#else
foreach (int iid in m_iidToRemove)
m_nodes.Remove(iid);
m_iidToRemove.Clear();
#endif

// advance time
++m_frameCount;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ Mesh AddMeshComponents(GameObject go)

internal static Material GetDefaultMaterial()
{
var pipelineAsset = GraphicsSettings.renderPipelineAsset;
var pipelineAsset = GraphicsSettings.defaultRenderPipeline;
if (pipelineAsset != null)
{
return pipelineAsset.defaultMaterial;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
#if UNITY_6000_4_OR_NEWER
using System.Threading;
#endif
using Unity.Jobs;
using UnityEngine;
#if UNITY_EDITOR
Expand Down Expand Up @@ -87,6 +90,16 @@ public void Execute()

static List<AlembicStream> s_streams = new List<AlembicStream>();

#if UNITY_6000_4_OR_NEWER
// Native aiContext keys are int; use a monotonic surrogate instead of narrowing EntityId (ulong → int
// would silently discard the upper 32 bits). Starts at 0; Interlocked.Increment returns 1 on the
// first call, so UID 0 is never issued — matching GetInstanceID's guarantee for live objects.
// Theoretical int overflow after ~2 billion AbcLoad calls is impossible in practice.
static int s_nextNativeContextUid;

static int AllocateNativeContextUid() => Interlocked.Increment(ref s_nextNativeContextUid);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should/could this be uint? we are leaving half of its range on the table

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using the type of the native function that this id is for (aka aiContextCreate(int uid))

#endif

public static void DisconnectStreamsWithPath(string path)
{
aiContext.DestroyByPath(path);
Expand Down Expand Up @@ -222,7 +235,11 @@ void ClearMotionVectors(AlembicTreeNode node)
public bool AbcLoad(bool createMissingNodes, bool serializeMesh)
{
m_time = 0.0f;
#if UNITY_6000_4_OR_NEWER
m_context = new SafeContext(aiContext.Create(AllocateNativeContextUid()));
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The native function called at the end of the line is aiContextCreate(int uid). So if we keep using a ulong here, we ultimately have to cast it, which can lead to some narrowing and rare but potential collisions.
But in the code where the id is used, there is no need for a correlation between the id and the abcTreeRoot object, we just need an id to destroy on dispose (cf aiContextDestroy(aiContext* ctx)).

Thus the introduction of an alternative surrogate id.

A question for reviewers: what is the bigger risk ?
1- Changing this code
2- Narrowing and collision
😅

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this approach you took is great!

#else
m_context = new SafeContext(aiContext.Create(m_abcTreeRoot.gameObject.GetInstanceID()));
#endif

var settings = m_streamDesc.Settings;
m_config.swapHandedness = settings.SwapHandedness;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ public void Execute(int curveIdx)

static Material GetDefaultMaterial()
{
return GraphicsSettings.renderPipelineAsset != null ? GraphicsSettings.renderPipelineAsset.defaultMaterial : new Material(Shader.Find("Diffuse"));
return GraphicsSettings.defaultRenderPipeline != null ? GraphicsSettings.defaultRenderPipeline.defaultMaterial : new Material(Shader.Find("Diffuse"));
}
}
}
Loading