using NUnit.Framework;
using System;
using UnityEditor;
using System.Runtime.InteropServices;
using Unity.Mathematics;
using Unity.Collections.LowLevel.Unsafe;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEngine.Rendering.RadeonRays;

namespace UnityEngine.Rendering.UnifiedRayTracing.Tests
{
    internal static class MeshUtil
    {
        static internal Mesh CreateSingleTriangleMesh(float2 scaling, float3 translation)
        {
            Mesh mesh = new Mesh();

            Vector3[] vertices = new Vector3[]
            {
                (Vector3)translation + new Vector3(0.0f, 0.0f, 0),
                (Vector3)translation + new Vector3(1.0f * scaling.x, 0.0f, 0),
                (Vector3)translation + new Vector3(0.0f, 1.0f * scaling.y, 0)
            };
            mesh.vertices = vertices;

            Vector3[] normals = new Vector3[]
            {
                -Vector3.forward,
                -Vector3.forward,
                -Vector3.forward
            };
            mesh.normals = normals;

            Vector2[] uv = new Vector2[]
            {
                new Vector2(0, 1),
                new Vector2(1, 1),
                new Vector2(0, 0)
            };
            mesh.uv = uv;

            int[] tris = new int[3]
            {
                0, 2, 1
            };
            mesh.triangles = tris;

            return mesh;
        }

        static internal Mesh CreateQuadMesh()
        {
            Mesh mesh = new Mesh();

            Vector3[] vertices = new Vector3[]
            {
                new Vector3(-0.5f, -0.5f, 0.0f),
                new Vector3(0.5f, -0.5f, 0.0f),
                new Vector3(-0.5f, 0.5f, 0.0f),
                new Vector3(0.5f, 0.5f, 0.0f)
            };
            mesh.vertices = vertices;

            Vector3[] normals = new Vector3[]
            {
                -Vector3.forward,
                -Vector3.forward,
                -Vector3.forward,
                -Vector3.forward
            };
            mesh.normals = normals;

            Vector2[] uv = new Vector2[]
            {
                new Vector2(0, 0),
                new Vector2(1, 0),
                new Vector2(0, 1),
                new Vector2(1, 1)
            };
            mesh.uv = uv;

            int[] tris = new int[6]
            {
                0, 2, 1,
                2, 3, 1
            };
            mesh.triangles = tris;

            return mesh;
        }
    }

    internal class ComputeRayTracingAccelStructTests
    {
        static private void AssertFloat3sAreEqual(float3 expected, float3 actual, float tolerance)
        {
            Assert.AreEqual(expected.x, actual.x, tolerance);
            Assert.AreEqual(expected.y, actual.y, tolerance);
            Assert.AreEqual(expected.z, actual.z, tolerance);
        }

        static private void AssertAABBsAreEqual(float3 expectedMin, float3 expectedMax, float3 actualMin, float3 actualMax, float tolerance)
        {
            AssertFloat3sAreEqual(expectedMin, actualMin, tolerance);
            AssertFloat3sAreEqual(expectedMax, actualMax, tolerance);
        }

