using System.Collections.Generic;
using System.IO;
using System;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.LightTransport;
using UnityEngine.PathTracing.Integration;
using UnityEngine.Rendering;
using UnityEngine.Assertions;
using UnityEngine.PathTracing.Core;
using UnityEngine.PathTracing.Lightmapping;
using UnityEngine.Rendering.UnifiedRayTracing;
using Unity.Collections.LowLevel.Unsafe;
using System.Runtime.InteropServices;
using UnityEditor.PathTracing.Debugging;

namespace UnityEditor.PathTracing.LightBakerBridge
{
    using static BakeLightmapDriver;
    using InstanceHandle = Handle<World.InstanceKey>;

    internal static class WorldHelpers
    {
        static void MultiValueDictAdd<TKey, TValue>(Dictionary<TKey, List<TValue>> dict, TKey key, TValue value)
        {
            List<TValue> values = null;
            if (dict.TryGetValue(key, out values))
            {
                values.Add(value);
            }
            else
            {
                values = new List<TValue> { value };
                dict.Add(key, values);
            }
        }

        internal static void AddContributingInstancesToWorld(World world, in FatInstance[] fatInstances, out Dictionary<int, List<LodInstanceBuildData>> lodInstances, out Dictionary<Int32, List<ContributorLodInfo>> lodgroupToContributorInstances)
        {
            lodInstances = new();
            lodgroupToContributorInstances = new();
            // Add instances to world
            foreach (var fatInstance in fatInstances)
            {
                if (fatInstance.LodIdentifier.IsValid() && !fatInstance.LodIdentifier.IsContributor())
                {
                    WorldHelpers.MultiValueDictAdd(lodInstances, fatInstance.LodIdentifier.LodGroup, new LodInstanceBuildData
                    {
                        LodMask = fatInstance.LodIdentifier.LodMask,
                        Mesh = fatInstance.Mesh,
                        Materials = fatInstance.Materials,
                        Masks = fatInstance.SubMeshMasks,
                        LocalToWorldMatrix = fatInstance.LocalToWorldMatrix,
                        Bounds = fatInstance.Bounds,
                        IsStatic = fatInstance.IsStatic,
                        Filter = fatInstance.Filter
                    });
                    continue;
                }

                var instanceHandle = world.AddInstance(
                    fatInstance.Mesh,
                    fatInstance.Materials,
                    fatInstance.SubMeshMasks,
                    fatInstance.RenderingObjectLayer,
                    fatInstance.LocalToWorldMatrix,
                    fatInstance.Bounds,
                    fatInstance.IsStatic,
                    fatInstance.Filter,
                    fatInstance.EnableEmissiveSampling);

                if (fatInstance.LodIdentifier.IsValid() && fatInstance.LodIdentifier.IsContributor())
                    WorldHelpers.MultiValueDictAdd(lodgroupToContributorInstances, fatInstance.LodIdentifier.LodGroup, new ContributorLodInfo
                    {
                        LodMask = fatInstance.LodIdentifier.LodMask,
                        InstanceHandle = instanceHandle,
                        Masks = fatInstance.SubMeshMasks
                    });
            }
        }
    }

    [InitializeOnLoad]
    internal class SetLightmappingUnifiedBaker
    {
        static SetLightmappingUnifiedBaker()
        {
            try
            {
                var lightmappingType = typeof(UnityEditor.Lightmapping);
                var unifiedBakerProperty = lightmappingType.GetProperty("UnifiedBaker",
                    System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);

                if (unifiedBakerProperty != null && unifiedBakerProperty.CanWrite)
                {
    #if UNIFIED_BAKER
                    unifiedBakerProperty.SetValue(null, true);
    #else
                    unifiedBakerProperty.SetValue(null, false);
    #endif
                }
                else
                {
                    UnityEngine.Debug.LogWarning("Could not find or access UnifiedBaker property on Lightmapping class");
                }
            }
            catch (System.Exception ex)
            {
                UnityEngine.Debug.LogError($"Failed to set UnifiedBaker property via reflection: {ex.Message}");
            }
        }
    }

    internal class LightBakerStrangler
    {
        internal enum Result
        {
            Success,
            InitializeFailure,
            CreateDirectoryFailure,
            CreateLightmapFailure,
            AddResourcesToCacheFailure,
            InitializeExpandedBufferFailure,
            WriteToDiskFailure,
            Cancelled
        };

        // Mirrors native side LightProbeOcclusion struct
        [StructLayout(LayoutKind.Sequential)]
        public unsafe struct LightProbeOcclusion
        {
            public const int MaxLightsPerProbe = 4;

            public fixed int ProbeOcclusionLightIndex[MaxLightsPerProbe];
            public fixed float Occlusion[MaxLightsPerProbe];
            public fixed sbyte OcclusionMaskChannel[MaxLightsPerProbe];

            public int GetProbeOcclusionLightIndex(int index) => ProbeOcclusionLightIndex[index];
            public void SetProbeOcclusionLightIndex(int index, int value) => ProbeOcclusionLightIndex[index] = value;
            public float GetOcclusion(int index) => Occlusion[index];
            public void SetOcclusion(int index, float value) => Occlusion[index] = value;
            public sbyte GetOcclusionMaskChannel(int index) => OcclusionMaskChannel[index];
            public void SetOcclusionMaskChannel(int index, sbyte value) => OcclusionMaskChannel[index] = value;

            public void SetDefaultValues()
            {
                for (int i = 0; i < MaxLightsPerProbe; ++i)
                {
                    ProbeOcclusionLightIndex[i] = -1;
                    Occlusion[i] = 0.0f;
                    OcclusionMaskChannel[i] = -1;
                }
            }
        }

        // File layout matches WriteInterleavedSHArrayToFile.
        private static bool SaveProbesToFileInterleaved(string filename, NativeArray<SphericalHarmonicsL2> shArray)
        {
            Debug.Assert(filename is not null, "Filename is null");

            string path = Path.GetDirectoryName(filename);
            Debug.Assert(path is not null, "path is null");
            var info = Directory.CreateDirectory(path);
            if (info.Exists == false)
                return false;

            // Write the number of probes to file as Int64.
            Int64 arraySize = shArray.Length;
            byte[] probeCountBytes = BitConverter.GetBytes(arraySize);
            List<byte> byteList = new();
            byteList.AddRange(probeCountBytes);

            // Write all probe coefficients, ordered by coefficient.
            const int sphericalHarmonicsL2CoeffCount = 9;
            byte[] floatPaddingBytes = BitConverter.GetBytes(0.0f);
            for (int coefficient = 0; coefficient < sphericalHarmonicsL2CoeffCount; ++coefficient)
            {
                for (int i = 0; i < shArray.Length; i++)
                {
                    SphericalHarmonicsL2 sh = shArray[i];
                    for (int rgb = 0; rgb < 3; rgb++)
                    {
                        float coefficientValue = sh[rgb, coefficient];
                        byte[] floatBytes = BitConverter.GetBytes(coefficientValue);
                        byteList.AddRange(floatBytes);
                    }
                    byteList.AddRange(floatPaddingBytes); // pad to match Vector4 size.
                }
            }
            File.WriteAllBytes(filename, byteList.ToArray());
            return true;
        }

        private static bool SaveArrayToFile<T>(string filename, int count, T[] array)
            where T : unmanaged
        {
            // Create output directory if it doesn't exist already.
            Debug.Assert(filename is not null, "Filename is null");
            string path = Path.GetDirectoryName(filename);
            Debug.Assert(path is not null, "path is null");
            var info = Directory.CreateDirectory(path);
            if (info.Exists == false)
                return false;

            // Prepare output buffer.
            int numElementBytes = array.Length * UnsafeUtility.SizeOf<T>();
            byte[] bytes = new byte[sizeof(Int64) + numElementBytes]; // + sizeof(Int64) for the count.

            // Write the number of elements to file as Int64.
            Int64 arraySize = count;
            byte[] countBytes = BitConverter.GetBytes(arraySize);
            Buffer.BlockCopy(countBytes, 0, bytes, 0, countBytes.Length);

            // Write contents of array to file. This is safe because of the unmanaged constraint on T.
            unsafe
            {
                fixed (T* arrayPtr = array)
                fixed (byte* bytesPtr = bytes)
                {
                    UnsafeUtility.MemCpy(bytesPtr + sizeof(Int64), arrayPtr, numElementBytes);
                }
            }
            File.WriteAllBytes(filename, bytes);
            return true;
        }

        private static HashSet<LightmapRequestOutputType> BitfieldToList(int bitfield)
        {
            var outputTypes = new HashSet<LightmapRequestOutputType>();

            // Loop through the enum values
            foreach (LightmapRequestOutputType channel in Enum.GetValues(typeof(LightmapRequestOutputType)))
            {
                // Check if the bitfield has the current value set
                if ((bitfield & (int) channel) == 0)
                    continue;
                // Add the value to the list
                if (channel != LightmapRequestOutputType.All)
                    outputTypes.Add(channel);
            }
            return outputTypes;
        }

        private static bool LightmapRequestOutputTypeToIntegratedOutputType(LightmapRequestOutputType type, out IntegratedOutputType integratedOutputType)
        {
            integratedOutputType = IntegratedOutputType.AO;
            switch (type)
            {
                case LightmapRequestOutputType.IrradianceIndirect:
                    integratedOutputType = IntegratedOutputType.Indirect;
                    return true;
                case LightmapRequestOutputType.IrradianceDirect:
                    integratedOutputType = IntegratedOutputType.Direct;
                    return true;
                case LightmapRequestOutputType.Occupancy: // Occupancy do not need accumulation
                    return false;
                case LightmapRequestOutputType.Validity:
                    integratedOutputType = IntegratedOutputType.Validity;
                    return true;
                case LightmapRequestOutputType.DirectionalityIndirect:
                    integratedOutputType = IntegratedOutputType.DirectionalityIndirect;
                    return true;
                case LightmapRequestOutputType.DirectionalityDirect:
                    integratedOutputType = IntegratedOutputType.DirectionalityDirect;
                    return true;
                case LightmapRequestOutputType.AmbientOcclusion:
                    integratedOutputType = IntegratedOutputType.AO;
                    return true;
                case LightmapRequestOutputType.Shadowmask:
                    integratedOutputType = IntegratedOutputType.ShadowMask;
                    return true;
                // These types do not need accumulation:
                case LightmapRequestOutputType.Normal:
                case LightmapRequestOutputType.ChartIndex:
                case LightmapRequestOutputType.OverlapPixelIndex:
                case LightmapRequestOutputType.IrradianceEnvironment:
                    return false;
            }
            Debug.Assert(false, $"Error unknown LightmapRequestOutputType {type}.");
            return false;
        }

