The DebugStream system doesn't clean up properly when the system is destroyed

The DebugStream system spawns a GameObject and attaches a DrawComponent MonoBehaviour that points back to the system. This GameObject doesn’t get properly cleaned up/destroyed in OnDestroy on the system. It’s quite easy to reproduce this by just spawning and destroying worlds that contain the dots physics systems.

I’m not looking for a solution to this problem, as I know how I would handle cleaning up this object myself. Merely reporting a small and simple bug.

2 Likes

Do you know what triggers this? I occasionally get it too but I am unsure what specifically causes it.

i found a simple “solution” to this, just disable “Enter Play Mode Settings”, but it’s definely a bug

Thanks, I’ve logged this issue. It’s been bugging me for a while as well.

This doesn’t seem to be handled properly quite yet, so I thought I’d post a suggestion to how you can change your DebugStream system to handle it better. I see it as there being a few issues:

  • The GameObject created isn’t properly tagged with HideFlags.DontSave. The created object is therefore often saved inside scenes when running physics systems in the Editor World (outside of Playmode).
  • When destroying the system, the object created by the system isn’t cleaned up. We create and destroy worlds occasionally, which can lead to an abundance of these gameobjects.

I’ve modified OnUpdate and OnDestroy in my DebugStream.cs in this way:

And here’s the whole file with the diffs made.

using System.Collections.Generic;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Physics;
using Unity.Physics.Systems;
using UnityEditor;
using UnityEngine;

[UpdateBefore(typeof(BuildPhysicsWorld))]
public class DebugStream : SystemBase
{
    readonly List<NativeStream> m_DebugStreams = new List<NativeStream>();
    DrawComponent m_DrawComponent;
    EndFramePhysicsSystem m_EndFramePhysicsSystem;

    protected override void OnCreate()
    {
        m_EndFramePhysicsSystem = World.GetOrCreateSystem<EndFramePhysicsSystem>();
    }

    public struct Context
    {
        public void Begin(int index)
        {
            Writer.BeginForEachIndex(index);
        }

        public void End()
        {
            Writer.EndForEachIndex();
        }

        public void Point(float3 x, float size, Color color)
        {
            Writer.Write(Type.Point);
            Writer.Write(new Point { X = x, Size = size, Color = color });
        }

        public void Line(float3 x0, float3 x1, Color color)
        {
            Writer.Write(Type.Line);
            Writer.Write(new Line { X0 = x0, X1 = x1, Color = color });
        }

        public void Arrow(float3 x, float3 v, Color color)
        {
            Writer.Write(Type.Arrow);
            Writer.Write(new Line { X0 = x, X1 = x + v, Color = color });
        }

        public void Plane(float3 x, float3 v, Color color)
        {
            Writer.Write(Type.Plane);
            Writer.Write(new Line { X0 = x, X1 = x + v, Color = color });
        }

        public void Circle(float3 x, float3 v, Color color)
        {
            Writer.Write(Type.Circle);
            Writer.Write(new Line { X0 = x, X1 = x + v, Color = color });
        }

        public void Arc(float3 center, float3 normal, float3 arm, float angle, Color color)
        {
            Writer.Write(Type.Arc);
            Writer.Write(new Arc { Center = center, Normal = normal, Arm = arm, Angle = angle, Color = color });
        }

        public void Cone(float3 point, float3 axis, float angle, Color color)
        {
            Writer.Write(Type.Cone);
            Writer.Write(new Cone { Point = point, Axis = axis, Angle = angle, Color = color });
        }

        public void Box(float3 size, float3 center, quaternion orientation, Color color)
        {
            Writer.Write(Type.Box);
            Writer.Write(new Box { Size = size, Center = center, Orientation = orientation, Color = color });
        }

        public void Text(char[] text, float3 x, Color color)
        {
            Writer.Write(Type.Text);
            Writer.Write(new Text { X = x, Color = color, Length = text.Length });

            foreach (char c in text)
            {
                Writer.Write(c);
            }
        }

        internal NativeStream.Writer Writer;
    }

    public Context GetContext(int foreachCount)
    {
        var stream = new NativeStream(foreachCount, Allocator.TempJob);
        m_DebugStreams.Add(stream);
        return new Context { Writer = stream.AsWriter() };
    }

    public enum Type
    {
        Point,
        Line,
        Arrow,
        Plane,
        Circle,
        Arc,
        Cone,
        Text,
        Box
    }

    public struct Point
    {
        public float3 X;
        public float Size;
        public Color Color;