        [Test]
        public void Build_TwoInstancesOfASingleTriangleMesh_ShouldGenerateCorrectResult()
        {
            var resources = new RayTracingResources();
            resources.Load();

            using var accelStruct = new ComputeRayTracingAccelStruct(
                new AccelerationStructureOptions() { buildFlags = BuildFlags.PreferFastBuild },
                resources,
                new ReferenceCounter());

            uint instanceCount = 2;

            {
                var mesh = MeshUtil.CreateSingleTriangleMesh(new float2(1.0f, 1.0f), float3.zero);
                var globalTranslation = new float3(1.0f, 1.0f, 0.0f);
                for (uint i = 0; i < instanceCount; i++)
                {
                    var instanceDesc = new MeshInstanceDesc(mesh);
                    instanceDesc.localToWorldMatrix = Matrix4x4.Translate(globalTranslation + new float3(2.0f * i, 0.0f, 0.0f));
                    accelStruct.AddInstance(instanceDesc);
                }

                using var scratchBuffer = RayTracingHelper.CreateScratchBufferForBuild(accelStruct);
                using var cmd = new CommandBuffer();
                accelStruct.Build(cmd, scratchBuffer);
                Graphics.ExecuteCommandBuffer(cmd);
                Object.DestroyImmediate(mesh);
            }

            var tolerance = 0.001f;
            {
                // Verify bottom level BVH.
                uint expectedTotalNodeCount = 1;
                var bottomLevelNodes = new BvhNode[(int)expectedTotalNodeCount + 1]; // plus one for header
                accelStruct.bottomLevelBvhBuffer.GetData(bottomLevelNodes);

                var header = UnsafeUtility.As<BvhNode, BvhHeader>(ref bottomLevelNodes[0]);
                Assert.AreEqual(expectedTotalNodeCount, header.internalNodeCount + header.leafNodeCount);
                Assert.AreEqual(1, header.leafNodeCount);
                Assert.AreEqual(expectedTotalNodeCount, header.internalNodeCount + header.leafNodeCount);
                AssertAABBsAreEqual(new float3(0.0f, 0.0f, 0.0f), new float3(1.0f, 1.0f, 0.0f), header.globalAabbMin, header.globalAabbMax, tolerance);
            }

            {
                // Verify top level BVH.
                uint expectedInternalNodeCount = instanceCount - 1;
                uint expectedLeafNodeCount = instanceCount;
                var topLevelNodes = new BvhNode[(int)expectedInternalNodeCount + 1]; // plus one for header
                accelStruct.topLevelBvhBuffer.GetData(topLevelNodes);

                var header = UnsafeUtility.As<BvhNode, BvhHeader>(ref topLevelNodes[0]);
                Assert.AreEqual(expectedInternalNodeCount, header.internalNodeCount);
                Assert.AreEqual(expectedLeafNodeCount, header.leafNodeCount);

                AssertAABBsAreEqual(new float3(1.0f, 1.0f, 0.0f), new float3(4.0f, 2.0f, 0.0f), header.globalAabbMin, header.globalAabbMax, tolerance);

                var instanceBvhRoot = topLevelNodes[1];
                Assert.AreEqual(0u | (1u << 31), instanceBvhRoot.child0); // MSB is set for leaf node indices
                Assert.AreEqual(1u | (1u << 31), instanceBvhRoot.child1);
                AssertAABBsAreEqual(new float3(1.0f, 1.0f, 0.0f), new float3(2.0f, 2.0f, 0.0f), instanceBvhRoot.aabb0_min, instanceBvhRoot.aabb0_max, tolerance);
                AssertAABBsAreEqual(new float3(3.0f, 1.0f, 0.0f), new float3(4.0f, 2.0f, 0.0f), instanceBvhRoot.aabb1_min, instanceBvhRoot.aabb1_max, tolerance);
            }
        }

        [Test]
        [Ignore("Test too unstable on Yamato (UUM-95662, UUM-67382)")]
        public void AddInstance_MeshWith2GBWorthOfVertices_Throws()
        {
            var resources = new RayTracingResources();
            resources.Load();

            using var accelStruct = new ComputeRayTracingAccelStruct(
                new AccelerationStructureOptions() { buildFlags = BuildFlags.PreferFastBuild },
                resources,
                new ReferenceCounter());

            Random.InitState(1987);

            int vertexCount = 200000001; // 200 millions
            int indexCount = vertexCount; // must be a multiple of 3

            Mesh mesh = new Mesh();
            mesh.indexFormat = IndexFormat.UInt32;

            Vector3[] vertices = new Vector3[vertexCount];
            for (int i = 0; i < vertexCount; ++i)
            {
                vertices[i] = new Vector3(0.0f, 0.0f, Random.Range(0.0f, 1.0f));
            }
            mesh.SetVertices(vertices);

            Vector3[] normals = new Vector3[vertexCount];
            mesh.SetNormals(normals);

            Vector2[] uv = new Vector2[vertexCount];
            mesh.SetUVs(0, uv);

            int[] tris = new int[indexCount];
            for (int i = 0; i < indexCount; ++i)
                tris[i] = i;

            mesh.SetIndices(tris, MeshTopology.Triangles, 0);

            MeshInstanceDesc instanceDesc = new MeshInstanceDesc(mesh);
            Assert.Throws<UnifiedRayTracingException>(() => accelStruct.AddInstance(instanceDesc));

            Assert.That(accelStruct.m_BlasAllocator.allocatedSize, Is.Zero);
            Assert.That(accelStruct.m_BlasLeavesAllocator.allocatedSize, Is.Zero);
        }
    }