        // We can ignore the G-Buffer data part of atlassing, we are using stochastic sampling instead.
        internal static LightmapDesc[] PopulateLightmapDescsFromAtlassing(in PVRAtlassingData atlassing, in FatInstance[] fatInstances)
        {
            int atlasCount = atlassing.m_AtlasHashToGBufferHash.Count;
            var lightmapDescs = new LightmapDesc[atlasCount];
            foreach (var atlasIdToAtlasHash in atlassing.m_AtlasIdToAtlasHash)
            {
                int atlasId = atlasIdToAtlasHash.m_AtlasId;

                // Get the array of object ID hashes for the given atlas.
                bool found = atlassing.m_AtlasHashToObjectIDHashes.TryGetValue(
                        atlasIdToAtlasHash.m_AtlasHash, out IndexHash128[] objectIDHashes);
                Debug.Assert(found, $"Couldn't find object ID hashes for atlas {atlasIdToAtlasHash.m_AtlasHash}.");

                // Get the GBuffer data for the given atlas, so we can use it to find instance data like IDs and transforms.
                found = atlassing.m_AtlasHashToGBufferHash.TryGetValue(
                    atlasIdToAtlasHash.m_AtlasHash, out _);
                if (found == false)
                    continue; // Skip atlasses without GBuffers, they are used for SSD objects.

                uint lightmapResolution = (uint)atlassing.m_AtlasSizes[atlasId].width;
                Debug.Assert(lightmapResolution == atlassing.m_AtlasSizes[atlasId].height,
                    "The following code assumes that we always have square lightmaps.");
                uint antiAliasingSampleCount = (uint)((int)atlasIdToAtlasHash.m_BakeParameters.supersamplingMultiplier * (int)atlasIdToAtlasHash.m_BakeParameters.supersamplingMultiplier);
                var lightmapDesc = new LightmapDesc
                {
                    Resolution = lightmapResolution,
                    PushOff = atlasIdToAtlasHash.m_BakeParameters.pushOff
                };
                int instanceCount = objectIDHashes.Length;
                var bakeInstances = new BakeInstance[instanceCount];
                int instanceCounter = 0;
                foreach (var instanceHash in objectIDHashes)
                {
                    // Get the AtlassedInstanceData for the instance.
                    found = atlassing.m_InstanceAtlassingData.TryGetValue(instanceHash,
                        out AtlassedInstanceData atlassedInstanceData);
                    Debug.Assert(found, $"Didn't find AtlassedInstanceData for instance {instanceHash}.");
                    Debug.Assert(atlassedInstanceData.m_AtlasId == atlasId, "Atlas ID mismatch.");

                    // Get the instance from bakeInput and create a Mesh.
                    uint bakeInputInstanceIndex = (uint)instanceHash.Index;
                    ref readonly FatInstance fatInstance = ref fatInstances[bakeInputInstanceIndex];
                    LightmapIntegrationHelpers.ComputeOccupiedTexelRegionForInstance(
                        lightmapResolution, lightmapResolution, atlassedInstanceData.m_LightmapST, fatInstance.UVBoundsSize, fatInstance.UVBoundsOffset,
                        out Vector4 normalizedOccupiedST, out Vector2Int occupiedTexelSize, out Vector2Int occupiedTexelOffset);
                    bakeInstances[instanceCounter].Build(
                        fatInstance.Mesh,
                        normalizedOccupiedST,
                        atlassedInstanceData.m_LightmapST,
                        occupiedTexelSize,
                        occupiedTexelOffset,
                        fatInstance.LocalToWorldMatrix,
                        fatInstance.ReceiveShadows,
                        fatInstance.LodIdentifier,
                        bakeInputInstanceIndex);
                    ++instanceCounter;
                }

                lightmapDesc.BakeInstances = bakeInstances;
                lightmapDescs[atlasId] = lightmapDesc;
            }

            return lightmapDescs;
        }

        private static void MakeOutputFolderPathsFullyQualified(ProbeRequest[] probeRequestsToFixUp, string bakeOutputFolderPath)
        {
            for (int i = 0; i < probeRequestsToFixUp.Length; i++)
            {
                ref ProbeRequest request = ref probeRequestsToFixUp[i];
                request.outputFolderPath = Path.GetFullPath(request.outputFolderPath, bakeOutputFolderPath);
            }
        }

        private static void MakeOutputFolderPathsFullyQualified(LightmapRequest[] requestsToFixUp, string bakeOutputFolderPath)
        {
            for (int i = 0; i < requestsToFixUp.Length; i++)
            {
                ref LightmapRequest request = ref requestsToFixUp[i];
                request.outputFolderPath = Path.GetFullPath(request.outputFolderPath, bakeOutputFolderPath);
            }
        }

        internal static bool Bake(string bakeInputPath, string lightmapRequestsPath, string lightProbeRequestsPath, string bakeOutputFolderPath, BakeProgressState progressState)
        {
            using (new BakeProfilingScope())
            {
                // Read BakeInput and requests from LightBaker
                if (!BakeInputSerialization.Deserialize(bakeInputPath, out BakeInput bakeInput))
                    return false;
                if (!BakeInputSerialization.Deserialize(lightmapRequestsPath, out LightmapRequestData lightmapRequestData))
                    return false;
                if (!BakeInputSerialization.Deserialize(lightProbeRequestsPath, out ProbeRequestData probeRequestData))
                    return false;

                // Change output folder paths from relative to absolute
                MakeOutputFolderPathsFullyQualified(probeRequestData.requests, bakeOutputFolderPath);
                MakeOutputFolderPathsFullyQualified(lightmapRequestData.requests, bakeOutputFolderPath);
                IntegrationSettings integrationSettings = GetIntegrationSettings(bakeInput);

                // Setup ray tracing context
                RayTracingBackend backend = integrationSettings.Backend;
                Debug.Assert(RayTracingContext.IsBackendSupported(backend), $"Backend {backend} is not supported!");
                RayTracingResources rayTracingResources = new RayTracingResources();
                rayTracingResources.Load();
                using var rayTracingContext = new RayTracingContext(backend, rayTracingResources);

                // Setup device context
                using UnityComputeDeviceContext deviceContext = new();
                bool initOk = deviceContext.Initialize();
                Assert.IsTrue(initOk, "Failed to initialize DeviceContext.");

                // Create and init world
                var worldResources = new WorldResourceSet();
                worldResources.LoadFromAssetDatabase();
                using UnityComputeWorld world = new();
                world.Init(rayTracingContext, worldResources);

                using var samplingResources = new UnityEngine.Rendering.Sampling.SamplingResources();
                samplingResources.Load((uint)UnityEngine.Rendering.Sampling.SamplingResources.ResourceType.All);

                // Deserialize BakeInput, inject data into world
                const bool useLegacyBakingBehavior = true;
                const bool autoEstimateLUTRange = true;
                BakeInputToWorldConversion.InjectBakeInputData(world.PathTracingWorld, autoEstimateLUTRange, in bakeInput,
                    out Bounds sceneBounds, out world.Meshes, out FatInstance[] fatInstances, out world.LightHandles,
                    world.TemporaryObjects, UnityComputeWorld.RenderingObjectLayer);

                // Add instances to world
                WorldHelpers.AddContributingInstancesToWorld(world.PathTracingWorld, in fatInstances, out var lodInstances, out var lodgroupToContributorInstances);

                // Build world with extracted data
                const bool emissiveSampling = true;
                world.PathTracingWorld.Build(sceneBounds, deviceContext.GetCommandBuffer(), ref world.ScratchBuffer, samplingResources, emissiveSampling);

                LightmapBakeSettings lightmapBakeSettings = GetLightmapBakeSettings(bakeInput);
                // Build array of lightmap descriptors based on the atlassing data and instances.
                LightmapDesc[] lightmapDescriptors = PopulateLightmapDescsFromAtlassing(in lightmapRequestData.atlassing, in fatInstances);
                SortInstancesByMeshAndResolution(lightmapDescriptors);

                ulong probeWorkSteps = CalculateWorkStepsForProbeRequests(in bakeInput, in probeRequestData);
                ulong lightmapWorkSteps = CalculateWorkStepsForLightmapRequests(in lightmapRequestData, lightmapDescriptors, lightmapBakeSettings);
                progressState.SetTotalWorkSteps(probeWorkSteps + lightmapWorkSteps);

                if (!ExecuteProbeRequests(in bakeInput, in probeRequestData, deviceContext, useLegacyBakingBehavior, world, bakeInput.lightingSettings.maxBounces, progressState, samplingResources))
                    return false;

                if (lightmapRequestData.requests.Length <= 0)
                    return true;

                // Populate resources structure
                LightmapResourceLibrary resources = new();
                resources.Load(world.RayTracingContext);

                if (ExecuteLightmapRequests(in lightmapRequestData, deviceContext, world, in fatInstances, in lodInstances, in lodgroupToContributorInstances, integrationSettings, useLegacyBakingBehavior, resources, progressState, lightmapDescriptors, lightmapBakeSettings, samplingResources) != Result.Success)
                    return false;

                CoreUtils.Destroy(resources.UVFallbackBufferGenerationMaterial);

                return true;
            }
        }

        internal static ulong CalculateWorkStepsForProbeRequests(in BakeInput bakeInput, in ProbeRequestData probeRequestData)
        {
            (uint directSampleCount, uint effectiveIndirectSampleCount) = GetProbeSampleCounts(bakeInput.lightingSettings.probeSampleCounts);
            ulong calculatedWorkSteps = 0;
            foreach (ProbeRequest probeRequest in probeRequestData.requests)
                calculatedWorkSteps += CalculateProbeWorkSteps(probeRequest.count, probeRequest.outputTypeMask, directSampleCount, effectiveIndirectSampleCount, bakeInput.lightingSettings.mixedLightingMode != MixedLightingMode.IndirectOnly,
                    bakeInput.lightingSettings.maxBounces);

            return calculatedWorkSteps;
        }

        private static ulong CalculateProbeWorkSteps(ulong count, ProbeRequestOutputType outputTypeMask, uint directSampleCount, uint effectiveIndirectSampleCount, bool usesProbeOcclusion, uint bounceCount)
        {
            ulong workSteps = 0;
            if (outputTypeMask.HasFlag(ProbeRequestOutputType.RadianceIndirect))
                workSteps += ProbeIntegrator.CalculateWorkSteps((uint)count, effectiveIndirectSampleCount, bounceCount);
            if (outputTypeMask.HasFlag(ProbeRequestOutputType.RadianceDirect))
                workSteps += ProbeIntegrator.CalculateWorkSteps((uint)count, directSampleCount, 0);
            if (outputTypeMask.HasFlag(ProbeRequestOutputType.Validity))
                workSteps += ProbeIntegrator.CalculateWorkSteps((uint)count, effectiveIndirectSampleCount, 0);
            if (outputTypeMask.HasFlag(ProbeRequestOutputType.LightProbeOcclusion) && usesProbeOcclusion)
                workSteps += ProbeIntegrator.CalculateWorkSteps((uint)count, effectiveIndirectSampleCount, 0);

            return workSteps;
        }

