From db51bdd0ffbb4817ab1d603d3a9e70bd4b73dcd7 Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Mon, 29 Jun 2026 13:06:36 +0100 Subject: [PATCH 1/5] Avoid invalid reads in global splat merge --- Runtime/Shaders/GsplatMergeOrderBuffers.compute | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Runtime/Shaders/GsplatMergeOrderBuffers.compute b/Runtime/Shaders/GsplatMergeOrderBuffers.compute index 840458b..0f85ffe 100644 --- a/Runtime/Shaders/GsplatMergeOrderBuffers.compute +++ b/Runtime/Shaders/GsplatMergeOrderBuffers.compute @@ -109,7 +109,10 @@ void MergeTwo(uint3 id : SV_DispatchThreadID) float dA = (i < _CountA) ? _DepthsA[_OffsetA + i] : FLOAT_MAX; float dB = (j < _CountB) ? _DepthsB[_OffsetB + j] : FLOAT_MAX; - _OutputOrders[_OutputOffset + p] = (dA <= dB) ? _OrdersA[_OffsetA + i] : _OrdersB[_OffsetB + j]; + if (dA <= dB) + _OutputOrders[_OutputOffset + p] = _OrdersA[_OffsetA + i]; + else + _OutputOrders[_OutputOffset + p] = _OrdersB[_OffsetB + j]; } // Intermediate merge pass — writes merged orders AND depths to scratch. From ca5135120dbc8e5e20035ecdd01ce0c8a3e9192e Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Sun, 12 Jul 2026 17:00:58 +0100 Subject: [PATCH 2/5] Respect camera masks in global rendering --- Runtime/GsplatGlobalRenderer.cs | 5 ++++- Runtime/GsplatSorter.cs | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Runtime/GsplatGlobalRenderer.cs b/Runtime/GsplatGlobalRenderer.cs index f7375e0..fcf10f9 100644 --- a/Runtime/GsplatGlobalRenderer.cs +++ b/Runtime/GsplatGlobalRenderer.cs @@ -57,6 +57,7 @@ struct RendererParams Matrix4x4[] m_rendererTransformsCache; RendererParams[] m_rendererParamsCache; MaterialPropertyBlock m_globalPropertyBlock; + int m_renderLayer; public bool Valid => m_globalMaterial && m_globalMaterial.Valid() @@ -156,6 +157,7 @@ public void Update(List activeGsplats) { EnsureGlobalBuffers(activeGsplats); if (m_totalSplatCount == 0) return; + m_renderLayer = (activeGsplats[0] as Component)?.gameObject.layer ?? 0; UpdateRendererTransforms(activeGsplats); UpdateRendererParams(activeGsplats); Render(); @@ -492,7 +494,8 @@ void Render() var rp = new RenderParams(m_globalMaterial.Materials[m_globalSHBands]) { worldBounds = new Bounds(Vector3.zero, Vector3.one * 1e6f), - matProps = m_globalPropertyBlock + matProps = m_globalPropertyBlock, + layer = m_renderLayer }; int instances = Mathf.CeilToInt(m_totalRemainingCount / (float)GsplatSettings.Instance.SplatInstanceSize); diff --git a/Runtime/GsplatSorter.cs b/Runtime/GsplatSorter.cs index db476eb..4f0d3ec 100644 --- a/Runtime/GsplatSorter.cs +++ b/Runtime/GsplatSorter.cs @@ -165,6 +165,12 @@ bool CanRenderGlobally() return false; } + // A single global draw can only have one Unity layer. Mixed-layer sets must retain + // per-renderer draws so each camera's culling mask is respected. + var renderLayer = (m_activeGsplats[0] as Component)?.gameObject.layer ?? 0; + if (m_activeGsplats.Any(gs => ((gs as Component)?.gameObject.layer ?? 0) != renderLayer)) + return false; + return true; } From d0e35bbae7ffeb1533f58969b748fc9c53dbf495 Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Thu, 16 Jul 2026 16:49:40 +0100 Subject: [PATCH 3/5] Honor layers for custom IGsplat implementations --- Runtime/GsplatGlobalRenderer.cs | 4 ++-- Runtime/GsplatSorter.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Runtime/GsplatGlobalRenderer.cs b/Runtime/GsplatGlobalRenderer.cs index fcf10f9..8f23768 100644 --- a/Runtime/GsplatGlobalRenderer.cs +++ b/Runtime/GsplatGlobalRenderer.cs @@ -157,7 +157,7 @@ public void Update(List activeGsplats) { EnsureGlobalBuffers(activeGsplats); if (m_totalSplatCount == 0) return; - m_renderLayer = (activeGsplats[0] as Component)?.gameObject.layer ?? 0; + m_renderLayer = activeGsplats[0].transform.gameObject.layer; UpdateRendererTransforms(activeGsplats); UpdateRendererParams(activeGsplats); Render(); @@ -540,4 +540,4 @@ public void DisposeGlobalBuffers() m_builtSplatCounts = null; } } -} \ No newline at end of file +} diff --git a/Runtime/GsplatSorter.cs b/Runtime/GsplatSorter.cs index 4f0d3ec..19dc2fa 100644 --- a/Runtime/GsplatSorter.cs +++ b/Runtime/GsplatSorter.cs @@ -167,8 +167,8 @@ bool CanRenderGlobally() // A single global draw can only have one Unity layer. Mixed-layer sets must retain // per-renderer draws so each camera's culling mask is respected. - var renderLayer = (m_activeGsplats[0] as Component)?.gameObject.layer ?? 0; - if (m_activeGsplats.Any(gs => ((gs as Component)?.gameObject.layer ?? 0) != renderLayer)) + var renderLayer = m_activeGsplats[0].transform.gameObject.layer; + if (m_activeGsplats.Any(gs => gs.transform.gameObject.layer != renderLayer)) return false; return true; From d5ac39f68fbc11d684914f2713e2037def4a22bc Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Thu, 16 Jul 2026 16:52:19 +0100 Subject: [PATCH 4/5] Choose one render path after runtime changes --- Runtime/GsplatRenderer.cs | 17 +++++++++++------ Runtime/GsplatSorter.cs | 19 +++++++++++++------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Runtime/GsplatRenderer.cs b/Runtime/GsplatRenderer.cs index a257ce6..97396fe 100644 --- a/Runtime/GsplatRenderer.cs +++ b/Runtime/GsplatRenderer.cs @@ -163,12 +163,17 @@ public void Update() { m_renderer.EvaluateRefreshRequired(SortMode, SortRefreshRate - 1, CutoutsRefreshRate - 1); m_renderer.DispatchInitOrder(Cutouts, transform.localToWorldMatrix, CutoutsUpdateBounds); - // When the global sorter has merged all renderers into a single draw call, - // skip the per-renderer draw — GsplatSorter.DrawAll handles rendering. - if (!GsplatSorter.Instance.GlobalRenderEnabled) - m_renderer.Render(transform, gameObject.layer, GammaToLinear, SHDegree, Brightness, - 1.0f - SplatDownscaleFactor, RenderOrder); } } + + // Called by GsplatSorter after it has selected one render path for the frame. + internal void Render() + { + if (!Valid || !GsplatSettings.Instance.Valid || !GsplatSorter.Instance.Valid) + return; + + m_renderer.Render(transform, gameObject.layer, GammaToLinear, SHDegree, Brightness, + 1.0f - SplatDownscaleFactor, RenderOrder); + } } -} \ No newline at end of file +} diff --git a/Runtime/GsplatSorter.cs b/Runtime/GsplatSorter.cs index 19dc2fa..53f534a 100644 --- a/Runtime/GsplatSorter.cs +++ b/Runtime/GsplatSorter.cs @@ -244,15 +244,22 @@ public void DispatchSort(CommandBuffer cmd, Camera camera) // Called by GsplatPlayerLoopHook once per frame, before Unity's PostLateUpdate phase public void Update() { - GlobalRenderEnabled = m_globalRenderer.Valid && GsplatSettings.Instance.EnableGlobalSort && - m_activeGsplats.Count >= 2; - if (!GlobalRenderEnabled) return; m_activeGsplats.Clear(); foreach (var gs in m_gsplats.Where(gs => gs is { isActiveAndEnabled: true, Valid: true })) m_activeGsplats.Add(gs); - GlobalRenderEnabled = GlobalRenderEnabled && CanRenderGlobally(); - if (!GlobalRenderEnabled) return; - m_globalRenderer.Update(m_activeGsplats); + + GlobalRenderEnabled = m_globalRenderer.Valid && GsplatSettings.Instance.EnableGlobalSort && + m_activeGsplats.Count >= 2 && CanRenderGlobally(); + if (GlobalRenderEnabled) + { + m_globalRenderer.Update(m_activeGsplats); + return; + } + + // Select and submit the fallback path here as well, so a runtime transition cannot + // make GsplatRenderer.Update observe the previous frame's global-render state. + foreach (var renderer in m_activeGsplats.OfType()) + renderer.Render(); } public ISorterResource CreateSorterResource(uint count, GraphicsBuffer orderBuffer) From 4db8f620dd9b9c8c83e7f2ae4f27def5e0a3d139 Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Sat, 18 Jul 2026 15:58:17 +0100 Subject: [PATCH 5/5] Allow renderers to opt out of global sort --- Runtime/GsplatRenderer.cs | 19 +++++++++++++++++++ Runtime/GsplatSorter.cs | 32 ++++++++++++++++++++++++-------- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/Runtime/GsplatRenderer.cs b/Runtime/GsplatRenderer.cs index 97396fe..08cb84d 100644 --- a/Runtime/GsplatRenderer.cs +++ b/Runtime/GsplatRenderer.cs @@ -34,6 +34,24 @@ public enum GsplatSortMode public bool AsyncUpload; public bool RenderBeforeUploadComplete = true; + [SerializeField] + [Tooltip("When enabled, this renderer can be merged with other compatible renderers for global depth sorting. Disable it for isolated previews or other renderers that must retain an independent draw.")] + bool m_participateInGlobalSort = true; + + public bool ParticipateInGlobalSort + { + get => m_participateInGlobalSort; + set + { + if (m_participateInGlobalSort == value) + return; + + m_participateInGlobalSort = value; + if (isActiveAndEnabled) + GsplatSorter.Instance.MarkGlobalBuffersDirty(); + } + } + [Tooltip("Does cutouts update the Gsplat world bounds? (Costly on moving cutouts)")] public bool CutoutsUpdateBounds = true; @@ -123,6 +141,7 @@ public void OnDrawGizmos() void OnValidate() { ForceRefresh(); + GsplatSorter.Instance.MarkGlobalBuffersDirty(); #if UNITY_EDITOR if (GsplatAsset && AssetDatabase.TryGetGUIDAndLocalFileIdentifier(GsplatAsset, out var guid, out long localId)) diff --git a/Runtime/GsplatSorter.cs b/Runtime/GsplatSorter.cs index 53f534a..765434c 100644 --- a/Runtime/GsplatSorter.cs +++ b/Runtime/GsplatSorter.cs @@ -68,6 +68,8 @@ public void Dispose() readonly HashSet m_gsplats = new(); readonly HashSet m_camerasInjected = new(); readonly List m_activeGsplats = new(); + readonly List m_globalGsplats = new(); + readonly List m_fallbackGsplats = new(); readonly HashSet m_warnedUncompressed = new(); GsplatSortPass m_sortPass; public const string k_passName = "SortGsplats"; @@ -124,6 +126,8 @@ public void UnregisterGsplat(IGsplat gsplat) } m_activeGsplats.Clear(); + m_globalGsplats.Clear(); + m_fallbackGsplats.Clear(); m_commandBuffer?.Dispose(); m_commandBuffer = null; Camera.onPreCull -= OnPreCullCamera; @@ -146,7 +150,7 @@ public bool GatherGsplatsForCamera(Camera cam) bool CanRenderGlobally() { // renderer_id is packed into 8 bits ([31:24]); max 255 renderers. - if (m_activeGsplats.Count > 255) + if (m_globalGsplats.Count > 255) { Debug.LogError( "[GsplatSorter] Global merge supports at most 255 renderers. Falling back to per-renderer rendering."); @@ -154,21 +158,21 @@ bool CanRenderGlobally() } // Global merge requires every active renderer to use SPARK compression. - foreach (var gs in m_activeGsplats) + foreach (var gs in m_globalGsplats) { if (gs.GsplatResource is GsplatResourceSpark) continue; var obj = gs as UnityEngine.Object; var id = obj ? obj.GetInstanceID() : 0; if (m_warnedUncompressed.Add(id)) Debug.LogWarning( - $"[GsplatSorter] '{obj?.name}' uses an uncompressed asset; global sort requires every active renderer to use SPARK compression. Disabling global sort for this scene — all renderers fall back to per-renderer rendering."); + $"[GsplatSorter] '{obj?.name}' uses an uncompressed asset; global sort requires every participating renderer to use SPARK compression. Participating renderers will fall back to per-renderer rendering."); return false; } // A single global draw can only have one Unity layer. Mixed-layer sets must retain // per-renderer draws so each camera's culling mask is respected. - var renderLayer = m_activeGsplats[0].transform.gameObject.layer; - if (m_activeGsplats.Any(gs => gs.transform.gameObject.layer != renderLayer)) + var renderLayer = m_globalGsplats[0].transform.gameObject.layer; + if (m_globalGsplats.Any(gs => gs.transform.gameObject.layer != renderLayer)) return false; return true; @@ -238,7 +242,7 @@ public void DispatchSort(CommandBuffer cmd, Camera camera) // --- Global K-way merge --- if (GlobalRenderEnabled) - m_globalRenderer.DispatchMerge(cmd, m_activeGsplats); + m_globalRenderer.DispatchMerge(cmd, m_globalGsplats); } // Called by GsplatPlayerLoopHook once per frame, before Unity's PostLateUpdate phase @@ -248,11 +252,23 @@ public void Update() foreach (var gs in m_gsplats.Where(gs => gs is { isActiveAndEnabled: true, Valid: true })) m_activeGsplats.Add(gs); + m_globalGsplats.Clear(); + m_fallbackGsplats.Clear(); + foreach (var gs in m_activeGsplats) + { + if (gs is GsplatRenderer { ParticipateInGlobalSort: false }) + m_fallbackGsplats.Add(gs); + else + m_globalGsplats.Add(gs); + } + GlobalRenderEnabled = m_globalRenderer.Valid && GsplatSettings.Instance.EnableGlobalSort && - m_activeGsplats.Count >= 2 && CanRenderGlobally(); + m_globalGsplats.Count >= 2 && CanRenderGlobally(); if (GlobalRenderEnabled) { - m_globalRenderer.Update(m_activeGsplats); + m_globalRenderer.Update(m_globalGsplats); + foreach (var renderer in m_fallbackGsplats.OfType()) + renderer.Render(); return; }