    internal class AccelStructTestsBase
    {
        readonly protected RayTracingBackend m_Backend;
        protected RayTracingContext m_Context;
        protected RayTracingResources m_Resources;
        protected IRayTracingAccelStruct m_AccelStruct;
        protected IRayTracingShader m_Shader;

        public AccelStructTestsBase(string backendAsString)
        {
            m_Backend = Enum.Parse<RayTracingBackend>(backendAsString);
        }

        protected void CreateRayTracingResources(string shaderFilename)
        {
            if (!SystemInfo.supportsRayTracing && m_Backend == RayTracingBackend.Hardware)
            {
                Assert.Ignore("Cannot run test on this Graphics API. Hardware RayTracing is not supported");
            }

            if (!SystemInfo.supportsComputeShaders && m_Backend == RayTracingBackend.Compute)
            {
                Assert.Ignore("Cannot run test on this Graphics API. Compute shaders are not supported");
            }

            if (SystemInfo.graphicsDeviceName.Contains("llvmpipe"))
            {
                Assert.Ignore("Cannot run test on this device (Renderer: llvmpipe (LLVM 10.0.0, 128 bits)). Tests are disabled because they fail on some platforms (that do not support 11 SSBOs). Once we do not run Ubuntu 18.04 try removing this");
            }

            m_Resources = new RayTracingResources();
            m_Resources.Load();

            m_Context = new RayTracingContext(m_Backend, m_Resources);
            m_AccelStruct = m_Context.CreateAccelerationStructure(new AccelerationStructureOptions());
            m_Shader = m_Context.LoadRayTracingShader("Packages/com.unity.render-pipelines.core/Tests/Editor/UnifiedRayTracing/" + shaderFilename);
        }

        protected void DisposeRayTracingResources()
        {
            m_AccelStruct?.Dispose();
            m_Context?.Dispose();
        }

        protected THit[] TraceRays<THit>(RayWithFlags[] rays, AnyHitDecision anyHitDecision = AnyHitDecision.Invalid)
        {
            var bufferTarget = GraphicsBuffer.Target.Structured;
            var rayCount = rays.Length;
            using var raysBuffer = new GraphicsBuffer(bufferTarget, rayCount, Marshal.SizeOf<RayWithFlags>());
            raysBuffer.SetData(rays);
            using var hitsBuffer = new GraphicsBuffer(bufferTarget, rayCount, Marshal.SizeOf<THit>());

            using var scratchBuffer = RayTracingHelper.CreateScratchBufferForBuildAndDispatch(m_AccelStruct, m_Shader, (uint)rayCount, 1, 1);

            var cmd = new CommandBuffer();
            m_AccelStruct.Build(cmd, scratchBuffer);

            if (anyHitDecision != AnyHitDecision.Invalid)
                m_Shader.SetIntParam(cmd, Shader.PropertyToID("_AnyHitDecision"), (int)anyHitDecision);

            m_Shader.SetAccelerationStructure(cmd, "_AccelStruct", m_AccelStruct);
            m_Shader.SetBufferParam(cmd, Shader.PropertyToID("_Rays"), raysBuffer);
            m_Shader.SetBufferParam(cmd, Shader.PropertyToID("_Hits"), hitsBuffer);
            m_Shader.Dispatch(cmd, scratchBuffer, (uint)rayCount, 1, 1);
            Graphics.ExecuteCommandBuffer(cmd);

            var hits = new THit[rayCount];
            hitsBuffer.GetData(hits);

            return hits;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct RayWithFlags
        {
            public float3 origin;
            public float minT;
            public float3 direction;
            public float maxT;
            public uint culling;
            public uint instanceMask;
            uint padding;
            uint padding2;

            public RayWithFlags(float3 origin, float3 direction)
            {
                this.origin = origin;
                this.direction = direction;
                minT = 0.0f;
                maxT = float.MaxValue;
                instanceMask = 0xFFFFFFFF;
                culling = 0;
                padding = 0;
                padding2 = 0;
            }
        }

        protected enum AnyHitDecision { Invalid = -1, IgnoreHit = 0, AcceptHit = 1, AcceptHitAndEndSearch = 2 };

        [System.Flags]
        protected enum RayFlags { None = 0, ForceOpaque = 0x01, ForceNonOpaque = 0x02, AcceptFirstHitAndEndSearch = 0x04, SkipClosestHit = 0x08, CullBackFace = 0x10, CullFrontFace = 0x20, CullOpaque = 0x40, CullNonOpaque = 0x80 }
    }