        internal static ulong CalculateWorkStepsForLightmapRequests(in LightmapRequestData lightmapRequestData, LightmapDesc[] lightmapDescriptors, LightmapBakeSettings lightmapBakeSettings)
        {
            ulong calculatedWorkSteps = 0;
            foreach (LightmapRequest r in lightmapRequestData.requests)
            {
                ref readonly var request = ref r;
                if (request.lightmapCount == 0)
                    continue;
                for (int lightmapIndex = 0; lightmapIndex < request.lightmapCount; lightmapIndex++)
                {
                    LightmapDesc currentLightmapDesc = lightmapDescriptors[lightmapIndex];
                    Dictionary<IntegratedOutputType, RequestedSubOutput> requestedLightmapTypes = GetRequestedIntegratedOutputTypes(request);
                    foreach (IntegratedOutputType lightmapType in requestedLightmapTypes.Keys)
                    {
                        uint sampleCount = lightmapBakeSettings.GetSampleCount(lightmapType);
                        foreach (BakeInstance bakeInstance in currentLightmapDesc.BakeInstances)
                        {
                            uint instanceWidth = (uint)bakeInstance.TexelSize.x;
                            uint instanceHeight = (uint)bakeInstance.TexelSize.y;

                            calculatedWorkSteps += CalculateIntegratedLightmapWorkSteps(sampleCount, instanceWidth * instanceHeight, lightmapType, lightmapBakeSettings.BounceCount, 1);
                        }
                    }
                    HashSet<LightmapRequestOutputType> requestedNonIntegratedLightmapTypes = GetRequestedNonIntegratedOutputTypes(request);
                    foreach (LightmapRequestOutputType _ in requestedNonIntegratedLightmapTypes)
                        calculatedWorkSteps += CalculateNonIntegratedLightmapWorkSteps(currentLightmapDesc.Resolution * currentLightmapDesc.Resolution);
                }
            }

            return calculatedWorkSteps;
        }

        private static ulong CalculateIntegratedLightmapWorkSteps(uint samplesPerTexel, uint chunkSize, IntegratedOutputType outputType, uint bounces, uint multiplier)
        {
            uint bouncesMultiplier = outputType == IntegratedOutputType.Indirect
                ? 0 == bounces ? 1 : bounces
                : 1;

            return samplesPerTexel*chunkSize*bouncesMultiplier*multiplier;
        }

        private static ulong CalculateNonIntegratedLightmapWorkSteps(uint lightmapResolution) => lightmapResolution;

        private static IntegrationSettings GetIntegrationSettings(in BakeInput bakeInput)
        {
            IntegrationSettings retVal = IntegrationSettings.Default;
            retVal.Backend = RayTracingBackend.Compute;
            // TODO(pema.malling)
            // retVal.Backend =
            //     bakeInput.lightingSettings.useHardwareRayTracing && RayTracingContext.IsBackendSupported(RayTracingBackend.Hardware) ?
            //         RayTracingBackend.Hardware : RayTracingBackend.Compute;

            return retVal;
        }

        internal static LightmapBakeSettings GetLightmapBakeSettings(in BakeInput bakeInput)
        {
            // Lightmap settings
            LightmapBakeSettings lightmapBakeSettings = new()
            {
                AOSampleCount = math.max(0, bakeInput.lightingSettings.lightmapSampleCounts.indirectSampleCount),
                DirectSampleCount = math.max(0, bakeInput.lightingSettings.lightmapSampleCounts.directSampleCount),
                IndirectSampleCount = math.max(0, bakeInput.lightingSettings.lightmapSampleCounts.indirectSampleCount),
                BounceCount = math.max(0, bakeInput.lightingSettings.maxBounces),
                AOMaxDistance = math.max(0.0f, bakeInput.lightingSettings.aoDistance)
            };
            lightmapBakeSettings.ValiditySampleCount = lightmapBakeSettings.IndirectSampleCount;
            return lightmapBakeSettings;
        }

        [Flags]
        private enum RequestedSubOutput
        {
            PrimaryTexture = 1 << 0,
            DirectionalityTexture = 1 << 1
        }

        private struct HashedInstanceIndex : IComparable<HashedInstanceIndex>
        {
            public BakeInstance bakeInstance;
            public int texelCount;
            public int offsetX;
            public int offsetY;
            public int hashCode;

            public int CompareTo(HashedInstanceIndex other)
            {
                    int size = other.texelCount - texelCount;
                    if (size != 0)
                        return size;
                    int xOffset = other.offsetX - offsetX;
                    if (xOffset != 0)
                        return xOffset;
                    int yOffset = offsetY - other.offsetY;
                    if (yOffset != 0)
                        return yOffset;
                    return hashCode - other.hashCode;
            }
        };

        internal static void SortInstancesByMeshAndResolution(LightmapDesc[] lightmapDescriptors)
        {
            int lightmapDescIndex = 0;
            foreach (var lightmapDesc in lightmapDescriptors)
            {
                HashedInstanceIndex[] hashedInstances = new HashedInstanceIndex[lightmapDesc.BakeInstances.Length];
                int instanceIndex = 0;
                foreach (var instance in lightmapDesc.BakeInstances)
                {
                    int hashCode = System.HashCode.Combine(instance.TexelSize.x, instance.TexelSize.y);
                    hashedInstances[instanceIndex] = new();
                    hashedInstances[instanceIndex].bakeInstance = instance;
                    hashedInstances[instanceIndex].texelCount = instance.TexelSize.x * instance.TexelSize.y;
                    hashedInstances[instanceIndex].offsetX = instance.TexelOffset.x;
                    hashedInstances[instanceIndex].offsetY = instance.TexelOffset.y;
                    hashedInstances[instanceIndex].hashCode = hashCode;
                    instanceIndex++;
                }
                Array.Sort(hashedInstances);
                instanceIndex = 0;
                foreach (var hashedInstance in hashedInstances)
                {
                    lightmapDescriptors[lightmapDescIndex].BakeInstances[instanceIndex++] = hashedInstance.bakeInstance;
                }
                lightmapDescIndex++;
            }
        }

        // Gets all meshes used by the specified instances, ordered by the first instance they are used in.
        private static List<Mesh> GetMeshesInInstanceOrder(LightmapDesc[] lightmapDescriptors)
        {
            List<Mesh> sortedMeshes = new List<Mesh>();
            HashSet<Mesh> seenMeshes = new HashSet<Mesh>();
            foreach (var lightmapDesc in lightmapDescriptors)
            {
                foreach (var instance in lightmapDesc.BakeInstances)
                {
                    if (seenMeshes.Add(instance.Mesh))
                    {
                        sortedMeshes.Add(instance.Mesh);
                    }
                }
            }
            return sortedMeshes;
        }

        private static bool AnyLightmapRequestHasOutput(LightmapRequest[] requests, LightmapRequestOutputType type)
        {
            foreach (var req in requests)
            {
                if (req.outputTypeMask.HasFlag(type))
                {
                    return true;
                }
            }

            return false;
        }

        private static bool EnsureInstanceInCacheAndClearExistingEntries(CommandBuffer cmd,
            LightmappingContext lightmappingContext,
            BakeInstance bakeInstance)
        {
            var bakeInstances = new[] { bakeInstance };
            if (!lightmappingContext.ResourceCache.CacheIsHot(bakeInstances))
            {
                // Build the required resources for the current instance
                GraphicsHelpers.Flush(cmd); // need to flush as we are removing resources from the cache which might be in use
                lightmappingContext.ResourceCache.FreeResources(bakeInstances);

                if (!lightmappingContext.ResourceCache.AddResources(
                    bakeInstances, lightmappingContext.World.RayTracingContext, cmd,
                    lightmappingContext.IntegratorContext.UVFallbackBufferBuilder))
                {
                    return false;
                }
                // Flush as there can be a substantial amount of work done in the commandbuffer
                GraphicsHelpers.Flush(cmd);
            }

            return true;
        }

        private static bool GetInstanceUVResources(
            CommandBuffer cmd,
            LightmappingContext lightmappingContext,
            BakeInstance bakeInstance,
            out UVMesh uvMesh,
            out UVAccelerationStructure uvAS,
            out UVFallbackBuffer uvFallbackBuffer)
        {
            uvMesh = default;
            uvAS = default;
            uvFallbackBuffer = default;

            if (!EnsureInstanceInCacheAndClearExistingEntries(cmd, lightmappingContext, bakeInstance))
                return false;

            bool gotResources = lightmappingContext.ResourceCache.GetResources(new[] { bakeInstance },
                out UVMesh[] uvMeshes, out UVAccelerationStructure[] uvAccelerationStructures, out UVFallbackBuffer[] uvFallbackBuffers);
            if (!gotResources)
                return false;

            uvMesh = uvMeshes[0];
            uvAS = uvAccelerationStructures[0];
            uvFallbackBuffer = uvFallbackBuffers[0];

            return true;
        }

        static void GetLodZeroInstanceMasks(bool pathtracingShader, uint[] originalMasks, Span<uint> lodZeroMasks)
        {
            for (int j = 0; j < originalMasks.Length; j++)
            {
                // if we don't need bounce rays, we just make the lod0 invisible (instanceMask=0), so that only the current lod can be traced against
                // otherwise we set bits so that we can select one or the other using the raymask in the shader
                if (pathtracingShader)
                {
                    lodZeroMasks[j] = (uint)InstanceFlags.LOD_ZERO_FOR_LIGHTMAP_INSTANCE;
                    if ((originalMasks[j] & (uint)InstanceFlags.SHADOW_RAY_VIS_MASK) != 0)
                        lodZeroMasks[j] |= (uint)InstanceFlags.LOD_ZERO_FOR_LIGHTMAP_INSTANCE_SHADOW;
                }
                else
                {
                    lodZeroMasks[j] = 0;
                }
            }
        }

        static void GetCurrentLodInstanceMasks(bool pathtracingShader, uint[] originalMasks, Span<uint> currentLodMasks)
        {
            for (int j = 0; j < originalMasks.Length; j++)
            {
                if (pathtracingShader)
                {
                    currentLodMasks[j] = (uint)InstanceFlags.CURRENT_LOD_FOR_LIGHTMAP_INSTANCE;
                    if ((originalMasks[j] & (uint)InstanceFlags.SHADOW_RAY_VIS_MASK) != 0)
                        currentLodMasks[j] |= (uint)InstanceFlags.CURRENT_LOD_FOR_LIGHTMAP_INSTANCE_SHADOW;
                }
                else
                {
                    currentLodMasks[j] = originalMasks[j];
                }
            }
        }

