IJobParallelForTransforms wrong indexes

I tried to write my version of CopyTransformToGameObjectSystem which also can do the same on request instead of every frame. It is just like CopyTransformToGameObjectSystem but it also handles entities with custom CopyTransformToGameObjectRequest.

[UpdateInGroup(typeof(TransformSystemGroup))]
public class CustomTransformSyncSystem : JobComponentSystem
{
    private EntityQuery _transformSyncQuery;

    protected override void OnCreate()
    {
        base.OnCreate();
        _transformSyncQuery = GetEntityQuery
        (
            new EntityQueryDesc
            {
                All = new[]
                {
                    ComponentType.ReadOnly<Transform>(),
                    ComponentType.ReadOnly<LocalToWorld>()
                },
                Any = new[]
                {
                    ComponentType.ReadOnly<CopyTransformToGameObject>(),
                    ComponentType.ReadOnly<CopyTransformToGameObjectRequest>()
                }
            }
        );
    }

    protected override Unity.Jobs.JobHandle OnUpdate(Unity.Jobs.JobHandle inputDeps)
    {
        var copyTransformsJob = new CopyTransforms
        {
            LocalToWorlds = _transformSyncQuery.ToComponentDataArrayAsync<LocalToWorld>(Allocator.TempJob, out inputDeps),
        };

        return copyTransformsJob.Schedule(_transformSyncQuery.GetTransformAccessArray(), inputDeps);
    }
}
[BurstCompile]
struct CopyTransforms : IJobParallelForTransform
{
    [DeallocateOnJobCompletion]
    [ReadOnly] public NativeArray<LocalToWorld> LocalToWorlds;

    public void Execute(int index, TransformAccess transform)
    {
        var value = LocalToWorlds[index];
        transform.localPosition = value.Position;
        transform.rotation = new quaternion(value.Value);
    }
}

To make CopyTransformToGameObjectRequest one frame i use another system to cleanup entities with this component.

public class CopyTransformRequestCleanupSystem : SystemBase
{
    private EntityQuery _cleanupQuery;

    protected override void OnCreate()
    {
        base.OnCreate();
        _cleanupQuery = GetEntityQuery
        (
            typeof(CopyTransformToGameObjectRequest)
        );
    }
    protected override void OnUpdate()
    {
        EntityManager.RemoveComponent<CopyTransformToGameObjectRequest>(_cleanupQuery);
    }
}

And if CopyTransformRequestCleanupSystem goes after CustomTransformSyncSystem it produces index out of range exception.

System.IndexOutOfRangeException: Index {0} is out of range of '{1}' Length.
Thrown from job: AssemblyCSharp.Assets.Source.Simulation.Test.CopyTransforms
This Exception was thrown from a job compiled with Burst, which has limited exception support. Turn off burst (Jobs -> Burst -> Enable Compilation) to inspect full exceptions & stacktraces

The reason i tried to implement such system is that i recognized that some transform sync happens not immediately but in random moment. Trying to find issue i faced this.

LocalToWorlds = _transformSyncQuery.ToComponentDataArrayAsync(Allocator.TempJob, out inputDeps),

You’re overriding the input handle and losing the dependency chain being passed into the system.

You need to combine dependencies with the handle from ToComponentDataArrayAsync and inputdeps, not override it.

Here is original CopyTransformToGameObjectSystem which do the same.

protected override JobHandle OnUpdate(JobHandle inputDeps)
{
    var transforms = m_TransformGroup.GetTransformAccessArray();

    var copyTransformsJob = new CopyTransforms
    {
        LocalToWorlds = m_TransformGroup.ToComponentDataArrayAsync<LocalToWorld>(Allocator.TempJob, out inputDeps),
    };

    return copyTransformsJob.Schedule(transforms, inputDeps);
}

Have we found a bug?

I was trying to dig one more time into CopyTransformToGameObjectSystem and have changed little bit CopyTransforms job to debug values.

struct CopyTransforms : IJobParallelForTransform
{
    [DeallocateOnJobCompletion]
    [ReadOnly] public NativeArray<LocalToWorld> LocalToWorlds;

    public void Execute(int index, TransformAccess transform)
    {
        var value = LocalToWorlds[index];
        var temp = transform.position;
        transform.position = value.Position;
        transform.rotation = new quaternion(value.Value);
        UnityEngine.Debug.Log($"start pos: {temp}, expected: {value.Position}, real: {transform.position}");
    }
}


As you can see expected and real values are different. But i can’t understand how it even possible, because TransformAccess transform is just struct with auto properties so how it can produce those results?
It happens with entities which have CompanionLink and after some entity with CopyTransformToGameObject componet but without CompanionLink has appeared all goes well and transform got right values.

Internally, it looks like this:

using System;
using System.Runtime.CompilerServices;
using UnityEngine.Bindings;

namespace UnityEngine.Jobs
{
  /// <summary>
  ///   <para>Position, rotation and scale of an object.</para>
  /// </summary>
  [NativeHeader("Runtime/Transform/ScriptBindings/TransformAccess.bindings.h")]
  public struct TransformAccess
  {
    private IntPtr hierarchy;
    private int index;

