diff --git a/Packages/com.unity.render-pipelines.core/Editor/AssemblyInfo.cs b/Packages/com.unity.render-pipelines.core/Editor/AssemblyInfo.cs index 8b43a084d74..7d31d8580e1 100644 --- a/Packages/com.unity.render-pipelines.core/Editor/AssemblyInfo.cs +++ b/Packages/com.unity.render-pipelines.core/Editor/AssemblyInfo.cs @@ -4,5 +4,6 @@ [assembly: InternalsVisibleTo("Unity.RenderPipelines.Core.Editor.Tests")] [assembly: InternalsVisibleTo("Unity.RenderPipelines.HighDefinition.Editor.Tests")] [assembly: InternalsVisibleTo("Unity.RenderPipelines.Universal.Editor.Tests")] +[assembly: InternalsVisibleTo("Unity.Testing.VisualEffectGraph.EditorTests")] [assembly: InternalsVisibleTo("Assembly-CSharp-Editor-testable")] diff --git a/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.LightTransport.cs b/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.LightTransport.cs index 73f82a1d7da..207d96af906 100644 --- a/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.LightTransport.cs +++ b/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.LightTransport.cs @@ -1005,27 +1005,36 @@ internal static void BakeAdjustmentVolume(ProbeVolumeBakingSet bakingSet, ProbeA if (!failed) { - for (int c = 0; c < bakingCells.Length; c++) + // Validate baking cells size before any global state modifications + var chunkSizeInProbes = ProbeBrickPool.GetChunkSizeInProbeCount(); + var hasVirtualOffsets = m_BakingSet.settings.virtualOffsetSettings.useVirtualOffset; + var hasRenderingLayers = m_BakingSet.useRenderingLayers; + + if (ValidateBakingCellsSize(bakingCells, chunkSizeInProbes, hasVirtualOffsets, hasRenderingLayers)) { - ref var cell = ref bakingCells[c]; - ComputeValidityMasks(cell); - } - - // Write result to disk - WriteBakingCells(bakingCells); + for (int c = 0; c < bakingCells.Length; c++) + { + ref var cell = ref bakingCells[c]; + ComputeValidityMasks(cell); + } - // Reload everything - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); + // Attempt to write the result to disk + if (WriteBakingCells(bakingCells)) + { + // Reload everything + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); - if (m_BakingSet.hasDilation) - { - // Force reloading of data - foreach (var data in prv.perSceneDataList) - data.Initialize(); + if (m_BakingSet.hasDilation) + { + // Force reloading of data + foreach (var data in prv.perSceneDataList) + data.Initialize(); - InitDilationShaders(); - PerformDilation(); + InitDilationShaders(); + PerformDilation(); + } + } } } } @@ -1045,6 +1054,7 @@ internal static void BakeAdjustmentVolume(ProbeVolumeBakingSet bakingSet, ProbeA bakingSet.settings.virtualOffsetSettings.useVirtualOffset = savedVirtualOffset; bakingSet.useRenderingLayers = savedRenderingLayers; + m_BakingBatch?.Dispose(); m_BakingBatch = null; m_BakingSet = null; } diff --git a/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.Placement.cs b/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.Placement.cs index 3c92c237838..cc44edcc970 100644 --- a/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.Placement.cs +++ b/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.Placement.cs @@ -3,7 +3,7 @@ using Unity.Collections; using UnityEngine.SceneManagement; using UnityEditor; - +using System.Runtime.InteropServices; using Brick = UnityEngine.Rendering.ProbeBrickIndex.Brick; namespace UnityEngine.Rendering @@ -191,7 +191,8 @@ static NativeList ApplySubdivisionResults(ProbeSubdivisionResult result int positionStart = positions.Length; ConvertBricksToPositions(bricks, out var probePositions, out var brickSubdivLevels); - DeduplicateProbePositions(in probePositions, in brickSubdivLevels, m_BakingBatch, positions, out var probeIndices); + if (!DeduplicateProbePositions(in probePositions, in brickSubdivLevels, m_BakingBatch, positions, out var probeIndices)) + return new NativeList(Allocator.Persistent); BakingCell cell = new BakingCell() { @@ -210,9 +211,22 @@ static NativeList ApplySubdivisionResults(ProbeSubdivisionResult result return positions; } - private static void DeduplicateProbePositions(in Vector3[] probePositions, in int[] brickSubdivLevel, BakingBatch batch, + // We know that the current limitation on native containers is this. When an integer overflow bug (https://jira.unity3d.com/browse/UUM-113721) has been fixed, we can raise the limit + // This and related work is tracked by https://jira.unity3d.com/browse/GFXLIGHT-1738 + static readonly long k_MaxNumberOfPositions = 67180350; + + static bool DeduplicateProbePositions(in Vector3[] probePositions, in int[] brickSubdivLevel, BakingBatch batch, NativeList uniquePositions, out int[] indices) { + long numberOfPositions = (long)probePositions.Length + batch.positionToIndex.Count; + if (numberOfPositions > k_MaxNumberOfPositions) + { + Debug.LogError($"The number of Adaptive Probe Volume (APV) probes Unity generated exceeds the current system limit of {k_MaxNumberOfPositions} probes per Baking Set. Reduce density either by adjusting the general Probe Spacing in the Lighting window, or by modifying the Adaptive Probe Volumes in the scene to limit where the denser subdivision levels are used."); + indices = null; + + return false; + } + indices = new int[probePositions.Length]; int uniqueIndex = batch.positionToIndex.Count; @@ -238,6 +252,8 @@ private static void DeduplicateProbePositions(in Vector3[] probePositions, in in uniqueIndex++; } } + + return true; } static ProbeSubdivisionResult GetBricksFromLoaded(List dataList) diff --git a/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.Serialization.cs b/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.Serialization.cs index e44748d4704..150525aeff7 100644 --- a/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.Serialization.cs +++ b/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.Serialization.cs @@ -726,6 +726,63 @@ static void ExtractBakingCells() static long AlignRemainder16(long count) => count % 16L; + /// + /// Calculates support data chunk size based on provided configuration. + /// + /// Number of probes per chunk + /// Whether virtual offsets are enabled + /// Whether rendering layers are enabled + /// The size in bytes of a single support data chunk + static int CalculateSupportDataChunkSize(int chunkSizeInProbes, bool hasVirtualOffsets, bool hasRenderingLayers) + { + int supportPositionChunkSize = UnsafeUtility.SizeOf() * chunkSizeInProbes; + int supportValidityChunkSize = UnsafeUtility.SizeOf() * chunkSizeInProbes; + int supportTouchupChunkSize = UnsafeUtility.SizeOf() * chunkSizeInProbes; + int supportLayerMaskChunkSize = hasRenderingLayers ? UnsafeUtility.SizeOf() * chunkSizeInProbes : 0; + int supportOffsetsChunkSize = hasVirtualOffsets ? UnsafeUtility.SizeOf() * chunkSizeInProbes : 0; + + return supportPositionChunkSize + supportValidityChunkSize + + supportOffsetsChunkSize + supportLayerMaskChunkSize + supportTouchupChunkSize; + } + + /// + /// Validates that the baking cells can be written without exceeding system limits. + /// This method performs size calculations without accessing any global state. + /// + /// Array of baking cells to validate + /// Number of probes per chunk + /// Whether virtual offsets are enabled + /// Whether rendering layers are enabled + /// True if cells can be written safely, false if they exceed limits + static bool ValidateBakingCellsSize(BakingCell[] bakingCells, int chunkSizeInProbes, bool hasVirtualOffsets, bool hasRenderingLayers) + { + if (bakingCells == null || bakingCells.Length == 0) + return true; + + int supportDataChunkSize = CalculateSupportDataChunkSize(chunkSizeInProbes, hasVirtualOffsets, hasRenderingLayers); + + // Calculate total chunks count - need to call AnalyzeBrickForIndirectionEntries to get shChunkCount + // Create a copy to avoid modifying the original cells during validation + var tempCells = new BakingCell[bakingCells.Length]; + int totalChunksCount = 0; + for (var i = 0; i < bakingCells.Length; ++i) + { + tempCells[i] = bakingCells[i]; // Shallow copy is sufficient for this validation + AnalyzeBrickForIndirectionEntries(ref tempCells[i]); + totalChunksCount += tempCells[i].shChunkCount; + } + + // Perform the critical size check + long supportDataTotalSize = (long)totalChunksCount * supportDataChunkSize; + if (supportDataTotalSize > int.MaxValue) + { + Debug.LogError($"The size of the Adaptive Probe Volume (APV) baking set chunks exceed the current system limit of {int.MaxValue}, unable to save the baked cell assets. Reduce density either by adjusting the general Probe Spacing in the Lighting window, or by modifying the Adaptive Probe Volumes in the scene to limit where the denser subdivision levels are used."); + return false; + } + + return true; + } + static void WriteNativeArray(System.IO.FileStream fs, NativeArray array) where T : struct { unsafe @@ -736,7 +793,7 @@ static void WriteNativeArray(System.IO.FileStream fs, NativeArray array) w } /// - /// This method converts a list of baking cells into 5 separate assets: + /// This method attempts to convert a list of baking cells into 5 separate assets: /// 2 assets per baking state: /// CellData: a binary flat file containing L0L1 probes data /// CellOptionalData: a binary flat file containing L2 probe data (when present) @@ -745,7 +802,7 @@ static void WriteNativeArray(System.IO.FileStream fs, NativeArray array) w /// CellSharedData: a binary flat file containing bricks data /// CellSupportData: a binary flat file containing debug data (stripped from player builds if building without debug shaders) /// - unsafe static void WriteBakingCells(BakingCell[] bakingCells) + static unsafe bool WriteBakingCells(BakingCell[] bakingCells) { m_BakingSet.GetBlobFileNames(m_BakingSet.lightingScenario, out var cellDataFilename, out var cellBricksDataFilename, out var cellOptionalDataFilename, out var cellProbeOcclusionDataFilename, out var cellSharedDataFilename, out var cellSupportDataFilename); @@ -846,16 +903,16 @@ unsafe static void WriteBakingCells(BakingCell[] bakingCells) // Brick data using var bricks = new NativeArray(m_TotalCellCounts.bricksCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); - // CellSupportData - m_BakingSet.supportPositionChunkSize = sizeof(Vector3) * chunkSizeInProbes; - m_BakingSet.supportValidityChunkSize = sizeof(float) * chunkSizeInProbes; - m_BakingSet.supportOffsetsChunkSize = hasVirtualOffsets ? sizeof(Vector3) * chunkSizeInProbes : 0; - m_BakingSet.supportTouchupChunkSize = sizeof(float) * chunkSizeInProbes; - m_BakingSet.supportLayerMaskChunkSize = hasRenderingLayers ? sizeof(byte) * chunkSizeInProbes : 0; + // CellSupportData - use pure helper function for calculation + m_BakingSet.supportPositionChunkSize = UnsafeUtility.SizeOf() * chunkSizeInProbes; + m_BakingSet.supportValidityChunkSize = UnsafeUtility.SizeOf() * chunkSizeInProbes; + m_BakingSet.supportOffsetsChunkSize = hasVirtualOffsets ? UnsafeUtility.SizeOf() * chunkSizeInProbes : 0; + m_BakingSet.supportTouchupChunkSize = UnsafeUtility.SizeOf() * chunkSizeInProbes; + m_BakingSet.supportLayerMaskChunkSize = hasRenderingLayers ? UnsafeUtility.SizeOf() * chunkSizeInProbes : 0; - m_BakingSet.supportDataChunkSize = m_BakingSet.supportPositionChunkSize + m_BakingSet.supportValidityChunkSize + m_BakingSet.supportOffsetsChunkSize + m_BakingSet.supportLayerMaskChunkSize + m_BakingSet.supportTouchupChunkSize; - var supportDataTotalSize = m_TotalCellCounts.chunksCount * m_BakingSet.supportDataChunkSize; - using var supportData = new NativeArray(supportDataTotalSize, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); + m_BakingSet.supportDataChunkSize = CalculateSupportDataChunkSize(chunkSizeInProbes, hasVirtualOffsets, hasRenderingLayers); + long supportDataTotalSize = (long)m_TotalCellCounts.chunksCount * m_BakingSet.supportDataChunkSize; + using var supportData = new NativeArray((int)supportDataTotalSize, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); var sceneStateHash = m_BakingSet.GetBakingHashCode(); var startCounts = new CellCounts(); @@ -1093,6 +1150,8 @@ unsafe static void WriteBakingCells(BakingCell[] bakingCells) m_BakingSet.cellSupportDataAsset = new ProbeVolumeStreamableAsset(kAPVStreamingAssetsPath, cellSupportDescs, m_BakingSet.supportDataChunkSize, bakingSetGUID, AssetDatabase.AssetPathToGUID(cellSupportDataFilename)); EditorUtility.SetDirty(m_BakingSet); + + return true; } unsafe static void WriteDilatedCells(List cells) diff --git a/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.VirtualOffset.cs b/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.VirtualOffset.cs index 7718fa3b90a..29588cad039 100644 --- a/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.VirtualOffset.cs +++ b/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.VirtualOffset.cs @@ -342,8 +342,19 @@ static internal void RecomputeVOForDebugOnly() // Make sure unloading happens. prv.PerformPendingOperations(); - // Write back the assets. - WriteBakingCells(m_BakingBatch.cells.ToArray()); + // Validate baking cells size before writing + var bakingCellsArray = m_BakingBatch.cells.ToArray(); + var chunkSizeInProbes = ProbeBrickPool.GetChunkSizeInProbeCount(); + var hasVirtualOffsets = m_BakingSet.settings.virtualOffsetSettings.useVirtualOffset; + var hasRenderingLayers = m_BakingSet.useRenderingLayers; + + if (ValidateBakingCellsSize(bakingCellsArray, chunkSizeInProbes, hasVirtualOffsets, hasRenderingLayers)) + { + // Write back the assets. + WriteBakingCells(bakingCellsArray); + } + + m_BakingBatch?.Dispose(); m_BakingBatch = null; foreach (var data in prv.perSceneDataList) diff --git a/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.cs b/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.cs index 52ee43b5ce1..0e00537cab2 100644 --- a/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.cs +++ b/Packages/com.unity.render-pipelines.core/Editor/Lighting/ProbeVolume/ProbeGIBaking.cs @@ -268,15 +268,15 @@ internal int GetBakingHashCode() } } - class BakingBatch + class BakingBatch : IDisposable { public Dictionary> cellIndex2SceneReferences = new (); public List cells = new (); // Used to retrieve probe data from it's position in order to fix seams - public Dictionary positionToIndex = new (); + public NativeHashMap positionToIndex; // Allow to get a mapping to subdiv level with the unique positions. It stores the minimum subdiv level found for a given position. // Can be probably done cleaner. - public Dictionary uniqueBrickSubdiv = new (); + public NativeHashMap uniqueBrickSubdiv; // Mapping for explicit invalidation, whether it comes from the auto finding of occluders or from the touch up volumes // TODO: This is not used yet. Will soon. public Dictionary invalidatedPositions = new (); @@ -306,6 +306,19 @@ public BakingBatch(Vector3Int cellCount, ProbeReferenceVolume refVolume) maxBrickCount = cellCount * ProbeReferenceVolume.CellSize(refVolume.GetMaxSubdivision()); inverseScale = ProbeBrickPool.kBrickCellCount / refVolume.MinBrickSize(); offset = refVolume.ProbeOffset(); + + // Initialize NativeHashMaps with reasonable initial capacity + // Using a larger capacity to reduce allocations during baking + positionToIndex = new NativeHashMap(100000, Allocator.Persistent); + uniqueBrickSubdiv = new NativeHashMap(100000, Allocator.Persistent); + } + + public void Dispose() + { + if (positionToIndex.IsCreated) + positionToIndex.Dispose(); + if (uniqueBrickSubdiv.IsCreated) + uniqueBrickSubdiv.Dispose(); } public int GetProbePositionHash(Vector3 position) @@ -1202,6 +1215,7 @@ static void OnBakeCancelled() static void CleanBakeData() { s_BakeData.Dispose(); + m_BakingBatch?.Dispose(); m_BakingBatch = null; s_AdjustmentVolumes = null; @@ -1478,6 +1492,15 @@ static void ApplyPostBakeOperations() // Use the globalBounds we just computed, as the one in probeRefVolume doesn't include scenes that have never been baked probeRefVolume.globalBounds = globalBounds; + // Validate baking cells size before any state modifications + var bakingCellsArray = m_BakedCells.Values.ToArray(); + var chunkSizeInProbes = ProbeBrickPool.GetChunkSizeInProbeCount(); + var hasVirtualOffsets = m_BakingSet.settings.virtualOffsetSettings.useVirtualOffset; + var hasRenderingLayers = m_BakingSet.useRenderingLayers; + + if (!ValidateBakingCellsSize(bakingCellsArray, chunkSizeInProbes, hasVirtualOffsets, hasRenderingLayers)) + return; // Early exit if validation fails + PrepareCellsForWriting(isBakingSceneSubset); m_BakingSet.chunkSizeInBricks = ProbeBrickPool.GetChunkSizeInBrickCount(); @@ -1488,9 +1511,13 @@ static void ApplyPostBakeOperations() m_BakingSet.scenarios.TryAdd(m_BakingSet.lightingScenario, new ProbeVolumeBakingSet.PerScenarioDataInfo()); - // Convert baking cells to runtime cells + // Attempt to convert baking cells to runtime cells + bool succeededWritingBakingCells; using (new BakingCompleteProfiling(BakingCompleteProfiling.Stages.WriteBakedData)) - WriteBakingCells(m_BakedCells.Values.ToArray()); + succeededWritingBakingCells = WriteBakingCells(m_BakedCells.Values.ToArray()); + + if (!succeededWritingBakingCells) + return; // Reset internal structures depending on current bake. Debug.Assert(probeRefVolume.EnsureCurrentBakingSet(m_BakingSet)); @@ -1534,6 +1561,7 @@ static void ApplyPostBakeOperations() } // Mark stuff as up to date + m_BakingBatch?.Dispose(); m_BakingBatch = null; foreach (var probeVolume in GetProbeVolumeList()) probeVolume.OnBakeCompleted(); diff --git a/Packages/com.unity.render-pipelines.core/Editor/Properties/PropertiesPreferencesProvider.cs b/Packages/com.unity.render-pipelines.core/Editor/Properties/PropertiesPreferencesProvider.cs index 8190d23d055..d0b4622acf4 100644 --- a/Packages/com.unity.render-pipelines.core/Editor/Properties/PropertiesPreferencesProvider.cs +++ b/Packages/com.unity.render-pipelines.core/Editor/Properties/PropertiesPreferencesProvider.cs @@ -19,9 +19,11 @@ class Styles public void PreferenceGUI() { + EditorGUI.indentLevel++; AdvancedProperties.enabled = EditorGUILayout.IntPopup(Styles.additionalPropertiesLabel, AdvancedProperties.enabled ? 1 : 0, Styles.additionalPropertiesNames, Styles.additionalPropertiesValues) == 1; + EditorGUI.indentLevel--; } } } diff --git a/Packages/com.unity.render-pipelines.core/Editor/SampleDependencyImportSystem/SampleDependencyImporter.cs b/Packages/com.unity.render-pipelines.core/Editor/SampleDependencyImportSystem/SampleDependencyImporter.cs index 734e122df1e..d2a8bef4d18 100644 --- a/Packages/com.unity.render-pipelines.core/Editor/SampleDependencyImportSystem/SampleDependencyImporter.cs +++ b/Packages/com.unity.render-pipelines.core/Editor/SampleDependencyImportSystem/SampleDependencyImporter.cs @@ -8,8 +8,7 @@ using PackageInfo = UnityEditor.PackageManager.PackageInfo; /// -/// To implement this, the package needs to be in the allowedPackageList -/// Then, in the package.json, an array can be added after the path variable of the sample. The path should start from the Packages/ folder, as such: +/// In the package.json, an array can be added after the path variable of the sample. The path should start from the Packages/ folder, as such: /// "samples": [ /// { /// "displayName": "Sample name", @@ -27,42 +26,123 @@ /// [InitializeOnLoad] -class SampleDependencyImporter : IPackageManagerExtension +internal class SampleDependencyImporter : IPackageManagerExtension { - /// - /// An implementation of AssetPostProcessor which will raise an event when a new asset is imported. - /// - class SamplePostprocessor : AssetPostprocessor + internal static SampleDependencyImporter instance { get; private set; } + + static SampleDependencyImporter() { - public static event Action AssetImported; + instance = new SampleDependencyImporter(); + PackageManagerExtensions.RegisterExtension(instance); + } + + bool importingTextMeshProEssentialResources = false; + + PackageInfo m_PackageInfo; + SampleList m_SampleList; + List m_Samples; - static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) + VisualElement injectingElement; + VisualElement _panelRoot; + VisualElement panelRoot + { + get { - for (int i = 0; i < importedAssets.Length; i++) - AssetImported?.Invoke(importedAssets[i]); + _panelRoot ??= injectingElement.panel.visualTree; + return _panelRoot; } } - static SampleDependencyImporter() + /// + /// Use the extension UI to "inject" an invisible element in package manager UI + /// that will serve as a base to hook up additional logic to the import buttons. + /// + VisualElement IPackageManagerExtension.CreateExtensionUI() { - PackageManagerExtensions.RegisterExtension(new SampleDependencyImporter()); + injectingElement = new VisualElement(); + injectingElement.style.display = DisplayStyle.None; + + // This callback is called once the element is added to the UI, at this point we should have access to rest of the elements. + injectingElement.RegisterCallback((callback) => { + //Force clear the cached elements to fetch those from the newly openned window + _panelRoot = null; + samplesButton = null; + + RefreshSampleButtons(); + }); + + return injectingElement; } - - string[] allowedPackageList = + + Button samplesButton; + const string samplesButtonName = "samplesButton"; + const string sampleContainerClassName = "sampleContainer"; + const string importButtonClassName = "importButton"; + const string injectedButtonClassName = "importWithDependenciesButton"; + + void RefreshSampleButtons() { - "com.unity.render-pipelines.high-definition", - "com.unity.render-pipelines.universal", - "com.unity.shadergraph", - "com.unity.visualeffectgraph" - }; + if (injectingElement == null || m_PackageInfo == null || m_SampleList == null) + return; - bool importingTextMeshProEssentialResources = false; + // Call refresh of samples and button injection when switching to the "Samples" tab. + if (samplesButton == null ) + { + samplesButton = panelRoot.Q