    [TestFixture("Compute")]
    [TestFixture("Hardware")]
    internal class AccelStructTests : AccelStructTestsBase
    {
        public AccelStructTests(string backendAsString) : base(backendAsString)
        {
        }

        [SetUp]
        public void SetUp()
        {
            CreateRayTracingResources("TraceRays.urtshader");
        }

        [TearDown]
        public void TearDown()
        {
            DisposeRayTracingResources();
        }

        [Test]
        public void RayTracePixelsInUnitQuad([Values(1, 10, 100)] int rayResolution, [Values(0, 1, 2, 3)] int buildFlagsAsInteger)
        {
            var buildFlags = (BuildFlags)buildFlagsAsInteger; // We do this ugly but simple cast hack because the BuildFlags type is not public as time of this writing (test methods must be public and so must their argument types).

            // re-create the acceleration structure with suitable options
            m_AccelStruct?.Dispose();
            var options = new AccelerationStructureOptions() { buildFlags = buildFlags };
            m_AccelStruct = m_Context.CreateAccelerationStructure(options);

            Mesh mesh = MeshUtil.CreateQuadMesh();

            var instanceDesc = new MeshInstanceDesc(mesh);
            instanceDesc.localToWorldMatrix = Matrix4x4.identity;
            instanceDesc.localToWorldMatrix.SetTRS(new Vector3(0.5f, 0.5f, 0.0f), Quaternion.identity, new Vector3(1.0f, 1.0f, 1.0f));
            instanceDesc.enableTriangleCulling = false;
            instanceDesc.frontTriangleCounterClockwise = true;
            m_AccelStruct.AddInstance(instanceDesc);

            // trace N*N rays towards the quad and expect to hit it
            int N = rayResolution;
            var rays = new RayWithFlags[N * N];
            int rayI = 0;
            for (int v = 0; v < N; ++v)
            {
                for (int u = 0; u < N; ++u)
                {
                    float2 uv = new float2((float)u, (float)v);
                    uv += 0.5f;
                    uv /= N;
                    float3 origin = new float3(uv.x, uv.y, 1.0f);
                    float3 direction = new float3(0.0f, 0.0f, -1.0f);
                    rays[rayI] = new RayWithFlags(origin, direction);
                    rays[rayI].culling = (uint)RayFlags.None;
                    rayI++;
                }
            }

            var hits = TraceRays<Hit>(rays);
            for (int i = 0; i < hits.Length; ++i)
            {
                Assert.IsTrue(hits[i].Valid(), $"Expected all rays to hit the quad but ray {i} missed.");
            }
        }

        [Test]
        public void FrontOrBackFaceCulling()
        {
            const int instanceCount = 4;
            Mesh mesh = MeshUtil.CreateSingleTriangleMesh(new float2(1.5f, 1.5f), new float3(-0.5f, -0.5f, 0.0f));
            CreateMatchingRaysAndInstanceDescs(instanceCount, mesh, out RayWithFlags[] rays, out MeshInstanceDesc[] instanceDescs);

            var raysDuplicated = new RayWithFlags[instanceCount * 3];
            Array.Copy(rays, 0, raysDuplicated, 0, instanceCount);
            Array.Copy(rays, 0, raysDuplicated, instanceCount, instanceCount);
            Array.Copy(rays, 0, raysDuplicated, 2 * instanceCount, instanceCount);

            for (int i = 0; i < instanceCount; ++i)
            {
                raysDuplicated[i].culling = (uint)RayFlags.None;
                raysDuplicated[i + instanceCount].culling = (uint)RayFlags.CullBackFace;
                raysDuplicated[i + instanceCount * 2].culling = (uint)RayFlags.CullFrontFace;
            }

            instanceDescs[0].enableTriangleCulling = false;
            instanceDescs[0].frontTriangleCounterClockwise = true;

            instanceDescs[1].enableTriangleCulling = false;
            instanceDescs[1].frontTriangleCounterClockwise = false;

            instanceDescs[2].enableTriangleCulling = true;
            instanceDescs[2].frontTriangleCounterClockwise = true;

            instanceDescs[3].enableTriangleCulling = true;
            instanceDescs[3].frontTriangleCounterClockwise = false;

            for (int i = 0; i < instanceCount; ++i)
            {
                m_AccelStruct.AddInstance(instanceDescs[i]);
            }

            var hits = TraceRays<Hit>(raysDuplicated);
            // No culling
            Assert.IsTrue(hits[0].Valid());
            Assert.IsTrue(hits[1].Valid());
            Assert.IsTrue(hits[2].Valid());
            Assert.IsTrue(hits[3].Valid());

            // FrontFace culling
            Assert.IsTrue(hits[4].Valid());
            Assert.IsTrue(hits[5].Valid());
            Assert.IsTrue(hits[6].Valid());
            Assert.IsTrue(!hits[7].Valid());

            // BackFace culling
            Assert.IsTrue(hits[8].Valid());
            Assert.IsTrue(hits[9].Valid());
            Assert.IsTrue(!hits[10].Valid());
            Assert.IsTrue(hits[11].Valid());


        }


