Zgragselus

ClusterCull.compute

Jul 5th, 2026
36
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 13.09 KB | None | 0 0
  1. // M3a proved out writing Unity's indirect-draw-args buffer from a compute shader, in isolation
  2. // from any cluster-selection logic. M3b moved root-only selection onto the GPU (no accept/expand -
  3. // always the coarsest LOD). M3c added a real accept/expand test but only one level deep from each
  4. // root (a failed root's children were selected unconditionally, without being tested in turn).
  5. //
  6. // M3d (this step) replaces M3c with a fully recursive top-down traversal that matches
  7. // ClusterLODManager.SelectClusters exactly: starting from the roots, each node is tested, and if
  8. // it fails, its children are queued for testing in the *next* pass - repeated pass after pass until
  9. // every live node has either been selected or bottomed out at a true leaf. Since the fan-out at
  10. // each node is data/camera-dependent (unlike M3c, which sized everything up front from
  11. // camera-independent worst cases), this needs a real atomic-append mechanism instead of fixed
  12. // per-root output slots, and a way to keep dispatching without a CPU readback between passes.
  13. //
  14. // The approach: two ping-pong "frontier" buffers holding this pass's/next pass's node indices, a
  15. // small counters buffer ([0]=current frontier count, [1]=next frontier count being accumulated via
  16. // InterlockedAdd, [2]=cumulative selected count, [3]=overflow count - see below), and a tiny
  17. // Advance kernel that runs between passes to promote next->current, reset next to zero, and write
  18. // GPU-computed thread-group counts into a dispatch-args buffer that the *next* TraverseLevel call
  19. // consumes via ComputeShader.DispatchIndirect - so the number of passes needed is the only thing
  20. // decided on the CPU (a heuristic upper bound based on the bake's LOD count, see
  21. // ClusterLODManager.RenderGpu), never the per-pass node counts themselves.
  22. //
  23. // _NextFrontier/_Selected are sized to meshletsCount plus a small fixed safety pad (M3e - see
  24. // ClusterLODManager.GetOrBuildBuffers), not a generous multiple of it: now that group expansion is
  25. // deduplicated (see _ClaimedGroups below), each real meshlet can enter the frontier or get selected
  26. // at most once per traversal - a root exactly once (roots are already unique), a non-root exactly
  27. // once (it belongs to exactly one ClusterGroup as a child, and that group is only ever expanded
  28. // once) - so meshletsCount is a tight, not just generous, upper bound. Before dedup existed, a
  29. // ClusterGroup with more than one parent meshlet (siblings sharing a group) could be expanded once
  30. // per sibling, doubling the work at every subsequent level and compounding into an exponential
  31. // blowup pass over pass - caught during bring-up (a small bake produced a clean 4x per-pass growth
  32. // in the frontier before saturating every buffer and collapsing the visible result onto whichever
  33. // single leaf happened to dominate the flood of duplicate writes). Writes past the sized capacity
  34. // are still dropped (counted in _Counters[3]) as a last-resort safety net against genuinely
  35. // corrupted data (e.g. a duplicate group-child reference), not the primary defense against blowup.
  36. //
  37. // Dedup mechanism: _ClaimedGroups is a per-group flag, reset to 0 once per frame (per object) by
  38. // ClusterLODManager.RenderGpu before the traversal starts. The first node to fail its test and
  39. // need to expand a given group calls InterlockedCompareExchange to atomically flip that group's
  40. // flag from 0 to 1 - only the thread that performed the actual 0->1 transition proceeds to push
  41. // the group's children; every other node that independently also wanted to expand the same group
  42. // (its siblings) sees the flag already set and does nothing further, exactly like SelectClusters'
  43. // "if (!expandedGroups.Add(...)) continue;".
  44. //
  45. // M3e: parity with SelectClusters' defensive checks (out-of-range meshlet index, out-of-range
  46. // group index) - a corrupted or stale bake (see SelectClusters' own comments for how this can
  47. // happen, e.g. a schema change leaving a new field zero-initialized instead of -1) could otherwise
  48. // cause TraverseLevel to read past the end of a buffer. The CPU aborts the whole traversal the
  49. // instant it sees this, which isn't practical for thousands of parallel GPU threads - instead,
  50. // each thread validates its own indices, silently drops just that one node if they're out of
  51. // range, and counts it in _Counters[4] so ClusterLODManager can warn once per asset (see
  52. // RenderGpu's periodic, throttled health check - deliberately not a readback every frame).
  53.  
  54. #pragma kernel TraverseLevel
  55. #pragma kernel Advance
  56. #pragma kernel FinalizeArgs
  57.  
  58. // Do NOT declare _Args as RWStructuredBuffer<IndirectDrawArgs> (a custom 4-uint struct). This is a
  59. // documented Unity/driver bug: on some AMD GPUs under DirectX, a RWStructuredBuffer<T> for a
  60. // custom struct silently uses a stride of 4 bytes instead of the struct's real size, so writes
  61. // for element N land only 4 bytes after element N-1 instead of sizeof(T) bytes after - every
  62. // element after the first overlaps/smears into its neighbors. That matches exactly what was seen
  63. // dumping this buffer from C# (instanceCount/startInstance reading back as unrelated garbage
  64. // values instead of the 1/0 this kernel always writes). Nvidia isn't affected, which is why this
  65. // slipped through - see
  66. // https://discussions.unity.com/t/do-not-use-unitys-indirectdrawindexedargs-in-a-structuredbuffer/1623092
  67. // The documented fix: only ever declare indirect-args buffers as (RW)StructuredBuffer<uint> (or
  68. // (RW)Buffer<uint>) in a compute shader, and index by hand in units of uint.
  69. static const uint IndirectDrawArgsUintCount = 4; // vertexCountPerInstance, instanceCount, startVertex, startInstance
  70.  
  71. // Counter layout (single small buffer, shared across all three kernels):
  72. //   [0] currentFrontierCount - how many entries in _CurrentFrontier are valid this pass.
  73. //   [1] nextFrontierCount    - accumulated via InterlockedAdd as this pass's failed nodes expand.
  74. //   [2] selectedCount        - cumulative across every pass so far (never reset mid-traversal).
  75. //   [3] overflowCount        - how many appends were dropped because a buffer was full (see above).
  76. //   [4] corruptionCount      - how many nodes were dropped for referencing an out-of-range meshlet
  77. //                              or group index (M3e - see file header comment).
  78. RWStructuredBuffer<uint> _Counters;
  79.  
  80. // This pass's input (read-only) and output (accumulated via InterlockedAdd) node-index lists. For
  81. // pass 0, _CurrentFrontier is bound directly to the data asset's root indices buffer; from pass 1
  82. // onward it's one of ClusterLODManager's two per-object ping-pong buffers, alternating with
  83. // _NextFrontier every pass.
  84. StructuredBuffer<uint> _CurrentFrontier;
  85. RWStructuredBuffer<uint> _NextFrontier;
  86. int _NextFrontierCapacity;
  87.  
  88. // Every node that passes its test lands here (cumulative across all passes), consumed by
  89. // FinalizeArgs at the end of the traversal.
  90. RWStructuredBuffer<uint> _Selected;
  91. int _SelectedCapacity;
  92.  
  93. // Per meshlet (indexed by any meshlet index, root or child): xyz = object-space bounding sphere
  94. // center, w = error. Per meshlet: the group (if any) whose children refine it further, or -1 if
  95. // this meshlet is a true leaf - matches Meshlet.boundingSphere/error/parentGroupIndex on the CPU.
  96. StructuredBuffer<float4> _MeshletBoundingSphereError;
  97. StructuredBuffer<int> _MeshletParentGroup;
  98. int _MeshletsCount;
  99.  
  100. // Per group: (childMeshletOffset, childMeshletCount) into _GroupChildMeshletIndices.
  101. StructuredBuffer<uint2> _GroupChildOffsetCount;
  102. StructuredBuffer<uint> _GroupChildMeshletIndices;
  103. int _GroupsCount;
  104.  
  105. // Per-group claim flag (0 = not yet expanded this traversal, 1 = claimed) - see file header
  106. // comment. Reset to all-zero once per frame by ClusterLODManager.RenderGpu before pass 0.
  107. RWStructuredBuffer<uint> _ClaimedGroups;
  108.  
  109. // Per-frame view state - same inputs the CPU's IsDetailSufficient/SelectClusters use.
  110. float3 _ObjectWorldPosition;
  111. float3 _CameraWorldPosition;
  112. float _ErrorThreshold;
  113.  
  114. [numthreads(64, 1, 1)]
  115. void TraverseLevel(uint3 id : SV_DispatchThreadID)
  116. {
  117.     uint currentCount = _Counters[0];
  118.     if (id.x >= currentCount)
  119.     {
  120.         return;
  121.     }
  122.  
  123.     uint meshletIndex = _CurrentFrontier[id.x];
  124.  
  125.     // M3e: mirrors SelectClusters' out-of-range meshlet index check - a corrupted/stale bake could
  126.     // otherwise send this thread reading past the end of every per-meshlet buffer below. Drop just
  127.     // this one node (not the whole dispatch, unlike the CPU) and count it for ClusterLODManager's
  128.     // periodic health-check warning.
  129.     if (meshletIndex >= (uint)_MeshletsCount)
  130.     {
  131.         uint corruptionDummy;
  132.         InterlockedAdd(_Counters[4], 1u, corruptionDummy);
  133.         return;
  134.     }
  135.  
  136.     float4 sphereError = _MeshletBoundingSphereError[meshletIndex];
  137.     int parentGroupIndex = _MeshletParentGroup[meshletIndex];
  138.  
  139.     float d = max(0.0, distance(_CameraWorldPosition, _ObjectWorldPosition + sphereError.xyz));
  140.     float e = d * _ErrorThreshold;
  141.     bool sufficient = (parentGroupIndex < 0) || (sphereError.w <= e);
  142.  
  143.     if (sufficient)
  144.     {
  145.         uint outIndex;
  146.         InterlockedAdd(_Counters[2], 1u, outIndex);
  147.  
  148.         if (outIndex < (uint)_SelectedCapacity)
  149.         {
  150.             _Selected[outIndex] = meshletIndex;
  151.         }
  152.         else
  153.         {
  154.             uint dummy;
  155.             InterlockedAdd(_Counters[3], 1u, dummy);
  156.         }
  157.     }
  158.     else if (parentGroupIndex >= (int)_GroupsCount)
  159.     {
  160.         // M3e: mirrors SelectClusters' out-of-range group index check (parentGroupIndex >=
  161.         // Groups.Length) - same reasoning as the meshlet-index check above.
  162.         uint corruptionDummy;
  163.         InterlockedAdd(_Counters[4], 1u, corruptionDummy);
  164.     }
  165.     else
  166.     {
  167.         // Claim this group before expanding it: only the thread that actually flips the flag from
  168.         // 0 to 1 proceeds. Any other node (a sibling meshlet sharing the same parentGroupIndex)
  169.         // that also fails its test this pass sees previousClaim == 1 and does nothing further -
  170.         // this is what stops a shared group from being expanded once per sibling, which otherwise
  171.         // compounds exponentially pass over pass (see file header comment).
  172.         uint previousClaim;
  173.         InterlockedCompareExchange(_ClaimedGroups[(uint)parentGroupIndex], 0u, 1u, previousClaim);
  174.  
  175.         if (previousClaim == 0u)
  176.         {
  177.             uint2 range = _GroupChildOffsetCount[(uint)parentGroupIndex];
  178.             uint childOffset = range.x;
  179.             uint childCount = range.y;
  180.  
  181.             for (uint c = 0; c < childCount; c++)
  182.             {
  183.                 uint childIndex = _GroupChildMeshletIndices[childOffset + c];
  184.  
  185.                 uint outIndex;
  186.                 InterlockedAdd(_Counters[1], 1u, outIndex);
  187.  
  188.                 if (outIndex < (uint)_NextFrontierCapacity)
  189.                 {
  190.                     _NextFrontier[outIndex] = childIndex;
  191.                 }
  192.                 else
  193.                 {
  194.                     uint dummy;
  195.                     InterlockedAdd(_Counters[3], 1u, dummy);
  196.                 }
  197.             }
  198.         }
  199.     }
  200. }
  201.  
  202. // Runs as a single thread between passes: promotes next->current, resets next to zero, and writes
  203. // GPU-computed thread-group counts for the *following* TraverseLevel dispatch (issued via
  204. // ComputeShader.DispatchIndirect from ClusterLODManager.RenderGpu) so no CPU readback is needed to
  205. // know how many nodes the next pass has to process.
  206. RWStructuredBuffer<uint> _DispatchArgs; // [0]=groupsX, [1]=groupsY, [2]=groupsZ
  207.  
  208. [numthreads(1, 1, 1)]
  209. void Advance(uint3 id : SV_DispatchThreadID)
  210. {
  211.     uint nextCount = _Counters[1];
  212.     _Counters[0] = nextCount;
  213.     _Counters[1] = 0u;
  214.  
  215.     _DispatchArgs[0] = (nextCount + 63u) / 64u;
  216.     _DispatchArgs[1] = 1u;
  217.     _DispatchArgs[2] = 1u;
  218. }
  219.  
  220. // Runs once after every pass is done, with a fixed CPU-known dispatch size (data.MeshletsCount) -
  221. // unlike the traversal passes, this never needs to be camera/data-dependent, since it's just
  222. // converting whatever ended up in _Selected (up to _Counters[2] entries) into IndirectDrawArgs, one
  223. // thread per potential output slot. Slots beyond the real selected count are padded with a
  224. // zero-vertex-count no-op draw, exactly like M3c did.
  225. RWStructuredBuffer<uint> _Args;
  226. StructuredBuffer<uint> _MeshletIndexCounts;
  227. int _ClusterMaxPrimitives;
  228. int _SlotCount;
  229.  
  230. [numthreads(64, 1, 1)]
  231. void FinalizeArgs(uint3 id : SV_DispatchThreadID)
  232. {
  233.     if ((int)id.x >= _SlotCount)
  234.     {
  235.         return;
  236.     }
  237.  
  238.     uint selectedCount = _Counters[2];
  239.     uint baseIndex = id.x * IndirectDrawArgsUintCount;
  240.  
  241.     if (id.x < selectedCount && id.x < (uint)_SelectedCapacity)
  242.     {
  243.         uint meshletIndex = _Selected[id.x];
  244.         uint vertexCountPerInstance = _MeshletIndexCounts[meshletIndex];
  245.         uint startVertex = meshletIndex * (uint)_ClusterMaxPrimitives * 3u;
  246.  
  247.         _Args[baseIndex + 0] = vertexCountPerInstance;
  248.         _Args[baseIndex + 1] = 1u;
  249.         _Args[baseIndex + 2] = startVertex;
  250.         _Args[baseIndex + 3] = 0u;
  251.     }
  252.     else
  253.     {
  254.         _Args[baseIndex + 0] = 0u;
  255.         _Args[baseIndex + 1] = 0u;
  256.         _Args[baseIndex + 2] = 0u;
  257.         _Args[baseIndex + 3] = 0u;
  258.     }
  259. }
  260.  
Add Comment
Please, Sign In to add comment