    /// <summary>
    ///   <para>The position of the transform in world space.</para>
    /// </summary>
    public Vector3 position
    {
      get
      {
        Vector3 p;
        TransformAccess.GetPosition(ref this, out p);
        return p;
      }
      set => TransformAccess.SetPosition(ref this, ref value);
    }

    /// <summary>
    ///   <para>The rotation of the transform in world space stored as a Quaternion.</para>
    /// </summary>
    public Quaternion rotation
    {
      get
      {
        Quaternion r;
        TransformAccess.GetRotation(ref this, out r);
        return r;
      }
      set => TransformAccess.SetRotation(ref this, ref value);
    }

    /// <summary>
    ///   <para>The position of the transform relative to the parent.</para>
    /// </summary>
    public Vector3 localPosition
    {
      get
      {
        Vector3 p;
        TransformAccess.GetLocalPosition(ref this, out p);
        return p;
      }
      set => TransformAccess.SetLocalPosition(ref this, ref value);
    }

    /// <summary>
    ///   <para>The rotation of the transform relative to the parent transform's rotation.</para>
    /// </summary>
    public Quaternion localRotation
    {
      get
      {
        Quaternion r;
        TransformAccess.GetLocalRotation(ref this, out r);
        return r;
      }
      set => TransformAccess.SetLocalRotation(ref this, ref value);
    }

    /// <summary>
    ///   <para>The scale of the transform relative to the parent.</para>
    /// </summary>
    public Vector3 localScale
    {
      get
      {
        Vector3 r;
        TransformAccess.GetLocalScale(ref this, out r);
        return r;
      }
      set => TransformAccess.SetLocalScale(ref this, ref value);
    }

    /// <summary>
    ///   <para>Matrix that transforms a point from local space into world space (Read Only).</para>
    /// </summary>
    public Matrix4x4 localToWorldMatrix
    {
      get
      {
        Matrix4x4 m;
        TransformAccess.GetLocalToWorldMatrix(ref this, out m);
        return m;
      }
    }

    /// <summary>
    ///   <para>Matrix that transforms a point from world space into local space (Read Only).</para>
    /// </summary>
    public Matrix4x4 worldToLocalMatrix
    {
      get
      {
        Matrix4x4 m;
        TransformAccess.GetWorldToLocalMatrix(ref this, out m);
        return m;
      }
    }

    [NativeMethod(IsFreeFunction = true, IsThreadSafe = true, Name = "TransformAccessBindings::GetPosition")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void GetPosition(ref TransformAccess access, out Vector3 p);

    [NativeMethod(IsFreeFunction = true, IsThreadSafe = true, Name = "TransformAccessBindings::SetPosition")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void SetPosition(ref TransformAccess access, ref Vector3 p);

    [NativeMethod(IsFreeFunction = true, IsThreadSafe = true, Name = "TransformAccessBindings::GetRotation")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void GetRotation(ref TransformAccess access, out Quaternion r);

    [NativeMethod(IsFreeFunction = true, IsThreadSafe = true, Name = "TransformAccessBindings::SetRotation")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void SetRotation(ref TransformAccess access, ref Quaternion r);

    [NativeMethod(IsFreeFunction = true, IsThreadSafe = true, Name = "TransformAccessBindings::GetLocalPosition")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void GetLocalPosition(ref TransformAccess access, out Vector3 p);

    [NativeMethod(IsFreeFunction = true, IsThreadSafe = true, Name = "TransformAccessBindings::SetLocalPosition")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void SetLocalPosition(ref TransformAccess access, ref Vector3 p);

    [NativeMethod(IsFreeFunction = true, IsThreadSafe = true, Name = "TransformAccessBindings::GetLocalRotation")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void GetLocalRotation(ref TransformAccess access, out Quaternion r);

    [NativeMethod(IsFreeFunction = true, IsThreadSafe = true, Name = "TransformAccessBindings::SetLocalRotation")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void SetLocalRotation(ref TransformAccess access, ref Quaternion r);

    [NativeMethod(IsFreeFunction = true, IsThreadSafe = true, Name = "TransformAccessBindings::GetLocalScale")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void GetLocalScale(ref TransformAccess access, out Vector3 r);

    [NativeMethod(IsFreeFunction = true, IsThreadSafe = true, Name = "TransformAccessBindings::SetLocalScale")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void SetLocalScale(ref TransformAccess access, ref Vector3 r);

    [NativeMethod(IsFreeFunction = true, IsThreadSafe = true, Name = "TransformAccessBindings::GetLocalToWorldMatrix")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void GetLocalToWorldMatrix(ref TransformAccess access, out Matrix4x4 m);

    [NativeMethod(IsFreeFunction = true, IsThreadSafe = true, Name = "TransformAccessBindings::GetWorldToLocalMatrix")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void GetWorldToLocalMatrix(ref TransformAccess access, out Matrix4x4 m);
  }
}

So the actual Write data location and Read locations are different. This most likely done to ensure internal data change is applied to the Transform only after all jobs complete.

1 Like