        internal static InstanceHandle[] AddLODInstances(World world, CommandBuffer cmd, LodIdentifier lodIdentifier, in List<LodInstanceBuildData> lodInstancesBuildData, bool pathtracingShader, Dictionary<Int32, List<ContributorLodInfo>> lodgroupToContributorInstances)
        {
            Debug.Assert(lodIdentifier.IsValid() && !lodIdentifier.IsContributor());

            // add current lod instances
            var instanceHandles = new InstanceHandle[lodInstancesBuildData.Count];
            int i = 0;
            uint currentLodLevel = lodIdentifier.MinLodLevelMask();
            foreach (LodInstanceBuildData lodBuildData in lodInstancesBuildData)
            {
                if ((lodBuildData.LodMask & currentLodLevel) == 0)
                    continue;

                Span<uint> currentLodMasks = stackalloc uint[lodBuildData.Masks.Length];
                GetCurrentLodInstanceMasks(pathtracingShader, lodBuildData.Masks, currentLodMasks);

                instanceHandles[i++] = world.AddInstance(lodBuildData.Mesh, lodBuildData.Materials, currentLodMasks, UnityComputeWorld.RenderingObjectLayer, lodBuildData.LocalToWorldMatrix, lodBuildData.Bounds, lodBuildData.IsStatic, lodBuildData.Filter, true);
            }
            Array.Resize(ref instanceHandles, i);

            // update lod0 instance mask
            List<ContributorLodInfo> lodZeroInstances;
            if (lodgroupToContributorInstances.TryGetValue(lodIdentifier.LodGroup, out lodZeroInstances))
            {
                foreach (var lodZeroInstance in lodZeroInstances)
                {
                    if ((lodZeroInstance.LodMask & currentLodLevel) != 0)
                        continue;

                    Span<uint> lodZeroMasks = stackalloc uint[lodZeroInstance.Masks.Length];
                    GetLodZeroInstanceMasks(pathtracingShader, lodZeroInstance.Masks, lodZeroMasks);

                    world.UpdateInstanceMask(lodZeroInstance.InstanceHandle, lodZeroMasks);
                }
            }

            return instanceHandles;
        }

        internal static void RemoveLODInstances(World world, CommandBuffer cmd, LodIdentifier lodIdentifier, Span<InstanceHandle> currentLodInstanceHandles, Dictionary<Int32, List<ContributorLodInfo>> lodgroupToContributorInstances)
        {
            if (!lodIdentifier.IsValid() || lodIdentifier.IsContributor())
                return;

            // Remove current lod instances
            foreach (var instanceHandle in currentLodInstanceHandles)
                world.RemoveInstance(instanceHandle);

            // Restore lod0 instances
            List<ContributorLodInfo> lodZeroInstances;
            if (lodgroupToContributorInstances.TryGetValue(lodIdentifier.LodGroup, out lodZeroInstances))
            {
                foreach (var lodZeroInstance in lodZeroInstances)
                    world.UpdateInstanceMask(lodZeroInstance.InstanceHandle, lodZeroInstance.Masks);
            }
        }

        private static InstanceHandle[] PrepareLodInstances(
            CommandBuffer cmd,
            UnityComputeWorld world,
            BakeInstance bakeInstance,
            Dictionary<int, List<LodInstanceBuildData>> lodInstances,
            Dictionary<Int32, List<ContributorLodInfo>> lodgroupToContributorInstances,
            bool isPathTracingPass)
        {
            if (bakeInstance.LodIdentifier.IsValid() && !bakeInstance.LodIdentifier.IsContributor())
            {
                // AddLODInstances calls _pathTracingWorld.Add/RemoveInstance(...) that doesn't take a cmd buffer arg. Need to flush as cmdbuffer and immediate calls cannot be mixed.
                GraphicsHelpers.Flush(cmd);

                var currentLodInstancesBuildData = lodInstances[bakeInstance.LodIdentifier.LodGroup];
                InstanceHandle[] handles = AddLODInstances(world.PathTracingWorld, cmd, bakeInstance.LodIdentifier, currentLodInstancesBuildData, isPathTracingPass, lodgroupToContributorInstances);
                world.BuildAccelerationStructure(cmd);
                return handles;
            }
            return null;
        }

        private static void ClearLodInstances(
            CommandBuffer cmd,
            UnityComputeWorld world,
            BakeInstance bakeInstance,
            InstanceHandle[] currentLodInstances,
            Dictionary<Int32, List<ContributorLodInfo>> lodgroupToContributorInstances)
        {
            if (bakeInstance.LodIdentifier.IsValid() && !bakeInstance.LodIdentifier.IsContributor())
            {
                // RemoveLODInstances calls _pathTracingWorld.Add/RemoveInstance(...) that doesn't take a cmd buffer arg. Need to flush as cmdbuffer and immediate calls cannot be mixed.
                GraphicsHelpers.Flush(cmd);

                RemoveLODInstances(world.PathTracingWorld, cmd, bakeInstance.LodIdentifier, currentLodInstances, lodgroupToContributorInstances);
                world.BuildAccelerationStructure(cmd);
            }
        }

         private static Result IntegrateLightmapInstance(CommandBuffer cmd,
            int lightmapIndex,
            IntegratedOutputType integratedOutputType,
            BakeInstance bakeInstance,
            in Dictionary<int, List<LodInstanceBuildData>> lodInstances,
            in Dictionary<Int32, List<ContributorLodInfo>> lodgroupToContributorInstances,
            LightmappingContext lightmappingContext,
            UVAccelerationStructure uvAS,
            UVFallbackBuffer uvFallbackBuffer,
            IntegrationSettings integrationSettings,
            LightmapBakeSettings lightmapBakeSettings,
            bool doDirectionality,
            BakeProgressState progressState,
            LightmapIntegrationHelpers.GPUSync gpuSync,
            bool debugDispatches)
        {
            Debug.Assert(!lightmappingContext.ExpandedBufferNeedsUpdating(lightmapBakeSettings.ExpandedBufferSize), "The integration data must be allocated at this point.");

            // Bake the lightmap instances
            int bakeDispatches = 0;
            LightmapBakeState bakeState = new();
            bakeState.Init();
            uint samples = 0;
            bool isInstanceDone = false;
            uint dispatchCount = 0;
            uint instanceWidth = (uint)bakeInstance.TexelSize.x;
            uint instanceHeight = (uint)bakeInstance.TexelSize.y;
            InstanceHandle[] currentLodInstances = null;

            // accumulate the instance
            System.Diagnostics.Stopwatch instanceFlushStopwatch = System.Diagnostics.Stopwatch.StartNew();
            System.Diagnostics.Stopwatch dispatchStopwatch = System.Diagnostics.Stopwatch.StartNew();
            do
            {
                if (debugDispatches)
                {
                    gpuSync.Sync(cmd);
                    dispatchStopwatch.Restart();
                    Console.WriteLine($"Begin pass {bakeDispatches} for lm: {lightmapIndex}, type: {integratedOutputType}, sample count: {bakeState.SampleIndex}, res: [{bakeInstance.TexelSize.x} x {bakeInstance.TexelSize.y}], offset: [{bakeInstance.TexelOffset.x} x {bakeInstance.TexelOffset.y}].");
                }

                bool isInstanceStart = (bakeState.SampleIndex == 0);
                if (isInstanceStart)
                {
                    bool isPathTracingPass = integratedOutputType == IntegratedOutputType.Indirect || integratedOutputType == IntegratedOutputType.DirectionalityIndirect;
                    currentLodInstances = PrepareLodInstances(cmd, lightmappingContext.World, bakeInstance, lodInstances, lodgroupToContributorInstances, isPathTracingPass);
                }

                // Enqueue some baking work in the command buffer.
                cmd.BeginSample($"Bake {integratedOutputType}");
                dispatchCount++;

                uint passSamplesPerTexel = BakeLightmapDriver.AccumulateLightmapInstance(
                    bakeState,
                    bakeInstance,
                    lightmapBakeSettings,
                    integratedOutputType,
                    lightmappingContext,
                    uvAS,
                    uvFallbackBuffer,
                    doDirectionality,
                    out uint chunkSize,
                    out isInstanceDone);

                samples += instanceWidth * instanceHeight * passSamplesPerTexel;
                cmd.EndSample($"Bake {integratedOutputType}");

                if (isInstanceDone)
                {
                    ClearLodInstances(cmd, lightmappingContext.World, bakeInstance, currentLodInstances, lodgroupToContributorInstances);
                }

                if (debugDispatches)
                {
                    gpuSync.Sync(cmd);
                    dispatchStopwatch.Stop();
                    Console.WriteLine($"Finished pass {bakeDispatches}. Elapsed ms: {dispatchStopwatch.ElapsedMilliseconds}");
                }

                bakeDispatches++;

                // Execute the baking work scheduled in BakeLightmaps.
                if (bakeDispatches % integrationSettings.MaxDispatchesPerFlush != 0 && !isInstanceDone)
                    continue;

                // Chip-off work steps based on the chunk done so far
                ulong completedWorkSteps =
                    CalculateIntegratedLightmapWorkSteps(passSamplesPerTexel, chunkSize, integratedOutputType, lightmapBakeSettings.BounceCount, integrationSettings.MaxDispatchesPerFlush);
                gpuSync.RequestAsyncReadback(cmd, _ => progressState.IncrementCompletedWorkSteps(completedWorkSteps));
                GraphicsHelpers.Flush(cmd);

                if (!debugDispatches)
                    continue;

                gpuSync.Sync(cmd);
                instanceFlushStopwatch.Stop();
                Console.WriteLine($"Bake dispatch flush -> Instance: {bakeInstance.Mesh.GetEntityId()}, dispatches {dispatchCount}, Samples: \t{samples}\t. Elapsed ms:\t{instanceFlushStopwatch.ElapsedMilliseconds}");
                samples = 0;
                dispatchCount = 0;
                instanceFlushStopwatch.Restart();

                //LightmapIntegrationHelpers.WriteRenderTexture(cmd, $"Temp/lm{lightmapIndex}_type{lightmapType}_pass{bakeDispatches}.r2d", lightmappingContext.AccumulatedOutput, lightmappingContext.AccumulatedOutput.width, lightmappingContext.AccumulatedOutput.height);
            }
            while (isInstanceDone == false);

            return Result.Success;
        }