        [Test]
        public void InstanceAndRayMask()
        {
            const int instanceCount = 8;
            Mesh mesh = MeshUtil.CreateSingleTriangleMesh(new float2(1.5f, 1.5f), new float3(-0.5f, -0.5f, 0.0f));
            CreateMatchingRaysAndInstanceDescs(instanceCount, mesh, out RayWithFlags[] rays, out MeshInstanceDesc[] instanceDescs);

            var rayAndInstanceMasks = new (uint instanceMask, uint rayMask)[]
            {
                (0, 0),
                (0xFFFFFFFF, 0xFFFFFFFF),
                (0, 0xFFFFFFFF),
                (0xFFFFFFFF, 0),
                (0x0F, 0x01),
                (0x0F, 0xF0),
                (0x90, 0xF0),
                (0xF0, 0x10),
            };

            for (int i = 0; i < instanceCount; ++i)
            {
                instanceDescs[i].mask = rayAndInstanceMasks[i].instanceMask;
                rays[i].instanceMask = rayAndInstanceMasks[i].rayMask;
            }

            for (int i = 0; i < instanceCount; ++i)
            {
                m_AccelStruct.AddInstance(instanceDescs[i]);
            }

            var hits = TraceRays<Hit>(rays);

            for (int i = 0; i < instanceCount; ++i)
            {
                bool rayShouldHit = ((rayAndInstanceMasks[i].instanceMask & rayAndInstanceMasks[i].rayMask) != 0);
                bool rayHit = hits[i].Valid();

                var message = String.Format("Ray {0} hit for InstanceMask: 0x{1:X} & RayMask: 0x{2:X}",
                    rayShouldHit ? "should" : "shouldn't",
                    rayAndInstanceMasks[i].instanceMask,
                    rayAndInstanceMasks[i].rayMask);

                Assert.AreEqual(rayShouldHit, rayHit, message);
            }
        }

        [Test]
        public void AddAndRemoveInstances()
        {
            const int instanceCount = 4;
            Mesh mesh = MeshUtil.CreateSingleTriangleMesh(new float2(1.5f, 1.5f), new float3(-0.5f, -0.5f, 0.0f));
            CreateMatchingRaysAndInstanceDescs(instanceCount, mesh, out RayWithFlags[] rays, out MeshInstanceDesc[] instanceDescs);

            var instanceHandles = new int[instanceCount];
            var expectedVisibleInstances = new bool[instanceCount];

            for (int i = 0; i < instanceCount; ++i)
            {
                instanceHandles[i] = m_AccelStruct.AddInstance(instanceDescs[i]);
                expectedVisibleInstances[i] = true;
            }

            CheckVisibleInstances(rays, expectedVisibleInstances);

            m_AccelStruct.RemoveInstance(instanceHandles[0]); expectedVisibleInstances[0] = false;
            m_AccelStruct.RemoveInstance(instanceHandles[2]); expectedVisibleInstances[2] = false;

            CheckVisibleInstances(rays, expectedVisibleInstances);

            m_AccelStruct.ClearInstances();

            Array.Fill(expectedVisibleInstances, false);

            CheckVisibleInstances(rays, expectedVisibleInstances);

            m_AccelStruct.AddInstance(instanceDescs[3]);
            expectedVisibleInstances[3] = true;

            CheckVisibleInstances(rays, expectedVisibleInstances);
        }