        public void Draw()
        {
#if UNITY_EDITOR
            Handles.color = Color;
            Handles.DrawLine(X - new float3(Size, 0, 0), X + new float3(Size, 0, 0));
            Handles.DrawLine(X - new float3(0, Size, 0), X + new float3(0, Size, 0));
            Handles.DrawLine(X - new float3(0, 0, Size), X + new float3(0, 0, Size));
#endif
        }
    }

    public struct Line
    {
        public float3 X0;
        public float3 X1;
        public Color Color;

        public void Draw()
        {
#if UNITY_EDITOR
            Handles.color = Color;
            Handles.DrawLine(X0, X1);
#endif
        }

        public void DrawArrow()
        {
            if (!math.all(X0 == X1))
            {
#if UNITY_EDITOR
                Handles.color = Color;

                Handles.DrawLine(X0, X1);
                float3 v = X1 - X0;
                float3 dir;
                float length = Math.NormalizeWithLength(v, out dir);
                float3 perp, perp2;
                Math.CalculatePerpendicularNormalized(dir, out perp, out perp2);
                float3 scale = length * 0.2f;

                Handles.DrawLine(X1, X1 + (perp - dir) * scale);
                Handles.DrawLine(X1, X1 - (perp + dir) * scale);
                Handles.DrawLine(X1, X1 + (perp2 - dir) * scale);
                Handles.DrawLine(X1, X1 - (perp2 + dir) * scale);
#endif
            }
        }

        public void DrawPlane()
        {
            if (!math.all(X0 == X1))
            {
#if UNITY_EDITOR
                Handles.color = Color;

                Handles.DrawLine(X0, X1);
                float3 v = X1 - X0;
                float3 dir;
                float length = Math.NormalizeWithLength(v, out dir);
                float3 perp, perp2;
                Math.CalculatePerpendicularNormalized(dir, out perp, out perp2);
                float3 scale = length * 0.2f;

                Handles.DrawLine(X1, X1 + (perp - dir) * scale);
                Handles.DrawLine(X1, X1 - (perp + dir) * scale);
                Handles.DrawLine(X1, X1 + (perp2 - dir) * scale);
                Handles.DrawLine(X1, X1 - (perp2 + dir) * scale);

                perp *= length;
                perp2 *= length;
                Handles.DrawLine(X0 + perp + perp2, X0 + perp - perp2);
                Handles.DrawLine(X0 + perp - perp2, X0 - perp - perp2);
                Handles.DrawLine(X0 - perp - perp2, X0 - perp + perp2);
                Handles.DrawLine(X0 - perp + perp2, X0 + perp + perp2);
#endif
            }
        }

        public void DrawCircle()
        {
            if (!math.all(X0 == X1))
            {
#if UNITY_EDITOR
                Handles.color = Color;

                float3 v = X1 - X0;
                float3 dir;
                float length = Math.NormalizeWithLength(v, out dir);
                float3 perp, perp2;
                Math.CalculatePerpendicularNormalized(dir, out perp, out perp2);
                float3 scale = length * 0.2f;

                const int res = 16;
                quaternion q = quaternion.AxisAngle(dir, 2.0f * (float)math.PI / res);
                float3 arm = perp * length;
                for (int i = 0; i < res; i++)
                {
                    float3 nextArm = math.mul(q, arm);
                    Handles.DrawLine(X0 + arm, X0 + nextArm);
                    arm = nextArm;
                }
#endif
            }
        }
    }

    public struct Arc
    {
        public float3 Center;
        public float3 Normal;
        public float3 Arm;
        public float Angle;
        public Color Color;

        public void Draw()
        {
#if UNITY_EDITOR
            Handles.color = Color;

            const int res = 16;
            quaternion q = quaternion.AxisAngle(Normal, Angle / res);
            float3 currentArm = Arm;
            Handles.DrawLine(Center, Center + currentArm);
            for (int i = 0; i < res; i++)
            {
                float3 nextArm = math.mul(q, currentArm);
                Handles.DrawLine(Center + currentArm, Center + nextArm);
                currentArm = nextArm;
            }
            Handles.DrawLine(Center, Center + currentArm);
#endif
        }
    }

    public struct Cone
    {
        public float3 Point;
        public float3 Axis;
        public float Angle;
        public Color Color;

        public void Draw()
        {
#if UNITY_EDITOR
            Handles.color = Color;

            float3 dir;
            float scale = Math.NormalizeWithLength(Axis, out dir);

            float3 arm;
            {
                float3 perp1, perp2;
                Math.CalculatePerpendicularNormalized(dir, out perp1, out perp2);
                arm = math.mul(quaternion.AxisAngle(perp1, Angle), dir) * scale;
            }

            const int res = 16;
            quaternion q = quaternion.AxisAngle(dir, 2.0f * (float)math.PI / res);
            for (int i = 0; i < res; i++)
            {
                float3 nextArm = math.mul(q, arm);
                Handles.DrawLine(Point, Point + arm);
                Handles.DrawLine(Point + arm, Point + nextArm);
                arm = nextArm;
            }
#endif
        }
    }