        internal static Result ExecuteLightmapRequests(
            in LightmapRequestData lightmapRequestData,
            UnityComputeDeviceContext deviceContext,
            UnityComputeWorld world,
            in FatInstance[] fatInstances,
            in Dictionary<int, List<LodInstanceBuildData>> lodInstances,
            in Dictionary<Int32, List<ContributorLodInfo>> lodgroupToContributorInstances,
            IntegrationSettings integrationSettings,
            bool useLegacyBakingBehavior,
            LightmapResourceLibrary lightmapResourceLib,
            BakeProgressState progressState,
            LightmapDesc[] lightmapDescriptors,
            LightmapBakeSettings lightmapBakeSettings,
            UnityEngine.Rendering.Sampling.SamplingResources samplingResources)
        {
            using var lightmappingContext = new LightmappingContext();

            bool debugDispatches = integrationSettings.DebugDispatches;
            bool doDirectionality = AnyLightmapRequestHasOutput(lightmapRequestData.requests, LightmapRequestOutputType.DirectionalityDirect) || AnyLightmapRequestHasOutput(lightmapRequestData.requests, LightmapRequestOutputType.DirectionalityIndirect);

            // Find the max index and vertex count in any mesh, so we can pre-allocate various buffers based on it.
            uint maxIndexCount = 1;
            uint maxVertexCount = 1;
            for (int meshIdx = 0; meshIdx < world.Meshes.Length; ++meshIdx)
            {
                maxIndexCount = Math.Max(maxIndexCount, world.Meshes[meshIdx].GetTotalIndexCount());
                maxVertexCount = Math.Max(maxVertexCount, (uint)world.Meshes[meshIdx].vertexCount);
            }

            (int width, int height)[] atlasSizes = lightmapRequestData.atlassing.m_AtlasSizes;
            int initialLightmapResolution = atlasSizes.Length > 0 ? atlasSizes[0].width : 1024;
            if (!lightmappingContext.Initialize(deviceContext, initialLightmapResolution, initialLightmapResolution, world, maxIndexCount, maxVertexCount, lightmapResourceLib))
                return Result.InitializeFailure;

            lightmappingContext.IntegratorContext.Initialize(samplingResources, lightmapResourceLib, !useLegacyBakingBehavior);

            // Chart identification happens in multithreaded fashion on the CPU. We start it immediately so it can run in tandem with other work.
            bool usesChartIdentification = AnyLightmapRequestHasOutput(lightmapRequestData.requests, LightmapRequestOutputType.ChartIndex) ||
                                           AnyLightmapRequestHasOutput(lightmapRequestData.requests, LightmapRequestOutputType.OverlapPixelIndex);
            using ParallelChartIdentification chartIdentification = usesChartIdentification ? new ParallelChartIdentification(GetMeshesInInstanceOrder(lightmapDescriptors)) : null;
            if (usesChartIdentification)
                chartIdentification.Start();

            if (debugDispatches)
            {
                for (int i = 0; i < lightmapDescriptors.Length; ++i)
                {
                    Console.WriteLine($"Desc:");
                    int instanceIndex = 0;
                    foreach (var bakeInstance in lightmapDescriptors[i].BakeInstances)
                        Console.WriteLine($"  Instance[{instanceIndex++}]: {bakeInstance.Mesh.GetEntityId()}, res: [{bakeInstance.TexelSize.x} x {bakeInstance.TexelSize.y}], offset: [{bakeInstance.TexelOffset.x} x {bakeInstance.TexelOffset.y}].");
                }
            }

            CommandBuffer cmd = lightmappingContext.GetCommandBuffer();
            using LightmapIntegrationHelpers.GPUSync gpuSync = new(); // used for sync points in debug mode
            gpuSync.Create();

            // process requests
            for (int requestIndex = 0; requestIndex < lightmapRequestData.requests.Length; requestIndex++)
            {
                if (progressState.WasCancelled())
                    return Result.Cancelled;

                ref readonly var request = ref lightmapRequestData.requests[requestIndex];
                if (request.lightmapCount == 0)
                    continue;

                var createDirResult = Directory.CreateDirectory(request.outputFolderPath);
                if (createDirResult.Exists == false)
                    return Result.CreateDirectoryFailure;

                Dictionary<IntegratedOutputType, RequestedSubOutput> integratedRequestOutputs = GetRequestedIntegratedOutputTypes(request);
                bool needsNormalsForDirectionality = request.outputTypeMask.HasFlag(LightmapRequestOutputType.DirectionalityIndirect) || request.outputTypeMask.HasFlag(LightmapRequestOutputType.DirectionalityDirect);
                bool writeNormals = request.outputTypeMask.HasFlag(LightmapRequestOutputType.Normal);

                // Request related bake settings
                for (int lightmapIndex = 0; lightmapIndex < request.lightmapCount; lightmapIndex++)
                {
                    if (progressState.WasCancelled())
                        return Result.Cancelled;

                    LightmapDesc currentLightmapDesc = lightmapDescriptors[lightmapIndex];
                    lightmapBakeSettings.PushOff = currentLightmapDesc.PushOff;
                    int resolution = (int)currentLightmapDesc.Resolution;
                    UInt64 lightmapSize = (UInt64)resolution * (UInt64)resolution;
                    lightmapBakeSettings.ExpandedBufferSize = LightmapRequest.TilingModeToLightmapExpandedBufferSize(request.tilingMode);

                    // allocate expanded buffer
                    if (lightmappingContext.ExpandedBufferNeedsUpdating(lightmapBakeSettings.ExpandedBufferSize))
                    {
                        GraphicsHelpers.Flush(cmd); // need to flush as we are removing resources from the cache which might be in use
                        if (!lightmappingContext.InitializeExpandedBuffer(lightmapBakeSettings.ExpandedBufferSize))
                            return Result.InitializeExpandedBufferFailure;
                        // The scratch buffer is used for tracing rays in the lightmap integrators it needs to be sufficiently large to ray trace an expanded buffer
                        uint scratchBufferSize = (uint)lightmapBakeSettings.ExpandedBufferSize;
                        lightmappingContext.InitializeTraceScratchBuffer(scratchBufferSize, 1, 1);
                        if (debugDispatches)
                            Console.WriteLine($"Built expanded buffer for {lightmapBakeSettings.ExpandedBufferSize} samples, lm w: {lightmappingContext.AccumulatedOutput.width}, lm h: {lightmappingContext.AccumulatedOutput.height}].");
                    }

                    ref RenderTexture accumulatedOutput = ref lightmappingContext.AccumulatedOutput;
                    if ((accumulatedOutput.width != resolution) || (accumulatedOutput.height != resolution))
                        if (!lightmappingContext.SetOutputResolution(resolution, resolution))
                            return Result.InitializeFailure;

                    // Bake normals output
                    RenderTexture normalBuffer = null;
                    if (needsNormalsForDirectionality || writeNormals)
                    {
                        lightmappingContext.ClearOutputs();

                        IRayTracingShader normalShader = lightmapResourceLib.NormalAccumulationShader;
                        GraphicsBuffer compactedGBufferLength = lightmappingContext.CompactedGBufferLength;
                        GraphicsBuffer indirectDispatchBuffer = lightmappingContext.IndirectDispatchBuffer;
                        GraphicsBuffer indirectRayTracingDispatchBuffer = lightmappingContext.IndirectDispatchRayTracingBuffer;

                        uint maxChunkSize = (uint)lightmappingContext.ExpandedOutput.count;
                        var expansionShaders = lightmapResourceLib.ExpansionHelpers;
                        var compactionKernel = expansionShaders.FindKernel("CompactGBuffer");
                        var populateCopyDispatchKernel = expansionShaders.FindKernel("PopulateCopyDispatch");
                        var copyToLightmapKernel = expansionShaders.FindKernel("AdditivelyCopyCompactedTo2D");
                        var populateNormalShaderDispatchKernel = expansionShaders.FindKernel("PopulateAccumulationDispatch");

                        expansionShaders.GetKernelThreadGroupSizes(copyToLightmapKernel, out uint copyThreadGroupSizeX, out uint copyThreadGroupSizeY, out uint copyThreadGroupSizeZ);
                        Debug.Assert(copyThreadGroupSizeY == 1 && copyThreadGroupSizeZ == 1);

                        foreach (var bakeInstance in currentLightmapDesc.BakeInstances)
                        {
                            if (progressState.WasCancelled())
                                return Result.Cancelled;

                            if (!GetInstanceUVResources(cmd, lightmappingContext, bakeInstance, out _, out var uvAS, out var uvFallbackBuffer))
                                return Result.AddResourcesToCacheFailure;

                            InstanceHandle[] currentLodInstances = PrepareLodInstances(cmd, lightmappingContext.World, bakeInstance, lodInstances, lodgroupToContributorInstances, false);
                            var instanceGeometryIndex = lightmappingContext.World.PathTracingWorld.GetAccelerationStructure().GeometryPool.GetInstanceGeometryIndex(bakeInstance.Mesh);

                            uint instanceWidth = (uint)bakeInstance.TexelSize.x;
                            uint instanceHeight = (uint)bakeInstance.TexelSize.y;
                            UInt64 instanceSize = (UInt64)instanceWidth * (UInt64)instanceHeight;
                            UInt64 chunkTexelOffset = 0;
                            do
                            {
                                uint2 chunkOffset = new uint2((uint)(chunkTexelOffset % (UInt64)instanceWidth), (uint)(chunkTexelOffset / (UInt64)instanceWidth));
                                uint remainingTexels = instanceWidth - chunkOffset.x + ((instanceHeight - 1) - chunkOffset.y) * instanceWidth;
                                uint chunkSize = math.min(maxChunkSize, remainingTexels);
                                Debug.Assert(remainingTexels > 0);

                                cmd.BeginSample($"Bake Normals");
                                ExpansionHelpers.CompactGBuffer(
                                    cmd,
                                    expansionShaders,
                                    compactionKernel,
                                    instanceWidth,
                                    chunkSize,
                                    chunkOffset,
                                    uvFallbackBuffer,
                                    compactedGBufferLength,
                                    lightmappingContext.CompactedTexelIndices);

                                // build a gBuffer for the chunk
                                ExpansionHelpers.GenerateGBuffer(
                                    cmd,
                                    lightmappingContext.IntegratorContext.GBufferShader,
                                    lightmappingContext.GBuffer,
                                    lightmappingContext.TraceScratchBuffer,
                                    lightmappingContext.IntegratorContext.SamplingResources,
                                    uvAS,
                                    uvFallbackBuffer,
                                    compactedGBufferLength,
                                    lightmappingContext.CompactedTexelIndices,
                                    bakeInstance.TexelOffset,
                                    chunkOffset,
                                    chunkSize,
                                    expandedSampleWidth: 1,
                                    passSampleCount: 1,
                                    sampleOffset: 0,
                                    AntiAliasingType.SuperSampling,
                                    superSampleWidth: 1);
                                chunkTexelOffset += chunkSize;

                                // now perform the normal generation for the chunk
                                // geometry pool bindings
                                Util.BindAccelerationStructure(cmd, normalShader, world.PathTracingWorld.GetAccelerationStructure());

                                var requiredSizeInBytes = normalShader.GetTraceScratchBufferRequiredSizeInBytes((uint)chunkSize, 1, 1);
                                if (requiredSizeInBytes > 0)
                                {
                                    var actualScratchBufferSize = (ulong)(lightmappingContext.TraceScratchBuffer.count * lightmappingContext.TraceScratchBuffer.stride);
                                    Debug.Assert(lightmappingContext.TraceScratchBuffer.stride == sizeof(uint));
                                    Debug.Assert(requiredSizeInBytes <= actualScratchBufferSize);
                                }

                                normalShader.SetMatrixParam(cmd, LightmapIntegratorShaderIDs.ShaderLocalToWorld, bakeInstance.LocalToWorldMatrix);
                                normalShader.SetMatrixParam(cmd, LightmapIntegratorShaderIDs.ShaderLocalToWorldNormals, bakeInstance.LocalToWorldMatrixNormals);
                                normalShader.SetIntParam(cmd, LightmapIntegratorShaderIDs.InstanceGeometryIndex, instanceGeometryIndex);
                                normalShader.SetIntParam(cmd, LightmapIntegratorShaderIDs.InstanceWidth, (int)instanceWidth);

                                normalShader.SetBufferParam(cmd, LightmapIntegratorShaderIDs.GBuffer, lightmappingContext.GBuffer);
                                normalShader.SetBufferParam(cmd, LightmapIntegratorShaderIDs.CompactedGBuffer, lightmappingContext.CompactedTexelIndices);
                                normalShader.SetBufferParam(cmd, LightmapIntegratorShaderIDs.ExpandedOutput, lightmappingContext.ExpandedOutput);
                                normalShader.SetIntParam(cmd, LightmapIntegratorShaderIDs.ExpandedTexelSampleWidth, 1);
                                normalShader.SetIntParam(cmd, LightmapIntegratorShaderIDs.ChunkOffsetX, (int)chunkOffset.x);
                                normalShader.SetIntParam(cmd, LightmapIntegratorShaderIDs.ChunkOffsetY, (int)chunkOffset.y);

                                ExpansionHelpers.PopulateAccumulationIndirectDispatch(cmd, normalShader, expansionShaders, populateNormalShaderDispatchKernel, 1, compactedGBufferLength, indirectRayTracingDispatchBuffer);
                                normalShader.SetIntParam(cmd, LightmapIntegratorShaderIDs.SampleOffset, 0);
                                normalShader.SetIntParam(cmd, LightmapIntegratorShaderIDs.MaxLocalSampleCount, 1);
                                cmd.BeginSample("Normal Generation");
                                normalShader.Dispatch(cmd, lightmappingContext.TraceScratchBuffer, indirectRayTracingDispatchBuffer);
                                cmd.EndSample("Normal Generation");

                                // copy back to the output
                                ExpansionHelpers.PopulateCopyToLightmapIndirectDispatch(cmd, expansionShaders, populateCopyDispatchKernel, copyThreadGroupSizeX, compactedGBufferLength, indirectDispatchBuffer);
                                ExpansionHelpers.CopyToLightmap(cmd, expansionShaders, copyToLightmapKernel, 1, (int)instanceWidth, bakeInstance.TexelOffset, chunkOffset, compactedGBufferLength, lightmappingContext.CompactedTexelIndices, lightmappingContext.ExpandedOutput, indirectDispatchBuffer, lightmappingContext.AccumulatedOutput);
                                cmd.EndSample("Bake Normals");
                            }
                            while (chunkTexelOffset < instanceSize);

                            ClearLodInstances(cmd, lightmappingContext.World, bakeInstance, currentLodInstances, lodgroupToContributorInstances);
                        }

                        if (writeNormals)
                        {
                            if (LightmapIntegrationHelpers.WriteLightmap(cmd, accumulatedOutput, "normal", lightmapIndex, request.outputFolderPath) == false)
                                return Result.WriteToDiskFailure;
                        }

                        if (needsNormalsForDirectionality)
                        {
                            // Hang on to normal buffer for normalization of directionality
                            normalBuffer = LightmappingContext.MakeRenderTexture(accumulatedOutput.width, accumulatedOutput.height, "TempNormalBuffer");
                            cmd.CopyTexture(accumulatedOutput, normalBuffer);
                        }

                        ulong workSteps = CalculateNonIntegratedLightmapWorkSteps((uint) lightmapSize);
                        progressState.IncrementCompletedWorkSteps(workSteps);
                    }

                    // Bake occupancy output
                    if (request.outputTypeMask.HasFlag(LightmapRequestOutputType.Occupancy))
                    {
                        lightmappingContext.ClearOutputs();

                        foreach (var bakeInstance in currentLightmapDesc.BakeInstances)
                        {
                            if (!GetInstanceUVResources(cmd, lightmappingContext, bakeInstance, out _, out _, out var uvFallbackBuffer))
                                return Result.AddResourcesToCacheFailure;

                            lightmappingContext.IntegratorContext.LightmapOccupancyIntegrator.Accumulate(
                                cmd,
                                bakeInstance.TexelSize,
                                bakeInstance.TexelOffset,
                                uvFallbackBuffer,
                                accumulatedOutput);
                        }

                        if (LightmapIntegrationHelpers.WriteLightmap(cmd, accumulatedOutput, "occupancy", lightmapIndex, request.outputFolderPath) == false)
                            return Result.WriteToDiskFailure;

                        ulong workSteps = CalculateNonIntegratedLightmapWorkSteps((uint) lightmapSize);
                        progressState.IncrementCompletedWorkSteps(workSteps);
                    }

                    // Bake chart index output
                    if (request.outputTypeMask.HasFlag(LightmapRequestOutputType.ChartIndex))
                    {
                        var rasterizer = lightmappingContext.ChartRasterizer;

                        cmd.SetRenderTarget(lightmappingContext.AccumulatedOutput);
                        cmd.ClearRenderTarget(false, true, new Color(-1, -1, -1, -1)); // -1 = No chart

                        // Keep track of how many charts we have rasterized, so we can ensure uniqueness across the entire lightmap.
                        uint chartIndexOffset = 0;

                        foreach (var bakeInstance in currentLightmapDesc.BakeInstances)
                        {
                            if (!GetInstanceUVResources(cmd, lightmappingContext, bakeInstance, out var uvMesh, out _, out _))
                                return Result.AddResourcesToCacheFailure;

                            var vertexToChartId = chartIdentification.CompleteAndGetResult(bakeInstance.Mesh);
                            cmd.SetBufferData(lightmappingContext.ChartRasterizerBuffers.vertexToChartID, vertexToChartId.VertexChartIndices);

                            ChartRasterizer.PrepareRasterizeSoftware(cmd, uvMesh.Mesh,
                                lightmappingContext.ChartRasterizerBuffers.vertex, lightmappingContext.ChartRasterizerBuffers.vertexToOriginalVertex);
                            rasterizer.RasterizeSoftware(cmd, lightmappingContext.ChartRasterizerBuffers.vertex,
                                lightmappingContext.ChartRasterizerBuffers.vertexToOriginalVertex, lightmappingContext.ChartRasterizerBuffers.vertexToChartID,
                                uvMesh.Mesh.GetTotalIndexCount(), bakeInstance.NormalizedOccupiedST, chartIndexOffset, lightmappingContext.AccumulatedOutput);

                            chartIndexOffset += vertexToChartId.ChartCount;
                        }

                        // Readback texture
                        Vector4[] pixels = null;
                        cmd.RequestAsyncReadback(lightmappingContext.AccumulatedOutput, 0, request => pixels = request.GetData<Vector4>().ToArray());
                        cmd.WaitAllAsyncReadbackRequests();

                        GraphicsHelpers.Flush(cmd);

                        // Write it to disk
                        int[] chartIndices = new int[pixels.Length];
                        for (int i = 0; i < pixels.Length; i++)
                            chartIndices[i] = (int)pixels[i].x;
                        if (!SaveArrayToFile(request.outputFolderPath + $"/chartIndex{lightmapIndex}.int", chartIndices.Length, chartIndices))
                            return Result.WriteToDiskFailure;

                        ulong workSteps = CalculateNonIntegratedLightmapWorkSteps((uint)lightmapSize);
                        progressState.IncrementCompletedWorkSteps(workSteps);
                    }

                    // Compute pixel overlap indices
                    if (request.outputTypeMask.HasFlag(LightmapRequestOutputType.OverlapPixelIndex))
                    {
                        using var overlapDetection = new UVOverlapDetection();
                        overlapDetection.Initialize(UVOverlapDetection.LoadShader(), (uint)resolution, maxIndexCount, (uint)fatInstances.Length);

                        // Mark overlaps in every instance. Keep track of how many charts we have processed,
                        // so we can ensure uniqueness across the entire lightmap.
                        uint chartIndexOffset = 0;
                        foreach (var bakeInstance in currentLightmapDesc.BakeInstances)
                        {
                            if (!GetInstanceUVResources(cmd, lightmappingContext, bakeInstance, out var uvMesh, out _, out _))
                                return Result.AddResourcesToCacheFailure;

                            var vertexToChartId = chartIdentification.CompleteAndGetResult(bakeInstance.Mesh);

                            cmd.BeginSample("UV Overlap Detection");
                            overlapDetection.MarkOverlapsInInstance(
                                cmd,
                                uvMesh.Mesh,
                                vertexToChartId.VertexChartIndicesIgnoringNormals,
                                bakeInstance.NormalizedOccupiedST,
                                bakeInstance.InstanceIndex,
                                chartIndexOffset);
                            cmd.EndSample("UV Overlap Detection");

                            chartIndexOffset += vertexToChartId.ChartCount;
                        }

                        // Readback result, write to disk
                        overlapDetection.CompactAndReadbackOverlaps(
                            cmd,
                            out var uniqueOverlapPixelIndices,
                            out var uniqueOverlapInstanceIndices);

                        SaveArrayToFile(request.outputFolderPath + $"/overlapPixelIndex{lightmapIndex}.uint", uniqueOverlapPixelIndices.Length, uniqueOverlapPixelIndices);
                        SaveArrayToFile(request.outputFolderPath + $"/uvOverlapInstanceIds{lightmapIndex}.size_t", uniqueOverlapInstanceIndices.Length, uniqueOverlapInstanceIndices);

                        ulong workSteps = CalculateNonIntegratedLightmapWorkSteps((uint)lightmapSize);
                        progressState.IncrementCompletedWorkSteps(workSteps);
                    }

                    // Bake all the integrator components requests
                    foreach (var integratedRequestOutputType in integratedRequestOutputs)
                    {
                        if (progressState.WasCancelled())
                            return Result.Cancelled;

                        lightmappingContext.ClearOutputs();

                        IntegratedOutputType integratedOutputType = integratedRequestOutputType.Key;
                        foreach (var bakeInstance in currentLightmapDesc.BakeInstances)
                        {
                            if (!GetInstanceUVResources(cmd, lightmappingContext, bakeInstance, out _, out var uvAS, out var uvFallbackBuffer))
                                return Result.AddResourcesToCacheFailure;

                            var result = IntegrateLightmapInstance(
                                cmd,
                                lightmapIndex,
                                integratedRequestOutputType.Key,
                                bakeInstance,
                                lodInstances,
                                lodgroupToContributorInstances,
                                lightmappingContext,
                                uvAS,
                                uvFallbackBuffer,
                                integrationSettings,
                                lightmapBakeSettings,
                                doDirectionality,
                                progressState,
                                gpuSync,
                                debugDispatches
                            );

                            if (result != Result.Success)
                                return result;
                        }

                        // Copy out the results
                        bool hasDirectionality = integratedRequestOutputType.Value.HasFlag(RequestedSubOutput.DirectionalityTexture);
                        if (hasDirectionality)
                        {
                            Debug.Assert(normalBuffer != null, "Normal buffer must be set when directionality is requested.");
                            Debug.Assert(needsNormalsForDirectionality);

                            // This uses the accumulatedOutput to get the sample count, so directionality must be normalized first since the sample count will be normalized when the lightmap is normalized
                            // Currently the directional output from LightBaker is not normalized - so for now we don't do it here either. JIRA: https://jira.unity3d.com/browse/LIGHT-1814
                            switch (integratedOutputType)
                            {
                                case IntegratedOutputType.Direct:
                                    lightmappingContext.IntegratorContext.LightmapDirectIntegrator.NormalizeDirectional(cmd, lightmappingContext.AccumulatedDirectionalOutput, accumulatedOutput, normalBuffer);
                                    break;
                                case IntegratedOutputType.Indirect:
                                    lightmappingContext.IntegratorContext.LightmapIndirectIntegrator.NormalizeDirectional(cmd, lightmappingContext.AccumulatedDirectionalOutput, accumulatedOutput, normalBuffer);
                                    break;
                            }
                        }

                        switch (integratedOutputType)
                        {
                            case IntegratedOutputType.AO:
                                lightmappingContext.IntegratorContext.LightmapAOIntegrator.Normalize(cmd, accumulatedOutput);
                                break;
                            case IntegratedOutputType.Direct:
                                lightmappingContext.IntegratorContext.LightmapDirectIntegrator.Normalize(cmd, accumulatedOutput);
                                break;
                            case IntegratedOutputType.Indirect:
                                lightmappingContext.IntegratorContext.LightmapIndirectIntegrator.Normalize(cmd, accumulatedOutput);
                                break;
                            case IntegratedOutputType.Validity:
                                lightmappingContext.IntegratorContext.LightmapValidityIntegrator.Normalize(cmd, accumulatedOutput);
                                break;
                            case IntegratedOutputType.ShadowMask:
                                lightmappingContext.IntegratorContext.LightmapShadowMaskIntegrator.Normalize(cmd, accumulatedOutput, lightmappingContext.AccumulatedDirectionalOutput);
                                break;
                        }

                        // Make sure all the work is done before writing to disk
                        GraphicsHelpers.Flush(cmd);

                        if (debugDispatches)
                        {
                            gpuSync.Sync(cmd);
                            Console.WriteLine($"Write {integratedOutputType} lightmap {lightmapIndex} to disk.");
                            //LightmapIntegrationHelpers.WriteRenderTexture(cmd, $"Temp/lm{lightmapIndex}_type{lightmapType}_final.r2dr", lightmappingContext.AccumulatedOutput, lightmappingContext.AccumulatedOutput.width, lightmappingContext.AccumulatedOutput.height);
                        }

                        // Write lightmap to disk. TODO: kick off a C# job so the compression and file IO happens in a different thread LIGHT-1772
                        bool outputPrimaryTexture = integratedRequestOutputType.Value.HasFlag(RequestedSubOutput.PrimaryTexture);
                        // We explicitly take into account whether to write for directionality, no directionality or for both.
                        if (outputPrimaryTexture)
                            if (LightmapIntegrationHelpers.WriteLightmap(cmd, accumulatedOutput, integratedOutputType, lightmapIndex, request.outputFolderPath) == false)
                                return Result.WriteToDiskFailure;

                        if (!integratedRequestOutputType.Value.HasFlag(RequestedSubOutput.DirectionalityTexture))
                            continue;

                        // Write 'direct/indirect directionality' if requested - it is baked in the same pass as 'direct/indirect'.
                        IntegratedOutputType directionalityLightmapType =
                            integratedOutputType == IntegratedOutputType.Direct ? IntegratedOutputType.DirectionalityDirect : IntegratedOutputType.DirectionalityIndirect;
                        if (LightmapIntegrationHelpers.WriteLightmap(cmd, lightmappingContext.AccumulatedDirectionalOutput, directionalityLightmapType, lightmapIndex, request.outputFolderPath) == false)
                            return Result.WriteToDiskFailure;
                    }

                    // Get rid of the normal output
                    if (normalBuffer is not null)
                        normalBuffer.Release();
                    CoreUtils.Destroy(normalBuffer);

                    // Write black outputs for the lightmap components we didn't bake. LightBaker does this too.
                    Texture2D blackOutput = new Texture2D(resolution, resolution, TextureFormat.RGBAFloat, false);
                    blackOutput.name = "BlackOutput";
                    blackOutput.hideFlags = HideFlags.HideAndDontSave;
                    blackOutput.SetPixels(new Color[resolution * resolution]);
                    blackOutput.Apply();
                    byte[] compressedBlack = blackOutput.EncodeToR2D();
                    try
                    {
                        // Environment is part of indirect irradiance.
                        if (request.outputTypeMask.HasFlag(LightmapRequestOutputType.IrradianceEnvironment))
                            File.WriteAllBytes(request.outputFolderPath + $"/irradianceEnvironment{lightmapIndex}.r2d", compressedBlack);

                        // occupiedTexels is needed for analytics, don't fail the bake if we cannot write it.
                        UInt64 occupiedTexels = (UInt64)lightmapRequestData.atlassing.m_EstimatedTexelCount;
                        if (request.outputTypeMask.HasFlag(LightmapRequestOutputType.Occupancy))
                            File.WriteAllBytes(request.outputFolderPath + $"/occupiedTexels{lightmapIndex}.UInt64", BitConverter.GetBytes(occupiedTexels));
                    }
                    catch (Exception e)
                    {
                        CoreUtils.Destroy(blackOutput);
                        Debug.Assert(false, e.Message);
                        return Result.WriteToDiskFailure;
                    }
                    CoreUtils.Destroy(blackOutput);
                }
            }
            return Result.Success;
        }