        private void AddTerrainToAccelerationStructure(int heightmapResolution)
        {
            Terrain.CreateTerrainGameObject(new TerrainData());
            #pragma warning disable CS0618 // Type or member is obsolete
            Terrain terrain = GameObject.FindFirstObjectByType<Terrain>();
#pragma warning restore CS0618 // Type or member is obsolete
            Assert.NotNull(terrain);

            // Set terrain texture resolution on terrain data.
            terrain.terrainData.heightmapResolution = heightmapResolution;

            // Convert to mesh.
            AsyncTerrainToMeshRequest request = TerrainToMesh.ConvertAsync(terrain);
            request.WaitForCompletion();

            // Add the terrain to the acceleration structure.
            MeshInstanceDesc instanceDesc = new MeshInstanceDesc(request.GetMesh());
            instanceDesc.localToWorldMatrix = float4x4.identity;
            m_AccelStruct.AddInstance(instanceDesc);
        }

        [Test]
        public void Add_1KTerrain_Works()
        {
            AddTerrainToAccelerationStructure(1025);
        }

        [Test]
        [Ignore("This test is disabled because of the allocation limitation of 2 GB in GraphicsBuffer.")]
        public void Add_4KTerrain_Works()
        {
            AddTerrainToAccelerationStructure(4097);
        }

        void CheckVisibleInstances(RayWithFlags[] rays, bool[] expectedVisibleInstances)
        {
            var hits = TraceRays<Hit>(rays);
            for (int i = 0; i < rays.Length; ++i)
            {
                Assert.AreEqual(expectedVisibleInstances[i], hits[i].Valid(), $"Unexpected state of intersection with instance {i}");
            }
        }

        void CreateMatchingRaysAndInstanceDescs(uint instanceCount, Mesh mesh, out RayWithFlags[] rays, out MeshInstanceDesc[] instanceDescs)
        {
            instanceDescs = new MeshInstanceDesc[instanceCount];
            rays = new RayWithFlags[instanceCount];
            var ray = new RayWithFlags(new float3(0.0f, 0.0f, 1.0f), new float3(0.0f, 0.0f, -1.0f));
            float3 step = new float3(2.0f, 0.0f, 0.0f);

            for (int i = 0; i < instanceCount; ++i)
            {
                instanceDescs[i] = new MeshInstanceDesc(mesh);
                instanceDescs[i].localToWorldMatrix = float4x4.Translate(step * i);

                rays[i] = ray;
                rays[i].origin += step * i;
            }
        }


        [StructLayout(LayoutKind.Sequential)]
        public struct Hit
        {
            public uint instanceID;
            public uint primitiveIndex;
            public float2 uvBarycentrics;
            public float hitDistance;
            public uint isFrontFace;

            public bool Valid() { return instanceID != 0xFFFFFFFF; }
        }

    }

    [TestFixture("Compute")]
    [TestFixture("Hardware")]
    internal class AccelStructTransparencyTests : AccelStructTestsBase
    {
        public AccelStructTransparencyTests(string backendAsString) : base(backendAsString)
        {
        }

        [SetUp]
        public void SetUp()
        {
            CreateRayTracingResources("TraceTransparentRays.urtshader");
        }

        [TearDown]
        public void TearDown()
        {
            DisposeRayTracingResources();
        }