    struct Box
    {
        public float3 Size;
        public float3 Center;
        public quaternion Orientation;
        public Color Color;

        public void Draw()
        {
#if UNITY_EDITOR
            Matrix4x4 orig = Handles.matrix;

            Matrix4x4 mat = Matrix4x4.TRS(Center, Orientation, Vector3.one);
            Handles.matrix = mat;
            Handles.color = Color;
            Handles.DrawWireCube(Vector3.zero, new Vector3(Size.x, Size.y, Size.z));

            Handles.matrix = orig;
#endif
        }
    }

    struct Text
    {
        public float3 X;
        public Color Color;
        public int Length;

        public void Draw(ref NativeStream.Reader reader)
        {
            // Read string data.
            char[] stringBuf = new char[Length];
            for (int i = 0; i < Length; i++)
            {
                stringBuf[i] = reader.Read<char>();
            }

            GUIStyle style = new GUIStyle();
            style.normal.textColor = Color;
#if UNITY_EDITOR
            Handles.Label(X, new string(stringBuf), style);
#endif
        }
    }

    private void Draw()
    {
        for (int i = 0; i < m_DebugStreams.Count; i++)
        {
            NativeStream.Reader reader = m_DebugStreams[i].AsReader();
            for (int j = 0; j != reader.ForEachCount; j++)
            {
                reader.BeginForEachIndex(j);
                while (reader.RemainingItemCount != 0)
                {
                    switch (reader.Read<Type>())
                    {
                        case Type.Point: reader.Read<Point>().Draw(); break;
                        case Type.Line: reader.Read<Line>().Draw(); break;
                        case Type.Arrow: reader.Read<Line>().DrawArrow(); break;
                        case Type.Plane: reader.Read<Line>().DrawPlane(); break;
                        case Type.Circle: reader.Read<Line>().DrawCircle(); break;
                        case Type.Arc: reader.Read<Arc>().Draw(); break;
                        case Type.Cone: reader.Read<Cone>().Draw(); break;
                        case Type.Text: reader.Read<Text>().Draw(ref reader); break;
                        case Type.Box: reader.Read<Box>().Draw(); break;
                        default: return; // unknown type
                    }
                }
                reader.EndForEachIndex();
            }
        }
    }

    private class DrawComponent : MonoBehaviour
    {
        public DebugStream DebugDraw;

        public void OnDrawGizmos()
        {
            if (DebugDraw != null)
            {
                // Make sure all potential debug display jobs are finished
                DebugDraw.m_EndFramePhysicsSystem.GetOutputDependency().Complete();
                DebugDraw.Draw();
            }
        }
    }

    protected override void OnUpdate()
    {
        // Make sure all potential debug display jobs are finished
        m_EndFramePhysicsSystem.GetOutputDependency().Complete();

        // Reset
        for (int i = 0; i < m_DebugStreams.Count; i++)
        {
            m_DebugStreams[i].Dispose();
        }
        m_DebugStreams.Clear();

        // Set up component to draw
        if (m_DrawComponent == null)
        {
#if UNITY_EDITOR
            // For the editor, we create the gameobject and component with HideFlags.DontSave to not save it inside scenes.
            GameObject drawObject = EditorUtility.CreateGameObjectWithHideFlags("DebugStream.DrawComponent", HideFlags.DontSave, typeof(DrawComponent));
            m_DrawComponent = drawObject.GetComponent<DrawComponent>();
#else
            // During builds, we just create the gameobject.
            GameObject drawObject = new GameObject();
            m_DrawComponent = drawObject.AddComponent<DrawComponent>();
            m_DrawComponent.name = "DebugStream.DrawComponent";
#endif
            m_DrawComponent.DebugDraw = this;
        }
    }

    protected override void OnDestroy()
    {
        // Clean up our created DrawComponent. If we're running the systems outside of playmode, we should destroy with DestroyImmediate.
        if (m_DrawComponent != null)
        {
            if (Application.isPlaying)
                Object.Destroy(m_DrawComponent.gameObject);
            else
                Object.DestroyImmediate(m_DrawComponent.gameObject);
            m_DrawComponent = null;
        }

        for (int i = 0; i < m_DebugStreams.Count; i++)
            m_DebugStreams[i].Dispose();
        m_DebugStreams.Clear();
    }
}

(Apologies for necroing my old thread, but it was on the exact same subject)

3 Likes

Thanks for the ping and the changes. I’m pushed these to the repo so you should see them in the next point release.

3 Likes