        private static Dictionary<IntegratedOutputType, RequestedSubOutput> GetRequestedIntegratedOutputTypes(in LightmapRequest request)
        {
            // TODO handle lightmapOffset and lightmapCount from the request.
            // Turn the bits set in the request bitmask into an array of LightmapRequestOutputType.
            HashSet<LightmapRequestOutputType> lightmapRequestOutputTypes = BitfieldToList((int)request.outputTypeMask);

            // Make a dictionary of the lightmap types that are requested, the value indicates if directionality is requested for that stage (when it makes sense).
            // This accounts for the fact that for direct and indirect directionality is baked in the same pass. But for purposes of deciding what lightmap output
            // files to write we need to store that information as a bit flag.
            Dictionary<IntegratedOutputType, RequestedSubOutput> requestedLightmapTypes = new();
            void AddToRequestedLightmapTypes(IntegratedOutputType lightmapType, RequestedSubOutput type)
            {
                if (!requestedLightmapTypes.TryAdd(lightmapType, type))
                    requestedLightmapTypes[lightmapType] |= type;
            }

            foreach (var lightmapRequestOutputType in lightmapRequestOutputTypes)
            {
                if (!LightmapRequestOutputTypeToIntegratedOutputType(lightmapRequestOutputType, out IntegratedOutputType lightmapType))
                    continue;

                switch (lightmapType)
                {
                    case IntegratedOutputType.Indirect:
                        AddToRequestedLightmapTypes(IntegratedOutputType.Indirect, RequestedSubOutput.PrimaryTexture);
                        break;
                    case IntegratedOutputType.DirectionalityIndirect:
                        AddToRequestedLightmapTypes(IntegratedOutputType.Indirect, RequestedSubOutput.DirectionalityTexture);
                        break;
                    case IntegratedOutputType.Direct:
                        AddToRequestedLightmapTypes(IntegratedOutputType.Direct, RequestedSubOutput.PrimaryTexture);
                        break;
                    case IntegratedOutputType.DirectionalityDirect:
                        AddToRequestedLightmapTypes(IntegratedOutputType.Direct, RequestedSubOutput.DirectionalityTexture);
                        break;
                    default:
                        AddToRequestedLightmapTypes(lightmapType, RequestedSubOutput.PrimaryTexture);
                        break;
                }
            }

            return requestedLightmapTypes;
        }