        [Test]
        public void WithTransparentInstances_ClosestAndAnyHitsFuncsAreCalled()
        {
            Mesh mesh = MeshUtil.CreateSingleTriangleMesh(new float2(1.5f, 1.5f), new float3(-0.5f, -0.5f, 0.0f));

            var isOpaque = new bool[] { false, false, true, false, false, false, true, false };
            for (int i = 0; i < isOpaque.Length; ++i)
            {
                var instanceDesc = new MeshInstanceDesc(mesh);
                instanceDesc.localToWorldMatrix = float4x4.Translate(new float3(0.0f, 0.0f, 2.0f) * i);
                instanceDesc.opaqueGeometry = isOpaque[i];
                instanceDesc.instanceID = (uint)i;

                m_AccelStruct.AddInstance(instanceDesc);
            }

            {
                var testCases = new TransparencyTestCase[] {
                    new(RayFlags.None,                                  2, new int[] { 0, 1 }),
                    new(RayFlags.ForceOpaque,                           0, new int[] { }),
                    new(RayFlags.ForceNonOpaque,                       -1, new int[] { 0, 1, 2, 3, 4, 5, 6, 7 }),
                    new(RayFlags.CullOpaque,                           -1, new int[] { 0, 1, 3, 4, 5, 7 }),
                    new(RayFlags.CullNonOpaque,                         2, new int[] { }),
                    new(RayFlags.CullOpaque | RayFlags.SkipClosestHit, -1, new int[] { 0, 1, 3, 4, 5, 7 }),
                    new(RayFlags.AcceptFirstHitAndEndSearch,            2, new int[] { 0, 1 }),
                    new(RayFlags.ForceNonOpaque | RayFlags.AcceptFirstHitAndEndSearch, -1, new int[] { 0, 1, 2, 3, 4, 5, 6, 7 }),
                };

                TestTransparentInstances(testCases, AnyHitDecision.IgnoreHit);
            }

            {
                var testCases = new TransparencyTestCase[] {
                    new(RayFlags.None,                                  0, new int[] { 0 }),
                    new(RayFlags.ForceOpaque,                           0, new int[] { }),
                    new(RayFlags.ForceNonOpaque,                        0, new int[] { 0 }),
                    new(RayFlags.CullOpaque,                            0, new int[] { 0 }),
                    new(RayFlags.CullNonOpaque,                         2, new int[] { }),
                    new(RayFlags.CullOpaque | RayFlags.SkipClosestHit, -1, new int[] { 0 }),
                    new(RayFlags.AcceptFirstHitAndEndSearch,            0, new int[] { 0 }),
                    new(RayFlags.ForceNonOpaque | RayFlags.AcceptFirstHitAndEndSearch, 0, new int[] { 0 }),
                };

                TestTransparentInstances(testCases, AnyHitDecision.AcceptHit);
                TestTransparentInstances(testCases, AnyHitDecision.AcceptHitAndEndSearch);
            }
        }

        void TestTransparentInstances(TransparencyTestCase[] testCases, AnyHitDecision anyHitDecision)
        {
            var expectedResults = new List<TransparentRayResult>();
            var rayWithFlags = new List<RayWithFlags>();

            foreach (var testCase in testCases)
            {
                var ray = new RayWithFlags(new float3(0.0f, 0.0f, -1.0f), new float3(0.0f, 0.0f, 1.0f));
                ray.culling = (uint)testCase.rayFlags;
                rayWithFlags.Add(ray);

                expectedResults.Add(testCase.expectedResult);
            }

            var hits = TraceRays<TransparentRayResult>(rayWithFlags.ToArray(), anyHitDecision);

            for (int i = 0; i < testCases.Length; ++i)
            {
                Assert.AreEqual(testCases[i].expectedResult.closestHit, hits[i].closestHit, $"Unexpected closestHit with RayFlags=[{(RayFlags)rayWithFlags[i].culling}] and AnyHitDecision=[{anyHitDecision}]");
                Assert.AreEqual(testCases[i].expectedResult.anyHits, hits[i].anyHits, $"Unexpected anyHits with RayFlags=[{(RayFlags)rayWithFlags[i].culling}] and AnyHitDecision=[{anyHitDecision}]");
            }
        }



        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        public struct TransparentRayResult
        {
            public uint anyHits;
            public int closestHit;
        }

        struct TransparencyTestCase
        {
            public TransparencyTestCase(RayFlags flags, int expectedClosestHit, int[] expectedAnyHitInvocations)
            {
                rayFlags = flags;

                expectedResult.closestHit = expectedClosestHit;
                expectedResult.anyHits = 0;
                for (int i = 0; i < expectedAnyHitInvocations.Length; ++i)
                    expectedResult.anyHits |= (1u << expectedAnyHitInvocations[i]);
            }

            public RayFlags rayFlags;
            public TransparentRayResult expectedResult;
        }
    }
}