        private static HashSet<LightmapRequestOutputType> GetRequestedNonIntegratedOutputTypes(in LightmapRequest request)
        {
            HashSet<LightmapRequestOutputType> outputTypes =
                BitfieldToList((int) (request.outputTypeMask & (LightmapRequestOutputType.Normal | LightmapRequestOutputType.Occupancy | LightmapRequestOutputType.ChartIndex | LightmapRequestOutputType.OverlapPixelIndex)));
            bool needsNormalsForDirectionality = request.outputTypeMask.HasFlag(LightmapRequestOutputType.DirectionalityIndirect) || request.outputTypeMask.HasFlag(LightmapRequestOutputType.DirectionalityDirect);
            if (needsNormalsForDirectionality)
                outputTypes.Add(LightmapRequestOutputType.Normal);

            return outputTypes;
        }

        private static (uint directSampleCount, uint effectiveIndirectSampleCount) GetProbeSampleCounts(in SampleCount probeSampleCounts)
        {
            uint indirectSampleCount = probeSampleCounts.indirectSampleCount;

            return (probeSampleCounts.directSampleCount, indirectSampleCount);
        }

        internal static bool ExecuteProbeRequests(in BakeInput bakeInput, in ProbeRequestData probeRequestData, UnityComputeDeviceContext deviceContext,
            bool useLegacyBakingBehavior, UnityComputeWorld world, uint bounceCount, BakeProgressState progressState,
            UnityEngine.Rendering.Sampling.SamplingResources samplingResources)
        {
            if (probeRequestData.requests.Length == 0)
                return true;

            ProbeIntegratorResources integrationResources = new();
            integrationResources.Load(world.RayTracingContext);

            var probeOcclusionLightIndexMappingShader = UnityEditor.AssetDatabase.LoadAssetAtPath<ComputeShader>("Packages/com.unity.render-pipelines.core/Runtime/PathTracing/Shaders/ProbeOcclusionLightIndexMapping.compute");
            using UnityComputeProbeIntegrator probeIntegrator = new(!useLegacyBakingBehavior, samplingResources, integrationResources, probeOcclusionLightIndexMappingShader);
            probeIntegrator.SetProgressReporter(progressState);

            // Create input position buffer
            using NativeArray<float3> inputPositions = new(probeRequestData.positions, Allocator.Temp);
            var positionsBuffer = deviceContext.CreateBuffer((ulong)probeRequestData.positions.Length, sizeof(float) * 3);
            BufferSlice<float3> positionsBufferSlice = positionsBuffer.Slice<float3>();
            var positionsWriteEvent = deviceContext.CreateEvent();
            deviceContext.WriteBuffer(positionsBufferSlice, inputPositions, positionsWriteEvent);
            deviceContext.DestroyEvent(positionsWriteEvent);

            // Create input probe occlusion light indices buffer (shared for all requests)
            using NativeArray<int> inputPerProbeLightIndices = new(probeRequestData.occlusionLightIndices, Allocator.Temp);
            var perProbeLightIndicesBuffer = deviceContext.CreateBuffer((ulong)probeRequestData.occlusionLightIndices.Length, sizeof(int));
            BufferSlice<int> perProbeLightIndicesBufferSlice = perProbeLightIndicesBuffer.Slice<int>();
            deviceContext.WriteBuffer(perProbeLightIndicesBufferSlice, inputPerProbeLightIndices);

            (uint directSampleCount, uint effectiveIndirectSampleCount) = GetProbeSampleCounts(bakeInput.lightingSettings.probeSampleCounts);

            ProbeRequest[] probeRequests = probeRequestData.requests;
            for (int probeRequestIndex = 0; probeRequestIndex < probeRequests.Length; probeRequestIndex++)
            {
                // Read data from request and prepare integrator
                ref readonly ProbeRequest request = ref probeRequests[probeRequestIndex];
                int requestOffset = (int)request.positionOffset;
                int requestLength = (int)request.count;
                ulong floatBufferSize = sizeof(float) * request.count;
                float pushoff = request.pushoff;
                probeIntegrator.Prepare(deviceContext, world, positionsBuffer.Slice<Vector3>(), pushoff, (int)bounceCount);

                List<EventID> eventsToWaitFor = new();
                List<BufferID> buffersToDestroy = new();

                // Integrate indirect radiance
                using NativeArray<SphericalHarmonicsL2> outputIndirectRadiance = new(requestLength, Allocator.Persistent);
                if (request.outputTypeMask.HasFlag(ProbeRequestOutputType.RadianceIndirect))
                {
                    Debug.Assert(effectiveIndirectSampleCount > 0);
                    var shIndirectBuffer = deviceContext.CreateBuffer(request.count * 27, sizeof(float));
                    buffersToDestroy.Add(shIndirectBuffer);
                    var shIndirectBufferSlice = shIndirectBuffer.Slice<SphericalHarmonicsL2>();
                    var integrationResult = probeIntegrator.IntegrateIndirectRadiance(deviceContext, requestOffset, requestLength,
                        (int)effectiveIndirectSampleCount, request.ignoreIndirectEnvironment, shIndirectBufferSlice);
                    Assert.AreEqual(IProbeIntegrator.ResultType.Success, integrationResult.type, "IntegrateIndirectRadiance failed.");
                    var readEvent = deviceContext.CreateEvent();
                    deviceContext.ReadBuffer(shIndirectBufferSlice, outputIndirectRadiance, readEvent);
                    eventsToWaitFor.Add(readEvent);
                }

                // Integrate direct radiance
                using NativeArray<SphericalHarmonicsL2> outputDirectRadiance = new(requestLength, Allocator.Persistent);
                if (request.outputTypeMask.HasFlag(ProbeRequestOutputType.RadianceDirect))
                {
                    Debug.Assert(directSampleCount > 0);
                    var shDirectBuffer = deviceContext.CreateBuffer(request.count * 27, sizeof(float));
                    buffersToDestroy.Add(shDirectBuffer);
                    var shDirectBufferSlice = shDirectBuffer.Slice<SphericalHarmonicsL2>();
                    var integrationResult = probeIntegrator.IntegrateDirectRadiance(deviceContext, requestOffset, requestLength,
                        (int)directSampleCount, request.ignoreDirectEnvironment, shDirectBufferSlice);
                    Assert.AreEqual(IProbeIntegrator.ResultType.Success, integrationResult.type, "IntegrateDirectRadiance failed.");
                    var readEvent = deviceContext.CreateEvent();
                    deviceContext.ReadBuffer(shDirectBufferSlice, outputDirectRadiance, readEvent);
                    eventsToWaitFor.Add(readEvent);
                }

                // Integrate validity
                using NativeArray<float> outputValidity = new(requestLength, Allocator.Persistent);
                if (request.outputTypeMask.HasFlag(ProbeRequestOutputType.Validity))
                {
                    Debug.Assert(effectiveIndirectSampleCount > 0);
                    var validityBuffer = deviceContext.CreateBuffer(request.count, sizeof(float));
                    buffersToDestroy.Add(validityBuffer);
                    var validityBufferSlice = validityBuffer.Slice<float>();
                    var integrationResult = probeIntegrator.IntegrateValidity(deviceContext, requestOffset, requestLength,
                        (int)effectiveIndirectSampleCount, validityBufferSlice);
                    Assert.AreEqual(IProbeIntegrator.ResultType.Success, integrationResult.type, "IntegrateValidity failed.");
                    var readEvent = deviceContext.CreateEvent();
                    deviceContext.ReadBuffer(validityBufferSlice, outputValidity, readEvent);
                    eventsToWaitFor.Add(readEvent);
                }

                // Integrate occlusion values
                const int maxOcclusionLightsPerProbe = 4;
                bool usesProbeOcclusion = bakeInput.lightingSettings.mixedLightingMode != MixedLightingMode.IndirectOnly;
                using NativeArray<float> outputOcclusion = new(requestLength * maxOcclusionLightsPerProbe, Allocator.Persistent, NativeArrayOptions.ClearMemory);
                if (request.outputTypeMask.HasFlag(ProbeRequestOutputType.LightProbeOcclusion) && usesProbeOcclusion)
                {
                    var occlusionBuffer = deviceContext.CreateBuffer(maxOcclusionLightsPerProbe * request.count, sizeof(float));
                    buffersToDestroy.Add(occlusionBuffer);
                    var occlusionBufferSlice = occlusionBuffer.Slice<float>();

                    var integrationResult = probeIntegrator.IntegrateOcclusion(deviceContext, requestOffset, requestLength,
                        (int)effectiveIndirectSampleCount, maxOcclusionLightsPerProbe, perProbeLightIndicesBufferSlice, occlusionBufferSlice);
                    Assert.AreEqual(IProbeIntegrator.ResultType.Success, integrationResult.type, "IntegrateOcclusion failed.");

                    EventID readEvent = deviceContext.CreateEvent();
                    deviceContext.ReadBuffer(occlusionBufferSlice, outputOcclusion, readEvent);
                    eventsToWaitFor.Add(readEvent);
                }

                // Gather occlusion containers / indices
                var outputOcclusionIndices = new LightProbeOcclusion[requestLength];
                for (int probeIdx = 0; probeIdx < requestLength; probeIdx++)
                {
                    ref var occlusion = ref outputOcclusionIndices[probeIdx];
                    occlusion.SetDefaultValues();
                }
                if (request.outputTypeMask.HasFlag(ProbeRequestOutputType.MixedLightOcclusion) && usesProbeOcclusion)
                {
                    // Build output data
                    for (int probeIdx = 0; probeIdx < requestLength; probeIdx++)
                    {
                        LightProbeOcclusion occlusion = new();
                        occlusion.SetDefaultValues();
                        for (int indirectLightIdx = 0; indirectLightIdx < maxOcclusionLightsPerProbe; indirectLightIdx++)
                        {
                            int bakeInputLightIdx = inputPerProbeLightIndices[probeIdx * maxOcclusionLightsPerProbe + indirectLightIdx];
                            if (bakeInputLightIdx >= 0)
                            {
                                sbyte occlusionMaskChannel = (sbyte)bakeInput.lightData[bakeInputLightIdx].shadowMaskChannel;

                                occlusion.SetProbeOcclusionLightIndex(indirectLightIdx, bakeInputLightIdx);
                                occlusion.SetOcclusion(indirectLightIdx, 0.0f);
                                occlusion.SetOcclusionMaskChannel(indirectLightIdx, occlusionMaskChannel);
                            }
                        }
                        outputOcclusionIndices[probeIdx] = occlusion;
                    }
                }

                // Flush and wait for all events to complete
                deviceContext.Flush();
                foreach (var evt in eventsToWaitFor)
                {
                    bool ok = deviceContext.Wait(evt);
                    Assert.IsTrue(ok);
                    deviceContext.DestroyEvent(evt);
                }

                // Cleanup temporary buffers
                foreach (var buffer in buffersToDestroy)
                {
                    deviceContext.DestroyBuffer(buffer);
                }

                // Write output data to disk
                // Write light probe data to disk, so the C++ side post processing stage can pick it up.
                // We can use the C# side post processing API in the future, once we can write the LDA from C#.
                string path = request.outputFolderPath;
                if (request.outputTypeMask.HasFlag(ProbeRequestOutputType.RadianceDirect))
                {
                    string filePath = path;
                    filePath += string.Format("/radianceDirect{0}.shl2", probeRequestIndex);
                    if (!SaveProbesToFileInterleaved(filePath, outputDirectRadiance))
                        return false;
                }
                if (request.outputTypeMask.HasFlag(ProbeRequestOutputType.RadianceIndirect))
                {
                    string filePath = path;
                    filePath += string.Format("/radianceIndirect{0}.shl2", probeRequestIndex);
                    if (!SaveProbesToFileInterleaved(filePath, outputIndirectRadiance))
                        return false;
                }
                if (request.outputTypeMask.HasFlag(ProbeRequestOutputType.Validity))
                {
                    string filePath = path;
                    filePath += string.Format("/validity{0}.float", probeRequestIndex);
                    if (!SaveArrayToFile(filePath, requestLength, outputValidity.ToArray()))
                        return false;
                }
                if (request.outputTypeMask.HasFlag(ProbeRequestOutputType.LightProbeOcclusion))
                {
                    string filePath = path;
                    filePath += string.Format("/lightProbeOcclusion{0}.vec4", probeRequestIndex);
                    if (!SaveArrayToFile(filePath, requestLength, outputOcclusion.ToArray()))
                        return false;
                }
                if (request.outputTypeMask.HasFlag(ProbeRequestOutputType.MixedLightOcclusion))
                {
                    // TODO(pema.malling): dummy data, the C++ side post processing stage needs this.
                    // https://jira.unity3d.com/browse/LIGHT-1102
                    string filePath = path;
                    filePath += string.Format("/mixedLightOcclusion{0}.occ", probeRequestIndex);
                    if (!SaveArrayToFile(filePath, requestLength, outputOcclusionIndices))
                        return false;
                }
            }

            // Cleanup created buffers
            deviceContext.DestroyBuffer(positionsBuffer);
            deviceContext.DestroyBuffer(perProbeLightIndicesBuffer);

            return true;
        }
    }
}